@agentprojectcontext/apx 1.36.0 → 1.38.0

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.
Files changed (79) hide show
  1. package/README.md +81 -3
  2. package/package.json +1 -1
  3. package/src/core/mascot.js +80 -80
  4. package/src/host/daemon/api/agents.js +6 -0
  5. package/src/host/daemon/api/conversations.js +9 -2
  6. package/src/host/daemon/api/web.js +20 -1
  7. package/src/host/daemon/desktop-ws.js +31 -0
  8. package/src/host/daemon/index.js +12 -2
  9. package/src/interfaces/cli/commands/agent.js +20 -0
  10. package/src/interfaces/cli/commands/chat.js +15 -6
  11. package/src/interfaces/cli/commands/identity.js +20 -1
  12. package/src/interfaces/cli/commands/update.js +2 -0
  13. package/src/interfaces/cli/index.js +14 -0
  14. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +629 -0
  15. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
  16. package/src/interfaces/web/dist/assets/index-hwxuTPcK.css +1 -0
  17. package/src/interfaces/web/dist/index.html +2 -2
  18. package/src/interfaces/web/src/App.tsx +20 -9
  19. package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
  20. package/src/interfaces/web/src/components/Roby.tsx +96 -0
  21. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
  22. package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
  23. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
  24. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
  25. package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
  26. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
  27. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
  28. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
  29. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
  30. package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
  31. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
  32. package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
  33. package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
  34. package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
  35. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
  36. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
  37. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
  38. package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
  39. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  40. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
  41. package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
  42. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
  43. package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
  44. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
  45. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
  46. package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
  47. package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
  48. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
  49. package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
  50. package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
  51. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
  52. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
  53. package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
  54. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
  55. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
  56. package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
  57. package/src/interfaces/web/src/hooks/useChat.ts +6 -5
  58. package/src/interfaces/web/src/i18n/en.ts +519 -2
  59. package/src/interfaces/web/src/i18n/es.ts +519 -2
  60. package/src/interfaces/web/src/i18n/index.ts +1 -1
  61. package/src/interfaces/web/src/lib/api/voice.ts +5 -5
  62. package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
  63. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  64. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
  65. package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
  66. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
  67. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
  68. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
  69. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
  70. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
  71. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
  72. package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
  73. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
  74. package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
  75. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
  76. package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
  77. package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +0 -1
  78. package/src/interfaces/web/dist/assets/index-DJKA763h.js +0 -628
  79. package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +0 -1
