arcane-os 0.3.0 → 0.3.2
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 +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
display: block;
|
|
5
5
|
height: 100%;
|
|
6
6
|
min-width: 0;
|
|
7
|
+
min-inline-size:0;
|
|
8
|
+
max-inline-size:100%;
|
|
7
9
|
min-height: 0;
|
|
8
10
|
--arcane-chat-gap: var(--arcane-space-3, .75rem);
|
|
9
11
|
--arcane-chat-radius: var(--arcane-radius-large, 1rem);
|
|
@@ -15,12 +17,15 @@
|
|
|
15
17
|
justify-content: flex-end;
|
|
16
18
|
box-sizing: border-box;
|
|
17
19
|
width: min(100%, 72rem);
|
|
20
|
+
min-width:0;
|
|
21
|
+
min-inline-size:0;
|
|
22
|
+
max-inline-size:100%;
|
|
18
23
|
height: 100%;
|
|
19
24
|
min-height: 0;
|
|
20
25
|
margin-inline: auto;
|
|
21
26
|
padding: clamp(.65rem, 1.5vw, 1.25rem);
|
|
22
27
|
gap: var(--arcane-chat-gap);
|
|
23
|
-
overflow:
|
|
28
|
+
overflow: visible;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
.chat_area .thinking{
|
|
@@ -129,15 +134,34 @@
|
|
|
129
134
|
border:0;
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
.chat_session_status{
|
|
138
|
+
display:block;
|
|
139
|
+
flex:0 0 auto;
|
|
140
|
+
min-height:1.25em;
|
|
141
|
+
padding-inline:.25rem;
|
|
142
|
+
color:var(--text-color);
|
|
143
|
+
font-size:.82rem;
|
|
144
|
+
overflow-wrap:anywhere;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.chat_session_status[data-state="error"]{
|
|
148
|
+
color:var(--danger-color,var(--text-color));
|
|
149
|
+
}
|
|
150
|
+
|
|
132
151
|
.chat_output {
|
|
133
152
|
flex:1 1 auto;
|
|
153
|
+
display:flow-root;
|
|
134
154
|
box-sizing: border-box;
|
|
135
155
|
width: 100%;
|
|
136
156
|
min-height:0;
|
|
157
|
+
min-inline-size:0;
|
|
158
|
+
max-inline-size:100%;
|
|
137
159
|
margin: 0;
|
|
138
160
|
padding: clamp(1rem, 2vw, 1.5rem);
|
|
139
161
|
list-style: none;
|
|
162
|
+
overflow-x: hidden;
|
|
140
163
|
overflow-y: auto;
|
|
164
|
+
overscroll-behavior: contain;
|
|
141
165
|
scrollbar-gutter: stable;
|
|
142
166
|
border: 1px solid var(--arcane-border, var(--border-color));
|
|
143
167
|
border-radius: var(--arcane-chat-radius);
|
|
@@ -148,12 +172,16 @@
|
|
|
148
172
|
.chat_output > li {
|
|
149
173
|
box-sizing: border-box;
|
|
150
174
|
width: auto;
|
|
175
|
+
min-width: 0;
|
|
176
|
+
min-inline-size:0;
|
|
177
|
+
max-inline-size:min(92%,48rem);
|
|
151
178
|
max-width: min(92%, 48rem);
|
|
152
179
|
margin: 0 0 .85rem;
|
|
153
180
|
padding: .85rem 1rem;
|
|
154
181
|
clear: both;
|
|
155
182
|
border: 1px solid var(--arcane-border, var(--border-color));
|
|
156
183
|
border-radius: calc(var(--arcane-chat-radius) - .1rem);
|
|
184
|
+
overflow-x: hidden;
|
|
157
185
|
overflow-wrap: anywhere;
|
|
158
186
|
}
|
|
159
187
|
|
|
@@ -173,6 +201,93 @@
|
|
|
173
201
|
text-align: right;
|
|
174
202
|
}
|
|
175
203
|
|
|
204
|
+
.message_header{
|
|
205
|
+
display:block;
|
|
206
|
+
margin:0 0 .65rem;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.message_timestamp{
|
|
210
|
+
display:block;
|
|
211
|
+
margin-top:.65rem;
|
|
212
|
+
color:currentColor;
|
|
213
|
+
font-size:.72rem;
|
|
214
|
+
line-height:1.2;
|
|
215
|
+
opacity:.7;
|
|
216
|
+
text-align:right;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.message_tool_calls{
|
|
220
|
+
display:grid;
|
|
221
|
+
box-sizing:border-box;
|
|
222
|
+
width:100%;
|
|
223
|
+
min-width:0;
|
|
224
|
+
min-inline-size:0;
|
|
225
|
+
max-inline-size:100%;
|
|
226
|
+
max-width:100%;
|
|
227
|
+
gap:.55rem;
|
|
228
|
+
margin-top:.75rem;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.message_tool_call{
|
|
232
|
+
box-sizing:border-box;
|
|
233
|
+
width:100%;
|
|
234
|
+
min-width:0;
|
|
235
|
+
min-inline-size:0;
|
|
236
|
+
max-inline-size:100%;
|
|
237
|
+
max-width:100%;
|
|
238
|
+
padding:.65rem;
|
|
239
|
+
border:1px solid currentColor;
|
|
240
|
+
border-radius:var(--arcane-radius-small,.3rem);
|
|
241
|
+
overflow-x:hidden;
|
|
242
|
+
overflow-wrap:anywhere;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.message_tool_message{
|
|
246
|
+
margin:.45rem 0 0;
|
|
247
|
+
white-space:pre-wrap;
|
|
248
|
+
overflow-wrap:anywhere;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.message_tool_details{
|
|
252
|
+
box-sizing:border-box;
|
|
253
|
+
width:100%;
|
|
254
|
+
min-width:0;
|
|
255
|
+
min-inline-size:0;
|
|
256
|
+
max-inline-size:100%;
|
|
257
|
+
max-width:100%;
|
|
258
|
+
margin-top:.55rem;
|
|
259
|
+
overflow-x:hidden;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.message_tool_details summary{
|
|
263
|
+
cursor:pointer;
|
|
264
|
+
overflow-wrap:anywhere;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.message_tool_call pre{
|
|
268
|
+
box-sizing:border-box;
|
|
269
|
+
width:100%;
|
|
270
|
+
min-width:0;
|
|
271
|
+
min-inline-size:0;
|
|
272
|
+
max-inline-size:100%;
|
|
273
|
+
max-width:100%;
|
|
274
|
+
margin:.45rem 0 0;
|
|
275
|
+
white-space:pre-wrap;
|
|
276
|
+
word-break:break-word;
|
|
277
|
+
overflow-x:auto;
|
|
278
|
+
overflow-wrap:anywhere;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.message_tool_call code{
|
|
282
|
+
display:block;
|
|
283
|
+
box-sizing:border-box;
|
|
284
|
+
min-inline-size:0;
|
|
285
|
+
max-inline-size:100%;
|
|
286
|
+
white-space:inherit;
|
|
287
|
+
word-break:inherit;
|
|
288
|
+
overflow-wrap:anywhere;
|
|
289
|
+
}
|
|
290
|
+
|
|
176
291
|
.chat_output li ul,
|
|
177
292
|
.chat_output li ol {
|
|
178
293
|
display: block;
|
|
@@ -230,6 +345,7 @@
|
|
|
230
345
|
}
|
|
231
346
|
|
|
232
347
|
.ai_activation_copy {
|
|
348
|
+
flex:1 1 auto;
|
|
233
349
|
min-width:0;
|
|
234
350
|
}
|
|
235
351
|
|
|
@@ -244,6 +360,18 @@
|
|
|
244
360
|
font-size:.9rem;
|
|
245
361
|
}
|
|
246
362
|
|
|
363
|
+
.ai_activation_progress{
|
|
364
|
+
display:block;
|
|
365
|
+
width:100%;
|
|
366
|
+
height:.6rem;
|
|
367
|
+
margin-block-start:.7rem;
|
|
368
|
+
accent-color:var(--arcane-action,var(--primary-color));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.ai_activation_progress[hidden]{
|
|
372
|
+
display:none;
|
|
373
|
+
}
|
|
374
|
+
|
|
247
375
|
.ai_activation button {
|
|
248
376
|
flex:0 0 auto;
|
|
249
377
|
min-height:2.65rem;
|
|
@@ -271,7 +399,6 @@
|
|
|
271
399
|
box-sizing: border-box;
|
|
272
400
|
width: 100%;
|
|
273
401
|
min-height: 3rem;
|
|
274
|
-
max-height: 10rem;
|
|
275
402
|
margin: 0;
|
|
276
403
|
padding: .82rem .9rem;
|
|
277
404
|
border: 1px solid var(--arcane-border, var(--border-color));
|
|
@@ -281,8 +408,8 @@
|
|
|
281
408
|
font: inherit;
|
|
282
409
|
font-size: 1rem;
|
|
283
410
|
line-height: 1.35;
|
|
284
|
-
resize:
|
|
285
|
-
overflow
|
|
411
|
+
resize: vertical;
|
|
412
|
+
overflow: visible;
|
|
286
413
|
}
|
|
287
414
|
|
|
288
415
|
.chat_input:focus-visible {
|
|
@@ -380,6 +507,16 @@
|
|
|
380
507
|
outline-offset: .18rem;
|
|
381
508
|
}
|
|
382
509
|
|
|
510
|
+
:host([presentation="basic"]) .chat_input_area {
|
|
511
|
+
grid-template-columns:minmax(0,1fr) auto;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
:host([presentation="basic"]) #chat_upload,
|
|
515
|
+
:host([presentation="basic"]) #file_upload_input,
|
|
516
|
+
:host([presentation="basic"]) .controls {
|
|
517
|
+
display:none;
|
|
518
|
+
}
|
|
519
|
+
|
|
383
520
|
@media (max-width: 44rem) {
|
|
384
521
|
.chat_area {
|
|
385
522
|
padding: var(--arcane-space-2, .5rem);
|
|
@@ -453,8 +590,7 @@
|
|
|
453
590
|
background: var(--border-color);
|
|
454
591
|
padding:.4em;
|
|
455
592
|
border: 1px solid var(--accent-color);
|
|
456
|
-
|
|
457
|
-
overflow: auto;
|
|
593
|
+
overflow: visible;
|
|
458
594
|
}
|
|
459
595
|
</style>
|
|
460
596
|
|
|
@@ -479,6 +615,13 @@
|
|
|
479
615
|
>Starting the elapsed timer.</span>
|
|
480
616
|
</section>
|
|
481
617
|
<ul type='text' class='chat_output' id='chat_output' tabindex='-1' aria-label='Conversation transcript'></ul>
|
|
618
|
+
<output
|
|
619
|
+
id="chat_session_status"
|
|
620
|
+
class="chat_session_status"
|
|
621
|
+
role="status"
|
|
622
|
+
aria-live="polite"
|
|
623
|
+
data-state="idle"
|
|
624
|
+
>Chat session is not connected.</output>
|
|
482
625
|
<section
|
|
483
626
|
id="ai_activation"
|
|
484
627
|
class="ai_activation"
|
|
@@ -490,11 +633,17 @@
|
|
|
490
633
|
<span class="ai_activation_copy">
|
|
491
634
|
<strong id="ai_activation_title">Language model not active</strong>
|
|
492
635
|
<span id="ai_activation_status">Start the selected language model to enable chat.</span>
|
|
636
|
+
<progress
|
|
637
|
+
id="ai_activation_progress"
|
|
638
|
+
class="ai_activation_progress"
|
|
639
|
+
aria-label="Language model activation progress"
|
|
640
|
+
hidden
|
|
641
|
+
></progress>
|
|
493
642
|
</span>
|
|
494
643
|
<button type="button" id="ai_activation_button">Start language model</button>
|
|
495
644
|
</section>
|
|
496
645
|
<div class="chat_input_area">
|
|
497
|
-
<textarea type='text' class='chat_input' id='chat_input'></textarea>
|
|
646
|
+
<textarea type='text' class='chat_input' id='chat_input' placeholder='Message' aria-label='Message'></textarea>
|
|
498
647
|
<button type="button" id="chat_upload" class="chat_upload" title="Upload files" aria-label="Upload files">
|
|
499
648
|
<img class='upload-icon' src="./arcane/img/upload.svg" />
|
|
500
649
|
</button>
|
|
@@ -503,7 +652,7 @@
|
|
|
503
652
|
<img class='send-icon' src="./arcane/img/send.svg" />
|
|
504
653
|
</button>
|
|
505
654
|
</div>
|
|
506
|
-
<
|
|
655
|
+
<span id="speech_mount"></span>
|
|
507
656
|
<section class="controls">
|
|
508
657
|
<select id="languages">
|
|
509
658
|
<option value="english" selected>English</option>
|
|
@@ -690,12 +839,27 @@
|
|
|
690
839
|
}=await import('arcane-os/event-manager');
|
|
691
840
|
|
|
692
841
|
const host=this;
|
|
842
|
+
const componentURL=new URL(
|
|
843
|
+
host.getAttribute('href')||'./arcane/components/chat.html',
|
|
844
|
+
document.baseURI
|
|
845
|
+
);
|
|
846
|
+
const speechMount=host.shadowRoot.querySelector('#speech_mount');
|
|
847
|
+
const speechImport=document.createElement('html-import');
|
|
848
|
+
speechImport.id='speech';
|
|
849
|
+
speechImport.setAttribute(
|
|
850
|
+
'href',
|
|
851
|
+
new URL('./speech.html?v=3',componentURL).href
|
|
852
|
+
);
|
|
853
|
+
speechMount.replaceWith(speechImport);
|
|
693
854
|
const events=createArcaneEventSource(
|
|
694
855
|
host,
|
|
695
856
|
{
|
|
696
857
|
source:'arcane.component.chat',
|
|
697
858
|
eventTypes:[
|
|
698
859
|
'chat-ready',
|
|
860
|
+
'chat-session-bound',
|
|
861
|
+
'chat-session-message',
|
|
862
|
+
'chat-session-error',
|
|
699
863
|
'chat-send-message',
|
|
700
864
|
'chat-send-error',
|
|
701
865
|
'chat-file-uploaded',
|
|
@@ -714,7 +878,8 @@
|
|
|
714
878
|
const textArea=shadowRoot.querySelector('#chat_input');
|
|
715
879
|
const chatOutput = shadowRoot.querySelector('#chat_output');
|
|
716
880
|
const send = shadowRoot.querySelector('#chat_submit');
|
|
717
|
-
const
|
|
881
|
+
const chatSessionStatus=shadowRoot.querySelector('#chat_session_status');
|
|
882
|
+
const speech = speechImport;
|
|
718
883
|
const uploadBtn = shadowRoot.querySelector('#chat_upload');
|
|
719
884
|
const fileInput = shadowRoot.querySelector('#file_upload_input');
|
|
720
885
|
const conversationTimeboxPanel=shadowRoot.querySelector('#conversation_timebox');
|
|
@@ -723,17 +888,20 @@
|
|
|
723
888
|
const aiActivationPanel=shadowRoot.querySelector('#ai_activation');
|
|
724
889
|
const aiActivationTitle=shadowRoot.querySelector('#ai_activation_title');
|
|
725
890
|
const aiActivationStatus=shadowRoot.querySelector('#ai_activation_status');
|
|
891
|
+
const aiActivationProgress=shadowRoot.querySelector('#ai_activation_progress');
|
|
726
892
|
const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
|
|
727
893
|
const [
|
|
728
894
|
{default:MD},
|
|
729
895
|
{default:FileEntity},
|
|
730
896
|
{ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
|
|
731
|
-
{requestAIRuntimeIntent,subscribeAIRuntimeState}
|
|
897
|
+
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
898
|
+
{createPersistentAIChatSession}
|
|
732
899
|
]=await Promise.all([
|
|
733
900
|
import('../modules/MD.js'),
|
|
734
901
|
import('../entities/File.js'),
|
|
735
902
|
import('../modules/ConversationTimebox.js'),
|
|
736
|
-
import('../modules/AIRuntimeState.js')
|
|
903
|
+
import('../modules/AIRuntimeState.js'),
|
|
904
|
+
import('../modules/PersistentAIChatSession.js')
|
|
737
905
|
]);
|
|
738
906
|
|
|
739
907
|
host.language=host.language||'English';
|
|
@@ -746,6 +914,7 @@
|
|
|
746
914
|
let latestAIRuntimeRoles=null;
|
|
747
915
|
const hostSubmissionBarrier=new ConversationSubmissionBarrier();
|
|
748
916
|
const aiRuntimeStateAbortController=new AbortController();
|
|
917
|
+
const AI_TTS_FAILURE_EVENT='ai-tts-failure';
|
|
749
918
|
let programmaticSubmissionQueue=Promise.resolve(true);
|
|
750
919
|
let conversationTimeboxController=null;
|
|
751
920
|
let conversationTimeboxUnsubscribe=null;
|
|
@@ -754,10 +923,23 @@
|
|
|
754
923
|
let hostSubmissionGeneration=0;
|
|
755
924
|
let eventOperationSequence=0;
|
|
756
925
|
let destroyed=false;
|
|
926
|
+
let boundChatSession=null;
|
|
927
|
+
let boundChatAI=null;
|
|
928
|
+
let sessionBindingPending=false;
|
|
929
|
+
let sessionMessagePending=false;
|
|
930
|
+
let activeSessionMessageToken=null;
|
|
931
|
+
let sessionBindingGeneration=0;
|
|
932
|
+
let sessionMessageSequence=0;
|
|
933
|
+
let pendingStructuralToolCall=null;
|
|
934
|
+
let pendingStructuralToolMessage='';
|
|
757
935
|
const activeSubmissionOwnerships=new Set();
|
|
758
|
-
const conversationTimeboxRetryDelays=
|
|
759
|
-
const chatReasons=
|
|
936
|
+
const conversationTimeboxRetryDelays=[0,500,1_500];
|
|
937
|
+
const chatReasons={
|
|
760
938
|
ready:'chat-ready',
|
|
939
|
+
sessionBindingCompleted:'session-binding-completed',
|
|
940
|
+
sessionBindingRejected:'session-binding-rejected',
|
|
941
|
+
sessionMessageCompleted:'session-message-completed',
|
|
942
|
+
sessionMessageRejected:'session-message-rejected',
|
|
761
943
|
messageSubmissionRequested:'message-submission-requested',
|
|
762
944
|
messageSubmissionCancelled:'message-submission-cancelled',
|
|
763
945
|
callerSignalAborted:'caller-signal-aborted',
|
|
@@ -771,8 +953,12 @@
|
|
|
771
953
|
languageModelActivationRejected:'language-model-activation-rejected',
|
|
772
954
|
speechSynthesisRejected:'speech-synthesis-rejected',
|
|
773
955
|
conversationTimeboxDeliveryRejected:'conversation-timebox-delivery-rejected'
|
|
774
|
-
}
|
|
775
|
-
const chatErrorCodes=
|
|
956
|
+
};
|
|
957
|
+
const chatErrorCodes={
|
|
958
|
+
destroyed:'ARCANE_CHAT_DESTROYED',
|
|
959
|
+
sessionAlreadyBound:'ARCANE_CHAT_SESSION_ALREADY_BOUND',
|
|
960
|
+
sessionBindingRejected:'ARCANE_CHAT_SESSION_BINDING_REJECTED',
|
|
961
|
+
sessionMessageRejected:'ARCANE_CHAT_SESSION_MESSAGE_REJECTED',
|
|
776
962
|
messageSubmissionAborted:'ARCANE_CHAT_MESSAGE_SUBMISSION_ABORTED',
|
|
777
963
|
languageModelActivationRejected:'ARCANE_CHAT_LANGUAGE_MODEL_ACTIVATION_REQUEST_REJECTED',
|
|
778
964
|
hostMessageSubmissionRejected:'ARCANE_CHAT_HOST_MESSAGE_SUBMISSION_REJECTED',
|
|
@@ -780,7 +966,7 @@
|
|
|
780
966
|
languageChangeCallbackRejected:'ARCANE_CHAT_LANGUAGE_CHANGE_CALLBACK_REJECTED',
|
|
781
967
|
speechSynthesisRejected:'ARCANE_CHAT_SPEECH_SYNTHESIS_REQUEST_REJECTED',
|
|
782
968
|
conversationTimeboxDeliveryRejected:'ARCANE_CHAT_CONVERSATION_TIMEBOX_DELIVERY_REJECTED'
|
|
783
|
-
}
|
|
969
|
+
};
|
|
784
970
|
|
|
785
971
|
function nextChatOperationId(kind){
|
|
786
972
|
eventOperationSequence+=1;
|
|
@@ -826,15 +1012,528 @@
|
|
|
826
1012
|
|
|
827
1013
|
function publicErrorFields(error,boundaryCode){
|
|
828
1014
|
let causeCode='';
|
|
1015
|
+
let message='';
|
|
829
1016
|
try{
|
|
830
|
-
causeCode=typeof error?.code==='string'?error.code
|
|
1017
|
+
causeCode=typeof error?.code==='string'?error.code:'';
|
|
1018
|
+
message=typeof error?.message==='string'?error.message:'';
|
|
831
1019
|
}catch{}
|
|
832
|
-
return
|
|
1020
|
+
return {
|
|
1021
|
+
error,
|
|
833
1022
|
code:boundaryCode,
|
|
834
|
-
...(causeCode&&causeCode!==boundaryCode?{causeCode}:{})
|
|
1023
|
+
...(causeCode&&causeCode!==boundaryCode?{causeCode}:{}),
|
|
1024
|
+
...(message?{message}:{})
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function chatError(message,code){
|
|
1029
|
+
const error=new Error(message);
|
|
1030
|
+
error.code=code;
|
|
1031
|
+
return error;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function isPlainRecord(value){
|
|
1035
|
+
return Boolean(value)
|
|
1036
|
+
&&typeof value==='object'
|
|
1037
|
+
&&!Array.isArray(value)
|
|
1038
|
+
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function visibleErrorMessage(error,fallback){
|
|
1042
|
+
try{
|
|
1043
|
+
// Only errors deliberately classified by their owner may provide UI copy.
|
|
1044
|
+
// Provider and protocol diagnostics remain complete in console/events.
|
|
1045
|
+
if(error?.userSafe===true){
|
|
1046
|
+
const message=typeof error.userMessage==='string'
|
|
1047
|
+
?error.userMessage
|
|
1048
|
+
:error.message;
|
|
1049
|
+
if(typeof message==='string'&&message.trim()){
|
|
1050
|
+
return message;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}catch{}
|
|
1054
|
+
return fallback;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function visibleProgressText(value,fallback,field){
|
|
1058
|
+
if(typeof value==='string'){
|
|
1059
|
+
return value||fallback;
|
|
1060
|
+
}
|
|
1061
|
+
if(value&&typeof value==='object'){
|
|
1062
|
+
console.error(`Arcane chat progress ${field} is not text.`,value);
|
|
1063
|
+
return visibleErrorMessage(value,fallback);
|
|
1064
|
+
}
|
|
1065
|
+
if(value!==undefined&&value!==null&&value!==''){
|
|
1066
|
+
const error=new TypeError(
|
|
1067
|
+
`Arcane chat progress ${field} must be text.`,
|
|
1068
|
+
{cause:value}
|
|
1069
|
+
);
|
|
1070
|
+
console.error(`Arcane chat progress ${field} is not text.`,error);
|
|
1071
|
+
}
|
|
1072
|
+
return fallback;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function setSessionStatus(state,message){
|
|
1076
|
+
const status={state:String(state),message:String(message)};
|
|
1077
|
+
host.sessionStatus=status;
|
|
1078
|
+
chatSessionStatus.dataset.state=status.state;
|
|
1079
|
+
chatSessionStatus.value=status.message;
|
|
1080
|
+
chatSessionStatus.textContent=status.message;
|
|
1081
|
+
return status;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function compatibleChatSession(session){
|
|
1085
|
+
return Boolean(session)
|
|
1086
|
+
&&typeof session==='object'
|
|
1087
|
+
&&typeof session.ready==='function'
|
|
1088
|
+
&&typeof session.history==='function'
|
|
1089
|
+
&&typeof session.send==='function';
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function scrollTranscriptToBottom(){
|
|
1093
|
+
chatOutput.scrollTop=chatOutput.scrollHeight;
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function transcriptTime(timestamp){
|
|
1098
|
+
const time=chatOutput.ownerDocument.createElement('time');
|
|
1099
|
+
time.className='message_timestamp';
|
|
1100
|
+
const value=timestamp instanceof Date
|
|
1101
|
+
?timestamp
|
|
1102
|
+
:new Date(timestamp);
|
|
1103
|
+
if(timestamp!==undefined&×tamp!==null&&Number.isFinite(value.getTime())){
|
|
1104
|
+
time.dateTime=value.toISOString();
|
|
1105
|
+
time.textContent=value.toLocaleTimeString([],{
|
|
1106
|
+
hour:'2-digit',
|
|
1107
|
+
minute:'2-digit',
|
|
1108
|
+
hourCycle:'h23'
|
|
1109
|
+
});
|
|
1110
|
+
time.title=value.toLocaleString();
|
|
1111
|
+
}else{
|
|
1112
|
+
time.textContent='Time unavailable';
|
|
1113
|
+
time.setAttribute('aria-label','Message time unavailable');
|
|
1114
|
+
}
|
|
1115
|
+
return time;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function normalizeVisibleToolCalls(value){
|
|
1119
|
+
if(value===undefined) return [];
|
|
1120
|
+
if(!Array.isArray(value)){
|
|
1121
|
+
const failure=new TypeError('Chat session tool calls must be an array.');
|
|
1122
|
+
failure.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
1123
|
+
throw failure;
|
|
1124
|
+
}
|
|
1125
|
+
return value.map((call,index)=>{
|
|
1126
|
+
if(
|
|
1127
|
+
!call
|
|
1128
|
+
||typeof call!=='object'
|
|
1129
|
+
||Array.isArray(call)
|
|
1130
|
+
||call.type!=='function'
|
|
1131
|
+
||!call.function
|
|
1132
|
+
||typeof call.function!=='object'
|
|
1133
|
+
||Array.isArray(call.function)
|
|
1134
|
+
||typeof call.id!=='string'
|
|
1135
|
+
||!call.id.trim()
|
|
1136
|
+
||typeof call.function.name!=='string'
|
|
1137
|
+
||!call.function.name.trim()
|
|
1138
|
+
||typeof call.function.arguments!=='string'
|
|
1139
|
+
){
|
|
1140
|
+
const failure=new TypeError(`Chat session tool call ${index+1} is invalid.`);
|
|
1141
|
+
failure.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
1142
|
+
throw failure;
|
|
1143
|
+
}
|
|
1144
|
+
structuralToolMessage(call);
|
|
1145
|
+
return call;
|
|
835
1146
|
});
|
|
836
1147
|
}
|
|
837
1148
|
|
|
1149
|
+
function structuralToolMessage(call){
|
|
1150
|
+
let argumentsRecord;
|
|
1151
|
+
try{
|
|
1152
|
+
argumentsRecord=JSON.parse(call.function.arguments);
|
|
1153
|
+
}catch(error){
|
|
1154
|
+
const failure=new TypeError(
|
|
1155
|
+
'Structural tool call arguments must encode a JSON object.',
|
|
1156
|
+
{cause:error}
|
|
1157
|
+
);
|
|
1158
|
+
failure.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
1159
|
+
throw failure;
|
|
1160
|
+
}
|
|
1161
|
+
if(!isPlainRecord(argumentsRecord)){
|
|
1162
|
+
const failure=new TypeError(
|
|
1163
|
+
'Structural tool call arguments must encode a JSON object.'
|
|
1164
|
+
);
|
|
1165
|
+
failure.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
1166
|
+
throw failure;
|
|
1167
|
+
}
|
|
1168
|
+
if(typeof argumentsRecord.message!=='string'||!argumentsRecord.message.trim()){
|
|
1169
|
+
const failure=new TypeError(
|
|
1170
|
+
'Structural tool call arguments must include a nonempty user-facing message.'
|
|
1171
|
+
);
|
|
1172
|
+
failure.code='AI_CHAT_TOOL_MESSAGE_REQUIRED';
|
|
1173
|
+
throw failure;
|
|
1174
|
+
}
|
|
1175
|
+
return argumentsRecord.message;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function setPendingStructuralToolCall(call=null){
|
|
1179
|
+
if(call===null){
|
|
1180
|
+
pendingStructuralToolCall=null;
|
|
1181
|
+
pendingStructuralToolMessage='';
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
pendingStructuralToolMessage=structuralToolMessage(call);
|
|
1185
|
+
pendingStructuralToolCall={
|
|
1186
|
+
id:call.id,
|
|
1187
|
+
type:'function',
|
|
1188
|
+
function:{
|
|
1189
|
+
name:call.function.name,
|
|
1190
|
+
arguments:call.function.arguments
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
return pendingStructuralToolCall;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function sameStructuralToolCall(left,right){
|
|
1197
|
+
return Boolean(left&&right)
|
|
1198
|
+
&&left.id===right.id
|
|
1199
|
+
&&left.type===right.type
|
|
1200
|
+
&&left.function?.name===right.function?.name
|
|
1201
|
+
&&left.function?.arguments===right.function?.arguments;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function pendingStructuralToolSummary(){
|
|
1205
|
+
return pendingStructuralToolCall
|
|
1206
|
+
?{
|
|
1207
|
+
id:pendingStructuralToolCall.id,
|
|
1208
|
+
name:pendingStructuralToolCall.function.name,
|
|
1209
|
+
message:pendingStructuralToolMessage
|
|
1210
|
+
}
|
|
1211
|
+
:null;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function appendVisibleToolCall(item,call){
|
|
1215
|
+
const toolCalls=item.querySelector('.message_tool_calls')
|
|
1216
|
+
??item.ownerDocument.createElement('section');
|
|
1217
|
+
if(!toolCalls.classList.contains('message_tool_calls')){
|
|
1218
|
+
toolCalls.className='message_tool_calls';
|
|
1219
|
+
toolCalls.setAttribute('aria-label','Tool calls');
|
|
1220
|
+
item.insertBefore(toolCalls,item.querySelector('.message_timestamp'));
|
|
1221
|
+
}
|
|
1222
|
+
const existing=[...toolCalls.children].find(
|
|
1223
|
+
candidate=>candidate.dataset.toolCallId===call.id
|
|
1224
|
+
);
|
|
1225
|
+
if(existing) return existing;
|
|
1226
|
+
const entry=item.ownerDocument.createElement('article');
|
|
1227
|
+
entry.className='message_tool_call';
|
|
1228
|
+
entry.dataset.toolCallId=call.id;
|
|
1229
|
+
const heading=item.ownerDocument.createElement('strong');
|
|
1230
|
+
heading.textContent=`Tool: ${call.function.name}`;
|
|
1231
|
+
const userMessage=item.ownerDocument.createElement('p');
|
|
1232
|
+
userMessage.className='message_tool_message';
|
|
1233
|
+
userMessage.textContent=structuralToolMessage(call);
|
|
1234
|
+
const details=item.ownerDocument.createElement('details');
|
|
1235
|
+
details.className='message_tool_details';
|
|
1236
|
+
const summary=item.ownerDocument.createElement('summary');
|
|
1237
|
+
summary.textContent='Tool call details';
|
|
1238
|
+
const argumentsBlock=item.ownerDocument.createElement('pre');
|
|
1239
|
+
const code=item.ownerDocument.createElement('code');
|
|
1240
|
+
code.textContent=call.function.arguments;
|
|
1241
|
+
argumentsBlock.append(code);
|
|
1242
|
+
details.append(summary,argumentsBlock);
|
|
1243
|
+
entry.append(heading,userMessage,details);
|
|
1244
|
+
toolCalls.append(entry);
|
|
1245
|
+
scrollTranscriptToBottom();
|
|
1246
|
+
return entry;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function setTranscriptMessageContent(item,content,timestamp){
|
|
1250
|
+
if(typeof content!=='string'){
|
|
1251
|
+
throw new TypeError('Chat session message content must be a string.');
|
|
1252
|
+
}
|
|
1253
|
+
item.querySelector('.thinking')?.remove();
|
|
1254
|
+
const markdown=item.querySelector('.markdown');
|
|
1255
|
+
markdown.raw=content;
|
|
1256
|
+
markdown.innerHTML=new MD(content).rendered;
|
|
1257
|
+
if(timestamp!==undefined){
|
|
1258
|
+
item.querySelector('.message_timestamp')?.replaceWith(
|
|
1259
|
+
transcriptTime(timestamp)
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
scrollTranscriptToBottom();
|
|
1263
|
+
return item;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function setTranscriptMessageTimestamp(item,timestamp){
|
|
1267
|
+
const current=item?.querySelector?.('.message_timestamp');
|
|
1268
|
+
if(!current||timestamp===undefined||timestamp===null){
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
current.replaceWith(transcriptTime(timestamp));
|
|
1272
|
+
scrollTranscriptToBottom();
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function latestCommittedTranscriptTurn(transcript,requestMessage){
|
|
1277
|
+
if(!Array.isArray(transcript)) return null;
|
|
1278
|
+
const requestRole=requestMessage?.role??'user';
|
|
1279
|
+
const requestContent=requestMessage?.content;
|
|
1280
|
+
for(let index=transcript.length-1;index>0;index--){
|
|
1281
|
+
const response=transcript[index];
|
|
1282
|
+
const request=transcript[index-1];
|
|
1283
|
+
if(
|
|
1284
|
+
response?.role==='assistant'
|
|
1285
|
+
&&request?.role===requestRole
|
|
1286
|
+
&&request.content===requestContent
|
|
1287
|
+
&&(
|
|
1288
|
+
requestRole!=='tool'
|
|
1289
|
+
||request.tool_call_id===requestMessage.tool_call_id
|
|
1290
|
+
)
|
|
1291
|
+
){
|
|
1292
|
+
return {request,response};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function createTranscriptMessage(role,content,name,{timestamp,toolCalls}={}){
|
|
1299
|
+
if(typeof content!=='string'){
|
|
1300
|
+
throw new TypeError('Chat session message content must be a string.');
|
|
1301
|
+
}
|
|
1302
|
+
const item=chatOutput.ownerDocument.createElement('li');
|
|
1303
|
+
item.className=role==='user'?'user':'ai';
|
|
1304
|
+
item.dataset.role=role;
|
|
1305
|
+
const header=chatOutput.ownerDocument.createElement('header');
|
|
1306
|
+
header.className='message_header';
|
|
1307
|
+
const heading=chatOutput.ownerDocument.createElement('strong');
|
|
1308
|
+
heading.textContent=name;
|
|
1309
|
+
header.append(heading);
|
|
1310
|
+
const markdown=chatOutput.ownerDocument.createElement('div');
|
|
1311
|
+
markdown.className='markdown';
|
|
1312
|
+
markdown.raw=content;
|
|
1313
|
+
markdown.innerHTML=new MD(content).rendered;
|
|
1314
|
+
item.append(header,markdown,transcriptTime(timestamp));
|
|
1315
|
+
for(const call of normalizeVisibleToolCalls(toolCalls)){
|
|
1316
|
+
appendVisibleToolCall(item,call);
|
|
1317
|
+
}
|
|
1318
|
+
return item;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function appendTranscriptMessage(role,content,name,{timestamp=Date.now(),toolCalls}={}){
|
|
1322
|
+
const item=createTranscriptMessage(role,content,name,{timestamp,toolCalls});
|
|
1323
|
+
chatOutput.append(item);
|
|
1324
|
+
scrollTranscriptToBottom();
|
|
1325
|
+
return item;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function renderSessionHistory(history){
|
|
1329
|
+
if(!Array.isArray(history)){
|
|
1330
|
+
throw new TypeError('Chat session history must be an array.');
|
|
1331
|
+
}
|
|
1332
|
+
const fragment=chatOutput.ownerDocument.createDocumentFragment();
|
|
1333
|
+
const rendered=[];
|
|
1334
|
+
let restoredPendingToolCall=null;
|
|
1335
|
+
for(const message of history){
|
|
1336
|
+
if(!message||typeof message!=='object'||Array.isArray(message)){
|
|
1337
|
+
throw new TypeError('Chat session history contains an invalid message.');
|
|
1338
|
+
}
|
|
1339
|
+
if(message.role==='system'){
|
|
1340
|
+
if(restoredPendingToolCall){
|
|
1341
|
+
throw chatError(
|
|
1342
|
+
'Saved chat contains a system record before its pending structural tool result.',
|
|
1343
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
if(!['assistant','tool','user'].includes(message.role)){
|
|
1349
|
+
throw new TypeError(`Chat session history contains an unsupported role: ${message.role}.`);
|
|
1350
|
+
}
|
|
1351
|
+
if(typeof message.content!=='string'){
|
|
1352
|
+
throw new TypeError('Chat session history message content must be a string.');
|
|
1353
|
+
}
|
|
1354
|
+
if(message.role==='tool'&&!message.content.trim()){
|
|
1355
|
+
throw chatError(
|
|
1356
|
+
'Saved chat contains a blank structural tool result.',
|
|
1357
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const calls=normalizeVisibleToolCalls(message.tool_calls);
|
|
1361
|
+
if(calls.length>1){
|
|
1362
|
+
throw chatError(
|
|
1363
|
+
'Saved chat contains parallel structural tool calls.',
|
|
1364
|
+
'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if(message.role!=='assistant'&&calls.length){
|
|
1368
|
+
throw chatError(
|
|
1369
|
+
'Only assistant records may contain structural tool calls.',
|
|
1370
|
+
'AI_CHAT_INVALID_TOOL_CALL'
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
if(message.role==='tool'){
|
|
1374
|
+
if(
|
|
1375
|
+
!restoredPendingToolCall
|
|
1376
|
+
||typeof message.tool_call_id!=='string'
|
|
1377
|
+
||message.tool_call_id!==restoredPendingToolCall.id
|
|
1378
|
+
){
|
|
1379
|
+
throw chatError(
|
|
1380
|
+
'Saved chat contains a tool result that does not match its pending structural tool call.',
|
|
1381
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
restoredPendingToolCall=null;
|
|
1385
|
+
}else{
|
|
1386
|
+
if(restoredPendingToolCall){
|
|
1387
|
+
throw chatError(
|
|
1388
|
+
'Saved chat continues before its pending structural tool result.',
|
|
1389
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if(calls.length){
|
|
1393
|
+
restoredPendingToolCall=calls[0];
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const name=message.role==='user'
|
|
1397
|
+
?host.name
|
|
1398
|
+
:message.role==='tool'
|
|
1399
|
+
?'Tool'
|
|
1400
|
+
:host.aiName;
|
|
1401
|
+
const item=createTranscriptMessage(message.role,message.content,name,{
|
|
1402
|
+
timestamp:message.timestamp,
|
|
1403
|
+
toolCalls:calls,
|
|
1404
|
+
});
|
|
1405
|
+
fragment.append(item);
|
|
1406
|
+
rendered.push(item);
|
|
1407
|
+
}
|
|
1408
|
+
setPendingStructuralToolCall(restoredPendingToolCall);
|
|
1409
|
+
chatOutput.replaceChildren(fragment);
|
|
1410
|
+
scrollTranscriptToBottom();
|
|
1411
|
+
return rendered;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async function bindSession(options={}){
|
|
1415
|
+
if(destroyed){
|
|
1416
|
+
throw chatError(
|
|
1417
|
+
'The chat component has been destroyed and cannot bind a session.',
|
|
1418
|
+
chatErrorCodes.destroyed
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
if(!isPlainRecord(options)){
|
|
1422
|
+
throw new TypeError('Chat session binding options must be a plain object.');
|
|
1423
|
+
}
|
|
1424
|
+
const unsupported=Object.keys(options).find(
|
|
1425
|
+
key=>!['ai','session','sessionOptions'].includes(key)
|
|
1426
|
+
);
|
|
1427
|
+
if(unsupported){
|
|
1428
|
+
throw new TypeError(`Unsupported chat session binding option: ${unsupported}.`);
|
|
1429
|
+
}
|
|
1430
|
+
if(sessionBindingPending||boundChatSession){
|
|
1431
|
+
throw chatError(
|
|
1432
|
+
'This chat component already has a session binding.',
|
|
1433
|
+
chatErrorCodes.sessionAlreadyBound
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const ai=options.ai??null;
|
|
1437
|
+
const suppliedSession=options.session??null;
|
|
1438
|
+
const sessionOptions=options.sessionOptions??{};
|
|
1439
|
+
if((ai===null)===(suppliedSession===null)){
|
|
1440
|
+
throw new TypeError('Specify exactly one of ai or session when binding chat.');
|
|
1441
|
+
}
|
|
1442
|
+
if(!isPlainRecord(sessionOptions)){
|
|
1443
|
+
throw new TypeError('sessionOptions must be a plain object.');
|
|
1444
|
+
}
|
|
1445
|
+
if(suppliedSession!==null&&Object.keys(sessionOptions).length>0){
|
|
1446
|
+
throw new TypeError('sessionOptions cannot be used with an existing session.');
|
|
1447
|
+
}
|
|
1448
|
+
if(suppliedSession!==null&&!compatibleChatSession(suppliedSession)){
|
|
1449
|
+
throw new TypeError('session must expose ready(), history(), and send().');
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const operationId=nextChatOperationId('session-binding');
|
|
1453
|
+
const generation=++sessionBindingGeneration;
|
|
1454
|
+
sessionBindingPending=true;
|
|
1455
|
+
setSessionStatus('binding','Opening chat session…');
|
|
1456
|
+
try{
|
|
1457
|
+
const session=suppliedSession??await createPersistentAIChatSession({
|
|
1458
|
+
...sessionOptions,
|
|
1459
|
+
ai
|
|
1460
|
+
});
|
|
1461
|
+
await session.ready();
|
|
1462
|
+
const history=typeof session.transcript==='function'
|
|
1463
|
+
?await session.transcript()
|
|
1464
|
+
:await session.history();
|
|
1465
|
+
if(destroyed||generation!==sessionBindingGeneration){
|
|
1466
|
+
throw chatError(
|
|
1467
|
+
'The chat component was destroyed before session binding completed.',
|
|
1468
|
+
chatErrorCodes.destroyed
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
renderSessionHistory(history);
|
|
1472
|
+
boundChatSession=session;
|
|
1473
|
+
boundChatAI=ai??session.ai??null;
|
|
1474
|
+
host.session=session;
|
|
1475
|
+
setSessionStatus(
|
|
1476
|
+
pendingStructuralToolMessage?'tool':'ready',
|
|
1477
|
+
pendingStructuralToolMessage||'Chat ready.'
|
|
1478
|
+
);
|
|
1479
|
+
applyAIAvailability(host.aiAvailability);
|
|
1480
|
+
sessionBindingPending=false;
|
|
1481
|
+
const detail={
|
|
1482
|
+
chat:host,
|
|
1483
|
+
session,
|
|
1484
|
+
ai:boundChatAI,
|
|
1485
|
+
history,
|
|
1486
|
+
pendingTool:pendingStructuralToolSummary()
|
|
1487
|
+
};
|
|
1488
|
+
dispatchChatEvent(
|
|
1489
|
+
'chat-session-bound',
|
|
1490
|
+
detail,
|
|
1491
|
+
{
|
|
1492
|
+
bubbles:true,
|
|
1493
|
+
composed:true,
|
|
1494
|
+
operationId,
|
|
1495
|
+
publicDetail:{
|
|
1496
|
+
...detail,
|
|
1497
|
+
reason:chatReasons.sessionBindingCompleted
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
);
|
|
1501
|
+
return session;
|
|
1502
|
+
}catch(error){
|
|
1503
|
+
if(!destroyed&&generation===sessionBindingGeneration){
|
|
1504
|
+
const internalProtocolFailure=internalStructuralToolFailure(error);
|
|
1505
|
+
console.error('Arcane saved chat could not be opened.',error);
|
|
1506
|
+
const message=internalProtocolFailure
|
|
1507
|
+
?'This saved chat could not be opened.'
|
|
1508
|
+
:visibleErrorMessage(error,'The chat session could not be opened.');
|
|
1509
|
+
setSessionStatus('error',message);
|
|
1510
|
+
dispatchChatEvent(
|
|
1511
|
+
'chat-session-error',
|
|
1512
|
+
{phase:'binding',error,options},
|
|
1513
|
+
{
|
|
1514
|
+
bubbles:true,
|
|
1515
|
+
composed:true,
|
|
1516
|
+
operationId,
|
|
1517
|
+
publicDetail:{
|
|
1518
|
+
phase:'binding',
|
|
1519
|
+
options,
|
|
1520
|
+
reason:chatReasons.sessionBindingRejected,
|
|
1521
|
+
...publicErrorFields(
|
|
1522
|
+
error,
|
|
1523
|
+
chatErrorCodes.sessionBindingRejected
|
|
1524
|
+
)
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
throw error;
|
|
1530
|
+
}finally{
|
|
1531
|
+
if(generation===sessionBindingGeneration){
|
|
1532
|
+
sessionBindingPending=false;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
838
1537
|
function isAbortSignal(value){
|
|
839
1538
|
return value!==null
|
|
840
1539
|
&&typeof value==='object'
|
|
@@ -879,11 +1578,11 @@
|
|
|
879
1578
|
return true;
|
|
880
1579
|
}
|
|
881
1580
|
|
|
882
|
-
const ownership=
|
|
1581
|
+
const ownership={
|
|
883
1582
|
signal:controller.signal,
|
|
884
1583
|
abort:abortChatSubmission,
|
|
885
1584
|
release:releaseChatSubmissionOwnership
|
|
886
|
-
}
|
|
1585
|
+
};
|
|
887
1586
|
activeSubmissionOwnerships.add(ownership);
|
|
888
1587
|
|
|
889
1588
|
function followAbortSignal(signal,fallbackReason){
|
|
@@ -940,6 +1639,7 @@
|
|
|
940
1639
|
panel:aiActivationPanel,
|
|
941
1640
|
title:aiActivationTitle,
|
|
942
1641
|
status:aiActivationStatus,
|
|
1642
|
+
progress:aiActivationProgress,
|
|
943
1643
|
button:aiActivationButton
|
|
944
1644
|
});
|
|
945
1645
|
|
|
@@ -949,8 +1649,18 @@
|
|
|
949
1649
|
host.setInitialSpeechMuted=setInitialSpeechMuted;
|
|
950
1650
|
host.setConversationComplete=setConversationComplete;
|
|
951
1651
|
host.bindConversationTimebox=bindConversationTimebox;
|
|
1652
|
+
host.bindSession=bindSession;
|
|
952
1653
|
host.submitMessage=submitMessage;
|
|
1654
|
+
host.submitToolResult=submitToolResult;
|
|
1655
|
+
Object.defineProperty(host,'pendingTool',{
|
|
1656
|
+
configurable:true,
|
|
1657
|
+
enumerable:true,
|
|
1658
|
+
get:pendingStructuralToolSummary
|
|
1659
|
+
});
|
|
953
1660
|
host.destroy=destroy;
|
|
1661
|
+
host.session=null;
|
|
1662
|
+
host.sessionStatus={state:'idle',message:'Chat session is not connected.'};
|
|
1663
|
+
host.modelName=typeof host.modelName==='string'?host.modelName:'';
|
|
954
1664
|
host.aiAvailability={llm:false,stt:false,tts:false};
|
|
955
1665
|
host.conversationComplete=false;
|
|
956
1666
|
applyAIAvailability(host.aiAvailability);
|
|
@@ -965,6 +1675,17 @@
|
|
|
965
1675
|
destroy,
|
|
966
1676
|
{once:true,signal:aiRuntimeStateAbortController.signal}
|
|
967
1677
|
);
|
|
1678
|
+
window.addEventListener(
|
|
1679
|
+
AI_TTS_FAILURE_EVENT,
|
|
1680
|
+
function reportActiveRuntimeTTSFailure(event){
|
|
1681
|
+
const aiRuntime=boundChatAI??globalThis.ai;
|
|
1682
|
+
if(!aiRuntime||event.detail?.ai!==aiRuntime){
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
reportTTSError(event.detail?.error,event.detail?.boundary);
|
|
1686
|
+
},
|
|
1687
|
+
{signal:aiRuntimeStateAbortController.signal}
|
|
1688
|
+
);
|
|
968
1689
|
|
|
969
1690
|
function setConversationComplete(complete=true){
|
|
970
1691
|
if(typeof complete!=='boolean'){
|
|
@@ -975,11 +1696,14 @@
|
|
|
975
1696
|
chatArea.dataset.conversationComplete=String(complete);
|
|
976
1697
|
textArea.disabled=complete;
|
|
977
1698
|
uploadBtn.disabled=complete;
|
|
978
|
-
send.disabled=complete
|
|
1699
|
+
send.disabled=complete
|
|
1700
|
+
||!host.aiAvailability.llm
|
|
1701
|
+
||sessionMessagePending
|
|
1702
|
+
||Boolean(pendingStructuralToolMessage);
|
|
979
1703
|
if(complete){
|
|
980
1704
|
textArea.blur?.();
|
|
981
1705
|
chatOutput.focus?.({preventScroll:true});
|
|
982
|
-
|
|
1706
|
+
scrollTranscriptToBottom();
|
|
983
1707
|
}
|
|
984
1708
|
return complete;
|
|
985
1709
|
}
|
|
@@ -1020,7 +1744,7 @@
|
|
|
1020
1744
|
status=''
|
|
1021
1745
|
}={}
|
|
1022
1746
|
){
|
|
1023
|
-
const messageId=String(id)
|
|
1747
|
+
const messageId=String(id);
|
|
1024
1748
|
const normalizedTotal=Number.isFinite(Number(total))
|
|
1025
1749
|
?Math.max(0,Math.floor(Number(total)))
|
|
1026
1750
|
:0;
|
|
@@ -1075,19 +1799,21 @@
|
|
|
1075
1799
|
const warningLabel=normalizedFailed>0
|
|
1076
1800
|
?` · ${normalizedFailed} warning${normalizedFailed===1?'':'s'}`
|
|
1077
1801
|
:'';
|
|
1802
|
+
const visibleLabel=visibleProgressText(label,'Working','label');
|
|
1803
|
+
const visibleStatus=visibleProgressText(status,'Streaming','status');
|
|
1078
1804
|
const progressStatus=isIndeterminate
|
|
1079
|
-
?`${
|
|
1805
|
+
?`${visibleStatus}${warningLabel}`
|
|
1080
1806
|
:`${normalizedCurrent} of ${normalizedTotal}${warningLabel}`;
|
|
1081
1807
|
const percent=normalizedTotal>0
|
|
1082
1808
|
?normalizedCurrent/normalizedTotal*100
|
|
1083
1809
|
:0;
|
|
1084
1810
|
|
|
1085
|
-
labelElement.textContent=
|
|
1811
|
+
labelElement.textContent=visibleLabel;
|
|
1086
1812
|
countElement.textContent=progressStatus;
|
|
1087
1813
|
progress.classList.toggle('indeterminate',isIndeterminate);
|
|
1088
1814
|
fill.style.width=isIndeterminate?'32%':`${percent}%`;
|
|
1089
1815
|
progress.setAttribute('role','progressbar');
|
|
1090
|
-
progress.setAttribute('aria-label',
|
|
1816
|
+
progress.setAttribute('aria-label',visibleLabel);
|
|
1091
1817
|
if(isIndeterminate){
|
|
1092
1818
|
progress.removeAttribute('aria-valuemin');
|
|
1093
1819
|
progress.removeAttribute('aria-valuemax');
|
|
@@ -1099,9 +1825,9 @@
|
|
|
1099
1825
|
}
|
|
1100
1826
|
progress.setAttribute(
|
|
1101
1827
|
'aria-valuetext',
|
|
1102
|
-
`${
|
|
1828
|
+
`${visibleLabel} ${progressStatus}`
|
|
1103
1829
|
);
|
|
1104
|
-
|
|
1830
|
+
scrollTranscriptToBottom();
|
|
1105
1831
|
return true;
|
|
1106
1832
|
}
|
|
1107
1833
|
|
|
@@ -1134,8 +1860,13 @@
|
|
|
1134
1860
|
|
|
1135
1861
|
function applyAIAvailability(availability){
|
|
1136
1862
|
host.aiAvailability={...availability};
|
|
1137
|
-
send.disabled=host.conversationComplete
|
|
1138
|
-
|
|
1863
|
+
send.disabled=host.conversationComplete
|
|
1864
|
+
||!availability.llm
|
|
1865
|
+
||sessionMessagePending
|
|
1866
|
+
||Boolean(pendingStructuralToolMessage);
|
|
1867
|
+
send.title=pendingStructuralToolMessage
|
|
1868
|
+
?pendingStructuralToolMessage
|
|
1869
|
+
:availability.llm
|
|
1139
1870
|
?'Send message'
|
|
1140
1871
|
:'The selected language model service is unavailable.';
|
|
1141
1872
|
send.setAttribute('aria-label',send.title);
|
|
@@ -1146,6 +1877,7 @@
|
|
|
1146
1877
|
panel,
|
|
1147
1878
|
title,
|
|
1148
1879
|
status,
|
|
1880
|
+
progress,
|
|
1149
1881
|
button,
|
|
1150
1882
|
publish=dispatchChatEvent,
|
|
1151
1883
|
createOperationId=nextChatOperationId,
|
|
@@ -1159,13 +1891,6 @@
|
|
|
1159
1891
|
let requestGeneration=0;
|
|
1160
1892
|
let destroyed=false;
|
|
1161
1893
|
|
|
1162
|
-
function visibleError(error,fallback){
|
|
1163
|
-
const message=typeof error?.message==='string'
|
|
1164
|
-
?error.message.trim()
|
|
1165
|
-
:'';
|
|
1166
|
-
return (message||fallback).slice(0,240);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
1894
|
function cancellation(error){
|
|
1170
1895
|
return error?.name==='AbortError'
|
|
1171
1896
|
||['ARCANE_AI_REQUEST_ABORTED','AI_CHAT_ABORTED'].includes(error?.code);
|
|
@@ -1178,15 +1903,76 @@
|
|
|
1178
1903
|
&&role.modelId.length>0;
|
|
1179
1904
|
}
|
|
1180
1905
|
|
|
1906
|
+
function modelDisplayName(){
|
|
1907
|
+
const configured=typeof host.modelName==='string'
|
|
1908
|
+
?host.modelName.trim()
|
|
1909
|
+
:'';
|
|
1910
|
+
return configured||role?.modelId||'the selected language model';
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function byteProgressUnit(value){
|
|
1914
|
+
const unit=String(value??'')
|
|
1915
|
+
.trim()
|
|
1916
|
+
.toLowerCase()
|
|
1917
|
+
.replace(/[\s_-]+/g,'');
|
|
1918
|
+
return unit.includes('byte')
|
|
1919
|
+
||unit.includes('octet')
|
|
1920
|
+
||/^(?:[kmgtpe]?i?b)(?:(?:\/|per)?(?:s|sec|second))?$/u.test(unit);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function determinateProgress(){
|
|
1924
|
+
const total=Number(role?.progress?.total);
|
|
1925
|
+
const completed=Number(role?.progress?.completed);
|
|
1926
|
+
const unit=typeof role?.progress?.unit==='string'&&role.progress.unit
|
|
1927
|
+
?role.progress.unit
|
|
1928
|
+
:'items';
|
|
1929
|
+
if(
|
|
1930
|
+
byteProgressUnit(unit)
|
|
1931
|
+
||!Number.isFinite(total)
|
|
1932
|
+
||total<=0
|
|
1933
|
+
||!Number.isFinite(completed)
|
|
1934
|
+
){
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
return {
|
|
1938
|
+
completed:Math.max(0,completed),
|
|
1939
|
+
total,
|
|
1940
|
+
unit
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1181
1944
|
function progressMessage(){
|
|
1182
|
-
|
|
1183
|
-
|
|
1945
|
+
const measured=determinateProgress();
|
|
1946
|
+
const phase=typeof role?.progress?.phase==='string'&&role.progress.phase
|
|
1947
|
+
?role.progress.phase
|
|
1948
|
+
:'loading';
|
|
1949
|
+
const parts=[
|
|
1950
|
+
`Loading ${modelDisplayName()} through the Arcane SDK`,
|
|
1951
|
+
phase
|
|
1952
|
+
];
|
|
1953
|
+
if(measured){
|
|
1954
|
+
parts.push(`${measured.completed} of ${measured.total} ${measured.unit}`);
|
|
1955
|
+
}
|
|
1956
|
+
parts.push('The first activation can take several minutes; keep this tab open');
|
|
1957
|
+
return parts.join(' · ');
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function renderProgress(){
|
|
1961
|
+
const visible=requestPending||role?.state==='loading';
|
|
1962
|
+
progress.hidden=!visible;
|
|
1963
|
+
if(!visible){
|
|
1964
|
+
progress.removeAttribute('value');
|
|
1965
|
+
progress.removeAttribute('max');
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const measured=determinateProgress();
|
|
1969
|
+
if(!measured){
|
|
1970
|
+
progress.removeAttribute('value');
|
|
1971
|
+
progress.removeAttribute('max');
|
|
1972
|
+
return;
|
|
1184
1973
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
?`${progress.completed} ${progress.unit}`
|
|
1188
|
-
:`${progress.completed} of ${progress.total} ${progress.unit}`;
|
|
1189
|
-
return `${progress.phase}, ${amount}${progress.heartbeat?', active heartbeat':''}.`;
|
|
1974
|
+
progress.max=measured.total;
|
|
1975
|
+
progress.value=Math.min(measured.completed,measured.total);
|
|
1190
1976
|
}
|
|
1191
1977
|
|
|
1192
1978
|
function render(){
|
|
@@ -1200,6 +1986,7 @@
|
|
|
1200
1986
|
'aria-busy',
|
|
1201
1987
|
String(requestPending||['loading','unloading'].includes(role?.state))
|
|
1202
1988
|
);
|
|
1989
|
+
renderProgress();
|
|
1203
1990
|
if(!visible){
|
|
1204
1991
|
return;
|
|
1205
1992
|
}
|
|
@@ -1211,9 +1998,9 @@
|
|
|
1211
1998
|
return;
|
|
1212
1999
|
}
|
|
1213
2000
|
if(role.state==='loading'){
|
|
1214
|
-
title.textContent
|
|
2001
|
+
title.textContent=`Loading ${modelDisplayName()}`;
|
|
1215
2002
|
status.textContent=progressMessage();
|
|
1216
|
-
button.textContent='Cancel
|
|
2003
|
+
button.textContent='Cancel activation';
|
|
1217
2004
|
button.disabled=false;
|
|
1218
2005
|
return;
|
|
1219
2006
|
}
|
|
@@ -1226,7 +2013,7 @@
|
|
|
1226
2013
|
}
|
|
1227
2014
|
if(role.state==='error'||requestError){
|
|
1228
2015
|
title.textContent='Language model activation failed';
|
|
1229
|
-
status.textContent=requestError||
|
|
2016
|
+
status.textContent=requestError||visibleErrorMessage(
|
|
1230
2017
|
role.error,
|
|
1231
2018
|
'The selected language model could not start.'
|
|
1232
2019
|
);
|
|
@@ -1258,8 +2045,8 @@
|
|
|
1258
2045
|
return false;
|
|
1259
2046
|
}
|
|
1260
2047
|
requestError='';
|
|
1261
|
-
const intent=
|
|
1262
|
-
const request=
|
|
2048
|
+
const intent={role:'llm',action,reason:'user'};
|
|
2049
|
+
const request={intent,state:role};
|
|
1263
2050
|
const operationId=typeof role?.operationId==='string'&&role.operationId
|
|
1264
2051
|
?role.operationId
|
|
1265
2052
|
:createOperationId('llm-activation');
|
|
@@ -1272,12 +2059,7 @@
|
|
|
1272
2059
|
composed:true,
|
|
1273
2060
|
cancelable:true,
|
|
1274
2061
|
operationId,
|
|
1275
|
-
publicDetail:
|
|
1276
|
-
role:'llm',
|
|
1277
|
-
action,
|
|
1278
|
-
reason:reasons.languageModelActivationRequested,
|
|
1279
|
-
state:role.state
|
|
1280
|
-
}
|
|
2062
|
+
publicDetail:request
|
|
1281
2063
|
}
|
|
1282
2064
|
);
|
|
1283
2065
|
if(!accepted||destroyed||generation!==requestGeneration){
|
|
@@ -1301,22 +2083,20 @@
|
|
|
1301
2083
|
&&['unloaded','unloading'].includes(role?.state)){
|
|
1302
2084
|
return false;
|
|
1303
2085
|
}
|
|
1304
|
-
const message=
|
|
2086
|
+
const message=visibleErrorMessage(
|
|
1305
2087
|
error,
|
|
1306
2088
|
`The language model ${action} request failed.`
|
|
1307
2089
|
);
|
|
1308
2090
|
requestError=message;
|
|
2091
|
+
console.error('Arcane language model activation request failed.',error);
|
|
1309
2092
|
publish(
|
|
1310
2093
|
'chat-ai-activation-error',
|
|
1311
|
-
|
|
2094
|
+
{request,error,message},
|
|
1312
2095
|
{
|
|
1313
2096
|
bubbles:true,
|
|
1314
2097
|
composed:true,
|
|
1315
2098
|
operationId,
|
|
1316
|
-
publicDetail:{
|
|
1317
|
-
role:'llm',
|
|
1318
|
-
action,
|
|
1319
|
-
reason:reasons.languageModelActivationRejected,
|
|
2099
|
+
publicDetail:{request,error,message,
|
|
1320
2100
|
...readErrorFields(
|
|
1321
2101
|
error,
|
|
1322
2102
|
errorCodes.languageModelActivationRejected
|
|
@@ -1342,6 +2122,13 @@
|
|
|
1342
2122
|
if(destroyed){
|
|
1343
2123
|
return;
|
|
1344
2124
|
}
|
|
2125
|
+
if(
|
|
2126
|
+
nextRole?.state==='error'
|
|
2127
|
+
&&nextRole.error
|
|
2128
|
+
&&nextRole.error!==role?.error
|
|
2129
|
+
){
|
|
2130
|
+
console.error('Arcane language model runtime failed.',nextRole.error);
|
|
2131
|
+
}
|
|
1345
2132
|
if(role?.state!==nextRole.state
|
|
1346
2133
|
||role?.providerId!==nextRole.providerId
|
|
1347
2134
|
||role?.modelId!==nextRole.modelId
|
|
@@ -1366,7 +2153,7 @@
|
|
|
1366
2153
|
}
|
|
1367
2154
|
|
|
1368
2155
|
button.addEventListener('click',activateSelectedAI);
|
|
1369
|
-
return
|
|
2156
|
+
return {request,synchronize,destroy};
|
|
1370
2157
|
}
|
|
1371
2158
|
|
|
1372
2159
|
function synchronizeAIRuntimeState(snapshot){
|
|
@@ -1421,6 +2208,7 @@
|
|
|
1421
2208
|
try{
|
|
1422
2209
|
controller.start();
|
|
1423
2210
|
}catch(error){
|
|
2211
|
+
console.error('Arcane conversation timebox could not start.',error);
|
|
1424
2212
|
conversationTimeboxStatus.textContent='The elapsed timer could not start.';
|
|
1425
2213
|
conversationTimeboxPanel.dataset.error='true';
|
|
1426
2214
|
conversationTimeboxUnsubscribe?.();
|
|
@@ -1503,7 +2291,7 @@
|
|
|
1503
2291
|
const operationId=typeof context?.operationId==='string'&&context.operationId
|
|
1504
2292
|
?context.operationId
|
|
1505
2293
|
:nextChatOperationId('conversation-timebox');
|
|
1506
|
-
const deliveryContext=
|
|
2294
|
+
const deliveryContext={...context,operationId};
|
|
1507
2295
|
const prior=programmaticSubmissionQueue.catch(
|
|
1508
2296
|
function ignorePriorProgrammaticSubmissionFailure(){
|
|
1509
2297
|
return false;
|
|
@@ -1613,7 +2401,7 @@
|
|
|
1613
2401
|
}
|
|
1614
2402
|
}
|
|
1615
2403
|
);
|
|
1616
|
-
console.error('The conversation timebox message could not be sent.');
|
|
2404
|
+
console.error('The conversation timebox message could not be sent.',error);
|
|
1617
2405
|
return false;
|
|
1618
2406
|
}
|
|
1619
2407
|
);
|
|
@@ -1663,19 +2451,36 @@
|
|
|
1663
2451
|
);
|
|
1664
2452
|
|
|
1665
2453
|
async function streamMessage(text='', id='', isThinking){
|
|
2454
|
+
if(typeof text!=='string'){
|
|
2455
|
+
if(text instanceof Error){
|
|
2456
|
+
console.error('Arcane chat stream delivered an Error as content.',text);
|
|
2457
|
+
throw text;
|
|
2458
|
+
}
|
|
2459
|
+
const error=new TypeError(
|
|
2460
|
+
'Arcane chat stream content must be text.',
|
|
2461
|
+
{cause:text}
|
|
2462
|
+
);
|
|
2463
|
+
error.code='ARCANE_CHAT_STREAM_CONTENT_INVALID';
|
|
2464
|
+
console.error('Arcane chat stream delivered invalid content.',error);
|
|
2465
|
+
throw error;
|
|
2466
|
+
}
|
|
1666
2467
|
if(!id){
|
|
1667
2468
|
console.warn('Streaming messages require ids, none specified.')
|
|
1668
2469
|
return false;
|
|
1669
2470
|
}
|
|
1670
2471
|
|
|
1671
|
-
let message=chatOutput.
|
|
2472
|
+
let message=[...chatOutput.children].find(
|
|
2473
|
+
candidate=>candidate.id===`message-${id}`
|
|
2474
|
+
)||null;
|
|
1672
2475
|
|
|
1673
2476
|
if(!message){
|
|
1674
2477
|
await receivedMessage(host.aiName,isThinking?'':text,id);
|
|
1675
2478
|
if(!isThinking){
|
|
1676
2479
|
return false;
|
|
1677
2480
|
}
|
|
1678
|
-
message=chatOutput.
|
|
2481
|
+
message=[...chatOutput.children].find(
|
|
2482
|
+
candidate=>candidate.id===`message-${id}`
|
|
2483
|
+
)||null;
|
|
1679
2484
|
if(!message){
|
|
1680
2485
|
return false;
|
|
1681
2486
|
}
|
|
@@ -1688,7 +2493,10 @@
|
|
|
1688
2493
|
if(isThinking && !thinkingHTML){
|
|
1689
2494
|
thinkingHTML=message.ownerDocument.createElement('span');
|
|
1690
2495
|
thinkingHTML.className='thinking';
|
|
1691
|
-
message.
|
|
2496
|
+
message.insertBefore(
|
|
2497
|
+
thinkingHTML,
|
|
2498
|
+
message.querySelector('.message_timestamp')
|
|
2499
|
+
);
|
|
1692
2500
|
}
|
|
1693
2501
|
|
|
1694
2502
|
if(isThinking){
|
|
@@ -1699,7 +2507,7 @@
|
|
|
1699
2507
|
thinkingHTML.append(
|
|
1700
2508
|
message.ownerDocument.createTextNode(String(text))
|
|
1701
2509
|
);
|
|
1702
|
-
|
|
2510
|
+
scrollTranscriptToBottom();
|
|
1703
2511
|
return true;
|
|
1704
2512
|
}
|
|
1705
2513
|
|
|
@@ -1712,7 +2520,7 @@
|
|
|
1712
2520
|
&&host.aiAvailability.tts
|
|
1713
2521
|
&&speech.muted===false
|
|
1714
2522
|
){
|
|
1715
|
-
const aiRuntime=globalThis.ai;
|
|
2523
|
+
const aiRuntime=boundChatAI??globalThis.ai;
|
|
1716
2524
|
if(typeof aiRuntime?.streamTTS==='function'){
|
|
1717
2525
|
void Promise.resolve(aiRuntime.streamTTS(text)).catch(
|
|
1718
2526
|
reportTTSError
|
|
@@ -1732,7 +2540,7 @@
|
|
|
1732
2540
|
|
|
1733
2541
|
target.innerHTML=new MD(target.raw).rendered;
|
|
1734
2542
|
|
|
1735
|
-
|
|
2543
|
+
scrollTranscriptToBottom();
|
|
1736
2544
|
}
|
|
1737
2545
|
|
|
1738
2546
|
textArea.addEventListener(
|
|
@@ -1780,7 +2588,7 @@
|
|
|
1780
2588
|
cancelable:true,
|
|
1781
2589
|
operationId,
|
|
1782
2590
|
publicDetail:{
|
|
1783
|
-
|
|
2591
|
+
file,
|
|
1784
2592
|
type:String(file.type||''),
|
|
1785
2593
|
reason:chatReasons.fileStorageCompleted
|
|
1786
2594
|
}
|
|
@@ -1796,7 +2604,7 @@
|
|
|
1796
2604
|
composed:true,
|
|
1797
2605
|
operationId,
|
|
1798
2606
|
publicDetail:{
|
|
1799
|
-
|
|
2607
|
+
file,
|
|
1800
2608
|
type:String(file.type||''),
|
|
1801
2609
|
reason:chatReasons.fileStorageRejected,
|
|
1802
2610
|
...publicErrorFields(
|
|
@@ -1879,13 +2687,6 @@
|
|
|
1879
2687
|
{signal:aiRuntimeStateAbortController.signal}
|
|
1880
2688
|
);
|
|
1881
2689
|
|
|
1882
|
-
function getMilTime() {
|
|
1883
|
-
const now = new Date();
|
|
1884
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
1885
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
1886
|
-
return `${hours}:${minutes}`;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
2690
|
async function resizeTextArea(e={}) {
|
|
1890
2691
|
if(e.key && e.key == 'Enter' && !e.shiftKey && !send.disabled){
|
|
1891
2692
|
e.preventDefault();
|
|
@@ -1894,11 +2695,423 @@
|
|
|
1894
2695
|
|
|
1895
2696
|
// Reset height to shrink to content size
|
|
1896
2697
|
textArea.style.height = 'auto';
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2698
|
+
textArea.style.height = `${textArea.scrollHeight}px`;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
function renderSessionMessageFailure(messageId,error){
|
|
2702
|
+
const message=[...chatOutput.children].find(
|
|
2703
|
+
candidate=>candidate.id===`message-${messageId}`
|
|
2704
|
+
)||null;
|
|
2705
|
+
if(!message){
|
|
2706
|
+
return false;
|
|
2707
|
+
}
|
|
2708
|
+
message.classList.add('session_error');
|
|
2709
|
+
message.querySelector('.thinking')?.remove();
|
|
2710
|
+
const markdown=message.querySelector('.markdown');
|
|
2711
|
+
const text=visibleErrorMessage(error,'The AI request failed.');
|
|
2712
|
+
markdown.raw=text;
|
|
2713
|
+
markdown.innerHTML=new MD(text).rendered;
|
|
2714
|
+
scrollTranscriptToBottom();
|
|
2715
|
+
return true;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function internalStructuralToolFailure(error){
|
|
2719
|
+
return [
|
|
2720
|
+
'ARCANE_AI_TOOL_CALL_INVALID',
|
|
2721
|
+
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED',
|
|
2722
|
+
'ARCANE_AI_TOOL_MESSAGE_REQUIRED',
|
|
2723
|
+
'AI_CHAT_INVALID_TOOL_CALL',
|
|
2724
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE',
|
|
2725
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
2726
|
+
'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
|
|
2727
|
+
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
2728
|
+
'AI_CHAT_TOOL_MESSAGE_REQUIRED',
|
|
2729
|
+
'AI_CHAT_TOOL_RESULT_NOT_PENDING',
|
|
2730
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED',
|
|
2731
|
+
'AI_CHAT_TRANSACTION_SETTLED'
|
|
2732
|
+
].includes(error?.code);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
function restoreRejectedStructuralDraft(messageId,operationId,text){
|
|
2736
|
+
const response=[...chatOutput.children].find(
|
|
2737
|
+
candidate=>candidate.id===`message-${messageId}`
|
|
2738
|
+
)||null;
|
|
2739
|
+
const request=[...chatOutput.children].find(
|
|
2740
|
+
candidate=>candidate.dataset.operationId===operationId
|
|
2741
|
+
)||null;
|
|
2742
|
+
response?.remove();
|
|
2743
|
+
request?.remove();
|
|
2744
|
+
const existing=textArea.value;
|
|
2745
|
+
if(existing!==text){
|
|
2746
|
+
textArea.value=existing
|
|
2747
|
+
?`${text}\n${existing}`
|
|
2748
|
+
:text;
|
|
2749
|
+
}
|
|
2750
|
+
textArea.style.height='auto';
|
|
2751
|
+
textArea.style.height=`${textArea.scrollHeight}px`;
|
|
2752
|
+
scrollTranscriptToBottom();
|
|
2753
|
+
return Boolean(response||request);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
async function sendMessageThroughBoundSession(
|
|
2757
|
+
text,
|
|
2758
|
+
context,
|
|
2759
|
+
sessionRequestMessage={content:text,role:'user'},
|
|
2760
|
+
perTurnRequest=null
|
|
2761
|
+
){
|
|
2762
|
+
const session=boundChatSession;
|
|
2763
|
+
const bindingGeneration=sessionBindingGeneration;
|
|
2764
|
+
if(!session){
|
|
2765
|
+
throw chatError(
|
|
2766
|
+
'The chat session is not bound.',
|
|
2767
|
+
chatErrorCodes.sessionBindingRejected
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
if(sessionMessagePending){
|
|
2771
|
+
throw chatError(
|
|
2772
|
+
'A chat session message is already active.',
|
|
2773
|
+
'AI_CHAT_BUSY'
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
const request={
|
|
2778
|
+
message:sessionRequestMessage,
|
|
2779
|
+
signal:context.signal,
|
|
2780
|
+
...(perTurnRequest===null?{}:{request:perTurnRequest})
|
|
2781
|
+
};
|
|
2782
|
+
const previousPendingToolCall=pendingStructuralToolCall;
|
|
2783
|
+
let streamedStructuralToolCall=null;
|
|
2784
|
+
const messageId=`session-${++sessionMessageSequence}`;
|
|
2785
|
+
const sessionMessageToken=Symbol(messageId);
|
|
2786
|
+
activeSessionMessageToken=sessionMessageToken;
|
|
2787
|
+
sessionMessagePending=true;
|
|
2788
|
+
applyAIAvailability(host.aiAvailability);
|
|
2789
|
+
setSessionStatus('sending','AI is responding…');
|
|
2790
|
+
try{
|
|
2791
|
+
await receivedMessage(host.aiName,'',messageId);
|
|
2792
|
+
await streamMessage('',messageId,true);
|
|
2793
|
+
setMessageProgress(
|
|
2794
|
+
messageId,
|
|
2795
|
+
{
|
|
2796
|
+
label:'AI response',
|
|
2797
|
+
indeterminate:true,
|
|
2798
|
+
status:'Working'
|
|
2799
|
+
}
|
|
2800
|
+
);
|
|
2801
|
+
const result=typeof session.stream==='function'
|
|
2802
|
+
?await session.stream(
|
|
2803
|
+
request,
|
|
2804
|
+
{
|
|
2805
|
+
onChunk:async function renderSessionStreamChunk(chunk,requestId,isThinking){
|
|
2806
|
+
if(
|
|
2807
|
+
destroyed
|
|
2808
|
+
||bindingGeneration!==sessionBindingGeneration
|
|
2809
|
+
||session!==boundChatSession
|
|
2810
|
+
) return false;
|
|
2811
|
+
return streamMessage(chunk,messageId,isThinking);
|
|
2812
|
+
},
|
|
2813
|
+
onToolCall:async function renderSessionToolCall(call){
|
|
2814
|
+
if(
|
|
2815
|
+
destroyed
|
|
2816
|
+
||bindingGeneration!==sessionBindingGeneration
|
|
2817
|
+
||session!==boundChatSession
|
|
2818
|
+
) return false;
|
|
2819
|
+
const message=[...chatOutput.children].find(
|
|
2820
|
+
candidate=>candidate.id===`message-${messageId}`
|
|
2821
|
+
)||null;
|
|
2822
|
+
if(!message) return false;
|
|
2823
|
+
const normalized=normalizeVisibleToolCalls([call])[0];
|
|
2824
|
+
if(
|
|
2825
|
+
streamedStructuralToolCall
|
|
2826
|
+
&&!sameStructuralToolCall(streamedStructuralToolCall,normalized)
|
|
2827
|
+
){
|
|
2828
|
+
throw chatError(
|
|
2829
|
+
'The streamed structural tool call changed before completion.',
|
|
2830
|
+
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
if(streamedStructuralToolCall){
|
|
2834
|
+
return true;
|
|
2835
|
+
}
|
|
2836
|
+
streamedStructuralToolCall=normalized;
|
|
2837
|
+
setPendingStructuralToolCall(normalized);
|
|
2838
|
+
appendVisibleToolCall(message,normalized);
|
|
2839
|
+
setSessionStatus('tool',pendingStructuralToolMessage);
|
|
2840
|
+
return true;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
)
|
|
2844
|
+
:await session.send(request);
|
|
2845
|
+
if(
|
|
2846
|
+
destroyed
|
|
2847
|
+
||bindingGeneration!==sessionBindingGeneration
|
|
2848
|
+
||session!==boundChatSession
|
|
2849
|
+
){
|
|
2850
|
+
return false;
|
|
2851
|
+
}
|
|
2852
|
+
if(!result||typeof result!=='object'||Array.isArray(result)){
|
|
2853
|
+
throw new TypeError('The chat session returned an invalid result.');
|
|
2854
|
+
}
|
|
2855
|
+
if(!result.message||typeof result.message.content!=='string'){
|
|
2856
|
+
throw new TypeError('The chat session result must contain assistant message content.');
|
|
2857
|
+
}
|
|
2858
|
+
const committedTurn=typeof session.transcript==='function'
|
|
2859
|
+
?latestCommittedTranscriptTurn(
|
|
2860
|
+
await session.transcript(),
|
|
2861
|
+
sessionRequestMessage
|
|
2862
|
+
)
|
|
2863
|
+
:null;
|
|
2864
|
+
const message=[...chatOutput.children].find(
|
|
2865
|
+
candidate=>candidate.id===`message-${messageId}`
|
|
2866
|
+
)||null;
|
|
2867
|
+
if(!message){
|
|
2868
|
+
throw new Error('The active chat response card is unavailable.');
|
|
2869
|
+
}
|
|
2870
|
+
setTranscriptMessageContent(
|
|
2871
|
+
message,
|
|
2872
|
+
result.message.content,
|
|
2873
|
+
committedTurn?.response?.timestamp??result.message.timestamp
|
|
2874
|
+
);
|
|
2875
|
+
const requestMessage=[...chatOutput.children].find(
|
|
2876
|
+
candidate=>candidate.dataset.operationId===context.operationId
|
|
2877
|
+
)||null;
|
|
2878
|
+
setTranscriptMessageTimestamp(
|
|
2879
|
+
requestMessage,
|
|
2880
|
+
committedTurn?.request?.timestamp
|
|
2881
|
+
);
|
|
2882
|
+
const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
|
|
2883
|
+
if(terminalToolCalls.length>1){
|
|
2884
|
+
throw chatError(
|
|
2885
|
+
'The chat session returned parallel structural tool calls.',
|
|
2886
|
+
'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
if(
|
|
2890
|
+
streamedStructuralToolCall
|
|
2891
|
+
&&(
|
|
2892
|
+
terminalToolCalls.length!==1
|
|
2893
|
+
||!sameStructuralToolCall(
|
|
2894
|
+
streamedStructuralToolCall,
|
|
2895
|
+
terminalToolCalls[0]
|
|
2896
|
+
)
|
|
2897
|
+
)
|
|
2898
|
+
){
|
|
2899
|
+
throw chatError(
|
|
2900
|
+
'The terminal structural tool call does not match the streamed call.',
|
|
2901
|
+
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
if(!streamedStructuralToolCall&&terminalToolCalls.length){
|
|
2905
|
+
appendVisibleToolCall(message,terminalToolCalls[0]);
|
|
2906
|
+
}
|
|
2907
|
+
setPendingStructuralToolCall(
|
|
2908
|
+
terminalToolCalls.length?terminalToolCalls.at(-1):null
|
|
2909
|
+
);
|
|
2910
|
+
setSessionStatus(
|
|
2911
|
+
pendingStructuralToolMessage?'tool':'ready',
|
|
2912
|
+
pendingStructuralToolMessage||'Chat ready.'
|
|
2913
|
+
);
|
|
2914
|
+
const detail={
|
|
2915
|
+
session,
|
|
2916
|
+
request,
|
|
2917
|
+
result,
|
|
2918
|
+
message:result.message,
|
|
2919
|
+
context
|
|
2920
|
+
};
|
|
2921
|
+
if(activeSessionMessageToken===sessionMessageToken){
|
|
2922
|
+
activeSessionMessageToken=null;
|
|
2923
|
+
sessionMessagePending=false;
|
|
2924
|
+
applyAIAvailability(host.aiAvailability);
|
|
2925
|
+
}
|
|
2926
|
+
dispatchChatEvent(
|
|
2927
|
+
'chat-session-message',
|
|
2928
|
+
detail,
|
|
2929
|
+
{
|
|
2930
|
+
bubbles:true,
|
|
2931
|
+
composed:true,
|
|
2932
|
+
operationId:context.operationId||null,
|
|
2933
|
+
publicDetail:{
|
|
2934
|
+
...detail,
|
|
2935
|
+
reason:chatReasons.sessionMessageCompleted
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
);
|
|
2939
|
+
return result;
|
|
2940
|
+
}catch(error){
|
|
2941
|
+
if(!destroyed&&bindingGeneration===sessionBindingGeneration){
|
|
2942
|
+
setPendingStructuralToolCall(previousPendingToolCall);
|
|
2943
|
+
}
|
|
2944
|
+
if(
|
|
2945
|
+
!destroyed
|
|
2946
|
+
&&bindingGeneration===sessionBindingGeneration
|
|
2947
|
+
&&internalStructuralToolFailure(error)
|
|
2948
|
+
){
|
|
2949
|
+
if(sessionRequestMessage.role==='tool'){
|
|
2950
|
+
const response=[...chatOutput.children].find(
|
|
2951
|
+
candidate=>candidate.id===`message-${messageId}`
|
|
2952
|
+
)||null;
|
|
2953
|
+
const requestMessage=[...chatOutput.children].find(
|
|
2954
|
+
candidate=>candidate.dataset.operationId===context.operationId
|
|
2955
|
+
)||null;
|
|
2956
|
+
response?.remove();
|
|
2957
|
+
requestMessage?.remove();
|
|
2958
|
+
scrollTranscriptToBottom();
|
|
2959
|
+
}else{
|
|
2960
|
+
restoreRejectedStructuralDraft(messageId,context.operationId,text);
|
|
2961
|
+
}
|
|
2962
|
+
console.error('Arcane structural tool protocol failure.',error);
|
|
2963
|
+
setSessionStatus(
|
|
2964
|
+
pendingStructuralToolCall?'tool':'ready',
|
|
2965
|
+
pendingStructuralToolMessage||'Chat ready.'
|
|
2966
|
+
);
|
|
2967
|
+
}else if(!destroyed&&bindingGeneration===sessionBindingGeneration){
|
|
2968
|
+
console.error('Arcane chat request failed.',error);
|
|
2969
|
+
if(sessionRequestMessage.role==='tool'){
|
|
2970
|
+
const requestMessage=[...chatOutput.children].find(
|
|
2971
|
+
candidate=>candidate.dataset.operationId===context.operationId
|
|
2972
|
+
)||null;
|
|
2973
|
+
requestMessage?.remove();
|
|
2974
|
+
}
|
|
2975
|
+
renderSessionMessageFailure(messageId,error);
|
|
2976
|
+
setSessionStatus(
|
|
2977
|
+
'error',
|
|
2978
|
+
visibleErrorMessage(error,'The AI request failed.')
|
|
2979
|
+
);
|
|
2980
|
+
dispatchChatEvent(
|
|
2981
|
+
'chat-session-error',
|
|
2982
|
+
{phase:'message',session,request,context,error},
|
|
2983
|
+
{
|
|
2984
|
+
bubbles:true,
|
|
2985
|
+
composed:true,
|
|
2986
|
+
operationId:context.operationId||null,
|
|
2987
|
+
publicDetail:{
|
|
2988
|
+
phase:'message',
|
|
2989
|
+
session,
|
|
2990
|
+
request,
|
|
2991
|
+
context,
|
|
2992
|
+
reason:chatReasons.sessionMessageRejected,
|
|
2993
|
+
...publicErrorFields(
|
|
2994
|
+
error,
|
|
2995
|
+
chatErrorCodes.sessionMessageRejected
|
|
2996
|
+
)
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
throw error;
|
|
3002
|
+
}finally{
|
|
3003
|
+
if(activeSessionMessageToken===sessionMessageToken){
|
|
3004
|
+
activeSessionMessageToken=null;
|
|
3005
|
+
sessionMessagePending=false;
|
|
3006
|
+
}
|
|
3007
|
+
if(!destroyed&&activeSessionMessageToken===null){
|
|
3008
|
+
applyAIAvailability(host.aiAvailability);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
async function submitToolResult(options={},context={}){
|
|
3014
|
+
if(destroyed||host.conversationComplete){
|
|
3015
|
+
return false;
|
|
3016
|
+
}
|
|
3017
|
+
if(!isPlainRecord(options)||!isPlainRecord(context)){
|
|
3018
|
+
throw new TypeError('Tool-result options and context must be plain objects.');
|
|
3019
|
+
}
|
|
3020
|
+
const unsupportedOption=Object.keys(options).find(
|
|
3021
|
+
key=>!['disposition','message','persist','request','toolCallId'].includes(key)
|
|
3022
|
+
);
|
|
3023
|
+
if(unsupportedOption){
|
|
3024
|
+
throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
|
|
3025
|
+
}
|
|
3026
|
+
const unsupportedContext=Object.keys(context).find(
|
|
3027
|
+
key=>!['operationId','signal'].includes(key)
|
|
3028
|
+
);
|
|
3029
|
+
if(unsupportedContext){
|
|
3030
|
+
throw new TypeError(`Unsupported tool-result context: ${unsupportedContext}.`);
|
|
3031
|
+
}
|
|
3032
|
+
if(!boundChatSession||sessionBindingPending||sessionMessagePending){
|
|
3033
|
+
return false;
|
|
3034
|
+
}
|
|
3035
|
+
if(!host.aiAvailability.llm){
|
|
3036
|
+
return false;
|
|
3037
|
+
}
|
|
3038
|
+
if(!pendingStructuralToolCall){
|
|
3039
|
+
const error=new TypeError('There is no pending structural tool call to settle.');
|
|
3040
|
+
error.code='AI_CHAT_TOOL_RESULT_NOT_PENDING';
|
|
3041
|
+
throw error;
|
|
3042
|
+
}
|
|
3043
|
+
const disposition=options.disposition;
|
|
3044
|
+
const dispositions=new Map([
|
|
3045
|
+
['executed','Executed'],
|
|
3046
|
+
['declined','Declined'],
|
|
3047
|
+
['cancelled','Cancelled'],
|
|
3048
|
+
['not-executed','Not executed']
|
|
3049
|
+
]);
|
|
3050
|
+
if(!dispositions.has(disposition)){
|
|
3051
|
+
throw new TypeError(
|
|
3052
|
+
'Tool-result disposition must be executed, declined, cancelled, or not-executed.'
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
if(typeof options.message!=='string'||!options.message.trim()){
|
|
3056
|
+
throw new TypeError('Tool-result message must contain user-facing text.');
|
|
3057
|
+
}
|
|
3058
|
+
const perTurnRequest=options.request??null;
|
|
3059
|
+
if(perTurnRequest!==null&&!isPlainRecord(perTurnRequest)){
|
|
3060
|
+
throw new TypeError('Tool-result request options must be a plain object.');
|
|
3061
|
+
}
|
|
3062
|
+
const managedRequestField=perTurnRequest&&Object.keys(perTurnRequest).find(
|
|
3063
|
+
key=>['messages','onChunk','onResponse','onToolCall','signal','stream'].includes(key)
|
|
3064
|
+
);
|
|
3065
|
+
if(managedRequestField){
|
|
3066
|
+
throw new TypeError(
|
|
3067
|
+
`Tool-result request.${managedRequestField} is managed by the chat session.`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
const toolCallId=options.toolCallId??pendingStructuralToolCall.id;
|
|
3071
|
+
if(typeof toolCallId!=='string'||toolCallId!==pendingStructuralToolCall.id){
|
|
3072
|
+
const error=new TypeError('Tool-result ID does not match the pending structural tool call.');
|
|
3073
|
+
error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
|
|
3074
|
+
throw error;
|
|
3075
|
+
}
|
|
3076
|
+
const persist=options.persist??true;
|
|
3077
|
+
if(typeof persist!=='boolean'){
|
|
3078
|
+
throw new TypeError('Tool-result persist must be boolean.');
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
const text=`${dispositions.get(disposition)} — ${options.message}`;
|
|
3082
|
+
const operationId=typeof context.operationId==='string'&&context.operationId
|
|
3083
|
+
?context.operationId
|
|
3084
|
+
:nextChatOperationId('tool-result');
|
|
3085
|
+
const ownership=createChatSubmissionOwnership(context.signal??null);
|
|
3086
|
+
const eventContext={
|
|
3087
|
+
source:'tool',
|
|
3088
|
+
synthetic:true,
|
|
3089
|
+
operationId,
|
|
3090
|
+
signal:ownership.signal
|
|
3091
|
+
};
|
|
3092
|
+
if(ownership.signal.aborted){
|
|
3093
|
+
ownership.release();
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
try{
|
|
3097
|
+
const visibleMessage=appendTranscriptMessage('tool',text,'Tool');
|
|
3098
|
+
visibleMessage.dataset.operationId=operationId;
|
|
3099
|
+
const result=sendMessageThroughBoundSession(
|
|
3100
|
+
text,
|
|
3101
|
+
eventContext,
|
|
3102
|
+
{
|
|
3103
|
+
content:text,
|
|
3104
|
+
persist,
|
|
3105
|
+
role:'tool',
|
|
3106
|
+
tool_call_id:toolCallId
|
|
3107
|
+
},
|
|
3108
|
+
perTurnRequest
|
|
3109
|
+
);
|
|
3110
|
+
return observeHostSubmission(result,eventContext,ownership);
|
|
3111
|
+
}catch(error){
|
|
3112
|
+
ownership.release();
|
|
3113
|
+
throw error;
|
|
3114
|
+
}
|
|
1902
3115
|
}
|
|
1903
3116
|
|
|
1904
3117
|
function observeHostSubmission(result,context,ownership){
|
|
@@ -1913,6 +3126,9 @@
|
|
|
1913
3126
|
if(destroyed||context.signal.aborted){
|
|
1914
3127
|
return false;
|
|
1915
3128
|
}
|
|
3129
|
+
if(internalStructuralToolFailure(error)){
|
|
3130
|
+
return false;
|
|
3131
|
+
}
|
|
1916
3132
|
dispatchChatEvent(
|
|
1917
3133
|
'chat-send-error',
|
|
1918
3134
|
{error,context:{...context}},
|
|
@@ -1930,7 +3146,7 @@
|
|
|
1930
3146
|
}
|
|
1931
3147
|
}
|
|
1932
3148
|
);
|
|
1933
|
-
console.error('The chat message could not be submitted.');
|
|
3149
|
+
console.error('The chat message could not be submitted.',error);
|
|
1934
3150
|
return false;
|
|
1935
3151
|
}
|
|
1936
3152
|
).finally(
|
|
@@ -1957,12 +3173,15 @@
|
|
|
1957
3173
|
if(!host.aiAvailability.llm){
|
|
1958
3174
|
return false;
|
|
1959
3175
|
}
|
|
1960
|
-
|
|
3176
|
+
if(sessionBindingPending||sessionMessagePending||pendingStructuralToolMessage){
|
|
3177
|
+
return false;
|
|
3178
|
+
}
|
|
3179
|
+
const submissionContext={
|
|
1961
3180
|
source:'user',
|
|
1962
3181
|
preserveDraft:false,
|
|
1963
3182
|
synthetic:false,
|
|
1964
3183
|
...context
|
|
1965
|
-
}
|
|
3184
|
+
};
|
|
1966
3185
|
const text=textOverride.length>0?textOverride:textArea.value;
|
|
1967
3186
|
if(!text.trim()){
|
|
1968
3187
|
return false;
|
|
@@ -1972,13 +3191,11 @@
|
|
|
1972
3191
|
?submissionContext.operationId
|
|
1973
3192
|
:nextChatOperationId('message');
|
|
1974
3193
|
const ownership=createChatSubmissionOwnership(submissionContext.signal??null);
|
|
1975
|
-
const eventContext=
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
}
|
|
1981
|
-
);
|
|
3194
|
+
const eventContext={
|
|
3195
|
+
...submissionContext,
|
|
3196
|
+
operationId,
|
|
3197
|
+
signal:ownership.signal
|
|
3198
|
+
};
|
|
1982
3199
|
if(ownership.signal.aborted){
|
|
1983
3200
|
ownership.release();
|
|
1984
3201
|
return false;
|
|
@@ -1997,7 +3214,8 @@
|
|
|
1997
3214
|
cancelable:true,
|
|
1998
3215
|
operationId,
|
|
1999
3216
|
publicDetail:{
|
|
2000
|
-
|
|
3217
|
+
message:text,
|
|
3218
|
+
context:eventContext,
|
|
2001
3219
|
source:eventContext.source,
|
|
2002
3220
|
synthetic:eventContext.synthetic===true,
|
|
2003
3221
|
reason:chatReasons.messageSubmissionRequested
|
|
@@ -2022,7 +3240,6 @@
|
|
|
2022
3240
|
return false;
|
|
2023
3241
|
}
|
|
2024
3242
|
try{
|
|
2025
|
-
const messageTime = getMilTime();
|
|
2026
3243
|
if(!eventContext.preserveDraft){
|
|
2027
3244
|
textArea.value='';
|
|
2028
3245
|
}
|
|
@@ -2031,32 +3248,28 @@
|
|
|
2031
3248
|
// console.log(forceText,text,1);
|
|
2032
3249
|
if(!eventContext.synthetic&&!eventContext.reuseVisibleMessage){
|
|
2033
3250
|
hostSubmissionGeneration++;
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
<br>
|
|
2039
|
-
<div class='markdown'>
|
|
2040
|
-
${new MD(text).rendered}
|
|
2041
|
-
</div>
|
|
2042
|
-
</li>
|
|
2043
|
-
`;
|
|
2044
|
-
}
|
|
2045
|
-
chatOutput.scrollTop = chatOutput.scrollHeight;
|
|
3251
|
+
const visibleMessage=appendTranscriptMessage('user',text,host.name);
|
|
3252
|
+
visibleMessage.dataset.operationId=operationId;
|
|
3253
|
+
}
|
|
3254
|
+
scrollTranscriptToBottom();
|
|
2046
3255
|
|
|
2047
3256
|
if(host.aiAvailability.tts&&speech.muted===false){
|
|
2048
|
-
const aiRuntime=globalThis.ai;
|
|
3257
|
+
const aiRuntime=boundChatAI??globalThis.ai;
|
|
2049
3258
|
aiRuntime?.stopAudio?.();
|
|
2050
3259
|
if(typeof aiRuntime?.resumeAudio==='function'){
|
|
2051
3260
|
void Promise.resolve(aiRuntime.resumeAudio()).catch(
|
|
2052
|
-
|
|
3261
|
+
function reportPlaybackResumeFailure(error){
|
|
3262
|
+
reportTTSError(error,'playback-resume');
|
|
3263
|
+
}
|
|
2053
3264
|
);
|
|
2054
3265
|
}
|
|
2055
3266
|
}
|
|
2056
3267
|
|
|
2057
3268
|
let result;
|
|
2058
3269
|
try{
|
|
2059
|
-
result=
|
|
3270
|
+
result=boundChatSession
|
|
3271
|
+
?sendMessageThroughBoundSession(text,eventContext)
|
|
3272
|
+
:host.sendMessage(text,eventContext);
|
|
2060
3273
|
}catch(error){
|
|
2061
3274
|
result=Promise.reject(error);
|
|
2062
3275
|
}
|
|
@@ -2068,45 +3281,46 @@
|
|
|
2068
3281
|
}
|
|
2069
3282
|
|
|
2070
3283
|
async function receivedMessage(name = 'Assistant', text = '', id=''){
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
</div>
|
|
2081
|
-
</li>
|
|
2082
|
-
`;
|
|
2083
|
-
chatOutput.scrollTop = chatOutput.scrollHeight;
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
function reportTTSError(error){
|
|
3284
|
+
const message=appendTranscriptMessage('assistant',text,name);
|
|
3285
|
+
if(id){
|
|
3286
|
+
message.id=`message-${id}`;
|
|
3287
|
+
}
|
|
3288
|
+
scrollTranscriptToBottom();
|
|
3289
|
+
return message;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
function reportTTSError(error,boundary='synthesis'){
|
|
2087
3293
|
if(destroyed){
|
|
2088
3294
|
return false;
|
|
2089
3295
|
}
|
|
2090
3296
|
if(typeof speech.reportTTSError==='function'){
|
|
2091
3297
|
try{
|
|
2092
|
-
speech.reportTTSError(error);
|
|
3298
|
+
speech.reportTTSError(error,boundary);
|
|
2093
3299
|
return true;
|
|
2094
3300
|
}catch(reportingError){
|
|
2095
3301
|
error=reportingError;
|
|
2096
3302
|
}
|
|
2097
3303
|
}
|
|
2098
3304
|
|
|
2099
|
-
const message=
|
|
2100
|
-
|
|
2101
|
-
|
|
3305
|
+
const message=visibleErrorMessage(
|
|
3306
|
+
error,
|
|
3307
|
+
boundary==='decode'
|
|
3308
|
+
?'Speech audio could not be decoded.'
|
|
3309
|
+
:boundary==='playback-start'
|
|
3310
|
+
?'Speech playback could not start.'
|
|
3311
|
+
:boundary==='playback-resume'
|
|
3312
|
+
?'Speech playback could not resume.'
|
|
3313
|
+
:'Speech synthesis failed.'
|
|
3314
|
+
);
|
|
2102
3315
|
dispatchChatEvent(
|
|
2103
3316
|
'chat-speech-synthesis-error',
|
|
2104
|
-
{error,message},
|
|
3317
|
+
{boundary,error,message},
|
|
2105
3318
|
{
|
|
2106
3319
|
bubbles:true,
|
|
2107
3320
|
composed:true,
|
|
2108
3321
|
operationId:nextChatOperationId('speech-synthesis'),
|
|
2109
3322
|
publicDetail:{
|
|
3323
|
+
boundary,
|
|
2110
3324
|
reason:chatReasons.speechSynthesisRejected,
|
|
2111
3325
|
...publicErrorFields(
|
|
2112
3326
|
error,
|
|
@@ -2115,14 +3329,16 @@
|
|
|
2115
3329
|
}
|
|
2116
3330
|
}
|
|
2117
3331
|
);
|
|
2118
|
-
console.error('Speech synthesis failed.');
|
|
3332
|
+
console.error('Speech synthesis failed.',error);
|
|
2119
3333
|
return false;
|
|
2120
3334
|
}
|
|
2121
3335
|
|
|
2122
3336
|
function destroy(){
|
|
2123
3337
|
if(destroyed){
|
|
2124
|
-
return;
|
|
3338
|
+
return false;
|
|
2125
3339
|
}
|
|
3340
|
+
sessionBindingGeneration+=1;
|
|
3341
|
+
setSessionStatus('destroyed','Chat closed.');
|
|
2126
3342
|
destroyed=true;
|
|
2127
3343
|
const destroyReason=createChatSubmissionAbort(
|
|
2128
3344
|
chatReasons.componentDestroyed,
|
|
@@ -2138,9 +3354,17 @@
|
|
|
2138
3354
|
conversationTimeboxUnsubscribe=null;
|
|
2139
3355
|
conversationTimeboxController=null;
|
|
2140
3356
|
initialSpeechMuteListenerInstalled=false;
|
|
3357
|
+
sessionBindingPending=false;
|
|
3358
|
+
sessionMessagePending=false;
|
|
3359
|
+
activeSessionMessageToken=null;
|
|
3360
|
+
setPendingStructuralToolCall();
|
|
3361
|
+
boundChatSession=null;
|
|
3362
|
+
boundChatAI=null;
|
|
3363
|
+
host.session=null;
|
|
2141
3364
|
speech.destroy?.();
|
|
2142
3365
|
events.dispose();
|
|
2143
3366
|
host.ready=false;
|
|
3367
|
+
return true;
|
|
2144
3368
|
}
|
|
2145
3369
|
|
|
2146
3370
|
// Initialize and adjust on input
|