Vaibhav Kathait commited on
Commit
7f73287
·
1 Parent(s): 4cfabc8

Add en/it/fr/es/de language support; default to our own agent LLM instead of Anam's

Browse files
agent/test_blood_rag.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quick test: upload the sample blood test → search → LLM question.
3
+ Run from the agent/ directory: python test_blood_rag.py
4
+ """
5
+ import asyncio
6
+ from pathlib import Path
7
+ import blood_rag
8
+
9
+
10
+ SAMPLE = Path(__file__).parent / "data" / "blood_test_sample.txt"
11
+
12
+ QUERIES = [
13
+ "what is the patient's iron level and ferritin?",
14
+ "which vitamins are deficient?",
15
+ "what are the cholesterol findings?",
16
+ "is there any inflammation?",
17
+ ]
18
+
19
+
20
+ async def main():
21
+ # 1. Index the sample report
22
+ print("\n=== Indexing sample blood test report ===")
23
+ text = SAMPLE.read_text()
24
+ meta = await blood_rag.add_document("blood_test_sample.txt", text)
25
+ print(f"Indexed: {meta}")
26
+
27
+ # 2. Search each query and print results
28
+ for q in QUERIES:
29
+ print(f"\n--- Query: '{q}' ---")
30
+ result = await blood_rag.search(q, top_k=2)
31
+ print(result[:600])
32
+ print("…")
33
+
34
+ print("\n=== RAG test complete ===")
35
+ print(f"Indexed documents: {blood_rag.list_documents()}")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ asyncio.run(main())
anam/.env.example CHANGED
@@ -14,12 +14,16 @@ ANAM_AVATAR_ID=NA
14
  ANAM_VOICE_ID=NA
15
 
16
  # ── LLM ────────────────────────────────────────────────────────────────────────
17
- # LLM_PROVIDER=NA → use Anam's default built-in LLM (GPT OSS 120B)
 
 
 
 
 
18
  # LLM_PROVIDER=CustomLLM → use a custom LLM registered in your Anam Lab
19
  # (requires ANAM_LLM_ID set to the ID from Anam Lab)
20
- # LLM_PROVIDER=ClientSide → disable Anam's brain; use .talk() commands from frontend only
21
  #
22
- # Available built-in LLM IDs (set LLM_PROVIDER=CustomLLM + paste one below as ANAM_LLM_ID):
23
  # a7cf662c-2ace-4de1-a21e-ef0fbf144bb7 → GPT OSS 120B (recommended for new projects)
24
  # ANAM_LLAMA_v3_3_70B_V1 → Llama 3.3 70B (fast, great conversationalist)
25
  # b4f89001-9638-4879-a9c3-02cc9f9f2004 → GPT 4.1 (powerful, slower)
@@ -29,7 +33,7 @@ ANAM_VOICE_ID=NA
29
  # 2. Add your LLM endpoint, API key, and model spec
30
  # 3. Anam will validate it and give you a custom LLM ID
31
  # 4. Set LLM_PROVIDER=CustomLLM and ANAM_LLM_ID=<that-id>
32
- LLM_PROVIDER=NA
33
  # ANAM_LLM_ID=your-custom-llm-id-from-anam-lab
34
 
35
  # ── TTS ────────────────────────────────────────────────────────────────────────
 
14
  ANAM_VOICE_ID=NA
15
 
16
  # ── LLM ────────────────────────────────────────────────────────────────────────
17
+ # LLM_PROVIDER=ClientSide → (default) disable Anam's brain; every turn is routed
18
+ # to OUR OWN agent server (agent/main.py) instead of
19
+ # an Anam-hosted model. This is what we want normally —
20
+ # don't change it unless you specifically want Anam's
21
+ # managed LLM instead of ours.
22
+ # LLM_PROVIDER=NA → use Anam's default built-in LLM (GPT OSS 120B)
23
  # LLM_PROVIDER=CustomLLM → use a custom LLM registered in your Anam Lab
24
  # (requires ANAM_LLM_ID set to the ID from Anam Lab)
 
25
  #
26
+ # Available built-in LLM IDs (only relevant for LLM_PROVIDER=NA/CustomLLM):
27
  # a7cf662c-2ace-4de1-a21e-ef0fbf144bb7 → GPT OSS 120B (recommended for new projects)
28
  # ANAM_LLAMA_v3_3_70B_V1 → Llama 3.3 70B (fast, great conversationalist)
29
  # b4f89001-9638-4879-a9c3-02cc9f9f2004 → GPT 4.1 (powerful, slower)
 
33
  # 2. Add your LLM endpoint, API key, and model spec
34
  # 3. Anam will validate it and give you a custom LLM ID
35
  # 4. Set LLM_PROVIDER=CustomLLM and ANAM_LLM_ID=<that-id>
36
+ LLM_PROVIDER=ClientSide
37
  # ANAM_LLM_ID=your-custom-llm-id-from-anam-lab
38
 
39
  # ── TTS ────────────────────────────────────────────────────────────────────────
anam/config.py CHANGED
@@ -46,21 +46,52 @@ DEFAULT_VOICE_ID = DEFAULT_VOICE_ID or _FALLBACK_VOICE_ID
46
  # ISO 639-1 code override for Anam's STT pipeline (what the avatar understands
47
  # when listening). Set ANAM_LANGUAGE_CODE=NA to use Anam's default multilingual
48
  # auto-detection instead.
49
- _language_env = os.getenv("ANAM_LANGUAGE_CODE", "it")
50
  DEFAULT_LANGUAGE_CODE = None if _language_env.upper() == "NA" else _language_env
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  # ── LLM ────────────────────────────────────────────────────────────────────────
53
  # LLM_PROVIDER=NA → use Anam's default built-in LLM
54
  # LLM_PROVIDER=CustomLLM → use a custom LLM registered in Anam Lab
55
  # (set ANAM_LLM_ID to the ID returned by Anam Lab)
56
- # LLM_PROVIDER=ClientSide → disable Anam's brain entirely; use .talk() commands only
 
 
 
 
 
 
57
  #
58
- # Available built-in Anam LLM IDs:
59
  # a7cf662c-2ace-4de1-a21e-ef0fbf144bb7 → GPT OSS 120B (recommended)
60
  # ANAM_LLAMA_v3_3_70B_V1 → Llama 3.3 70B (fast, no tools/RAG)
61
  # b4f89001-9638-4879-a9c3-02cc9f9f2004 → GPT 4.1 (capable, slower)
62
  # CUSTOMER_CLIENT_V1 → Client-side LLM (use .talk() only)
63
- LLM_PROVIDER = os.getenv("LLM_PROVIDER", "NA")
64
  _llm_id_env = os.getenv("ANAM_LLM_ID", "NA")
65
 
66
  if LLM_PROVIDER.upper() == "CLIENTSIDE":
@@ -87,13 +118,18 @@ TTS_PROVIDER = os.getenv("TTS_PROVIDER", "NA")
87
  ENABLE_AUDIO_PASSTHROUGH: bool = TTS_PROVIDER.upper() != "NA"
88
 
89
  # ── RAG system prompt ──────────────────────────────────────────────────────────
 
 
 
90
  _rag_prompt_env = os.getenv("ANAM_RAG_SYSTEM_PROMPT", "NA")
91
- RAG_SYSTEM_PROMPT = (
92
  "You are a helpful, friendly AI assistant with access to a knowledge base of uploaded documents. "
93
  "When the user asks about something that may be in the documents, use the search_knowledge_base tool to look it up and answer based on what you find. "
94
  "For questions not covered in the documents, answer freely from your general knowledge. Never refuse to answer. "
95
  "VERY IMPORTANT: You are a speaking avatar. Always respond in plain natural spoken language only. "
96
  "Never use bullet points, numbered lists, asterisks, pipe characters, hyphens as list markers, markdown headers, or any other formatting symbols. "
97
- "Never write symbols that would sound strange when spoken aloud. Use only clear conversational sentences. "
98
- "IMPORTANT: Always respond in Italian (italiano), regardless of the language the question was asked in."
 
 
99
  ) if _rag_prompt_env.upper() == "NA" else _rag_prompt_env
 
46
  # ISO 639-1 code override for Anam's STT pipeline (what the avatar understands
47
  # when listening). Set ANAM_LANGUAGE_CODE=NA to use Anam's default multilingual
48
  # auto-detection instead.
49
+ _language_env = os.getenv("ANAM_LANGUAGE_CODE", "en")
50
  DEFAULT_LANGUAGE_CODE = None if _language_env.upper() == "NA" else _language_env
51
 
52
+ # The five languages the frontend's language switcher and this backend's
53
+ # system-prompt instructions are wired for. Adding a language means adding it
54
+ # here, to LANGUAGE_NAMES below, and to the frontend's i18n translations.
55
+ SUPPORTED_LANGUAGES = {"en", "it", "fr", "es", "de"}
56
+
57
+ LANGUAGE_NAMES = {
58
+ "en": "English",
59
+ "it": "Italian (italiano)",
60
+ "fr": "French (français)",
61
+ "es": "Spanish (español)",
62
+ "de": "German (Deutsch)",
63
+ }
64
+
65
+
66
+ def language_instruction(language_code: str | None) -> str:
67
+ """Build the '(always) respond in <language>' clause for a system prompt.
68
+
69
+ Returns an empty string for None/unrecognised codes so callers can always
70
+ do `base_prompt + language_instruction(code)` without a conditional.
71
+ """
72
+ name = LANGUAGE_NAMES.get((language_code or "").lower())
73
+ if not name:
74
+ return ""
75
+ return f" Always respond in {name}, regardless of the language the question was asked in."
76
+
77
  # ── LLM ────────────────────────────────────────────────────────────────────────
78
  # LLM_PROVIDER=NA → use Anam's default built-in LLM
79
  # LLM_PROVIDER=CustomLLM → use a custom LLM registered in Anam Lab
80
  # (set ANAM_LLM_ID to the ID returned by Anam Lab)
81
+ # LLM_PROVIDER=ClientSide → disable Anam's brain entirely; route every turn
82
+ # through OUR OWN agent server (agent/main.py,
83
+ # see AGENT_URL) instead of an Anam-hosted model.
84
+ #
85
+ # Default is ClientSide: we want our own agent's LLM (model/key/system-prompt we
86
+ # control) driving conversations, not Anam's managed models, unless a deployment
87
+ # explicitly opts back into Anam's built-in LLM via LLM_PROVIDER=NA/CustomLLM.
88
  #
89
+ # Available built-in Anam LLM IDs (only used when LLM_PROVIDER=NA or CustomLLM):
90
  # a7cf662c-2ace-4de1-a21e-ef0fbf144bb7 → GPT OSS 120B (recommended)
91
  # ANAM_LLAMA_v3_3_70B_V1 → Llama 3.3 70B (fast, no tools/RAG)
92
  # b4f89001-9638-4879-a9c3-02cc9f9f2004 → GPT 4.1 (capable, slower)
93
  # CUSTOMER_CLIENT_V1 → Client-side LLM (use .talk() only)
94
+ LLM_PROVIDER = os.getenv("LLM_PROVIDER", "ClientSide")
95
  _llm_id_env = os.getenv("ANAM_LLM_ID", "NA")
96
 
97
  if LLM_PROVIDER.upper() == "CLIENTSIDE":
 
118
  ENABLE_AUDIO_PASSTHROUGH: bool = TTS_PROVIDER.upper() != "NA"
119
 
120
  # ── RAG system prompt ──────────────────────────────────────────────────────────
121
+ # Language-neutral base — the "always respond in X" clause is appended from
122
+ # DEFAULT_LANGUAGE_CODE (or overridden per-request, see routers/sessions.py and
123
+ # rag.py's update_rag_persona) rather than hardcoded to one language.
124
  _rag_prompt_env = os.getenv("ANAM_RAG_SYSTEM_PROMPT", "NA")
125
+ _RAG_SYSTEM_PROMPT_BASE = (
126
  "You are a helpful, friendly AI assistant with access to a knowledge base of uploaded documents. "
127
  "When the user asks about something that may be in the documents, use the search_knowledge_base tool to look it up and answer based on what you find. "
128
  "For questions not covered in the documents, answer freely from your general knowledge. Never refuse to answer. "
129
  "VERY IMPORTANT: You are a speaking avatar. Always respond in plain natural spoken language only. "
130
  "Never use bullet points, numbered lists, asterisks, pipe characters, hyphens as list markers, markdown headers, or any other formatting symbols. "
131
+ "Never write symbols that would sound strange when spoken aloud. Use only clear conversational sentences."
132
+ )
133
+ RAG_SYSTEM_PROMPT = (
134
+ _RAG_SYSTEM_PROMPT_BASE + language_instruction(DEFAULT_LANGUAGE_CODE)
135
  ) if _rag_prompt_env.upper() == "NA" else _rag_prompt_env
anam/index.html CHANGED
@@ -10,8 +10,20 @@
10
  <body>
11
 
12
  <header>
13
- <h1>Anam AI Demo</h1>
14
- <p>Real-time conversation with an Anam AI persona — with document Q&amp;A</p>
 
 
 
 
 
 
 
 
 
 
 
 
15
  </header>
16
 
17
  <!-- Video -->
@@ -21,8 +33,8 @@
21
  <circle cx="12" cy="8" r="4"/>
22
  <path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
23
  </svg>
24
- <p>Avatar will appear here</p>
25
- <p id="placeholder-hint" style="font-size:11px;margin-top:2px;">Click <strong>Connect</strong> to start</p>
26
  <div id="placeholder-spinner"></div>
27
  <p id="placeholder-status"></p>
28
  </div>
@@ -39,56 +51,54 @@
39
  <!-- Status -->
40
  <div id="status-bar">
41
  <div id="status-dot"></div>
42
- <span id="status-text">Not connected</span>
43
  <span id="call-timer"></span>
44
  </div>
45
 
46
  <!-- Plan toggle (Free = 2 min cap) -->
47
  <div id="plan-section">
48
  <div id="plan-row">
49
- <span class="plan-label" id="plan-free-label">Free · 2 min limit</span>
50
  <label class="switch">
51
  <input type="checkbox" id="plan-switch" checked />
52
  <span class="knob"></span>
53
  </label>
54
- <span class="plan-label active" id="plan-pro-label">Pro · unlimited</span>
55
  </div>
56
  <div id="plan-banner">
57
- ⏱ <strong>Free plan:</strong> the avatar will tell you up front that you have
58
- <strong>2 minutes</strong> together, show a live countdown, and the session will
59
- disconnect automatically when time runs out.
60
  </div>
61
  </div>
62
 
63
  <!-- Controls -->
64
  <div class="controls">
65
- <select id="persona-select"><option value="">— Default ephemeral —</option></select>
66
- <button id="connect-btn">Connect</button>
67
- <button id="disconnect-btn" disabled>Disconnect</button>
68
  </div>
69
 
70
  <div id="preset-row">
71
- <button id="advanced-toggle">⚙ Advanced (system prompt)</button>
72
- <button id="clinical-preset-btn">🩺 Clinical Assistant preset</button>
73
  </div>
74
 
75
  <div id="advanced-panel">
76
- <label for="system-prompt-input">System prompt (used when no persona selected and RAG is off)</label>
77
- <textarea id="system-prompt-input" rows="3">You are a helpful, friendly AI assistant. Keep responses concise and conversational. Always respond in plain spoken language — never use bullet points, asterisks, pipe characters, markdown headers, or any formatting symbols. Always respond in Italian (italiano), regardless of the language the question was asked in.</textarea>
78
 
79
  <!-- ── LLM selector ───────────────────────────────────────────────────── -->
80
  <div style="margin-top:10px;display:flex;flex-direction:column;gap:4px">
81
  <label for="llm-select" style="font-size:11px;color:#666;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">LLM</label>
82
  <select id="llm-select" style="padding:8px 10px;border-radius:6px;border:1px solid #2a2a2a;background:#1a1a1a;color:#e0e0e0;font-size:13px">
 
 
 
 
83
  <optgroup label="Anam built-in">
84
- <option value="a7cf662c-2ace-4de1-a21e-ef0fbf144bb7">GPT OSS 120B — recommended default</option>
85
  <option value="b4f89001-9638-4879-a9c3-02cc9f9f2004">GPT 4.1 — most capable</option>
86
  <option value="ANAM_LLAMA_v3_3_70B_V1">Llama 3.3 70B — fastest (no tools/RAG)</option>
87
  </optgroup>
88
- <optgroup label="Custom">
89
- <option value="__agent__">Agent LLM (localhost:8005) — tools &amp; notepad</option>
90
- <option value="__custom__">Custom Anam LLM ID…</option>
91
- </optgroup>
92
  </select>
93
  </div>
94
 
@@ -128,8 +138,8 @@
128
 
129
  <!-- Mode tabs (shown after connect) -->
130
  <div class="mode-tabs" id="mode-tabs" style="display:none">
131
- <button class="mode-tab active" data-mode="voice">🎙 Voice</button>
132
- <button class="mode-tab" data-mode="text" >⌨️ Text</button>
133
  </div>
134
 
135
  <!-- Input area -->
@@ -144,36 +154,36 @@
144
  </button>
145
  <button id="interrupt-btn" disabled>⏹ Interrupt</button>
146
  </div>
147
- <span id="voice-hint">Microphone is active — speak to the avatar</span>
148
  </div>
149
  <div id="text-mode-row">
150
- <input id="message-input" type="text" placeholder="Type a message to the avatar…" disabled />
151
- <button id="send-btn" disabled>Send</button>
152
  </div>
153
  </div>
154
 
155
  <!-- Knowledge Base -->
156
  <div id="kb-section">
157
  <div class="kb-header">
158
- <h3>📚 Knowledge Base</h3>
159
  <div class="kb-header-right">
160
  <!-- RAG mode tabs -->
161
  <div class="rag-mode-tabs" id="rag-mode-tabs">
162
- <button class="rag-mode-tab active" data-mode="anam">☁ Anam Cloud</button>
163
- <button class="rag-mode-tab" data-mode="local">💻 Local</button>
164
  </div>
165
  </div>
166
  </div>
167
 
168
  <!-- ── Anam Cloud RAG panel ─────────────────────────────────────────────── -->
169
  <div id="anam-rag-panel" style="display:flex;flex-direction:column;gap:14px">
170
- <div class="rag-mode-note">Documents indexed in Anam's cloud — avatar searches per turn</div>
171
 
172
  <div class="kb-header" style="margin-top:4px">
173
  <div></div>
174
  <div class="kb-header-right">
175
  <span id="kb-setup-badge"></span>
176
- <button class="kb-reset-btn" id="kb-reset-btn" title="Clear RAG state (does not delete from Anam)">Reset</button>
177
  </div>
178
  </div>
179
 
@@ -183,35 +193,35 @@
183
  <polyline points="17 8 12 3 7 8"/>
184
  <line x1="12" y1="3" x2="12" y2="15"/>
185
  </svg>
186
- <p>Drop documents here</p>
187
- <span>PDF · TXT · MD · DOCX · CSV — max 50 MB</span>
188
- <button id="browse-btn">Browse files</button>
189
  </div>
190
  <input type="file" id="file-input" multiple accept=".pdf,.txt,.md,.docx,.csv" hidden />
191
 
192
  <div id="upload-status">
193
  <div class="spinner"></div>
194
- <span id="upload-status-text">Uploading…</span>
195
  </div>
196
 
197
  <div id="doc-list"></div>
198
 
199
  <label id="use-rag-row" for="use-rag-checkbox">
200
  <input type="checkbox" id="use-rag-checkbox" checked />
201
- <span id="use-rag-label">Use knowledge base — avatar will <strong>search your docs</strong> when answering</span>
202
  <span id="rag-doc-count"></span>
203
  </label>
204
 
205
  <div id="rag-prompt-section">
206
- <button id="rag-prompt-toggle">✏️ Edit RAG assistant behaviour (system prompt)</button>
207
  <div id="rag-prompt-body" style="display:none;flex-direction:column;gap:8px">
208
- <label for="rag-system-prompt">
209
  System prompt — controls how the avatar uses the knowledge base and answers general questions
210
  </label>
211
  <textarea id="rag-system-prompt" rows="6"></textarea>
212
  <div class="rag-prompt-actions">
213
- <button id="apply-rag-prompt-btn">Apply to persona</button>
214
- <span id="rag-prompt-hint">Changes apply to the next session you start.</span>
215
  </div>
216
  </div>
217
  </div>
@@ -219,8 +229,8 @@
219
 
220
  <!-- ── Local RAG panel ──────────────────────────────────────────────────── -->
221
  <div id="local-rag-panel" style="display:none;flex-direction:column;gap:14px">
222
- <div class="rag-mode-note">Documents chunked &amp; embedded locally — only the most relevant passages are retrieved per question.
223
- <br><span style="color:#555">Ideal for clinical guidelines, protocols &amp; reference PDFs. Embeddings persist across restarts.</span>
224
  </div>
225
 
226
  <div id="local-drop-zone">
@@ -229,9 +239,9 @@
229
  <polyline points="17 8 12 3 7 8"/>
230
  <line x1="12" y1="3" x2="12" y2="15"/>
231
  </svg>
232
- <p>Drop reference documents here</p>
233
- <span>PDF · DOCX · TXT · MD</span>
234
- <button id="local-browse-btn">Browse files</button>
235
  </div>
236
  <input type="file" id="local-file-input" multiple accept=".pdf,.docx,.txt,.md" hidden />
237
 
@@ -244,7 +254,7 @@
244
 
245
  <label id="use-local-rag-row" for="use-local-rag-checkbox" style="display:none">
246
  <input type="checkbox" id="use-local-rag-checkbox" checked />
247
- <span>Inject documents into avatar's context when connecting</span>
248
  <span id="local-doc-count"></span>
249
  </label>
250
  </div>
@@ -253,10 +263,10 @@
253
  <!-- Blood Test Report -->
254
  <div id="clinical-section">
255
  <div class="kb-header">
256
- <h3>🩺 Blood Test Report</h3>
257
  <div class="kb-header-right">
258
  <span id="report-status-badge"></span>
259
- <button class="kb-reset-btn" id="report-reset-btn" title="Clear uploaded report">Reset</button>
260
  </div>
261
  </div>
262
 
@@ -266,15 +276,15 @@
266
  <polyline points="17 8 12 3 7 8"/>
267
  <line x1="12" y1="3" x2="12" y2="15"/>
268
  </svg>
269
- <p>Drop your blood test report here</p>
270
- <span>Text-based PDF — max 50 MB</span>
271
- <button id="report-browse-btn">Browse files</button>
272
  </div>
273
  <input type="file" id="report-file-input" accept=".pdf" hidden />
274
 
275
  <div id="report-upload-status">
276
  <div class="spinner"></div>
277
- <span id="report-upload-status-text">Analysing report…</span>
278
  </div>
279
 
280
  <div id="report-summary"></div>
@@ -283,26 +293,26 @@
283
  <!-- Usage & cost telemetry -->
284
  <div id="telemetry-panel">
285
  <div class="tm-header">
286
- <h3>📊 Session Cost</h3>
287
- <button class="kb-reset-btn" id="telemetry-reset-btn">Reset</button>
288
  </div>
289
 
290
  <div id="telemetry-stats">
291
  <div class="tm-stat">
292
  <span class="tm-stat-value" id="tm-total-cost">$0.00</span>
293
- <span class="tm-stat-label">Estimated cost</span>
294
  </div>
295
  <div class="tm-stat">
296
  <span class="tm-stat-value" id="tm-total-minutes">0</span>
297
- <span class="tm-stat-label">Minutes billed</span>
298
  </div>
299
  <div class="tm-stat">
300
  <span class="tm-stat-value" id="tm-session-count">0</span>
301
- <span class="tm-stat-label">Sessions</span>
302
  </div>
303
  <div class="tm-stat live" id="tm-live-stat" style="display:none">
304
  <span class="tm-stat-value" id="tm-live-cost">$0.00</span>
305
- <span class="tm-stat-label">Live session</span>
306
  </div>
307
  </div>
308
 
 
10
  <body>
11
 
12
  <header>
13
+ <div class="header-row">
14
+ <h1>Anam AI Demo</h1>
15
+ <div id="lang-switcher">
16
+ <label for="lang-select" data-i18n="lang.label">Language</label>
17
+ <select id="lang-select">
18
+ <option value="en">English</option>
19
+ <option value="it">Italiano</option>
20
+ <option value="fr">Français</option>
21
+ <option value="es">Español</option>
22
+ <option value="de">Deutsch</option>
23
+ </select>
24
+ </div>
25
+ </div>
26
+ <p data-i18n="header.subtitle">Real-time conversation with an Anam AI persona — with document Q&amp;A</p>
27
  </header>
28
 
29
  <!-- Video -->
 
33
  <circle cx="12" cy="8" r="4"/>
34
  <path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
35
  </svg>
36
+ <p data-i18n="placeholder.text">Avatar will appear here</p>
37
+ <p id="placeholder-hint" style="font-size:11px;margin-top:2px;" data-i18n="placeholder.hint">Click Connect to start</p>
38
  <div id="placeholder-spinner"></div>
39
  <p id="placeholder-status"></p>
40
  </div>
 
51
  <!-- Status -->
52
  <div id="status-bar">
53
  <div id="status-dot"></div>
54
+ <span id="status-text" data-i18n="status.notConnected">Not connected</span>
55
  <span id="call-timer"></span>
56
  </div>
57
 
58
  <!-- Plan toggle (Free = 2 min cap) -->
59
  <div id="plan-section">
60
  <div id="plan-row">
61
+ <span class="plan-label" id="plan-free-label" data-i18n="plan.free">Free · 2 min limit</span>
62
  <label class="switch">
63
  <input type="checkbox" id="plan-switch" checked />
64
  <span class="knob"></span>
65
  </label>
66
+ <span class="plan-label active" id="plan-pro-label" data-i18n="plan.pro">Pro · unlimited</span>
67
  </div>
68
  <div id="plan-banner">
69
+ ⏱ <span data-i18n="plan.banner">Free plan: the avatar will tell you up front that you have 2 minutes together, show a live countdown, and the session will disconnect automatically when time runs out.</span>
 
 
70
  </div>
71
  </div>
72
 
73
  <!-- Controls -->
74
  <div class="controls">
75
+ <select id="persona-select"><option value="" data-i18n="persona.default">— Default ephemeral —</option></select>
76
+ <button id="connect-btn" data-i18n="controls.connect">Connect</button>
77
+ <button id="disconnect-btn" disabled data-i18n="controls.disconnect">Disconnect</button>
78
  </div>
79
 
80
  <div id="preset-row">
81
+ <button id="advanced-toggle" data-i18n="advanced.toggle">⚙ Advanced (system prompt)</button>
82
+ <button id="clinical-preset-btn" data-i18n="advanced.clinicalPreset">🩺 Clinical Assistant preset</button>
83
  </div>
84
 
85
  <div id="advanced-panel">
86
+ <label for="system-prompt-input" data-i18n="advanced.systemPromptLabel">System prompt (used when no persona selected and RAG is off)</label>
87
+ <textarea id="system-prompt-input" rows="3">You are a helpful, friendly AI assistant. Keep responses concise and conversational. Always respond in plain spoken language — never use bullet points, asterisks, pipe characters, markdown headers, or any formatting symbols.</textarea>
88
 
89
  <!-- ── LLM selector ───────────────────────────────────────────────────── -->
90
  <div style="margin-top:10px;display:flex;flex-direction:column;gap:4px">
91
  <label for="llm-select" style="font-size:11px;color:#666;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">LLM</label>
92
  <select id="llm-select" style="padding:8px 10px;border-radius:6px;border:1px solid #2a2a2a;background:#1a1a1a;color:#e0e0e0;font-size:13px">
93
+ <optgroup label="Ours (default)">
94
+ <option value="__agent__" selected>Our Agent LLM — tools &amp; notepad</option>
95
+ <option value="__custom__">Custom Anam LLM ID…</option>
96
+ </optgroup>
97
  <optgroup label="Anam built-in">
98
+ <option value="a7cf662c-2ace-4de1-a21e-ef0fbf144bb7">GPT OSS 120B</option>
99
  <option value="b4f89001-9638-4879-a9c3-02cc9f9f2004">GPT 4.1 — most capable</option>
100
  <option value="ANAM_LLAMA_v3_3_70B_V1">Llama 3.3 70B — fastest (no tools/RAG)</option>
101
  </optgroup>
 
 
 
 
102
  </select>
103
  </div>
104
 
 
138
 
139
  <!-- Mode tabs (shown after connect) -->
140
  <div class="mode-tabs" id="mode-tabs" style="display:none">
141
+ <button class="mode-tab active" data-mode="voice" data-i18n="mode.voice">🎙 Voice</button>
142
+ <button class="mode-tab" data-mode="text" data-i18n="mode.text">⌨️ Text</button>
143
  </div>
144
 
145
  <!-- Input area -->
 
154
  </button>
155
  <button id="interrupt-btn" disabled>⏹ Interrupt</button>
156
  </div>
157
+ <span id="voice-hint" data-i18n="voice.hint">Microphone is active — speak to the avatar</span>
158
  </div>
159
  <div id="text-mode-row">
160
+ <input id="message-input" type="text" placeholder="Type a message to the avatar…" disabled data-i18n-placeholder="text.placeholder" />
161
+ <button id="send-btn" disabled data-i18n="text.send">Send</button>
162
  </div>
163
  </div>
164
 
165
  <!-- Knowledge Base -->
166
  <div id="kb-section">
167
  <div class="kb-header">
168
+ <h3 data-i18n="kb.title">📚 Knowledge Base</h3>
169
  <div class="kb-header-right">
170
  <!-- RAG mode tabs -->
171
  <div class="rag-mode-tabs" id="rag-mode-tabs">
172
+ <button class="rag-mode-tab active" data-mode="anam" data-i18n="kb.cloudTab">☁ Anam Cloud</button>
173
+ <button class="rag-mode-tab" data-mode="local" data-i18n="kb.localTab">💻 Local</button>
174
  </div>
175
  </div>
176
  </div>
177
 
178
  <!-- ── Anam Cloud RAG panel ─────────────────────────────────────────────── -->
179
  <div id="anam-rag-panel" style="display:flex;flex-direction:column;gap:14px">
180
+ <div class="rag-mode-note" data-i18n="kb.cloudNote">Documents indexed in Anam's cloud — avatar searches per turn</div>
181
 
182
  <div class="kb-header" style="margin-top:4px">
183
  <div></div>
184
  <div class="kb-header-right">
185
  <span id="kb-setup-badge"></span>
186
+ <button class="kb-reset-btn" id="kb-reset-btn" title="Clear RAG state (does not delete from Anam)" data-i18n="kb.reset">Reset</button>
187
  </div>
188
  </div>
189
 
 
193
  <polyline points="17 8 12 3 7 8"/>
194
  <line x1="12" y1="3" x2="12" y2="15"/>
195
  </svg>
196
+ <p data-i18n="kb.dropHint">Drop documents here</p>
197
+ <span data-i18n="kb.dropSubtext">PDF · TXT · MD · DOCX · CSV — max 50 MB</span>
198
+ <button id="browse-btn" data-i18n="kb.browse">Browse files</button>
199
  </div>
200
  <input type="file" id="file-input" multiple accept=".pdf,.txt,.md,.docx,.csv" hidden />
201
 
202
  <div id="upload-status">
203
  <div class="spinner"></div>
204
+ <span id="upload-status-text" data-i18n="kb.uploading">Uploading…</span>
205
  </div>
206
 
207
  <div id="doc-list"></div>
208
 
209
  <label id="use-rag-row" for="use-rag-checkbox">
210
  <input type="checkbox" id="use-rag-checkbox" checked />
211
+ <span id="use-rag-label" data-i18n="kb.useRagLabel">Use knowledge base — avatar will search your docs when answering</span>
212
  <span id="rag-doc-count"></span>
213
  </label>
214
 
215
  <div id="rag-prompt-section">
216
+ <button id="rag-prompt-toggle" data-i18n="kb.editPrompt">✏️ Edit RAG assistant behaviour (system prompt)</button>
217
  <div id="rag-prompt-body" style="display:none;flex-direction:column;gap:8px">
218
+ <label for="rag-system-prompt" data-i18n="kb.promptLabel">
219
  System prompt — controls how the avatar uses the knowledge base and answers general questions
220
  </label>
221
  <textarea id="rag-system-prompt" rows="6"></textarea>
222
  <div class="rag-prompt-actions">
223
+ <button id="apply-rag-prompt-btn" data-i18n="kb.applyPrompt">Apply to persona</button>
224
+ <span id="rag-prompt-hint" data-i18n="kb.promptHint">Changes apply to the next session you start.</span>
225
  </div>
226
  </div>
227
  </div>
 
229
 
230
  <!-- ── Local RAG panel ──────────────────────────────────────────────────── -->
231
  <div id="local-rag-panel" style="display:none;flex-direction:column;gap:14px">
232
+ <div class="rag-mode-note"><span data-i18n="kb.localNote">Documents chunked &amp; embedded locally — only the most relevant passages are retrieved per question.</span>
233
+ <br><span style="color:#555" data-i18n="kb.localNoteSub">Ideal for clinical guidelines, protocols &amp; reference PDFs. Embeddings persist across restarts.</span>
234
  </div>
235
 
236
  <div id="local-drop-zone">
 
239
  <polyline points="17 8 12 3 7 8"/>
240
  <line x1="12" y1="3" x2="12" y2="15"/>
241
  </svg>
242
+ <p data-i18n="kb.localDropHint">Drop reference documents here</p>
243
+ <span data-i18n="kb.localDropSubtext">PDF · DOCX · TXT · MD</span>
244
+ <button id="local-browse-btn" data-i18n="kb.localBrowse">Browse files</button>
245
  </div>
246
  <input type="file" id="local-file-input" multiple accept=".pdf,.docx,.txt,.md" hidden />
247
 
 
254
 
255
  <label id="use-local-rag-row" for="use-local-rag-checkbox" style="display:none">
256
  <input type="checkbox" id="use-local-rag-checkbox" checked />
257
+ <span data-i18n="kb.localInjectLabel">Inject documents into avatar's context when connecting</span>
258
  <span id="local-doc-count"></span>
259
  </label>
260
  </div>
 
263
  <!-- Blood Test Report -->
264
  <div id="clinical-section">
265
  <div class="kb-header">
266
+ <h3 data-i18n="clinical.title">🩺 Blood Test Report</h3>
267
  <div class="kb-header-right">
268
  <span id="report-status-badge"></span>
269
+ <button class="kb-reset-btn" id="report-reset-btn" title="Clear uploaded report" data-i18n="kb.reset">Reset</button>
270
  </div>
271
  </div>
272
 
 
276
  <polyline points="17 8 12 3 7 8"/>
277
  <line x1="12" y1="3" x2="12" y2="15"/>
278
  </svg>
279
+ <p data-i18n="clinical.dropHint">Drop your blood test report here</p>
280
+ <span data-i18n="clinical.dropSubtext">Text-based PDF — max 50 MB</span>
281
+ <button id="report-browse-btn" data-i18n="clinical.browse">Browse files</button>
282
  </div>
283
  <input type="file" id="report-file-input" accept=".pdf" hidden />
284
 
285
  <div id="report-upload-status">
286
  <div class="spinner"></div>
287
+ <span id="report-upload-status-text" data-i18n="clinical.analysing">Analysing report…</span>
288
  </div>
289
 
290
  <div id="report-summary"></div>
 
293
  <!-- Usage & cost telemetry -->
294
  <div id="telemetry-panel">
295
  <div class="tm-header">
296
+ <h3 data-i18n="telemetry.title">📊 Session Cost</h3>
297
+ <button class="kb-reset-btn" id="telemetry-reset-btn" data-i18n="kb.reset">Reset</button>
298
  </div>
299
 
300
  <div id="telemetry-stats">
301
  <div class="tm-stat">
302
  <span class="tm-stat-value" id="tm-total-cost">$0.00</span>
303
+ <span class="tm-stat-label" data-i18n="telemetry.cost">Estimated cost</span>
304
  </div>
305
  <div class="tm-stat">
306
  <span class="tm-stat-value" id="tm-total-minutes">0</span>
307
+ <span class="tm-stat-label" data-i18n="telemetry.minutes">Minutes billed</span>
308
  </div>
309
  <div class="tm-stat">
310
  <span class="tm-stat-value" id="tm-session-count">0</span>
311
+ <span class="tm-stat-label" data-i18n="telemetry.sessions">Sessions</span>
312
  </div>
313
  <div class="tm-stat live" id="tm-live-stat" style="display:none">
314
  <span class="tm-stat-value" id="tm-live-cost">$0.00</span>
315
+ <span class="tm-stat-label" data-i18n="telemetry.live">Live session</span>
316
  </div>
317
  </div>
318
 
anam/rag.py CHANGED
@@ -1,12 +1,22 @@
1
  import json
2
  from pathlib import Path
 
3
 
4
  import httpx
5
  from fastapi import APIRouter, HTTPException, UploadFile, File
6
  from fastapi.responses import JSONResponse
7
  from pydantic import BaseModel
8
 
9
- from config import BASE, ANAM_API_KEY, DEFAULT_AVATAR_ID, DEFAULT_VOICE_ID, DEFAULT_LLM_ID, DEFAULT_LANGUAGE_CODE, RAG_SYSTEM_PROMPT
 
 
 
 
 
 
 
 
 
10
  from helpers import auth_headers, check
11
 
12
  # ── Persistent state ──────────────────────────────────────────────────────────
@@ -149,18 +159,32 @@ async def rag_delete_document(doc_id: str):
149
 
150
  class UpdatePersonaRequest(BaseModel):
151
  system_prompt: str
 
 
 
152
 
153
 
154
  @router.put("/persona")
155
  async def update_rag_persona(req: UpdatePersonaRequest):
156
- """Update the RAG persona's system prompt on the fly (no need to reconnect)."""
 
 
 
