arcane-os 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ - Add a reusable profile component for browser NPU setup and WebNN/WebGPU API
6
+ availability. Its setup button uses the existing GPU guidance approach:
7
+ attempt the appropriate Chrome or Edge flags page, then show an alert with
8
+ instructions and the address to paste if the browser blocks navigation.
9
+ - Share browser identification and the flags-opening helper with the existing
10
+ high-performance GPU notice. Preserve that notice's targets and wording.
11
+ Setup does not change saved preferences, browser flags, selected models, or
12
+ inference; API availability is not reported as physical NPU execution.
13
+
3
14
  ## 0.6.0
4
15
 
5
16
  - Prefer WebNN NPU, then WebGPU, then CPU through WASM for browser Whisper
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.6.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.6.1` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.6.0 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.6.1 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -363,7 +363,7 @@ uses the same controller for automatic memory extraction.
363
363
  Create a new repository-shaped Arcane application with the exact stable SDK:
364
364
 
365
365
  ```bash
366
- npx arcane-os@0.6.0 new my-app --path ./my-app --target portable --git
366
+ npx arcane-os@0.6.1 new my-app --path ./my-app --target portable --git
367
367
  cd my-app
368
368
  npm install
369
369
  npm run dev
@@ -373,7 +373,7 @@ To enroll an existing repository, install the exact SDK and initialize only
373
373
  missing Arcane files:
374
374
 
375
375
  ```bash
376
- npm install --save-dev --save-exact arcane-os@0.6.0
376
+ npm install --save-dev --save-exact arcane-os@0.6.1
377
377
  npm exec -- arcane init my-app --target portable
378
378
  ```
379
379
 
@@ -389,7 +389,7 @@ npm exec -- arcane-os targets
389
389
  No global SDK install or standalone Arcane CLI is required. The application
390
390
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
391
391
 
392
- Use `npx arcane-os@0.6.0` for the initial bootstrap because it names this npm
392
+ Use `npx arcane-os@0.6.1` for the initial bootstrap because it names this npm
393
393
  package explicitly; bare `npx arcane` outside an installed project could resolve
394
394
  a different package. Both installed commands invoke the same headless toolchain.
395
395
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -409,7 +409,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
409
409
 
410
410
  # From the generated app repository
411
411
  cd ../local-app
412
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.0.tgz
412
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.1.tgz
413
413
  npm ci
