arcane-os 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/NOTICE +5 -3
  3. package/README.md +73 -24
  4. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
  5. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
  6. package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
  7. package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
  8. package/browser-runtime/ai/browser-speech-providers.mjs +780 -0
  9. package/browser-runtime/ai/browser-speech.mjs +9 -0
  10. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
  11. package/browser-runtime/ai/browser-wasm.mjs +46 -1
  12. package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
  13. package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
  14. package/browser-runtime/ai/model-controller.mjs +138 -12
  15. package/browser-runtime/ai/speech-worker-client.mjs +207 -0
  16. package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
  17. package/browser-runtime/ai/wllama/index.mjs +389 -0
  18. package/docs/architecture.md +132 -22
  19. package/docs/reference/README.md +1 -1
  20. package/docs/reference/ai/browser-wasm.md +101 -42
  21. package/docs/reference/availability-and-normalization.md +19 -5
  22. package/docs/reference/behavioral-testing.md +18 -5
  23. package/docs/reference/cli.md +2 -2
  24. package/docs/reference/inventory/package-api.json +14 -14
  25. package/docs/reference/protocols.md +4 -4
  26. package/docs/reference/sdk-api.md +68 -38
  27. package/docs/work-amplification.md +8 -4
  28. package/package.json +7 -3
  29. package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
  30. package/runtime/arcane/components/chat.html +551 -62
  31. package/runtime/arcane/components/speech.html +1113 -265
  32. package/runtime/arcane/entities/Chat.js +246 -43
  33. package/runtime/arcane/modules/AI.js +1394 -162
  34. package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
  35. package/runtime/arcane/modules/AIRuntimeState.js +872 -0
  36. package/runtime/arcane/modules/ConfiguredAIChatSession.js +382 -31
  37. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +1106 -0
  38. package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
  39. package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
  40. package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
  41. package/schemas/arcane-lock.schema.json +10 -6
  42. package/src/cli/main.mjs +14 -2
  43. package/src/constants.mjs +1 -1
  44. package/src/dev-server.mjs +273 -26
  45. package/src/doctor.mjs +1 -3
  46. package/src/import-map.mjs +193 -84
  47. package/src/packager/core.mjs +313 -41
  48. package/src/runtime.mjs +14 -4
  49. package/src/scaffold.mjs +45 -17
  50. package/src/sdk-browser-runtime.mjs +28 -75
  51. package/src/templates/workspace-template.mjs +27 -8
  52. package/src/toolchain.mjs +13 -2
  53. package/src/workspace-runtime.mjs +1 -1
  54. package/src/workspace.mjs +178 -25
@@ -1,11 +1,25 @@
1
1
  <link rel="stylesheet" href="./arcane/css/layout.css?v=3" />
2
2
  <style>
3
+ :host {
4
+ display: block;
5
+ height: 100%;
6
+ min-width: 0;
7
+ min-height: 0;
8
+ --arcane-chat-gap: var(--arcane-space-3, .75rem);
9
+ --arcane-chat-radius: var(--arcane-radius-large, 1rem);
10
+ }
11
+
3
12
  .chat_area {
4
13
  display: flex;
5
14
  flex-direction: column;
6
15
  justify-content: flex-end;
7
- padding: 1em;
8
- height: calc(100% - 2em);
16
+ box-sizing: border-box;
17
+ width: min(100%, 72rem);
18
+ height: 100%;
19
+ min-height: 0;
20
+ margin-inline: auto;
21
+ padding: clamp(.65rem, 1.5vw, 1.25rem);
22
+ gap: var(--arcane-chat-gap);
9
23
  overflow: hidden;
10
24
  }
11
25
 
@@ -86,6 +100,7 @@
86
100
  }
87
101
 
88
102
  .chat_area[data-conversation-complete="true"] .conversation_timebox,
103
+ .chat_area[data-conversation-complete="true"] .ai_activation,
89
104
  .chat_area[data-conversation-complete="true"] .chat_input_area,
90
105
  .chat_area[data-conversation-complete="true"] > #speech,
91
106
  .chat_area[data-conversation-complete="true"] > .controls{
@@ -114,80 +129,185 @@
114
129
  border:0;
115
130
  }
