@agentprojectcontext/apx 1.37.0 → 1.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/package.json +1 -1
- package/src/core/mascot.js +80 -80
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +629 -0
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
- package/src/interfaces/web/dist/assets/{index-B6sYFQFa.css → index-hwxuTPcK.css} +1 -1
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +15 -24
- package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
- package/src/interfaces/web/src/components/Roby.tsx +96 -0
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
- package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
- package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
- package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
- package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
- package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
- package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
- package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
- package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
- package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
- package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
- package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
- package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
- package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
- package/src/interfaces/web/src/hooks/useChat.ts +6 -5
- package/src/interfaces/web/src/i18n/en.ts +517 -1
- package/src/interfaces/web/src/i18n/es.ts +517 -1
- package/src/interfaces/web/src/i18n/index.ts +1 -1
- package/src/interfaces/web/src/lib/api/voice.ts +5 -5
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
- package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
- package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
- package/src/interfaces/web/dist/assets/index-DsADpObh.js +0 -633
- package/src/interfaces/web/dist/assets/index-DsADpObh.js.map +0 -1
|
@@ -144,9 +144,9 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
144
144
|
});
|
|
145
145
|
if (r.error) { setModelError(r.error); return; }
|
|
146
146
|
setAvailableModels(r.models);
|
|
147
|
-
if (r.models.length === 0) setModelError("
|
|
147
|
+
if (r.models.length === 0) setModelError(t("providers_modal.err_no_models"));
|
|
148
148
|
} catch (e) {
|
|
149
|
-
setModelError((e as Error).message || "
|
|
149
|
+
setModelError((e as Error).message || t("providers_modal.err_list_models"));
|
|
150
150
|
} finally { setLoadingModels(false); }
|
|
151
151
|
};
|
|
152
152
|
|
|
@@ -158,8 +158,8 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
158
158
|
|
|
159
159
|
const buildProvider = (): { provider: Provider; modelLimits?: Record<string, number> } | null => {
|
|
160
160
|
const slug = (f.slug || slugify(f.name)).trim();
|
|
161
|
-
if (!slug) { setError("
|
|
162
|
-
if (!isEdit && existingSlugs.includes(slug)) { setError(
|
|
161
|
+
if (!slug) { setError(t("providers_modal.err_slug_required")); return null; }
|
|
162
|
+
if (!isEdit && existingSlugs.includes(slug)) { setError(t("providers_modal.err_slug_exists", { slug })); return null; }
|
|
163
163
|
|
|
164
164
|
let modelLimits: Record<string, number> | undefined;
|
|
165
165
|
if (f.model_context_limits_json.trim()) {
|
|
@@ -167,7 +167,7 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
167
167
|
const parsed = JSON.parse(f.model_context_limits_json);
|
|
168
168
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
169
169
|
modelLimits = parsed;
|
|
170
|
-
} catch { setError("
|
|
170
|
+
} catch { setError(t("providers_modal.err_model_limits_json")); return null; }
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const pricingVals = [f.p_input, f.p_output, f.p_cache_read, f.p_cache_write].map((x) => x.trim());
|
|
@@ -227,16 +227,16 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
227
227
|
try {
|
|
228
228
|
if (jsonMode) {
|
|
229
229
|
const slug = (f.slug || slugify(f.name)).trim();
|
|
230
|
-
if (!slug) { setError("
|
|
230
|
+
if (!slug) { setError(t("providers_modal.err_slug_required_form")); return; }
|
|
231
231
|
let parsed: unknown;
|
|
232
232
|
try { parsed = JSON.parse(jsonText); }
|
|
233
|
-
catch { setError("
|
|
233
|
+
catch { setError(t("providers_modal.err_json_invalid")); return; }
|
|
234
234
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
235
|
-
setError("
|
|
235
|
+
setError(t("providers_modal.err_json_object")); return;
|
|
236
236
|
}
|
|
237
237
|
const raw = parsed as Record<string, unknown>;
|
|
238
238
|
if (!raw.engine || typeof raw.engine !== "string") {
|
|
239
|
-
setError(
|
|
239
|
+
setError(t("providers_modal.err_engine_missing")); return;
|
|
240
240
|
}
|
|
241
241
|
const provider: Provider = {
|
|
242
242
|
slug,
|
|
@@ -256,13 +256,13 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
256
256
|
await onSave({ provider: built.provider, apiKeyValue: f.api_key_value.trim() || undefined, originalSlug: initial?.slug });
|
|
257
257
|
onClose();
|
|
258
258
|
} catch (e) {
|
|
259
|
-
setError((e as Error).message || "
|
|
259
|
+
setError((e as Error).message || t("providers_modal.err_save"));
|
|
260
260
|
} finally { setBusy(false); }
|
|
261
261
|
};
|
|
262
262
|
|
|
263
263
|
const existingKey = isEdit && isSecretMarker(initial?.api_key);
|
|
264
264
|
const keySuffix = secretSuffix(initial?.api_key);
|
|
265
|
-
const keyPlaceholder = existingKey ?
|
|
265
|
+
const keyPlaceholder = existingKey ? t("providers_modal.api_key_set", { suffix: keySuffix ?? "" }) : "sk-…";
|
|
266
266
|
const isOllama = f.engine === "ollama";
|
|
267
267
|
const apiKeyEnv = ENGINE_PRESETS[f.engine]?.api_key_env;
|
|
268
268
|
|
|
@@ -271,12 +271,12 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
271
271
|
open={open}
|
|
272
272
|
onClose={onClose}
|
|
273
273
|
title={isEdit ? t("providers_modal.edit_title", { name: initial?.name || initial?.slug || "" }) : t("providers_modal.new_title")}
|
|
274
|
-
description="
|
|
274
|
+
description={t("providers_modal.description")}
|
|
275
275
|
size="lg"
|
|
276
276
|
footer={
|
|
277
277
|
<>
|
|
278
|
-
<Button variant="ghost" onClick={onClose} disabled={busy}>
|
|
279
|
-
<Button variant="primary" onClick={submit} loading={busy}>{isEdit ? "
|
|
278
|
+
<Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
|
|
279
|
+
<Button variant="primary" onClick={submit} loading={busy}>{isEdit ? t("common.save") : t("common.create")}</Button>
|
|
280
280
|
</>
|
|
281
281
|
}
|
|
282
282
|
>
|
|
@@ -286,7 +286,7 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
286
286
|
<div className="flex flex-wrap gap-1.5">
|
|
287
287
|
{PRESET_PILLS.map((eng) => {
|
|
288
288
|
const Icon = ENGINE_ICONS[eng];
|
|
289
|
-
const label = eng === "custom" ? "
|
|
289
|
+
const label = eng === "custom" ? t("providers_modal.custom") : (ENGINE_OPTIONS.find((o) => o.value === eng)?.label || eng);
|
|
290
290
|
const selected = f.engine === eng;
|
|
291
291
|
return (
|
|
292
292
|
<button
|
|
@@ -312,13 +312,13 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
312
312
|
jsonMode ? "border-sky-500/50 bg-sky-500/10 text-sky-400" : "border-border text-muted-fg hover:text-foreground"
|
|
313
313
|
}`}
|
|
314
314
|
>
|
|
315
|
-
<Braces className="size-3.5" /> {jsonMode ? "
|
|
315
|
+
<Braces className="size-3.5" /> {jsonMode ? t("providers_modal.form_mode") : t("providers_modal.json_mode")}
|
|
316
316
|
</button>
|
|
317
317
|
</div>
|
|
318
318
|
|
|
319
319
|
{jsonMode ? (
|
|
320
320
|
<div className="space-y-2">
|
|
321
|
-
<Field label="
|
|
321
|
+
<Field label={t("providers_modal.json_label")} hint={t("providers_modal.json_hint", { slug: (f.slug || slugify(f.name)) || "<slug>" })}>
|
|
322
322
|
<Textarea
|
|
323
323
|
rows={14}
|
|
324
324
|
className="font-mono text-xs"
|
|
@@ -327,15 +327,15 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
327
327
|
spellCheck={false}
|
|
328
328
|
/>
|
|
329
329
|
</Field>
|
|
330
|
-
<p className="text-[11px] text-muted-fg">
|
|
330
|
+
<p className="text-[11px] text-muted-fg">{t("providers_modal.json_help")}</p>
|
|
331
331
|
</div>
|
|
332
332
|
) : (
|
|
333
333
|
<>
|
|
334
334
|
<div className="grid grid-cols-2 gap-3">
|
|
335
|
-
<Field label="
|
|
336
|
-
<Input value={f.name} onChange={(e) => up({ name: e.target.value, slug: isEdit ? f.slug : slugify(e.target.value) })} placeholder="
|
|
335
|
+
<Field label={t("providers_modal.name_label")}>
|
|
336
|
+
<Input value={f.name} onChange={(e) => up({ name: e.target.value, slug: isEdit ? f.slug : slugify(e.target.value) })} placeholder={t("providers_modal.name_ph")} />
|
|
337
337
|
</Field>
|
|
338
|
-
<Field label="
|
|
338
|
+
<Field label={t("providers_modal.engine_label")}>
|
|
339
339
|
<UiSelect
|
|
340
340
|
value={f.engine}
|
|
341
341
|
onChange={(v) => changeEngine(v as EngineType)}
|
|
@@ -344,17 +344,17 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
344
344
|
</Field>
|
|
345
345
|
</div>
|
|
346
346
|
|
|
347
|
-
<Field label="
|
|
348
|
-
<Input value={f.base_url} onChange={(e) => up({ base_url: e.target.value })} placeholder="
|
|
347
|
+
<Field label={t("providers_modal.base_url_label")} hint={t("providers_modal.base_url_hint")}>
|
|
348
|
+
<Input value={f.base_url} onChange={(e) => up({ base_url: e.target.value })} placeholder={t("providers_modal.base_url_ph")} />
|
|
349
349
|
</Field>
|
|
350
350
|
|
|
351
351
|
{!isOllama && (
|
|
352
|
-
<Field label="
|
|
352
|
+
<Field label={t("providers_modal.api_key_label")} hint={existingKey ? t("providers_modal.api_key_hint_existing") : apiKeyEnv ? t("providers_modal.api_key_hint_env", { env: apiKeyEnv }) : t("providers_modal.api_key_hint")}>
|
|
353
353
|
<Input type="password" autoComplete="new-password" value={f.api_key_value} onChange={(e) => up({ api_key_value: e.target.value })} placeholder={keyPlaceholder} />
|
|
354
354
|
</Field>
|
|
355
355
|
)}
|
|
356
356
|
|
|
357
|
-
<Field label="
|
|
357
|
+
<Field label={t("providers_modal.model_label")}>
|
|
358
358
|
<div className="space-y-2">
|
|
359
359
|
<div className="flex items-start gap-2">
|
|
360
360
|
<ModelCombobox
|
|
@@ -365,7 +365,7 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
365
365
|
/>
|
|
366
366
|
<Button size="sm" variant="secondary" onClick={loadModels} disabled={loadingModels} title={t("providers_modal.list_models_hint")} aria-label={t("providers_modal.list_models_hint")}>
|
|
367
367
|
{loadingModels ? <Loader2 className="size-3.5 animate-spin" /> : <RefreshCw className="size-3.5" />}
|
|
368
|
-
|
|
368
|
+
{t("providers_modal.load_models")}
|
|
369
369
|
</Button>
|
|
370
370
|
</div>
|
|
371
371
|
{modelError && <p className="text-[11px] text-amber-400">{modelError}</p>}
|
|
@@ -373,29 +373,29 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
|
|
|
373
373
|
</Field>
|
|
374
374
|
|
|
375
375
|
<div className="grid grid-cols-2 gap-3">
|
|
376
|
-
<Field label="
|
|
377
|
-
<Field label={
|
|
376
|
+
<Field label={t("providers_modal.max_tokens_label")}><Input type="number" min={256} step={256} value={f.default_max_tokens} onChange={(e) => up({ default_max_tokens: parseInt(e.target.value) || 4096 })} /></Field>
|
|
377
|
+
<Field label={t("providers_modal.temperature_label", { value: f.default_temperature.toFixed(1) })}>
|
|
378
378
|
<input type="range" min={0} max={2} step={0.1} value={f.default_temperature} onChange={(e) => up({ default_temperature: parseFloat(e.target.value) })} className="mt-2 w-full accent-foreground" />
|
|
379
379
|
</Field>
|
|
380
380
|
</div>
|
|
381
381
|
|
|
382
382
|
<details className="rounded-md border border-border bg-muted/20 p-3">
|
|
383
|
-
<summary className="cursor-pointer text-xs font-medium text-muted-fg">
|
|
383
|
+
<summary className="cursor-pointer text-xs font-medium text-muted-fg">{t("providers_modal.pricing_summary")}</summary>
|
|
384
384
|
<div className="mt-3 space-y-3">
|
|
385
|
-
<Field label="
|
|
385
|
+
<Field label={t("providers_modal.context_limit_label")}><Input type="number" min={0} step={1024} value={f.context_limit_tokens} onChange={(e) => up({ context_limit_tokens: parseInt(e.target.value) || 0 })} /></Field>
|
|
386
386
|
<div className="grid grid-cols-2 gap-3">
|
|
387
|
-
<Field label="
|
|
388
|
-
<Field label="
|
|
389
|
-
<Field label="
|
|
390
|
-
<Field label="
|
|
387
|
+
<Field label={t("providers_modal.price_input")}><Input type="number" min={0} step={0.0001} value={f.p_input} onChange={(e) => up({ p_input: e.target.value })} placeholder="0.15" /></Field>
|
|
388
|
+
<Field label={t("providers_modal.price_output")}><Input type="number" min={0} step={0.0001} value={f.p_output} onChange={(e) => up({ p_output: e.target.value })} placeholder="0.60" /></Field>
|
|
389
|
+
<Field label={t("providers_modal.price_cache_read")}><Input type="number" min={0} step={0.0001} value={f.p_cache_read} onChange={(e) => up({ p_cache_read: e.target.value })} placeholder="0.03" /></Field>
|
|
390
|
+
<Field label={t("providers_modal.price_cache_write")}><Input type="number" min={0} step={0.0001} value={f.p_cache_write} onChange={(e) => up({ p_cache_write: e.target.value })} placeholder="0.00" /></Field>
|
|
391
391
|
</div>
|
|
392
|
-
<Field label="
|
|
392
|
+
<Field label={t("providers_modal.model_limits_label")} hint='{"gpt-4o-mini":128000}'>
|
|
393
393
|
<Textarea rows={3} className="font-mono text-xs" value={f.model_context_limits_json} onChange={(e) => up({ model_context_limits_json: e.target.value })} />
|
|
394
394
|
</Field>
|
|
395
395
|
</div>
|
|
396
396
|
</details>
|
|
397
397
|
|
|
398
|
-
<Switch checked={f.is_active} onChange={(v) => up({ is_active: v })} label="
|
|
398
|
+
<Switch checked={f.is_active} onChange={(v) => up({ is_active: v })} label={t("providers_modal.active_label")} />
|
|
399
399
|
</>
|
|
400
400
|
)}
|
|
401
401
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type TtsEngineInfo,
|
|
8
8
|
type TtsMode,
|
|
9
9
|
} from "../../lib/api/voice";
|
|
10
|
+
import { t } from "../../i18n";
|
|
10
11
|
|
|
11
12
|
// TTS engine selector. Two modes:
|
|
12
13
|
// chain — ordered fallback router; toggles enable/disable engines and the
|
|
@@ -63,11 +64,11 @@ export function VoiceProviderList({
|
|
|
63
64
|
<div className="rounded-lg border border-border p-3">
|
|
64
65
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
65
66
|
<div className="min-w-0">
|
|
66
|
-
<div className="text-sm font-medium">
|
|
67
|
+
<div className="text-sm font-medium">{t("voice_ui.selection_mode")}</div>
|
|
67
68
|
<div className="text-xs text-muted-fg">
|
|
68
69
|
{isChain
|
|
69
|
-
? "
|
|
70
|
-
: "
|
|
70
|
+
? t("voice_ui.mode_chain_desc")
|
|
71
|
+
: t("voice_ui.mode_single_desc")}
|
|
71
72
|
</div>
|
|
72
73
|
</div>
|
|
73
74
|
<div className="flex shrink-0 overflow-hidden rounded-md border border-border" role="group">
|
|
@@ -81,7 +82,7 @@ export function VoiceProviderList({
|
|
|
81
82
|
isChain ? "bg-emerald-500/15 text-emerald-300" : "text-muted-fg hover:text-fg",
|
|
82
83
|
)}
|
|
83
84
|
>
|
|
84
|
-
|
|
85
|
+
{t("voice_ui.mode_chain_btn")}
|
|
85
86
|
</button>
|
|
86
87
|
<button
|
|
87
88
|
type="button"
|
|
@@ -93,7 +94,7 @@ export function VoiceProviderList({
|
|
|
93
94
|
!isChain ? "bg-emerald-500/15 text-emerald-300" : "text-muted-fg hover:text-fg",
|
|
94
95
|
)}
|
|
95
96
|
>
|
|
96
|
-
|
|
97
|
+
{t("voice_ui.mode_single_btn")}
|
|
97
98
|
</button>
|
|
98
99
|
</div>
|
|
99
100
|
</div>
|
|
@@ -122,7 +123,7 @@ export function VoiceProviderList({
|
|
|
122
123
|
type="button"
|
|
123
124
|
onClick={() => move(id, -1)}
|
|
124
125
|
disabled={busy || idx === 0}
|
|
125
|
-
aria-label="
|
|
126
|
+
aria-label={t("voice_ui.move_up")}
|
|
126
127
|
data-testid={`voice-provider-${id}-up`}
|
|
127
128
|
className="text-muted-fg hover:text-fg disabled:opacity-30"
|
|
128
129
|
>
|
|
@@ -132,7 +133,7 @@ export function VoiceProviderList({
|
|
|
132
133
|
type="button"
|
|
133
134
|
onClick={() => move(id, 1)}
|
|
134
135
|
disabled={busy || idx === ids.length - 1}
|
|
135
|
-
aria-label="
|
|
136
|
+
aria-label={t("voice_ui.move_down")}
|
|
136
137
|
data-testid={`voice-provider-${id}-down`}
|
|
137
138
|
className="text-muted-fg hover:text-fg disabled:opacity-30"
|
|
138
139
|
>
|
|
@@ -145,15 +146,15 @@ export function VoiceProviderList({
|
|
|
145
146
|
<div className="min-w-0 flex-1">
|
|
146
147
|
<div className="flex items-center gap-2">
|
|
147
148
|
<span className="text-sm font-medium">{meta.name}</span>
|
|
148
|
-
{meta.local && <Badge tone="info">
|
|
149
|
+
{meta.local && <Badge tone="info">{t("voice_ui.badge_local")}</Badge>}
|
|
149
150
|
{e.available ? (
|
|
150
|
-
<Badge tone="success">
|
|
151
|
+
<Badge tone="success">{t("voice_ui.badge_available")}</Badge>
|
|
151
152
|
) : e.configured ? (
|
|
152
|
-
<Badge tone="warning">
|
|
153
|
+
<Badge tone="warning">{t("voice_ui.badge_unavailable")}</Badge>
|
|
153
154
|
) : (
|
|
154
|
-
<Badge tone="muted">
|
|
155
|
+
<Badge tone="muted">{t("voice_ui.badge_not_configured")}</Badge>
|
|
155
156
|
)}
|
|
156
|
-
{isDefault && <Badge tone="success">
|
|
157
|
+
{isDefault && <Badge tone="success">{t("voice_ui.badge_default")}</Badge>}
|
|
157
158
|
</div>
|
|
158
159
|
<div className="truncate text-xs text-muted-fg">{meta.note}</div>
|
|
159
160
|
</div>
|
|
@@ -174,7 +175,7 @@ export function VoiceProviderList({
|
|
|
174
175
|
disabled={busy}
|
|
175
176
|
data-testid={`voice-provider-${id}-default`}
|
|
176
177
|
>
|
|
177
|
-
<Circle className="size-3.5" />
|
|
178
|
+
<Circle className="size-3.5" /> {t("voice_ui.set_as_default")}
|
|
178
179
|
</Button>
|
|
179
180
|
)
|
|
180
181
|
)}
|
|
@@ -185,7 +186,7 @@ export function VoiceProviderList({
|
|
|
185
186
|
onClick={() => onConfigure(id)}
|
|
186
187
|
data-testid={`voice-provider-${id}-config`}
|
|
187
188
|
>
|
|
188
|
-
<Settings2 className="size-3.5" />
|
|
189
|
+
<Settings2 className="size-3.5" /> {t("voice_ui.configure")}
|
|
189
190
|
</Button>
|
|
190
191
|
</div>
|
|
191
192
|
</div>
|
|
@@ -77,7 +77,7 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
|
|
|
77
77
|
|
|
78
78
|
const hasSecret = providerId !== "piper" && providerId !== "mock";
|
|
79
79
|
const existingKey = hasSecret && isSecretMarker((config as { api_key?: unknown })?.api_key);
|
|
80
|
-
const keyPlaceholder = existingKey ?
|
|
80
|
+
const keyPlaceholder = existingKey ? t("voice_ui.api_key_set", { suffix: secretSuffix((config as { api_key?: unknown })?.api_key) ?? "" }) : t("voice_ui.api_key_label");
|
|
81
81
|
|
|
82
82
|
const submit = async () => {
|
|
83
83
|
setBusy(true);
|
|
@@ -115,7 +115,7 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
|
|
|
115
115
|
await onSave({ set, unset });
|
|
116
116
|
onClose();
|
|
117
117
|
} catch (e) {
|
|
118
|
-
setError((e as Error).message || "
|
|
118
|
+
setError((e as Error).message || t("voice_ui.err_save"));
|
|
119
119
|
} finally {
|
|
120
120
|
setBusy(false);
|
|
121
121
|
}
|
|
@@ -130,21 +130,21 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
|
|
|
130
130
|
size="md"
|
|
131
131
|
footer={
|
|
132
132
|
<>
|
|
133
|
-
<Button variant="ghost" onClick={onClose} disabled={busy}>
|
|
134
|
-
<Button variant="primary" onClick={submit} loading={busy} data-testid="voice-provider-save">
|
|
133
|
+
<Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
|
|
134
|
+
<Button variant="primary" onClick={submit} loading={busy} data-testid="voice-provider-save">{t("common.save")}</Button>
|
|
135
135
|
</>
|
|
136
136
|
}
|
|
137
137
|
>
|
|
138
138
|
<div className="space-y-3">
|
|
139
139
|
{providerId === "piper" && (
|
|
140
140
|
<>
|
|
141
|
-
<Field label="
|
|
141
|
+
<Field label={t("voice_ui.piper_bin_label")} hint={t("voice_ui.piper_bin_hint")}>
|
|
142
142
|
<Input value={f.bin} onChange={(e) => up({ bin: e.target.value })} placeholder="piper" />
|
|
143
143
|
</Field>
|
|
144
|
-
<Field label="
|
|
145
|
-
<Input value={f.model} onChange={(e) => up({ model: e.target.value })} placeholder="/abs/path/
|
|
144
|
+
<Field label={t("voice_ui.piper_model_label")} hint={t("voice_ui.piper_model_hint")}>
|
|
145
|
+
<Input value={f.model} onChange={(e) => up({ model: e.target.value })} placeholder="/abs/path/voice.onnx" />
|
|
146
146
|
</Field>
|
|
147
|
-
<Field label="
|
|
147
|
+
<Field label={t("voice_ui.piper_speaker_label")} hint={t("voice_ui.piper_speaker_hint")}>
|
|
148
148
|
<Input value={f.speaker} onChange={(e) => up({ speaker: e.target.value })} placeholder="0" />
|
|
149
149
|
</Field>
|
|
150
150
|
</>
|
|
@@ -152,16 +152,16 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
|
|
|
152
152
|
|
|
153
153
|
{providerId === "elevenlabs" && (
|
|
154
154
|
<>
|
|
155
|
-
<Field label="
|
|
155
|
+
<Field label={t("voice_ui.api_key_label")} hint={existingKey ? t("voice_ui.api_key_keep_hint") : t("voice_ui.api_key_secret_hint", { env: "ELEVENLABS_API_KEY" })}>
|
|
156
156
|
<Input type="password" autoComplete="new-password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder={keyPlaceholder} />
|
|
157
157
|
</Field>
|
|
158
|
-
<Field label="
|
|
158
|
+
<Field label={t("voice_ui.model_label")}>
|
|
159
159
|
<UiSelect value={f.model || ""} onChange={(v) => up({ model: v })} options={ELEVENLABS_MODELS.map((m) => ({ value: m, label: m }))} placeholder="eleven_multilingual_v2" />
|
|
160
160
|
</Field>
|
|
161
|
-
<Field label="
|
|
161
|
+
<Field label={t("voice_ui.voice_id_label")} hint={t("voice_ui.voice_id_hint")}>
|
|
162
162
|
<Input value={f.voice_id} onChange={(e) => up({ voice_id: e.target.value })} placeholder="EXAVITQu4vr4xnSDxMaL" />
|
|
163
163
|
</Field>
|
|
164
|
-
<Field label="
|
|
164
|
+
<Field label={t("voice_ui.output_format_label")}>
|
|
165
165
|
<Input value={f.output_format} onChange={(e) => up({ output_format: e.target.value })} placeholder="mp3_44100_128" />
|
|
166
166
|
</Field>
|
|
167
167
|
</>
|
|
@@ -169,16 +169,16 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
|
|
|
169
169
|
|
|
170
170
|
{providerId === "openai" && (
|
|
171
171
|
<>
|
|
172
|
-
<Field label="
|
|
172
|
+
<Field label={t("voice_ui.api_key_label")} hint={existingKey ? t("voice_ui.api_key_keep_hint") : t("voice_ui.api_key_reuse_hint", { engine: "engines.openai.api_key", env: "OPENAI_API_KEY" })}>
|
|
173
173
|
<Input type="password" autoComplete="new-password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder={keyPlaceholder} />
|
|
174
174
|
</Field>
|
|
175
|
-
<Field label="
|
|
175
|
+
<Field label={t("voice_ui.model_label")}>
|
|
176
176
|
<UiSelect value={f.model || "tts-1"} onChange={(v) => up({ model: v })} options={OPENAI_TTS_MODELS.map((m) => ({ value: m, label: m }))} />
|
|
177
177
|
</Field>
|
|
178
|
-
<Field label="
|
|
178
|
+
<Field label={t("voice_ui.voice_label")}>
|
|
179
179
|
<UiSelect value={f.voice || "alloy"} onChange={(v) => up({ voice: v })} options={OPENAI_TTS_VOICES.map((m) => ({ value: m, label: m }))} />
|
|
180
180
|
</Field>
|
|
181
|
-
<Field label="
|
|
181
|
+
<Field label={t("voice_ui.format_label")}>
|
|
182
182
|
<UiSelect value={f.format || "mp3"} onChange={(v) => up({ format: v })} options={["mp3", "opus", "aac", "flac", "wav"].map((m) => ({ value: m, label: m }))} />
|
|
183
183
|
</Field>
|
|
184
184
|
</>
|
|
@@ -186,24 +186,24 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
|
|
|
186
186
|
|
|
187
187
|
{providerId === "gemini" && (
|
|
188
188
|
<>
|
|
189
|
-
<Field label="
|
|
189
|
+
<Field label={t("voice_ui.api_key_label")} hint={existingKey ? t("voice_ui.api_key_keep_hint") : t("voice_ui.api_key_reuse_hint", { engine: "engines.gemini.api_key", env: "GEMINI_API_KEY" })}>
|
|
190
190
|
<Input type="password" autoComplete="new-password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder={keyPlaceholder} />
|
|
191
191
|
</Field>
|
|
192
|
-
<Field label="
|
|
192
|
+
<Field label={t("voice_ui.model_label")} hint={t("voice_ui.gemini_model_hint")}>
|
|
193
193
|
<Input value={f.model} onChange={(e) => up({ model: e.target.value })} placeholder="gemini-2.5-flash-preview-tts" />
|
|
194
194
|
</Field>
|
|
195
|
-
<Field label="
|
|
195
|
+
<Field label={t("voice_ui.voice_label")}>
|
|
196
196
|
<UiSelect value={f.voice || "Kore"} onChange={(v) => up({ voice: v })} options={GEMINI_TTS_VOICES.map((m) => ({ value: m, label: m }))} />
|
|
197
197
|
</Field>
|
|
198
|
-
<Field label="
|
|
199
|
-
<Textarea rows={2} value={f.style || ""} onChange={(e) => up({ style: e.target.value })} placeholder="
|
|
198
|
+
<Field label={t("voice_ui.style_label")} hint={t("voice_ui.style_hint")}>
|
|
199
|
+
<Textarea rows={2} value={f.style || ""} onChange={(e) => up({ style: e.target.value })} placeholder={t("voice_ui.style_ph")} />
|
|
200
200
|
</Field>
|
|
201
201
|
</>
|
|
202
202
|
)}
|
|
203
203
|
|
|
204
204
|
{providerId === "mock" && (
|
|
205
205
|
<p className="text-sm text-muted-fg">
|
|
206
|
-
|
|
206
|
+
{t("voice_ui.mock_desc")}
|
|
207
207
|
</p>
|
|
208
208
|
)}
|
|
209
209
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Field } from "../ui";
|
|
2
2
|
import { UiSelect } from "../UiSelect";
|
|
3
3
|
import { WHISPER_MODELS, type TranscriptionConfig } from "../../lib/api/voice";
|
|
4
|
+
import { t } from "../../i18n";
|
|
4
5
|
|
|
5
6
|
// STT (speech-to-text) configuration. Persisted under config.transcription.
|
|
6
7
|
// The actual capture happens in the deck overlay / Telegram / CLI; here the
|
|
@@ -12,20 +13,20 @@ interface Props {
|
|
|
12
13
|
busy?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
{ value: "auto", label: "
|
|
17
|
-
{ value: "local", label: "
|
|
18
|
-
{ value: "openai", label: "
|
|
16
|
+
const providerOptions = () => [
|
|
17
|
+
{ value: "auto", label: t("voice_ui.stt_provider_auto") },
|
|
18
|
+
{ value: "local", label: t("voice_ui.stt_provider_local") },
|
|
19
|
+
{ value: "openai", label: t("voice_ui.stt_provider_openai") },
|
|
19
20
|
];
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
{ value: "auto", label: "
|
|
23
|
-
{ value: "es", label: "
|
|
24
|
-
{ value: "en", label: "
|
|
25
|
-
{ value: "pt", label: "
|
|
26
|
-
{ value: "fr", label: "
|
|
27
|
-
{ value: "it", label: "
|
|
28
|
-
{ value: "de", label: "
|
|
22
|
+
const langOptions = () => [
|
|
23
|
+
{ value: "auto", label: t("voice_ui.lang_auto") },
|
|
24
|
+
{ value: "es", label: t("voice_ui.lang_es") },
|
|
25
|
+
{ value: "en", label: t("voice_ui.lang_en") },
|
|
26
|
+
{ value: "pt", label: t("voice_ui.lang_pt") },
|
|
27
|
+
{ value: "fr", label: t("voice_ui.lang_fr") },
|
|
28
|
+
{ value: "it", label: t("voice_ui.lang_it") },
|
|
29
|
+
{ value: "de", label: t("voice_ui.lang_de") },
|
|
29
30
|
];
|
|
30
31
|
|
|
31
32
|
export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
@@ -37,11 +38,11 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<div className="space-y-3">
|
|
40
|
-
<Field label="
|
|
41
|
+
<Field label={t("voice_ui.stt_engine_label")} hint={t("voice_ui.stt_engine_hint")}>
|
|
41
42
|
<UiSelect
|
|
42
43
|
value={provider}
|
|
43
44
|
onChange={(v) => onPatch({ "transcription.provider": v })}
|
|
44
|
-
options={
|
|
45
|
+
options={providerOptions()}
|
|
45
46
|
disabled={busy}
|
|
46
47
|
className="max-w-md"
|
|
47
48
|
/>
|
|
@@ -49,7 +50,7 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
49
50
|
|
|
50
51
|
{usesLocal && (
|
|
51
52
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
52
|
-
<Field label="
|
|
53
|
+
<Field label={t("voice_ui.stt_model_label")} hint={t("voice_ui.stt_model_hint")}>
|
|
53
54
|
<UiSelect
|
|
54
55
|
value={model}
|
|
55
56
|
onChange={(v) => onPatch({ "transcription.local.model": v })}
|
|
@@ -57,11 +58,11 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
57
58
|
disabled={busy}
|
|
58
59
|
/>
|
|
59
60
|
</Field>
|
|
60
|
-
<Field label="
|
|
61
|
+
<Field label={t("voice_ui.stt_language_label")} hint={t("voice_ui.stt_language_hint")}>
|
|
61
62
|
<UiSelect
|
|
62
63
|
value={language}
|
|
63
64
|
onChange={(v) => onPatch({ "transcription.local.language": v })}
|
|
64
|
-
options={
|
|
65
|
+
options={langOptions()}
|
|
65
66
|
disabled={busy}
|
|
66
67
|
/>
|
|
67
68
|
</Field>
|
|
@@ -5,6 +5,7 @@ import { UiSelect } from "../UiSelect";
|
|
|
5
5
|
import { useToast } from "../Toast";
|
|
6
6
|
import { useTtsPlayer } from "./useTtsPlayer";
|
|
7
7
|
import { Voice, TTS_PROVIDER_META, type TtsEngineInfo, type TtsMode, type TtsSayResult } from "../../lib/api/voice";
|
|
8
|
+
import { t } from "../../i18n";
|
|
8
9
|
|
|
9
10
|
// "Decir esto" tester. Lets you pick which engine to synthesize with (overriding
|
|
10
11
|
// the saved default) and add a free-text speaking-style instruction, then plays
|
|
@@ -21,7 +22,7 @@ interface Props {
|
|
|
21
22
|
export function VoiceTestCard({ engines, defaultProvider, mode }: Props) {
|
|
22
23
|
const toast = useToast();
|
|
23
24
|
const { play, stop, playing, loading: playLoading } = useTtsPlayer();
|
|
24
|
-
const [text, setText] = useState("
|
|
25
|
+
const [text, setText] = useState(t("voice_ui.test_default_text"));
|
|
25
26
|
// "" = use the saved default; otherwise force a specific engine.
|
|
26
27
|
const [engine, setEngine] = useState("");
|
|
27
28
|
const [style, setStyle] = useState("");
|
|
@@ -30,34 +31,34 @@ export function VoiceTestCard({ engines, defaultProvider, mode }: Props) {
|
|
|
30
31
|
|
|
31
32
|
const defaultLabel =
|
|
32
33
|
mode === "single" && defaultProvider && defaultProvider !== "auto"
|
|
33
|
-
?
|
|
34
|
-
: "
|
|
34
|
+
? t("voice_ui.test_default_engine", { name: TTS_PROVIDER_META[defaultProvider]?.name || defaultProvider })
|
|
35
|
+
: t("voice_ui.test_default_chain");
|
|
35
36
|
|
|
36
37
|
const options = [
|
|
37
38
|
{ value: "", label: defaultLabel },
|
|
38
39
|
...engines.map((e) => ({
|
|
39
40
|
value: e.id,
|
|
40
|
-
label: `${TTS_PROVIDER_META[e.id]?.name || e.id}${e.available ? "" : "
|
|
41
|
+
label: `${TTS_PROVIDER_META[e.id]?.name || e.id}${e.available ? "" : t("voice_ui.test_unavailable_suffix")}`,
|
|
41
42
|
})),
|
|
42
43
|
];
|
|
43
44
|
|
|
44
45
|
const say = async () => {
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
47
|
-
toast.error("
|
|
46
|
+
const txt = text.trim();
|
|
47
|
+
if (!txt) {
|
|
48
|
+
toast.error(t("voice_ui.test_empty_error"));
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
setBusy(true);
|
|
51
52
|
try {
|
|
52
53
|
const res = await Voice.say({
|
|
53
|
-
text:
|
|
54
|
+
text: txt,
|
|
54
55
|
provider: engine || undefined,
|
|
55
56
|
style: style.trim() || undefined,
|
|
56
57
|
});
|
|
57
58
|
setLast(res);
|
|
58
59
|
await play(res.audio_path);
|
|
59
60
|
} catch (e) {
|
|
60
|
-
toast.error((e as Error).message || "
|
|
61
|
+
toast.error((e as Error).message || t("voice_ui.test_synth_error"));
|
|
61
62
|
} finally {
|
|
62
63
|
setBusy(false);
|
|
63
64
|
}
|
|
@@ -66,43 +67,43 @@ export function VoiceTestCard({ engines, defaultProvider, mode }: Props) {
|
|
|
66
67
|
return (
|
|
67
68
|
<div className="space-y-3">
|
|
68
69
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
69
|
-
<Field label="
|
|
70
|
+
<Field label={t("voice_ui.test_engine_label")} hint={t("voice_ui.test_engine_hint")}>
|
|
70
71
|
<UiSelect value={engine} onChange={setEngine} options={options} />
|
|
71
72
|
</Field>
|
|
72
|
-
<Field label="
|
|
73
|
+
<Field label={t("voice_ui.test_style_label")} hint={t("voice_ui.test_style_hint")}>
|
|
73
74
|
<Input
|
|
74
75
|
value={style}
|
|
75
76
|
onChange={(e) => setStyle(e.target.value)}
|
|
76
|
-
placeholder="
|
|
77
|
+
placeholder={t("voice_ui.style_ph")}
|
|
77
78
|
data-testid="voice-test-style"
|
|
78
79
|
/>
|
|
79
80
|
</Field>
|
|
80
81
|
</div>
|
|
81
|
-
<Field label="
|
|
82
|
+
<Field label={t("voice_ui.test_text_label")}>
|
|
82
83
|
<Textarea
|
|
83
84
|
rows={2}
|
|
84
85
|
value={text}
|
|
85
86
|
onChange={(e) => setText(e.target.value)}
|
|
86
|
-
placeholder="
|
|
87
|
+
placeholder={t("voice_ui.test_text_ph")}
|
|
87
88
|
data-testid="voice-test-input"
|
|
88
89
|
/>
|
|
89
90
|
</Field>
|
|
90
91
|
<div className="flex items-center gap-2">
|
|
91
92
|
<Button variant="primary" onClick={say} loading={busy} disabled={playLoading} data-testid="voice-test-say">
|
|
92
|
-
<Volume2 className="size-4" />
|
|
93
|
+
<Volume2 className="size-4" /> {t("voice_ui.say_this")}
|
|
93
94
|
</Button>
|
|
94
95
|
{playing ? (
|
|
95
96
|
<Button variant="secondary" onClick={stop} data-testid="voice-test-stop">
|
|
96
|
-
<Square className="size-4" />
|
|
97
|
+
<Square className="size-4" /> {t("voice_ui.stop")}
|
|
97
98
|
</Button>
|
|
98
99
|
) : last ? (
|
|
99
100
|
<Button variant="secondary" onClick={() => play(last.audio_path)} loading={playLoading} data-testid="voice-test-replay">
|
|
100
|
-
<Play className="size-4" />
|
|
101
|
+
<Play className="size-4" /> {t("voice_ui.replay")}
|
|
101
102
|
</Button>
|
|
102
103
|
) : null}
|
|
103
104
|
{last && (
|
|
104
105
|
<span className="text-xs text-muted-fg">
|
|
105
|
-
|
|
106
|
+
{t("voice_ui.engine_result")}: <strong>{last.provider}</strong>
|
|
106
107
|
{last.duration_s ? ` · ${last.duration_s.toFixed(1)}s` : ""}
|
|
107
108
|
</span>
|
|
108
109
|
)}
|