414
414
  ```
415
415
 
@@ -418,7 +418,7 @@ same location. The lockfile retains the selected package dependency while
418
418
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
419
419
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
420
420
  runner also needs that tarball at the locked path. After publication, replace
421
- the local declaration with the exact `arcane-os@0.6.0` registry package and
421
+ the local declaration with the exact `arcane-os@0.6.1` registry package and
422
422
  commit the regenerated lock.
423
423
 
424
424
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -557,7 +557,7 @@ package installation, or assertions.
557
557
 
558
558
  ## Current target support
559
559
 
560
- Version `0.6.0` exposes one browser target and five explicitly paired
560
+ Version `0.6.1` exposes one browser target and five explicitly paired
561
561
  native development targets: a non-runnable portable directory, a
562
562
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
563
563
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -0,0 +1,83 @@
1
+ import Is from "../dependencies/strong-type/index.js";
2
+
3
+ const is = new Is(false);
4
+
5
+ export const WEBNN_BROWSER_SETTINGS = [
6
+ { browserId: "chrome", name: "Google Chrome", url: "chrome://flags/#web-machine-learning-neural-network" },
7
+ { browserId: "edge", name: "Microsoft Edge", url: "edge://flags/#web-machine-learning-neural-network" },
8
+ ];
9
+
10
+ // Internal-page navigation may be blocked. The instructions remain available
11
+ // regardless; returning from this function is not evidence that settings opened.
12
+ export function openBrowserDeviceSettings(target, message) {
13
+ try {
14
+ globalThis.open?.(target.url, "_blank", "noopener,noreferrer");
15
+ } catch {
16
+ // Continue to the address-bar instructions when the browser rejects the URL.
17
+ }
18
+ globalThis.alert?.(message);
19
+ }
20
+
21
+ // Browser identity selects setup instructions, never hardware or model readiness.
22
+ export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator) {
23
+ const userAgent = String(navigatorObject?.userAgent ?? "");
24
+ const clientHints = navigatorObject?.userAgentData;
25
+ const platform = String(clientHints?.platform || navigatorObject?.platform || userAgent);
26
+ const brands = new Set();
27
+ for (const entry of clientHints?.brands ?? []) brands.add(entry.brand);
28
+ const desktopWindows = /\b(?:Windows|Win32|Win64)\b/iu.test(platform)
29
+ && clientHints?.mobile !== true
30
+ && !/\b(?:Android|iPhone|iPad|iPod|Mobile)\b/iu.test(userAgent);
31
+ let browserId = "unknown";
32
+ let name = "this browser";
33
+ let highPerformanceGpu = null;
34
+
35
+ // Preserve the existing GPU guidance for browsers that also identify as Chrome.
36
+ if (brands.has("Vivaldi") || /\bVivaldi\//u.test(userAgent)) {
37
+ browserId = "vivaldi";
38
+ name = "Vivaldi";
39
+ highPerformanceGpu = { name, url: "vivaldi://flags/#force-high-performance-gpu" };
40
+ } else if (brands.has("Brave") || is.function(navigatorObject?.brave?.isBrave)) {
41
+ browserId = "brave";
42
+ name = "Brave";
43
+ highPerformanceGpu = { name, url: "brave://flags/#force-high-performance-gpu" };
44
+ } else if (brands.has("Opera") || /\bOPR\//u.test(userAgent)) {
45
+ browserId = "opera";
46
+ name = "Opera";
47
+ highPerformanceGpu = { name, url: "opera://flags/#force-high-performance-gpu" };
48
+ } else if (brands.has("Microsoft Edge") || /\bEdg\//u.test(userAgent)) {
49
+ browserId = "edge";
50
+ name = "Microsoft Edge";
51
+ highPerformanceGpu = { name, url: "edge://flags/#force-high-performance-gpu" };
52
+ } else if (
53
+ brands.has("Chromium")
54
+ || brands.has("Google Chrome")
55
+ || /\b(?:Chrome|Chromium)\//u.test(userAgent)
56
+ ) {
57
+ // Keep the established generic GPU address for Chromium browsers that mask their brand.
58
+ highPerformanceGpu = { name: "your browser", url: "about://flags/#force-high-performance-gpu" };
59
+ if (
60
+ brands.has("Google Chrome")
61
+ || (!clientHints?.brands && /\bChrome\//u.test(userAgent)
62
+ && !/\b(?:SamsungBrowser|UCBrowser|YaBrowser)\//u.test(userAgent))
63
+ ) {
64
+ browserId = "chrome";
65
+ name = "Google Chrome";
66
+ } else {
67
+ browserId = "chromium";
68
+ name = "your Chromium browser";
69
+ }
70
+ }
71
+
72
+ const webnnFlagsURL = WEBNN_BROWSER_SETTINGS.find(function matchesBrowser(entry) {
73
+ return entry.browserId === browserId;
74
+ })?.url ?? null;
75
+ return {
76
+ browserId,
77
+ name,
78
+ webnnFlagsURL,
79
+ highPerformanceGpu: desktopWindows ? highPerformanceGpu : null,
80
+ webnnAvailable: is.function(navigatorObject?.ml?.createContext),
81
+ webgpuAvailable: Boolean(navigatorObject?.gpu),
82
+ };
83
+ }
@@ -8,6 +8,7 @@ import {
8
8
  resolveModelSecurity,
9
9
  } from "./model-controller.mjs";
10
10
  import { createPackagedWllamaRuntime } from "./browser-wllama-runtime.mjs";
11
+ import { getBrowserDeviceSettings, openBrowserDeviceSettings } from "./browser-device-settings.mjs";
11
12
  import { arcaneEvents } from "../event-manager.mjs";
12
13
 
13
14
  const is = new Is(false);
@@ -264,65 +265,20 @@ function publicDescriptor(source) {
264
265
  return modelDescriptor(source);
265
266
  }
266
267
 
267
- function highPerformanceGpuBrowser() {
268
- const navigatorObject = globalThis.navigator;
269
- const userAgent = String(navigatorObject?.userAgent ?? "");
270
- const clientHints = navigatorObject?.userAgentData;
271
- const platform = String(clientHints?.platform || navigatorObject?.platform || userAgent);
272
- if (
273
- !/\b(?:Windows|Win32|Win64)\b/iu.test(platform)
274
- || clientHints?.mobile === true
275
- || /\b(?:Android|iPhone|iPad|iPod|Mobile)\b/iu.test(userAgent)
276
- ) {
277
- return null;
278
- }
279
-
280
- const brands = new Set();
281
- for (const entry of clientHints?.brands ?? []) {
282
- brands.add(entry.brand);
283
- }
284
- // Vivaldi can append its identity while also identifying as Edge or Chrome.
285
- if (brands.has("Vivaldi") || /\bVivaldi\//u.test(userAgent)) {
286
- return { name: "Vivaldi", url: "vivaldi://flags/#force-high-performance-gpu" };
287
- }
288
- if (brands.has("Brave") || is.function(navigatorObject?.brave?.isBrave)) {
289
- return { name: "Brave", url: "brave://flags/#force-high-performance-gpu" };
290
- }
291
- if (brands.has("Opera") || /\bOPR\//u.test(userAgent)) {
292
- return { name: "Opera", url: "opera://flags/#force-high-performance-gpu" };
293
- }
294
- if (brands.has("Microsoft Edge") || /\bEdg\//u.test(userAgent)) {
295
- return { name: "Microsoft Edge", url: "edge://flags/#force-high-performance-gpu" };
296
- }
297
- if (
298
- brands.has("Chromium")
299
- || brands.has("Google Chrome")
300
- || /\b(?:Chrome|Chromium)\//u.test(userAgent)
301
- ) {
302
- // Some Chromium browsers mask their brand. about:// uses their own flags page.
303
- return { name: "your browser", url: "about://flags/#force-high-performance-gpu" };
304
- }
305
- return null;
306
- }
307
-
308
268
  function notifyHighPerformanceGpu(adapter) {
309
269
  if (highPerformanceGpuNoticeShown) {
310
270
  return;
311
271
  }
312
- const browser = highPerformanceGpuBrowser();
272
+ const browser = getBrowserDeviceSettings().highPerformanceGpu;
313
273
  if (!browser) {
314
274
  return;
315
275
  }
316
276
  highPerformanceGpuNoticeShown = true;
317
- try {
318
- globalThis.open?.(browser.url, "_blank", "noopener,noreferrer");
319
- } catch {
320
- // Browsers may reject internal-page navigation from web content.
321
- }
322
277
  const adapterName = adapter.description || adapter.name
323
278
  || [adapter.vendor, adapter.architecture].filter(Boolean).join(" ")
324
279
  || "the available WebGPU adapter";
325
- globalThis.alert?.(
280
+ openBrowserDeviceSettings(
281
+ browser,
326
282
  `Selected WebGPU adapter: ${adapterName}.\n\n`
327
283
  + `If this computer has multiple GPUs, enable “Force High Performance GPU” in ${browser.name} `
328
284
  + "to request the high-performance GPU when available. "
@@ -279,7 +279,7 @@ paths are withheld from the native provider. The provider copies the complete
279
279
  selected release rather than accepting an unrelated source path. Verification
280
280
  is a separate explicit operation for a selected release artifact.
281
281
 
282
- The SDK `0.6.0` runtime requires Arcane `0.8.12` or newer. Compatibility
282
+ The SDK `0.6.1` runtime requires Arcane `0.8.12` or newer. Compatibility
283
283
  is contractual rather than exact-version pinning: the prepared Core must meet
284
284
  the highest minimum declared by the runtime, selected app, and bundled app
285
285
  dependencies; keep each app's Arcane protocol generation; and provide every
@@ -130,6 +130,36 @@ window.addEventListener('ai-tts-failure', function reportSpeechFailure(event) {
130
130
 
131
131
  Call `speechEvents.abort()` when disposing that interface to remove the listener.
132
132
 
133
+ ## Browser NPU setup
134
+
135
+ Applications can place the shared `browser-ai-setup.html` component in their
136
+ profile or settings page. It reports whether this page exposes WebNN and
137
+ WebGPU, without loading a model or creating an accelerator context:
138
+
139
+ ```html
140
+ <html-import
141
+ id="browserAISetup"
142
+ href="/arcane/components/browser-ai-setup.html">
143
+ </html-import>
144
+ ```
145
+
146
+ **Set up NPU** uses the same browser-settings approach as the existing
147
+ high-performance GPU notice: attempt to open the browser's flags page, then
148
+ show instructions including the full address to paste if navigation was
149
+ blocked. Chrome uses `chrome://flags/#web-machine-learning-neural-network`;
150
+ Edge uses `edge://flags/#web-machine-learning-neural-network`. Unrecognized
151
+ browsers receive explicit Chrome and Edge choices instead of an assumed target.
152
+ The [ONNX Runtime WebNN guide](https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html)
153
+ documents the **Enables WebNN API** flag and model/operator requirements.
154
+
155
+ This control does not save an execution preference, change browser settings,
156
+ restart the browser, or report that the NPU is active. A WebNN API presence
157
+ result is not proof of NPU hardware, a compatible model, or physical execution.
158
+ The profile also cannot report a different chat page's loaded runtime. The
159
+ existing automatic speech route remains NPU, then GPU, then CPU; upstream
160
+ sessions can still place unsupported operators on CPU. Use the owning model
161
+ runtime's evidence to determine actual accelerator execution.
162
+
133
163
  ## Developer diagnostics