116
131
 
117
- .chat_area ul {
132
+ .chat_output {
118
133
  flex:1 1 auto;
134
+ box-sizing: border-box;
135
+ width: 100%;
119
136
  min-height:0;
137
+ margin: 0;
138
+ padding: clamp(1rem, 2vw, 1.5rem);
120
139
  list-style: none;
121
140
  overflow-y: auto;
122
- padding: 0 1em;
141
+ scrollbar-gutter: stable;
142
+ border: 1px solid var(--arcane-border, var(--border-color));
143
+ border-radius: var(--arcane-chat-radius);
144
+ background: var(--arcane-surface-raised, var(--modal-background));
145
+ box-shadow: var(--arcane-shadow-medium, 0 .65rem 1.8rem var(--shadow-color));
123
146
  }
124
147
 
125
- .chat_area li {
126
- padding: 0.5em;
127
- margin: 0 0 1em;
148
+ .chat_output > li {
149
+ box-sizing: border-box;
128
150
  width: auto;
129
- max-width: 95%;
151
+ max-width: min(92%, 48rem);
152
+ margin: 0 0 .85rem;
153
+ padding: .85rem 1rem;
130
154
  clear: both;
155
+ border: 1px solid var(--arcane-border, var(--border-color));
156
+ border-radius: calc(var(--arcane-chat-radius) - .1rem);
157
+ overflow-wrap: anywhere;
131
158
  }
132
159
 
133
- .chat_area li.ai {
134
- width: 90%;
160
+ .chat_output > li.ai {
161
+ width: auto;
135
162
  float: left;
136
- padding: 10px; /* Ensures padding around the content */
163
+ background: var(--arcane-surface, var(--background));
164
+ border-start-start-radius: var(--arcane-radius-small, .3rem);
137
165
  }
138
166
 
139
-
140
- .chat_area li.user {
141
- text-align: right;
167
+ .chat_output > li.user {
142
168
  float: right;
169
+ background: var(--arcane-action, var(--primary-color));
170
+ color: var(--arcane-action-text, var(--button-text-color, var(--text-color)));
171
+ border-color: var(--arcane-action, var(--primary-color));
172
+ border-start-end-radius: var(--arcane-radius-small, .3rem);
173
+ text-align: right;
174
+ }
175
+
176
+ .chat_output li ul,
177
+ .chat_output li ol {
178
+ display: block;
179
+ min-height: auto;
180
+ margin: .55rem 0 .55rem 1.35rem;
181
+ padding: 0;
182
+ overflow: visible;
183
+ list-style: revert;
184
+ }
185
+
186
+ .chat_output li li {
187
+ float: none;
188
+ width: auto;
189
+ max-width: none;
190
+ margin: .28rem 0;
191
+ padding: 0;
192
+ border: 0;
193
+ border-radius: 0;
194
+ background: transparent;
195
+ color: inherit;
196
+ text-align: start;
143
197
  }
144
198
 
145
199
  .chat_input_area {
146
200
  display: grid;
147
- padding-bottom: 2em;
148
- grid-template-columns: 1fr auto auto;
149
- column-gap: 0.5em;
201
+ box-sizing: border-box;
202
+ width: 100%;
203
+ padding: var(--arcane-space-3, .7rem);
204
+ grid-template-columns: minmax(0, 1fr) auto auto;
205
+ align-items: center;
206
+ gap: var(--arcane-space-2, .6rem);
207
+ border: 1px solid var(--arcane-border, var(--border-color));
208
+ border-radius: var(--arcane-chat-radius);
209
+ background: var(--arcane-surface-raised, var(--modal-background));
210
+ box-shadow: var(--arcane-shadow-small, 0 .35rem 1rem var(--shadow-color));
211
+ }
212
+
213
+ .ai_activation {
214
+ display:flex;
215
+ flex:0 0 auto;
216
+ box-sizing:border-box;
217
+ width:100%;
218
+ padding:var(--arcane-space-3, .75rem);
219
+ align-items:center;
220
+ justify-content:space-between;
221
+ gap:var(--arcane-space-3, .75rem);
222
+ border:1px solid var(--arcane-border, var(--border-color));
223
+ border-radius:var(--arcane-chat-radius);
224
+ background:var(--arcane-surface-raised, var(--modal-background));
225
+ color:var(--text-color);
226
+ }
227
+
228
+ .ai_activation[hidden] {
229
+ display:none;
230
+ }
231
+
232
+ .ai_activation_copy {
233
+ min-width:0;
234
+ }
235
+
236
+ .ai_activation_copy strong,
237
+ .ai_activation_copy span {
238
+ display:block;
239
+ overflow-wrap:anywhere;
240
+ }
241
+
242
+ .ai_activation_copy span {
243
+ margin-block-start:.22rem;
244
+ font-size:.9rem;
245
+ }
246
+
247
+ .ai_activation button {
248
+ flex:0 0 auto;
249
+ min-height:2.65rem;
250
+ padding:.55rem .8rem;
251
+ border:1px solid var(--arcane-action, var(--primary-color));
252
+ border-radius:var(--arcane-radius-medium, .7rem);
253
+ background:var(--arcane-action, var(--primary-color));
254
+ color:var(--arcane-action-text, var(--button-text-color, var(--text-color)));
255
+ font:inherit;
256
+ font-weight:600;
257
+ cursor:pointer;
258
+ }
259
+
260
+ .ai_activation button:focus-visible {
261
+ outline:.2rem solid var(--arcane-focus, var(--focus-color));
262
+ outline-offset:.18rem;
263
+ }
264
+
265
+ .ai_activation button:disabled {
266
+ cursor:not-allowed;
267
+ opacity:.65;
150
268
  }
151
269
 
152
270
  .chat_input {
153
- width: calc(100% - 1em);
154
- min-height: 48px;
155
- max-height: 160px;
156
- padding: 1.1em 1em;
157
- margin: 0;
158
- border:2px solid var(--border-color);
159
- border-radius: 0.5em;
160
- font-family: Arial, sans-serif;
161
- font-size: 16px;
162
- line-height: 20px;
163
271
  box-sizing: border-box;
272
+ width: 100%;
273
+ min-height: 3rem;
274
+ max-height: 10rem;
275
+ margin: 0;
276
+ padding: .82rem .9rem;
277
+ border: 1px solid var(--arcane-border, var(--border-color));
278
+ border-radius: var(--arcane-radius-medium, .7rem);
279
+ background: var(--arcane-surface, var(--background));
280
+ color: var(--text-color);
281
+ font: inherit;
282
+ font-size: 1rem;
283
+ line-height: 1.35;
164
284
  resize: none;
165
285
  overflow-y: auto;
166
286
  }
167
287
 
168
- .chat_input:focus {
169
- outline:.18em solid var(--focus-color);
170
- outline-offset:.12em;
171
- border-color:var(--focus-color);
288
+ .chat_input:focus-visible {
289
+ outline: .18em solid var(--arcane-focus, var(--focus-color));
290
+ outline-offset: .12em;
291
+ border-color: var(--arcane-focus, var(--focus-color));
172
292
  }
173
293
 
174
294
  .chat_submit,
175
295
  button.chat_upload {
176
296
  display:grid;
177
297
  place-items:center;
178
- width:3.75em;
179
- height:3.75em;
298
+ width:3rem;
299
+ height:3rem;
180
300
  padding:0;
181
301
  margin:0;
182
- background-color:var(--primary-color);
183
- border:.2em solid var(--primary-color);
184
- border-radius:.8em;
302
+ background-color:var(--arcane-action, var(--primary-color));
303
+ border:.2em solid var(--arcane-action, var(--primary-color));
304
+ border-radius:var(--arcane-radius-medium, .7rem);
185
305
  cursor: pointer;
186
306
  outline: none;
187
- box-shadow:0 .18em .45em var(--shadow-color);
188
- align-self: end;
307
+ box-shadow:var(--arcane-shadow-small, 0 .18em .45em var(--shadow-color));
308
+ align-self:center;
189
309
  transition:background-color 180ms,border-color 180ms,box-shadow 180ms,transform 120ms;
190
- color:var(--button-text-color,var(--text-color));
310
+ color:var(--arcane-action-text, var(--button-text-color,var(--text-color)));
191
311
  }
192
312
 
193
313
  .chat_submit::before,
@@ -221,12 +341,59 @@
221
341
 
222
342
  .chat_submit:hover,
223
343
  button.chat_upload:hover {
224
- background-color:var(--secondary-color);
225
- border-color:var(--secondary-color);
226
- color:var(--text-color);
344
+ background-color:var(--arcane-secondary, var(--secondary-color));
345
+ border-color:var(--arcane-secondary, var(--secondary-color));
346
+ color:var(--arcane-secondary-text, var(--text-color));
227
347
  box-shadow:0 .25em .6em var(--shadow-color);
228
348
  }
229
349
 
350
+ .chat_submit:focus-visible,
351
+ button.chat_upload:focus-visible {
352
+ outline: .2rem solid var(--arcane-focus, var(--focus-color));
353
+ outline-offset: .18rem;
354
+ }
355
+
356
+ #speech {
357
+ display: grid;
358
+ flex: 0 0 auto;
359
+ min-width: 0;
360
+ }
361
+
362
+ .controls {
363
+ display: flex;
364
+ justify-content: flex-end;
365
+ }
366
+
367
+ .controls select {
368
+ width: min(100%, 18rem);
369
+ min-height: 2.65rem;
370
+ padding: .55rem .75rem;
371
+ border: 1px solid var(--arcane-border, var(--border-color));
372
+ border-radius: var(--arcane-radius-medium, .7rem);
373
+ background: var(--arcane-surface-raised, var(--modal-background));
374
+ color: var(--text-color);
375
+ font: inherit;
376
+ }
377
+
378
+ .controls select:focus-visible {
379
+ outline: .2rem solid var(--arcane-focus, var(--focus-color));
380
+ outline-offset: .18rem;
381
+ }
382
+
383
+ @media (max-width: 44rem) {
384
+ .chat_area {
385
+ padding: var(--arcane-space-2, .5rem);
386
+ }
387
+
388
+ .chat_output > li {
389
+ max-width: 96%;
390
+ }
391
+
392
+ .controls select {
393
+ width: 100%;
394
+ }
395
+ }
396
+
230
397
 
231
398
  :host(.journal){
232
399
  .chat_output {
@@ -312,6 +479,20 @@
312
479
  >Starting the elapsed timer.</span>
313
480
  </section>
314
481
  <ul type='text' class='chat_output' id='chat_output' tabindex='-1' aria-label='Conversation transcript'></ul>
482
+ <section
483
+ id="ai_activation"
484
+ class="ai_activation"
485
+ role="status"
486
+ aria-live="polite"
487
+ aria-busy="false"
488
+ hidden
489
+ >
490
+ <span class="ai_activation_copy">
491
+ <strong id="ai_activation_title">Language model not active</strong>
492
+ <span id="ai_activation_status">Start the selected language model to enable chat.</span>
493
+ </span>
494
+ <button type="button" id="ai_activation_button">Start language model</button>
495
+ </section>
315
496
  <div class="chat_input_area">
316
497
  <textarea type='text' class='chat_input' id='chat_input'></textarea>
317
498
  <button type="button" id="chat_upload" class="chat_upload" title="Upload files" aria-label="Upload files">
@@ -515,14 +696,20 @@
515
696
  const conversationTimeboxPanel=shadowRoot.querySelector('#conversation_timebox');
516
697
  const conversationTimeboxElapsed=shadowRoot.querySelector('#conversation_timebox_elapsed');
517
698
  const conversationTimeboxStatus=shadowRoot.querySelector('#conversation_timebox_status');
699
+ const aiActivationPanel=shadowRoot.querySelector('#ai_activation');
700
+ const aiActivationTitle=shadowRoot.querySelector('#ai_activation_title');
701
+ const aiActivationStatus=shadowRoot.querySelector('#ai_activation_status');
702
+ const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
518
703
  const [
519
704
  {default:MD},
520
705
  {default:FileEntity},
521
- {ConversationSubmissionBarrier,requireConversationTimeboxDelivery}
706
+ {ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
707
+ {requestAIRuntimeIntent,subscribeAIRuntimeState}
522
708
  ]=await Promise.all([
523
709
  import('../modules/MD.js'),
524
710
  import('../entities/File.js'),
525
- import('../modules/ConversationTimebox.js')
711
+ import('../modules/ConversationTimebox.js'),
712
+ import('../modules/AIRuntimeState.js')
526
713
  ]);
527
714
 
528
715
  host.language=host.language||'English';
@@ -532,7 +719,10 @@
532
719
  ?speech.initialMuted
533
720
  :true;
534
721
  let initialSpeechMuteListenerInstalled=false;
722
+ let pendingSpeechAvailability=null;
723
+ let speechAvailabilityListenerInstalled=false;
535
724
  const hostSubmissionBarrier=new ConversationSubmissionBarrier();
725
+ const aiRuntimeStateAbortController=new AbortController();
536
726
  let programmaticSubmissionQueue=Promise.resolve(true);
537
727
  let conversationTimeboxController=null;
538
728
  let conversationTimeboxUnsubscribe=null;
@@ -549,6 +739,18 @@
549
739
  console.warn('html-import chat.html `host.languageChanged` method not implemented.\n\nThis can also be done by listening for the "chat-language-changed" event.\n\nIt is still best practice to implement the `.languageChanged` method atleast with an empty function.\n\n')
550
740
  }
551
741
 
742
+ host.requestAIActivation=host.requestAIActivation||function requestAIActivation(intent){
743
+ return requestAIRuntimeIntent(intent);
744
+ }
745
+
746
+ const aiActivationController=createAIActivationController({
747
+ host,
748
+ panel:aiActivationPanel,
749
+ title:aiActivationTitle,
750
+ status:aiActivationStatus,
751
+ button:aiActivationButton
752
+ });
753
+
552
754
  host.streamMessage=streamMessage;
553
755
  host.setMessageProgress=setMessageProgress;
554
756
  host.setAIAvailability=setAIAvailability;
@@ -556,8 +758,17 @@
556
758
  host.setConversationComplete=setConversationComplete;
557
759
  host.bindConversationTimebox=bindConversationTimebox;
558
760
  host.submitMessage=submitMessage;
559
- host.aiAvailability={llm:true,stt:true,tts:true};
761
+ host.destroy=destroy;
762
+ host.aiAvailability={llm:false,stt:false,tts:false};
560
763
  host.conversationComplete=false;
764
+ applyAIAvailability(host.aiAvailability);
765
+ subscribeAIRuntimeState(
766
+ synchronizeAIRuntimeState,
767
+ {
768
+ signal:aiRuntimeStateAbortController.signal
769
+ }
770
+ );
771
+ window.addEventListener('pagehide',destroy,{once:true});
561
772
 
562
773
  function setConversationComplete(complete=true){
563
774
  if(typeof complete!=='boolean'){
@@ -699,6 +910,7 @@
699
910
  }
700
911
 
701
912
  function setAIAvailability(input={}){
913
+ const explicitSpeechAvailability={};
702
914
  const availability={
703
915
  llm:Object.prototype.hasOwnProperty.call(input,'llm')
704
916
  ?Boolean(input.llm)
@@ -711,25 +923,251 @@
711
923
  :host.aiAvailability.tts
712
924
  };
713
925
 
714
- host.aiAvailability=availability;
926
+ if(Object.prototype.hasOwnProperty.call(input,'stt')){
927
+ explicitSpeechAvailability.stt=availability.stt;
928
+ }
929
+ if(Object.prototype.hasOwnProperty.call(input,'tts')){
930
+ explicitSpeechAvailability.tts=availability.tts;
931
+ }
932
+ applyAIAvailability(availability);
933
+ if(Object.keys(explicitSpeechAvailability).length){
934
+ pendingSpeechAvailability={
935
+ ...(pendingSpeechAvailability||{}),
936
+ ...explicitSpeechAvailability
937
+ };
938
+ synchronizeSpeechCompatibilityAvailability();
939
+ }
940
+ return {...availability};
941
+ }
942
+
943
+ function applyAIAvailability(availability){
944
+ host.aiAvailability={...availability};
715
945
  send.disabled=host.conversationComplete||!availability.llm;
716
946
  send.title=availability.llm
717
947
  ?'Send message'
718
948
  :'The selected language model service is unavailable.';
719
949
  send.setAttribute('aria-label',send.title);
950
+ }
951
+
952
+ function createAIActivationController({host,panel,title,status,button,EventClass=CustomEvent}){
953
+ let role=null;
954
+ let requestPending=false;
955
+ let requestError='';
956
+ let destroyed=false;
957
+
958
+ function visibleError(error,fallback){
959
+ const message=typeof error?.message==='string'
960
+ ?error.message.trim()
961
+ :'';
962
+ return (message||fallback).slice(0,240);
963
+ }
964
+
965
+ function cancellation(error){
966
+ return error?.name==='AbortError'
967
+ ||['ARCANE_AI_REQUEST_ABORTED','AI_CHAT_ABORTED'].includes(error?.code);
968
+ }
969
+
970
+ function selected(){
971
+ return typeof role?.providerId==='string'
972
+ &&role.providerId.length>0
973
+ &&typeof role?.modelId==='string'
974
+ &&role.modelId.length>0;
975
+ }
976
+
977
+ function progressMessage(){
978
+ if(!role?.progress){
979
+ return 'The selected language model is loading.';
980
+ }
981
+ const progress=role.progress;
982
+ const amount=progress.total===null
983
+ ?`${progress.completed} ${progress.unit}`
984
+ :`${progress.completed} of ${progress.total} ${progress.unit}`;
985
+ return `${progress.phase}, ${amount}${progress.heartbeat?', active heartbeat':''}.`;
986
+ }
987
+
988
+ function render(){
989
+ const visible=selected()&&role.state!=='ready';
990
+ panel.hidden=!visible;
991
+ panel.setAttribute(
992
+ 'aria-busy',
993
+ String(requestPending||['loading','unloading'].includes(role?.state))
994
+ );
995
+ if(!visible){
996
+ return;
997
+ }
998
+ if(requestPending){
999
+ title.textContent='Requesting language model activation';
1000
+ status.textContent='Waiting for the selected language model runtime.';
1001
+ button.textContent='Requesting…';
1002
+ button.disabled=true;
1003
+ return;
1004
+ }
1005
+ if(role.state==='loading'){
1006
+ title.textContent='Starting language model';
1007
+ status.textContent=progressMessage();
1008
+ button.textContent='Cancel loading';
1009
+ button.disabled=false;
1010
+ return;
1011
+ }
1012
+ if(role.state==='unloading'){
1013
+ title.textContent='Canceling language model load';
1014
+ status.textContent='Waiting for the selected language model runtime to stop.';
1015
+ button.textContent='Canceling…';
1016
+ button.disabled=true;
1017
+ return;
1018
+ }
1019
+ if(role.state==='error'||requestError){
1020
+ title.textContent='Language model activation failed';
1021
+ status.textContent=requestError||visibleError(
1022
+ role.error,
1023
+ 'The selected language model could not start.'
1024
+ );
1025
+ button.textContent='Try again';
1026
+ button.disabled=false;
1027
+ return;
1028
+ }
1029
+ if(role.state==='unloaded'){
1030
+ title.textContent='Language model not active';
1031
+ status.textContent='Start the selected language model to enable chat.';
1032
+ button.textContent='Start language model';
1033
+ button.disabled=false;
1034
+ return;
1035
+ }
1036
+ title.textContent='Language model unavailable';
1037
+ status.textContent='The selected language model cannot be activated in this state.';
1038
+ button.textContent='Unavailable';
1039
+ button.disabled=true;
1040
+ }
1041
+
1042
+ async function request(action){
1043
+ if(destroyed||requestPending||!selected()){
1044
+ return false;
1045
+ }
1046
+ const permitted=action==='load'
1047
+ ?['unloaded','error'].includes(role.state)
1048
+ :action==='unload'&&role.state==='loading';
1049
+ if(!permitted){
1050
+ return false;
1051
+ }
1052
+ requestError='';
1053
+ const intent=Object.freeze({role:'llm',action,reason:'user'});
1054
+ const request=Object.freeze({intent,state:role});
1055
+ const activationEvent=new EventClass(
1056
+ 'chat-ai-activation-request',
1057
+ {
1058
+ bubbles:true,
1059
+ composed:true,
1060
+ cancelable:true,
1061
+ detail:request
1062
+ }
1063
+ );
1064
+ if(!host.dispatchEvent(activationEvent)){
1065
+ render();
1066
+ return false;
1067
+ }
1068
+ requestPending=true;
1069
+ render();
1070
+ try{
1071
+ await host.requestAIActivation(intent);
1072
+ return true;
1073
+ }catch(error){
1074
+ if(action==='load'
1075
+ &&cancellation(error)
1076
+ &&['unloaded','unloading'].includes(role?.state)){
1077
+ return false;
1078
+ }
1079
+ const message=visibleError(
1080
+ error,
1081
+ `The language model ${action} request failed.`
1082
+ );
1083
+ requestError=message;
1084
+ host.dispatchEvent(
1085
+ new EventClass(
1086
+ 'chat-ai-activation-error',
1087
+ {
1088
+ bubbles:true,
1089
+ composed:true,
1090
+ detail:Object.freeze({request,error,message})
1091
+ }
1092
+ )
1093
+ );
1094
+ return false;
1095
+ }finally{
1096
+ requestPending=false;
1097
+ render();
1098
+ }
1099
+ }
720
1100
 
721
- const configureSpeech=()=>speech.setAvailability?.({
722
- stt:availability.stt,
723
- tts:availability.tts
1101
+ function activateSelectedAI(){
1102
+ const action=role?.state==='loading'?'unload':'load';
1103
+ void request(action);
1104
+ }
1105
+
1106
+ function synchronize(nextRole){
1107
+ if(destroyed){
1108
+ return;
1109
+ }
1110
+ if(role?.state!==nextRole.state
1111
+ ||role?.providerId!==nextRole.providerId
1112
+ ||role?.modelId!==nextRole.modelId){
1113
+ requestError='';
1114
+ requestPending=false;
1115
+ }
1116
+ role=nextRole;
1117
+ render();
1118
+ }
1119
+
1120
+ function destroy(){
1121
+ if(destroyed){
1122
+ return;
1123
+ }
1124
+ destroyed=true;
1125
+ button.removeEventListener('click',activateSelectedAI);
1126
+ panel.hidden=true;
1127
+ }
1128
+
1129
+ button.addEventListener('click',activateSelectedAI);
1130
+ return Object.freeze({request,synchronize,destroy});
1131
+ }
1132
+
1133
+ function synchronizeAIRuntimeState(snapshot){
1134
+ aiActivationController.synchronize(snapshot.roles.llm);
1135
+ applyAIAvailability({
1136
+ llm:snapshot.roles.llm.state==='ready',
1137
+ stt:snapshot.roles.stt.state==='ready',
1138
+ tts:snapshot.roles.tts.state==='ready'
724
1139
  });
1140
+ }
725
1141
 
1142
+ function synchronizeSpeechCompatibilityAvailability(){
1143
+ if(!pendingSpeechAvailability){
1144
+ return;
1145
+ }
726
1146
  if(speech.componentReady){
727
- configureSpeech();
728
- }else{
729
- speech.addEventListener('speech-ready',configureSpeech,{once:true});
1147
+ const availability=pendingSpeechAvailability;
1148
+ pendingSpeechAvailability=null;
1149
+ speech.setAvailability?.({...availability});
1150
+ return;
1151
+ }
1152
+ if(speechAvailabilityListenerInstalled){
1153
+ return;
730
1154
  }
731
1155
 
732
- return {...availability};
1156
+ speechAvailabilityListenerInstalled=true;
1157
+ speech.addEventListener(
1158
+ 'speech-ready',
1159
+ applyPendingSpeechAvailability,
1160
+ {once:true}
1161
+ );
1162
+ }
1163
+
1164
+ function applyPendingSpeechAvailability(){
1165
+ speechAvailabilityListenerInstalled=false;
1166
+ if(pendingSpeechAvailability){
1167
+ const availability=pendingSpeechAvailability;
1168
+ pendingSpeechAvailability=null;
1169
+ speech.setAvailability?.({...availability});
1170
+ }
733
1171
  }
734
1172
 
735
1173
  function formatConversationTime(milliseconds=0){
@@ -928,7 +1366,7 @@
928
1366
  }
929
1367
  )
930
1368
  );
931
- console.error('The conversation timebox message could not be sent.',error);
1369
+ console.error('The conversation timebox message could not be sent.');
932
1370
  return false;
933
1371
  }
934
1372
  );
@@ -997,10 +1435,22 @@
997
1435
  const clearThinking=thinkingHTML;
998
1436
 
999
1437
  //console.table(arguments);
1000
- if(!isThinking&&text&&host.aiAvailability.tts){
1001
- ai.streamTTS(
1002
- text
1003
- )
1438
+ if(
1439
+ !isThinking
1440
+ &&text
1441
+ &&host.aiAvailability.tts
1442
+ &&speech.muted===false
1443
+ ){
1444
+ const aiRuntime=globalThis.ai;
1445
+ if(typeof aiRuntime?.streamTTS==='function'){
1446
+ void Promise.resolve(aiRuntime.streamTTS(text)).catch(
1447
+ reportTTSError
1448
+ );
1449
+ }else{
1450
+ reportTTSError(
1451
+ new Error('The ready speech provider has no synthesis adapter.')
1452
+ );
1453
+ }
1004
1454
  }
1005
1455
 
1006
1456
  if(clearThinking){
@@ -1126,7 +1576,7 @@
1126
1576
  }
1127
1577
  )
1128
1578
  );
1129
- console.error('The chat message could not be submitted.',error);
1579
+ console.error('The chat message could not be submitted.');
1130
1580
  return false;
1131
1581
  }
1132
1582
  );
@@ -1143,6 +1593,9 @@
1143
1593
  if(host.conversationComplete){
1144
1594
  return false;
1145
1595
  }
1596
+ if(!host.aiAvailability.llm){
1597
+ return false;
1598
+ }
1146
1599
  const submissionContext=Object.freeze({
1147
1600
  source:'user',
1148
1601
  preserveDraft:false,
@@ -1154,7 +1607,6 @@
1154
1607
  return false;
1155
1608
  }
1156
1609
  const messageTime = getMilTime();
1157
- console.info("Dispatching message:", text);
1158
1610
  if(!submissionContext.preserveDraft){
1159
1611
  textArea.value='';
1160
1612
  }
@@ -1189,8 +1641,15 @@
1189
1641
  }
1190
1642
  );
1191
1643
 
1192
- ai.stopAudio();
1193
- ai.resumeAudio?.();
1644
+ if(host.aiAvailability.tts&&speech.muted===false){
1645
+ const aiRuntime=globalThis.ai;
1646
+ aiRuntime?.stopAudio?.();
1647
+ if(typeof aiRuntime?.resumeAudio==='function'){
1648
+ void Promise.resolve(aiRuntime.resumeAudio()).catch(
1649
+ reportTTSError
1650
+ );
1651
+ }
1652
+ }
1194
1653
 
1195
1654
  host.dispatchEvent(chatEvent);
1196
1655
 
@@ -1219,6 +1678,36 @@
1219
1678
  chatOutput.scrollTop = chatOutput.scrollHeight;
1220
1679
  }
1221
1680
 
1681
+ function reportTTSError(error){
1682
+ if(typeof speech.reportTTSError==='function'){
1683
+ speech.reportTTSError(error);
1684
+ return;
1685
+ }
1686
+
1687
+ const message=typeof error?.message==='string'&&error.message.trim()
1688
+ ?error.message.trim().slice(0,240)
1689
+ :'Speech synthesis failed.';
1690
+ host.dispatchEvent(
1691
+ new CustomEvent(
1692
+ 'chat-speech-synthesis-error',
1693
+ {
1694
+ bubbles:true,
1695
+ composed:true,
1696
+ detail:{error,message}
1697
+ }
1698
+ )
1699
+ );
1700
+ console.error('Speech synthesis failed.');
1701
+ }
1702
+
1703
+ function destroy(){
1704
+ aiRuntimeStateAbortController.abort();
1705
+ aiActivationController.destroy();
1706
+ window.removeEventListener('pagehide',destroy);
1707
+ speech.destroy?.();
1708
+ host.ready=false;
1709
+ }
1710
+
1222
1711
  // Initialize and adjust on input
1223
1712
  resizeTextArea();
1224
1713