@agentprojectcontext/apx 1.42.1 → 1.43.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/package.json +1 -1
- package/src/core/channels/telegram/api.js +62 -0
- package/src/core/channels/telegram/ask-callbacks.js +238 -0
- package/src/core/config/index.js +2 -0
- package/src/core/config/redact.js +2 -0
- package/src/core/confirmation/adapters/telegram.js +20 -37
- package/src/core/desktop/process.js +126 -0
- package/src/core/voice/stt-hardware.js +87 -0
- package/src/core/voice/stt-models.js +97 -0
- package/src/core/voice/transcription.js +147 -16
- package/src/host/daemon/api/desktop.js +54 -8
- package/src/host/daemon/api/transcribe.js +40 -1
- package/src/host/daemon/plugins/desktop/index.js +6 -1
- package/src/host/daemon/plugins/telegram/index.js +61 -351
- package/src/host/daemon/whisper-server.js +18 -8
- package/src/host/daemon/whisper-server.py +71 -44
- package/src/interfaces/cli/commands/desktop.js +13 -68
- package/src/interfaces/desktop/main.js +32 -4
- package/src/interfaces/desktop/renderer.js +26 -5
- package/src/interfaces/web/dist/assets/index-B0nTYflm.js +651 -0
- package/src/interfaces/web/dist/assets/index-B0nTYflm.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-C22PmKCD.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/components/ShortcutInput.tsx +156 -0
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +101 -5
- package/src/interfaces/web/src/i18n/en.ts +28 -2
- package/src/interfaces/web/src/i18n/es.ts +28 -2
- package/src/interfaces/web/src/lib/api/desktop.ts +28 -0
- package/src/interfaces/web/src/lib/api/voice.ts +26 -2
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +55 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +98 -36
- package/src/interfaces/web/dist/assets/index-BReF4_xV.js +0 -646
- package/src/interfaces/web/dist/assets/index-BReF4_xV.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-wrEbTJbc.css +0 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { Field } from "../ui";
|
|
1
|
+
import { Field, Input } from "../ui";
|
|
2
2
|
import { UiSelect } from "../UiSelect";
|
|
3
3
|
import { WHISPER_MODELS, type TranscriptionConfig } from "../../lib/api/voice";
|
|
4
|
+
import { isSecretMarker, secretSuffix } from "../../lib/secrets";
|
|
4
5
|
import { t } from "../../i18n";
|
|
5
6
|
|
|
6
7
|
// STT (speech-to-text) configuration. Persisted under config.transcription.
|
|
7
|
-
// The actual capture happens in the
|
|
8
|
-
// owner
|
|
8
|
+
// The actual capture happens in the desktop window / Telegram / CLI; here the
|
|
9
|
+
// owner picks the engine and configures it:
|
|
10
|
+
// local — embedded faster-whisper (offline; model + language)
|
|
11
|
+
// openai — OpenAI cloud Whisper (api_key + model)
|
|
12
|
+
// custom — any OpenAI-compatible server: mlx-audio on this Mac's Metal GPU,
|
|
13
|
+
// a Radeon/NVIDIA box on the LAN, or a remote endpoint (base_url).
|
|
9
14
|
|
|
10
15
|
interface Props {
|
|
11
16
|
config: TranscriptionConfig;
|
|
@@ -17,6 +22,7 @@ const providerOptions = () => [
|
|
|
17
22
|
{ value: "auto", label: t("voice_ui.stt_provider_auto") },
|
|
18
23
|
{ value: "local", label: t("voice_ui.stt_provider_local") },
|
|
19
24
|
{ value: "openai", label: t("voice_ui.stt_provider_openai") },
|
|
25
|
+
{ value: "custom", label: t("voice_ui.stt_provider_custom") },
|
|
20
26
|
];
|
|
21
27
|
|
|
22
28
|
const langOptions = () => [
|
|
@@ -32,9 +38,30 @@ const langOptions = () => [
|
|
|
32
38
|
export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
33
39
|
const provider = config.provider || "auto";
|
|
34
40
|
const local = config.local || {};
|
|
41
|
+
const openai = config.openai || {};
|
|
42
|
+
const custom = config.custom || {};
|
|
35
43
|
const model = local.model || "small";
|
|
36
44
|
const language = local.language || "auto";
|
|
37
|
-
|
|
45
|
+
// "auto" tries local first, so its tuning shares the local block.
|
|
46
|
+
const showLocal = provider === "auto" || provider === "local";
|
|
47
|
+
|
|
48
|
+
// Text fields patch on blur (not every keystroke), and only when changed.
|
|
49
|
+
const patchText = (key: string, prev: string | undefined, value: string) => {
|
|
50
|
+
const next = value.trim();
|
|
51
|
+
if (next === (prev || "").trim()) return;
|
|
52
|
+
onPatch({ [key]: next });
|
|
53
|
+
};
|
|
54
|
+
// Secrets: a blank field keeps the stored key; the daemon ignores redacted
|
|
55
|
+
// "*** set ***" markers, so we never echo one back as a real value.
|
|
56
|
+
const patchKey = (key: string, value: string) => {
|
|
57
|
+
const next = value.trim();
|
|
58
|
+
if (!next || isSecretMarker(next)) return;
|
|
59
|
+
onPatch({ [key]: next });
|
|
60
|
+
};
|
|
61
|
+
const keyPlaceholder = (marker: unknown) =>
|
|
62
|
+
isSecretMarker(marker)
|
|
63
|
+
? t("voice_ui.api_key_set", { suffix: secretSuffix(marker) ?? "" })
|
|
64
|
+
: t("voice_ui.api_key_label");
|
|
38
65
|
|
|
39
66
|
return (
|
|
40
67
|
<div className="space-y-3">
|
|
@@ -48,7 +75,7 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
48
75
|
/>
|
|
49
76
|
</Field>
|
|
50
77
|
|
|
51
|
-
{
|
|
78
|
+
{showLocal && (
|
|
52
79
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
53
80
|
<Field label={t("voice_ui.stt_model_label")} hint={t("voice_ui.stt_model_hint")}>
|
|
54
81
|
<UiSelect
|
|
@@ -68,6 +95,75 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
68
95
|
</Field>
|
|
69
96
|
</div>
|
|
70
97
|
)}
|
|
98
|
+
|
|
99
|
+
{provider === "openai" && (
|
|
100
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
101
|
+
<Field
|
|
102
|
+
label={t("voice_ui.api_key_label")}
|
|
103
|
+
hint={isSecretMarker(openai.api_key)
|
|
104
|
+
? t("voice_ui.api_key_keep_hint")
|
|
105
|
+
: t("voice_ui.api_key_reuse_hint", { engine: "engines.openai.api_key", env: "OPENAI_API_KEY" })}
|
|
106
|
+
>
|
|
107
|
+
<Input
|
|
108
|
+
type="password"
|
|
109
|
+
autoComplete="new-password"
|
|
110
|
+
defaultValue=""
|
|
111
|
+
placeholder={keyPlaceholder(openai.api_key)}
|
|
112
|
+
onBlur={(e) => patchKey("transcription.openai.api_key", e.target.value)}
|
|
113
|
+
disabled={busy}
|
|
114
|
+
/>
|
|
115
|
+
</Field>
|
|
116
|
+
<Field label={t("voice_ui.stt_openai_model_label")} hint={t("voice_ui.stt_openai_model_hint")}>
|
|
117
|
+
<Input
|
|
118
|
+
defaultValue={openai.model || ""}
|
|
119
|
+
placeholder="whisper-1"
|
|
120
|
+
onBlur={(e) => patchText("transcription.openai.model", openai.model, e.target.value)}
|
|
121
|
+
disabled={busy}
|
|
122
|
+
/>
|
|
123
|
+
</Field>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{provider === "custom" && (
|
|
128
|
+
<div className="space-y-3">
|
|
129
|
+
<Field label={t("voice_ui.stt_custom_baseurl_label")} hint={t("voice_ui.stt_custom_baseurl_hint")}>
|
|
130
|
+
<Input
|
|
131
|
+
defaultValue={custom.base_url || ""}
|
|
132
|
+
placeholder="http://localhost:8000/v1"
|
|
133
|
+
onBlur={(e) => patchText("transcription.custom.base_url", custom.base_url, e.target.value)}
|
|
134
|
+
disabled={busy}
|
|
135
|
+
/>
|
|
136
|
+
</Field>
|
|
137
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
138
|
+
<Field label={t("voice_ui.stt_custom_model_label")} hint={t("voice_ui.stt_custom_model_hint")}>
|
|
139
|
+
<Input
|
|
140
|
+
defaultValue={custom.model || ""}
|
|
141
|
+
placeholder="mlx-community/whisper-large-v3-turbo"
|
|
142
|
+
onBlur={(e) => patchText("transcription.custom.model", custom.model, e.target.value)}
|
|
143
|
+
disabled={busy}
|
|
144
|
+
/>
|
|
145
|
+
</Field>
|
|
146
|
+
<Field label={t("voice_ui.stt_language_label")} hint={t("voice_ui.stt_language_hint")}>
|
|
147
|
+
<UiSelect
|
|
148
|
+
value={custom.language || "auto"}
|
|
149
|
+
onChange={(v) => onPatch({ "transcription.custom.language": v })}
|
|
150
|
+
options={langOptions()}
|
|
151
|
+
disabled={busy}
|
|
152
|
+
/>
|
|
153
|
+
</Field>
|
|
154
|
+
</div>
|
|
155
|
+
<Field label={t("voice_ui.api_key_label")} hint={t("voice_ui.stt_custom_key_hint")}>
|
|
156
|
+
<Input
|
|
157
|
+
type="password"
|
|
158
|
+
autoComplete="new-password"
|
|
159
|
+
defaultValue=""
|
|
160
|
+
placeholder={keyPlaceholder(custom.api_key)}
|
|
161
|
+
onBlur={(e) => patchKey("transcription.custom.api_key", e.target.value)}
|
|
162
|
+
disabled={busy}
|
|
163
|
+
/>
|
|
164
|
+
</Field>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
71
167
|
</div>
|
|
72
168
|
);
|
|
73
169
|
}
|
|
@@ -940,6 +940,9 @@ export const en = {
|
|
|
940
940
|
reload_manifest: "Reload manifest",
|
|
941
941
|
widget_native: "Native APX widget",
|
|
942
942
|
widget_external: "External widget",
|
|
943
|
+
preview_badge: "Preview",
|
|
944
|
+
preview_title: "Deck — Coming soon",
|
|
945
|
+
preview_body: "The Deck module is still in development and not released yet. We'll re-enable it once Deck ships in a stable release. For now everything here is read-only and no changes will be saved.",
|
|
943
946
|
},
|
|
944
947
|
|
|
945
948
|
memory_panel: {
|
|
@@ -1150,9 +1153,17 @@ export const en = {
|
|
|
1150
1153
|
stt_model_hint: "Bigger = more accurate and slower.",
|
|
1151
1154
|
stt_language_label: "Language",
|
|
1152
1155
|
stt_language_hint: "For Spanish, setting \"Spanish\" improves accuracy.",
|
|
1153
|
-
stt_provider_auto: "Automatic (local, then
|
|
1156
|
+
stt_provider_auto: "Automatic (local, then remote)",
|
|
1154
1157
|
stt_provider_local: "Local — faster-whisper (offline)",
|
|
1155
1158
|
stt_provider_openai: "OpenAI — Whisper-1 (cloud)",
|
|
1159
|
+
stt_provider_custom: "Custom — OpenAI-compatible server",
|
|
1160
|
+
stt_openai_model_label: "OpenAI model",
|
|
1161
|
+
stt_openai_model_hint: "Defaults to whisper-1.",
|
|
1162
|
+
stt_custom_baseurl_label: "Base URL (OpenAI-compatible)",
|
|
1163
|
+
stt_custom_baseurl_hint: "e.g. http://localhost:8000/v1 (mlx-audio on Metal) or http://192.168.1.50:9000/v1 (Radeon/NVIDIA on the LAN).",
|
|
1164
|
+
stt_custom_model_label: "Model",
|
|
1165
|
+
stt_custom_model_hint: "e.g. mlx-community/whisper-large-v3-turbo or large-v3.",
|
|
1166
|
+
stt_custom_key_hint: "Optional — most local servers need no key.",
|
|
1156
1167
|
lang_auto: "Auto-detect",
|
|
1157
1168
|
lang_es: "Spanish",
|
|
1158
1169
|
lang_en: "English",
|
|
@@ -1302,18 +1313,33 @@ export const en = {
|
|
|
1302
1313
|
desktop_pos_left: "Left",
|
|
1303
1314
|
desktop_pos_center: "Center",
|
|
1304
1315
|
desktop_pos_right: "Right",
|
|
1316
|
+
desktop_theme_system: "System",
|
|
1305
1317
|
desktop_theme_light: "Light",
|
|
1306
1318
|
desktop_theme_dark: "Dark",
|
|
1307
1319
|
desktop_status_desc: "The window launches from the terminal or via autostart.",
|
|
1308
1320
|
desktop_running: "Running",
|
|
1309
1321
|
desktop_stopped: "Stopped",
|
|
1310
1322
|
desktop_refresh: "refresh",
|
|
1323
|
+
desktop_start: "Start",
|
|
1324
|
+
desktop_stop: "Stop",
|
|
1325
|
+
desktop_restart: "Restart",
|
|
1326
|
+
desktop_restart_hint: "Reload the open window so config changes (theme, position) apply now.",
|
|
1327
|
+
desktop_restart_done: "Restarting the window — applying the latest config.",
|
|
1328
|
+
desktop_restart_none: "No desktop window is connected.",
|
|
1329
|
+
desktop_start_done: "Desktop window launched.",
|
|
1330
|
+
desktop_start_already: "Desktop window is already running.",
|
|
1331
|
+
desktop_stop_done: "Desktop window stopped.",
|
|
1332
|
+
desktop_stop_none: "No desktop window was running.",
|
|
1311
1333
|
desktop_from_terminal: "From terminal:",
|
|
1312
1334
|
desktop_autostart_desc: "Launches the window at user login. Equivalent to `apx desktop install` (no sudo required).",
|
|
1313
1335
|
desktop_platform: "platform: {platform}",
|
|
1314
1336
|
desktop_shortcut_desc: "Global hotkey that shows/hides the window and starts listening.",
|
|
1315
1337
|
desktop_accelerator: "Accelerator",
|
|
1316
|
-
desktop_accelerator_hint:"
|
|
1338
|
+
desktop_accelerator_hint:"Click the field and press your key combo. Restart the window to apply.",
|
|
1339
|
+
desktop_shortcut_record: "Click to set a shortcut",
|
|
1340
|
+
desktop_shortcut_recording: "Press your combo…",
|
|
1341
|
+
desktop_shortcut_change: "click to change",
|
|
1342
|
+
desktop_shortcut_esc: "Esc to cancel",
|
|
1317
1343
|
desktop_shortcut_saved: "Shortcut saved. Restart the window (apx desktop stop && start) to apply it.",
|
|
1318
1344
|
desktop_autostart_on: "Autostart enabled for the next login.",
|
|
1319
1345
|
desktop_autostart_off: "Autostart disabled.",
|
|
@@ -938,6 +938,9 @@ export const es = {
|
|
|
938
938
|
reload_manifest: "Recargar manifest",
|
|
939
939
|
widget_native: "Widget nativo APX",
|
|
940
940
|
widget_external: "Widget externo",
|
|
941
|
+
preview_badge: "Vista previa",
|
|
942
|
+
preview_title: "Deck — Próximamente",
|
|
943
|
+
preview_body: "El módulo Deck todavía está en desarrollo y no fue lanzado aún. Lo volveremos a activar cuando Deck salga en una versión estable. Por ahora todo acá es de solo lectura y no se guardará ningún cambio.",
|
|
941
944
|
},
|
|
942
945
|
|
|
943
946
|
memory_panel: {
|
|
@@ -1148,9 +1151,17 @@ export const es = {
|
|
|
1148
1151
|
stt_model_hint: "Más grande = más preciso y más lento.",
|
|
1149
1152
|
stt_language_label: "Idioma",
|
|
1150
1153
|
stt_language_hint: "Para español, elegir \"Español\" mejora la precisión.",
|
|
1151
|
-
stt_provider_auto: "Automático (local, después
|
|
1154
|
+
stt_provider_auto: "Automático (local, después remoto)",
|
|
1152
1155
|
stt_provider_local: "Local — faster-whisper (offline)",
|
|
1153
1156
|
stt_provider_openai: "OpenAI — Whisper-1 (cloud)",
|
|
1157
|
+
stt_provider_custom: "Custom — server OpenAI-compatible",
|
|
1158
|
+
stt_openai_model_label: "Modelo OpenAI",
|
|
1159
|
+
stt_openai_model_hint: "Por defecto whisper-1.",
|
|
1160
|
+
stt_custom_baseurl_label: "URL base (OpenAI-compatible)",
|
|
1161
|
+
stt_custom_baseurl_hint: "Ej: http://localhost:8000/v1 (mlx-audio en Metal) o http://192.168.1.50:9000/v1 (Radeon/NVIDIA en la red).",
|
|
1162
|
+
stt_custom_model_label: "Modelo",
|
|
1163
|
+
stt_custom_model_hint: "Ej: mlx-community/whisper-large-v3-turbo o large-v3.",
|
|
1164
|
+
stt_custom_key_hint: "Opcional — la mayoría de los servers locales no requieren key.",
|
|
1154
1165
|
lang_auto: "Detección automática",
|
|
1155
1166
|
lang_es: "Español",
|
|
1156
1167
|
lang_en: "Inglés",
|
|
@@ -1300,18 +1311,33 @@ export const es = {
|
|
|
1300
1311
|
desktop_pos_left: "Izquierda",
|
|
1301
1312
|
desktop_pos_center: "Centro",
|
|
1302
1313
|
desktop_pos_right: "Derecha",
|
|
1314
|
+
desktop_theme_system: "Sistema",
|
|
1303
1315
|
desktop_theme_light: "Claro",
|
|
1304
1316
|
desktop_theme_dark: "Oscuro",
|
|
1305
1317
|
desktop_status_desc: "La ventana se abre desde la terminal o por arranque automático.",
|
|
1306
1318
|
desktop_running: "Corriendo",
|
|
1307
1319
|
desktop_stopped: "Detenida",
|
|
1308
1320
|
desktop_refresh: "refrescar",
|
|
1321
|
+
desktop_start: "Iniciar",
|
|
1322
|
+
desktop_stop: "Detener",
|
|
1323
|
+
desktop_restart: "Reiniciar",
|
|
1324
|
+
desktop_restart_hint: "Recarga la ventana abierta para aplicar ya los cambios de config (tema, posición).",
|
|
1325
|
+
desktop_restart_done: "Reiniciando la ventana — aplicando la última config.",
|
|
1326
|
+
desktop_restart_none: "No hay ninguna ventana de desktop conectada.",
|
|
1327
|
+
desktop_start_done: "Ventana de desktop iniciada.",
|
|
1328
|
+
desktop_start_already: "La ventana de desktop ya estaba corriendo.",
|
|
1329
|
+
desktop_stop_done: "Ventana de desktop detenida.",
|
|
1330
|
+
desktop_stop_none: "No había ninguna ventana de desktop corriendo.",
|
|
1309
1331
|
desktop_from_terminal: "Desde la terminal:",
|
|
1310
1332
|
desktop_autostart_desc: "Abre la ventana al iniciar sesión. Equivale a `apx desktop install` (no requiere sudo).",
|
|
1311
1333
|
desktop_platform: "plataforma: {platform}",
|
|
1312
1334
|
desktop_shortcut_desc: "Atajo global que muestra/oculta la ventana y empieza a escuchar.",
|
|
1313
1335
|
desktop_accelerator: "Acelerador",
|
|
1314
|
-
desktop_accelerator_hint:"
|
|
1336
|
+
desktop_accelerator_hint:"Hacé clic en el campo y apretá tu combinación de teclas. Reiniciá la ventana para aplicar.",
|
|
1337
|
+
desktop_shortcut_record: "Hacé clic para definir un atajo",
|
|
1338
|
+
desktop_shortcut_recording: "Apretá tu combinación…",
|
|
1339
|
+
desktop_shortcut_change: "clic para cambiar",
|
|
1340
|
+
desktop_shortcut_esc: "Esc para cancelar",
|
|
1315
1341
|
desktop_shortcut_saved: "Atajo guardado. Reiniciá la ventana (apx desktop stop && start) para aplicarlo.",
|
|
1316
1342
|
desktop_autostart_on: "Arranque automático habilitado para el próximo inicio de sesión.",
|
|
1317
1343
|
desktop_autostart_off: "Arranque automático deshabilitado.",
|
|
@@ -16,6 +16,25 @@ export interface DesktopStatus {
|
|
|
16
16
|
running: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface DesktopRestartResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** How many connected windows were told to reload (0 = none connected). */
|
|
22
|
+
reloaded: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DesktopStartResult {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
pid?: number;
|
|
28
|
+
/** True when a window was already running (start was a no-op). */
|
|
29
|
+
already?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DesktopStopResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
/** False when nothing was running. */
|
|
35
|
+
stopped?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export interface AutostartStatus {
|
|
20
39
|
ok: boolean;
|
|
21
40
|
enabled: boolean;
|
|
@@ -26,6 +45,15 @@ export const Desktop = {
|
|
|
26
45
|
/** GET /desktop/status — connected window count + running flag (live probe). */
|
|
27
46
|
status: () => http.get<DesktopStatus>("/desktop/status"),
|
|
28
47
|
|
|
48
|
+
/** POST /desktop/start — launch the floating window (detached Electron). */
|
|
49
|
+
start: () => http.post<DesktopStartResult>("/desktop/start", {}),
|
|
50
|
+
|
|
51
|
+
/** POST /desktop/stop — terminate the running window. */
|
|
52
|
+
stop: () => http.post<DesktopStopResult>("/desktop/stop", {}),
|
|
53
|
+
|
|
54
|
+
/** POST /desktop/restart — tell every live window to reload + re-read config. */
|
|
55
|
+
restart: () => http.post<DesktopRestartResult>("/desktop/restart", {}),
|
|
56
|
+
|
|
29
57
|
/** GET /desktop/autostart — current login-item state for this platform. */
|
|
30
58
|
autostartGet: () => http.get<AutostartStatus>("/desktop/autostart"),
|
|
31
59
|
|
|
@@ -99,9 +99,33 @@ export interface TranscriptionLocalConfig {
|
|
|
99
99
|
beam_size?: number;
|
|
100
100
|
idle_minutes?: number;
|
|
101
101
|
}
|
|
102
|
+
export interface TranscriptionOpenAIConfig {
|
|
103
|
+
base_url?: string; // defaults to https://api.openai.com/v1
|
|
104
|
+
api_key?: string; // may carry a redacted "*** set ***" marker
|
|
105
|
+
model?: string; // defaults to whisper-1
|
|
106
|
+
}
|
|
107
|
+
export interface TranscriptionCustomConfig {
|
|
108
|
+
base_url?: string; // OpenAI-compatible server, e.g. http://localhost:8000/v1
|
|
109
|
+
api_key?: string; // optional; may carry a redacted marker
|
|
110
|
+
model?: string; // e.g. mlx-community/whisper-large-v3-turbo
|
|
111
|
+
language?: string; // ISO code or "auto"
|
|
112
|
+
}
|
|
102
113
|
export interface TranscriptionConfig {
|
|
103
|
-
provider?: string; // "auto" | "local" | "openai"
|
|
114
|
+
provider?: string; // "auto" | "local" | "openai" | "custom"
|
|
104
115
|
local?: TranscriptionLocalConfig;
|
|
116
|
+
openai?: TranscriptionOpenAIConfig;
|
|
117
|
+
custom?: TranscriptionCustomConfig;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One STT engine entry as reported by GET /transcribe/providers. */
|
|
121
|
+
export interface SttProviderEntry {
|
|
122
|
+
id: string; // "local" | "openai" | "custom"
|
|
123
|
+
available: boolean;
|
|
124
|
+
configured: boolean;
|
|
125
|
+
}
|
|
126
|
+
export interface SttProvidersResponse {
|
|
127
|
+
configured_provider: string;
|
|
128
|
+
engines: SttProviderEntry[];
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
// Known engine voice presets used to fill selects without a daemon round-trip.
|
|
@@ -109,7 +133,7 @@ export const OPENAI_TTS_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shi
|
|
|
109
133
|
export const GEMINI_TTS_VOICES = ["Kore", "Puck", "Charon", "Fenrir", "Aoede"];
|
|
110
134
|
export const ELEVENLABS_MODELS = ["eleven_multilingual_v2", "eleven_turbo_v2_5", "eleven_flash_v2_5"];
|
|
111
135
|
export const OPENAI_TTS_MODELS = ["tts-1", "tts-1-hd"];
|
|
112
|
-
export const WHISPER_MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3"];
|
|
136
|
+
export const WHISPER_MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3", "large-v3-turbo"];
|
|
113
137
|
|
|
114
138
|
// Friendly labels + ordering for the provider list. The daemon is the source
|
|
115
139
|
// of truth for availability; this only adds display names + a stable order.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import useSWR from "swr";
|
|
2
|
-
import { RefreshCw } from "lucide-react";
|
|
2
|
+
import { RefreshCw, Construction } from "lucide-react";
|
|
3
3
|
import { Deck } from "../../lib/api/deck";
|
|
4
4
|
import { Section } from "../../components/Section";
|
|
5
5
|
import { Button, Empty, Loading } from "../../components/ui";
|
|
@@ -21,7 +21,17 @@ export function DeckScreen() {
|
|
|
21
21
|
{ refreshInterval: 30_000 }
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
+
// ── Deck is NOT released yet ───────────────────────────────────────────
|
|
25
|
+
// The whole module is shown behind a non-dismissable "coming soon" modal
|
|
26
|
+
// (see the overlay at the bottom of this component) and the UI underneath is
|
|
27
|
+
// made inert. As defense-in-depth, the real widget mutation is disabled here
|
|
28
|
+
// too: if someone forces the modal out of the DOM, toggling a widget does
|
|
29
|
+
// NOTHING — no request ever hits the daemon, so nothing breaks.
|
|
30
|
+
// 👉 Re-enable the body below (remove this guard) when Deck ships in a stable
|
|
31
|
+
// release. The original implementation is kept intact, just unreachable.
|
|
24
32
|
const handleToggle = async (widgetId: string, enabled: boolean) => {
|
|
33
|
+
return; // no-op while Deck is pre-release — see note above.
|
|
34
|
+
// eslint-disable-next-line no-unreachable
|
|
25
35
|
try {
|
|
26
36
|
await Deck.setWidget(widgetId, { enabled });
|
|
27
37
|
// Optimistically update local data so the switch flips immediately.
|
|
@@ -58,7 +68,11 @@ export function DeckScreen() {
|
|
|
58
68
|
// Re-validate after a short delay to get the server's persisted state.
|
|
59
69
|
setTimeout(() => mutate(), 800);
|
|
60
70
|
} catch (e: unknown) {
|
|
61
|
-
|
|
71
|
+
// `e` reads as `unknown` here because the early `return` above makes this
|
|
72
|
+
// catch unreachable while Deck is pre-release — TS skips control-flow
|
|
73
|
+
// narrowing in dead code. Cast so the kept-for-later body still
|
|
74
|
+
// typechecks; behaviour is unchanged (this block never runs today).
|
|
75
|
+
const msg = e instanceof Error ? (e as Error).message : t("modules_ui.deck_save_error");
|
|
62
76
|
toast.error(msg);
|
|
63
77
|
}
|
|
64
78
|
};
|
|
@@ -77,7 +91,18 @@ export function DeckScreen() {
|
|
|
77
91
|
const enabledCount = externalWidgets.filter((w) => w.user_enabled === true).length;
|
|
78
92
|
|
|
79
93
|
return (
|
|
80
|
-
<div className="
|
|
94
|
+
<div className="relative min-h-full" data-testid="screen-deck">
|
|
95
|
+
{/* ── Underlying Deck UI ──────────────────────────────────────────────
|
|
96
|
+
Shown for context only while Deck is pre-release. It is made fully
|
|
97
|
+
inert (no focus, no pointer events) and blurred behind the overlay
|
|
98
|
+
below. All real actions are also neutralized in code (see
|
|
99
|
+
handleToggle), so nothing runs even if this layer is re-enabled by
|
|
100
|
+
hand. Remove the `inert`/blur wrapper when Deck ships. */}
|
|
101
|
+
<div
|
|
102
|
+
className="mx-auto max-w-4xl space-y-6 p-6 pointer-events-none select-none blur-[2px] opacity-60"
|
|
103
|
+
aria-hidden
|
|
104
|
+
inert
|
|
105
|
+
>
|
|
81
106
|
{/* Daemon info card */}
|
|
82
107
|
{data && <DaemonCard manifest={data} />}
|
|
83
108
|
|
|
@@ -160,5 +185,32 @@ export function DeckScreen() {
|
|
|
160
185
|
</Section>
|
|
161
186
|
)}
|
|
162
187
|
</div>
|
|
188
|
+
|
|
189
|
+
{/* ── "Coming soon" overlay ───────────────────────────────────────────
|
|
190
|
+
Non-dismissable: there is no close button and clicking the backdrop
|
|
191
|
+
does nothing. It floats over the inert Deck UI above. Remove this
|
|
192
|
+
whole block (and the inert wrapper / handleToggle guard) when Deck is
|
|
193
|
+
released. */}
|
|
194
|
+
<div
|
|
195
|
+
className="absolute inset-0 z-10 flex items-center justify-center p-6 backdrop-blur-[1px]"
|
|
196
|
+
role="dialog"
|
|
197
|
+
aria-modal="true"
|
|
198
|
+
aria-labelledby="deck-coming-soon-title"
|
|
199
|
+
data-testid="deck-coming-soon"
|
|
200
|
+
>
|
|
201
|
+
<div className="w-full max-w-md rounded-2xl border border-border bg-card/95 p-8 text-center shadow-2xl">
|
|
202
|
+
<div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-muted">
|
|
203
|
+
<Construction className="size-6 text-muted-fg" />
|
|
204
|
+
</div>
|
|
205
|
+
<span className="inline-block rounded-full bg-muted px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-fg">
|
|
206
|
+
{t("deck_screen.preview_badge")}
|
|
207
|
+
</span>
|
|
208
|
+
<h2 id="deck-coming-soon-title" className="mt-3 text-lg font-semibold">
|
|
209
|
+
{t("deck_screen.preview_title")}
|
|
210
|
+
</h2>
|
|
211
|
+
<p className="mt-2 text-sm text-muted-fg">{t("deck_screen.preview_body")}</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
163
215
|
);
|
|
164
216
|
}
|
|
@@ -2,8 +2,9 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import useSWR from "swr";
|
|
4
4
|
import { Section, Kbd, StatusDot } from "../../components/Section";
|
|
5
|
-
import { Button, Field,
|
|
5
|
+
import { Button, Field, Switch, Loading, Empty } from "../../components/ui";
|
|
6
6
|
import { UiSelect } from "../../components/UiSelect";
|
|
7
|
+
import { ShortcutInput } from "../../components/ShortcutInput";
|
|
7
8
|
import { useToast } from "../../components/Toast";
|
|
8
9
|
import { useGlobalConfig } from "../../hooks/useGlobalConfig";
|
|
9
10
|
import { Desktop, fetchDesktopMessages, type GlobalMessage } from "../../lib/api/desktop";
|
|
@@ -16,8 +17,9 @@ const positionOpts = () => [
|
|
|
16
17
|
{ value: "right", label: t("modules_ui.desktop_pos_right") },
|
|
17
18
|
];
|
|
18
19
|
const themeOpts = () => [
|
|
19
|
-
{ value: "
|
|
20
|
-
{ value: "
|
|
20
|
+
{ value: "system", label: t("modules_ui.desktop_theme_system") },
|
|
21
|
+
{ value: "light", label: t("modules_ui.desktop_theme_light") },
|
|
22
|
+
{ value: "dark", label: t("modules_ui.desktop_theme_dark") },
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
// Desktop module — manage the floating voice window (the Electron app launched
|
|
@@ -32,14 +34,16 @@ export function DesktopScreen() {
|
|
|
32
34
|
const cfgView = config as unknown as {
|
|
33
35
|
desktop?: {
|
|
34
36
|
shortcut?: string; enabled?: boolean;
|
|
35
|
-
theme?: "light" | "dark";
|
|
37
|
+
theme?: "light" | "dark" | "system";
|
|
36
38
|
position?: "left" | "center" | "right";
|
|
37
39
|
};
|
|
38
40
|
overlay?: { shortcut?: string }; // legacy fallback
|
|
39
41
|
};
|
|
40
42
|
const savedShortcut = cfgView.desktop?.shortcut || cfgView.overlay?.shortcut || DEFAULT_SHORTCUT;
|
|
41
43
|
const enabled = cfgView.desktop?.enabled !== false;
|
|
42
|
-
|
|
44
|
+
// Default to "system" so the window follows the OS appearance until the
|
|
45
|
+
// user explicitly pins light/dark.
|
|
46
|
+
const theme = cfgView.desktop?.theme || "system";
|
|
43
47
|
const position = cfgView.desktop?.position || "right";
|
|
44
48
|
|
|
45
49
|
const { data: status, isLoading: stLoading, mutate: mutateStatus } = useSWR(
|
|
@@ -63,8 +67,35 @@ export function DesktopScreen() {
|
|
|
63
67
|
const [shortcut, setShortcut] = useState(savedShortcut);
|
|
64
68
|
const [busy, setBusy] = useState(false);
|
|
65
69
|
const [autostartBusy, setAutostartBusy] = useState(false);
|
|
70
|
+
// Which lifecycle action (start/stop/restart) is in flight — drives the
|
|
71
|
+
// per-button spinner and disables its siblings while one runs.
|
|
72
|
+
const [lifeAction, setLifeAction] = useState<"start" | "stop" | "restart" | null>(null);
|
|
66
73
|
useEffect(() => setShortcut(savedShortcut), [savedShortcut]);
|
|
67
74
|
|
|
75
|
+
// Start/Stop launch or kill the Electron window (daemon spawns/SIGTERMs it);
|
|
76
|
+
// Restart tells a live window to reload + re-read config (theme, position,
|
|
77
|
+
// shortcut) — the "apply now" the static status-poll never did. All three
|
|
78
|
+
// re-poll status shortly after so the dot + buttons settle.
|
|
79
|
+
const runLifecycle = async (action: "start" | "stop" | "restart", fn: () => Promise<void>) => {
|
|
80
|
+
setLifeAction(action);
|
|
81
|
+
try { await fn(); }
|
|
82
|
+
catch (e) { toast.error((e as Error).message); }
|
|
83
|
+
finally { setLifeAction(null); setTimeout(() => mutateStatus(), 1200); }
|
|
84
|
+
};
|
|
85
|
+
const startDesktop = () => runLifecycle("start", async () => {
|
|
86
|
+
const r = await Desktop.start();
|
|
87
|
+
toast.success(r.already ? t("modules_ui.desktop_start_already") : t("modules_ui.desktop_start_done"));
|
|
88
|
+
});
|
|
89
|
+
const stopDesktop = () => runLifecycle("stop", async () => {
|
|
90
|
+
const r = await Desktop.stop();
|
|
91
|
+
toast.success(r.stopped ? t("modules_ui.desktop_stop_done") : t("modules_ui.desktop_stop_none"));
|
|
92
|
+
});
|
|
93
|
+
const restartDesktop = () => runLifecycle("restart", async () => {
|
|
94
|
+
const r = await Desktop.restart();
|
|
95
|
+
if (r.reloaded > 0) toast.success(t("modules_ui.desktop_restart_done"));
|
|
96
|
+
else toast.info(t("modules_ui.desktop_restart_none"));
|
|
97
|
+
});
|
|
98
|
+
|
|
68
99
|
const saveShortcut = async () => {
|
|
69
100
|
const next = shortcut.trim();
|
|
70
101
|
if (!next || next === savedShortcut) return;
|
|
@@ -101,23 +132,58 @@ export function DesktopScreen() {
|
|
|
101
132
|
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
|
102
133
|
{/* ── LEFT: configuration + status ─────────────────────────────── */}
|
|
103
134
|
<div className="space-y-6">
|
|
104
|
-
<Section
|
|
135
|
+
<Section
|
|
136
|
+
title={t("desktop_screen.status_title")}
|
|
137
|
+
description={t("modules_ui.desktop_status_desc")}
|
|
138
|
+
action={
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<Button
|
|
141
|
+
variant="primary"
|
|
142
|
+
size="sm"
|
|
143
|
+
onClick={startDesktop}
|
|
144
|
+
loading={lifeAction === "start"}
|
|
145
|
+
disabled={running || (lifeAction !== null && lifeAction !== "start")}
|
|
146
|
+
>
|
|
147
|
+
{t("modules_ui.desktop_start")}
|
|
148
|
+
</Button>
|
|
149
|
+
<Button
|
|
150
|
+
variant="secondary"
|
|
151
|
+
size="sm"
|
|
152
|
+
onClick={stopDesktop}
|
|
153
|
+
loading={lifeAction === "stop"}
|
|
154
|
+
disabled={!running || (lifeAction !== null && lifeAction !== "stop")}
|
|
155
|
+
>
|
|
156
|
+
{t("modules_ui.desktop_stop")}
|
|
157
|
+
</Button>
|
|
158
|
+
<Button
|
|
159
|
+
variant="secondary"
|
|
160
|
+
size="sm"
|
|
161
|
+
onClick={restartDesktop}
|
|
162
|
+
loading={lifeAction === "restart"}
|
|
163
|
+
disabled={!running || (lifeAction !== null && lifeAction !== "restart")}
|
|
164
|
+
title={t("modules_ui.desktop_restart_hint")}
|
|
165
|
+
>
|
|
166
|
+
{t("modules_ui.desktop_restart")}
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
}
|
|
170
|
+
>
|
|
105
171
|
{stLoading ? <Loading /> : (
|
|
106
|
-
<div className="flex items-center gap-2 text-sm">
|
|
172
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
|
107
173
|
<StatusDot ok={running} />
|
|
108
174
|
<span className="font-medium">{running ? t("modules_ui.desktop_running") : t("modules_ui.desktop_stopped")}</span>
|
|
109
175
|
<button
|
|
110
176
|
type="button"
|
|
111
177
|
onClick={() => mutateStatus()}
|
|
112
|
-
className="
|
|
178
|
+
className="text-xs text-muted-fg underline-offset-2 hover:underline"
|
|
113
179
|
>
|
|
114
180
|
{t("modules_ui.desktop_refresh")}
|
|
115
181
|
</button>
|
|
182
|
+
<span className="text-xs text-muted-fg">
|
|
183
|
+
({t("modules_ui.desktop_from_terminal")} <Kbd>apx desktop start</Kbd> · <Kbd>apx desktop --debug</Kbd>)
|
|
184
|
+
</span>
|
|
116
185
|
</div>
|
|
117
186
|
)}
|
|
118
|
-
<p className="mt-3 text-xs text-muted-fg">
|
|
119
|
-
{t("modules_ui.desktop_from_terminal")} <Kbd>apx desktop start</Kbd> · <Kbd>apx desktop --debug</Kbd>
|
|
120
|
-
</p>
|
|
121
187
|
</Section>
|
|
122
188
|
|
|
123
189
|
<Section
|
|
@@ -142,31 +208,27 @@ export function DesktopScreen() {
|
|
|
142
208
|
description={t("modules_ui.desktop_shortcut_desc")}
|
|
143
209
|
>
|
|
144
210
|
{cfgLoading ? <Loading /> : (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
>
|
|
167
|
-
{t("common.save")}
|
|
168
|
-
</Button>
|
|
169
|
-
</div>
|
|
211
|
+
<Field
|
|
212
|
+
label={t("modules_ui.desktop_accelerator")}
|
|
213
|
+
hint={t("modules_ui.desktop_accelerator_hint")}
|
|
214
|
+
>
|
|
215
|
+
<ShortcutInput
|
|
216
|
+
value={shortcut}
|
|
217
|
+
onChange={setShortcut}
|
|
218
|
+
disabled={busy}
|
|
219
|
+
trailing={
|
|
220
|
+
<Button
|
|
221
|
+
variant="primary"
|
|
222
|
+
size="sm"
|
|
223
|
+
onClick={saveShortcut}
|
|
224
|
+
loading={busy}
|
|
225
|
+
disabled={!shortcut.trim() || shortcut.trim() === savedShortcut}
|
|
226
|
+
>
|
|
227
|
+
{t("common.save")}
|
|
228
|
+
</Button>
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
</Field>
|
|
170
232
|
)}
|
|
171
233
|
</Section>
|
|
172
234
|
|