@happyvertical/smrt-svelte 0.30.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/AGENTS.md +317 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +185 -0
- package/dist/Provider.svelte +204 -0
- package/dist/Provider.svelte.d.ts +73 -0
- package/dist/Provider.svelte.d.ts.map +1 -0
- package/dist/__tests__/app-state.test.js +156 -0
- package/dist/__tests__/warm-clients.test.js +186 -0
- package/dist/browser-ai/adapters/llm/factory.d.ts +38 -0
- package/dist/browser-ai/adapters/llm/factory.d.ts.map +1 -0
- package/dist/browser-ai/adapters/llm/factory.js +91 -0
- package/dist/browser-ai/adapters/llm/index.d.ts +7 -0
- package/dist/browser-ai/adapters/llm/index.d.ts.map +1 -0
- package/dist/browser-ai/adapters/llm/index.js +6 -0
- package/dist/browser-ai/adapters/llm/types.d.ts +182 -0
- package/dist/browser-ai/adapters/llm/types.d.ts.map +1 -0
- package/dist/browser-ai/adapters/llm/types.js +43 -0
- package/dist/browser-ai/adapters/llm/webllm.d.ts +33 -0
- package/dist/browser-ai/adapters/llm/webllm.d.ts.map +1 -0
- package/dist/browser-ai/adapters/llm/webllm.js +225 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts +31 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/browser-speech.js +217 -0
- package/dist/browser-ai/adapters/stt/factory.d.ts +49 -0
- package/dist/browser-ai/adapters/stt/factory.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/factory.js +110 -0
- package/dist/browser-ai/adapters/stt/index.d.ts +9 -0
- package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/index.js +8 -0
- package/dist/browser-ai/adapters/stt/types.d.ts +154 -0
- package/dist/browser-ai/adapters/stt/types.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/types.js +4 -0
- package/dist/browser-ai/adapters/stt/whisper-cpp.d.ts +46 -0
- package/dist/browser-ai/adapters/stt/whisper-cpp.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/whisper-cpp.js +348 -0
- package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +51 -0
- package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/whisper-wasm.js +380 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +42 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.js +235 -0
- package/dist/browser-ai/adapters/tts/factory.d.ts +53 -0
- package/dist/browser-ai/adapters/tts/factory.d.ts.map +1 -0
- package/dist/browser-ai/adapters/tts/factory.js +92 -0
- package/dist/browser-ai/adapters/tts/index.d.ts +7 -0
- package/dist/browser-ai/adapters/tts/index.d.ts.map +1 -0
- package/dist/browser-ai/adapters/tts/index.js +6 -0
- package/dist/browser-ai/adapters/tts/types.d.ts +140 -0
- package/dist/browser-ai/adapters/tts/types.d.ts.map +1 -0
- package/dist/browser-ai/adapters/tts/types.js +4 -0
- package/dist/browser-ai/capabilities/detector.d.ts +38 -0
- package/dist/browser-ai/capabilities/detector.d.ts.map +1 -0
- package/dist/browser-ai/capabilities/detector.js +211 -0
- package/dist/browser-ai/core/errors.d.ts +62 -0
- package/dist/browser-ai/core/errors.d.ts.map +1 -0
- package/dist/browser-ai/core/errors.js +92 -0
- package/dist/browser-ai/core/index.d.ts +6 -0
- package/dist/browser-ai/core/index.d.ts.map +1 -0
- package/dist/browser-ai/core/index.js +5 -0
- package/dist/browser-ai/core/types.d.ts +115 -0
- package/dist/browser-ai/core/types.d.ts.map +1 -0
- package/dist/browser-ai/core/types.js +34 -0
- package/dist/browser-ai/index.d.ts +12 -0
- package/dist/browser-ai/index.d.ts.map +1 -0
- package/dist/browser-ai/index.js +16 -0
- package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte +77 -0
- package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte.d.ts +16 -0
- package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte.d.ts.map +1 -0
- package/dist/browser-ai/svelte/components/CapabilityGate.svelte +57 -0
- package/dist/browser-ai/svelte/components/CapabilityGate.svelte.d.ts +15 -0
- package/dist/browser-ai/svelte/components/CapabilityGate.svelte.d.ts.map +1 -0
- package/dist/browser-ai/svelte/components/DownloadProgress.svelte +141 -0
- package/dist/browser-ai/svelte/components/DownloadProgress.svelte.d.ts +15 -0
- package/dist/browser-ai/svelte/components/DownloadProgress.svelte.d.ts.map +1 -0
- package/dist/browser-ai/svelte/components/STTTest.svelte +379 -0
- package/dist/browser-ai/svelte/components/STTTest.svelte.d.ts +9 -0
- package/dist/browser-ai/svelte/components/STTTest.svelte.d.ts.map +1 -0
- package/dist/browser-ai/svelte/components/VoiceInput.svelte +200 -0
- package/dist/browser-ai/svelte/components/VoiceInput.svelte.d.ts +16 -0
- package/dist/browser-ai/svelte/components/VoiceInput.svelte.d.ts.map +1 -0
- package/dist/browser-ai/svelte/index.d.ts +15 -0
- package/dist/browser-ai/svelte/index.d.ts.map +1 -0
- package/dist/browser-ai/svelte/index.js +28 -0
- package/dist/browser-ai/ui.d.ts +16 -0
- package/dist/browser-ai/ui.d.ts.map +1 -0
- package/dist/browser-ai/ui.js +67 -0
- package/dist/components/admin/AgentAdminPanel.svelte +111 -0
- package/dist/components/admin/AgentAdminPanel.svelte.d.ts +25 -0
- package/dist/components/admin/AgentAdminPanel.svelte.d.ts.map +1 -0
- package/dist/components/admin/AgentAdminTabs.svelte +280 -0
- package/dist/components/admin/AgentAdminTabs.svelte.d.ts +23 -0
- package/dist/components/admin/AgentAdminTabs.svelte.d.ts.map +1 -0
- package/dist/components/admin/AgentSettingsShell.svelte +257 -0
- package/dist/components/admin/AgentSettingsShell.svelte.d.ts +33 -0
- package/dist/components/admin/AgentSettingsShell.svelte.d.ts.map +1 -0
- package/dist/components/admin/index.d.ts +5 -0
- package/dist/components/admin/index.d.ts.map +1 -0
- package/dist/components/admin/index.js +6 -0
- package/dist/components/forms/AddressInput.svelte +500 -0
- package/dist/components/forms/AddressInput.svelte.d.ts +36 -0
- package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/CheckboxInput.svelte +208 -0
- package/dist/components/forms/CheckboxInput.svelte.d.ts +20 -0
- package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/DateRangeInput.svelte +628 -0
- package/dist/components/forms/DateRangeInput.svelte.d.ts +33 -0
- package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/DateTimeInput.svelte +521 -0
- package/dist/components/forms/DateTimeInput.svelte.d.ts +24 -0
- package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/FileUpload.svelte +358 -0
- package/dist/components/forms/FileUpload.svelte.d.ts +22 -0
- package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -0
- package/dist/components/forms/Form.svelte +771 -0
- package/dist/components/forms/Form.svelte.d.ts +26 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -0
- package/dist/components/forms/FormGroup.svelte +86 -0
- package/dist/components/forms/FormGroup.svelte.d.ts +13 -0
- package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -0
- package/dist/components/forms/FormMicButton.svelte +179 -0
- package/dist/components/forms/FormMicButton.svelte.d.ts +10 -0
- package/dist/components/forms/FormMicButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +83 -0
- package/dist/components/forms/Input.svelte.d.ts +9 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -0
- package/dist/components/forms/MeasurementInput.svelte +505 -0
- package/dist/components/forms/MeasurementInput.svelte.d.ts +35 -0
- package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/MoneyInput.svelte +412 -0
- package/dist/components/forms/MoneyInput.svelte.d.ts +30 -0
- package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/NumberInput.svelte +310 -0
- package/dist/components/forms/NumberInput.svelte.d.ts +28 -0
- package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/PhoneInput.svelte +530 -0
- package/dist/components/forms/PhoneInput.svelte.d.ts +22 -0
- package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/SearchInput.svelte +358 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +33 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +83 -0
- package/dist/components/forms/Select.svelte.d.ts +11 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -0
- package/dist/components/forms/SelectInput.svelte +254 -0
- package/dist/components/forms/SelectInput.svelte.d.ts +25 -0
- package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/TextInput.svelte +415 -0
- package/dist/components/forms/TextInput.svelte.d.ts +26 -0
- package/dist/components/forms/TextInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Textarea.svelte +85 -0
- package/dist/components/forms/Textarea.svelte.d.ts +10 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -0
- package/dist/components/forms/TextareaInput.svelte +386 -0
- package/dist/components/forms/TextareaInput.svelte.d.ts +26 -0
- package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Toggle.svelte +217 -0
- package/dist/components/forms/Toggle.svelte.d.ts +37 -0
- package/dist/components/forms/Toggle.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/AddressInput.behavior.test.js +122 -0
- package/dist/components/forms/__tests__/CheckboxInput.test.js +92 -0
- package/dist/components/forms/__tests__/DateRangeInput.behavior.test.js +135 -0
- package/dist/components/forms/__tests__/DateTimeInput.behavior.test.js +103 -0
- package/dist/components/forms/__tests__/FileUpload.test.js +90 -0
- package/dist/components/forms/__tests__/Form.behavior.test.js +137 -0
- package/dist/components/forms/__tests__/Form.test.js +58 -0
- package/dist/components/forms/__tests__/FormGroup.test.js +48 -0
- package/dist/components/forms/__tests__/FormMicButton.test.js +86 -0
- package/dist/components/forms/__tests__/Input.test.js +49 -0
- package/dist/components/forms/__tests__/MeasurementInput.behavior.test.js +129 -0
- package/dist/components/forms/__tests__/MoneyInput.behavior.test.js +124 -0
- package/dist/components/forms/__tests__/NumberInput.behavior.test.js +141 -0
- package/dist/components/forms/__tests__/PhoneInput.behavior.test.js +96 -0
- package/dist/components/forms/__tests__/SearchInput.test.js +79 -0
- package/dist/components/forms/__tests__/Select.test.js +37 -0
- package/dist/components/forms/__tests__/SelectInput.behavior.test.js +132 -0
- package/dist/components/forms/__tests__/TextInput.behavior.test.js +131 -0
- package/dist/components/forms/__tests__/Textarea.test.js +39 -0
- package/dist/components/forms/__tests__/TextareaInput.behavior.test.js +96 -0
- package/dist/components/forms/__tests__/Toggle.test.js +87 -0
- package/dist/components/forms/__tests__/composite-inputs-a11y.test.js +69 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte +16 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/rich-inputs-a11y.test.js +87 -0
- package/dist/components/forms/index.d.ts +25 -0
- package/dist/components/forms/index.d.ts.map +1 -0
- package/dist/components/forms/index.js +25 -0
- package/dist/components/forms/types.d.ts +33 -0
- package/dist/components/forms/types.d.ts.map +1 -0
- package/dist/components/forms/types.js +4 -0
- package/dist/components/module/ModulePanel.svelte +134 -0
- package/dist/components/module/ModulePanel.svelte.d.ts +22 -0
- package/dist/components/module/ModulePanel.svelte.d.ts.map +1 -0
- package/dist/components/module/index.d.ts +5 -0
- package/dist/components/module/index.d.ts.map +1 -0
- package/dist/components/module/index.js +4 -0
- package/dist/components/workspace/Breadcrumbs.svelte +141 -0
- package/dist/components/workspace/Breadcrumbs.svelte.d.ts +21 -0
- package/dist/components/workspace/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/components/workspace/NavTree.svelte +354 -0
- package/dist/components/workspace/NavTree.svelte.d.ts +45 -0
- package/dist/components/workspace/NavTree.svelte.d.ts.map +1 -0
- package/dist/components/workspace/README.md +34 -0
- package/dist/components/workspace/RoleShell.svelte +309 -0
- package/dist/components/workspace/RoleShell.svelte.d.ts +91 -0
- package/dist/components/workspace/RoleShell.svelte.d.ts.map +1 -0
- package/dist/components/workspace/WorkspaceShell.svelte +951 -0
- package/dist/components/workspace/WorkspaceShell.svelte.d.ts +112 -0
- package/dist/components/workspace/WorkspaceShell.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/RoleShell.test.js +772 -0
- package/dist/components/workspace/__tests__/WorkspaceShell.test.js +630 -0
- package/dist/components/workspace/__tests__/breadcrumbs-helpers.test.js +141 -0
- package/dist/components/workspace/__tests__/context-forwarding-harness.svelte +45 -0
- package/dist/components/workspace/__tests__/context-forwarding-harness.svelte.d.ts +21 -0
- package/dist/components/workspace/__tests__/context-forwarding-harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/define-tools-dock.test.js +1010 -0
- package/dist/components/workspace/__tests__/harness.svelte +25 -0
- package/dist/components/workspace/__tests__/harness.svelte.d.ts +14 -0
- package/dist/components/workspace/__tests__/harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/index.test.js +37 -0
- package/dist/components/workspace/__tests__/manifest-nav-helpers.test.js +24 -0
- package/dist/components/workspace/__tests__/manifest-nav.test.js +599 -0
- package/dist/components/workspace/__tests__/nav-helpers.test.js +95 -0
- package/dist/components/workspace/__tests__/render-harness.svelte +66 -0
- package/dist/components/workspace/__tests__/render-harness.svelte.d.ts +32 -0
- package/dist/components/workspace/__tests__/render-harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/render-tools-dock.test.js +243 -0
- package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte +58 -0
- package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte.d.ts +16 -0
- package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte +41 -0
- package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte.d.ts +13 -0
- package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/test-icon.svelte +17 -0
- package/dist/components/workspace/__tests__/test-icon.svelte.d.ts +19 -0
- package/dist/components/workspace/__tests__/test-icon.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte +38 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte.d.ts +22 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts +65 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.js +115 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.d.ts +15 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.js +7 -0
- package/dist/components/workspace/__tests__/typed-tool-fixture.test.js +115 -0
- package/dist/components/workspace/__tests__/use-harness-orphan.svelte +9 -0
- package/dist/components/workspace/__tests__/use-harness-orphan.svelte.d.ts +19 -0
- package/dist/components/workspace/__tests__/use-harness-orphan.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/use-harness.svelte +23 -0
- package/dist/components/workspace/__tests__/use-harness.svelte.d.ts +8 -0
- package/dist/components/workspace/__tests__/use-harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/__tests__/use-tools-dock.test.js +33 -0
- package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte +43 -0
- package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte.d.ts +11 -0
- package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte.d.ts.map +1 -0
- package/dist/components/workspace/breadcrumbs-helpers.d.ts +44 -0
- package/dist/components/workspace/breadcrumbs-helpers.d.ts.map +1 -0
- package/dist/components/workspace/breadcrumbs-helpers.js +88 -0
- package/dist/components/workspace/index.d.ts +16 -0
- package/dist/components/workspace/index.d.ts.map +1 -0
- package/dist/components/workspace/index.js +14 -0
- package/dist/components/workspace/manifest-nav.d.ts +200 -0
- package/dist/components/workspace/manifest-nav.d.ts.map +1 -0
- package/dist/components/workspace/manifest-nav.js +408 -0
- package/dist/components/workspace/nav-helpers.d.ts +36 -0
- package/dist/components/workspace/nav-helpers.d.ts.map +1 -0
- package/dist/components/workspace/nav-helpers.js +60 -0
- package/dist/components/workspace/server/__tests__/compose-availability.test.js +383 -0
- package/dist/components/workspace/server/__tests__/typed-context-fixture.d.ts +78 -0
- package/dist/components/workspace/server/__tests__/typed-context-fixture.d.ts.map +1 -0
- package/dist/components/workspace/server/__tests__/typed-context-fixture.js +104 -0
- package/dist/components/workspace/server/compose-availability.d.ts +73 -0
- package/dist/components/workspace/server/compose-availability.d.ts.map +1 -0
- package/dist/components/workspace/server/compose-availability.js +114 -0
- package/dist/components/workspace/server/index.d.ts +13 -0
- package/dist/components/workspace/server/index.d.ts.map +1 -0
- package/dist/components/workspace/server/index.js +11 -0
- package/dist/components/workspace/server/types.d.ts +108 -0
- package/dist/components/workspace/server/types.d.ts.map +1 -0
- package/dist/components/workspace/server/types.js +11 -0
- package/dist/components/workspace/tools-dock/ToolsDock.svelte +565 -0
- package/dist/components/workspace/tools-dock/ToolsDock.svelte.d.ts +14 -0
- package/dist/components/workspace/tools-dock/ToolsDock.svelte.d.ts.map +1 -0
- package/dist/components/workspace/tools-dock/define-tools-dock.svelte.d.ts +143 -0
- package/dist/components/workspace/tools-dock/define-tools-dock.svelte.d.ts.map +1 -0
- package/dist/components/workspace/tools-dock/define-tools-dock.svelte.js +487 -0
- package/dist/components/workspace/tools-dock/use-tools-dock.d.ts +41 -0
- package/dist/components/workspace/tools-dock/use-tools-dock.d.ts.map +1 -0
- package/dist/components/workspace/tools-dock/use-tools-dock.js +50 -0
- package/dist/components/workspace/types.d.ts +372 -0
- package/dist/components/workspace/types.d.ts.map +1 -0
- package/dist/components/workspace/types.js +6 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/useAppState.svelte.d.ts +46 -0
- package/dist/hooks/useAppState.svelte.d.ts.map +1 -0
- package/dist/hooks/useAppState.svelte.js +59 -0
- package/dist/hooks/useAuth.svelte.d.ts +41 -0
- package/dist/hooks/useAuth.svelte.d.ts.map +1 -0
- package/dist/hooks/useAuth.svelte.js +43 -0
- package/dist/hooks/useLLM.svelte.d.ts +69 -0
- package/dist/hooks/useLLM.svelte.d.ts.map +1 -0
- package/dist/hooks/useLLM.svelte.js +85 -0
- package/dist/hooks/useSTT.svelte.d.ts +68 -0
- package/dist/hooks/useSTT.svelte.d.ts.map +1 -0
- package/dist/hooks/useSTT.svelte.js +97 -0
- package/dist/hooks/useSocket.svelte.d.ts +45 -0
- package/dist/hooks/useSocket.svelte.d.ts.map +1 -0
- package/dist/hooks/useSocket.svelte.js +54 -0
- package/dist/hooks/useTTS.svelte.d.ts +65 -0
- package/dist/hooks/useTTS.svelte.d.ts.map +1 -0
- package/dist/hooks/useTTS.svelte.js +93 -0
- package/dist/hooks/useTheme.d.ts +13 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +16 -0
- package/dist/i18n/__tests__/server.spec.js +50 -0
- package/dist/i18n/server.d.ts +47 -0
- package/dist/i18n/server.d.ts.map +1 -0
- package/dist/i18n/server.js +58 -0
- package/dist/i18n/strings.forms.d.ts +33 -0
- package/dist/i18n/strings.forms.d.ts.map +1 -0
- package/dist/i18n/strings.forms.js +54 -0
- package/dist/i18n/strings.workspace.d.ts +34 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -0
- package/dist/i18n/strings.workspace.js +40 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/state/__tests__/warm-clients.test.js +40 -0
- package/dist/state/app-state.d.ts +308 -0
- package/dist/state/app-state.d.ts.map +1 -0
- package/dist/state/app-state.js +64 -0
- package/dist/state/app-state.svelte.d.ts +196 -0
- package/dist/state/app-state.svelte.d.ts.map +1 -0
- package/dist/state/app-state.svelte.js +774 -0
- package/dist/state/context.d.ts +23 -0
- package/dist/state/context.d.ts.map +1 -0
- package/dist/state/context.js +32 -0
- package/dist/state/form-context.d.ts +59 -0
- package/dist/state/form-context.d.ts.map +1 -0
- package/dist/state/form-context.js +31 -0
- package/dist/state/form-group-context.d.ts +13 -0
- package/dist/state/form-group-context.d.ts.map +1 -0
- package/dist/state/form-group-context.js +28 -0
- package/dist/state/index.d.ts +9 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +8 -0
- package/dist/state/warm-clients.d.ts +136 -0
- package/dist/state/warm-clients.d.ts.map +1 -0
- package/dist/state/warm-clients.js +231 -0
- package/package.json +137 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import { onDestroy } from 'svelte';
|
|
5
|
+
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
6
|
+
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
7
|
+
import { M } from '../../i18n/strings.forms.js';
|
|
8
|
+
import {
|
|
9
|
+
type FieldDefinition,
|
|
10
|
+
type SMRTFormContext,
|
|
11
|
+
setFormContext,
|
|
12
|
+
} from '../../state/form-context.js';
|
|
13
|
+
import type { LLMModelId, STTAdapterType } from './types.js';
|
|
14
|
+
|
|
15
|
+
const { t } = useI18n();
|
|
16
|
+
|
|
17
|
+
export interface Props {
|
|
18
|
+
/** Form children */
|
|
19
|
+
children: Snippet;
|
|
20
|
+
/** Show mode toggle button */
|
|
21
|
+
showModeToggle?: boolean;
|
|
22
|
+
/** Show form-level listen button */
|
|
23
|
+
showFormListen?: boolean;
|
|
24
|
+
/** Silence timeout in seconds before stopping */
|
|
25
|
+
silenceTimeout?: number;
|
|
26
|
+
/** STT adapter type */
|
|
27
|
+
sttAdapter?: STTAdapterType;
|
|
28
|
+
/** LLM model for extraction (or 'none' for regex-only) */
|
|
29
|
+
llmModel?: LLMModelId;
|
|
30
|
+
/** Called when form is submitted (if provided, prevents native submission) */
|
|
31
|
+
onsubmit?: (data: Record<string, unknown>) => void;
|
|
32
|
+
/** HTTP method for native form submission (default: GET) */
|
|
33
|
+
method?: 'GET' | 'POST';
|
|
34
|
+
/** Form action URL for native form submission */
|
|
35
|
+
action?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
children,
|
|
40
|
+
showModeToggle = false,
|
|
41
|
+
showFormListen = false,
|
|
42
|
+
silenceTimeout = 5,
|
|
43
|
+
sttAdapter = 'whisper-wasm',
|
|
44
|
+
llmModel = 'none',
|
|
45
|
+
onsubmit,
|
|
46
|
+
method,
|
|
47
|
+
action,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
const app = useAppState();
|
|
51
|
+
const stt = useSTT();
|
|
52
|
+
|
|
53
|
+
// Internal state
|
|
54
|
+
let fields = $state<Map<string, FieldDefinition>>(new Map());
|
|
55
|
+
let isFormListening = $state(false);
|
|
56
|
+
let isExtracting = $state(false);
|
|
57
|
+
let spokenText = $state('');
|
|
58
|
+
let extractError = $state<string | null>(null);
|
|
59
|
+
let lastSpeechTime = $state(0);
|
|
60
|
+
let silenceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
61
|
+
let audioLevelInterval: ReturnType<typeof setInterval> | null = null;
|
|
62
|
+
let audioContext: AudioContext | null = null;
|
|
63
|
+
let analyser: AnalyserNode | null = null;
|
|
64
|
+
let levelMonitorStream: MediaStream | null = null;
|
|
65
|
+
|
|
66
|
+
const isSmrt = $derived(app.state.mode === 'smrt');
|
|
67
|
+
|
|
68
|
+
// Create form context
|
|
69
|
+
const formContext: SMRTFormContext = {
|
|
70
|
+
get mode() {
|
|
71
|
+
return app.state.mode === 'smrt' ? 'smrt' : 'default';
|
|
72
|
+
},
|
|
73
|
+
registerField(field: FieldDefinition) {
|
|
74
|
+
fields.set(field.name, field);
|
|
75
|
+
fields = new Map(fields); // Trigger reactivity
|
|
76
|
+
},
|
|
77
|
+
unregisterField(name: string) {
|
|
78
|
+
fields.delete(name);
|
|
79
|
+
fields = new Map(fields);
|
|
80
|
+
},
|
|
81
|
+
getFieldSchema() {
|
|
82
|
+
return Array.from(fields.values());
|
|
83
|
+
},
|
|
84
|
+
get isFormListening() {
|
|
85
|
+
return isFormListening;
|
|
86
|
+
},
|
|
87
|
+
get isExtracting() {
|
|
88
|
+
return isExtracting;
|
|
89
|
+
},
|
|
90
|
+
toggleListening: () => toggleFormListening(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Provide context to children
|
|
94
|
+
setFormContext(formContext);
|
|
95
|
+
|
|
96
|
+
// Clean up extracted values based on field type
|
|
97
|
+
function cleanValue(value: unknown, fieldType: string): unknown {
|
|
98
|
+
if (typeof value !== 'string') return value;
|
|
99
|
+
|
|
100
|
+
let cleaned = value.trim();
|
|
101
|
+
|
|
102
|
+
switch (fieldType) {
|
|
103
|
+
case 'text':
|
|
104
|
+
// Remove trailing periods from names
|
|
105
|
+
cleaned = cleaned.replace(/\.$/, '');
|
|
106
|
+
// Remove common speech artifacts
|
|
107
|
+
cleaned = cleaned.replace(/^(my |the |it's |is )/i, '');
|
|
108
|
+
break;
|
|
109
|
+
case 'email':
|
|
110
|
+
// Ensure email is properly formatted
|
|
111
|
+
cleaned = cleaned
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/\s+at\s+/gi, '@')
|
|
114
|
+
.replace(/\bat\b/gi, '@')
|
|
115
|
+
.replace(/\s+dot\s+/gi, '.')
|
|
116
|
+
.replace(/\bdot\b/gi, '.')
|
|
117
|
+
.replace(/\s+/g, '');
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return cleaned;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Regex-based extraction from spoken text
|
|
125
|
+
function extractFieldsFromText(text: string): Record<string, unknown> {
|
|
126
|
+
const result: Record<string, unknown> = {};
|
|
127
|
+
const fieldDefs = Array.from(fields.values());
|
|
128
|
+
|
|
129
|
+
// Normalize text: remove commas, extra spaces
|
|
130
|
+
const normalized = text.replace(/,/g, ' ').replace(/\s+/g, ' ').trim();
|
|
131
|
+
|
|
132
|
+
// Build list of all field triggers for boundary detection
|
|
133
|
+
const allTriggers: string[] = [];
|
|
134
|
+
for (const f of fieldDefs) {
|
|
135
|
+
allTriggers.push(f.name.toLowerCase());
|
|
136
|
+
if (f.label) {
|
|
137
|
+
allTriggers.push(f.label.toLowerCase().replace(/\s+/g, '\\s+'));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const field of fieldDefs) {
|
|
142
|
+
const name = field.name.toLowerCase();
|
|
143
|
+
const label = field.label?.toLowerCase() || '';
|
|
144
|
+
|
|
145
|
+
// Build trigger patterns for this field
|
|
146
|
+
// For "email" field with label "Email Address", match both:
|
|
147
|
+
// - "email ..."
|
|
148
|
+
// - "email address ..."
|
|
149
|
+
const triggers: string[] = [];
|
|
150
|
+
|
|
151
|
+
// Add label first (more specific, e.g., "email address")
|
|
152
|
+
if (label) {
|
|
153
|
+
triggers.push(label.replace(/\s+/g, '\\s+'));
|
|
154
|
+
}
|
|
155
|
+
// Add field name
|
|
156
|
+
triggers.push(name);
|
|
157
|
+
|
|
158
|
+
// Build boundary pattern (other field triggers)
|
|
159
|
+
const otherTriggers = allTriggers.filter(
|
|
160
|
+
(t) => t !== name && t !== label.replace(/\s+/g, '\\s+'),
|
|
161
|
+
);
|
|
162
|
+
const boundaryPattern =
|
|
163
|
+
otherTriggers.length > 0
|
|
164
|
+
? `(?=\\s+(?:${otherTriggers.join('|')})|$)`
|
|
165
|
+
: '$';
|
|
166
|
+
|
|
167
|
+
for (const trigger of triggers) {
|
|
168
|
+
// Match: trigger followed by optional "is", then capture value until boundary
|
|
169
|
+
const pattern = new RegExp(
|
|
170
|
+
`(?:^|\\s)${trigger}\\s+(?:is\\s+|and\\s+)?(.+?)${boundaryPattern}`,
|
|
171
|
+
'i',
|
|
172
|
+
);
|
|
173
|
+
const match = normalized.match(pattern);
|
|
174
|
+
|
|
175
|
+
if (match?.[1]) {
|
|
176
|
+
let value = match[1].trim();
|
|
177
|
+
|
|
178
|
+
// Remove trailing punctuation
|
|
179
|
+
value = value.replace(/[.,!?]+$/, '').trim();
|
|
180
|
+
|
|
181
|
+
// Clean up based on field type
|
|
182
|
+
if (field.type === 'email') {
|
|
183
|
+
value = value
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
.replace(/\s+at\s+/gi, '@')
|
|
186
|
+
.replace(/\bat\b/gi, '@')
|
|
187
|
+
.replace(/\s+dot\s+/gi, '.')
|
|
188
|
+
.replace(/\bdot\b/gi, '.')
|
|
189
|
+
.replace(/\s+/g, '');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (value) {
|
|
193
|
+
result[field.name] = value;
|
|
194
|
+
break; // Found value for this field, move to next
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Extract fields from spoken text
|
|
204
|
+
// Currently uses regex-only since small local LLMs produce unreliable output
|
|
205
|
+
async function extractFields(text: string): Promise<Record<string, unknown>> {
|
|
206
|
+
// Use regex extraction directly - fast and reliable
|
|
207
|
+
// TODO: Add LLM enhancement when larger models are available
|
|
208
|
+
const result = extractFieldsFromText(text);
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Apply extracted values to fields with cleanup
|
|
213
|
+
function applyExtractedValues(values: Record<string, unknown>) {
|
|
214
|
+
for (const [name, value] of Object.entries(values)) {
|
|
215
|
+
const field = fields.get(name);
|
|
216
|
+
if (field && value !== undefined && value !== null) {
|
|
217
|
+
const cleanedValue = cleanValue(value, field.type);
|
|
218
|
+
field.setValue(cleanedValue);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Reset silence timer
|
|
224
|
+
function resetSilenceTimer() {
|
|
225
|
+
if (silenceTimer) {
|
|
226
|
+
clearTimeout(silenceTimer);
|
|
227
|
+
}
|
|
228
|
+
lastSpeechTime = Date.now();
|
|
229
|
+
silenceTimer = setTimeout(() => {
|
|
230
|
+
if (isFormListening) {
|
|
231
|
+
stopFormListening();
|
|
232
|
+
}
|
|
233
|
+
}, silenceTimeout * 1000);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Start monitoring audio levels to detect speech (for Whisper which has no interim results)
|
|
237
|
+
async function startAudioLevelMonitoring(stream: MediaStream) {
|
|
238
|
+
try {
|
|
239
|
+
levelMonitorStream = stream;
|
|
240
|
+
audioContext = new AudioContext();
|
|
241
|
+
|
|
242
|
+
// Ensure AudioContext is running (may be suspended until user interaction)
|
|
243
|
+
if (audioContext.state === 'suspended') {
|
|
244
|
+
await audioContext.resume();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
analyser = audioContext.createAnalyser();
|
|
248
|
+
analyser.fftSize = 256;
|
|
249
|
+
analyser.smoothingTimeConstant = 0.3;
|
|
250
|
+
|
|
251
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
252
|
+
source.connect(analyser);
|
|
253
|
+
|
|
254
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
255
|
+
const SPEECH_THRESHOLD = 20; // Lowered from 30 for better sensitivity
|
|
256
|
+
let checksWithSpeech = 0;
|
|
257
|
+
|
|
258
|
+
audioLevelInterval = setInterval(() => {
|
|
259
|
+
if (!analyser || !isFormListening) return;
|
|
260
|
+
|
|
261
|
+
analyser.getByteFrequencyData(dataArray);
|
|
262
|
+
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
|
|
263
|
+
|
|
264
|
+
// Log periodically to debug
|
|
265
|
+
checksWithSpeech++;
|
|
266
|
+
if (checksWithSpeech % 10 === 0) {
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (average > SPEECH_THRESHOLD) {
|
|
270
|
+
// Speech detected - reset silence timer
|
|
271
|
+
resetSilenceTimer();
|
|
272
|
+
}
|
|
273
|
+
}, 200); // Check every 200ms
|
|
274
|
+
} catch (err) {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Stop audio level monitoring
|
|
278
|
+
function stopAudioLevelMonitoring() {
|
|
279
|
+
if (audioLevelInterval) {
|
|
280
|
+
clearInterval(audioLevelInterval);
|
|
281
|
+
audioLevelInterval = null;
|
|
282
|
+
}
|
|
283
|
+
if (levelMonitorStream) {
|
|
284
|
+
for (const track of levelMonitorStream.getTracks()) {
|
|
285
|
+
track.stop();
|
|
286
|
+
}
|
|
287
|
+
levelMonitorStream = null;
|
|
288
|
+
}
|
|
289
|
+
if (audioContext) {
|
|
290
|
+
audioContext.close().catch(() => {});
|
|
291
|
+
audioContext = null;
|
|
292
|
+
analyser = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check for "done" keyword
|
|
297
|
+
function checkForDoneKeyword(text: string): boolean {
|
|
298
|
+
const lowerText = text.toLowerCase();
|
|
299
|
+
// Check if the last word is "done" or ends with "done"
|
|
300
|
+
return lowerText.endsWith('done') || lowerText.endsWith('done.');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Guard to prevent multiple simultaneous stop calls
|
|
304
|
+
let isStopping = false;
|
|
305
|
+
// Guard to prevent premature stop during startup
|
|
306
|
+
let isStarting = false;
|
|
307
|
+
// Timestamp of last stop to prevent immediate restart
|
|
308
|
+
let lastStopTime = 0;
|
|
309
|
+
const RESTART_COOLDOWN_MS = 1000; // 1 second cooldown after stopping
|
|
310
|
+
// Track the last processed STT result to avoid re-processing stale results
|
|
311
|
+
let lastProcessedResult = '';
|
|
312
|
+
|
|
313
|
+
// Watch STT results while form is listening
|
|
314
|
+
// Only track speech and check for "done" - extraction happens at the end
|
|
315
|
+
$effect(() => {
|
|
316
|
+
if (isFormListening && stt.lastResult && !isStopping) {
|
|
317
|
+
const newText = stt.lastResult;
|
|
318
|
+
|
|
319
|
+
// Skip if this is the same result we already processed (stale from previous session)
|
|
320
|
+
if (newText === lastProcessedResult) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
lastProcessedResult = newText;
|
|
324
|
+
spokenText = newText;
|
|
325
|
+
|
|
326
|
+
// Reset silence timer on new speech (for browser STT with interim results)
|
|
327
|
+
if (sttAdapter === 'browser-speech') {
|
|
328
|
+
resetSilenceTimer();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check for "done" keyword
|
|
332
|
+
if (checkForDoneKeyword(newText)) {
|
|
333
|
+
// Don't set isStopping here - let stopFormListening() do it after passing the guard
|
|
334
|
+
stopFormListening();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Watch for STT stopping unexpectedly (e.g., browser STT auto-stops after silence)
|
|
340
|
+
$effect(() => {
|
|
341
|
+
// If form thinks we're listening but STT has stopped, handle it
|
|
342
|
+
// Don't trigger during startup phase (isStarting) or shutdown phase (isStopping)
|
|
343
|
+
if (isFormListening && !stt.isListening && !isStopping && !isStarting) {
|
|
344
|
+
// Use setTimeout to avoid state update during render
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
if (isFormListening && !isStopping && !isStarting) {
|
|
347
|
+
stopFormListening();
|
|
348
|
+
}
|
|
349
|
+
}, 0);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
async function toggleFormListening() {
|
|
354
|
+
if (isFormListening) {
|
|
355
|
+
await stopFormListening();
|
|
356
|
+
} else {
|
|
357
|
+
// Prevent immediate restart after stopping (UI might lag behind state)
|
|
358
|
+
const timeSinceStop = Date.now() - lastStopTime;
|
|
359
|
+
if (lastStopTime > 0 && timeSinceStop < RESTART_COOLDOWN_MS) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
await startFormListening();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function startFormListening() {
|
|
367
|
+
if (!isSmrt) return;
|
|
368
|
+
|
|
369
|
+
// Set starting flag to prevent premature stop detection
|
|
370
|
+
isStarting = true;
|
|
371
|
+
|
|
372
|
+
// Initialize STT with selected adapter
|
|
373
|
+
if (!stt.isReady || stt.adapterType !== sttAdapter) {
|
|
374
|
+
await stt.initialize({ type: sttAdapter });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
extractError = null;
|
|
378
|
+
spokenText = '';
|
|
379
|
+
// Set to current stale result so the effect skips it, but new results will be processed
|
|
380
|
+
lastProcessedResult = stt.lastResult || '';
|
|
381
|
+
isFormListening = true;
|
|
382
|
+
isStopping = false;
|
|
383
|
+
lastSpeechTime = Date.now();
|
|
384
|
+
|
|
385
|
+
// Start silence timer
|
|
386
|
+
resetSilenceTimer();
|
|
387
|
+
|
|
388
|
+
// Start audio level monitoring for Whisper (no interim results)
|
|
389
|
+
// Browser STT has interim results so doesn't need this
|
|
390
|
+
if (sttAdapter === 'whisper-wasm') {
|
|
391
|
+
try {
|
|
392
|
+
const levelStream = await navigator.mediaDevices.getUserMedia({
|
|
393
|
+
audio: true,
|
|
394
|
+
});
|
|
395
|
+
startAudioLevelMonitoring(levelStream);
|
|
396
|
+
} catch (err) {}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await stt.start({ continuous: true, interimResults: true });
|
|
400
|
+
|
|
401
|
+
// Clear starting flag now that STT is actually listening
|
|
402
|
+
isStarting = false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function stopFormListening() {
|
|
406
|
+
if (!isFormListening || isStopping) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
isStopping = true;
|
|
410
|
+
isStarting = false; // Ensure starting flag is cleared
|
|
411
|
+
|
|
412
|
+
// Clear silence timer
|
|
413
|
+
if (silenceTimer) {
|
|
414
|
+
clearTimeout(silenceTimer);
|
|
415
|
+
silenceTimer = null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Stop audio level monitoring
|
|
419
|
+
stopAudioLevelMonitoring();
|
|
420
|
+
|
|
421
|
+
// IMPORTANT: Keep isFormListening = true until after stt.stop() completes
|
|
422
|
+
// This allows the $effect to capture any final results
|
|
423
|
+
await stt.stop();
|
|
424
|
+
|
|
425
|
+
// Now capture the final result AFTER stop completes
|
|
426
|
+
// stt.stop() waits for transcription to finish in whisper-wasm
|
|
427
|
+
const finalResult = stt.lastResult || '';
|
|
428
|
+
|
|
429
|
+
// NOW we can set isFormListening to false
|
|
430
|
+
isFormListening = false;
|
|
431
|
+
|
|
432
|
+
// Final extraction on the captured result
|
|
433
|
+
const textToExtract = (finalResult || spokenText || '')
|
|
434
|
+
.replace(/\s*done\.?$/i, '')
|
|
435
|
+
.trim();
|
|
436
|
+
|
|
437
|
+
if (textToExtract) {
|
|
438
|
+
isExtracting = true;
|
|
439
|
+
extractError = null;
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const values = await extractFields(textToExtract);
|
|
443
|
+
applyExtractedValues(values);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
extractError =
|
|
446
|
+
err instanceof Error ? err.message : 'Failed to extract fields';
|
|
447
|
+
} finally {
|
|
448
|
+
isExtracting = false;
|
|
449
|
+
isStopping = false;
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
isStopping = false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Record stop time to prevent immediate restart
|
|
456
|
+
lastStopTime = Date.now();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Cleanup on destroy
|
|
460
|
+
onDestroy(() => {
|
|
461
|
+
if (silenceTimer) {
|
|
462
|
+
clearTimeout(silenceTimer);
|
|
463
|
+
}
|
|
464
|
+
stopAudioLevelMonitoring();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
function handleModeToggle() {
|
|
468
|
+
const newMode = app.state.mode === 'smrt' ? 'default' : 'smrt';
|
|
469
|
+
app.setMode(newMode);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function handleSubmit(e: Event) {
|
|
473
|
+
// Only prevent default if we have an onsubmit handler
|
|
474
|
+
// This allows native form submission for SvelteKit form actions
|
|
475
|
+
if (onsubmit) {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
const data: Record<string, unknown> = {};
|
|
478
|
+
for (const [name, field] of fields) {
|
|
479
|
+
data[name] = field.getValue();
|
|
480
|
+
}
|
|
481
|
+
onsubmit(data);
|
|
482
|
+
}
|
|
483
|
+
// Otherwise, let native form submission happen (e.g., for SvelteKit use:enhance)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function getFormData(): Record<string, unknown> {
|
|
487
|
+
const data: Record<string, unknown> = {};
|
|
488
|
+
for (const [name, field] of fields) {
|
|
489
|
+
data[name] = field.getValue();
|
|
490
|
+
}
|
|
491
|
+
return data;
|
|
492
|
+
}
|
|
493
|
+
</script>
|
|
494
|
+
|
|
495
|
+
<form class="smrt-form" onsubmit={handleSubmit} {method} {action}>
|
|
496
|
+
<!--
|
|
497
|
+
Screen-reader status region (L1 #1420): announces async STT / field-
|
|
498
|
+
extraction state politely, since the visible cues live on a button whose
|
|
499
|
+
label change isn't reliably announced. Visually hidden — the button still
|
|
500
|
+
shows the same state to sighted users.
|
|
501
|
+
-->
|
|
502
|
+
<div class="smrt-form__status" role="status" aria-live="polite">
|
|
503
|
+
{#if isExtracting}
|
|
504
|
+
{t(M['ui.form.extracting'])}
|
|
505
|
+
{:else if isFormListening}
|
|
506
|
+
{t(M['ui.form.listening_status'])}
|
|
507
|
+
{/if}
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
{#if showModeToggle || showFormListen}
|
|
511
|
+
<div class="form-controls">
|
|
512
|
+
{#if showModeToggle}
|
|
513
|
+
<div class="mode-toggle">
|
|
514
|
+
<button
|
|
515
|
+
type="button"
|
|
516
|
+
class="mode-btn"
|
|
517
|
+
class:active={!isSmrt}
|
|
518
|
+
onclick={handleModeToggle}
|
|
519
|
+
>
|
|
520
|
+
Dumb
|
|
521
|
+
</button>
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
class="mode-btn"
|
|
525
|
+
class:active={isSmrt}
|
|
526
|
+
onclick={handleModeToggle}
|
|
527
|
+
>
|
|
528
|
+
s-m-r-t
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
{/if}
|
|
532
|
+
|
|
533
|
+
{#if showFormListen && isSmrt}
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
class="form-listen-btn"
|
|
537
|
+
class:listening={isFormListening}
|
|
538
|
+
class:extracting={isExtracting}
|
|
539
|
+
onclick={toggleFormListening}
|
|
540
|
+
>
|
|
541
|
+
{#if isExtracting}
|
|
542
|
+
<svg
|
|
543
|
+
width="18"
|
|
544
|
+
height="18"
|
|
545
|
+
viewBox="0 0 24 24"
|
|
546
|
+
fill="none"
|
|
547
|
+
stroke="currentColor"
|
|
548
|
+
stroke-width="2"
|
|
549
|
+
class="spinner"
|
|
550
|
+
>
|
|
551
|
+
<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/>
|
|
552
|
+
</svg>
|
|
553
|
+
Extracting...
|
|
554
|
+
{:else if isFormListening}
|
|
555
|
+
<svg
|
|
556
|
+
width="18"
|
|
557
|
+
height="18"
|
|
558
|
+
viewBox="0 0 24 24"
|
|
559
|
+
fill="none"
|
|
560
|
+
stroke="currentColor"
|
|
561
|
+
stroke-width="2"
|
|
562
|
+
stroke-linecap="round"
|
|
563
|
+
stroke-linejoin="round"
|
|
564
|
+
>
|
|
565
|
+
<rect x="6" y="4" width="4" height="16"/>
|
|
566
|
+
<rect x="14" y="4" width="4" height="16"/>
|
|
567
|
+
</svg>
|
|
568
|
+
{t(M['ui.form.listening_button'])}
|
|
569
|
+
{:else}
|
|
570
|
+
<svg
|
|
571
|
+
width="18"
|
|
572
|
+
height="18"
|
|
573
|
+
viewBox="0 0 24 24"
|
|
574
|
+
fill="none"
|
|
575
|
+
stroke="currentColor"
|
|
576
|
+
stroke-width="2"
|
|
577
|
+
stroke-linecap="round"
|
|
578
|
+
stroke-linejoin="round"
|
|
579
|
+
>
|
|
580
|
+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
|
|
581
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
582
|
+
<line x1="12" x2="12" y1="19" y2="22"/>
|
|
583
|
+
</svg>
|
|
584
|
+
{t(M['ui.form.speak_all_fields'])}
|
|
585
|
+
{/if}
|
|
586
|
+
</button>
|
|
587
|
+
{/if}
|
|
588
|
+
</div>
|
|
589
|
+
{/if}
|
|
590
|
+
|
|
591
|
+
{#if extractError}
|
|
592
|
+
<div class="extract-error" role="alert">{extractError}</div>
|
|
593
|
+
{/if}
|
|
594
|
+
|
|
595
|
+
<div class="form-fields">
|
|
596
|
+
{@render children()}
|
|
597
|
+
</div>
|
|
598
|
+
</form>
|
|
599
|
+
|
|
600
|
+
{#if isFormListening && spokenText}
|
|
601
|
+
<div class="spoken-toaster">
|
|
602
|
+
<strong>{t(M['ui.form.you_said'])}</strong> {spokenText}
|
|
603
|
+
</div>
|
|
604
|
+
{/if}
|
|
605
|
+
|
|
606
|
+
<style>
|
|
607
|
+
.smrt-form {
|
|
608
|
+
display: flex;
|
|
609
|
+
flex-direction: column;
|
|
610
|
+
gap: var(--smrt-spacing-4, 16px);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* Visually-hidden live region (L1 #1420) — announced to screen readers only. */
|
|
614
|
+
.smrt-form__status {
|
|
615
|
+
position: absolute;
|
|
616
|
+
width: 1px;
|
|
617
|
+
height: 1px;
|
|
618
|
+
padding: 0;
|
|
619
|
+
margin: -1px;
|
|
620
|
+
overflow: hidden;
|
|
621
|
+
clip: rect(0, 0, 0, 0);
|
|
622
|
+
white-space: nowrap;
|
|
623
|
+
border: 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.form-controls {
|
|
627
|
+
display: flex;
|
|
628
|
+
align-items: center;
|
|
629
|
+
gap: var(--smrt-spacing-4, 16px);
|
|
630
|
+
flex-wrap: wrap;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.mode-toggle {
|
|
634
|
+
display: flex;
|
|
635
|
+
background: var(--smrt-color-surface-container-high, #f3f4f6);
|
|
636
|
+
padding: var(--smrt-spacing-1, 4px);
|
|
637
|
+
border-radius: var(--smrt-radius-md, 8px);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.mode-btn {
|
|
641
|
+
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-4, 16px);
|
|
642
|
+
border: none;
|
|
643
|
+
background: transparent;
|
|
644
|
+
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
645
|
+
border-radius: var(--smrt-radius-md, 8px);
|
|
646
|
+
cursor: pointer;
|
|
647
|
+
font-size: var(--smrt-typography-label-large-size, 0.875rem);
|
|
648
|
+
font-weight: var(--smrt-typography-weight-medium, 500);
|
|
649
|
+
transition: all var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.mode-btn:hover {
|
|
653
|
+
color: var(--smrt-color-on-surface, #374151);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.mode-btn.active {
|
|
657
|
+
background: var(--smrt-color-surface, #fff);
|
|
658
|
+
color: var(--smrt-color-primary, #3b82f6);
|
|
659
|
+
box-shadow: var(--smrt-elevation-1, 0 1px 3px color-mix(in srgb, var(--smrt-color-shadow) 10%, transparent));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.form-listen-btn {
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
gap: var(--smrt-spacing-2, 8px);
|
|
666
|
+
padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-5, 20px);
|
|
667
|
+
border: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
668
|
+
background: var(--smrt-color-surface);
|
|
669
|
+
color: var(--smrt-color-primary);
|
|
670
|
+
border-radius: var(--smrt-radius-md, 8px);
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
font-size: var(--smrt-typography-label-large-size, 0.875rem);
|
|
673
|
+
font-weight: var(--smrt-typography-weight-medium, 500);
|
|
674
|
+
transition: all var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.form-listen-btn:hover {
|
|
678
|
+
background: var(--smrt-color-primary-container, #eff6ff);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.form-listen-btn.listening {
|
|
682
|
+
background: var(--smrt-color-primary, #22c55e);
|
|
683
|
+
border-color: var(--smrt-color-primary, #22c55e);
|
|
684
|
+
color: var(--smrt-color-on-primary, #fff);
|
|
685
|
+
animation: pulse-btn 1.5s var(--smrt-easing-standard, ease-in-out) infinite;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.form-listen-btn.extracting {
|
|
689
|
+
background: var(--smrt-color-secondary, #f59e0b);
|
|
690
|
+
border-color: var(--smrt-color-secondary, #f59e0b);
|
|
691
|
+
color: var(--smrt-color-on-secondary, #fff);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.form-listen-btn:disabled {
|
|
695
|
+
cursor: not-allowed;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
@keyframes pulse-btn {
|
|
699
|
+
0%, 100% {
|
|
700
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--smrt-color-success) 40%, transparent);
|
|
701
|
+
}
|
|
702
|
+
50% {
|
|
703
|
+
box-shadow: 0 0 0 8px transparent;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
@media (prefers-reduced-motion: reduce) {
|
|
708
|
+
.form-listen-btn.listening {
|
|
709
|
+
animation: none;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.spinner {
|
|
714
|
+
animation: spin 1s linear infinite;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
@keyframes spin {
|
|
718
|
+
to {
|
|
719
|
+
transform: rotate(360deg);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.spoken-toaster {
|
|
724
|
+
position: fixed;
|
|
725
|
+
bottom: 0;
|
|
726
|
+
left: 0;
|
|
727
|
+
right: 0;
|
|
728
|
+
padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-6, 24px);
|
|
729
|
+
/* 90%-opacity primary. The old `--smrt-color-primary-rgb` channel token
|
|
730
|
+
was never emitted (issue #1431); color-mix derives the alpha from the
|
|
731
|
+
emitted `--smrt-color-primary` token instead. */
|
|
732
|
+
background: color-mix(in srgb, var(--smrt-color-primary, #166534) 90%, transparent);
|
|
733
|
+
color: white;
|
|
734
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
735
|
+
box-shadow: 0 -2px 12px color-mix(in srgb, var(--smrt-color-shadow) 15%, transparent);
|
|
736
|
+
z-index: var(--smrt-z-index-toast, 1500);
|
|
737
|
+
text-align: center;
|
|
738
|
+
backdrop-filter: blur(8px);
|
|
739
|
+
animation: slideUp 0.2s ease-out;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.spoken-toaster strong {
|
|
743
|
+
color: var(--smrt-color-on-primary-container, #bbf7d0);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@keyframes slideUp {
|
|
747
|
+
from {
|
|
748
|
+
opacity: 0;
|
|
749
|
+
transform: translateY(100%);
|
|
750
|
+
}
|
|
751
|
+
to {
|
|
752
|
+
opacity: 1;
|
|
753
|
+
transform: translateY(0);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.extract-error {
|
|
758
|
+
padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-4, 16px);
|
|
759
|
+
background: var(--smrt-color-error-container);
|
|
760
|
+
border: 1px solid var(--smrt-color-error);
|
|
761
|
+
border-radius: var(--smrt-radius-md, 8px);
|
|
762
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
763
|
+
color: var(--smrt-color-error);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.form-fields {
|
|
767
|
+
display: flex;
|
|
768
|
+
flex-direction: column;
|
|
769
|
+
gap: var(--smrt-spacing-4, 16px);
|
|
770
|
+
}
|
|
771
|
+
</style>
|