157
  if not rag_state.get("persona_id"):
158
  raise HTTPException(status_code=404, detail="RAG persona not set up yet — upload a document first.")
 
 
 
 
 
 
 
 
159
  async with httpx.AsyncClient(timeout=30) as c:
160
  r = await c.put(
161
  f"{BASE}/personas/{rag_state['persona_id']}",
162
  headers=auth_headers(),
163
- json={"systemPrompt": req.system_prompt},
164
  )
165
  check(r)
166
  return JSONResponse({"ok": True})
@@ -168,14 +192,17 @@ async def update_rag_persona(req: UpdatePersonaRequest):
168
 
169
  @router.get("/persona")
170
  async def get_rag_persona():
171
- """Return the current RAG persona's system prompt."""
172
  if not rag_state.get("persona_id"):
173
- return JSONResponse({"system_prompt": RAG_SYSTEM_PROMPT})
174
  async with httpx.AsyncClient(timeout=30) as c:
175
  r = await c.get(f"{BASE}/personas/{rag_state['persona_id']}", headers=auth_headers())
176
  check(r)
177
  data = r.json()
178
- return JSONResponse({"system_prompt": data.get("systemPrompt", RAG_SYSTEM_PROMPT)})
 
 
 
179
 
180
 
181
  @router.post("/reset")
 
