@happyvertical/smrt-svelte 0.51.11 → 0.51.13

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 (27) hide show
  1. package/AGENTS.md +27 -0
  2. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.d.ts +129 -0
  3. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.d.ts.map +1 -0
  4. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.js +331 -0
  5. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.test.js +464 -0
  6. package/dist/browser-ai/adapters/llm/index.d.ts +2 -0
  7. package/dist/browser-ai/adapters/llm/index.d.ts.map +1 -1
  8. package/dist/browser-ai/adapters/llm/index.js +2 -0
  9. package/dist/browser-ai/adapters/llm/types.d.ts +18 -0
  10. package/dist/browser-ai/adapters/llm/types.d.ts.map +1 -1
  11. package/dist/browser-ai/adapters/llm/webllm-inference-backend.d.ts +67 -0
  12. package/dist/browser-ai/adapters/llm/webllm-inference-backend.d.ts.map +1 -0
  13. package/dist/browser-ai/adapters/llm/webllm-inference-backend.js +364 -0
  14. package/dist/browser-ai/adapters/llm/webllm-inference-backend.test.js +646 -0
  15. package/dist/browser-ai/adapters/llm/webllm.d.ts.map +1 -1
  16. package/dist/browser-ai/adapters/llm/webllm.js +19 -2
  17. package/dist/browser-ai/svelte/components/ModelStatusControl.svelte +190 -0
  18. package/dist/browser-ai/svelte/components/ModelStatusControl.svelte.d.ts +17 -0
  19. package/dist/browser-ai/svelte/components/ModelStatusControl.svelte.d.ts.map +1 -0
  20. package/dist/browser-ai/svelte/components/__tests__/ModelStatusControl.test.js +172 -0
  21. package/dist/browser-ai/svelte/index.d.ts +2 -1
  22. package/dist/browser-ai/svelte/index.d.ts.map +1 -1
  23. package/dist/browser-ai/svelte/index.js +3 -1
  24. package/dist/i18n/strings.browser-ai.d.ts +4 -0
  25. package/dist/i18n/strings.browser-ai.d.ts.map +1 -0
  26. package/dist/i18n/strings.browser-ai.js +4 -0
  27. package/package.json +19 -15
@@ -42,12 +42,19 @@ export class WebLLMAdapter {
42
42
  await this.unloadModel();
43
43
  }
