@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.
- package/README.md +81 -3
- package/package.json +1 -1
- package/src/core/mascot.js +80 -80
- package/src/host/daemon/api/agents.js +6 -0
- package/src/host/daemon/api/conversations.js +9 -2
- package/src/host/daemon/api/web.js +20 -1
- package/src/host/daemon/desktop-ws.js +31 -0
- package/src/host/daemon/index.js +12 -2
- package/src/interfaces/cli/commands/agent.js +20 -0
- package/src/interfaces/cli/commands/chat.js +15 -6
- package/src/interfaces/cli/commands/identity.js +20 -1
- package/src/interfaces/cli/commands/update.js +2 -0
- package/src/interfaces/cli/index.js +14 -0
- 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-hwxuTPcK.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +20 -9
- 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 +519 -2
- package/src/interfaces/web/src/i18n/es.ts +519 -2
- 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-Cm0KyPoZ.css +0 -1
- package/src/interfaces/web/dist/assets/index-DJKA763h.js +0 -628
- package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +0 -1
|
@@ -5,6 +5,7 @@ import { Section } from "../Section";
|
|
|
5
5
|
import { Button, Field, Input, Loading, Badge, Switch } from "../ui";
|
|
6
6
|
import { useToast } from "../Toast";
|
|
7
7
|
import { Skills, type InspectTrace } from "../../lib/api/skills";
|
|
8
|
+
import { t } from "../../i18n";
|
|
8
9
|
|
|
9
10
|
// Skill Inspector — per-turn skill RAG middleware. When ON, the static
|
|
10
11
|
// "available skills" slug-dump is removed from the agent's system prompt and a
|
|
@@ -19,15 +20,17 @@ import { Skills, type InspectTrace } from "../../lib/api/skills";
|
|
|
19
20
|
// Numeric knobs with human labels + sane ranges. We keep them as plain number
|
|
20
21
|
// inputs (same idiom as the embeddings model fields) rather than sliders so the
|
|
21
22
|
// values are explicit and copy-pasteable.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
function knobs(): { key: keyof NumericKnobs; label: string; hint: string; step: number; min: number; max: number }[] {
|
|
24
|
+
return [
|
|
25
|
+
{ key: "load_threshold", label: t("settings_ui.knob_load_threshold"), hint: t("settings_ui.knob_load_threshold_hint"), step: 0.01, min: 0, max: 1 },
|
|
26
|
+
{ key: "hint_threshold", label: t("settings_ui.knob_hint_threshold"), hint: t("settings_ui.knob_hint_threshold_hint"), step: 0.01, min: 0, max: 1 },
|
|
27
|
+
{ key: "margin", label: t("settings_ui.knob_margin"), hint: t("settings_ui.knob_margin_hint"), step: 0.01, min: 0, max: 1 },
|
|
28
|
+
{ key: "max_loaded", label: t("settings_ui.knob_max_loaded"), hint: t("settings_ui.knob_max_loaded_hint"), step: 1, min: 0, max: 5 },
|
|
29
|
+
{ key: "max_hints", label: t("settings_ui.knob_max_hints"), hint: t("settings_ui.knob_max_hints_hint"), step: 1, min: 0, max: 8 },
|
|
30
|
+
{ key: "prompt_floor", label: t("settings_ui.knob_prompt_floor"), hint: t("settings_ui.knob_prompt_floor_hint"), step: 1, min: 0, max: 40 },
|
|
31
|
+
{ key: "body_char_cap", label: t("settings_ui.knob_body_char_cap"), hint: t("settings_ui.knob_body_char_cap_hint"), step: 500, min: 500, max: 20000 },
|
|
32
|
+
];
|
|
33
|
+
}
|
|
31
34
|
|
|
32
35
|
type NumericKnobs = {
|
|
33
36
|
load_threshold: number; hint_threshold: number; margin: number;
|
|
@@ -52,7 +55,7 @@ export function SkillsInspectorPanel() {
|
|
|
52
55
|
await Skills.updateInspector(patch);
|
|
53
56
|
await mutate();
|
|
54
57
|
} catch (e) {
|
|
55
|
-
toast.error(
|
|
58
|
+
toast.error(t("settings_ui.could_not_save", { msg: (e as Error).message }));
|
|
56
59
|
} finally {
|
|
57
60
|
setBusy(false);
|
|
58
61
|
}
|
|
@@ -63,11 +66,17 @@ export function SkillsInspectorPanel() {
|
|
|
63
66
|
try {
|
|
64
67
|
const r = await Skills.index({ force });
|
|
65
68
|
toast.success(
|
|
66
|
-
|
|
69
|
+
t("settings_ui.indexed_with", {
|
|
70
|
+
embedder: r.embedder,
|
|
71
|
+
dim: r.dim,
|
|
72
|
+
added: r.changed.added,
|
|
73
|
+
refreshed: r.changed.refreshed,
|
|
74
|
+
removed: r.changed.removed,
|
|
75
|
+
}),
|
|
67
76
|
);
|
|
68
77
|
await mutate();
|
|
69
78
|
} catch (e) {
|
|
70
|
-
toast.error(
|
|
79
|
+
toast.error(t("settings_ui.index_failed", { msg: (e as Error).message }));
|
|
71
80
|
} finally {
|
|
72
81
|
setBusy(false);
|
|
73
82
|
}
|
|
@@ -81,7 +90,7 @@ export function SkillsInspectorPanel() {
|
|
|
81
90
|
const r = await Skills.inspect(probe.trim());
|
|
82
91
|
setProbeResult(r.trace);
|
|
83
92
|
} catch (e) {
|
|
84
|
-
toast.error(
|
|
93
|
+
toast.error(t("settings_ui.dry_run_failed", { msg: (e as Error).message }));
|
|
85
94
|
} finally {
|
|
86
95
|
setBusy(false);
|
|
87
96
|
}
|
|
@@ -90,55 +99,55 @@ export function SkillsInspectorPanel() {
|
|
|
90
99
|
return (
|
|
91
100
|
<div className="grid gap-6 xl:grid-cols-2 xl:items-start">
|
|
92
101
|
<Section
|
|
93
|
-
title="
|
|
94
|
-
description="
|
|
102
|
+
title={t("settings_ui.inspector_title")}
|
|
103
|
+
description={t("settings_ui.inspector_desc")}
|
|
95
104
|
>
|
|
96
105
|
<div className="space-y-4">
|
|
97
106
|
<Field
|
|
98
|
-
label="
|
|
99
|
-
hint="
|
|
107
|
+
label={t("settings_ui.enable_inspector")}
|
|
108
|
+
hint={t("settings_ui.enable_inspector_hint")}
|
|
100
109
|
>
|
|
101
110
|
<Switch
|
|
102
111
|
checked={cfg.enabled}
|
|
103
112
|
disabled={busy}
|
|
104
113
|
onChange={(v) => apply({ enabled: v })}
|
|
105
|
-
label={cfg.enabled ? "
|
|
114
|
+
label={cfg.enabled ? t("settings_ui.on") : t("settings_ui.off")}
|
|
106
115
|
/>
|
|
107
116
|
</Field>
|
|
108
117
|
|
|
109
118
|
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
110
119
|
<Badge tone={idx.count > 0 ? "success" : "warning"}>
|
|
111
|
-
|
|
120
|
+
{t("settings_ui.index_count", { n: idx.count })}
|
|
112
121
|
</Badge>
|
|
113
|
-
<Badge tone="muted">{idx.embedder || "
|
|
114
|
-
{idx.dim ? <Badge tone="muted">dim {idx.dim}</Badge> : null}
|
|
122
|
+
<Badge tone="muted">{idx.embedder || t("settings_ui.not_indexed")}</Badge>
|
|
123
|
+
{idx.dim ? <Badge tone="muted">{t("settings_ui.dim", { dim: idx.dim })}</Badge> : null}
|
|
115
124
|
{idx.updated_at ? (
|
|
116
125
|
<span className="text-xs text-muted-foreground">
|
|
117
|
-
|
|
126
|
+
{t("settings_ui.updated_at", { date: new Date(idx.updated_at).toLocaleString() })}
|
|
118
127
|
</span>
|
|
119
128
|
) : null}
|
|
120
129
|
</div>
|
|
121
130
|
|
|
122
131
|
<div className="flex flex-wrap items-center gap-3 pt-1">
|
|
123
132
|
<Button variant="secondary" onClick={() => runIndex(false)} loading={busy}>
|
|
124
|
-
<RefreshCw size={14} />
|
|
133
|
+
<RefreshCw size={14} /> {t("settings_ui.reindex")}
|
|
125
134
|
</Button>
|
|
126
135
|
<Button variant="secondary" onClick={() => runIndex(true)} loading={busy}>
|
|
127
|
-
<RefreshCw size={14} />
|
|
136
|
+
<RefreshCw size={14} /> {t("settings_ui.reindex_forced")}
|
|
128
137
|
</Button>
|
|
129
138
|
<span className="text-xs text-muted-foreground">
|
|
130
|
-
|
|
139
|
+
{t("settings_ui.embedder_source")}
|
|
131
140
|
</span>
|
|
132
141
|
</div>
|
|
133
142
|
</div>
|
|
134
143
|
</Section>
|
|
135
144
|
|
|
136
145
|
<Section
|
|
137
|
-
title="
|
|
138
|
-
description="
|
|
146
|
+
title={t("settings_ui.thresholds_title")}
|
|
147
|
+
description={t("settings_ui.thresholds_desc")}
|
|
139
148
|
>
|
|
140
149
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
141
|
-
{
|
|
150
|
+
{knobs().map((k) => (
|
|
142
151
|
<Field key={k.key} label={k.label} hint={k.hint}>
|
|
143
152
|
<Input
|
|
144
153
|
type="number"
|
|
@@ -159,21 +168,21 @@ export function SkillsInspectorPanel() {
|
|
|
159
168
|
</Section>
|
|
160
169
|
|
|
161
170
|
<Section
|
|
162
|
-
title="
|
|
163
|
-
description="
|
|
171
|
+
title={t("settings_ui.test_title")}
|
|
172
|
+
description={t("settings_ui.test_desc")}
|
|
164
173
|
>
|
|
165
174
|
<div className="space-y-3">
|
|
166
175
|
<div className="flex flex-wrap items-center gap-2">
|
|
167
176
|
<Input
|
|
168
177
|
value={probe}
|
|
169
|
-
placeholder="
|
|
178
|
+
placeholder={t("settings_ui.test_placeholder")}
|
|
170
179
|
disabled={busy}
|
|
171
180
|
onChange={(ev) => setProbe(ev.target.value)}
|
|
172
181
|
onKeyDown={(ev) => { if (ev.key === "Enter") runProbe(); }}
|
|
173
182
|
className="max-w-xl flex-1"
|
|
174
183
|
/>
|
|
175
184
|
<Button variant="primary" onClick={runProbe} loading={busy}>
|
|
176
|
-
<Wand2 size={14} />
|
|
185
|
+
<Wand2 size={14} /> {t("settings_ui.test_btn")}
|
|
177
186
|
</Button>
|
|
178
187
|
</div>
|
|
179
188
|
|
|
@@ -182,7 +191,7 @@ export function SkillsInspectorPanel() {
|
|
|
182
191
|
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
183
192
|
<Sparkles size={14} className="text-muted-foreground" />
|
|
184
193
|
<span className="text-muted-foreground">{probeResult.embedder || "—"}</span>
|
|
185
|
-
{probeResult.jit ? <Badge tone="warning">
|
|
194
|
+
{probeResult.jit ? <Badge tone="warning">{t("settings_ui.jit_empty_index")}</Badge> : null}
|
|
186
195
|
{probeResult.reason && !probeResult.loaded?.length && !probeResult.hinted?.length ? (
|
|
187
196
|
<Badge tone="muted">{probeResult.reason}</Badge>
|
|
188
197
|
) : null}
|
|
@@ -190,7 +199,7 @@ export function SkillsInspectorPanel() {
|
|
|
190
199
|
|
|
191
200
|
{probeResult.loaded?.length ? (
|
|
192
201
|
<div className="mb-1">
|
|
193
|
-
<span className="text-muted-foreground">
|
|
202
|
+
<span className="text-muted-foreground">{t("settings_ui.loaded_label")} </span>
|
|
194
203
|
{probeResult.loaded.map((s) => (
|
|
195
204
|
<Badge key={s} tone="success" className="mr-1">{s}</Badge>
|
|
196
205
|
))}
|
|
@@ -199,7 +208,7 @@ export function SkillsInspectorPanel() {
|
|
|
199
208
|
|
|
200
209
|
{probeResult.hinted?.length ? (
|
|
201
210
|
<div className="mb-1">
|
|
202
|
-
<span className="text-muted-foreground">
|
|
211
|
+
<span className="text-muted-foreground">{t("settings_ui.suggested_label")} </span>
|
|
203
212
|
{probeResult.hinted.map((s) => (
|
|
204
213
|
<Badge key={s} tone="info" className="mr-1">{s}</Badge>
|
|
205
214
|
))}
|
|
@@ -53,20 +53,20 @@ export function SuperAgentPanel() {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
-
<Section title={t("settings.super_agent.title")} description="
|
|
56
|
+
<Section title={t("settings.super_agent.title")} description={t("settings.super_agent.behavior_subtitle")}>
|
|
57
57
|
<div className="space-y-4">
|
|
58
58
|
<div className="flex items-center gap-3">
|
|
59
|
-
<Switch checked={enabled} onChange={setEnabled} label="
|
|
59
|
+
<Switch checked={enabled} onChange={setEnabled} label={t("settings.super_agent.enabled_label")} />
|
|
60
60
|
</div>
|
|
61
61
|
|
|
62
62
|
{/* Model lives in the Router now — single source of truth. */}
|
|
63
63
|
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/20 p-3">
|
|
64
64
|
<div className="min-w-0">
|
|
65
|
-
<div className="text-sm font-medium">
|
|
65
|
+
<div className="text-sm font-medium">{t("settings.super_agent.model_active")}</div>
|
|
66
66
|
<div className="truncate font-mono text-xs text-muted-fg">{superAgent.model || "—"}</div>
|
|
67
67
|
</div>
|
|
68
68
|
<Button size="sm" variant="secondary" onClick={() => navigate("/p/0/models")}>
|
|
69
|
-
<Cpu size={13} />
|
|
69
|
+
<Cpu size={13} /> {t("settings.super_agent.model_configure")}
|
|
70
70
|
</Button>
|
|
71
71
|
</div>
|
|
72
72
|
|
|
@@ -82,7 +82,7 @@ export function SuperAgentPanel() {
|
|
|
82
82
|
className="font-mono text-xs"
|
|
83
83
|
value={system}
|
|
84
84
|
onChange={(e) => setSystem(e.target.value)}
|
|
85
|
-
placeholder="
|
|
85
|
+
placeholder={t("settings.super_agent.system_ph")}
|
|
86
86
|
/>
|
|
87
87
|
</Field>
|
|
88
88
|
|
|
@@ -49,7 +49,7 @@ export function TelegramChannelsPanel() {
|
|
|
49
49
|
<ul className="space-y-2 text-sm">
|
|
50
50
|
{channels.map((c) => {
|
|
51
51
|
const ownerLabel = c.owner_user_id != null
|
|
52
|
-
? (nameByUserId.get(String(c.owner_user_id)) ||
|
|
52
|
+
? (nameByUserId.get(String(c.owner_user_id)) || t("telegram_ui.user_id_fallback", { id: c.owner_user_id }))
|
|
53
53
|
: t("telegram_channels.no_owner");
|
|
54
54
|
return (
|
|
55
55
|
<li key={c.name} className="rounded-md border border-border bg-muted/30 px-3 py-2">
|
|
@@ -67,8 +67,8 @@ export function TelegramChannelsPanel() {
|
|
|
67
67
|
<div className="mt-1 grid grid-cols-2 gap-2 text-xs text-muted-fg">
|
|
68
68
|
<span>chat_id: {c.chat_id || "—"}</span>
|
|
69
69
|
<span>bot_token: {c.bot_token ? `…${secretSuffix(c.bot_token) ?? ""}` : "—"}</span>
|
|
70
|
-
<span>route_to_agent: {c.route_to_agent || "
|
|
71
|
-
<span>engine: {c.respond_with_engine ? "
|
|
70
|
+
<span>route_to_agent: {c.route_to_agent || t("telegram_ui.default_apx")}</span>
|
|
71
|
+
<span>engine: {c.respond_with_engine ? t("telegram_ui.yes") : t("telegram_ui.no")}</span>
|
|
72
72
|
<span className="col-span-2">{t("telegram_channels.owner_label")} {ownerLabel}</span>
|
|
73
73
|
</div>
|
|
74
74
|
</li>
|
|
@@ -28,7 +28,7 @@ export function TelegramContactsPanel({ bare = false }: Props) {
|
|
|
28
28
|
const setRole = async (c: TelegramContact, role: string) => {
|
|
29
29
|
try {
|
|
30
30
|
await Telegram.contacts.patch(c.user_id, { role });
|
|
31
|
-
toast.success(
|
|
31
|
+
toast.success(t("telegram_ui.role_assigned", { name: c.name || c.user_id, role }));
|
|
32
32
|
mutate();
|
|
33
33
|
} catch (e) { toast.error((e as Error).message); }
|
|
34
34
|
};
|
|
@@ -76,14 +76,14 @@ export function TelegramGlobalPanel() {
|
|
|
76
76
|
<Field
|
|
77
77
|
label={t("settings.telegram_global.bot_token")}
|
|
78
78
|
hint={channel?.bot_token
|
|
79
|
-
? `…${secretSuffix(channel.bot_token) ?? ""} (
|
|
80
|
-
: "
|
|
79
|
+
? `…${secretSuffix(channel.bot_token) ?? ""} ${t("telegram_ui.secret_set_replace")}`
|
|
80
|
+
: t("telegram_ui.bot_token_hint_short")}
|
|
81
81
|
>
|
|
82
82
|
<Input
|
|
83
83
|
type="password"
|
|
84
84
|
value={botToken}
|
|
85
85
|
onChange={(e) => setBotToken(e.target.value)}
|
|
86
|
-
placeholder={channel?.bot_token ? `…${secretSuffix(channel.bot_token) ?? ""} (
|
|
86
|
+
placeholder={channel?.bot_token ? `…${secretSuffix(channel.bot_token) ?? ""} ${t("telegram_ui.secret_already_set")}` : ""}
|
|
87
87
|
/>
|
|
88
88
|
</Field>
|
|
89
89
|
<Field label={t("settings.telegram_global.chat_id")}>
|
|
@@ -71,7 +71,7 @@ export function TelegramRolesPanel() {
|
|
|
71
71
|
<Button size="sm" variant="destructive" onClick={() => remove(n)}>{t("telegram_roles.delete_btn")}</Button>
|
|
72
72
|
)}
|
|
73
73
|
</div>
|
|
74
|
-
<div className="mt-1 text-xs text-muted-fg">
|
|
74
|
+
<div className="mt-1 text-xs text-muted-fg">{t("telegram_contacts.tools_label")} {toolsLabel}</div>
|
|
75
75
|
</li>
|
|
76
76
|
);
|
|
77
77
|
})}
|
|
@@ -55,7 +55,7 @@ export function ProviderCard({
|
|
|
55
55
|
)}
|
|
56
56
|
>
|
|
57
57
|
<span className={cn("size-1.5 rounded-full", active ? "bg-emerald-400" : "bg-muted-fg/40")} />
|
|
58
|
-
{active ? "
|
|
58
|
+
{active ? t("providers_card.active") : t("providers_card.off")}
|
|
59
59
|
</button>
|
|
60
60
|
</Tip>
|
|
61
61
|
<Tip content={t("providers_modal.delete")}>
|
|
@@ -72,14 +72,14 @@ export function ProviderCard({
|
|
|
72
72
|
|
|
73
73
|
{/* Body */}
|
|
74
74
|
<div className="mt-auto space-y-1 text-xs">
|
|
75
|
-
<Row label="
|
|
76
|
-
{provider.base_url && <Row label="
|
|
77
|
-
<Row label="
|
|
75
|
+
<Row label={t("providers_card.model")} value={provider.default_model || "—"} mono />
|
|
76
|
+
{provider.base_url && <Row label={t("providers_card.base_url")} value={provider.base_url} mono truncate />}
|
|
77
|
+
<Row label={t("providers_card.api_key")} value={hasKey ? (keySuffix ? `…${keySuffix}` : t("providers_card.key_set")) : "—"} mono={!!keySuffix} />
|
|
78
78
|
{provider.default_temperature !== undefined && (
|
|
79
|
-
<Row label="
|
|
79
|
+
<Row label={t("providers_card.temp")} value={provider.default_temperature.toFixed(1)} />
|
|
80
80
|
)}
|
|
81
81
|
{provider.pricing?.input_per_million !== undefined && (
|
|
82
|
-
<Row label="
|
|
82
|
+
<Row label={t("providers_card.price_io")} value={`${provider.pricing.input_per_million ?? 0} / ${provider.pricing.output_per_million ?? 0}`} />
|
|
83
83
|
)}
|
|
84
84
|
</div>
|
|
85
85
|
</div>
|
|
@@ -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>
|