1
  import json
2
  from pathlib import Path
3
+ from typing import Optional
4
 
5
  import httpx
6
  from fastapi import APIRouter, HTTPException, UploadFile, File
7
  from fastapi.responses import JSONResponse
8
  from pydantic import BaseModel
9
 
10
+ from config import (
11
+ BASE,
12
+ ANAM_API_KEY,
13
+ DEFAULT_AVATAR_ID,
14
+ DEFAULT_VOICE_ID,
15
+ DEFAULT_LLM_ID,
16
+ DEFAULT_LANGUAGE_CODE,
17
+ RAG_SYSTEM_PROMPT,
18
+ SUPPORTED_LANGUAGES,
19
+ )
20
  from helpers import auth_headers, check
21
 
22
  # ── Persistent state ──────────────────────────────────────────────────────────
 
159
 
160
  class UpdatePersonaRequest(BaseModel):
161
  system_prompt: str
162
+ # Optional — one of config.SUPPORTED_LANGUAGES. Omit to leave the
163
+ # persona's current language untouched.
164
+ language_code: Optional[str] = None
165
 
166
 
167
  @router.put("/persona")
168
  async def update_rag_persona(req: UpdatePersonaRequest):
169
+ """Update the RAG persona's system prompt (and optionally language) on the fly.
170
+
171
+ No need to reconnect — Anam applies persona edits to the next turn.
172
+ """
173
  if not rag_state.get("persona_id"):
