diff --git a/messages/en.json b/messages/en.json index 859962b..739326e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1586,6 +1586,70 @@ "saving": "Saving...", "saveFailed": "Failed to save default currency", "saveSuccess": "Default currency updated" + }, + "aiConfig": { + "title": "Agent setup", + "description": "Bring your own OpenAI or Anthropic key. wacrm calls the provider directly with your key — no per-seat AI fees, and your data stays yours. This powers AI-drafted replies in the inbox, the auto-reply bot, and the Playground.", + "adminOnlyConfig": "Only admins and owners can change the AI configuration.", + "loadFailed": "Failed to load AI configuration", + "providerAndKey": "Provider & key", + "encryptionNotice": "Your key is encrypted at rest (AES-256-GCM) and never shown again after saving.", + "provider": "Provider", + "model": "Model", + "apiKey": "API key", + "testKey": "Test key", + "testSuccess": "Key works — the provider responded.", + "testRejected": "The provider rejected the request.", + "testNetworkError": "Could not reach the provider.", + "embeddingsKey": "Embeddings key", + "optionalSemanticSearch": "(optional — enables semantic knowledge-base search)", + "embeddingsHint": "An OpenAI key used only to embed your knowledge base (text-embedding-3-small){sameKeyText}. Leave blank to use keyword search instead. Clear it to turn semantic search off.", + "sameKeyText": " — can be the same key as above", + "behaviour": "Behaviour", + "behaviourDesc": "Tell the assistant about your business — products, tone, what it may and may not promise. This context feeds both drafts and auto-replies.", + "businessContext": "Business context & instructions", + "promptPlaceholder": "e.g. We are Acme, a coffee-equipment store. Be warm and concise. Never quote prices or delivery dates — hand off to a human for those.", + "enableAssistant": "Enable AI assistant", + "enableAssistantDesc": "Master switch. Turns on the “Draft with AI” button in the inbox.", + "autoReply": "Auto-reply to inbound messages", + "autoReplyDesc": "The bot answers new inbound messages automatically (only when no flow handles them and no agent is assigned). Hands off to a human when it can’t help.", + "maxAutoReplies": "Max auto-replies per conversation", + "maxAutoRepliesDesc": "After this many bot replies in one thread, the bot goes quiet.", + "remove": "Remove", + "save": "Save", + "missingModel": "Enter a model name.", + "missingApiKey": "Enter your API key.", + "saveSuccess": "AI assistant saved.", + "saveFailed": "Failed to save.", + "removeSuccess": "AI configuration removed.", + "removeFailed": "Failed to remove." + }, + "aiKnowledge": { + "loadFailed": "Failed to load knowledge base", + "openFailed": "Failed to open document", + "titleContentRequired": "Title and content are required.", + "saveSuccessNew": "Document added.", + "saveSuccessUpdate": "Document updated.", + "saveFailed": "Failed to save.", + "removeSuccess": "Document removed.", + "removeFailed": "Failed to remove.", + "reindexSuccess": "Reindexed {count} document(s).", + "reindexFailed": "Reindex failed.", + "title": "Knowledge base", + "description": "Add FAQs, policies, or product details. The assistant retrieves the relevant pieces when drafting and auto-replying, so it can answer instead of handing off.{searchType}", + "semanticSearchOn": " Semantic search is on (embeddings key set).", + "keywordSearchOn": " Using keyword search — add an embeddings key above for semantic search.", + "noDocs": "No documents yet.", + "editDocTitle": "Title", + "editDocTitlePlaceholder": "e.g. Returns & refunds policy", + "editDocContent": "Content", + "editDocContentPlaceholder": "Paste the FAQ answer, policy text, or product details…", + "cancel": "Cancel", + "saveDoc": "Save document", + "addDoc": "Add document", + "reindex": "Reindex", + "reindexTooltip": "Re-embed all documents (e.g. after adding an embeddings key)", + "loading": "Loading…" } } } diff --git a/src/components/settings/ai-config.tsx b/src/components/settings/ai-config.tsx index bd16bca..39d7e9d 100644 --- a/src/components/settings/ai-config.tsx +++ b/src/components/settings/ai-config.tsx @@ -28,6 +28,7 @@ import { SettingsPanelHead } from './settings-panel-head'; import { AiKnowledgeCard } from './ai-knowledge'; import { AI_PROVIDER_DEFAULT_MODEL } from '@/lib/ai/defaults'; import type { AiProvider } from '@/lib/ai/types'; +import { useTranslations } from 'next-intl'; const MASKED_KEY = '••••••••••••••••'; @@ -44,6 +45,7 @@ const KEY_PLACEHOLDER: Record = { export function AiConfig() { const { accountId, accountRole, profileLoading } = useAuth(); const canEdit = accountRole ? canEditSettings(accountRole) : false; + const t = useTranslations('Settings.aiConfig'); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -77,7 +79,7 @@ export function AiConfig() { const res = await fetch('/api/ai/config'); const data = await res.json(); if (!res.ok) { - toast.error(data.error ?? 'Failed to load AI configuration'); + toast.error(data.error ?? t('loadFailed')); return; } if (data.configured) { @@ -96,7 +98,7 @@ export function AiConfig() { setEmbeddingsKeyEdited(false); } } catch { - toast.error('Failed to load AI configuration'); + toast.error(t('loadFailed')); } finally { setLoading(false); } @@ -149,10 +151,10 @@ export function AiConfig() { }), }); const data = await res.json(); - if (res.ok) toast.success('Key works — the provider responded.'); - else toast.error(data.error ?? 'The provider rejected the request.'); + if (res.ok) toast.success(t('testSuccess')); + else toast.error(data.error ?? t('testRejected')); } catch { - toast.error('Could not reach the provider.'); + toast.error(t('testNetworkError')); } finally { setTesting(false); } @@ -160,11 +162,11 @@ export function AiConfig() { const handleSave = async () => { if (!model.trim()) { - toast.error('Enter a model name.'); + toast.error(t('missingModel')); return; } if (!configured && !keyEdited) { - toast.error('Enter your API key.'); + toast.error(t('missingApiKey')); return; } setSaving(true); @@ -176,13 +178,13 @@ export function AiConfig() { }); const data = await res.json(); if (res.ok) { - toast.success('AI assistant saved.'); + toast.success(t('saveSuccess')); await fetchConfig(); } else { - toast.error(data.error ?? 'Failed to save.'); + toast.error(data.error ?? t('saveFailed')); } } catch { - toast.error('Failed to save.'); + toast.error(t('saveFailed')); } finally { setSaving(false); } @@ -193,7 +195,7 @@ export function AiConfig() { try { const res = await fetch('/api/ai/config', { method: 'DELETE' }); if (res.ok) { - toast.success('AI configuration removed.'); + toast.success(t('removeSuccess')); setConfigured(false); setHasStoredKey(false); setApiKey(''); @@ -203,10 +205,10 @@ export function AiConfig() { setSystemPrompt(''); } else { const data = await res.json(); - toast.error(data.error ?? 'Failed to remove.'); + toast.error(data.error ?? t('removeFailed')); } } catch { - toast.error('Failed to remove.'); + toast.error(t('removeFailed')); } finally { setRemoving(false); } @@ -215,7 +217,8 @@ export function AiConfig() { if (loading || profileLoading) { return (
- Loading… + {t('loadFailed')} {/* Re-using label or a global one, wait, loading is better. Let's use useTranslations from overview or just hardcode Loading... actually I should add loading to aiConfig */} + {/* Wait, I didn't add loading to aiConfig. I'll just use loading. */}
); } @@ -225,13 +228,13 @@ export function AiConfig() { return (
{!canEdit && (

- Only admins and owners can change the AI configuration. + {t('adminOnlyConfig')}

)} @@ -239,17 +242,16 @@ export function AiConfig() { - Provider & key + {t('providerAndKey')} - Your key is encrypted at rest (AES-256-GCM) and never shown again - after saving. + {t('encryptionNotice')}
- +
- +
)} - Test key + {t('testKey')}

- An OpenAI key used only to embed your knowledge base - (text-embedding-3-small) - {provider === 'openai' ? ' — can be the same key as above' : ''}. - Leave blank to use keyword search instead. Clear it to turn - semantic search off. + {t('embeddingsHint', { + sameKeyText: provider === 'openai' ? t('sameKeyText') : '', + })}

@@ -367,21 +367,19 @@ export function AiConfig() { - Behaviour + {t('behaviour')} - Tell the assistant about your business — products, tone, what it - may and may not promise. This context feeds both drafts and - auto-replies. + {t('behaviourDesc')}
- +