@wix/himalaya-cli 0.1305.0 → 0.1307.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/wix-gateway.rules.d.ts +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/genie-chat/src/index.d.ts +63 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-genie-chat.mjs +81 -0
- package/dist/cli.mjs +185 -27
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +36 -0
- package/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +15 -27
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/community-club-owner/dev/index.ts +15 -27
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/community-club-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/events-meetups-owner/dev/index.ts +15 -27
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/events-meetups-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/gym-fitness-owner/dev/index.ts +15 -27
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/nonprofit-cause-owner/dev/index.ts +15 -27
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/owner-lite/dev/index.ts +15 -27
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/owner-lite/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/podcast-creator-owner/dev/index.ts +15 -27
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/portfolio-creative-owner/dev/index.ts +15 -27
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/restaurant-owner/dev/index.ts +15 -27
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/restaurant-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/retail-store-owner/dev/index.ts +15 -27
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/retail-store-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
- package/templates/yoga-studio-owner/dev/index.ts +15 -27
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +293 -12
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-stream.ts +165 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +26 -2
|
@@ -34,6 +34,8 @@ import { alert } from "@himalaya/ui";
|
|
|
34
34
|
import {
|
|
35
35
|
activeSiteId,
|
|
36
36
|
createAriaChat,
|
|
37
|
+
authoredRealtimeUrl,
|
|
38
|
+
stashRealtimeUrl,
|
|
37
39
|
type AriaActionArg,
|
|
38
40
|
type AriaChatState,
|
|
39
41
|
type AriaDictationResult,
|
|
@@ -53,6 +55,10 @@ const SERVICE = "himiOwnerLiteAria";
|
|
|
53
55
|
// clone's blocking loop, which paints nothing for the length of a turn.
|
|
54
56
|
const FAST_MS = 2000;
|
|
55
57
|
const IDLE_MS = 15000;
|
|
58
|
+
// While the realtime stream is delivering a reply, the transcript is only read to
|
|
59
|
+
// CONFIRM the turn ended — the words come from the stream — so the ladder runs at
|
|
60
|
+
// this slower rate. A whole interval with no frame drops it back to FAST_MS.
|
|
61
|
+
const STREAM_MS = 6000;
|
|
56
62
|
|
|
57
63
|
function prompts(): AriaPromptChip[] {
|
|
58
64
|
return [
|
|
@@ -250,7 +256,13 @@ const core = createAriaChat({
|
|
|
250
256
|
const screenId = screenForUrl(url, allowedTargets());
|
|
251
257
|
return screenId ? { screenId } : null;
|
|
252
258
|
},
|
|
253
|
-
poll: { action: `${SERVICE}.tick`, fastMs: FAST_MS, idleMs: IDLE_MS },
|
|
259
|
+
poll: { action: `${SERVICE}.tick`, fastMs: FAST_MS, idleMs: IDLE_MS, streamMs: STREAM_MS },
|
|
260
|
+
// The realtime stream: the reply's words as they are written, plus the quick
|
|
261
|
+
// replies that follow it. Only where the screen was built with a stream url
|
|
262
|
+
// (authored onAppear load params); everywhere else — and wherever the host cannot
|
|
263
|
+
// open it — the turn is polled exactly as before. The screen must declare
|
|
264
|
+
// "stream" for the host to open it at all.
|
|
265
|
+
stream: { url: authoredRealtimeUrl },
|
|
254
266
|
// Composer dictation, on the platform recognizer: SFSpeechRecognizer,
|
|
255
267
|
// Android SpeechRecognizer, Web Speech. No audio crosses the bridge — only the
|
|
256
268
|
// final transcript. An empty language means the device's own locale, which is
|
|
@@ -319,6 +331,12 @@ const svc = defineService<AriaChatState, {
|
|
|
319
331
|
finishDictation: (s: AriaChatState) => AriaChatState;
|
|
320
332
|
// `undefined` for a result whose session is no longer the live one — see the core.
|
|
321
333
|
dictated: (s: AriaChatState, arg?: AriaDictationResult) => AriaChatState | undefined;
|
|
334
|
+
// The realtime stream's callbacks, named by the `_stream` directive the core
|
|
335
|
+
// returns from a send — never by the descriptor. `undefined` from
|
|
336
|
+
// `streamEvent` for the same reason `tick` has it.
|
|
337
|
+
streamEvent: (s: AriaChatState, event?: unknown) => Promise<AriaChatState | undefined>;
|
|
338
|
+
streamEnded: (s: AriaChatState) => AriaChatState;
|
|
339
|
+
streamFailed: (s: AriaChatState, err?: unknown) => AriaChatState;
|
|
322
340
|
mentionOpen: (s: AriaChatState) => Promise<AriaChatState | undefined>;
|
|
323
341
|
mentionClose: (s: AriaChatState) => AriaChatState;
|
|
324
342
|
mentionBack: (s: AriaChatState) => AriaChatState;
|
|
@@ -334,7 +352,7 @@ const svc = defineService<AriaChatState, {
|
|
|
334
352
|
state: core.initialState,
|
|
335
353
|
actions: {
|
|
336
354
|
// `load` is the onAppear dispatch, so it is where the copy pack arrives.
|
|
337
|
-
load: (s, arg) => { stashCopy(arg); stashTargets(arg); return core.load(s); },
|
|
355
|
+
load: (s, arg) => { stashCopy(arg); stashTargets(arg); stashRealtimeUrl(arg); return core.load(s); },
|
|
338
356
|
submit: core.submit,
|
|
339
357
|
submitPrompt: core.submitPrompt,
|
|
340
358
|
newChat: core.newChat,
|
|
@@ -391,6 +409,12 @@ const svc = defineService<AriaChatState, {
|
|
|
391
409
|
attachmentSettled: core.attachmentSettled,
|
|
392
410
|
removeAttachment: core.removeAttachment,
|
|
393
411
|
retryAttachment: core.retryAttachment,
|
|
412
|
+
// Dispatched by the HOST as the stream delivers, closes or fails. A name
|
|
413
|
+
// the entry does not export is a callback the host silently drops, and the
|
|
414
|
+
// turn would then be polled with nobody noticing the stream was ignored.
|
|
415
|
+
streamEvent: core.streamEvent,
|
|
416
|
+
streamEnded: core.streamEnded,
|
|
417
|
+
streamFailed: core.streamFailed,
|
|
394
418
|
},
|
|
395
419
|
});
|
|
396
420
|
|
|
@@ -56606,7 +56606,8 @@ const SCREENS = runtimeSetting({
|
|
|
56606
56606
|
"props": {
|
|
56607
56607
|
"text": "$item.content",
|
|
56608
56608
|
"style": "body",
|
|
56609
|
-
"maxLines": 50
|
|
56609
|
+
"maxLines": 50,
|
|
56610
|
+
"animateReveal": true
|
|
56610
56611
|
}
|
|
56611
56612
|
},
|
|
56612
56613
|
"modifiers": {
|
|
@@ -57114,7 +57115,7 @@ const SCREENS = runtimeSetting({
|
|
|
57114
57115
|
}
|
|
57115
57116
|
},
|
|
57116
57117
|
{
|
|
57117
|
-
"id": "
|
|
57118
|
+
"id": "",
|
|
57118
57119
|
"body": {
|
|
57119
57120
|
"package": "himalaya",
|
|
57120
57121
|
"type": "HStack",
|
|
@@ -57130,30 +57131,19 @@ const SCREENS = runtimeSetting({
|
|
|
57130
57131
|
}
|
|
57131
57132
|
},
|
|
57132
57133
|
{
|
|
57133
|
-
"id": "",
|
|
57134
|
-
"body": {
|
|
57135
|
-
"package": "himalaya",
|
|
57136
|
-
"type": "Icon",
|
|
57137
|
-
"props": {
|
|
57138
|
-
"name": "close",
|
|
57139
|
-
"size": 14,
|
|
57140
|
-
"tint": "accentPrimary"
|
|
57141
|
-
}
|
|
57142
|
-
},
|
|
57143
|
-
"modifiers": {
|
|
57144
|
-
"accessibilityRole": "decorative"
|
|
57145
|
-
}
|
|
57146
|
-
},
|
|
57147
|
-
{
|
|
57148
|
-
"id": "",
|
|
57134
|
+
"id": "himi_owner_lite_aria.stop",
|
|
57149
57135
|
"body": {
|
|
57150
57136
|
"package": "himalaya",
|
|
57151
|
-
"type": "
|
|
57137
|
+
"type": "Button",
|
|
57152
57138
|
"props": {
|
|
57153
|
-
"
|
|
57154
|
-
"style": "
|
|
57155
|
-
"
|
|
57156
|
-
"
|
|
57139
|
+
"title": "Stop",
|
|
57140
|
+
"style": "link",
|
|
57141
|
+
"size": "sm",
|
|
57142
|
+
"leadingIcon": "close",
|
|
57143
|
+
"fullWidth": false,
|
|
57144
|
+
"action": {
|
|
57145
|
+
"actionId": "tier5.himiOwnerLiteAria.stop"
|
|
57146
|
+
}
|
|
57157
57147
|
}
|
|
57158
57148
|
}
|
|
57159
57149
|
}
|
|
@@ -57167,9 +57157,6 @@ const SCREENS = runtimeSetting({
|
|
|
57167
57157
|
"top": 4,
|
|
57168
57158
|
"bottom": 0
|
|
57169
57159
|
},
|
|
57170
|
-
"onTap": {
|
|
57171
|
-
"actionId": "tier5.himiOwnerLiteAria.stop"
|
|
57172
|
-
},
|
|
57173
57160
|
"hiddenPath": "$service:himiOwnerLiteAria.stopHidden"
|
|
57174
57161
|
}
|
|
57175
57162
|
},
|
|
@@ -60897,7 +60884,8 @@ const SCREENS = runtimeSetting({
|
|
|
60897
60884
|
"ui",
|
|
60898
60885
|
"speech",
|
|
60899
60886
|
"camera",
|
|
60900
|
-
"presigned_upload"
|
|
60887
|
+
"presigned_upload",
|
|
60888
|
+
"stream"
|
|
60901
60889
|
],
|
|
60902
60890
|
"minBinaryVersion": "0.1.0",
|
|
60903
60891
|
"bundleVersion": "0.0.1"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// This file is copied verbatim into every generated starter app, so it explains
|
|
25
25
|
// itself without pointing at anything only this repo has.
|
|
26
26
|
|
|
27
|
-
import { navigate, type NavDirective, type PollDirective } from "@himalaya/state";
|
|
27
|
+
import { navigate, type NavDirective, type PollDirective, type StreamDirective } from "@himalaya/state";
|
|
28
28
|
import {
|
|
29
29
|
ARIA_ORIGIN,
|
|
30
30
|
abortAssistantMessageGeneration,
|
|
@@ -90,6 +90,7 @@ import { MENTION_PAGE_SIZE, allowlisted, emptyMentionModel, mentionView,
|
|
|
90
90
|
type MentionItemRow, type MentionLabels, type MentionModel, type MentionProvider, type MentionView } from "./aria-mentions.js";
|
|
91
91
|
import { isTransportFailure, ownerTransportFailureText } from "./owner-load-error.js";
|
|
92
92
|
import { LOCAL_ID_PREFIX, createRowMemo, mintLocalId, transcriptKeys, type RowMemo } from "./aria-hot-path.js";
|
|
93
|
+
import { createLiveReply } from "./aria-stream.js";
|
|
93
94
|
import { calendarDay, formatClock, formatDateMedium, formatMonthDay } from "./engine-intl.js";
|
|
94
95
|
|
|
95
96
|
// ---------------------------------------------------------------------------
|
|
@@ -280,13 +281,35 @@ export interface AriaChatState {
|
|
|
280
281
|
attachments: AriaAttachmentChip[];
|
|
281
282
|
/** Collapses the composer's attachment strip while it is empty. */
|
|
282
283
|
attachmentsHidden: boolean;
|
|
283
|
-
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
284
|
+
/** The quick replies the service suggested after the LATEST turn — short
|
|
285
|
+
* answers the owner can send with one tap. They arrive only on the realtime
|
|
286
|
+
* stream, a moment after the reply finishes, so a screen with no stream never
|
|
287
|
+
* has any. A projection like `attachments`: painted on the way out of every
|
|
288
|
+
* action, and empty again the moment a new turn is sent. */
|
|
289
|
+
quickReplies: AriaQuickReply[];
|
|
290
|
+
/** Collapses a quick-reply strip while there is nothing to offer. */
|
|
291
|
+
quickRepliesHidden: boolean;
|
|
292
|
+
/** Where the realtime stream for the current turn is: `off` (no stream was
|
|
293
|
+
* opened — the screen has none configured), `open` (asked for, nothing
|
|
294
|
+
* heard yet), `live` (frames are arriving) or `down` (it closed or failed;
|
|
295
|
+
* the poll is carrying the turn). Diagnostic — nothing binds it. */
|
|
296
|
+
_realtime: AriaRealtime;
|
|
297
|
+
/** Transient host directives — a poll cadence to install, a stream to open,
|
|
298
|
+
* and a navigation to perform. The runtime strips all three before storing
|
|
299
|
+
* the state, so they are never persisted and never re-fire when the state is
|
|
300
|
+
* spread. */
|
|
286
301
|
_poll?: PollDirective;
|
|
302
|
+
_stream?: StreamDirective;
|
|
287
303
|
_nav?: NavDirective;
|
|
288
304
|
}
|
|
289
305
|
|
|
306
|
+
/** One suggested reply, flattened for a `$item.` row: `label` is the chip,
|
|
307
|
+
* `reply` the text a tap sends, `id` a stable row id within the strip. */
|
|
308
|
+
export interface AriaQuickReply { id: string; label: string; reply: string; }
|
|
309
|
+
|
|
310
|
+
/** See `AriaChatState._realtime`. */
|
|
311
|
+
export type AriaRealtime = "off" | "open" | "live" | "down";
|
|
312
|
+
|
|
290
313
|
/** Cadence for the reply ladder. Absent ⇒ the legacy in-action busy loop. */
|
|
291
314
|
export interface AriaPollOptions {
|
|
292
315
|
/** Fully-qualified `"<service>.tick"`. A name that does not resolve to a real
|
|
@@ -300,6 +323,26 @@ export interface AriaPollOptions {
|
|
|
300
323
|
* re-armed slower instead — a visible Aria screen keeps ONE low-rate GET
|
|
301
324
|
* alive until the screen is backgrounded, pooled away or torn down. */
|
|
302
325
|
idleMs: number;
|
|
326
|
+
/** While the realtime stream is DELIVERING the reply. The words come from the
|
|
327
|
+
* stream then, and the transcript is only read to confirm the turn ended, so
|
|
328
|
+
* the ladder can run slower than `fastMs`. A whole interval with no frame is a
|
|
329
|
+
* stalled stream, and the ladder drops back to `fastMs`. Absent ⇒ the ladder
|
|
330
|
+
* stays at `fastMs` throughout. */
|
|
331
|
+
streamMs?: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The realtime stream (the `_stream` directive). Omit it and the
|
|
335
|
+
* conversation machine polls exactly as it always has. */
|
|
336
|
+
export interface AriaStreamOptions {
|
|
337
|
+
/** Base url of an SSE stream that re-emits the chat-service's side-effect
|
|
338
|
+
* frames, one `data:` event per frame (see `decodeSideEffect` in the
|
|
339
|
+
* genie-chat client). Read at SEND time; an empty string means "no stream
|
|
340
|
+
* here", and the turn is polled. The worker appends `conversationId` and, when
|
|
341
|
+
* the service named the turn, `userMessageId`. */
|
|
342
|
+
url: () => string;
|
|
343
|
+
/** Ask the host to attach its Wix credentials to the open request using the platform's
|
|
344
|
+
* `authorize`). Only for a url that should receive them. */
|
|
345
|
+
authorize?: boolean;
|
|
303
346
|
}
|
|
304
347
|
|
|
305
348
|
/** A value or a thunk for it. User-facing copy is read through `copyParam` at
|
|
@@ -506,6 +549,10 @@ export interface AriaChatOptions {
|
|
|
506
549
|
* authored `himi.open` gets — and a miss falls through to `openLink`. */
|
|
507
550
|
resolveEntity?: (url: string) => NavDirective | null;
|
|
508
551
|
poll?: AriaPollOptions;
|
|
552
|
+
/** The realtime stream. Meaningful only with `poll`: the stream paints the
|
|
553
|
+
* reply as it is written, and the poll stays the thing that ends the turn —
|
|
554
|
+
* and the whole of it wherever no stream can be opened. */
|
|
555
|
+
stream?: AriaStreamOptions;
|
|
509
556
|
/** Mirror conversation titles into `himalaya.storage` as an offline fallback
|
|
510
557
|
* for the history screen. Default true (the clone's behaviour).
|
|
511
558
|
*
|
|
@@ -619,8 +666,7 @@ const PLAIN_HOST = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])$/;
|
|
|
619
666
|
* treats `\` exactly like `/`. Without that, `https://evil.example\@wix.com/`
|
|
620
667
|
* reads as `wix.com` here while every browser opens `evil.example` with the
|
|
621
668
|
* path `/@wix.com/`. That is exactly the lie this disclosure exists to
|
|
622
|
-
* prevent, so the
|
|
623
|
-
* RFC 3986's.
|
|
669
|
+
* prevent, so the separators have to match what browsers use when parsing web addresses.
|
|
624
670
|
*
|
|
625
671
|
* A string that survives parsing but is not a plain hostname returns "", which
|
|
626
672
|
* callers must treat as untrusted rather than as "there is no host here". */
|
|
@@ -1237,6 +1283,16 @@ export function inputConversationId(): string {
|
|
|
1237
1283
|
return (input?.conversationId ?? input?.id ?? "").trim();
|
|
1238
1284
|
}
|
|
1239
1285
|
|
|
1286
|
+
// Authored onAppear parameters supply the endpoint. Navigation input is untrusted:
|
|
1287
|
+
// a deep link must never choose where the assistant sends turn identifiers.
|
|
1288
|
+
let realtimeUrl = "";
|
|
1289
|
+
export function stashRealtimeUrl(arg: unknown): void {
|
|
1290
|
+
const v = arg && typeof arg === "object" ? (arg as Record<string, unknown>).ariaRealtimeUrl : undefined;
|
|
1291
|
+
const url = typeof v === "string" ? v.trim() : "";
|
|
1292
|
+
realtimeUrl = /^https?:\/\/[^\s]+\/v1\/aria\/realtime$/i.test(url) ? url : "";
|
|
1293
|
+
}
|
|
1294
|
+
export function authoredRealtimeUrl(): string { return realtimeUrl; }
|
|
1295
|
+
|
|
1240
1296
|
// ---------------------------------------------------------------------------
|
|
1241
1297
|
// The chat core
|
|
1242
1298
|
// ---------------------------------------------------------------------------
|
|
@@ -1362,9 +1418,24 @@ export interface AriaChatCore {
|
|
|
1362
1418
|
removeAttachment(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
|
|
1363
1419
|
/** Re-send a failed chip's bytes from the copy it already holds. */
|
|
1364
1420
|
retryAttachment(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
|
|
1421
|
+
/** One event of the realtime stream, dispatched by the HOST (the `_stream`
|
|
1422
|
+
* directive's `onEvent`). Never named by a descriptor. Resolves to
|
|
1423
|
+
* `undefined` — leave the state untouched — when the transcript read a
|
|
1424
|
+
* completion triggers went stale across a turn boundary, on `tick`'s terms. */
|
|
1425
|
+
streamEvent(s: AriaChatState, event?: unknown): Promise<AriaChatState | undefined>;
|
|
1426
|
+
/** The stream closed (`onDone`) or failed (`onError`). Either way the poll
|
|
1427
|
+
* carries the rest of the turn, at the full rate. */
|
|
1428
|
+
streamEnded(s: AriaChatState): AriaChatState;
|
|
1429
|
+
streamFailed(s: AriaChatState, err?: unknown): AriaChatState;
|
|
1365
1430
|
}
|
|
1366
1431
|
|
|
1367
1432
|
const POLL_ID = "aria-turn";
|
|
1433
|
+
/** The one realtime stream a screen holds. A new turn opens it again under the
|
|
1434
|
+
* SAME id, which is what closes the previous turn's: the host supersedes a
|
|
1435
|
+
* stream by id, and the worker has no other way to close one. */
|
|
1436
|
+
const STREAM_ID = "aria-realtime";
|
|
1437
|
+
/** Prefix of the id of a reply bubble that so far exists only on the stream. */
|
|
1438
|
+
const LIVE_ID_PREFIX = "live-";
|
|
1368
1439
|
/** What Regenerate sends. Ordinary user text, matching the native Aria app —
|
|
1369
1440
|
* the chat-service has no retry verb, and inventing one here would be a
|
|
1370
1441
|
* protocol we then had to keep. */
|
|
@@ -1549,6 +1620,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1549
1620
|
listening: false, micHidden: !o.dictation, listeningHidden: true,
|
|
1550
1621
|
widget: NO_WIDGET, mention: mentionViewNow,
|
|
1551
1622
|
attachments: [], attachmentsHidden: true,
|
|
1623
|
+
quickReplies: [], quickRepliesHidden: true, _realtime: "off",
|
|
1552
1624
|
};
|
|
1553
1625
|
|
|
1554
1626
|
/** The composer's photos — the ONE copy. See `AriaChatState.attachments` for
|
|
@@ -2187,6 +2259,111 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2187
2259
|
return { ...s, _poll: { id: POLL_ID, intervalMs: next, action: o.poll.action, immediate: false } };
|
|
2188
2260
|
};
|
|
2189
2261
|
|
|
2262
|
+
// -------------------------------------------------------------------------
|
|
2263
|
+
// The realtime stream
|
|
2264
|
+
// -------------------------------------------------------------------------
|
|
2265
|
+
//
|
|
2266
|
+
// The stream paints the reply while it is being written; the poll confirms it
|
|
2267
|
+
// and ends the turn. Both views of the reply are kept, and which one is shown
|
|
2268
|
+
// is decided on the way out of every action (`withLive`, from `gated`), not by
|
|
2269
|
+
// the reducer that happened to run: a tick that read its snapshot before a
|
|
2270
|
+
// chunk arrived would otherwise paint the shorter text back over the longer.
|
|
2271
|
+
// That is the same latch-and-paint pattern the mention picker and the
|
|
2272
|
+
// attachment chips use, for the same reason.
|
|
2273
|
+
|
|
2274
|
+
/** What the stream has said about the current turn. Not persisted: a worker
|
|
2275
|
+
* that is torn down mid-turn comes back to the poll alone. */
|
|
2276
|
+
const live = createLiveReply();
|
|
2277
|
+
/** See `AriaChatState._realtime`. A latch, painted by `gated`. */
|
|
2278
|
+
let realtime: AriaRealtime = "off";
|
|
2279
|
+
/** The ladder is running at `poll.streamMs` because the stream is delivering.
|
|
2280
|
+
* Cleared when the stream stalls, fails or closes, or the turn ends. */
|
|
2281
|
+
let slowed = false;
|
|
2282
|
+
/** The live bubble last built, so a stream delivering at 20-30 frames a second
|
|
2283
|
+
* rebuilds it only when its text actually changed — the same rule the
|
|
2284
|
+
* transcript memo follows (`aria-hot-path.ts` rule 1). */
|
|
2285
|
+
let liveBubble: { key: string; bubble: AriaBubble | null } | null = null;
|
|
2286
|
+
/** The quick replies last painted, so an unchanged list keeps its identity and
|
|
2287
|
+
* a reducer that changed nothing still returns the SAME state object. */
|
|
2288
|
+
let paintedReplies: AriaQuickReply[] = [];
|
|
2289
|
+
|
|
2290
|
+
/** Ask the host to open the stream for a turn that was just sent, and reset
|
|
2291
|
+
* the live model to it. `turnId` may be empty (a service that named no id);
|
|
2292
|
+
* every frame of the conversation is then taken as this turn's. */
|
|
2293
|
+
const openRealtime = (s: AriaChatState, convId: string, turnId: string): AriaChatState => {
|
|
2294
|
+
live.reset(convId, turnId);
|
|
2295
|
+
slowed = false;
|
|
2296
|
+
liveBubble = null;
|
|
2297
|
+
const base = (o.stream?.url() ?? "").trim();
|
|
2298
|
+
if (!o.stream || !o.poll || !base || !convId) {
|
|
2299
|
+
realtime = "off";
|
|
2300
|
+
return s;
|
|
2301
|
+
}
|
|
2302
|
+
realtime = "open";
|
|
2303
|
+
const query = `conversationId=${encodeURIComponent(convId)}${turnId ? `&userMessageId=${encodeURIComponent(turnId)}` : ""}`;
|
|
2304
|
+
const request: StreamDirective["request"] & { authorize?: boolean } = {
|
|
2305
|
+
method: "GET",
|
|
2306
|
+
url: `${base}${base.includes("?") ? "&" : "?"}${query}`,
|
|
2307
|
+
headers: { Accept: "text/event-stream" },
|
|
2308
|
+
...(o.stream.authorize ? { authorize: true } : {}),
|
|
2309
|
+
};
|
|
2310
|
+
return { ...s, _stream: { id: STREAM_ID, request, onEvent: "streamEvent", onDone: "streamEnded", onError: "streamFailed" } };
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
/** Does the live model describe the turn `s` is showing? */
|
|
2314
|
+
const liveIsFor = (s: AriaChatState): boolean =>
|
|
2315
|
+
realtime !== "off" && !!s._convId && live.conversationId === s._convId && live.turnId === s._turnId;
|
|
2316
|
+
|
|
2317
|
+
/** The reply as the stream has it, painted over the transcript's view of it.
|
|
2318
|
+
*
|
|
2319
|
+
* Only while the turn is in flight: once the transcript has the finished
|
|
2320
|
+
* answer the turn ends and the transcript is shown as it is. The live text
|
|
2321
|
+
* replaces every reply row after the turn's own message — the transcript's
|
|
2322
|
+
* draft of the same reply — but only when it is at least as long, so a
|
|
2323
|
+
* stream that joined late never paints LESS than the poll already showed.
|
|
2324
|
+
* The bubble takes over the draft row's id when there is one, so the row
|
|
2325
|
+
* keeps its identity from the first word to the confirmed answer. */
|
|
2326
|
+
const withLive = (s: AriaChatState): AriaChatState => {
|
|
2327
|
+
if (s.status !== "sending" || !liveIsFor(s)) return s;
|
|
2328
|
+
let out = s;
|
|
2329
|
+
const settledNow = live.settled();
|
|
2330
|
+
const text = live.text();
|
|
2331
|
+
let at = -1;
|
|
2332
|
+
for (let i = s.messages.length - 1; i >= 0; i--) if (s.messages[i].role === "USER") { at = i; break; }
|
|
2333
|
+
if (text && at >= 0) {
|
|
2334
|
+
const tail = s.messages.slice(at + 1);
|
|
2335
|
+
let shown = 0;
|
|
2336
|
+
for (const b of tail) shown += b.content.length;
|
|
2337
|
+
const id = tail[0]?.id ?? `${LIVE_ID_PREFIX}${s._turnId || "turn"}`;
|
|
2338
|
+
const key = `${id}\u0000${settledNow ? "" : "w"}\u0000${text}`;
|
|
2339
|
+
if (!liveBubble || liveBubble.key !== key) {
|
|
2340
|
+
liveBubble = { key, bubble: bubbleOf(id, "ASSISTANT", assistant(), text, !settledNow) };
|
|
2341
|
+
}
|
|
2342
|
+
const b = liveBubble.bubble;
|
|
2343
|
+
if (b && b.content.length >= shown && !(tail.length === 1 && tail[0] === b)) {
|
|
2344
|
+
out = { ...out, messages: [...s.messages.slice(0, at + 1), b] };
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
// The model has finished writing; the server is still closing the turn.
|
|
2348
|
+
// "Typing…" under a finished reply would be a lie, so the line clears — but
|
|
2349
|
+
// only that line: a notice the owner still has to act on stays.
|
|
2350
|
+
if (settledNow && out.statusText && out.statusText === typingText()) out = { ...out, statusText: "" };
|
|
2351
|
+
// Another decision can start before the transcript closes the same turn.
|
|
2352
|
+
// Restore typing only when the line is free; keep actionable owner notices.
|
|
2353
|
+
if (!settledNow && !out.statusText) out = { ...out, statusText: typingText() };
|
|
2354
|
+
return out;
|
|
2355
|
+
};
|
|
2356
|
+
|
|
2357
|
+
/** The current turn's quick replies, as rows. */
|
|
2358
|
+
const repliesFor = (s: AriaChatState): AriaQuickReply[] => {
|
|
2359
|
+
const offered = !!s._turnId && live.turnId === s._turnId && live.conversationId === s._convId
|
|
2360
|
+
? live.quickReplies() : [];
|
|
2361
|
+
const same = offered.length === paintedReplies.length
|
|
2362
|
+
&& offered.every((r, i) => paintedReplies[i].label === r.label && paintedReplies[i].reply === r.reply);
|
|
2363
|
+
if (!same) paintedReplies = offered.map((r, i) => ({ id: `qr${i + 1}`, label: r.label, reply: r.reply }));
|
|
2364
|
+
return paintedReplies;
|
|
2365
|
+
};
|
|
2366
|
+
|
|
2190
2367
|
/** Ensure a conversation exists for the ACTIVE site, creating one when there
|
|
2191
2368
|
* is none or when the site changed under us. */
|
|
2192
2369
|
async function ensureConversation(s: AriaChatState): Promise<{ convId: string; site: string; created: boolean }> {
|
|
@@ -2264,6 +2441,11 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2264
2441
|
// this one. Bump before the first await so an in-flight tick is already stale
|
|
2265
2442
|
// by the time it resolves.
|
|
2266
2443
|
turnSeq += 1;
|
|
2444
|
+
// …and the previous turn's live reply and quick replies are over too. The
|
|
2445
|
+
// new turn's stream, if any, is asked for once the send has named the turn.
|
|
2446
|
+
live.reset("", "");
|
|
2447
|
+
realtime = "off";
|
|
2448
|
+
slowed = false;
|
|
2267
2449
|
// The picks above the composer ride WITH this turn, as references beside
|
|
2268
2450
|
// the text — never as tokens inside it. Read once, here: a chip the owner
|
|
2269
2451
|
// adds while this send is in the air belongs to the NEXT turn, and only the
|
|
@@ -2494,8 +2676,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2494
2676
|
if (o.poll) {
|
|
2495
2677
|
// Return NOW. The host owns the clock from here; `tick` merges the
|
|
2496
2678
|
// reply in when it lands.
|
|
2679
|
+
// …and, where a realtime stream is configured, ask for it too: the
|
|
2680
|
+
// reply's words then arrive as they are written instead of on the
|
|
2681
|
+
// next tick. The ladder is armed either way — the stream paints, the
|
|
2682
|
+
// poll confirms, and wherever the stream cannot open, the poll is all
|
|
2683
|
+
// there is.
|
|
2497
2684
|
const armed: AriaChatState = { ...sent, status: "sending", _convId: convId, _siteId: site };
|
|
2498
|
-
return cadence(armed, o.poll.fastMs, null);
|
|
2685
|
+
return cadence(openRealtime(armed, convId, abortId), o.poll.fastMs, null);
|
|
2499
2686
|
}
|
|
2500
2687
|
return busyLoop({ ...sent, _convId: convId, _siteId: site }, convId, optimistic);
|
|
2501
2688
|
} catch (e) {
|
|
@@ -2593,9 +2780,14 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2593
2780
|
const { _stream: _a, _nav: _b, _poll: _c, _error: _d, ...rest } = r;
|
|
2594
2781
|
return rest as AriaChatState;
|
|
2595
2782
|
};
|
|
2596
|
-
const gated = (
|
|
2783
|
+
const gated = (input: AriaChatState): AriaChatState => {
|
|
2597
2784
|
mentionSite();
|
|
2598
|
-
|
|
2785
|
+
// The live reply first, so every rule below sees what will be painted.
|
|
2786
|
+
const s = withLive(input);
|
|
2787
|
+
// Stop goes once the stream says the model has finished writing: there is
|
|
2788
|
+
// nothing left for an abort to save, and the turn only waits on the server
|
|
2789
|
+
// to close it.
|
|
2790
|
+
const hidden = !(turnInFlight(s) && !!s._abortId) || (liveIsFor(s) && live.settled());
|
|
2599
2791
|
// The reply actions follow the same rule for the same reason: a derived
|
|
2600
2792
|
// flag, recomputed on every action's way out, so no reducer can forget it.
|
|
2601
2793
|
// `_pending` rather than `turnInFlight` — see `regenerate` for why `status`
|
|
@@ -2624,6 +2816,11 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2624
2816
|
? actioned
|
|
2625
2817
|
: { ...actioned, attachments: chips, attachmentsHidden };
|
|
2626
2818
|
if (out.mention !== mentionViewNow) out = { ...out, mention: mentionViewNow };
|
|
2819
|
+
const quickReplies = repliesFor(out);
|
|
2820
|
+
if (out.quickReplies !== quickReplies || out.quickRepliesHidden !== (quickReplies.length === 0)) {
|
|
2821
|
+
out = { ...out, quickReplies, quickRepliesHidden: quickReplies.length === 0 };
|
|
2822
|
+
}
|
|
2823
|
+
if (out._realtime !== realtime) out = { ...out, _realtime: realtime };
|
|
2627
2824
|
// Every state the runtime commits passes through here (an `undefined`
|
|
2628
2825
|
// result commits nothing and skips it), so this is the newest committed
|
|
2629
2826
|
// state — what `copyMessage` puts its note on when other actions landed
|
|
@@ -2907,6 +3104,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2907
3104
|
// A fresh conversation is a turn boundary too: a tick parked on the OLD
|
|
2908
3105
|
// conversation's transcript must not reconcile it over the new greeting.
|
|
2909
3106
|
turnSeq += 1;
|
|
3107
|
+
// The old conversation's live reply and quick replies go with it. Its
|
|
3108
|
+
// stream stays open until the next turn supersedes it (a worker cannot
|
|
3109
|
+
// close one), and every frame it still delivers is for a conversation
|
|
3110
|
+
// the model no longer folds.
|
|
3111
|
+
live.reset("", "");
|
|
3112
|
+
realtime = "off";
|
|
3113
|
+
slowed = false;
|
|
2910
3114
|
// A fresh chat drops its drafts, the picks included — the same way it
|
|
2911
3115
|
// empties the composer. The provider list is kept: it belongs to the
|
|
2912
3116
|
// business, not to the conversation.
|
|
@@ -2985,7 +3189,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2985
3189
|
// is on the RAW transcript — bailing on the bubbles is what let such a
|
|
2986
3190
|
// reply go unnoticed and pinned the ladder fast forever. Safe to return
|
|
2987
3191
|
// `s` here: the guard above proved this tick is still current.
|
|
2988
|
-
if (!raw.length)
|
|
3192
|
+
if (!raw.length) {
|
|
3193
|
+
if (slowed && live.takeActivity() === 0) {
|
|
3194
|
+
slowed = false;
|
|
3195
|
+
return cadence(s, o.poll.fastMs, o.poll.streamMs ?? null);
|
|
3196
|
+
}
|
|
3197
|
+
return s;
|
|
3198
|
+
}
|
|
2989
3199
|
const { messages, pending } = reconcile(server, s._pending, s._pendingAfter, you(), s._pendingId, s._pendingAttach ?? [], s._pendingRefs);
|
|
2990
3200
|
// …but still never PAINT nothing over something. `messages` is only
|
|
2991
3201
|
// adopted when it has content: a transcript that renders empty leaves
|
|
@@ -3015,7 +3225,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3015
3225
|
...observed(raw, s.widget),
|
|
3016
3226
|
prompts: promptsFor(painted, promptList()),
|
|
3017
3227
|
statusText: ended ? "" : s.statusText,
|
|
3018
|
-
...(
|
|
3228
|
+
...(ended ? { _abortId: "" } : {}),
|
|
3229
|
+
...(failed ? toolFailureSlots(failed) : {}),
|
|
3019
3230
|
};
|
|
3020
3231
|
// The async job rides this same tick — see `withJobProgress`. Done
|
|
3021
3232
|
// AFTER the transcript rebuild, so a widget that has just been retired
|
|
@@ -3024,7 +3235,21 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3024
3235
|
const withItems = await withWidgetItems(withJob);
|
|
3025
3236
|
if (turnSeq !== seq || abortingEpochs.has(seq)) return undefined;
|
|
3026
3237
|
// Slow the ladder the moment the turn is over; leave it alone otherwise.
|
|
3027
|
-
|
|
3238
|
+
if (ended && wasFast) {
|
|
3239
|
+
slowed = false;
|
|
3240
|
+
return cadence(withItems, o.poll.idleMs, o.poll.fastMs);
|
|
3241
|
+
}
|
|
3242
|
+
// The ladder runs slow only while the stream proves it is delivering.
|
|
3243
|
+
// A whole slow interval in which no frame changed anything is a stalled
|
|
3244
|
+
// stream — or a finished reply waiting on the server — and either way the
|
|
3245
|
+
// transcript is all there is, so it goes back to being read at the full
|
|
3246
|
+
// rate. No clock needed: this tick IS the interval.
|
|
3247
|
+
if (slowed && live.takeActivity() === 0) {
|
|
3248
|
+
slowed = false;
|
|
3249
|
+
return cadence(withItems, o.poll.fastMs, o.poll.streamMs ?? null);
|
|
3250
|
+
}
|
|
3251
|
+
return withItems;
|
|
3252
|
+
|
|
3028
3253
|
} catch {
|
|
3029
3254
|
// A read that THREW is exactly as stale as one that resolved: if a turn
|
|
3030
3255
|
// boundary moved on while it was in flight, returning `s` merges the
|
|
@@ -3664,8 +3889,61 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3664
3889
|
regenerating = false;
|
|
3665
3890
|
}
|
|
3666
3891
|
},
|
|
3892
|
+
|
|
3893
|
+
// The realtime stream's three callbacks. The host dispatches them one at a
|
|
3894
|
+
// time, in arrival order, through the same queue as every other action.
|
|
3895
|
+
streamEvent: async (s, event) => {
|
|
3896
|
+
const change = live.fold(event);
|
|
3897
|
+
// A frame that changed nothing commits nothing — at 20-30 frames a second
|
|
3898
|
+
// a commit per frame would be a re-render per frame for no reason.
|
|
3899
|
+
if (change === "none") return undefined;
|
|
3900
|
+
// The stream has proved itself for this turn.
|
|
3901
|
+
if (realtime === "open") realtime = "live";
|
|
3902
|
+
let next = s;
|
|
3903
|
+
// Words are arriving, so the transcript only has to CONFIRM the turn now,
|
|
3904
|
+
// and the ladder can slow to the confirmation rate. Once per turn; `tick`
|
|
3905
|
+
// speeds it back up the first time a whole slow interval passes with no
|
|
3906
|
+
// frame (`takeActivity`), which the baseline taken here makes exact.
|
|
3907
|
+
if (o.poll?.streamMs && !slowed && turnInFlight(s) && (change === "text" || change === "completed")) {
|
|
3908
|
+
slowed = true;
|
|
3909
|
+
live.takeActivity();
|
|
3910
|
+
next = cadence(next, o.poll.streamMs, o.poll.fastMs);
|
|
3911
|
+
}
|
|
3912
|
+
// A completed reply, or the service saying the transcript changed, is a
|
|
3913
|
+
// HINT that the turn can be confirmed — never the confirmation. Read the
|
|
3914
|
+
// transcript now rather than at the next tick; only what it says ends the
|
|
3915
|
+
// turn. `tick` holds every guard a read needs (the turn epoch, an abort in
|
|
3916
|
+
// the air), including the `undefined` that leaves the state untouched when
|
|
3917
|
+
// a boundary moved on while the read was out.
|
|
3918
|
+
if ((change === "completed" && live.settled()) || change === "confirm") {
|
|
3919
|
+
if (!turnInFlight(s)) return next;
|
|
3920
|
+
const confirmed = await impl.tick(next);
|
|
3921
|
+
if (!confirmed) return undefined;
|
|
3922
|
+
// A `_poll` the tick did not replace still has to reach the host.
|
|
3923
|
+
return next._poll && !confirmed._poll ? { ...confirmed, _poll: next._poll } : confirmed;
|
|
3924
|
+
}
|
|
3925
|
+
return next;
|
|
3926
|
+
},
|
|
3927
|
+
streamEnded: (s) => {
|
|
3928
|
+
realtime = "down";
|
|
3929
|
+
return restoreLadder(s);
|
|
3930
|
+
},
|
|
3931
|
+
streamFailed: (s) => {
|
|
3932
|
+
realtime = "down";
|
|
3933
|
+
return restoreLadder(s);
|
|
3934
|
+
},
|
|
3667
3935
|
};
|
|
3668
3936
|
|
|
3937
|
+
/** The stream is gone for the rest of this turn, so if the ladder was slowed
|
|
3938
|
+
* for it, it goes back to the full rate. Nothing re-opens the stream until the
|
|
3939
|
+
* next turn: re-asking on every tick would hammer an endpoint that just
|
|
3940
|
+
* refused, and the poll alone already carries a turn to the end. */
|
|
3941
|
+
function restoreLadder(s: AriaChatState): AriaChatState {
|
|
3942
|
+
if (!slowed || !o.poll) return s;
|
|
3943
|
+
slowed = false;
|
|
3944
|
+
return turnInFlight(s) ? cadence(s, o.poll.fastMs, o.poll.streamMs ?? null) : s;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3669
3947
|
// Every action's result goes through the Stop gate on its way out.
|
|
3670
3948
|
return {
|
|
3671
3949
|
initialState,
|
|
@@ -3711,6 +3989,9 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3711
3989
|
attachmentSettled: (s, arg) => gated(impl.attachmentSettled(s, arg)),
|
|
3712
3990
|
removeAttachment: (s, arg) => gated(impl.removeAttachment(s, arg)),
|
|
3713
3991
|
retryAttachment: (s, arg) => gated(impl.retryAttachment(s, arg)),
|
|
3992
|
+
streamEvent: (s, event) => impl.streamEvent(s, event).then((r) => (r ? gated(r) : r)),
|
|
3993
|
+
streamEnded: (s) => gated(impl.streamEnded(s)),
|
|
3994
|
+
streamFailed: (s, err) => gated(impl.streamFailed(s, err)),
|
|
3714
3995
|
};
|
|
3715
3996
|
}
|
|
3716
3997
|
|