174
  raise HTTPException(status_code=404, detail="RAG persona not set up yet — upload a document first.")
175
+ if req.language_code and req.language_code not in SUPPORTED_LANGUAGES:
176
+ raise HTTPException(
177
+ status_code=400,
178
+ detail=f"Unsupported language_code '{req.language_code}'. Supported: {sorted(SUPPORTED_LANGUAGES)}",
179
+ )
180
+ payload: dict = {"systemPrompt": req.system_prompt}
181
+ if req.language_code:
182
+ payload["languageCode"] = req.language_code
183
  async with httpx.AsyncClient(timeout=30) as c:
184
  r = await c.put(
185
  f"{BASE}/personas/{rag_state['persona_id']}",
186
  headers=auth_headers(),
187
+ json=payload,
188
  )
189
  check(r)
190
  return JSONResponse({"ok": True})
 
192
 
193
  @router.get("/persona")
194
  async def get_rag_persona():
195
+ """Return the current RAG persona's system prompt and language."""
196
  if not rag_state.get("persona_id"):
197
+ return JSONResponse({"system_prompt": RAG_SYSTEM_PROMPT, "language_code": DEFAULT_LANGUAGE_CODE})
198
  async with httpx.AsyncClient(timeout=30) as c:
199
  r = await c.get(f"{BASE}/personas/{rag_state['persona_id']}", headers=auth_headers())