134
164
 
135
165
  The shared logging API and speech traces are available in SDK `0.5.14`.
@@ -60,6 +60,7 @@ appropriate.
60
60
  | --- | --- | --- | --- | --- |
61
61
  | [`app-bar.html`](#app-barhtml) | Responsive application navigation, route state, status, and trailing actions. | `setNavigation()`<br>`setActiveRoute()`<br>`setStatus()`<br>`refresh()`<br>`destroy()` | `app-bar-ready` | DOM-normalized |
62
62
  | [`assistant-panel.html`](#assistant-panelhtml) | Reusable assistant drawer, message area, composer, pending/streaming/empty/error state, and actions. | `open()`<br>`close()`<br>`toggle()`<br>`send()`<br>`clear()`<br>`setState()`<br>`focusComposer()`<br>`scrollToEnd()`<br>`destroy()` | `assistant-ready`<br>`assistant-opened`<br>`assistant-closed`<br>`assistant-send`<br>`assistant-clear` | DOM-normalized; caller/provider results remain external |
63
+ | [`browser-ai-setup.html`](#browser-ai-setuphtml) | Browser API availability and NPU setup instructions with browser-specific flags actions. | `refresh()`<br>`open()`<br>`destroy()`<br>`ready` | `browser-ai-setup-ready` | API presence only; settings navigation and hardware execution remain browser-owned |
63
64
  | [`calculator.html`](#calculatorhtml) | Calculator keypad and result/error event surface backed by CalculatorEngine. | `calculate()`<br>`destroy()` | `calculator-ready`<br>`calculation-complete`<br>`calculation-error` | Normalized Calculation/error events |
64
65
  | [`chart.html`](#charthtml) | Accessible uPlot line, area, or point chart with normalized options and rows. | `configure()`<br>`populate()`<br>`setData()`<br>`addData()`<br>`update()`<br>`destroy()` | `chart-ready`<br>`chart-remove` | Options/rows normalized; uPlot rendering is vendor-native |
65
66
  | [`chat.html`](#chathtml) | Shared chat, visible selected-model activation request, file upload, streaming, structural tool settlement, speech, language, availability, and conversation-timebox surface. | `streamMessage()`<br>`setMessageProgress()`<br>`setAIAvailability()`<br>`setInitialSpeechMuted()`<br>`setConversationComplete()`<br>`bindConversationTimebox()`<br>`bindSession()`<br>`submitMessage()`<br>`submitToolResult()`<br>`submitToolResults()`<br>`sendMessage()`<br>`languageChanged()`<br>`requestAIActivation()`<br>`destroy()` | `chat-ready`<br>`chat-session-bound`<br>`chat-session-message`<br>`chat-session-error`<br>`chat-send-message`<br>`chat-send-error`<br>`chat-file-uploaded`<br>`chat-file-upload-error`<br>`chat-language-changed`<br>`chat-language-change-error`<br>`chat-ai-activation-request`<br>`chat-ai-activation-error`<br>`chat-speech-synthesis-error`<br>`conversation-timebox-error` | UI/runtime state, explicit user activation intent, and honest structural-call settlement normalized; AI/storage/media behavior mixed |
@@ -153,6 +154,58 @@ Slots: `title`, `subtitle`, `identity`, `messages/message`, `composer`, `actions
153
154
  </html-import>
154
155
  ```
155
156
 
157
+ ## browser-ai-setup.html
158
+
159
+ ### Overview
160
+
161
+ Displays WebNN and WebGPU API availability for the current page and explains how
162
+ to enable WebNN for NPU use. Its setup action uses the same browser-flags opening
163
+ attempt and alert fallback as the existing high-performance GPU notice. Chrome
164
+ and Edge receive their own flags addresses; other or unidentified browsers show
165
+ both explicit choices.
166
+
167
+ ### Public surface
168
+
169
+ Methods/properties: `refresh()`, `open(browserId?)`, `destroy()`, `ready`.
170
+
171
+ Events: `browser-ai-setup-ready`.
172
+
173
+ `refresh()` synchronously updates API presence and browser guidance and returns
174
+ the current settings record: `browserId`, `name`, `webnnFlagsURL`,
175
+ `highPerformanceGpu`, `webnnAvailable`, and `webgpuAvailable`. It returns `false`
176
+ after destruction. Availability means that the browser exposes the API to this
177
+ page; it does not confirm a working adapter, loaded model, or hardware execution.
178
+
179
+ `open()` refreshes the display, attempts to open the detected Chrome or Edge flags
180
+ page, and presents an alert containing the full address and enable/relaunch
181
+ instructions. Pass `"chrome"` or `"edge"` to choose explicitly. Invoke it from a
182
+ user action. It returns `false` when destroyed or when no supported target is
183
+ selected, and otherwise returns `undefined`; it never reports that navigation
184
+ succeeded. Browsers may block internal-page navigation, so the full addresses
185
+ also remain visible and copyable in the component.
186
+
187
+ `destroy()` aborts owned listeners, disposes the event source, marks `ready`
188
+ false, and suppresses UI updates from a pending clipboard operation. It returns
189
+ `true` the first time and `false` thereafter. A BFCache-persisted `pagehide`
190
+ preserves the component; nonpersisted `pagehide` destroys it.
191
+
192
+ ### Availability and normalization
193
+
194
+ **Browser and supported native WebViews.** API presence and setup instructions
195
+ are normalized; browser flags, clipboard support, and device execution remain
196
+ platform-owned. Mounting or refreshing creates no model, GPU adapter, or WebNN
197
+ context. The component saves no preferences, changes no browser settings, and
198
+ does not restart the browser. It cannot report model state owned by another page.
199
+
200
+ ### Example
201
+
202
+ ```html
203
+ <html-import
204
+ id="browser-ai-setup"
205
+ href="/arcane/components/browser-ai-setup.html">
206
+ </html-import>
207
+ ```
208
+
156
209
  ## calculator.html
157
210
 
158
211
  ### Overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -0,0 +1,212 @@
1
+ <link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
+ <style>
3
+ :host{display:block;min-width:0}
4
+ .availability{display:grid;gap:var(--arcane-space-2);margin:0}
5
+ .availability div{display:grid;gap:var(--arcane-space-1)}
6
+ .availability dt{font-weight:700}
7
+ .availability dd{margin:0}
8
+ .flag-choice{margin-block:var(--arcane-space-4)}
9
+ .flag-choice h3{margin-block:var(--arcane-space-2)}
10
+ .actions{display:flex;flex-wrap:wrap;gap:var(--arcane-space-2)}
11
+ p,dt,dd,li,button{overflow-wrap:anywhere}
12
+ button{white-space:normal}
13
+ code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;margin-block:var(--arcane-space-2)}
14
+ li{margin-block:var(--arcane-space-2)}
15
+ </style>
16
+
17
+ <section class="arcane-card" aria-labelledby="setupTitle">
18
+ <header class="arcane-card__header">
19
+ <h2 id="setupTitle" class="arcane-section-heading">Browser AI acceleration</h2>
20
+ </header>
21
+ <div class="arcane-card__body">
22
+ <p id="browserName"></p>
23
+ <dl class="availability" aria-live="polite">
24
+ <div><dt>WebNN API</dt><dd id="webnnAvailability"></dd></div>
25
+ <div><dt>WebGPU API</dt><dd id="webgpuAvailability"></dd></div>
26
+ </dl>
27
+ <p class="arcane-help">These checks show browser APIs only. They do not report whether a model is running on an NPU or GPU.</p>
28
+ <h3>NPU setup</h3>
29
+ <p id="browserInstructions"></p>
30
+ <div id="flagAddresses"></div>
31
+ <ol id="setupSteps"></ol>
32
+ <p>Browser API availability does not confirm that an NPU or a particular model is supported. You choose whether to change the browser setting.</p>
33
+ <p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">ONNX Runtime WebNN setup documentation</a></p>
34
+ <p id="copyStatus" class="arcane-help" role="status" aria-live="polite"></p>
35
+ </div>
36
+ <footer class="arcane-card__footer">
37
+ <button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh browser availability</button>
38
+ <p id="setupStatus" class="arcane-help" role="status" aria-live="polite"></p>
39
+ </footer>
40
+ </section>
41
+
42
+ <script type="module">
43
+ const host=this;
44
+ const root=host.shadowRoot;
45
+ host.ready=false;
46
+
47
+ const {
48
+ getBrowserDeviceSettings,
49
+ openBrowserDeviceSettings,
50
+ WEBNN_BROWSER_SETTINGS
51
+ }=await import('../sdk/ai/browser-device-settings.mjs');
52
+ const {arcaneLogging}=await import('arcane-os/logging');
53
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
54
+ const events=createArcaneEventSource(host,{
55
+ source:'arcane.component.browser-ai-setup',
56
+ eventTypes:['browser-ai-setup-ready']
57
+ });
58
+ const lifecycleController=new AbortController();
59
+ const status=root.querySelector('#setupStatus');
60
+ const copyStatus=root.querySelector('#copyStatus');
61
+ const choices=[];
62
+ const setupSteps=[
63
+ 'If the flags page does not open, copy the matching address into your browser\'s address bar and open it.',
64
+ 'Find “Enables WebNN API” and choose Enabled.',
65
+ 'Save your work, then use the browser\'s Relaunch control.',
66
+ 'Return to this page and select Refresh browser availability.'
67
+ ];
68
+ let copyTask=null;
69
+ let destroyed=false;
70
+
71
+ host.refresh=refresh;
72
+ host.open=open;
73
+ host.destroy=destroy;
74
+
75
+ function refresh(){
76
+ if(destroyed)return false;
77
+ const settings=getBrowserDeviceSettings();
78
+ root.querySelector('#browserName').textContent=settings.browserId==='unknown'
79
+ ?'Browser not identified.'
80
+ :`Browser: ${settings.name}.`;
81
+ root.querySelector('#webnnAvailability').textContent=settings.webnnAvailable
82
+ ?'Exposed to this page.'
83
+ :'Unavailable to this page.';
84
+ root.querySelector('#webgpuAvailability').textContent=settings.webgpuAvailable
85
+ ?'Exposed to this page.'
86
+ :'Unavailable to this page.';
87
+ root.querySelector('#browserInstructions').textContent=settings.webnnFlagsURL
88
+ ?`Set up NPU attempts to open the WebNN flags page in ${settings.name} and shows the full instructions.`
89
+ :'A matching WebNN flags address is not known for this browser. Choose Google Chrome or Microsoft Edge below if that is the browser you are using.';
90
+ for(const choice of choices){
91
+ choice.row.hidden=Boolean(settings.webnnFlagsURL)
92
+ &&choice.target.url!==settings.webnnFlagsURL;
93
+ choice.setupButton.textContent=settings.webnnFlagsURL
94
+ ?'Set up NPU'
95
+ :`Set up NPU in ${choice.target.name}`;
96
+ }
97
+ return settings;
98
+ }
99
+
100
+ function open(browserId){
101
+ if(destroyed)return false;
102
+ const current=refresh();
103
+ const selectedId=browserId??current.browserId;
104
+ const target=WEBNN_BROWSER_SETTINGS.find(function matchSetupBrowser(browser){
105
+ return browser.browserId===selectedId;
106
+ });
107
+ if(!target){
108
+ status.textContent='Choose the Google Chrome or Microsoft Edge setup button that matches your browser.';
109
+ return false;
110
+ }
111
+ const instructions=setupSteps.map(function numberSetupStep(step,index){
112
+ return `${index+1}. ${step}`;
113
+ }).join('\n');
114
+ openBrowserDeviceSettings(
115
+ target,
116
+ `NPU setup for ${target.name}\n\n${instructions}\n\n${target.url}`
117
+ );
118
+ status.textContent='If the flags page did not open, copy its address above and paste it into your browser\'s address bar.';
119
+ }
120
+
121
+ function copyAddress(address){
122
+ if(destroyed)return Promise.resolve(false);
123
+ if(copyTask)return copyTask;
124
+ copyStatus.textContent='Copying address...';
125
+ for(const choice of choices)choice.copyButton.disabled=true;
126
+ copyTask=writeAddress().finally(function finishAddressCopy(){
127
+ copyTask=null;
128
+ if(!destroyed){
129
+ for(const choice of choices)choice.copyButton.disabled=false;
130
+ }
131
+ });
132
+ return copyTask;
133
+
134
+ async function writeAddress(){
135
+ try{
136
+ await globalThis.navigator.clipboard.writeText(address);
137
+ if(destroyed)return false;
138
+ copyStatus.textContent='Address copied. Paste it into your browser\'s address bar.';
139
+ return true;
140
+ }catch(error){
141
+ if(!destroyed){
142
+ copyStatus.textContent='The address could not be copied automatically. Select the complete address above, copy it manually, and paste it into your browser\'s address bar.';
143
+ arcaneLogging.error('Unable to copy the browser WebNN flags address.',error);
144
+ }
145
+ return false;
146
+ }
147
+ }
148
+ }
149
+
150
+ const addressList=root.querySelector('#flagAddresses');
151
+ for(const target of WEBNN_BROWSER_SETTINGS){
152
+ const row=document.createElement('section');
153
+ const heading=document.createElement('h3');
154
+ const address=document.createElement('code');
155
+ const actions=document.createElement('div');
156
+ const setupButton=document.createElement('button');
157
+ const copyButton=document.createElement('button');
158
+ row.className='flag-choice';
159
+ heading.textContent=target.name;
160
+ address.textContent=target.url;
161
+ address.tabIndex=0;
162
+ address.setAttribute('aria-label',`${target.name} flags address`);
163
+ actions.className='actions';
164
+ setupButton.type='button';
165
+ setupButton.className='arcane-button';
166
+ setupButton.addEventListener('click',function openSelectedBrowserSettings(){
167
+ return open(target.browserId);
168
+ },{signal:lifecycleController.signal});
169
+ copyButton.type='button';
170
+ copyButton.className='arcane-button arcane-button--secondary';
171
+ copyButton.textContent=`Copy ${target.name} address`;
172
+ copyButton.addEventListener('click',function copySelectedBrowserAddress(){
173
+ return copyAddress(target.url);
174
+ },{signal:lifecycleController.signal});
175
+ actions.append(setupButton,copyButton);
176
+ row.append(heading,address,actions);
177
+ addressList.append(row);
178
+ choices.push({row,target,setupButton,copyButton});
179
+ }
180
+ const stepList=root.querySelector('#setupSteps');
181
+ for(const step of setupSteps){
182
+ const item=document.createElement('li');
183
+ item.textContent=step;
184
+ stepList.append(item);
185
+ }
186
+
187
+ root.querySelector('#refreshAvailability').addEventListener('click',function refreshBrowserAvailability(){
188
+ refresh();
189
+ status.textContent='Browser API availability refreshed.';
190
+ },{signal:lifecycleController.signal});
191
+ host.ownerDocument.defaultView.addEventListener('pagehide',function disposeBeforePageUnload(event){
192
+ if(!event.persisted)destroy();
193
+ },{
194
+ signal:lifecycleController.signal
195
+ });
196
+
197
+ function destroy(){
198
+ if(destroyed)return false;
199
+ destroyed=true;
200
+ host.ready=false;
201
+ lifecycleController.abort('browser-ai-setup-destroyed');
202
+ events.dispose();
203
+ return true;
204
+ }
205
+
206
+ refresh();
207
+ host.ready=true;
208
+ const publication=events.dispatch('browser-ai-setup-ready',{}, {
209
+ publicDetail:{ready:true}
210
+ });
211
+ projectArcaneDOMEvent(host,publication.occurrence);
212
+ </script>