devglide 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/bin/claude-md-template.js +94 -0
- package/bin/devglide.js +387 -0
- package/package.json +85 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/apps/coder/.turbo/turbo-lint.log +5 -0
- package/src/apps/coder/package.json +16 -0
- package/src/apps/coder/public/favicon.svg +7 -0
- package/src/apps/coder/public/page.css +275 -0
- package/src/apps/coder/public/page.js +528 -0
- package/src/apps/coder/server.js +3 -0
- package/src/apps/documentation/public/page.css +597 -0
- package/src/apps/documentation/public/page.js +609 -0
- package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
- package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/kanban/package.json +32 -0
- package/src/apps/kanban/public/favicon.svg +7 -0
- package/src/apps/kanban/public/page.css +1010 -0
- package/src/apps/kanban/public/page.js +1730 -0
- package/src/apps/kanban/public/vendor/marked.min.js +6 -0
- package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
- package/src/apps/kanban/src/db.ts +319 -0
- package/src/apps/kanban/src/index.ts +14 -0
- package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
- package/src/apps/kanban/src/mcp-helpers.ts +60 -0
- package/src/apps/kanban/src/mcp.ts +59 -0
- package/src/apps/kanban/src/routes/attachments.ts +161 -0
- package/src/apps/kanban/src/routes/features.ts +233 -0
- package/src/apps/kanban/src/routes/issues.ts +373 -0
- package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
- package/src/apps/kanban/src/tools/item-tools.ts +307 -0
- package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
- package/src/apps/kanban/tsconfig.check.json +9 -0
- package/src/apps/kanban/tsconfig.json +9 -0
- package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
- package/src/apps/keymap/package.json +16 -0
- package/src/apps/keymap/public/page.css +275 -0
- package/src/apps/keymap/public/page.js +294 -0
- package/src/apps/keymap/server.js +25 -0
- package/src/apps/log/.turbo/turbo-build.log +5 -0
- package/src/apps/log/.turbo/turbo-lint.log +45 -0
- package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/log/node_modules/.bin/tsc +21 -0
- package/src/apps/log/node_modules/.bin/tsserver +21 -0
- package/src/apps/log/node_modules/.bin/tsx +21 -0
- package/src/apps/log/package.json +36 -0
- package/src/apps/log/public/console-sniffer.js +221 -0
- package/src/apps/log/public/favicon.svg +7 -0
- package/src/apps/log/public/page.css +322 -0
- package/src/apps/log/public/page.js +463 -0
- package/src/apps/log/src/index.ts +9 -0
- package/src/apps/log/src/mcp.ts +122 -0
- package/src/apps/log/src/routes/log.ts +333 -0
- package/src/apps/log/src/routes/status.ts +25 -0
- package/src/apps/log/src/server-sniffer.ts +118 -0
- package/src/apps/log/src/services/file-patterns.ts +39 -0
- package/src/apps/log/src/services/file-tailer.ts +228 -0
- package/src/apps/log/src/services/line-parser.ts +94 -0
- package/src/apps/log/src/services/log-writer.ts +39 -0
- package/src/apps/log/tsconfig.json +8 -0
- package/src/apps/prompts/.turbo/turbo-build.log +5 -0
- package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
- package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/prompts/mcp.ts +175 -0
- package/src/apps/prompts/node_modules/.bin/tsc +21 -0
- package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
- package/src/apps/prompts/node_modules/.bin/tsx +21 -0
- package/src/apps/prompts/package.json +25 -0
- package/src/apps/prompts/public/page.css +315 -0
- package/src/apps/prompts/public/page.js +541 -0
- package/src/apps/prompts/services/prompt-store.ts +212 -0
- package/src/apps/prompts/src/index.ts +9 -0
- package/src/apps/prompts/tsconfig.json +8 -0
- package/src/apps/prompts/types.ts +27 -0
- package/src/apps/shell/.turbo/turbo-build.log +5 -0
- package/src/apps/shell/.turbo/turbo-lint.log +34 -0
- package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/shell/package.json +35 -0
- package/src/apps/shell/public/favicon.svg +7 -0
- package/src/apps/shell/public/page.css +407 -0
- package/src/apps/shell/public/page.js +1577 -0
- package/src/apps/shell/src/index.ts +150 -0
- package/src/apps/shell/src/mcp.ts +398 -0
- package/src/apps/shell/src/shell-types.ts +41 -0
- package/src/apps/shell/tsconfig.json +8 -0
- package/src/apps/test/.turbo/turbo-build.log +5 -0
- package/src/apps/test/.turbo/turbo-lint.log +27 -0
- package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/test/node_modules/.bin/tsc +21 -0
- package/src/apps/test/node_modules/.bin/tsserver +21 -0
- package/src/apps/test/node_modules/.bin/tsx +21 -0
- package/src/apps/test/node_modules/.bin/uuid +21 -0
- package/src/apps/test/package.json +35 -0
- package/src/apps/test/public/favicon.svg +7 -0
- package/src/apps/test/public/page.css +499 -0
- package/src/apps/test/public/page.js +417 -0
- package/src/apps/test/public/scenario-runner.js +450 -0
- package/src/apps/test/src/index.ts +9 -0
- package/src/apps/test/src/mcp.ts +192 -0
- package/src/apps/test/src/routes/trigger.ts +285 -0
- package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
- package/src/apps/test/src/services/scenario-manager.ts +361 -0
- package/src/apps/test/src/services/scenario-store.ts +145 -0
- package/src/apps/test/tsconfig.json +8 -0
- package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
- package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
- package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/vocabulary/mcp.ts +173 -0
- package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
- package/src/apps/vocabulary/package.json +25 -0
- package/src/apps/vocabulary/public/page.css +247 -0
- package/src/apps/vocabulary/public/page.js +444 -0
- package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
- package/src/apps/vocabulary/src/index.ts +10 -0
- package/src/apps/vocabulary/tsconfig.json +8 -0
- package/src/apps/vocabulary/types.ts +22 -0
- package/src/apps/voice/.turbo/turbo-build.log +5 -0
- package/src/apps/voice/.turbo/turbo-lint.log +43 -0
- package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/voice/node_modules/.bin/openai +21 -0
- package/src/apps/voice/node_modules/.bin/tsc +21 -0
- package/src/apps/voice/node_modules/.bin/tsserver +21 -0
- package/src/apps/voice/node_modules/.bin/tsx +21 -0
- package/src/apps/voice/package.json +35 -0
- package/src/apps/voice/public/favicon.svg +7 -0
- package/src/apps/voice/public/page.css +388 -0
- package/src/apps/voice/public/page.js +718 -0
- package/src/apps/voice/src/index.ts +10 -0
- package/src/apps/voice/src/mcp.ts +70 -0
- package/src/apps/voice/src/providers/index.ts +85 -0
- package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
- package/src/apps/voice/src/providers/types.ts +27 -0
- package/src/apps/voice/src/routes/config.ts +118 -0
- package/src/apps/voice/src/routes/transcribe.ts +90 -0
- package/src/apps/voice/src/services/config-store.ts +129 -0
- package/src/apps/voice/src/services/stats.ts +108 -0
- package/src/apps/voice/src/transcribe.ts +11 -0
- package/src/apps/voice/src/utils/mime.ts +16 -0
- package/src/apps/voice/tsconfig.json +8 -0
- package/src/apps/workflow/.turbo/turbo-build.log +5 -0
- package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
- package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
- package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
- package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
- package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
- package/src/apps/workflow/engine/executors/index.ts +28 -0
- package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
- package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
- package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
- package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
- package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
- package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
- package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
- package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
- package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
- package/src/apps/workflow/engine/graph-runner.ts +438 -0
- package/src/apps/workflow/engine/node-executor.ts +104 -0
- package/src/apps/workflow/engine/node-registry.ts +15 -0
- package/src/apps/workflow/engine/variable-resolver.ts +109 -0
- package/src/apps/workflow/mcp.ts +223 -0
- package/src/apps/workflow/node_modules/.bin/tsc +21 -0
- package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
- package/src/apps/workflow/node_modules/.bin/tsx +21 -0
- package/src/apps/workflow/package.json +25 -0
- package/src/apps/workflow/public/editor/canvas.js +366 -0
- package/src/apps/workflow/public/editor/drag-manager.js +326 -0
- package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
- package/src/apps/workflow/public/editor/history-manager.js +147 -0
- package/src/apps/workflow/public/editor/layout-engine.js +159 -0
- package/src/apps/workflow/public/editor/node-renderer.js +199 -0
- package/src/apps/workflow/public/editor/selection-manager.js +193 -0
- package/src/apps/workflow/public/favicon.svg +7 -0
- package/src/apps/workflow/public/models/node-types.js +300 -0
- package/src/apps/workflow/public/models/workflow-model.js +257 -0
- package/src/apps/workflow/public/page.css +406 -0
- package/src/apps/workflow/public/page.js +658 -0
- package/src/apps/workflow/public/panels/inspector.js +360 -0
- package/src/apps/workflow/public/panels/palette.js +106 -0
- package/src/apps/workflow/public/panels/run-view.js +275 -0
- package/src/apps/workflow/public/panels/toolbar.js +232 -0
- package/src/apps/workflow/public/panels/workflow-list.js +237 -0
- package/src/apps/workflow/public/state/store.js +47 -0
- package/src/apps/workflow/services/custom-node-loader.ts +48 -0
- package/src/apps/workflow/services/legacy-converter.ts +72 -0
- package/src/apps/workflow/services/run-manager.ts +190 -0
- package/src/apps/workflow/services/workflow-store.ts +424 -0
- package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
- package/src/apps/workflow/services/workflow-validator.ts +98 -0
- package/src/apps/workflow/src/index.ts +10 -0
- package/src/apps/workflow/templates/ci-pipeline.json +18 -0
- package/src/apps/workflow/templates/code-review.json +22 -0
- package/src/apps/workflow/templates/kanban-testing.json +24 -0
- package/src/apps/workflow/tsconfig.json +8 -0
- package/src/apps/workflow/types.ts +268 -0
- package/src/packages/auth-middleware.ts +14 -0
- package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
- package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
- package/src/packages/design-tokens/build.js +413 -0
- package/src/packages/design-tokens/demo/index.html +1367 -0
- package/src/packages/design-tokens/demo/proposition-a.html +717 -0
- package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
- package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
- package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
- package/src/packages/design-tokens/dist/tokens.css +345 -0
- package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
- package/src/packages/design-tokens/dist/tokens.js +386 -0
- package/src/packages/design-tokens/package.json +25 -0
- package/src/packages/design-tokens/tokens.json +228 -0
- package/src/packages/devtools-middleware.ts +22 -0
- package/src/packages/eslint-config/index.js +63 -0
- package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
- package/src/packages/eslint-config/package.json +18 -0
- package/src/packages/json-file-store.ts +232 -0
- package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
- package/src/packages/mcp-utils/dist/index.d.ts +33 -0
- package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
- package/src/packages/mcp-utils/dist/index.js +126 -0
- package/src/packages/mcp-utils/dist/index.js.map +1 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
- package/src/packages/mcp-utils/package.json +32 -0
- package/src/packages/mcp-utils/src/index.ts +171 -0
- package/src/packages/mcp-utils/tsconfig.json +9 -0
- package/src/packages/paths.ts +18 -0
- package/src/packages/project-context/index.js +55 -0
- package/src/packages/project-context/package.json +13 -0
- package/src/packages/project-store.ts +127 -0
- package/src/packages/server-sniffer.ts +132 -0
- package/src/packages/shared-assets/favicon.svg +7 -0
- package/src/packages/shared-assets/keymap-registry.js +512 -0
- package/src/packages/shared-assets/logo.svg +6 -0
- package/src/packages/shared-assets/package.json +11 -0
- package/src/packages/shared-assets/ui-utils.js +48 -0
- package/src/packages/shared-assets/voice-widget.d.ts +37 -0
- package/src/packages/shared-assets/voice-widget.js +695 -0
- package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
- package/src/packages/shared-types/dist/index.d.ts +39 -0
- package/src/packages/shared-types/dist/index.d.ts.map +1 -0
- package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
- package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
- package/src/packages/shared-types/package.json +25 -0
- package/src/packages/shared-types/src/index.ts +41 -0
- package/src/packages/shared-types/tsconfig.json +11 -0
- package/src/packages/tsconfig/base.json +15 -0
- package/src/packages/tsconfig/next.json +14 -0
- package/src/packages/tsconfig/node.json +11 -0
- package/src/packages/tsconfig/package.json +10 -0
- package/turbo.json +25 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
// ── Voice App — Page Module ──────────────────────────────────────────
|
|
2
|
+
// ES module: mount(container, ctx), unmount(container), onProjectChange(project)
|
|
3
|
+
|
|
4
|
+
let _container = null;
|
|
5
|
+
let _statsTimer = null;
|
|
6
|
+
let _visibilityHandler = null;
|
|
7
|
+
let _escapeHandler = null;
|
|
8
|
+
|
|
9
|
+
let allProviders = [];
|
|
10
|
+
let currentProviderId = 'openai';
|
|
11
|
+
let audioFile = null;
|
|
12
|
+
|
|
13
|
+
// ── HTML ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const BODY_HTML = `
|
|
16
|
+
<div class="voice-toast-container" id="voice-toast-container"></div>
|
|
17
|
+
|
|
18
|
+
<!-- No Microphone Modal -->
|
|
19
|
+
<div id="mic-modal" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="mic-modal-title">
|
|
20
|
+
<div class="modal">
|
|
21
|
+
<div class="modal-icon">
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
23
|
+
<line x1="1" y1="1" x2="23" y2="23"/>
|
|
24
|
+
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/>
|
|
25
|
+
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"/>
|
|
26
|
+
<line x1="12" y1="19" x2="12" y2="23"/>
|
|
27
|
+
<line x1="8" y1="23" x2="16" y2="23"/>
|
|
28
|
+
</svg>
|
|
29
|
+
</div>
|
|
30
|
+
<h2 id="mic-modal-title">No Microphone Detected</h2>
|
|
31
|
+
<p class="modal-message" id="mic-modal-detail">No microphone device was found or access was denied.</p>
|
|
32
|
+
<p class="modal-hint">You can still transcribe audio using the file upload below.</p>
|
|
33
|
+
<button type="button" id="mic-modal-close" class="btn btn-secondary">Dismiss</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<header>
|
|
38
|
+
<span class="app-name">Voice</span>
|
|
39
|
+
<span id="status-badge" class="badge badge--unknown" role="status">checking...</span>
|
|
40
|
+
</header>
|
|
41
|
+
|
|
42
|
+
<div class="voice-container">
|
|
43
|
+
<!-- Configuration -->
|
|
44
|
+
<section class="card">
|
|
45
|
+
<div class="card-header">
|
|
46
|
+
<h2>Configuration</h2>
|
|
47
|
+
</div>
|
|
48
|
+
<form id="config-form" autocomplete="off">
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<div class="form-group">
|
|
51
|
+
<label for="voice-provider-select">Provider</label>
|
|
52
|
+
<select id="voice-provider-select"></select>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="form-group">
|
|
55
|
+
<label for="voice-language-select">Language</label>
|
|
56
|
+
<select id="voice-language-select">
|
|
57
|
+
<option value="auto">auto-detect</option>
|
|
58
|
+
<option value="en">English</option>
|
|
59
|
+
<option value="de">German</option>
|
|
60
|
+
<option value="fr">French</option>
|
|
61
|
+
<option value="es">Spanish</option>
|
|
62
|
+
<option value="it">Italian</option>
|
|
63
|
+
<option value="pt">Portuguese</option>
|
|
64
|
+
<option value="nl">Dutch</option>
|
|
65
|
+
<option value="ja">Japanese</option>
|
|
66
|
+
<option value="ko">Korean</option>
|
|
67
|
+
<option value="zh">Chinese</option>
|
|
68
|
+
<option value="ru">Russian</option>
|
|
69
|
+
<option value="ar">Arabic</option>
|
|
70
|
+
<option value="hi">Hindi</option>
|
|
71
|
+
<option value="pl">Polish</option>
|
|
72
|
+
<option value="uk">Ukrainian</option>
|
|
73
|
+
</select>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="form-group">
|
|
78
|
+
<label for="voice-model-input">Model</label>
|
|
79
|
+
<input type="text" id="voice-model-input" placeholder="e.g. whisper-1" spellcheck="false">
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div id="api-key-group" class="form-group hidden">
|
|
83
|
+
<label for="voice-api-key-input">
|
|
84
|
+
API Key
|
|
85
|
+
<span id="api-key-current" class="field-hint"></span>
|
|
86
|
+
</label>
|
|
87
|
+
<div class="input-with-action">
|
|
88
|
+
<input type="password" id="voice-api-key-input"
|
|
89
|
+
placeholder="Paste new key, or leave blank to keep current"
|
|
90
|
+
autocomplete="new-password" spellcheck="false"
|
|
91
|
+
aria-describedby="api-key-current">
|
|
92
|
+
<button type="button" id="toggle-key-btn" class="btn-icon" title="Toggle visibility" aria-label="Toggle API key visibility">
|
|
93
|
+
<svg id="eye-icon" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
94
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
95
|
+
<circle cx="12" cy="12" r="3"/>
|
|
96
|
+
</svg>
|
|
97
|
+
<svg id="eye-off-icon" class="hidden" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
98
|
+
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
|
99
|
+
<line x1="1" y1="1" x2="23" y2="23"/>
|
|
100
|
+
</svg>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div id="base-url-group" class="form-group hidden">
|
|
106
|
+
<label for="voice-base-url-input">Base URL</label>
|
|
107
|
+
<input type="url" id="voice-base-url-input" placeholder="http://localhost:8080" spellcheck="false">
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="form-actions">
|
|
111
|
+
<button type="button" id="test-btn" class="btn btn-secondary">
|
|
112
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
113
|
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
|
114
|
+
</svg>
|
|
115
|
+
Test Connection
|
|
116
|
+
</button>
|
|
117
|
+
<span id="test-result" class="test-result"></span>
|
|
118
|
+
<button type="submit" id="save-btn" class="btn btn-primary">Save</button>
|
|
119
|
+
</div>
|
|
120
|
+
</form>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<!-- Statistics -->
|
|
124
|
+
<section class="card">
|
|
125
|
+
<div class="card-header">
|
|
126
|
+
<h2>Statistics</h2>
|
|
127
|
+
<button id="reset-stats-btn" class="btn btn-secondary">Reset</button>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="stats-grid">
|
|
130
|
+
<div class="stat-card">
|
|
131
|
+
<span class="stat-value" id="total-transcriptions">0</span>
|
|
132
|
+
<span class="stat-label">Transcriptions</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="stat-card">
|
|
135
|
+
<span class="stat-value" id="total-duration">0s</span>
|
|
136
|
+
<span class="stat-label">Audio Processed</span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="stat-card">
|
|
139
|
+
<span class="stat-value" id="total-errors">0</span>
|
|
140
|
+
<span class="stat-label">Errors</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="stat-card">
|
|
143
|
+
<span class="stat-value" id="last-transcription">never</span>
|
|
144
|
+
<span class="stat-label">Last Transcription</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<!-- Test Transcription -->
|
|
150
|
+
<section class="card">
|
|
151
|
+
<h2>Test Transcription</h2>
|
|
152
|
+
<div class="upload-area" id="upload-area" aria-label="Audio file upload drop zone">
|
|
153
|
+
<input type="file" id="audio-file" accept="audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm" class="hidden">
|
|
154
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="upload-icon">
|
|
155
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
156
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
157
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
158
|
+
</svg>
|
|
159
|
+
<p class="upload-hint">
|
|
160
|
+
Drop an audio file here, or
|
|
161
|
+
<button type="button" id="choose-file-btn" class="btn-link">browse</button>
|
|
162
|
+
</p>
|
|
163
|
+
<p id="chosen-file-name" class="file-name hidden"></p>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="transcribe-row">
|
|
166
|
+
<button id="transcribe-btn" class="btn btn-primary" disabled>Transcribe</button>
|
|
167
|
+
<span id="transcribe-status" class="field-hint"></span>
|
|
168
|
+
</div>
|
|
169
|
+
<div id="transcription-result" class="result-box hidden">
|
|
170
|
+
<div id="result-meta" class="result-meta"></div>
|
|
171
|
+
<p id="result-text"></p>
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
<!-- MCP Tools -->
|
|
176
|
+
<section class="card">
|
|
177
|
+
<h2>MCP Tools</h2>
|
|
178
|
+
<div class="tools-list">
|
|
179
|
+
<div class="tool">
|
|
180
|
+
<code>voice_transcribe</code>
|
|
181
|
+
<span id="tool-transcribe-desc">Transcribe base64 audio via configured provider</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="tool">
|
|
184
|
+
<code>voice_status</code>
|
|
185
|
+
<span>Check service status and statistics</span>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</section>
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function $(sel) { return _container?.querySelector(sel); }
|
|
195
|
+
|
|
196
|
+
function arrayBufferToBase64(buffer) {
|
|
197
|
+
const bytes = new Uint8Array(buffer);
|
|
198
|
+
let binary = '';
|
|
199
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
200
|
+
return btoa(binary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const AUDIO_EXTENSIONS = ['.wav', '.mp3', '.m4a', '.ogg', '.flac', '.webm', '.aac', '.wma', '.opus'];
|
|
204
|
+
function isAudioFile(file) {
|
|
205
|
+
if (file.type && file.type.startsWith('audio/')) return true;
|
|
206
|
+
return AUDIO_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function setLoading(btn, loading) {
|
|
210
|
+
btn.disabled = loading;
|
|
211
|
+
btn.dataset.originalText = btn.dataset.originalText ?? btn.textContent.trim();
|
|
212
|
+
btn.textContent = loading ? '...' : btn.dataset.originalText;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function showToast(message, type = 'info') {
|
|
216
|
+
const container = $('#voice-toast-container');
|
|
217
|
+
if (!container) return;
|
|
218
|
+
const toast = document.createElement('div');
|
|
219
|
+
toast.className = `toast toast--${type}`;
|
|
220
|
+
toast.textContent = message;
|
|
221
|
+
container.appendChild(toast);
|
|
222
|
+
toast.getBoundingClientRect();
|
|
223
|
+
toast.classList.add('toast--visible');
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
toast.classList.remove('toast--visible');
|
|
226
|
+
const fallback = setTimeout(() => toast.remove(), 500);
|
|
227
|
+
toast.addEventListener('transitionend', () => { clearTimeout(fallback); toast.remove(); }, { once: true });
|
|
228
|
+
}, 3200);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Status badge ────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
function updateStatusBadge(configured) {
|
|
234
|
+
const badge = $('#status-badge');
|
|
235
|
+
if (!badge) return;
|
|
236
|
+
if (configured === null) {
|
|
237
|
+
badge.textContent = 'offline';
|
|
238
|
+
badge.className = 'badge badge--error';
|
|
239
|
+
} else if (configured) {
|
|
240
|
+
badge.textContent = 'connected';
|
|
241
|
+
badge.className = 'badge badge--ok';
|
|
242
|
+
} else {
|
|
243
|
+
badge.textContent = 'not configured';
|
|
244
|
+
badge.className = 'badge badge--error';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Provider form ───────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
function getSelectedProvider() {
|
|
251
|
+
return allProviders.find(p => p.id === currentProviderId) ?? null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function populateProviderDropdown() {
|
|
255
|
+
const select = $('#voice-provider-select');
|
|
256
|
+
if (!select) return;
|
|
257
|
+
select.innerHTML = allProviders.map(p => `<option value="${p.id}">${p.displayName}</option>`).join('');
|
|
258
|
+
select.value = currentProviderId;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function updateFormForProvider(provider) {
|
|
262
|
+
const apiKeyGroup = $('#api-key-group');
|
|
263
|
+
const baseUrlGroup = $('#base-url-group');
|
|
264
|
+
const modelInput = $('#voice-model-input');
|
|
265
|
+
const apiKeyInput = $('#voice-api-key-input');
|
|
266
|
+
const baseUrlInput = $('#voice-base-url-input');
|
|
267
|
+
const apiKeyHint = $('#api-key-current');
|
|
268
|
+
|
|
269
|
+
if (!modelInput) return;
|
|
270
|
+
|
|
271
|
+
modelInput.value = provider.currentModel ?? provider.defaultModel;
|
|
272
|
+
modelInput.placeholder = provider.defaultModel;
|
|
273
|
+
|
|
274
|
+
if (provider.requiresApiKey) {
|
|
275
|
+
apiKeyGroup.classList.remove('hidden');
|
|
276
|
+
baseUrlGroup.classList.add('hidden');
|
|
277
|
+
apiKeyInput.value = '';
|
|
278
|
+
apiKeyInput.placeholder = provider.currentApiKeyMasked
|
|
279
|
+
? 'Leave blank to keep current key'
|
|
280
|
+
: 'Paste API key';
|
|
281
|
+
apiKeyHint.textContent = provider.currentApiKeyMasked
|
|
282
|
+
? `Current: ${provider.currentApiKeyMasked}`
|
|
283
|
+
: '';
|
|
284
|
+
} else {
|
|
285
|
+
apiKeyGroup.classList.add('hidden');
|
|
286
|
+
baseUrlGroup.classList.remove('hidden');
|
|
287
|
+
baseUrlInput.value = provider.currentBaseURL ?? provider.defaultBaseURL ?? '';
|
|
288
|
+
baseUrlInput.placeholder = provider.defaultBaseURL ?? 'http://localhost:8080';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── API calls ───────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
async function loadProviders() {
|
|
295
|
+
try {
|
|
296
|
+
const res = await fetch('/api/voice/config/providers');
|
|
297
|
+
const data = await res.json();
|
|
298
|
+
|
|
299
|
+
allProviders = data.providers;
|
|
300
|
+
currentProviderId = data.current;
|
|
301
|
+
|
|
302
|
+
populateProviderDropdown();
|
|
303
|
+
|
|
304
|
+
const langSelect = $('#voice-language-select');
|
|
305
|
+
if (langSelect) langSelect.value = data.language;
|
|
306
|
+
|
|
307
|
+
const provider = getSelectedProvider();
|
|
308
|
+
if (provider) updateFormForProvider(provider);
|
|
309
|
+
|
|
310
|
+
const toolDesc = $('#tool-transcribe-desc');
|
|
311
|
+
if (toolDesc && provider) {
|
|
312
|
+
toolDesc.textContent = `Transcribe base64 audio via ${provider.displayName}`;
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
updateStatusBadge(null);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function fetchStatus() {
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch('/api/voice/config');
|
|
322
|
+
const cfg = await res.json();
|
|
323
|
+
updateStatusBadge(cfg.configured);
|
|
324
|
+
} catch {
|
|
325
|
+
updateStatusBadge(null);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function fetchStats() {
|
|
330
|
+
if (!_container) return;
|
|
331
|
+
try {
|
|
332
|
+
const res = await fetch('/api/voice/config/stats');
|
|
333
|
+
const stats = await res.json();
|
|
334
|
+
const el = (id) => _container.querySelector('#' + id);
|
|
335
|
+
const totalEl = el('total-transcriptions');
|
|
336
|
+
if (totalEl) totalEl.textContent = stats.totalTranscriptions;
|
|
337
|
+
const durEl = el('total-duration');
|
|
338
|
+
if (durEl) durEl.textContent = stats.totalDurationSec + 's';
|
|
339
|
+
const errEl = el('total-errors');
|
|
340
|
+
if (errEl) errEl.textContent = stats.totalErrors;
|
|
341
|
+
const lastEl = el('last-transcription');
|
|
342
|
+
if (lastEl) lastEl.textContent = stats.lastTranscriptionAt
|
|
343
|
+
? new Date(stats.lastTranscriptionAt).toLocaleTimeString()
|
|
344
|
+
: 'never';
|
|
345
|
+
} catch {
|
|
346
|
+
// ignore
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Microphone check ────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
async function checkMicrophone() {
|
|
353
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
354
|
+
showMicModal('Your browser does not support microphone access.');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
359
|
+
stream.getTracks().forEach(t => t.stop());
|
|
360
|
+
} catch (err) {
|
|
361
|
+
let message;
|
|
362
|
+
if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
|
363
|
+
message = 'No microphone device was found. Connect a microphone and reload the page.';
|
|
364
|
+
} else if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
|
365
|
+
message = 'Microphone access was denied. Allow access in your browser settings and reload.';
|
|
366
|
+
} else if (err.name === 'NotReadableError') {
|
|
367
|
+
message = 'Microphone is in use by another application.';
|
|
368
|
+
} else {
|
|
369
|
+
message = err.message || 'Microphone is unavailable.';
|
|
370
|
+
}
|
|
371
|
+
showMicModal(message);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function showMicModal(message) {
|
|
376
|
+
const detail = $('#mic-modal-detail');
|
|
377
|
+
const modal = $('#mic-modal');
|
|
378
|
+
if (detail) detail.textContent = message;
|
|
379
|
+
if (modal) modal.classList.remove('hidden');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Audio file handling ─────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
function setAudioFile(file) {
|
|
385
|
+
audioFile = file;
|
|
386
|
+
const nameEl = $('#chosen-file-name');
|
|
387
|
+
if (nameEl) {
|
|
388
|
+
nameEl.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
|
|
389
|
+
nameEl.classList.remove('hidden');
|
|
390
|
+
}
|
|
391
|
+
const transcribeBtn = $('#transcribe-btn');
|
|
392
|
+
if (transcribeBtn) transcribeBtn.disabled = false;
|
|
393
|
+
const resultBox = $('#transcription-result');
|
|
394
|
+
if (resultBox) resultBox.classList.add('hidden');
|
|
395
|
+
const statusEl = $('#transcribe-status');
|
|
396
|
+
if (statusEl) statusEl.textContent = '';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Event wiring ────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function wireEvents() {
|
|
402
|
+
if (!_container) return;
|
|
403
|
+
|
|
404
|
+
// Config form submit
|
|
405
|
+
const configForm = $('#config-form');
|
|
406
|
+
if (configForm) {
|
|
407
|
+
configForm.addEventListener('submit', async (e) => {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
const btn = $('#save-btn');
|
|
410
|
+
setLoading(btn, true);
|
|
411
|
+
|
|
412
|
+
const provider = getSelectedProvider();
|
|
413
|
+
const body = {
|
|
414
|
+
provider: currentProviderId,
|
|
415
|
+
language: $('#voice-language-select')?.value,
|
|
416
|
+
model: $('#voice-model-input')?.value.trim() || undefined,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (provider?.requiresApiKey) {
|
|
420
|
+
const key = $('#voice-api-key-input')?.value.trim();
|
|
421
|
+
if (key) body.apiKey = key;
|
|
422
|
+
} else {
|
|
423
|
+
const url = $('#voice-base-url-input')?.value.trim();
|
|
424
|
+
if (url) body.baseURL = url;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const res = await fetch('/api/voice/config', {
|
|
429
|
+
method: 'PUT',
|
|
430
|
+
headers: { 'Content-Type': 'application/json' },
|
|
431
|
+
body: JSON.stringify(body),
|
|
432
|
+
});
|
|
433
|
+
const data = await res.json();
|
|
434
|
+
if (data.ok) {
|
|
435
|
+
showToast('Configuration saved', 'ok');
|
|
436
|
+
const apiKeyInput = $('#voice-api-key-input');
|
|
437
|
+
if (apiKeyInput) apiKeyInput.value = '';
|
|
438
|
+
await loadProviders();
|
|
439
|
+
await fetchStatus();
|
|
440
|
+
} else {
|
|
441
|
+
showToast(data.error ?? 'Save failed', 'error');
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
showToast('Network error', 'error');
|
|
445
|
+
} finally {
|
|
446
|
+
setLoading(btn, false);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Provider change
|
|
452
|
+
const providerSelect = $('#voice-provider-select');
|
|
453
|
+
if (providerSelect) {
|
|
454
|
+
providerSelect.addEventListener('change', (e) => {
|
|
455
|
+
currentProviderId = e.target.value;
|
|
456
|
+
const provider = getSelectedProvider();
|
|
457
|
+
if (provider) updateFormForProvider(provider);
|
|
458
|
+
const testResult = $('#test-result');
|
|
459
|
+
if (testResult) testResult.textContent = '';
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Toggle API key visibility
|
|
464
|
+
const toggleKeyBtn = $('#toggle-key-btn');
|
|
465
|
+
if (toggleKeyBtn) {
|
|
466
|
+
toggleKeyBtn.addEventListener('click', () => {
|
|
467
|
+
const input = $('#voice-api-key-input');
|
|
468
|
+
const eye = $('#eye-icon');
|
|
469
|
+
const eyeOff = $('#eye-off-icon');
|
|
470
|
+
if (!input) return;
|
|
471
|
+
const isPassword = input.type === 'password';
|
|
472
|
+
input.type = isPassword ? 'text' : 'password';
|
|
473
|
+
if (eye) eye.classList.toggle('hidden', isPassword);
|
|
474
|
+
if (eyeOff) eyeOff.classList.toggle('hidden', !isPassword);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Test connection
|
|
479
|
+
const testBtn = $('#test-btn');
|
|
480
|
+
if (testBtn) {
|
|
481
|
+
testBtn.addEventListener('click', async () => {
|
|
482
|
+
const resultEl = $('#test-result');
|
|
483
|
+
setLoading(testBtn, true);
|
|
484
|
+
if (resultEl) { resultEl.textContent = ''; resultEl.className = 'test-result'; }
|
|
485
|
+
|
|
486
|
+
const provider = getSelectedProvider();
|
|
487
|
+
const testBody = {
|
|
488
|
+
provider: currentProviderId,
|
|
489
|
+
model: $('#voice-model-input')?.value.trim() || undefined,
|
|
490
|
+
};
|
|
491
|
+
if (provider?.requiresApiKey) {
|
|
492
|
+
const key = $('#voice-api-key-input')?.value.trim();
|
|
493
|
+
if (key) testBody.apiKey = key;
|
|
494
|
+
} else {
|
|
495
|
+
const url = $('#voice-base-url-input')?.value.trim();
|
|
496
|
+
if (url) testBody.baseURL = url;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const res = await fetch('/api/voice/config/test', {
|
|
501
|
+
method: 'POST',
|
|
502
|
+
headers: { 'Content-Type': 'application/json' },
|
|
503
|
+
body: JSON.stringify(testBody),
|
|
504
|
+
});
|
|
505
|
+
const data = await res.json();
|
|
506
|
+
if (data.ok) {
|
|
507
|
+
if (resultEl) { resultEl.textContent = `\u2713 ${data.displayName} is reachable`; resultEl.classList.add('test-result--ok'); }
|
|
508
|
+
} else {
|
|
509
|
+
if (resultEl) { resultEl.textContent = `\u2717 ${data.reason}`; resultEl.classList.add('test-result--error'); }
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
if (resultEl) { resultEl.textContent = '\u2717 Network error'; resultEl.classList.add('test-result--error'); }
|
|
513
|
+
} finally {
|
|
514
|
+
setLoading(testBtn, false);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Reset stats
|
|
520
|
+
const resetStatsBtn = $('#reset-stats-btn');
|
|
521
|
+
if (resetStatsBtn) {
|
|
522
|
+
resetStatsBtn.addEventListener('click', async () => {
|
|
523
|
+
setLoading(resetStatsBtn, true);
|
|
524
|
+
try {
|
|
525
|
+
await fetch('/api/voice/config/stats', { method: 'DELETE' });
|
|
526
|
+
await fetchStats();
|
|
527
|
+
showToast('Statistics reset', 'ok');
|
|
528
|
+
} catch {
|
|
529
|
+
showToast('Reset failed', 'error');
|
|
530
|
+
} finally {
|
|
531
|
+
setLoading(resetStatsBtn, false);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// File upload: browse button
|
|
537
|
+
const chooseFileBtn = $('#choose-file-btn');
|
|
538
|
+
const audioFileInput = $('#audio-file');
|
|
539
|
+
if (chooseFileBtn && audioFileInput) {
|
|
540
|
+
chooseFileBtn.addEventListener('click', () => audioFileInput.click());
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// File upload: input change
|
|
544
|
+
if (audioFileInput) {
|
|
545
|
+
audioFileInput.addEventListener('change', () => {
|
|
546
|
+
const file = audioFileInput.files?.[0];
|
|
547
|
+
if (file) setAudioFile(file);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// File upload: drag and drop
|
|
552
|
+
const uploadArea = $('#upload-area');
|
|
553
|
+
if (uploadArea) {
|
|
554
|
+
uploadArea.addEventListener('dragover', (e) => {
|
|
555
|
+
e.preventDefault();
|
|
556
|
+
uploadArea.classList.add('upload-area--drag');
|
|
557
|
+
});
|
|
558
|
+
uploadArea.addEventListener('dragleave', () => {
|
|
559
|
+
uploadArea.classList.remove('upload-area--drag');
|
|
560
|
+
});
|
|
561
|
+
uploadArea.addEventListener('drop', (e) => {
|
|
562
|
+
e.preventDefault();
|
|
563
|
+
uploadArea.classList.remove('upload-area--drag');
|
|
564
|
+
const file = e.dataTransfer?.files?.[0];
|
|
565
|
+
if (file) {
|
|
566
|
+
if (!isAudioFile(file)) {
|
|
567
|
+
showToast('Invalid file type. Please drop an audio file.', 'error');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
setAudioFile(file);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Transcribe button
|
|
576
|
+
const transcribeBtn = $('#transcribe-btn');
|
|
577
|
+
if (transcribeBtn) {
|
|
578
|
+
transcribeBtn.addEventListener('click', async () => {
|
|
579
|
+
if (!audioFile) return;
|
|
580
|
+
const statusEl = $('#transcribe-status');
|
|
581
|
+
const resultBox = $('#transcription-result');
|
|
582
|
+
setLoading(transcribeBtn, true);
|
|
583
|
+
if (statusEl) statusEl.textContent = 'Transcribing...';
|
|
584
|
+
if (resultBox) resultBox.classList.add('hidden');
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const buffer = await audioFile.arrayBuffer();
|
|
588
|
+
const base64 = arrayBufferToBase64(buffer);
|
|
589
|
+
|
|
590
|
+
const res = await fetch('/api/voice/config/test-transcription', {
|
|
591
|
+
method: 'POST',
|
|
592
|
+
headers: { 'Content-Type': 'application/json' },
|
|
593
|
+
body: JSON.stringify({ audioBase64: base64, filename: audioFile.name }),
|
|
594
|
+
});
|
|
595
|
+
const data = await res.json();
|
|
596
|
+
|
|
597
|
+
if (data.ok) {
|
|
598
|
+
const resultText = $('#result-text');
|
|
599
|
+
if (resultText) resultText.textContent = data.text;
|
|
600
|
+
const meta = [];
|
|
601
|
+
if (data.language) meta.push(`language: ${data.language}`);
|
|
602
|
+
if (data.duration != null) meta.push(`duration: ${data.duration.toFixed(1)}s`);
|
|
603
|
+
const resultMeta = $('#result-meta');
|
|
604
|
+
if (resultMeta) resultMeta.textContent = meta.join(' \u00b7 ');
|
|
605
|
+
if (resultBox) resultBox.classList.remove('hidden');
|
|
606
|
+
if (statusEl) statusEl.textContent = '';
|
|
607
|
+
await fetchStats();
|
|
608
|
+
} else {
|
|
609
|
+
if (statusEl) {
|
|
610
|
+
statusEl.textContent = `Error: ${data.error}`;
|
|
611
|
+
statusEl.style.color = 'var(--df-color-state-error)';
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
if (statusEl) statusEl.textContent = 'Network error';
|
|
616
|
+
} finally {
|
|
617
|
+
setLoading(transcribeBtn, false);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Mic modal dismiss
|
|
623
|
+
const micModalClose = $('#mic-modal-close');
|
|
624
|
+
if (micModalClose) {
|
|
625
|
+
micModalClose.addEventListener('click', () => {
|
|
626
|
+
const modal = $('#mic-modal');
|
|
627
|
+
if (modal) modal.classList.add('hidden');
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Escape key to close mic modal
|
|
632
|
+
_escapeHandler = (e) => {
|
|
633
|
+
if (e.key === 'Escape') {
|
|
634
|
+
const modal = $('#mic-modal');
|
|
635
|
+
if (modal && !modal.classList.contains('hidden')) {
|
|
636
|
+
modal.classList.add('hidden');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
document.addEventListener('keydown', _escapeHandler);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ── Stats polling ───────────────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
function startStatsPoll() {
|
|
646
|
+
if (_statsTimer) return;
|
|
647
|
+
fetchStats();
|
|
648
|
+
_statsTimer = setInterval(fetchStats, 5000);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function stopStatsPoll() {
|
|
652
|
+
if (_statsTimer) { clearInterval(_statsTimer); _statsTimer = null; }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
export function mount(container, ctx) {
|
|
658
|
+
_container = container;
|
|
659
|
+
|
|
660
|
+
// Reset module state
|
|
661
|
+
allProviders = [];
|
|
662
|
+
currentProviderId = 'openai';
|
|
663
|
+
audioFile = null;
|
|
664
|
+
|
|
665
|
+
// 1. Scope the container
|
|
666
|
+
container.classList.add('page-voice');
|
|
667
|
+
|
|
668
|
+
// 2. Build HTML
|
|
669
|
+
container.innerHTML = BODY_HTML;
|
|
670
|
+
|
|
671
|
+
// 3. Wire events
|
|
672
|
+
wireEvents();
|
|
673
|
+
|
|
674
|
+
// 4. Visibility change handler for stats polling
|
|
675
|
+
_visibilityHandler = () => {
|
|
676
|
+
if (document.hidden) stopStatsPoll();
|
|
677
|
+
else startStatsPoll();
|
|
678
|
+
};
|
|
679
|
+
document.addEventListener('visibilitychange', _visibilityHandler);
|
|
680
|
+
|
|
681
|
+
// 5. Initial data load
|
|
682
|
+
loadProviders();
|
|
683
|
+
fetchStatus();
|
|
684
|
+
startStatsPoll();
|
|
685
|
+
checkMicrophone();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function unmount(container) {
|
|
689
|
+
// 1. Stop stats polling
|
|
690
|
+
stopStatsPoll();
|
|
691
|
+
|
|
692
|
+
// 2. Remove visibility handler
|
|
693
|
+
if (_visibilityHandler) {
|
|
694
|
+
document.removeEventListener('visibilitychange', _visibilityHandler);
|
|
695
|
+
_visibilityHandler = null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 3. Remove escape handler
|
|
699
|
+
if (_escapeHandler) {
|
|
700
|
+
document.removeEventListener('keydown', _escapeHandler);
|
|
701
|
+
_escapeHandler = null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 4. Remove scope class & clear HTML
|
|
705
|
+
if (container) {
|
|
706
|
+
container.classList.remove('page-voice');
|
|
707
|
+
container.innerHTML = '';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 5. Clear module references
|
|
711
|
+
_container = null;
|
|
712
|
+
audioFile = null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function onProjectChange(project) {
|
|
716
|
+
// Voice config is global (not per-project), so no action needed.
|
|
717
|
+
// Kept for interface compliance.
|
|
718
|
+
}
|