200
  check(r)
201
  data = r.json()
202
+ return JSONResponse({
203
+ "system_prompt": data.get("systemPrompt", RAG_SYSTEM_PROMPT),
204
+ "language_code": data.get("languageCode", DEFAULT_LANGUAGE_CODE),
205
+ })
206
 
207
 
208
  @router.post("/reset")
anam/routers/sessions.py CHANGED
@@ -1,7 +1,7 @@
1
  import uuid
2
 
3
  import httpx
4
- from fastapi import APIRouter
5
  from fastapi.responses import JSONResponse
6
  from pydantic import BaseModel
7
 
@@ -13,7 +13,9 @@ from config import (
13
  DEFAULT_LLM_ID,
14
  DEFAULT_VOICE_ID,
15
  ENABLE_AUDIO_PASSTHROUGH,
 
16
  TTS_PROVIDER,
 
17
  )
18
  from helpers import auth_headers, check
19
  from rag import rag_state
@@ -31,6 +33,14 @@ async def create_session_token(req: SessionTokenRequest):
31
  system_prompt = req.system_prompt
32
 
33
  language_code = req.language_code or DEFAULT_LANGUAGE_CODE
 
 
 
 
 
 
 
 
34
 
35
  if req.use_custom_llm:
36
  persona_config = {
 
1
  import uuid
2
 
3
  import httpx
4
+ from fastapi import APIRouter, HTTPException
5
  from fastapi.responses import JSONResponse
6
  from pydantic import BaseModel
7
 
 
13
  DEFAULT_LLM_ID,
14
  DEFAULT_VOICE_ID,
15
  ENABLE_AUDIO_PASSTHROUGH,
16
+ SUPPORTED_LANGUAGES,
17
  TTS_PROVIDER,
18
+ language_instruction,
19
  )
20
  from helpers import auth_headers, check
21
  from rag import rag_state
 
33
  system_prompt = req.system_prompt
34
 
35
  language_code = req.language_code or DEFAULT_LANGUAGE_CODE
36
+ if language_code and language_code not in SUPPORTED_LANGUAGES:
37
+ raise HTTPException(
38
+ status_code=400,
39
+ detail=f"Unsupported language_code '{language_code}'. Supported: {sorted(SUPPORTED_LANGUAGES)}",
40
+ )
41
+ # Append the "respond in X" clause to whatever prompt the caller supplied
42
+ # (default, clinical preset, etc.) rather than baking one language into it.
43
+ system_prompt = system_prompt + language_instruction(language_code)
44
 
45
  if req.use_custom_llm:
