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.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,695 @@
1
+ /*!
2
+ * VoiceWidget — hold-to-record voice input widget
3
+ * No external dependencies. Plain JS + Web APIs.
4
+ */
5
+ (function (root, factory) {
6
+ if (typeof module === 'object' && module.exports) {
7
+ module.exports = factory();
8
+ } else {
9
+ root.VoiceWidget = factory();
10
+ }
11
+ })(typeof self !== 'undefined' ? self : this, function () {
12
+ 'use strict';
13
+
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // CSS — KITT SCANNER (Knight Rider-inspired, Material Design sensibility)
16
+ // Matte black panel · red Larson scanner strip · subtle elevation
17
+ // scanner freezes on malfunction · clean system-ui typography
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ var CSS = [
20
+ // ── Wrapper — NERV aesthetic ──
21
+ '.vw-wrap{',
22
+ 'position:relative;display:inline-flex;z-index:0;',
23
+ 'filter:drop-shadow(0 2px 4px rgba(0,0,0,.7)) drop-shadow(0 1px 2px rgba(0,0,0,.5));',
24
+ 'transition:filter .25s ease;',
25
+ '}',
26
+ '.vw-wrap:hover{',
27
+ 'filter:var(--df-glow-accent,drop-shadow(0 0 8px rgba(255,140,0,.35))) drop-shadow(0 2px 4px rgba(0,0,0,.6));',
28
+ '}',
29
+ // Corner brackets — NERV card pattern
30
+ '.vw-wrap::before,.vw-wrap::after{',
31
+ "content:'';position:absolute;width:8px;height:8px;",
32
+ 'border-color:var(--df-color-accent-default,#ff8c00);border-style:solid;pointer-events:none;opacity:.4;z-index:1;',
33
+ '}',
34
+ '.vw-wrap::before{top:0;left:0;border-width:2px 0 0 2px;}',
35
+ '.vw-wrap::after{bottom:0;right:0;border-width:0 2px 2px 0;}',
36
+ '.vw-wrap[data-state="recording"]{animation:vw-wrap-pulse 1.2s ease-in-out infinite alternate}',
37
+ '@keyframes vw-wrap-pulse{',
38
+ '0%{filter:drop-shadow(0 4px 10px rgba(255,21,0,.4)) drop-shadow(0 2px 4px rgba(0,0,0,.7))}',
39
+ '100%{filter:drop-shadow(0 4px 18px rgba(255,21,0,.7)) drop-shadow(0 2px 6px rgba(0,0,0,.8))}',
40
+ '}',
41
+ '.vw-wrap[data-state="error"]{',
42
+ 'filter:var(--df-glow-error,drop-shadow(0 4px 14px rgba(255,30,0,.6))) drop-shadow(0 2px 4px rgba(0,0,0,.7));',
43
+ '}',
44
+
45
+ // ── Button — NERV angular panel, fixed dimensions ──
46
+ '.vw-btn{',
47
+ 'position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;',
48
+ 'width:120px;height:36px;box-sizing:border-box;',
49
+ 'padding:0 14px 4px;', // 4px bottom for scanner strip
50
+ 'background:var(--df-color-bg-surface,#0f0c00);',
51
+ 'border:1px solid var(--df-color-border-default,#2a2000);',
52
+ 'border-radius:0;',
53
+ 'clip-path:var(--df-clip-sm,polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,8px 100%,0 calc(100% - 8px)));',
54
+ 'color:var(--df-color-text-primary,#e8dcc8);cursor:pointer;',
55
+ 'font-size:11px;font-weight:400;',
56
+ "font-family:var(--df-font-mono,'Courier New',Courier,monospace);",
57
+ 'letter-spacing:var(--df-letter-spacing-wide,0.12em);text-transform:uppercase;white-space:nowrap;',
58
+ 'user-select:none;-webkit-user-select:none;touch-action:none;',
59
+ 'outline:none;overflow:hidden;',
60
+ 'transition:background .25s ease,color .25s ease,border-color .25s ease;',
61
+ '}',
62
+
63
+ // ── Hover / active ──
64
+ '.vw-btn:hover{background:var(--df-color-bg-raised,#1a1400);color:var(--df-color-accent-default,#ff8c00);border-color:var(--df-color-accent-default,#ff8c00)}',
65
+ '.vw-btn:active{background:var(--df-color-bg-surface,#0f0c00)}',
66
+ '.vw-btn:hover .vw-icon{transform:scale(1.08)}',
67
+
68
+ // ── NERV scanner strip — orange in idle, red in recording ──
69
+ '.vw-bars{',
70
+ 'position:absolute;bottom:0;left:0;right:0;height:4px;',
71
+ 'background:var(--df-color-bg-raised,#1a1400);overflow:hidden;',
72
+ '}',
73
+ '.vw-bar{display:none}',
74
+
75
+ // ── Scanner beam — accent Larson sweep (idle, theme-aware) ──
76
+ '.vw-bars::before{',
77
+ "content:'';position:absolute;",
78
+ 'top:0;left:-20%;width:35%;height:100%;',
79
+ 'background:radial-gradient(ellipse at center,color-mix(in srgb,var(--df-color-accent-default,#ff8c00) 70%,transparent) 0%,color-mix(in srgb,var(--df-color-accent-default,#ff8c00) 30%,transparent) 50%,transparent 100%);',
80
+ 'animation:vw-kitt 2.8s ease-in-out infinite alternate;',
81
+ '}',
82
+ '@keyframes vw-kitt{0%{left:-20%}100%{left:85%}}',
83
+
84
+ // ── Recording — fast red scanner, state-recording color ──
85
+ '.vw-btn[data-state="recording"]{background:var(--df-color-bg-surface,#0f0c00);color:var(--df-color-state-recording,#ff1500);border-color:var(--df-color-state-recording,#ff1500)}',
86
+ '.vw-btn[data-state="recording"] .vw-bars::before{',
87
+ 'animation-duration:.65s;',
88
+ 'background:radial-gradient(ellipse at center,#ff1500 0%,rgba(255,21,0,.6) 40%,transparent 100%);',
89
+ 'filter:blur(.4px);',
90
+ '}',
91
+ '.vw-btn[data-state="recording"] .vw-icon{display:none}',
92
+ '.vw-btn[data-state="recording"] .vw-label{display:none}',
93
+
94
+ // ── KITT voice modulator — segmented LED bars ──
95
+ '.vw-modulator{display:none;align-items:flex-end;gap:3px;height:18px;position:relative;z-index:2}',
96
+ '.vw-btn[data-state="recording"] .vw-modulator{display:inline-flex}',
97
+ '.vw-modulator span{',
98
+ 'width:5px;border-radius:0;',
99
+ 'background:repeating-linear-gradient(to top,#ff4400 0,#ff4400 3px,#1a0500 3px,#1a0500 5px);',
100
+ 'box-shadow:0 0 4px rgba(255,80,0,.8),0 0 10px rgba(255,40,0,.4);',
101
+ 'animation:vw-mod .55s ease-in-out infinite alternate;',
102
+ '}',
103
+ '.vw-modulator span:nth-child(1){animation-delay:0s;min-height:4px}',
104
+ '.vw-modulator span:nth-child(2){animation-delay:.07s;min-height:6px}',
105
+ '.vw-modulator span:nth-child(3){animation-delay:.14s;min-height:3px}',
106
+ '.vw-modulator span:nth-child(4){animation-delay:.21s;min-height:8px}',
107
+ '.vw-modulator span:nth-child(5){animation-delay:.28s;min-height:4px}',
108
+ '.vw-modulator span:nth-child(6){animation-delay:.35s;min-height:6px}',
109
+ '@keyframes vw-mod{from{height:3px}to{height:18px}}',
110
+
111
+ // ── Transcribing — processing amber, medium scanner ──
112
+ '.vw-btn[data-state="transcribing"]{background:var(--df-color-bg-surface,#0f0c00);color:var(--df-color-state-processing,#cc8800);cursor:wait;border-color:var(--df-color-state-processing,#cc8800)}',
113
+ '.vw-btn[data-state="transcribing"] .vw-bars{background:var(--df-color-bg-raised,#1a1400)}',
114
+ '.vw-btn[data-state="transcribing"] .vw-bars::before{',
115
+ 'animation-duration:1.3s;',
116
+ 'background:radial-gradient(ellipse at center,color-mix(in srgb,var(--df-color-state-processing,#cc8800) 85%,transparent) 0%,color-mix(in srgb,var(--df-color-state-processing,#cc8800) 40%,transparent) 40%,transparent 100%);',
117
+ '}',
118
+ '.vw-btn[data-state="transcribing"] .vw-icon{display:none}',
119
+ '.vw-btn[data-state="transcribing"] .vw-label{animation:vw-process 1.6s ease-in-out infinite}',
120
+ '@keyframes vw-process{0%,100%{opacity:1}40%,60%{opacity:.3}}',
121
+
122
+ // ── Spinner ──
123
+ '.vw-spinner{',
124
+ 'display:none;width:12px;height:12px;flex-shrink:0;',
125
+ 'border:2px solid rgba(200,120,0,.2);border-top-color:var(--df-color-state-processing,#cc8800);',
126
+ 'border-radius:50%;animation:vw-spin .65s linear infinite;',
127
+ '}',
128
+ '.vw-btn[data-state="transcribing"] .vw-spinner{display:block}',
129
+ '@keyframes vw-spin{to{transform:rotate(360deg)}}',
130
+
131
+ // ── Error — scanner frozen, shake, error red ──
132
+ '.vw-btn[data-state="error"]{',
133
+ 'background:var(--df-color-bg-surface,#0f0c00);color:var(--df-color-state-error,#ff3333);',
134
+ 'border-color:var(--df-color-state-error,#ff3333);',
135
+ 'animation:vw-quake .45s ease;',
136
+ '}',
137
+ '@keyframes vw-quake{',
138
+ '0%,100%{transform:translate(0)}',
139
+ '15%{transform:translate(-4px,1px)}',
140
+ '30%{transform:translate(4px,-1px)}',
141
+ '45%{transform:translate(-3px,0)}',
142
+ '60%{transform:translate(3px,1px)}',
143
+ '75%{transform:translate(-2px,-1px)}',
144
+ '}',
145
+ '.vw-btn[data-state="error"] .vw-bars::before{',
146
+ 'animation-play-state:paused;',
147
+ 'background:radial-gradient(ellipse at center,rgba(255,51,51,.9) 0%,rgba(255,51,51,.4) 40%,transparent 100%);',
148
+ '}',
149
+
150
+ // ── Error flash overlay ──
151
+ '.vw-flash{',
152
+ 'position:absolute;inset:0;',
153
+ 'background:rgba(255,30,0,.25);opacity:0;pointer-events:none;z-index:5;',
154
+ '}',
155
+ '.vw-wrap[data-state="error"] .vw-flash{animation:vw-flashpop .5s ease-out}',
156
+ '@keyframes vw-flashpop{0%{opacity:1}100%{opacity:0}}',
157
+
158
+ // ── Icon & label ──
159
+ '.vw-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:transform .2s ease;position:relative;z-index:2}',
160
+ '.vw-label{font-size:11px;line-height:1;position:relative;z-index:2;letter-spacing:var(--df-letter-spacing-wide,0.12em);text-transform:uppercase}',
161
+ ].join('\n');
162
+
163
+ var ICONS = {
164
+ // Standard mic
165
+ idle: '<svg 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"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>',
166
+ // Red filled circle (recording indicator — KITT's active signal dot)
167
+ recording: '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><circle cx="6" cy="6" r="5" fill="#ff1500"/></svg>',
168
+ transcribing: '',
169
+ // Warning triangle
170
+ error: '<svg 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"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
171
+ };
172
+
173
+ var LABELS = {
174
+ idle: 'Speak',
175
+ recording: 'Transmitting\u2026',
176
+ transcribing: 'Processing\u2026',
177
+ error: 'Malfunction',
178
+ };
179
+
180
+ var _styleInjected = false;
181
+ function injectStyles() {
182
+ if (_styleInjected) return;
183
+ _styleInjected = true;
184
+ var el = document.createElement('style');
185
+ el.textContent = CSS;
186
+ document.head.appendChild(el);
187
+ }
188
+
189
+ function blobToBase64(blob) {
190
+ return new Promise(function (resolve, reject) {
191
+ var reader = new FileReader();
192
+ reader.onloadend = function () {
193
+ if (reader.readyState !== FileReader.DONE) {
194
+ reject(new Error('FileReader did not complete'));
195
+ return;
196
+ }
197
+ var result = reader.result;
198
+ if (typeof result !== 'string') {
199
+ reject(new Error('FileReader produced non-string result'));
200
+ return;
201
+ }
202
+ var base64 = result.split(',')[1];
203
+ if (!base64) {
204
+ reject(new Error('Failed to extract base64 from data URL'));
205
+ return;
206
+ }
207
+ resolve(base64);
208
+ };
209
+ reader.onerror = function () {
210
+ reject(reader.error || new Error('Failed to read audio blob'));
211
+ };
212
+ reader.readAsDataURL(blob);
213
+ });
214
+ }
215
+
216
+ function mimeToExt(mimeType) {
217
+ if (!mimeType) return 'webm';
218
+ if (mimeType.includes('ogg')) return 'ogg';
219
+ if (mimeType.includes('mp4')) return 'mp4';
220
+ return 'webm';
221
+ }
222
+
223
+ function pickMimeType() {
224
+ var candidates = [
225
+ 'audio/webm;codecs=opus',
226
+ 'audio/webm',
227
+ 'audio/ogg;codecs=opus',
228
+ 'audio/mp4',
229
+ ];
230
+ for (var i = 0; i < candidates.length; i++) {
231
+ if (MediaRecorder.isTypeSupported(candidates[i])) return candidates[i];
232
+ }
233
+ return '';
234
+ }
235
+
236
+ /**
237
+ * Create a VoiceWidget instance.
238
+ *
239
+ * @param {object} opts
240
+ * @param {string} opts.voiceUrl - Base URL of the voice app (e.g. 'http://localhost:7004')
241
+ * @param {function} opts.onResult - Called with transcribed text string
242
+ * @param {function} [opts.onError] - Called with Error object
243
+ * @param {string} [opts.language] - BCP-47 language tag (e.g. 'en')
244
+ * @param {string} [opts.hotkey] - Single key character to toggle recording (e.g. 'v')
245
+ * @param {string} [opts.label] - Idle label override (default: 'Speak')
246
+ */
247
+ function create(opts) {
248
+ if (!opts || !opts.voiceUrl) throw new Error('VoiceWidget: voiceUrl is required');
249
+ if (typeof opts.onResult !== 'function') throw new Error('VoiceWidget: onResult callback is required');
250
+
251
+ var voiceUrl = opts.voiceUrl.replace(/\/$/, '');
252
+ var onResult = opts.onResult;
253
+ var onError = opts.onError || function (err) { console.error('[VoiceWidget]', err); };
254
+ var language = opts.language || null;
255
+ var hotkey = opts.hotkey ? opts.hotkey.toLowerCase() : null;
256
+ var idleLabel = opts.label || LABELS.idle;
257
+
258
+ var state = 'idle'; // idle | recording | transcribing | error
259
+ var mediaRecorder = null;
260
+ var chunks = [];
261
+ var stream = null;
262
+ var errorTimer = null;
263
+ var pendingStop = false; // stop requested while getUserMedia pending
264
+ var recordingStartedAt = 0; // timestamp when recording began
265
+ var chimeTimer = null; // setTimeout id during chime-before-record delay
266
+ var MIN_RECORDING_MS = 350; // minimum recording duration to capture audio
267
+ var CHIME_LEAD_MS = 550; // ms to wait for start chime + echo-cancellation settling before opening mic
268
+
269
+ // ── audio feedback ─────────────────────────────────────────────────────────
270
+ var _audioCtx = null;
271
+
272
+ function _getAudioCtx() {
273
+ if (!_audioCtx) {
274
+ try { _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
275
+ }
276
+ if (_audioCtx && _audioCtx.state === 'suspended') { _audioCtx.resume(); }
277
+ return _audioCtx;
278
+ }
279
+
280
+ // "Her" OS-inspired chime — warm, layered, ethereal tones.
281
+ // Soft sine layers at consonant intervals with gentle attack and long decay.
282
+ function _playTone(ctx, freq, startTime, duration, vol) {
283
+ var osc = ctx.createOscillator();
284
+ var g = ctx.createGain();
285
+ osc.connect(g);
286
+ g.connect(ctx.destination);
287
+ osc.type = 'sine';
288
+ osc.frequency.setValueAtTime(freq, startTime);
289
+ g.gain.setValueAtTime(0, startTime);
290
+ g.gain.linearRampToValueAtTime(vol, startTime + 0.04);
291
+ g.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
292
+ osc.start(startTime);
293
+ osc.stop(startTime + duration + 0.01);
294
+ }
295
+
296
+ // Start chime — single soft rising tone
297
+ function _playStartChime() {
298
+ var ctx = _getAudioCtx();
299
+ if (!ctx) return;
300
+ try {
301
+ var t = ctx.currentTime;
302
+ _playTone(ctx, 523.25, t, 0.40, 0.06); // C5 fundamental
303
+ _playTone(ctx, 659.25, t + 0.12, 0.35, 0.05); // E5 gentle rise
304
+ } catch (e) {}
305
+ }
306
+
307
+ // Stop chime — single soft descending tone
308
+ function _playStopChime() {
309
+ var ctx = _getAudioCtx();
310
+ if (!ctx) return;
311
+ try {
312
+ var t = ctx.currentTime;
313
+ _playTone(ctx, 659.25, t, 0.30, 0.05); // E5
314
+ _playTone(ctx, 523.25, t + 0.10, 0.35, 0.04); // C5 resolve down
315
+ } catch (e) {}
316
+ }
317
+
318
+ // DOM refs
319
+ var wrap = null;
320
+ var btn = null;
321
+ var iconEl = null;
322
+ var labelEl = null;
323
+ var spinnerEl = null;
324
+
325
+ // ── state machine ──────────────────────────────────────────────────────────
326
+ function setState(s) {
327
+ state = s;
328
+ if (!btn) return;
329
+ btn.setAttribute('data-state', s);
330
+ wrap.setAttribute('data-state', s);
331
+ iconEl.innerHTML = ICONS[s] || '';
332
+ var lbl = s === 'idle' ? idleLabel : (LABELS[s] || s);
333
+ labelEl.textContent = lbl;
334
+ btn.setAttribute('aria-label', lbl);
335
+ }
336
+
337
+ // ── recording ──────────────────────────────────────────────────────────────
338
+ function startRecording() {
339
+ if (state !== 'idle' && state !== 'error') return;
340
+
341
+ clearTimeout(errorTimer);
342
+ pendingStop = false;
343
+ _getAudioCtx(); // init within user gesture so AudioContext is unlocked
344
+
345
+ // Guard: secure context and mediaDevices availability
346
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
347
+ handleError(new Error(
348
+ window.isSecureContext === false
349
+ ? 'Microphone requires a secure (HTTPS) connection'
350
+ : 'Microphone API not available in this browser'
351
+ ));
352
+ return;
353
+ }
354
+
355
+ navigator.mediaDevices.getUserMedia({ audio: true, video: false })
356
+ .then(function (s) {
357
+ stream = s;
358
+ chunks = [];
359
+ var mime = pickMimeType();
360
+ mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {});
361
+
362
+ mediaRecorder.ondataavailable = function (e) {
363
+ if (e.data && e.data.size > 0) chunks.push(e.data);
364
+ };
365
+
366
+ mediaRecorder.onstop = function () {
367
+ stopStream();
368
+ var usedMime = mediaRecorder.mimeType || 'audio/webm';
369
+ var blob = new Blob(chunks, { type: usedMime });
370
+ chunks = [];
371
+ if (blob.size === 0) {
372
+ handleError(new Error('No audio captured'));
373
+ return;
374
+ }
375
+ transcribe(blob, usedMime);
376
+ };
377
+
378
+ mediaRecorder.onstart = function () {
379
+ recordingStartedAt = Date.now();
380
+
381
+ // If stop was requested during chime delay, honour it
382
+ if (pendingStop) {
383
+ pendingStop = false;
384
+ setTimeout(doStop, MIN_RECORDING_MS);
385
+ }
386
+ };
387
+
388
+ // Play the chime BEFORE opening the mic so the beep never enters
389
+ // the recording. This avoids browser echo-cancellation suppressing
390
+ // the first moments of speech that follow the beep.
391
+ _playStartChime();
392
+ setState('recording');
393
+ chimeTimer = setTimeout(function () {
394
+ chimeTimer = null;
395
+ if (pendingStop) {
396
+ // Stop requested during chime — abort without recording
397
+ pendingStop = false;
398
+ stopStream();
399
+ setState('idle');
400
+ return;
401
+ }
402
+ mediaRecorder.start();
403
+ }, CHIME_LEAD_MS);
404
+ })
405
+ .catch(function (err) {
406
+ pendingStop = false;
407
+ var name = err.name || '';
408
+ var msg;
409
+ if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {
410
+ msg = 'Microphone permission denied — check browser site settings';
411
+ } else if (name === 'AbortError') {
412
+ msg = 'Microphone permission dismissed — click the mic and allow access';
413
+ } else if (name === 'NotFoundError') {
414
+ msg = 'No microphone found — connect a mic and retry';
415
+ } else if (name === 'NotReadableError') {
416
+ msg = 'Microphone is in use by another application';
417
+ } else if (name === 'SecurityError') {
418
+ msg = 'Microphone requires a secure (HTTPS) connection';
419
+ } else if (name === 'TypeError') {
420
+ msg = 'Microphone API not available — check browser compatibility';
421
+ } else {
422
+ msg = 'Microphone unavailable — check permissions and try again';
423
+ }
424
+ handleError(new Error(msg));
425
+ });
426
+ }
427
+
428
+ function doStop() {
429
+ if (state !== 'recording') return;
430
+ _playStopChime(); // descending 2-note: done speaking
431
+ setState('transcribing');
432
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
433
+ mediaRecorder.stop();
434
+ }
435
+ }
436
+
437
+ function stopRecording() {
438
+ // Stop requested before getUserMedia resolved — defer
439
+ if (state === 'idle') {
440
+ pendingStop = true;
441
+ return;
442
+ }
443
+ if (state !== 'recording') return;
444
+
445
+ // Stop during chime delay — mic hasn't opened yet, cancel gracefully
446
+ if (chimeTimer) {
447
+ clearTimeout(chimeTimer);
448
+ chimeTimer = null;
449
+ stopStream();
450
+ setState('idle');
451
+ return;
452
+ }
453
+
454
+ // Ensure minimum recording time so the blob isn't empty
455
+ var elapsed = Date.now() - recordingStartedAt;
456
+ if (elapsed < MIN_RECORDING_MS) {
457
+ setTimeout(doStop, MIN_RECORDING_MS - elapsed);
458
+ return;
459
+ }
460
+ doStop();
461
+ }
462
+
463
+ function stopStream() {
464
+ if (stream) {
465
+ stream.getTracks().forEach(function (t) { t.stop(); });
466
+ stream = null;
467
+ }
468
+ }
469
+
470
+ // ── transcription ──────────────────────────────────────────────────────────
471
+ function transcribe(blob, mimeType) {
472
+ // Guard against recordings that exceed the server's 25MB JSON body limit
473
+ // (base64 adds ~33% overhead, so cap the raw blob at 18MB)
474
+ if (blob.size > 18 * 1024 * 1024) {
475
+ handleError(new Error('Recording too large — try a shorter message'));
476
+ return;
477
+ }
478
+
479
+ var abortCtrl = new AbortController();
480
+ var fetchTimeout = setTimeout(function () { abortCtrl.abort(); }, 30000);
481
+
482
+ blobToBase64(blob)
483
+ .then(function (base64) {
484
+ var ext = mimeToExt(mimeType);
485
+ var payload = {
486
+ audioBase64: base64,
487
+ filename: 'recording.' + ext,
488
+ };
489
+ if (language) payload.language = language;
490
+ var body = JSON.stringify(payload);
491
+
492
+ return fetch(voiceUrl + '/api/voice/transcribe', {
493
+ method: 'POST',
494
+ headers: { 'Content-Type': 'application/json' },
495
+ body: body,
496
+ signal: abortCtrl.signal,
497
+ });
498
+ })
499
+ .then(function (res) {
500
+ if (!res.ok) {
501
+ return res.text().then(function (body) {
502
+ try {
503
+ var d = JSON.parse(body);
504
+ throw new Error(d.error || res.status + ' ' + res.statusText);
505
+ } catch (parseErr) {
506
+ if (parseErr instanceof SyntaxError) {
507
+ throw new Error(res.status + ' ' + (res.statusText || 'error') + (body ? ': ' + body.slice(0, 120) : ''));
508
+ }
509
+ throw parseErr;
510
+ }
511
+ });
512
+ }
513
+ return res.json();
514
+ })
515
+ .then(function (data) {
516
+ clearTimeout(fetchTimeout);
517
+ if (!data.ok) throw new Error(data.error || 'Transcription failed');
518
+ setState('idle');
519
+ onResult(data.text || '');
520
+ })
521
+ .catch(function (err) {
522
+ clearTimeout(fetchTimeout);
523
+ if (err.name === 'AbortError') {
524
+ handleError(new Error('Transcription timed out — server did not respond within 30 seconds'));
525
+ } else {
526
+ handleError(err);
527
+ }
528
+ });
529
+ }
530
+
531
+ // ── error handling ─────────────────────────────────────────────────────────
532
+ function handleError(err) {
533
+ if (chimeTimer) { clearTimeout(chimeTimer); chimeTimer = null; }
534
+ stopStream();
535
+ setState('error');
536
+ onError(err);
537
+ errorTimer = setTimeout(function () {
538
+ if (state === 'error') setState('idle');
539
+ }, 3000);
540
+ }
541
+
542
+ // ── pointer / touch events ─────────────────────────────────────────────────
543
+ function onPointerDown(e) {
544
+ e.preventDefault();
545
+ startRecording();
546
+ }
547
+
548
+ function onPointerUp(e) {
549
+ e.preventDefault();
550
+ stopRecording();
551
+ }
552
+
553
+ // ── keyboard shortcut ──────────────────────────────────────────────────────
554
+ var _keydownHandler = null;
555
+ var _keyupHandler = null;
556
+
557
+ function setupHotkey() {
558
+ if (!hotkey) return;
559
+
560
+ _keydownHandler = function (e) {
561
+ if (e.repeat) return;
562
+ if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) return;
563
+ if (e.key.toLowerCase() === hotkey) {
564
+ e.preventDefault();
565
+ startRecording();
566
+ }
567
+ };
568
+
569
+ _keyupHandler = function (e) {
570
+ if (e.key.toLowerCase() === hotkey) {
571
+ e.preventDefault();
572
+ stopRecording();
573
+ }
574
+ };
575
+
576
+ document.addEventListener('keydown', _keydownHandler);
577
+ document.addEventListener('keyup', _keyupHandler);
578
+ }
579
+
580
+ function teardownHotkey() {
581
+ if (_keydownHandler) document.removeEventListener('keydown', _keydownHandler);
582
+ if (_keyupHandler) document.removeEventListener('keyup', _keyupHandler);
583
+ _keydownHandler = null;
584
+ _keyupHandler = null;
585
+ }
586
+
587
+ // ── public API ─────────────────────────────────────────────────────────────
588
+ return {
589
+ /**
590
+ * Render the mic button into containerEl.
591
+ * @param {HTMLElement} containerEl
592
+ */
593
+ mount: function (containerEl) {
594
+ if (!containerEl) throw new Error('VoiceWidget.mount: containerEl is required');
595
+ injectStyles();
596
+
597
+ // ── Wrapper ──
598
+ wrap = document.createElement('div');
599
+ wrap.className = 'vw-wrap';
600
+ wrap.setAttribute('data-state', 'idle');
601
+
602
+ // ── Button ──
603
+ btn = document.createElement('button');
604
+ btn.type = 'button';
605
+ btn.className = 'vw-btn';
606
+ btn.setAttribute('aria-label', idleLabel);
607
+ btn.setAttribute('data-state', 'idle');
608
+
609
+ spinnerEl = document.createElement('span');
610
+ spinnerEl.className = 'vw-spinner';
611
+
612
+ iconEl = document.createElement('span');
613
+ iconEl.className = 'vw-icon';
614
+ iconEl.innerHTML = ICONS.idle;
615
+
616
+ // Scanner strip container (Larson scanner bar)
617
+ var barsEl = document.createElement('span');
618
+ barsEl.className = 'vw-bars';
619
+ for (var i = 0; i < 7; i++) {
620
+ var bar = document.createElement('span');
621
+ bar.className = 'vw-bar';
622
+ barsEl.appendChild(bar);
623
+ }
624
+
625
+ labelEl = document.createElement('span');
626
+ labelEl.className = 'vw-label';
627
+ labelEl.setAttribute('aria-live', 'polite');
628
+ labelEl.textContent = idleLabel;
629
+
630
+ var modulatorEl = document.createElement('span');
631
+ modulatorEl.className = 'vw-modulator';
632
+ for (var m = 0; m < 6; m++) {
633
+ modulatorEl.appendChild(document.createElement('span'));
634
+ }
635
+
636
+ btn.appendChild(spinnerEl);
637
+ btn.appendChild(iconEl);
638
+ btn.appendChild(barsEl);
639
+ btn.appendChild(modulatorEl);
640
+ btn.appendChild(labelEl);
641
+
642
+ // pointer events
643
+ btn.addEventListener('pointerdown', onPointerDown);
644
+ btn.addEventListener('pointerup', onPointerUp);
645
+ btn.addEventListener('pointercancel', onPointerUp);
646
+ btn.addEventListener('contextmenu', function (e) { e.preventDefault(); });
647
+
648
+ wrap.appendChild(btn);
649
+
650
+ // ── Error flash overlay ──
651
+ var flash = document.createElement('span');
652
+ flash.className = 'vw-flash';
653
+ flash.setAttribute('aria-live', 'polite');
654
+ flash.setAttribute('role', 'status');
655
+ wrap.appendChild(flash);
656
+
657
+ containerEl.appendChild(wrap);
658
+
659
+ setupHotkey();
660
+ },
661
+
662
+ /** Remove the widget and all event listeners. */
663
+ destroy: function () {
664
+ clearTimeout(errorTimer);
665
+ if (chimeTimer) { clearTimeout(chimeTimer); chimeTimer = null; }
666
+ teardownHotkey();
667
+ stopStream();
668
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
669
+ mediaRecorder.stop();
670
+ }
671
+ if (btn) {
672
+ btn.removeEventListener('pointerdown', onPointerDown);
673
+ btn.removeEventListener('pointerup', onPointerUp);
674
+ btn.removeEventListener('pointercancel', onPointerUp);
675
+ }
676
+ if (_audioCtx) { _audioCtx.close(); _audioCtx = null; }
677
+ if (wrap && wrap.parentNode) {
678
+ wrap.parentNode.removeChild(wrap);
679
+ }
680
+ wrap = btn = iconEl = labelEl = spinnerEl = null;
681
+ },
682
+
683
+ /** Programmatically start recording. */
684
+ startRecording: startRecording,
685
+
686
+ /** Programmatically stop recording and trigger transcription. */
687
+ stopRecording: stopRecording,
688
+
689
+ /** Current state: 'idle' | 'recording' | 'transcribing' | 'error' */
690
+ get state() { return state; },
691
+ };
692
+ }
693
+
694
+ return { create: create };
695
+ });