diff --git a/app/api/listings/translate/route.ts b/app/api/listings/translate/route.ts index fc8c503..f85ee4c 100644 --- a/app/api/listings/translate/route.ts +++ b/app/api/listings/translate/route.ts @@ -8,10 +8,17 @@ type LocaleFields = { title: string; teaser: string; description: string }; const SUPPORTED_LOCALES: Locale[] = ['en', 'fi', 'sv']; function loadApiKey() { - if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; - const keyPath = path.join(process.cwd(), 'creds', 'openai.key'); + if (process.env.OPENAI_TRANSLATIONS_KEY) return process.env.OPENAI_TRANSLATIONS_KEY; + const newKeyPath = path.join(process.cwd(), 'creds', 'openai-translations.key'); try { - return fs.readFileSync(keyPath, 'utf8').trim(); + return fs.readFileSync(newKeyPath, 'utf8').trim(); + } catch { + // ignore + } + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const fallbackPath = path.join(process.cwd(), 'creds', 'openai.key'); + try { + return fs.readFileSync(fallbackPath, 'utf8').trim(); } catch { return null; } @@ -96,13 +103,13 @@ export async function POST(req: Request) { if (!res.ok) { const errText = await res.text(); - return NextResponse.json({ error: 'AI request failed', detail: errText }, { status: 500 }); + return NextResponse.json({ error: 'AI request failed', detail: errText }, { status: res.status || 500 }); } const data = await res.json(); content = data?.choices?.[0]?.message?.content ?? ''; - } catch (err) { - return NextResponse.json({ error: 'AI request failed' }, { status: 500 }); + } catch (err: any) { + return NextResponse.json({ error: 'AI request failed', detail: err?.message }, { status: 500 }); } const jsonText = content.match(/\{[\s\S]*\}/)?.[0] ?? content; diff --git a/app/listings/new/page.tsx b/app/listings/new/page.tsx index 469c510..9300e0b 100644 --- a/app/listings/new/page.tsx +++ b/app/listings/new/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useI18n } from '../../components/I18nProvider'; import type { Locale } from '../../../lib/i18n'; @@ -58,6 +58,8 @@ export default function NewListingPage() { const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [isAuthed, setIsAuthed] = useState(false); + const [aiResponse, setAiResponse] = useState(''); + const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'error'>('idle'); const [aiLoading, setAiLoading] = useState(false); const [slugStatus, setSlugStatus] = useState<'idle' | 'checking' | 'available' | 'taken' | 'error'>('idle'); @@ -142,6 +144,31 @@ export default function NewListingPage() { return 'partial'; } + const aiPrompt = useMemo(() => { + const payload = { + task: 'Translate this localization file for a holiday rental listing.', + instructions: [ + 'Preserve meaning, tone, numbers, and any markup.', + 'Return valid JSON only with the same keys.', + 'Fill missing translations; keep existing text unchanged.', + ], + sourceLocale: currentLocale, + targetLocales: SUPPORTED_LOCALES.filter((loc) => loc !== currentLocale), + locales: SUPPORTED_LOCALES.reduce( + (acc, loc) => ({ + ...acc, + [loc]: { + title: translations[loc].title, + teaser: translations[loc].teaser, + description: translations[loc].description, + }, + }), + {} as Record + ), + }; + return JSON.stringify(payload, null, 2); + }, [translations, currentLocale]); + async function autoTranslate() { if (aiLoading) return; setMessage(null); @@ -178,6 +205,45 @@ export default function NewListingPage() { } } + function applyAiResponse() { + setMessage(null); + setError(null); + try { + const parsed = JSON.parse(aiResponse); + const locales = parsed?.locales || parsed; + if (!locales || typeof locales !== 'object') { + throw new Error('Missing locales'); + } + const next = { ...translations }; + SUPPORTED_LOCALES.forEach((loc) => { + if (locales[loc]) { + const incoming = locales[loc]; + next[loc] = { + title: typeof incoming.title === 'string' && incoming.title ? incoming.title : next[loc].title, + teaser: typeof incoming.teaser === 'string' && incoming.teaser ? incoming.teaser : next[loc].teaser, + description: typeof incoming.description === 'string' && incoming.description ? incoming.description : next[loc].description, + }; + } + }); + setTranslations(next); + setMessage(t('aiApplySuccess')); + } catch (err) { + setError(t('aiApplyError')); + } + } + + async function copyAiPrompt() { + try { + if (!navigator?.clipboard) throw new Error('clipboard unavailable'); + await navigator.clipboard.writeText(aiPrompt); + setCopyStatus('copied'); + setTimeout(() => setCopyStatus('idle'), 1500); + } catch (err) { + setCopyStatus('error'); + setTimeout(() => setCopyStatus('idle'), 2000); + } + } + function parseImages(): ImageInput[] { return selectedImages.map((img) => ({ data: img.dataUrl, @@ -378,6 +444,45 @@ export default function NewListingPage() { {t('aiHelperNote')} +
+ {t('aiManualLead')} +
+
+ {t('aiPromptLabel')} +
+ {copyStatus === 'copied' ? ( + {t('aiPromptCopied')} + ) : null} + {copyStatus === 'error' ? {t('aiCopyError')} : null} + +
+
+