46
  persona_config = {
anam/schemas.py CHANGED
@@ -9,13 +9,15 @@ class SessionTokenRequest(BaseModel):
9
  system_prompt: str = (
10
  "You are a helpful, friendly AI assistant. Keep responses concise and conversational. "
11
  "Always respond in plain spoken language. Never use bullet points, asterisks, pipe characters, "
12
- "markdown headers, or any formatting symbols — speak in natural sentences only. "
13
- "Always respond in Italian (italiano), regardless of the language the question was asked in."
14
  )
15
  avatar_id: Optional[str] = None
16
  voice_id: Optional[str] = None
17
  llm_id: Optional[str] = None
18
- language_code: Optional[str] = None # ISO 639-1 STT override, e.g. "it" — falls back to ANAM_LANGUAGE_CODE
 
 
 
19
  use_rag: bool = False
20
  local_rag_doc_ids: list[str] = [] # inject these local docs into system prompt
21
  use_custom_llm: bool = False # set llmId=CUSTOMER_CLIENT_V1 to bypass Anam's brain
 
9
  system_prompt: str = (
10
  "You are a helpful, friendly AI assistant. Keep responses concise and conversational. "
11
  "Always respond in plain spoken language. Never use bullet points, asterisks, pipe characters, "
12
+ "markdown headers, or any formatting symbols — speak in natural sentences only."
 
13
  )
14
  avatar_id: Optional[str] = None
15
  voice_id: Optional[str] = None
16
  llm_id: Optional[str] = None
17
+ # ISO 639-1 code, one of config.SUPPORTED_LANGUAGES (en/it/fr/es/de) — falls back
18
+ # to ANAM_LANGUAGE_CODE. Drives both Anam's STT and the "respond in X" instruction
19
+ # appended to system_prompt server-side (see routers/sessions.py).
20
+ language_code: Optional[str] = None
21
  use_rag: bool = False
22
  local_rag_doc_ids: list[str] = [] # inject these local docs into system prompt
23
  use_custom_llm: bool = False # set llmId=CUSTOMER_CLIENT_V1 to bypass Anam's brain
anam/static/css/style.css CHANGED
@@ -11,6 +11,20 @@ header { text-align: center; }
11
  header h1 { font-size: 26px; font-weight: 700; }
12
  header p { font-size: 13px; color: #666; margin-top: 4px; }
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  /* ── Video ── */
15
  #video-container {
16
  width: 640px; max-width: 100%; aspect-ratio: 16/9;
 
11
  header h1 { font-size: 26px; font-weight: 700; }
12
  header p { font-size: 13px; color: #666; margin-top: 4px; }
13
 
14
+ .header-row {
15
+ display: flex; align-items: center; justify-content: center;
16
+ gap: 16px; position: relative;
17
+ }
18
+ #lang-switcher {
19
+ position: absolute; right: 0; top: 50%; transform: translateY(-50%);
20
+ display: flex; align-items: center; gap: 6px;
21
+ }
22
+ #lang-switcher label { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
23
+ #lang-switcher select {
24
+ padding: 6px 8px; border-radius: 6px; border: 1px solid #2a2a2a;
25
+ background: #1a1a1a; color: #e0e0e0; font-size: 12px;
26
+ }
27
+
28
  /* ── Video ── */
29
  #video-container {
30
  width: 640px; max-width: 100%; aspect-ratio: 16/9;
anam/static/js/api.js CHANGED
@@ -23,11 +23,11 @@ export const getRagStatus = () => _request("/api/rag/status");
23
  export const getRagDocuments = () => _request("/api/rag/documents");
24
  export const deleteRagDocument = (id) => _request(`/api/rag/documents/${id}`, { method: "DELETE" });
25
  export const getRagPrompt = () => _request("/api/rag/persona");
26
- export const updateRagPrompt = (prompt) =>
27
  _request("/api/rag/persona", {
28
  method: "PUT",
29
  headers: { "Content-Type": "application/json" },
30
- body: JSON.stringify({ system_prompt: prompt }),
31
  });
32
  export const resetRag = () => _post("/api/rag/reset", {});
33
 
 
23
  export const getRagDocuments = () => _request("/api/rag/documents");
24
  export const deleteRagDocument = (id) => _request(`/api/rag/documents/${id}`, { method: "DELETE" });
25
  export const getRagPrompt = () => _request("/api/rag/persona");
26
+ export const updateRagPrompt = (prompt, languageCode) =>
27
  _request("/api/rag/persona", {
28
  method: "PUT",
29
  headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify({ system_prompt: prompt, language_code: languageCode || null }),
31
  });
32
  export const resetRag = () => _post("/api/rag/reset", {});
33
 
anam/static/js/app.js CHANGED
@@ -3,6 +3,7 @@ import * as kb from "./kb.js";
3
  import * as localRag from "./local_rag.js";
4
  import * as clinical from "./clinical.js";
5
  import * as telemetry from "./telemetry.js";
 
6
 
7
  const CLINICAL_SYSTEM_PROMPT =
8
  "You are a clinical assistant avatar for the OLMA longevity platform. " +
@@ -71,6 +72,7 @@ const tmStats = $("telemetry-stats");
71
  const tmSessions = $("telemetry-sessions");
72
  const tmRateNote = $("telemetry-rate-note");
73
  const tmResetBtn = $("telemetry-reset-btn");
 
74
 
75
  // ── Helpers ───────────────────────────────────────────────────────────────────
76
  function log(msg, type = "info") {
@@ -106,7 +108,7 @@ function resetUI() {
106
  modeTabs.style.display = "none";
107
  voiceRow.style.display = "none";
108
  textRow.style.display = "none";
109
- voiceHint.textContent = "Microphone is active — speak to the avatar";
110
  micMuted = false;
111
  anamClient = null;
112
  activeLocalDocIds = [];
@@ -233,6 +235,7 @@ function syncLlmRows() {
233
  if (cb) cb.checked = (v === "__agent__");
234
  }
235
  llmSelect?.addEventListener("change", syncLlmRows);
 
236
 
237
  // ── Mode tabs ─────────────────────────────────────────────────────────────────
238
  document.querySelectorAll(".mode-tab").forEach((tab) => {
@@ -339,6 +342,7 @@ connectBtn.addEventListener("click", async () => {
339
  voice_id: voiceId,
340
  use_custom_llm: useCustomLlm,
341
  plan: isFreePlan() ? "free" : "pro",
 
342
  });
343
  const sessionToken = tokData?.sessionToken;
344
  if (!sessionToken) throw new Error("No sessionToken in response: " + JSON.stringify(tokData));
@@ -490,13 +494,13 @@ micBtn.addEventListener("click", () => {
490
  anamClient.muteInputAudio();
491
  micBtn.classList.add("muted");
492
  micOverlay.classList.add("muted");
493
- voiceHint.textContent = "Microphone muted — click to unmute";
494
  log("Microphone muted", "info");
495
  } else {
496
  anamClient.unmuteInputAudio();
497
  micBtn.classList.remove("muted");
498
  micOverlay.classList.remove("muted");
499
- voiceHint.textContent = "Microphone active — speak to the avatar";
500
  log("Microphone unmuted", "success");
501
  }
502
  });
@@ -530,6 +534,22 @@ disconnBtn.addEventListener("click", () => {
530
  resetUI();
531
  });
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  // ── Init ──────────────────────────────────────────────────────────────────────
534
  tmResetBtn?.addEventListener("click", () => telemetry.reset(tmStats, tmSessions, tmRateNote));
535
  telemetry.refresh(tmStats, tmSessions, tmRateNote);
 
3
  import * as localRag from "./local_rag.js";
4
  import * as clinical from "./clinical.js";
5
  import * as telemetry from "./telemetry.js";
6
+ import * as i18n from "./i18n.js";
7
 
8
  const CLINICAL_SYSTEM_PROMPT =
9
  "You are a clinical assistant avatar for the OLMA longevity platform. " +
 
72
  const tmSessions = $("telemetry-sessions");
73
  const tmRateNote = $("telemetry-rate-note");
74
  const tmResetBtn = $("telemetry-reset-btn");
75
+ const langSelect = $("lang-select");
76
 
77
  // ── Helpers ───────────────────────────────────────────────────────────────────
78
  function log(msg, type = "info") {
 
108
  modeTabs.style.display = "none";
109
  voiceRow.style.display = "none";
110
  textRow.style.display = "none";
111
+ voiceHint.textContent = i18n.t("voice.hint");
112
  micMuted = false;
113
  anamClient = null;
114
  activeLocalDocIds = [];
 
235
  if (cb) cb.checked = (v === "__agent__");
236
  }
237
  llmSelect?.addEventListener("change", syncLlmRows);
238
+ syncLlmRows(); // reflect the "__agent__" default (our LLM) immediately, don't wait on getConfig()
239
 
240
  // ── Mode tabs ─────────────────────────────────────────────────────────────────
241
  document.querySelectorAll(".mode-tab").forEach((tab) => {
 
342
  voice_id: voiceId,
343
  use_custom_llm: useCustomLlm,
344
  plan: isFreePlan() ? "free" : "pro",
345
+ language_code: i18n.getLocale(),
346
  });
347
  const sessionToken = tokData?.sessionToken;
348
  if (!sessionToken) throw new Error("No sessionToken in response: " + JSON.stringify(tokData));
 
494
  anamClient.muteInputAudio();
495
  micBtn.classList.add("muted");
496
  micOverlay.classList.add("muted");
497
+ voiceHint.textContent = i18n.t("voice.hintMuted");
498
  log("Microphone muted", "info");
499
  } else {
500
  anamClient.unmuteInputAudio();
501
  micBtn.classList.remove("muted");
502
  micOverlay.classList.remove("muted");
503
+ voiceHint.textContent = i18n.t("voice.hint");
504
  log("Microphone unmuted", "success");
505
  }
506
  });
 
534
  resetUI();
535
  });
536
 
537
+ // ── Language switcher ─────────────────────────────────────────────────────────
538
+ // One control drives everything language-related: the visible UI text (via
539
+ // i18n.applyTranslations) and the language_code sent with the session token
540
+ // above, which the backend uses for both Anam's STT and the "respond in X"
541
+ // system-prompt instruction (see anam/config.py language_instruction).
542
+ if (langSelect) {
543
+ langSelect.value = i18n.getLocale();
544
+ langSelect.addEventListener("change", () => {
545
+ i18n.setLocale(langSelect.value);
546
+ i18n.applyTranslations();
547
+ voiceHint.textContent = i18n.t(micMuted ? "voice.hintMuted" : "voice.hint");
548
+ if (!anamClient) statusText.textContent = i18n.t("status.notConnected");
549
+ });
550
+ }
551
+ i18n.applyTranslations();
552
+
553
  // ── Init ──────────────────────────────────────────────────────────────────────
554
  tmResetBtn?.addEventListener("click", () => telemetry.reset(tmStats, tmSessions, tmRateNote));
555
  telemetry.refresh(tmStats, tmSessions, tmRateNote);