@@ -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 ? `…${secretSuffix((config as { api_key?: unknown })?.api_key) ?? ""} (ya seteada)` : "API key";
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 || "Error al guardar.");
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}>Cancelar</Button>
134
- <Button variant="primary" onClick={submit} loading={busy} data-testid="voice-provider-save">Guardar</Button>
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="Binario (bin)" hint="Ruta o nombre del CLI piper (PATH).">
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="Modelo (.onnx)" hint="Ruta absoluta al modelo de voz piper.">
145
- <Input value={f.model} onChange={(e) => up({ model: e.target.value })} placeholder="/abs/path/voz.onnx" />
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="Speaker (opcional)" hint="Id de hablante para modelos multi-voz.">
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="API key" hint={existingKey ? "Dejá en blanco para mantener la actual." : "Se guarda como secreto. Env: ELEVENLABS_API_KEY"}>
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="Modelo">
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="Voice ID" hint="Id de la voz de ElevenLabs (vacío = default).">
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="Formato de salida">
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="API key" hint={existingKey ? "Dejá en blanco para mantener la actual." : "Se reusa engines.openai.api_key si la dejás en blanco. Env: OPENAI_API_KEY"}>
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="Modelo">
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="Voz">
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="Formato">
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="API key" hint={existingKey ? "Dejá en blanco para mantener la actual." : "Se reusa engines.gemini.api_key si la dejás en blanco. Env: GEMINI_API_KEY"}>
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="Modelo" hint="TTS de Gemini sigue en preview.">
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="Voz">
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="Estilo (cómo querés que hable)" hint="Instrucción en lenguaje natural. Vacío = sin estilo. Ej: 'hablá en tono alegre y pausado'.">
199
- <Textarea rows={2} value={f.style || ""} onChange={(e) => up({ style: e.target.value })} placeholder="hablá en tono alegre y enérgico" />
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
- El motor <strong>mock</strong> genera un WAV silencioso de prueba. No tiene parámetros: sirve como fallback garantizado cuando no hay otro motor configurado.
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 PROVIDER_OPTIONS = [
16
- { value: "auto", label: "Automático (local, luego OpenAI)" },
17
- { value: "local", label: "Local — faster-whisper (offline)" },
18
- { value: "openai", label: "OpenAI — Whisper-1 (cloud)" },
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 LANG_OPTIONS = [
22
- { value: "auto", label: "Auto-detectar" },
23
- { value: "es", label: "Español" },
24
- { value: "en", label: "Inglés" },
25
- { value: "pt", label: "Portugués" },
26
- { value: "fr", label: "Francés" },
27
- { value: "it", label: "Italiano" },
28
- { value: "de", label: "Alemán" },
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="Motor de transcripción" hint="Local usa faster-whisper (requiere python3 + faster-whisper). OpenAI usa la key de engines.openai.">
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={PROVIDER_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="Modelo local (whisper)" hint="Más grande = más preciso y más lento.">
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="Idioma" hint='Para español, fijá "Español" mejora la precisión.'>
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={LANG_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("Hola, soy APX. Esto es una prueba de voz.");
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
- ? `Por defecto (${TTS_PROVIDER_META[defaultProvider]?.name || defaultProvider})`
34
- : "Por defecto (cadena)";
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 ? "" : " · no disponible"}`,
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 t = text.trim();
46
- if (!t) {
47
- toast.error("Escribí algo para decir.");
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: t,
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 || "No se pudo sintetizar.");
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="Motor" hint="Override del por defecto para probar.">
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="Estilo (solo Gemini)" hint="Cómo querés que hable. Vacío = sin estilo.">
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="hablá en tono alegre y enérgico"
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="Texto a decir">
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="Escribí lo que querés que diga…"
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" /> Decir esto
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" /> Parar
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" /> Repetir
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
- Motor: <strong>{last.provider}</strong>
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
  )}
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useRef, useState } from "react";
2
2
  import { SuperAgent, Agents, Conversations } from "../lib/api";
3
3
  import type { ChatStreamEvent, ChatUsage, ConversationMessage } from "../types/daemon";
4
+ import { t } from "../i18n";
4
5
 
5
6
  export type ToolStatus = "running" | "done" | "error" | "deduped";
6
7
 
@@ -259,7 +260,7 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
259
260
  const applyEvent = useCallback(
260
261
  (ev: ChatStreamEvent) => {
261
262
  if (ev.type === "error") {
262
- onError?.(ev.error || "stream error");
263
+ onError?.(ev.error || t("shared_ui.err_stream"));
263
264
  return;
264
265
  }
265
266
  patchLast((m) => applyStreamEvent(m, ev));
@@ -302,7 +303,7 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
302
303
  parts: [{ kind: "text", text: out.text }],
303
304
  }));
304
305
  } catch (e) {
305
- onError?.((e as Error)?.message || "fallo");
306
+ onError?.((e as Error)?.message || t("shared_ui.err_chat_failed"));
306
307
  setMsgs((curr) => curr.filter((_, i) => i !== curr.length - 1));
307
308
  } finally {
308
309
  setStreaming(false);
@@ -326,10 +327,10 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
326
327
  patchLast((m) => ({
327
328
  ...m,
328
329
  pending: false,
329
- parts: [...m.parts, { kind: "text", text: "[detenido]" }],
330
+ parts: [...m.parts, { kind: "text", text: t("code_module.stopped") }],
330
331
  }));
331
332
  } else {
332
- onError?.((e as Error)?.message || "stream falló");
333
+ onError?.((e as Error)?.message || t("shared_ui.err_stream_failed"));
333
334
  setMsgs((curr) => curr.filter((_, i) => i !== curr.length - 1));
334
335
  }
335
336
  } finally {
@@ -364,7 +365,7 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
364
365
  setConversationId(conversationId);
365
366
  setMsgs(loaded);
366
367
  } catch (e) {
367
- onError?.((e as Error)?.message || "could not load conversation");
368
+ onError?.((e as Error)?.message || t("shared_ui.err_load_conversation"));
368
369
  }
369
370
  },
370
371
  [pid, streaming, onError],