44
44
  if (this._initState === 'initializing') {
45
+ // The executor form is required here: `Promise.withResolvers` is not in
46
+ // the `lib` this package's typecheck resolves against.
45
47
  return new Promise((resolve, reject) => {
46
48
  const check = () => {
47
49
  if (this._initState === 'ready')
48
50
  resolve();
49
51
  else if (this._initState === 'error')
50
52
  reject(new Error('Initialization failed'));
53
+ else if (this._initState !== 'initializing')
54
+ // A concurrent `unloadModel()` reset the state under this waiter.
55
+ // It settles only on `ready` or `error`, so without this branch the
56
+ // poll waits forever — and `unload()` is reachable at any time.
57
+ reject(new Error('Initialization was released before it completed'));
51
58
  else
52
59
  setTimeout(check, 100);
53
60
  };
@@ -104,11 +111,21 @@ export class WebLLMAdapter {
104
111
  }
105
112
  }
106
113
  async importWebLLM() {
114
+ // A host-supplied loader is the only form that works in a browser: the
115
+ // default resolves a VARIABLE specifier (so this module never hard-depends
116
+ // on an optional peer), which the browser cannot resolve on its own.
117
+ const { loadModule } = this.options;
107
118
  try {
108
- return await importOptional('@mlc-ai/web-llm');
119
+ return (loadModule
120
+ ? await loadModule()
121
+ : await importOptional('@mlc-ai/web-llm'));
109
122
  }
110
123
  catch {
111
- throw new InitializationError('webllm', '@mlc-ai/web-llm not installed. Run: npm install @mlc-ai/web-llm');
124
+ throw new InitializationError('webllm', loadModule
125
+ ? 'The supplied WebLLM loader rejected while loading @mlc-ai/web-llm.'
126
+ : '@mlc-ai/web-llm could not be loaded. Install it, or pass a ' +
127
+ '`loadModule` that imports it statically: ' +
128
+ '`() => import("@mlc-ai/web-llm")`.');
112
129
  }
113
130
  }
114
131
  async getCapabilities() {
@@ -0,0 +1,190 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Model status control
4
+ *
5
+ * Presents ONE inference backend's model lifecycle to a user: whether it is
6
+ * loaded, a way to load or release it, and live download progress. Driven
7
+ * entirely by the backend's own `status`/`progress`/`subscribe` surface, so it
8
+ * works anywhere — a shell dock, a settings panel, or a workbench that has no
9
+ * `<Provider>` ancestor. When a `<Provider ai>` IS configured, pass the local
10
+ * backend built from the app-state-managed adapter and this control reflects
11
+ * the same warm-cached model the rest of the app uses.
12
+ *
13
+ * An `unavailable` backend is a first-class state, not an error: a device
14
+ * without WebGPU is expected to fall through to the route backend, and the
15
+ * message says so rather than offering a Load button that cannot succeed.
16
+ */
17
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
18
+ import { Button } from '@happyvertical/smrt-ui/ui';
19
+ import type {
20
+ InferenceBackend,
21
+ InferenceBackendStatus,
22
+ InferenceProgress,
23
+ } from '@happyvertical/smrt-web/ai';
24
+ import { M } from '../../../i18n/strings.browser-ai.js';
25
+ import DownloadProgress from './DownloadProgress.svelte';
26
+
27
+ export interface Props {
28
+ /** The backend whose model lifecycle this control presents. */
29
+ backend: InferenceBackend;
30
+ /** Human label for the model, e.g. "On-device model". */
31
+ label?: string;
32
+ /** Offer a release action once the model is ready. */
33
+ allowUnload?: boolean;
34
+ /** Extra class(es) for layout in a host. */
35
+ class?: string;
36
+ /** Called when a load or unload rejects (also rendered inline). */
37
+ onerror?: (error: Error) => void;
38
+ }
39
+
40
+ let {
41
+ backend,
42
+ label = 'Local model',
43
+ allowUnload = true,
44
+ class: className = '',
45
+ onerror,
46
+ }: Props = $props();
47
+
48
+ const { t } = useI18n();
49
+
50
+ const STATUS_LABEL: Record<InferenceBackendStatus, string> = {
51
+ unavailable: 'Not available on this device',
52
+ idle: 'Not loaded',
53
+ loading: 'Loading…',
54
+ ready: 'Ready',
55
+ error: 'Load failed',
56
+ };
57
+
58
+ let busy = $state(false);
59
+ let failure = $state<string | null>(null);
60
+
61
+ // `backend.status`/`backend.progress` are plain properties, so nothing tells
62
+ // this component when they change. `revision` is the subscription's pulse: the
63
+ // derived re-reads the LIVE backend properties each time it ticks, which keeps
64
+ // the initial render correct (no effect needed for it) and follows a `backend`
65
+ // prop swap, since reading the prop inside the derived is itself reactive.
66
+ let revision = $state(0);
67
+ const snapshot = $derived.by(() => {
68
+ void revision;
69
+ return { status: backend.status, progress: backend.progress };
70
+ });
71
+
72
+ $effect(() => {
73
+ const current = backend;
74
+ // No bump here: the derived reads `backend` itself, so a prop swap already
75
+ // re-derives. Bumping from the effect body would read AND write `revision`,
76
+ // which Svelte treats as an update loop.
77
+ return current.subscribe?.(() => {
78
+ revision += 1;
79
+ });
80
+ });
81
+
82
+ async function run(
83
+ action: (() => void | Promise<void>) | undefined,
84
+ ): Promise<void> {
85
+ if (!action) return;
86
+ busy = true;
87
+ failure = null;
88
+ try {
89
+ await action();
90
+ } catch (error) {
91
+ const err = error instanceof Error ? error : new Error(String(error));
92
+ failure = err.message;
93
+ onerror?.(err);
94
+ } finally {
95
+ busy = false;
96
+ }
97
+ }
98
+ </script>
99
+
100
+ <div class="model-status {className}" data-status={snapshot.status}>
101
+ <div class="header">
102
+ <span class="label">{label}</span>
103
+ <span class="state" role="status">{STATUS_LABEL[snapshot.status]}</span>
104
+ </div>
105
+
106
+ {#if snapshot.status === 'unavailable'}
107
+ <p class="hint">
108
+ {t(M['ui.model_status.webgpu_unavailable'])}
109
+ </p>
110
+ {:else if snapshot.status === 'loading'}
111
+ <DownloadProgress
112
+ progress={snapshot.progress ?? null}
113
+ label="Downloading model"
114
+ showPercent
115
+ showBytes
116
+ />
117
+ {/if}
118
+
119
+ {#if failure}
120
+ <p class="error" role="alert">{failure}</p>
121
+ {/if}
122
+
123
+ <div class="actions">
124
+ {#if (snapshot.status === 'idle' || snapshot.status === 'error') && backend.load}
125
+ <Button onclick={() => run(() => backend.load?.())} disabled={busy}>
126
+ {snapshot.status === 'error' ? 'Retry' : 'Load model'}
127
+ </Button>
128
+ {:else if snapshot.status === 'ready' && allowUnload && backend.unload}
129
+ <Button
130
+ variant="secondary"
131
+ onclick={() => run(() => backend.unload?.())}
132
+ disabled={busy}
133
+ >
134
+ Unload
135
+ </Button>
136
+ {/if}
137
+ </div>
138
+ </div>
139
+
140
+ <style>
141
+ .model-status {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: var(--smrt-spacing-sm, 8px);
145
+ padding: var(--smrt-spacing-md, 12px);
146
+ background: var(--smrt-color-surface-container-low, #f9fafb);
147
+ border-radius: var(--smrt-radius-medium, 8px);
148
+ }
149
+
150
+ .header {
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: baseline;
154
+ gap: var(--smrt-spacing-sm, 8px);
155
+ }
156
+
157
+ .label {
158
+ font-weight: var(--smrt-typography-weight-semibold, 600);
159
+ color: var(--smrt-color-on-surface, #1a1c1e);
160
+ }
161
+
162
+ .state {
163
+ font: var(--smrt-typography-body-small-font, 0.8125rem / 1.2 sans-serif);
164
+ color: var(--smrt-color-on-surface-variant, #44474f);
165
+ }
166
+
167
+ .model-status[data-status='ready'] .state {
168
+ color: var(--smrt-color-primary, #005ac1);
169
+ }
170
+
171
+ .model-status[data-status='error'] .state {
172
+ color: var(--smrt-color-error, #ba1a1a);
173
+ }
174
+
175
+ .hint,
176
+ .error {
177
+ margin: 0;
178
+ font: var(--smrt-typography-body-small-font, 0.8125rem / 1.2 sans-serif);
179
+ color: var(--smrt-color-on-surface-variant, #44474f);
180
+ }
181
+
182
+ .error {
183
+ color: var(--smrt-color-error, #ba1a1a);
184
+ }
185
+
186
+ .actions {
187
+ display: flex;
188
+ gap: var(--smrt-spacing-sm, 8px);
189
+ }
190
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { InferenceBackend } from '@happyvertical/smrt-web/ai';
2
+ export interface Props {
3
+ /** The backend whose model lifecycle this control presents. */
4
+ backend: InferenceBackend;
5
+ /** Human label for the model, e.g. "On-device model". */
6
+ label?: string;
7
+ /** Offer a release action once the model is ready. */
8
+ allowUnload?: boolean;
9
+ /** Extra class(es) for layout in a host. */
10
+ class?: string;
11
+ /** Called when a load or unload rejects (also rendered inline). */
12
+ onerror?: (error: Error) => void;
13
+ }
14
+ declare const ModelStatusControl: import("svelte").Component<Props, {}, "">;
15
+ type ModelStatusControl = ReturnType<typeof ModelStatusControl>;
16
+ export default ModelStatusControl;
17
+ //# sourceMappingURL=ModelStatusControl.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelStatusControl.svelte.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/svelte/components/ModelStatusControl.svelte.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,gBAAgB,EAGjB,MAAM,4BAA4B,CAAC;AAKpC,MAAM,WAAW,KAAK;IACpB,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAyGD,QAAA,MAAM,kBAAkB,2CAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * `ModelStatusControl` — the user-facing model lifecycle surface.
3
+ *
4
+ * The states that matter here are the ones a user acts on: whether the model is
5
+ * loaded, whether it can be loaded on this device at all, and what a download
6
+ * is doing. The `unavailable` case is asserted explicitly because offering a
7
+ * Load button on a device that cannot run the model is the failure this control
8
+ * exists to prevent.
9
+ */
10
+ import { expectNoA11yViolations } from '@happyvertical/smrt-ui/test-support/a11y';
11
+ import { render, screen } from '@testing-library/svelte';
12
+ import userEvent from '@testing-library/user-event';
13
+ import { tick } from 'svelte';
14
+ import { describe, expect, it, vi } from 'vitest';
15
+ import ModelStatusControl from '../ModelStatusControl.svelte';
16
+ function makeBackend(initial = {}) {
17
+ let status = initial.status ?? 'idle';
18
+ let progress;
19
+ const listeners = new Set();
20
+ const backend = {
21
+ id: 'local',
22
+ kind: 'local',
23
+ get status() {
24
+ return status;
25
+ },
26
+ get progress() {
27
+ return progress;
28
+ },
29
+ loadCalls: 0,
30
+ unloadCalls: 0,
31
+ loadImpl: async () => { },
32
+ setStatus(next) {
33
+ status = next;
34
+ for (const listener of [...listeners])
35
+ listener();
36
+ },
37
+ setProgress(next) {
38
+ progress = next;
39
+ for (const listener of [...listeners])
40
+ listener();
41
+ },
42
+ async load() {
43
+ backend.loadCalls += 1;
44
+ await backend.loadImpl();
45
+ },
46
+ async unload() {
47
+ backend.unloadCalls += 1;
48
+ status = 'idle';
49
+ for (const listener of [...listeners])
50
+ listener();
51
+ },
52
+ async chat() {
53
+ return { content: '' };
54
+ },
55
+ async *stream() {
56
+ yield '';
57
+ },
58
+ subscribe(listener) {
59
+ listeners.add(listener);
60
+ return () => {
61
+ listeners.delete(listener);
62
+ };
63
+ },
64
+ };
65
+ return backend;
66
+ }
67
+ describe('ModelStatusControl', () => {
68
+ it('offers a load action when the model is not loaded', async () => {
69
+ const backend = makeBackend({ status: 'idle' });
70
+ render(ModelStatusControl, { props: { backend } });
71
+ expect(screen.getByRole('status')).toHaveTextContent('Not loaded');
72
+ await userEvent.click(screen.getByRole('button', { name: 'Load model' }));
73
+ expect(backend.loadCalls).toBe(1);
74
+ });
75
+ it('offers a release action once the model is ready', async () => {
76
+ const backend = makeBackend({ status: 'ready' });
77
+ render(ModelStatusControl, { props: { backend } });
78
+ expect(screen.getByRole('status')).toHaveTextContent('Ready');
79
+ await userEvent.click(screen.getByRole('button', { name: 'Unload' }));
80
+ expect(backend.unloadCalls).toBe(1);
81
+ });
82
+ it('hides the release action when the host opts out', () => {
83
+ const backend = makeBackend({ status: 'ready' });
84
+ render(ModelStatusControl, {
85
+ props: { backend, allowUnload: false },
86
+ });
87
+ expect(screen.queryByRole('button', { name: 'Unload' })).toBeNull();
88
+ });
89
+ it('never offers a load action on a device that cannot run the model', () => {
90
+ const backend = makeBackend({ status: 'unavailable' });
91
+ render(ModelStatusControl, { props: { backend } });
92
+ expect(screen.getByRole('status')).toHaveTextContent('Not available on this device');
93
+ // The point of the state: no affordance that cannot succeed.
94
+ expect(screen.queryByRole('button')).toBeNull();
95
+ expect(screen.getByText(/will use the server route/)).toBeInTheDocument();
96
+ });
97
+ it('offers no action for a backend that cannot load or unload', () => {
98
+ // A route-kind backend defines neither method and always reports `ready`;
99
+ // offering an Unload that silently does nothing is the same defect as
100
+ // offering a Load on an unavailable device.
101
+ const backend = makeBackend({ status: 'ready' });
102
+ // "A backend with no unload" is not hypothetical: it is the route backend.
103
+ delete backend.load;
104
+ delete backend.unload;
105
+ render(ModelStatusControl, { props: { backend } });
106
+ expect(screen.getByRole('status')).toHaveTextContent('Ready');
107
+ expect(screen.queryByRole('button')).toBeNull();
108
+ });
109
+ it('offers no load action for an idle backend without load', () => {
110
+ const backend = makeBackend({ status: 'idle' });
111
+ delete backend.load;
112
+ render(ModelStatusControl, { props: { backend } });
113
+ expect(screen.getByRole('status')).toHaveTextContent('Not loaded');
114
+ expect(screen.queryByRole('button')).toBeNull();
115
+ });
116
+ it('renders download progress while loading', () => {
117
+ const backend = makeBackend({ status: 'loading' });
118
+ backend.setProgress({
119
+ state: 'downloading',
120
+ bytesLoaded: 250_000,
121
+ bytesTotal: 500_000,
122
+ percent: 50,
123
+ });
124
+ render(ModelStatusControl, { props: { backend } });
125
+ expect(screen.getByRole('status')).toHaveTextContent('Loading');
126
+ expect(screen.getByText('Downloading model')).toBeInTheDocument();
127
+ expect(screen.getByText('50%')).toBeInTheDocument();
128
+ });
129
+ it('offers a retry after a failed load', async () => {
130
+ const backend = makeBackend({ status: 'error' });
131
+ render(ModelStatusControl, { props: { backend } });
132
+ expect(screen.getByRole('status')).toHaveTextContent('Load failed');
133
+ await userEvent.click(screen.getByRole('button', { name: 'Retry' }));
134
+ expect(backend.loadCalls).toBe(1);
135
+ });
136
+ it('surfaces a rejected load inline and to the host', async () => {
137
+ const backend = makeBackend({ status: 'idle' });
138
+ backend.loadImpl = async () => {
139
+ throw new Error('out of memory');
140
+ };
141
+ const onerror = vi.fn();
142
+ render(ModelStatusControl, { props: { backend, onerror } });
143
+ await userEvent.click(screen.getByRole('button', { name: 'Load model' }));
144
+ await tick();
145
+ expect(await screen.findByRole('alert')).toHaveTextContent('out of memory');
146
+ expect(onerror).toHaveBeenCalledWith(expect.any(Error));
147
+ });
148
+ it('follows a backend status change without a host re-render', async () => {
149
+ const backend = makeBackend({ status: 'idle' });
150
+ render(ModelStatusControl, { props: { backend } });
151
+ expect(screen.getByRole('status')).toHaveTextContent('Not loaded');
152
+ backend.setStatus('ready');
153
+ await tick();
154
+ expect(screen.getByRole('status')).toHaveTextContent('Ready');
155
+ });
156
+ it('has no accessibility violations in any state', async () => {
157
+ for (const status of [
158
+ 'unavailable',
159
+ 'idle',
160
+ 'loading',
161
+ 'ready',
162
+ 'error',
163
+ ]) {
164
+ const backend = makeBackend({ status });
165
+ const { container, unmount } = render(ModelStatusControl, {
166
+ props: { backend },
167
+ });
168
+ await expectNoA11yViolations(container);
169
+ unmount();
170
+ }
171
+ });
172
+ });
@@ -9,7 +9,8 @@
9
9
  import AILoadingOverlay from './components/AILoadingOverlay.svelte';
10
10
  import CapabilityGate from './components/CapabilityGate.svelte';
11
11
  import DownloadProgress from './components/DownloadProgress.svelte';
12
+ import ModelStatusControl from './components/ModelStatusControl.svelte';
12
13
  import STTTest from './components/STTTest.svelte';
13
14
  import VoiceInput from './components/VoiceInput.svelte';
14
- export { AILoadingOverlay, CapabilityGate, DownloadProgress, STTTest, VoiceInput, };
15
+ export { AILoadingOverlay, CapabilityGate, DownloadProgress, ModelStatusControl, STTTest, VoiceInput, };
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/browser-ai/svelte/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,gBAAgB,MAAM,sCAAsC,CAAC;AACpE,OAAO,cAAc,MAAM,oCAAoC,CAAC;AAChE,OAAO,gBAAgB,MAAM,sCAAsC,CAAC;AACpE,OAAO,OAAO,MAAM,6BAA6B,CAAC;AAClD,OAAO,UAAU,MAAM,gCAAgC,CAAC;AAGxD,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,UAAU,GACX,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/browser-ai/svelte/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,gBAAgB,MAAM,sCAAsC,CAAC;AACpE,OAAO,cAAc,MAAM,oCAAoC,CAAC;AAChE,OAAO,gBAAgB,MAAM,sCAAsC,CAAC;AACpE,OAAO,kBAAkB,MAAM,wCAAwC,CAAC;AACxE,OAAO,OAAO,MAAM,6BAA6B,CAAC;AAClD,OAAO,UAAU,MAAM,gCAAgC,CAAC;AAGxD,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,OAAO,EACP,UAAU,GACX,CAAC"}
@@ -12,10 +12,11 @@ import { BROWSER_AI_MODULE_META } from '../ui.js';
12
12
  import AILoadingOverlay from './components/AILoadingOverlay.svelte';
13
13
  import CapabilityGate from './components/CapabilityGate.svelte';
14
14
  import DownloadProgress from './components/DownloadProgress.svelte';
15
+ import ModelStatusControl from './components/ModelStatusControl.svelte';
15
16
  import STTTest from './components/STTTest.svelte';
16
17
  import VoiceInput from './components/VoiceInput.svelte';
17
18
  // Export components
18
- export { AILoadingOverlay, CapabilityGate, DownloadProgress, STTTest, VoiceInput, };
19
+ export { AILoadingOverlay, CapabilityGate, DownloadProgress, ModelStatusControl, STTTest, VoiceInput, };
19
20
  // Note: Component prop types (AILoadingOverlayProps, etc.) are available
20
21
  // via svelte-package build output but cannot be re-exported here because
21
22
  // tsc --noEmit cannot resolve type exports from .svelte files.
@@ -24,5 +25,6 @@ ModuleUIRegistry.registerModule(BROWSER_AI_MODULE_META);
24
25
  ModuleUIRegistry.register('@happyvertical/smrt-svelte/browser-ai', 'ai-loading-overlay', AILoadingOverlay);
25
26
  ModuleUIRegistry.register('@happyvertical/smrt-svelte/browser-ai', 'capability-gate', CapabilityGate);
26
27
  ModuleUIRegistry.register('@happyvertical/smrt-svelte/browser-ai', 'download-progress', DownloadProgress);
28
+ ModuleUIRegistry.register('@happyvertical/smrt-svelte/browser-ai', 'model-status-control', ModelStatusControl);
27
29
  ModuleUIRegistry.register('@happyvertical/smrt-svelte/browser-ai', 'stt-test', STTTest);
28
30
  ModuleUIRegistry.register('@happyvertical/smrt-svelte/browser-ai', 'voice-input', VoiceInput);
@@ -0,0 +1,4 @@
1
+ export declare const M: {
2
+ readonly 'ui.model_status.webgpu_unavailable': "ui.model_status.webgpu_unavailable";
3
+ };
4
+ //# sourceMappingURL=strings.browser-ai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.browser-ai.d.ts","sourceRoot":"","sources":["../../src/i18n/strings.browser-ai.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,CAAC;;CAGZ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { defineMessages } from '@happyvertical/smrt-ui/i18n';
2
+ export const M = defineMessages({
3
+ 'ui.model_status.webgpu_unavailable': 'This browser does not expose WebGPU, so the model cannot run here. Inference will use the server route instead.',
4
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-svelte",
3
- "version": "0.51.11",
3
+ "version": "0.51.13",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -120,20 +120,21 @@
120
120
  "access": "public"
121
121
  },
122
122
  "dependencies": {
123
- "@happyvertical/logger": "^0.89.11",
124
- "@happyvertical/smrt-languages": "0.51.11",
125
- "@happyvertical/smrt-types": "0.51.11",
126
- "@happyvertical/smrt-ui": "0.51.11",
127
- "@happyvertical/smrt-web": "0.51.11",
123
+ "@happyvertical/logger": "^0.89.12",
124
+ "@happyvertical/smrt-languages": "0.51.13",
125
+ "@happyvertical/smrt-types": "0.51.13",
126
+ "@happyvertical/smrt-ui": "0.51.13",
127
+ "@happyvertical/smrt-web": "0.51.13",
128
128
  "@tanstack/db": "^0.6.14",
129
129
  "@tanstack/svelte-db": "^0.1.91",
130
130
  "esm-env": "^1.2.2"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "@huggingface/transformers": ">=3.8.1",
134
- "@mlc-ai/web-llm": ">=0.2.84",
134
+ "@mlc-ai/web-llm": ">=0.2.85",
135
135
  "@remotion/whisper-web": ">=4.0.484",
136
136
  "@xenova/transformers": "^2.17.2",
137
+ "bitgpu": "^0.19.1",
137
138
  "chrono-node": "^2.9.1",
138
139
  "svelte": "^5.56.4"
139
140
  },
@@ -150,19 +151,22 @@
150
151
  "@xenova/transformers": {
151
152
  "optional": true
152
153
  },
154
+ "bitgpu": {
155
+ "optional": true
156
+ },
153
157
  "chrono-node": {
154
158
  "optional": true
155
159
  }
156
160
  },
157
161
  "devDependencies": {
158
- "@happyvertical/smrt-agents": "0.51.11",
159
- "@happyvertical/smrt-chat": "0.51.11",
160
- "@happyvertical/smrt-content": "0.51.11",
161
- "@happyvertical/smrt-core": "0.51.11",
162
- "@happyvertical/smrt-reports": "0.51.11",
163
- "@happyvertical/smrt-scanner": "0.51.11",
164
- "@happyvertical/smrt-tenancy": "0.51.11",
165
- "@happyvertical/smrt-users": "0.51.11",
162
+ "@happyvertical/smrt-agents": "0.51.13",
163
+ "@happyvertical/smrt-chat": "0.51.13",
164
+ "@happyvertical/smrt-content": "0.51.13",
165
+ "@happyvertical/smrt-core": "0.51.13",
166
+ "@happyvertical/smrt-reports": "0.51.13",
167
+ "@happyvertical/smrt-scanner": "0.51.13",
168
+ "@happyvertical/smrt-tenancy": "0.51.13",
169
+ "@happyvertical/smrt-users": "0.51.13",
166
170
  "@sveltejs/package": "^2.5.8",
167
171
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
168
172
  "@testing-library/jest-dom": "^6.9.1",