anam/static/js/i18n.js ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Static UI translations + language switching for the five languages the
2
+ // backend's language_code also understands (see anam/config.py SUPPORTED_LANGUAGES).
3
+ // Selecting a language here does two things: (1) re-renders every element
4
+ // carrying a data-i18n[-*] attribute, and (2) is read by app.js/kb.js as the
5
+ // language_code sent to the backend, so the avatar's spoken language and the
6
+ // page's visible text always switch together.
7
+
8
+ export const LOCALES = ["en", "it", "fr", "es", "de"];
9
+ const STORAGE_KEY = "anam_locale";
10
+
11
+ export const LOCALE_LABELS = {
12
+ en: "English",
13
+ it: "Italiano",
14
+ fr: "Français",
15
+ es: "Español",
16
+ de: "Deutsch",
17
+ };
18
+
19
+ const T = {
20
+ en: {
21
+ "lang.label": "Language",
22
+ "header.subtitle": "Real-time conversation with an Anam AI persona — with document Q&A",
23
+ "placeholder.text": "Avatar will appear here",
24
+ "placeholder.hint": "Click Connect to start",
25
+ "status.notConnected": "Not connected",
26
+ "plan.free": "Free · 2 min limit",
27
+ "plan.pro": "Pro · unlimited",
28
+ "plan.banner": "Free plan: the avatar will tell you up front that you have 2 minutes together, show a live countdown, and the session will disconnect automatically when time runs out.",
29
+ "persona.default": "— Default ephemeral —",
30
+ "controls.connect": "Connect",
31
+ "controls.disconnect": "Disconnect",
32
+ "advanced.toggle": "⚙ Advanced (system prompt)",
33
+ "advanced.clinicalPreset": "🩺 Clinical Assistant preset",
34
+ "advanced.systemPromptLabel": "System prompt (used when no persona selected and RAG is off)",
35
+ "mode.voice": "🎙 Voice",
36
+ "mode.text": "⌨️ Text",
37
+ "voice.hint": "Microphone is active — speak to the avatar",
38
+ "voice.hintMuted": "Microphone muted — click to unmute",
39
+ "text.placeholder": "Type a message to the avatar…",
40
+ "text.send": "Send",
41
+ "kb.title": "📚 Knowledge Base",
42
+ "kb.cloudTab": "☁ Anam Cloud",
43
+ "kb.localTab": "💻 Local",
44
+ "kb.cloudNote": "Documents indexed in Anam's cloud — avatar searches per turn",
45
+ "kb.reset": "Reset",
46
+ "kb.dropHint": "Drop documents here",
47
+ "kb.dropSubtext": "PDF · TXT · MD · DOCX · CSV — max 50 MB",
48
+ "kb.browse": "Browse files",
49
+ "kb.uploading": "Uploading…",
50
+ "kb.useRagLabel": "Use knowledge base — avatar will search your docs when answering",
51
+ "kb.editPrompt": "✏️ Edit RAG assistant behaviour (system prompt)",
52
+ "kb.promptLabel": "System prompt — controls how the avatar uses the knowledge base and answers general questions",
53
+ "kb.applyPrompt": "Apply to persona",
54
+ "kb.promptHint": "Changes apply to the next session you start.",
55
+ "kb.localNote": "Documents chunked & embedded locally — only the most relevant passages are retrieved per question.",
56
+ "kb.localNoteSub": "Ideal for clinical guidelines, protocols & reference PDFs. Embeddings persist across restarts.",
57
+ "kb.localDropHint": "Drop reference documents here",
58
+ "kb.localDropSubtext": "PDF · DOCX · TXT · MD",
59
+ "kb.localBrowse": "Browse files",
60
+ "kb.localInjectLabel": "Inject documents into avatar's context when connecting",
61
+ "clinical.title": "🩺 Blood Test Report",
62
+ "clinical.dropHint": "Drop your blood test report here",
63
+ "clinical.dropSubtext": "Text-based PDF — max 50 MB",
64
+ "clinical.browse": "Browse files",
65
+ "clinical.analysing": "Analysing report…",
66
+ "telemetry.title": "📊 Session Cost",
67
+ "telemetry.cost": "Estimated cost",
68
+ "telemetry.minutes": "Minutes billed",
69
+ "telemetry.sessions": "Sessions",
70
+ "telemetry.live": "Live session",
71
+ },
72
+ it: {
73
+ "lang.label": "Lingua",
74
+ "header.subtitle": "Conversazione in tempo reale con una persona AI di Anam — con domande sui documenti",
75
+ "placeholder.text": "L'avatar apparirà qui",
76
+ "placeholder.hint": "Clicca Connetti per iniziare",
77
+ "status.notConnected": "Non connesso",
78
+ "plan.free": "Gratis · limite 2 min",
79
+ "plan.pro": "Pro · illimitato",
80
+ "plan.banner": "Piano gratuito: l'avatar ti avviserà subito che avete 2 minuti insieme, mostrerà un conto alla rovescia dal vivo e la sessione si disconnetterà automaticamente allo scadere del tempo.",
81
+ "persona.default": "— Predefinito effimero —",
82
+ "controls.connect": "Connetti",
83
+ "controls.disconnect": "Disconnetti",
84
+ "advanced.toggle": "⚙ Avanzate (system prompt)",
85
+ "advanced.clinicalPreset": "🩺 Preimpostazione Assistente Clinico",
86
+ "advanced.systemPromptLabel": "System prompt (usato quando nessuna persona è selezionata e RAG è disattivato)",
87
+ "mode.voice": "🎙 Voce",
88
+ "mode.text": "⌨️ Testo",
89
+ "voice.hint": "Il microfono è attivo — parla con l'avatar",
90
+ "voice.hintMuted": "Microfono disattivato — clicca per riattivare",
91
+ "text.placeholder": "Scrivi un messaggio all'avatar…",
92
+ "text.send": "Invia",
93
+ "kb.title": "📚 Base di conoscenza",
94
+ "kb.cloudTab": "☁ Anam Cloud",
95
+ "kb.localTab": "💻 Locale",
96
+ "kb.cloudNote": "Documenti indicizzati nel cloud di Anam — l'avatar cerca ad ogni turno",
97
+ "kb.reset": "Reimposta",
98
+ "kb.dropHint": "Trascina qui i documenti",
99
+ "kb.dropSubtext": "PDF · TXT · MD · DOCX · CSV — max 50 MB",
100
+ "kb.browse": "Sfoglia file",
101
+ "kb.uploading": "Caricamento…",
102
+ "kb.useRagLabel": "Usa la base di conoscenza — l'avatar cercherà nei tuoi documenti quando risponde",
103
+ "kb.editPrompt": "✏️ Modifica comportamento assistente RAG (system prompt)",
104
+ "kb.promptLabel": "System prompt — controlla come l'avatar usa la base di conoscenza e risponde alle domande generali",
105
+ "kb.applyPrompt": "Applica alla persona",
106
+ "kb.promptHint": "Le modifiche si applicano alla prossima sessione che avvii.",
107
+ "kb.localNote": "Documenti suddivisi ed elaborati localmente — vengono recuperati solo i passaggi più rilevanti per ogni domanda.",
108
+ "kb.localNoteSub": "Ideale per linee guida cliniche, protocolli e PDF di riferimento. Gli embedding persistono tra i riavvii.",
109
+ "kb.localDropHint": "Trascina qui i documenti di riferimento",
110
+ "kb.localDropSubtext": "PDF · DOCX · TXT · MD",
111
+ "kb.localBrowse": "Sfoglia file",
112
+ "kb.localInjectLabel": "Inietta i documenti nel contesto dell'avatar alla connessione",
113
+ "clinical.title": "🩺 Referto esami del sangue",
114
+ "clinical.dropHint": "Trascina qui il tuo referto",
115
+ "clinical.dropSubtext": "PDF testuale — max 50 MB",
116
+ "clinical.browse": "Sfoglia file",
117
+ "clinical.analysing": "Analisi del referto…",
118
+ "telemetry.title": "📊 Costo sessione",
119
+ "telemetry.cost": "Costo stimato",
120
+ "telemetry.minutes": "Minuti fatturati",
121
+ "telemetry.sessions": "Sessioni",
122
+ "telemetry.live": "Sessione in corso",
123
+ },
124
+ fr: {
125
+ "lang.label": "Langue",
126
+ "header.subtitle": "Conversation en temps réel avec un persona IA Anam — avec questions sur vos documents",
127
+ "placeholder.text": "L'avatar apparaîtra ici",
128
+ "placeholder.hint": "Cliquez sur Connecter pour commencer",
129
+ "status.notConnected": "Non connecté",
130
+ "plan.free": "Gratuit · limite 2 min",
131
+ "plan.pro": "Pro · illimité",
132
+ "plan.banner": "Plan gratuit : l'avatar vous préviendra d'emblée que vous avez 2 minutes ensemble, affichera un compte à rebours en direct, et la session se déconnectera automatiquement à la fin du temps imparti.",
133
+ "persona.default": "— Par défaut éphémère —",
134
+ "controls.connect": "Connecter",
135
+ "controls.disconnect": "Déconnecter",
136
+ "advanced.toggle": "⚙ Avancé (system prompt)",
137
+ "advanced.clinicalPreset": "🩺 Préréglage Assistant clinique",
138
+ "advanced.systemPromptLabel": "System prompt (utilisé si aucun persona n'est sélectionné et RAG désactivé)",
139
+ "mode.voice": "🎙 Voix",
140
+ "mode.text": "⌨️ Texte",
141
+ "voice.hint": "Le microphone est actif — parlez à l'avatar",
142
+ "voice.hintMuted": "Microphone coupé — cliquez pour réactiver",
143
+ "text.placeholder": "Écrivez un message à l'avatar…",
144
+ "text.send": "Envoyer",
145
+ "kb.title": "📚 Base de connaissances",
146
+ "kb.cloudTab": "☁ Anam Cloud",
147
+ "kb.localTab": "💻 Local",
148
+ "kb.cloudNote": "Documents indexés dans le cloud d'Anam — l'avatar effectue une recherche à chaque tour",
149
+ "kb.reset": "Réinitialiser",
150
+ "kb.dropHint": "Déposez vos documents ici",
151
+ "kb.dropSubtext": "PDF · TXT · MD · DOCX · CSV — max 50 Mo",
152
+ "kb.browse": "Parcourir les fichiers",
153
+ "kb.uploading": "Téléversement…",
154
+ "kb.useRagLabel": "Utiliser la base de connaissances — l'avatar recherchera dans vos documents pour répondre",
155
+ "kb.editPrompt": "✏️ Modifier le comportement de l'assistant RAG (system prompt)",
156
+ "kb.promptLabel": "System prompt — contrôle comment l'avatar utilise la base de connaissances et répond aux questions générales",
157
+ "kb.applyPrompt": "Appliquer au persona",
158
+ "kb.promptHint": "Les modifications s'appliquent à la prochaine session que vous démarrez.",
159
+ "kb.localNote": "Documents découpés et vectorisés localement — seuls les passages les plus pertinents sont récupérés pour chaque question.",
160
+ "kb.localNoteSub": "Idéal pour les recommandations cliniques, protocoles et PDF de référence. Les embeddings persistent entre les redémarrages.",
161
+ "kb.localDropHint": "Déposez vos documents de référence ici",
162
+ "kb.localDropSubtext": "PDF · DOCX · TXT · MD",
163
+ "kb.localBrowse": "Parcourir les fichiers",
164
+ "kb.localInjectLabel": "Injecter les documents dans le contexte de l'avatar à la connexion",
165
+ "clinical.title": "🩺 Rapport d'analyse de sang",
166
+ "clinical.dropHint": "Déposez votre rapport d'analyse ici",
167
+ "clinical.dropSubtext": "PDF textuel — max 50 Mo",
168
+ "clinical.browse": "Parcourir les fichiers",
169
+ "clinical.analysing": "Analyse du rapport…",
170
+ "telemetry.title": "📊 Coût de la session",
171
+ "telemetry.cost": "Coût estimé",
172
+ "telemetry.minutes": "Minutes facturées",
173
+ "telemetry.sessions": "Sessions",
174
+ "telemetry.live": "Session en direct",
175
+ },
176
+ es: {
177
+ "lang.label": "Idioma",
178
+ "header.subtitle": "Conversación en tiempo real con una persona de IA de Anam — con preguntas sobre documentos",
179
+ "placeholder.text": "El avatar aparecerá aquí",
180
+ "placeholder.hint": "Haz clic en Conectar para empezar",
181
+ "status.notConnected": "No conectado",
182
+ "plan.free": "Gratis · límite 2 min",
183
+ "plan.pro": "Pro · ilimitado",
184
+ "plan.banner": "Plan gratuito: el avatar te avisará de antemano de que tenéis 2 minutos juntos, mostrará una cuenta atrás en vivo y la sesión se desconectará automáticamente cuando se acabe el tiempo.",
185
+ "persona.default": "— Predeterminado efímero —",
186
+ "controls.connect": "Conectar",
187
+ "controls.disconnect": "Desconectar",
188
+ "advanced.toggle": "⚙ Avanzado (system prompt)",
189
+ "advanced.clinicalPreset": "🩺 Preajuste Asistente clínico",
190
+ "advanced.systemPromptLabel": "System prompt (usado cuando no hay persona seleccionada y RAG está desactivado)",
191
+ "mode.voice": "🎙 Voz",
192
+ "mode.text": "⌨️ Texto",
193
+ "voice.hint": "El micrófono está activo — habla con el avatar",
194
+ "voice.hintMuted": "Micrófono silenciado — haz clic para reactivar",
195
+ "text.placeholder": "Escribe un mensaje al avatar…",
196
+ "text.send": "Enviar",
197
+ "kb.title": "📚 Base de conocimiento",
198
+ "kb.cloudTab": "☁ Anam Cloud",
199
+ "kb.localTab": "💻 Local",
200
+ "kb.cloudNote": "Documentos indexados en la nube de Anam — el avatar busca en cada turno",
201
+ "kb.reset": "Restablecer",
202
+ "kb.dropHint": "Suelta los documentos aquí",
203
+ "kb.dropSubtext": "PDF · TXT · MD · DOCX · CSV — máx. 50 MB",
204
+ "kb.browse": "Examinar archivos",
205
+ "kb.uploading": "Subiendo…",
206
+ "kb.useRagLabel": "Usar la base de conocimiento — el avatar buscará en tus documentos al responder",
207
+ "kb.editPrompt": "✏️ Editar comportamiento del asistente RAG (system prompt)",
208
+ "kb.promptLabel": "System prompt — controla cómo el avatar usa la base de conocimiento y responde preguntas generales",
209
+ "kb.applyPrompt": "Aplicar a la persona",
210
+ "kb.promptHint": "Los cambios se aplican en la próxima sesión que inicies.",
211
+ "kb.localNote": "Documentos fragmentados y vectorizados localmente — solo se recuperan los pasajes más relevantes para cada pregunta.",
212
+ "kb.localNoteSub": "Ideal para guías clínicas, protocolos y PDF de referencia. Los embeddings persisten entre reinicios.",
213
+ "kb.localDropHint": "Suelta aquí los documentos de referencia",
214
+ "kb.localDropSubtext": "PDF · DOCX · TXT · MD",
215
+ "kb.localBrowse": "Examinar archivos",
216
+ "kb.localInjectLabel": "Inyectar documentos en el contexto del avatar al conectar",
217
+ "clinical.title": "🩺 Informe de análisis de sangre",
218
+ "clinical.dropHint": "Suelta aquí tu informe de análisis",
219
+ "clinical.dropSubtext": "PDF de texto — máx. 50 MB",
220
+ "clinical.browse": "Examinar archivos",
221
+ "clinical.analysing": "Analizando el informe…",
222
+ "telemetry.title": "📊 Coste de la sesión",
223
+ "telemetry.cost": "Coste estimado",
224
+ "telemetry.minutes": "Minutos facturados",
225
+ "telemetry.sessions": "Sesiones",
226
+ "telemetry.live": "Sesión en vivo",
227
+ },
228
+ de: {
229
+ "lang.label": "Sprache",
230
+ "header.subtitle": "Echtzeit-Gespräch mit einer Anam-KI-Persona — mit Fragen zu Dokumenten",
231
+ "placeholder.text": "Der Avatar erscheint hier",
232
+ "placeholder.hint": "Klicke auf Verbinden, um zu starten",
233
+ "status.notConnected": "Nicht verbunden",
234
+ "plan.free": "Kostenlos · 2 Min. Limit",
235
+ "plan.pro": "Pro · unbegrenzt",
236
+ "plan.banner": "Kostenloser Plan: Der Avatar teilt euch vorab mit, dass ihr 2 Minuten zusammen habt, zeigt einen Live-Countdown, und die Sitzung wird automatisch getrennt, wenn die Zeit abgelaufen ist.",
237
+ "persona.default": "— Standard, flüchtig —",
238
+ "controls.connect": "Verbinden",
239
+ "controls.disconnect": "Trennen",
240
+ "advanced.toggle": "⚙ Erweitert (System-Prompt)",
241
+ "advanced.clinicalPreset": "🩺 Voreinstellung Klinischer Assistent",
242
+ "advanced.systemPromptLabel": "System-Prompt (verwendet, wenn keine Persona ausgewählt und RAG deaktiviert ist)",
243
+ "mode.voice": "🎙 Sprache",
244
+ "mode.text": "⌨️ Text",
245
+ "voice.hint": "Mikrofon ist aktiv — sprich mit dem Avatar",
246
+ "voice.hintMuted": "Mikrofon stummgeschaltet — klicken zum Aktivieren",
247
+ "text.placeholder": "Nachricht an den Avatar eingeben…",
248
+ "text.send": "Senden",
249
+ "kb.title": "📚 Wissensdatenbank",
250
+ "kb.cloudTab": "☁ Anam Cloud",
251
+ "kb.localTab": "💻 Lokal",
252
+ "kb.cloudNote": "Dokumente in der Anam-Cloud indexiert — der Avatar sucht bei jedem Zug",
253
+ "kb.reset": "Zurücksetzen",
254
+ "kb.dropHint": "Dokumente hierher ziehen",
255
+ "kb.dropSubtext": "PDF · TXT · MD · DOCX · CSV — max. 50 MB",
256
+ "kb.browse": "Dateien durchsuchen",
257
+ "kb.uploading": "Wird hochgeladen…",
258
+ "kb.useRagLabel": "Wissensdatenbank verwenden — der Avatar durchsucht deine Dokumente bei der Antwort",
259
+ "kb.editPrompt": "✏️ Verhalten des RAG-Assistenten bearbeiten (System-Prompt)",
260
+ "kb.promptLabel": "System-Prompt — steuert, wie der Avatar die Wissensdatenbank nutzt und allgemeine Fragen beantwortet",
261
+ "kb.applyPrompt": "Auf Persona anwenden",
262
+ "kb.promptHint": "Änderungen gelten ab der nächsten Sitzung, die du startest.",
263
+ "kb.localNote": "Dokumente werden lokal zerlegt und eingebettet — pro Frage werden nur die relevantesten Passagen abgerufen.",
264
+ "kb.localNoteSub": "Ideal für klinische Leitlinien, Protokolle und Referenz-PDFs. Embeddings bleiben über Neustarts hinweg erhalten.",
265
+ "kb.localDropHint": "Referenzdokumente hierher ziehen",
266
+ "kb.localDropSubtext": "PDF · DOCX · TXT · MD",
267
+ "kb.localBrowse": "Dateien durchsuchen",
268
+ "kb.localInjectLabel": "Dokumente beim Verbinden in den Kontext des Avatars einfügen",
269
+ "clinical.title": "🩺 Blutbild-Bericht",
270
+ "clinical.dropHint": "Blutbild-Bericht hierher ziehen",
271
+ "clinical.dropSubtext": "Text-basiertes PDF — max. 50 MB",
272
+ "clinical.browse": "Dateien durchsuchen",
273
+ "clinical.analysing": "Bericht wird analysiert…",
274
+ "telemetry.title": "📊 Sitzungskosten",
275
+ "telemetry.cost": "Geschätzte Kosten",
276
+ "telemetry.minutes": "Abgerechnete Minuten",
277
+ "telemetry.sessions": "Sitzungen",
278
+ "telemetry.live": "Laufende Sitzung",
279
+ },
280
+ };
281
+
282
+ function normalize(locale) {
283
+ return LOCALES.includes(locale) ? locale : "en";
284
+ }
285
+
286
+ export function getLocale() {
287
+ return normalize(localStorage.getItem(STORAGE_KEY));
288
+ }
289
+
290
+ export function setLocale(locale) {
291
+ localStorage.setItem(STORAGE_KEY, normalize(locale));
292
+ }
293
+
294
+ /** Look up a single translation key for the current locale (falls back to English). */
295
+ export function t(key) {
296
+ const locale = getLocale();
297
+ return T[locale]?.[key] ?? T.en[key] ?? key;
298
+ }
299
+
300
+ /**
301
+ * Apply the current locale to every element in the document carrying:
302
+ * - data-i18n="key" → sets textContent
303
+ * - data-i18n-placeholder="key" → sets the placeholder attribute
304
+ * - data-i18n-title="key" → sets the title attribute
305
+ * Also updates <html lang="...">.
306
+ */
307
+ export function applyTranslations() {
308
+ const locale = getLocale();
309
+ document.documentElement.lang = locale;
310
+
311
+ document.querySelectorAll("[data-i18n]").forEach((el) => {
312
+ el.textContent = t(el.dataset.i18n);
313
+ });
314
+ document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
315
+ el.placeholder = t(el.dataset.i18nPlaceholder);
316
+ });
317
+ document.querySelectorAll("[data-i18n-title]").forEach((el) => {
318
+ el.title = t(el.dataset.i18nTitle);
319
+ });
320
+ }
anam/static/js/kb.js CHANGED
@@ -2,6 +2,7 @@ import {
2
  deleteRagDocument, getRagDocuments, getRagPrompt, getRagStatus,
3
  resetRag, updateRagPrompt, uploadRagFile,
4
  } from "./api.js";
 
5
 
6
  // ── Shared state (live bindings — app.js reads these directly) ────────────────
7
  export let ragSetupReady = false;
@@ -195,7 +196,7 @@ applyRagBtn.addEventListener("click", async () => {
195
  applyRagBtn.disabled = true;
196
  ragPromptHint.textContent = "Saving…";
197
  try {
198
- await updateRagPrompt(prompt);
199
  ragPromptHint.textContent = "✓ Saved — reconnect for changes to take effect.";
200
  } catch (e) {
201
  ragPromptHint.textContent = "Save failed: " + e.message;
 
2
  deleteRagDocument, getRagDocuments, getRagPrompt, getRagStatus,
3
  resetRag, updateRagPrompt, uploadRagFile,
4
  } from "./api.js";
5
+ import { getLocale } from "./i18n.js";
6
 
7
  // ── Shared state (live bindings — app.js reads these directly) ────────────────
8
  export let ragSetupReady = false;
 
196
  applyRagBtn.disabled = true;
197
  ragPromptHint.textContent = "Saving…";
198
  try {
199
+ await updateRagPrompt(prompt, getLocale());
200
  ragPromptHint.textContent = "✓ Saved — reconnect for changes to take effect.";
201
  } catch (e) {
202
  ragPromptHint.textContent = "Save failed: " + e.message;