@wix/himalaya-cli 0.970.0 → 0.972.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/stdlib/wix-sdk/genie-chat/src/index.d.ts +12 -0
- package/dist/cli.mjs +196 -161
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +7 -1
- package/guides/tier-5-add-a-screen.md +8 -4
- package/package.json +2 -2
- package/templates/beauty-salon-owner/config.ts +1 -1
- package/templates/beauty-salon-owner/dev/index.ts +79 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/community-club-owner/config.ts +1 -1
- package/templates/community-club-owner/dev/index.ts +79 -1
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/events-meetups-owner/config.ts +1 -1
- package/templates/events-meetups-owner/dev/index.ts +79 -1
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/gym-fitness-owner/config.ts +1 -1
- package/templates/gym-fitness-owner/dev/index.ts +79 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/nonprofit-cause-owner/config.ts +1 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +79 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/owner-lite/config.ts +1 -1
- package/templates/owner-lite/dev/index.ts +79 -1
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/podcast-creator-owner/config.ts +1 -1
- package/templates/podcast-creator-owner/dev/index.ts +79 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/portfolio-creative-owner/config.ts +1 -1
- package/templates/portfolio-creative-owner/dev/index.ts +79 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/restaurant-owner/config.ts +1 -1
- package/templates/restaurant-owner/dev/index.ts +79 -1
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/retail-store-owner/config.ts +1 -1
- package/templates/retail-store-owner/dev/index.ts +79 -1
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
- package/templates/yoga-studio-owner/config.ts +1 -1
- package/templates/yoga-studio-owner/dev/index.ts +79 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +238 -11
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +15 -0
|
@@ -89,6 +89,19 @@ export interface AriaChatState {
|
|
|
89
89
|
error: string;
|
|
90
90
|
/** Bound seed for the composer field. */
|
|
91
91
|
input: string;
|
|
92
|
+
/** Transient result of the last Copy: the confirmation, or the failure.
|
|
93
|
+
*
|
|
94
|
+
* It exists because `writeText` resolves or throws and NOTHING else in the
|
|
95
|
+
* state changes either way — so without this the owner cannot tell a copy
|
|
96
|
+
* that took from one the platform refused. Saying "Copied" when nothing
|
|
97
|
+
* reached the clipboard is a known way for a copy control to lie; this is
|
|
98
|
+
* the field that makes the difference observable. */
|
|
99
|
+
copyNote: string;
|
|
100
|
+
/** Collapses the Copy / Regenerate row. Derived, like `stopHidden`: shown
|
|
101
|
+
* only when the newest bubble is a REPLY (not the owner's message, not the
|
|
102
|
+
* local greeting) and no turn is out. A pure function of other fields, so it
|
|
103
|
+
* is computed once in `gated` rather than maintained by every reducer. */
|
|
104
|
+
replyActionsHidden: boolean;
|
|
92
105
|
messages: AriaBubble[];
|
|
93
106
|
/** Non-empty ONLY on the greeting-only empty state. */
|
|
94
107
|
prompts: AriaPromptChip[];
|
|
@@ -199,6 +212,12 @@ export interface AriaChatLabels {
|
|
|
199
212
|
/** Status line after the owner stopped a reply. Sits in the NEUTRAL slot:
|
|
200
213
|
* they asked for it, so nothing went wrong. */
|
|
201
214
|
stopped?: Resolvable<string>;
|
|
215
|
+
/** Shown after a clipboard write that RESOLVED. */
|
|
216
|
+
copied?: Resolvable<string>;
|
|
217
|
+
/** Shown after a clipboard write that threw, or when the host supplied no
|
|
218
|
+
* clipboard at all. Deliberately distinct from `copied`: the point of the
|
|
219
|
+
* pair is that the owner can tell which of the two happened. */
|
|
220
|
+
copyFailed?: Resolvable<string>;
|
|
202
221
|
/** Shown under a picker once the owner has chosen as many options as the
|
|
203
222
|
* service allows. Takes the limit, because "pick up to 3" is the whole of
|
|
204
223
|
* the information. */
|
|
@@ -242,6 +261,13 @@ export interface AriaChatOptions {
|
|
|
242
261
|
/** Opens an external URL. Apps whose descriptors carry their own open-url
|
|
243
262
|
* action (the clone) leave this unset and never invoke `openLink`. */
|
|
244
263
|
openLink?: (url: string) => Promise<boolean>;
|
|
264
|
+
|
|
265
|
+
/** Put `text` on the system clipboard. Injected rather than imported so the
|
|
266
|
+
* shared core stays free of the SDK, exactly as `openLink` is.
|
|
267
|
+
*
|
|
268
|
+
* It RESOLVES or THROWS — there is no boolean. A host that does not supply
|
|
269
|
+
* it has no clipboard, and `copyMessage` says so rather than pretending. */
|
|
270
|
+
copyText?: (text: string) => Promise<void>;
|
|
245
271
|
/** Maps an Aria action-link URL to an in-app screen. A hit navigates via a
|
|
246
272
|
* `_nav` directive — the host applies the same linkable-target check an
|
|
247
273
|
* authored `himi.open` gets — and a miss falls through to `openLink`. */
|
|
@@ -559,7 +585,10 @@ export function toBubbles(msgs: ConversationMessage[], assistantName: string, yo
|
|
|
559
585
|
// way, and an aborted turn consists of nothing else. Then envelopes, then
|
|
560
586
|
// the markdown stripper — each stage handing on only what a reader should
|
|
561
587
|
// actually see.
|
|
562
|
-
|
|
588
|
+
// A row the service is still writing is scanned as STREAMING: an
|
|
589
|
+
// envelope whose `>>` has not arrived yet is held back rather than
|
|
590
|
+
// painted as raw JSON until the row finishes.
|
|
591
|
+
const prose = role === "USER" ? raw : proseOfParts(scanEnvelopes(stripHidden(raw), 0, !stillWriting(m)).parts);
|
|
563
592
|
return {
|
|
564
593
|
id: m.id || `m${i}`,
|
|
565
594
|
role,
|
|
@@ -573,6 +602,16 @@ export function toBubbles(msgs: ConversationMessage[], assistantName: string, yo
|
|
|
573
602
|
.filter((b) => b.content && b.content.trim());
|
|
574
603
|
}
|
|
575
604
|
|
|
605
|
+
/** The reply the Copy / Regenerate row acts on: the LAST bubble, when it is an
|
|
606
|
+
* answer. Not "the newest assistant bubble": a widget-only answer leaves no
|
|
607
|
+
* bubble, so the transcript then ends on the owner's question, and searching
|
|
608
|
+
* back past it would offer the previous turn's answer as if it were this one.
|
|
609
|
+
* The greeting is excluded because it is a local bubble, not a reply. */
|
|
610
|
+
function lastReply(s: { messages: AriaBubble[] }): AriaBubble | undefined {
|
|
611
|
+
const last = s.messages[s.messages.length - 1];
|
|
612
|
+
return last && last.notUser && last.id !== GREETING_ID ? last : undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
576
615
|
/** Chips are visible only before the owner has said anything. */
|
|
577
616
|
function promptsFor(msgs: AriaBubble[], prompts: AriaPromptChip[]): AriaPromptChip[] {
|
|
578
617
|
return msgs.some((b) => b.role === "USER") ? [] : prompts;
|
|
@@ -653,6 +692,29 @@ function answerText(m: ConversationMessage): string {
|
|
|
653
692
|
return stripHidden(messageText(m)).trim();
|
|
654
693
|
}
|
|
655
694
|
|
|
695
|
+
/** Is the service still WRITING this assistant row?
|
|
696
|
+
*
|
|
697
|
+
* The chat-service stamps every message with a status, and an ASSISTANT row
|
|
698
|
+
* that is `STATUS_PENDING` holds a draft: the agent has started the reply and
|
|
699
|
+
* not finished it. Whatever text it carries is worth PAINTING — it is the only
|
|
700
|
+
* view of a reply in progress a worker has, since the words themselves stream
|
|
701
|
+
* over a socket no host exposes (see the genie-chat client's header) — but it
|
|
702
|
+
* is not the answer, and ending the turn on it is the bug this exists for: the
|
|
703
|
+
* typing line clears, Stop disappears while the agent is still writing, and the
|
|
704
|
+
* ladder drops to its idle rung, so the rest of the reply sits unseen for a
|
|
705
|
+
* whole idle interval. The native Aria app ends a turn only on
|
|
706
|
+
* `STATUS_COMPLETED`, for the same reason.
|
|
707
|
+
*
|
|
708
|
+
* Only an explicit PENDING counts. An absent or unrecognised status is
|
|
709
|
+
* "settled": no capture in this repo has ever shown the field, and a gate that
|
|
710
|
+
* demanded `STATUS_COMPLETED` would hang every turn on a service that simply
|
|
711
|
+
* does not send it. Callers apply it to ASSISTANT rows only: a USER row is
|
|
712
|
+
* counted to retire the optimistic bubble, and holding one back would paint
|
|
713
|
+
* the owner's words twice. */
|
|
714
|
+
function stillWriting(m: ConversationMessage): boolean {
|
|
715
|
+
return m.status === "STATUS_PENDING";
|
|
716
|
+
}
|
|
717
|
+
|
|
656
718
|
export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean | null {
|
|
657
719
|
if (!turnId) return null;
|
|
658
720
|
const at = msgs.findIndex((m) => m.id === turnId);
|
|
@@ -665,7 +727,9 @@ export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean
|
|
|
665
727
|
for (let i = at + 1; i < msgs.length; i++) {
|
|
666
728
|
const m = msgs[i];
|
|
667
729
|
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
668
|
-
|
|
730
|
+
// A row still being written is not the answer yet — keep looking, since
|
|
731
|
+
// the finished answer can land in a later row of the same turn.
|
|
732
|
+
if (answerText(m) && !stillWriting(m)) return true;
|
|
669
733
|
}
|
|
670
734
|
return false;
|
|
671
735
|
}
|
|
@@ -700,6 +764,26 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
700
764
|
return n;
|
|
701
765
|
}
|
|
702
766
|
|
|
767
|
+
/** How many FINISHED assistant answers the transcript holds — `countTurns`
|
|
768
|
+
* minus the rows the service is still writing (`stillWriting`).
|
|
769
|
+
*
|
|
770
|
+
* The counted path compares THIS against a mark taken with `countTurns`, and
|
|
771
|
+
* the asymmetry is the point. The mark includes drafts, so a follow-up sent
|
|
772
|
+
* while an earlier reply is still being written is marked AFTER that reply;
|
|
773
|
+
* the earlier reply finishing then leaves this count equal to the mark rather
|
|
774
|
+
* than past it, and cannot end the new turn. Were the mark taken without
|
|
775
|
+
* drafts too, that finish would read as the new turn's answer. And a draft
|
|
776
|
+
* of the turn's OWN answer is not counted here, so it does not end the turn
|
|
777
|
+
* either — the same rule `repliedTo` applies on the exact path. */
|
|
778
|
+
export function countSettledAnswers(msgs: ConversationMessage[]): number {
|
|
779
|
+
let n = 0;
|
|
780
|
+
for (const m of msgs) {
|
|
781
|
+
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
782
|
+
if (answerText(m) && !stillWriting(m)) n++;
|
|
783
|
+
}
|
|
784
|
+
return n;
|
|
785
|
+
}
|
|
786
|
+
|
|
703
787
|
/** Has the turn stored under `userMessageId` already been answered?
|
|
704
788
|
*
|
|
705
789
|
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
@@ -749,6 +833,12 @@ export function turnAnswered(msgs: ConversationMessage[], userMessageId: string)
|
|
|
749
833
|
// exactly these two, and the two scanners read the same transcript: if they
|
|
750
834
|
// disagree, one of them ends the turn while the other leaves the ladder
|
|
751
835
|
// running at the fast cadence.
|
|
836
|
+
//
|
|
837
|
+
// They DO part on one case, deliberately: a row the service is still
|
|
838
|
+
// writing (`stillWriting`). That does not end a turn, but it IS proof the
|
|
839
|
+
// trigger landed — the agent is answering — so re-triggering the stored id
|
|
840
|
+
// here would have it answer the same turn twice. The ladder never reads
|
|
841
|
+
// this scanner, so the split cannot pin it fast.
|
|
752
842
|
if (role === "ASSISTANT" && answerText(m)) return true;
|
|
753
843
|
}
|
|
754
844
|
return false;
|
|
@@ -845,6 +935,24 @@ export interface AriaChatCore {
|
|
|
845
935
|
* that moved during the abort makes this snapshot stale, and committing it
|
|
846
936
|
* would end a turn the owner has since started. */
|
|
847
937
|
stop(s: AriaChatState): Promise<AriaChatState | undefined>;
|
|
938
|
+
|
|
939
|
+
/** Put one reply's prose on the clipboard. Defaults to the newest assistant
|
|
940
|
+
* bubble, which is the only one the descriptor offers the control on.
|
|
941
|
+
*
|
|
942
|
+
* Copies `bubble.content`, which is ALREADY the normalized prose — the
|
|
943
|
+
* envelope scan and the markdown stripper both ran on the way in
|
|
944
|
+
* (`bubblesFrom`), so the owner gets what they can read rather than the
|
|
945
|
+
* wire text. */
|
|
946
|
+
/** `undefined` when the turn moved on while the write was out — see `stop`. */
|
|
947
|
+
copyMessage(s: AriaChatState, arg?: AriaActionArg): Promise<AriaChatState | undefined>;
|
|
948
|
+
|
|
949
|
+
/** Ask for another answer.
|
|
950
|
+
*
|
|
951
|
+
* NOT a protocol feature: it sends ordinary user text, the same literal the
|
|
952
|
+
* native Aria app sends, through the one send path (`send`). Not through
|
|
953
|
+
* `submit`: with no argument that reads the COMPOSER, so a regenerate would
|
|
954
|
+
* send whatever the owner had half-typed instead of the retry. */
|
|
955
|
+
regenerate(s: AriaChatState): Promise<AriaChatState>;
|
|
848
956
|
/** Tapping the active widget's primary / secondary footer button.
|
|
849
957
|
*
|
|
850
958
|
* The url is read from STATE rather than bound into the action's params.
|
|
@@ -880,6 +988,10 @@ export interface AriaChatCore {
|
|
|
880
988
|
}
|
|
881
989
|
|
|
882
990
|
const POLL_ID = "aria-turn";
|
|
991
|
+
/** What Regenerate sends. Ordinary user text, matching the native Aria app —
|
|
992
|
+
* the chat-service has no retry verb, and inventing one here would be a
|
|
993
|
+
* protocol we then had to keep. */
|
|
994
|
+
const REGENERATE_TEXT = "Let's try that again.";
|
|
883
995
|
/** Id of the local greeting bubble. Shared by the minting and the
|
|
884
996
|
* "is it still the empty state?" test so the two cannot drift apart. */
|
|
885
997
|
const GREETING_ID = "greeting";
|
|
@@ -915,7 +1027,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
915
1027
|
const localHistory = o.localHistory !== false;
|
|
916
1028
|
|
|
917
1029
|
const initialState: AriaChatState = {
|
|
918
|
-
status: "loading", statusText: "", error: "", input: "",
|
|
1030
|
+
status: "loading", statusText: "", error: "", input: "", copyNote: "", replyActionsHidden: true,
|
|
919
1031
|
messages: [], prompts: [], _convId: "", _siteId: "", _pending: "",
|
|
920
1032
|
_pendingAfter: 0, _replyAfter: 0, _srvUsers: 0, _srvAssistants: 0,
|
|
921
1033
|
_abortId: "", _turnId: "", stopHidden: true, ok: false,
|
|
@@ -966,6 +1078,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
966
1078
|
* those. `submitting` stays on the state because the descriptor needs
|
|
967
1079
|
* something to disable a button with; it is the shadow, not the guard. */
|
|
968
1080
|
let widgetSending = false;
|
|
1081
|
+
/** A regenerate is on its way. Refuses a double-tap in the window before
|
|
1082
|
+
* `send` has written `_pending` — the same window `widgetSending` next door
|
|
1083
|
+
* closes for two taps of one option. */
|
|
1084
|
+
let regenerating = false;
|
|
969
1085
|
|
|
970
1086
|
/** A turn the server STORED but never got asked to answer.
|
|
971
1087
|
*
|
|
@@ -1093,7 +1209,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1093
1209
|
// both; the widget path did neither.
|
|
1094
1210
|
widget: carrySelection(
|
|
1095
1211
|
activeWidgetOf(
|
|
1096
|
-
|
|
1212
|
+
// A draft offers no controls: the agent has not finished asking, and
|
|
1213
|
+
// an answer tapped now would start a second turn on top of this one.
|
|
1214
|
+
// An empty newest row is NO_WIDGET — the placeholder rule.
|
|
1215
|
+
msgs.map((m) => ({ role: m.role, content: stillWriting(m) ? "" : stripHidden(messageText(m)) })),
|
|
1097
1216
|
disclosedLabel,
|
|
1098
1217
|
o.subtree,
|
|
1099
1218
|
(url) => isWixMediaHost(hostOf(url)),
|
|
@@ -1158,7 +1277,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1158
1277
|
// widget-only reply is, even though it leaves no bubble to inspect. Asking
|
|
1159
1278
|
// the last BUBBLE stalled this loop on exactly that reply, the same way it
|
|
1160
1279
|
// stalled `tick`.
|
|
1161
|
-
if (repliedTo(raw, s._turnId) ??
|
|
1280
|
+
if (repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter) {
|
|
1162
1281
|
return { ...s, status: "ready", _convId: convId, messages: bubbles, prompts: [], input: "", statusText: "", error: "", _pending: "", ...observed(raw, s.widget), ok: false };
|
|
1163
1282
|
}
|
|
1164
1283
|
}
|
|
@@ -1188,7 +1307,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1188
1307
|
// state this action RETURNS, so on the `_poll` path they paint immediately.
|
|
1189
1308
|
const optimistic: AriaBubble[] = [...s.messages, plainBubble(`u${s.messages.length}`, "USER", you(), content)];
|
|
1190
1309
|
const base: AriaChatState = {
|
|
1191
|
-
|
|
1310
|
+
// `copyNote: ""` — a Copy confirmation belongs to the moment, and a new
|
|
1311
|
+
// turn ends it. Left in place it would sit under the next reply saying
|
|
1312
|
+
// "Copied" about text the owner copied several turns ago.
|
|
1313
|
+
...s, prompts: [], input: "", error: "", copyNote: "",
|
|
1192
1314
|
// The PREVIOUS turn's handles must not survive into this one: aborting by
|
|
1193
1315
|
// a stale id would abandon a reply that has already been read, and waiting
|
|
1194
1316
|
// on a stale turn id would look for an answer that already exists. Both
|
|
@@ -1410,9 +1532,36 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1410
1532
|
* function of two other fields, and eight reducers each remembering to set it
|
|
1411
1533
|
* is eight chances for the control to be shown when there is nothing to stop
|
|
1412
1534
|
* — or, worse, hidden while a reply the owner wants to abandon runs on. */
|
|
1535
|
+
let committed: AriaChatState | null = null;
|
|
1536
|
+
let commits = 0;
|
|
1537
|
+
/** A result as the runtime stores it: the one-shot directives it strips on
|
|
1538
|
+
* commit (`_stream`, `_nav`, `_poll`, `_error` — see `applyNext` in the web
|
|
1539
|
+
* host's runtime bootstrap) removed. */
|
|
1540
|
+
const settled = (s: AriaChatState): AriaChatState => {
|
|
1541
|
+
const r = s as AriaChatState & Record<string, unknown>;
|
|
1542
|
+
if (r._stream === undefined && r._nav === undefined && r._poll === undefined && r._error === undefined) return s;
|
|
1543
|
+
const { _stream: _a, _nav: _b, _poll: _c, _error: _d, ...rest } = r;
|
|
1544
|
+
return rest as AriaChatState;
|
|
1545
|
+
};
|
|
1413
1546
|
const gated = (s: AriaChatState): AriaChatState => {
|
|
1414
1547
|
const hidden = !(turnInFlight(s) && !!s._abortId);
|
|
1415
|
-
|
|
1548
|
+
// The reply actions follow the same rule for the same reason: a derived
|
|
1549
|
+
// flag, recomputed on every action's way out, so no reducer can forget it.
|
|
1550
|
+
// `_pending` rather than `turnInFlight` — see `regenerate` for why `status`
|
|
1551
|
+
// is not trusted to have been cleared.
|
|
1552
|
+
const replyActionsHidden = !(lastReply(s) && !s._pending);
|
|
1553
|
+
const out = s.stopHidden === hidden && s.replyActionsHidden === replyActionsHidden
|
|
1554
|
+
? s
|
|
1555
|
+
: { ...s, stopHidden: hidden, replyActionsHidden };
|
|
1556
|
+
// Every state the runtime commits passes through here (an `undefined`
|
|
1557
|
+
// result commits nothing and skips it), so this is the newest committed
|
|
1558
|
+
// state — what `copyMessage` puts its note on when other actions landed
|
|
1559
|
+
// while its write was out. Kept as the runtime STORES it, i.e. without the
|
|
1560
|
+
// transient directives it strips on commit: re-returning a `_nav` or a
|
|
1561
|
+
// `_poll` from here would navigate, or re-arm the timer, a second time.
|
|
1562
|
+
committed = settled(out);
|
|
1563
|
+
commits += 1;
|
|
1564
|
+
return out;
|
|
1416
1565
|
};
|
|
1417
1566
|
|
|
1418
1567
|
/** Ask the service how an async job is getting on.
|
|
@@ -1550,7 +1699,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1550
1699
|
const raw = messagesOf(gm);
|
|
1551
1700
|
const bubbles = toBubbles(raw, assistant(), you());
|
|
1552
1701
|
const resumed = bubbles.length ? bubbles : greetingOnly();
|
|
1553
|
-
|
|
1702
|
+
// `copyNote: ""` — a resumed transcript ends in a reply, so the row is
|
|
1703
|
+
// shown at once; a note carried over would say "Copied" beside an
|
|
1704
|
+
// answer from a different conversation that nobody copied.
|
|
1705
|
+
return { ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
|
|
1554
1706
|
messages: resumed, ...observed(raw, s.widget), prompts: promptsFor(resumed, promptList()) };
|
|
1555
1707
|
} catch { /* expired or unreachable — fall through to a fresh one */ }
|
|
1556
1708
|
}
|
|
@@ -1649,11 +1801,11 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1649
1801
|
// previous one, which reads as ➕ having done nothing.
|
|
1650
1802
|
turnSeq += 1;
|
|
1651
1803
|
return slow({ ...s, status: "ready", _convId: conversationIdOf(res), _siteId: site, input: "", statusText: "", error: "",
|
|
1652
|
-
_pending: "", _abortId: "", _turnId: "", messages: greetingOnly(), ...observed([]), prompts: promptList(), ok: false });
|
|
1804
|
+
_pending: "", _abortId: "", _turnId: "", copyNote: "", messages: greetingOnly(), ...observed([]), prompts: promptList(), ok: false });
|
|
1653
1805
|
} catch (e) {
|
|
1654
1806
|
turnSeq += 1; // see above — a failed create commits state too
|
|
1655
1807
|
return slow({ ...s, status: "ready", _convId: "", _siteId: site, input: "", ...failureSlots(e, unavailableText),
|
|
1656
|
-
_pending: "", _abortId: "", _turnId: "", messages: greetingOnly(), ...observed([]), prompts: promptList(), ok: false });
|
|
1808
|
+
_pending: "", _abortId: "", _turnId: "", copyNote: "", messages: greetingOnly(), ...observed([]), prompts: promptList(), ok: false });
|
|
1657
1809
|
}
|
|
1658
1810
|
},
|
|
1659
1811
|
|
|
@@ -1709,7 +1861,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1709
1861
|
// Counted over RAW messages, so a widget-only answer still ends the turn
|
|
1710
1862
|
// instead of pinning the ladder to its fast rung forever (`countTurns`).
|
|
1711
1863
|
// Exact when the service named the turn, counted when it did not.
|
|
1712
|
-
const replied = repliedTo(raw, s._turnId) ??
|
|
1864
|
+
const replied = repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter;
|
|
1713
1865
|
const next: AriaChatState = {
|
|
1714
1866
|
...s,
|
|
1715
1867
|
status: replied ? "ready" : s.status,
|
|
@@ -1972,6 +2124,79 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1972
2124
|
return slow(withDraft(
|
|
1973
2125
|
{ ...s, status: "ready", statusText: stoppedText(), error: "", _pending: "", _abortId: "" }, typedAt));
|
|
1974
2126
|
},
|
|
2127
|
+
|
|
2128
|
+
copyMessage: async (s, arg) => {
|
|
2129
|
+
// Which bubble. The descriptor offers Copy on the newest assistant reply,
|
|
2130
|
+
// so the id is optional and that bubble is the default — but an id IS
|
|
2131
|
+
// accepted, because a row-scoped control passing `$item.id` is the
|
|
2132
|
+
// established shape on this screen (the link rows do exactly that).
|
|
2133
|
+
const wanted = typeof arg === "object" && arg !== null ? String((arg as { id?: string }).id ?? "") : "";
|
|
2134
|
+
const target = wanted
|
|
2135
|
+
? s.messages.find((m) => m.id === wanted)
|
|
2136
|
+
// The LAST bubble, and only if it is a reply — the same test `gated`
|
|
2137
|
+
// shows the row on. Searching back for the newest reply instead would,
|
|
2138
|
+
// after a widget-only answer (which leaves no bubble), skip over the
|
|
2139
|
+
// owner's question and copy the answer to the turn BEFORE it. The
|
|
2140
|
+
// greeting is excluded because it is a local bubble, not a reply.
|
|
2141
|
+
: lastReply(s);
|
|
2142
|
+
// `content` is ALREADY the prose: `bubblesFrom` scans the widget envelope
|
|
2143
|
+
// out first and then strips markdown, so this is what the owner can read
|
|
2144
|
+
// rather than the wire text.
|
|
2145
|
+
const text = (target?.content ?? "").trim();
|
|
2146
|
+
const failed = () => res(o.labels?.copyFailed ?? "Couldn't copy");
|
|
2147
|
+
if (!text) return { ...s, copyNote: failed() };
|
|
2148
|
+
// No clipboard on this host: say so, rather than report a success that
|
|
2149
|
+
// never happened. Same rule as the throw below.
|
|
2150
|
+
if (!o.copyText) return { ...s, copyNote: failed() };
|
|
2151
|
+
// Read BEFORE the await, compared after: `s` is a pre-await snapshot,
|
|
2152
|
+
// and the runtime commits the whole returned state. Two cases.
|
|
2153
|
+
// - A send or + moved the turn boundary: the note is about a reply that
|
|
2154
|
+
// is gone, so stand down and commit nothing (as `stop` does).
|
|
2155
|
+
// - Anything else landed — a keystroke, a widget selection, a query —
|
|
2156
|
+
// so `{ ...s }` would silently undo it. Put the note on the newest
|
|
2157
|
+
// COMMITTED state instead of on the snapshot.
|
|
2158
|
+
const seq = turnSeq;
|
|
2159
|
+
const at = commits;
|
|
2160
|
+
let note: string;
|
|
2161
|
+
try {
|
|
2162
|
+
await o.copyText(text);
|
|
2163
|
+
note = res(o.labels?.copied ?? "Copied");
|
|
2164
|
+
} catch {
|
|
2165
|
+
// The web Async Clipboard API rejects without a user gesture, and
|
|
2166
|
+
// Android refuses while the app is unfocused. Both land here, and both
|
|
2167
|
+
// are a FAILURE the owner is told about.
|
|
2168
|
+
note = failed();
|
|
2169
|
+
}
|
|
2170
|
+
if (turnSeq !== seq) return undefined;
|
|
2171
|
+
// No separate guard for a resume: `load` resumes only when the screen
|
|
2172
|
+
// holds no live conversation, and a Copy row exists only under a reply
|
|
2173
|
+
// in one — so a resume cannot land while a Copy is out. (The nav input
|
|
2174
|
+
// and site are fixed for a worker's life; a site switch boots a new one.)
|
|
2175
|
+
return { ...(commits !== at && committed ? committed : s), copyNote: note };
|
|
2176
|
+
},
|
|
2177
|
+
|
|
2178
|
+
regenerate: async (s) => {
|
|
2179
|
+
// Refuse a double-tap, and refuse while a reply is still coming — there
|
|
2180
|
+
// is nothing to regenerate until the last one has landed.
|
|
2181
|
+
// `_pending` rather than `turnInFlight`, and the difference is load-bearing.
|
|
2182
|
+
// `turnInFlight` is `status === "sending" || !!_pending`, and `status` can
|
|
2183
|
+
// still read "sending" after a turn has actually closed — a tick whose read
|
|
2184
|
+
// straddled a turn boundary returns `undefined` and writes nothing back, so
|
|
2185
|
+
// the status it would have cleared stays put. Measured: with `turnInFlight`
|
|
2186
|
+
// here, the SECOND regenerate of a conversation was refused even though the
|
|
2187
|
+
// first reply had landed. `_pending` is set by `send` and cleared when the
|
|
2188
|
+
// reply arrives, so it tracks "a turn is out" exactly; the latch above
|
|
2189
|
+
// covers the double-tap that `_pending` is too slow for.
|
|
2190
|
+
if (regenerating || !!s._pending) return s;
|
|
2191
|
+
regenerating = true;
|
|
2192
|
+
try {
|
|
2193
|
+
// `send` directly: it is the one send path, and `submit` with no
|
|
2194
|
+
// argument would read the composer instead of sending the retry.
|
|
2195
|
+
return await send(s, REGENERATE_TEXT);
|
|
2196
|
+
} finally {
|
|
2197
|
+
regenerating = false;
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
1975
2200
|
};
|
|
1976
2201
|
|
|
1977
2202
|
// Every action's result goes through the Stop gate on its way out.
|
|
@@ -1985,6 +2210,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1985
2210
|
tick: (s) => impl.tick(s).then((r) => (r ? gated(r) : r)),
|
|
1986
2211
|
openLink: (s, arg) => impl.openLink(s, arg).then(gated),
|
|
1987
2212
|
stop: (s) => impl.stop(s).then((r) => (r ? gated(r) : r)),
|
|
2213
|
+
copyMessage: (s, arg) => impl.copyMessage(s, arg).then((r) => (r ? gated(r) : r)),
|
|
2214
|
+
regenerate: (s) => impl.regenerate(s).then(gated),
|
|
1988
2215
|
// Synchronous, like `typing`: a selection edit makes no network call, so
|
|
1989
2216
|
// gating it is a plain call rather than a `.then`.
|
|
1990
2217
|
widgetToggle: (s, arg) => gated(impl.widgetToggle(s, arg)),
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { defineService } from "@himalaya/state";
|
|
25
25
|
import { open } from "@himalaya/open-url";
|
|
26
|
+
import { writeText } from "@himalaya/clipboard";
|
|
26
27
|
import {
|
|
27
28
|
activeSiteId,
|
|
28
29
|
createAriaChat,
|
|
@@ -148,6 +149,11 @@ const core = createAriaChat({
|
|
|
148
149
|
typing: () => copyParam("ownerAria.typing", "Aria is typing…"),
|
|
149
150
|
slow: () => copyParam("ownerAria.slow", "Aria is taking a while…"),
|
|
150
151
|
stopped: () => copyParam("ownerAria.stopped", "Stopped."),
|
|
152
|
+
// The note beside Copy. Through copyParam for the same reason as the rest:
|
|
153
|
+
// the buttons are localized by the pack, and a note left on the core's
|
|
154
|
+
// English default would be the one line under them that is not.
|
|
155
|
+
copied: () => copyParam("ownerAria.copied", "Copied"),
|
|
156
|
+
copyFailed: () => copyParam("ownerAria.copyFailed", "Couldn't copy"),
|
|
151
157
|
// Shown when the HOST carries no Wix app binding, so no Wix call can leave
|
|
152
158
|
// the browser (the web preview). Through copyParam like every other string
|
|
153
159
|
// here — a default baked into the shared core cannot be localized by this
|
|
@@ -167,6 +173,9 @@ const core = createAriaChat({
|
|
|
167
173
|
localHistory: false,
|
|
168
174
|
siteId: activeSiteId,
|
|
169
175
|
openLink: async (url) => (await open(url, "system_browser")).opened,
|
|
176
|
+
// Resolves or throws — `copyMessage` keys its confirmation on which, so a
|
|
177
|
+
// write the platform refused is never reported as a copy.
|
|
178
|
+
copyText: (text) => writeText(text),
|
|
170
179
|
resolveEntity: (url) => {
|
|
171
180
|
const screenId = screenForUrl(url, allowedTargets());
|
|
172
181
|
return screenId ? { screenId } : null;
|
|
@@ -191,6 +200,10 @@ const svc = defineService<AriaChatState, {
|
|
|
191
200
|
// new chat) holds a pre-boundary snapshot, and committing it would end the
|
|
192
201
|
// turn the owner has since started.
|
|
193
202
|
stop: (s: AriaChatState) => Promise<AriaChatState | undefined>;
|
|
203
|
+
// `undefined` for the same reason again: a write still out when the turn
|
|
204
|
+
// moved on must not commit its pre-await snapshot over the new one.
|
|
205
|
+
copyMessage: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState | undefined>;
|
|
206
|
+
regenerate: (s: AriaChatState) => Promise<AriaChatState>;
|
|
194
207
|
widgetPrimary: (s: AriaChatState) => Promise<AriaChatState>;
|
|
195
208
|
widgetSecondary: (s: AriaChatState) => Promise<AriaChatState>;
|
|
196
209
|
widgetToggle: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
@@ -218,6 +231,8 @@ const svc = defineService<AriaChatState, {
|
|
|
218
231
|
// false — so this is only ever dispatched while a reply is outstanding and
|
|
219
232
|
// the worker is holding the handle that can abort it.
|
|
220
233
|
stop: core.stop,
|
|
234
|
+
copyMessage: core.copyMessage,
|
|
235
|
+
regenerate: core.regenerate,
|
|
221
236
|
// The active widget's footer buttons. Named here as well as in the core:
|
|
222
237
|
// the descriptor dispatches `tier5.<service>.<action>`, and an action the
|
|
223
238
|
// entry does not export is a control that renders and does nothing.
|
|
@@ -37,7 +37,7 @@ export default defineApp({
|
|
|
37
37
|
actions: "http://localhost:8190/v1/actions",
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
|
-
capabilities: ["live_activity","push","share","secure_storage","open_url","local_database"],
|
|
40
|
+
capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard"],
|
|
41
41
|
designTokensPath: "./tokens.json",
|
|
42
42
|
navigation: {
|
|
43
43
|
"navKind": "responsive",
|
|
@@ -54961,6 +54961,83 @@ const SCREENS = runtimeSetting({
|
|
|
54961
54961
|
}
|
|
54962
54962
|
}
|
|
54963
54963
|
},
|
|
54964
|
+
{
|
|
54965
|
+
"id": "himi_owner_lite_aria.reply_actions",
|
|
54966
|
+
"body": {
|
|
54967
|
+
"package": "himalaya",
|
|
54968
|
+
"type": "HStack",
|
|
54969
|
+
"props": {
|
|
54970
|
+
"spacing": 4,
|
|
54971
|
+
"children": [
|
|
54972
|
+
{
|
|
54973
|
+
"id": "himi_owner_lite_aria.copy",
|
|
54974
|
+
"body": {
|
|
54975
|
+
"package": "himalaya",
|
|
54976
|
+
"type": "Button",
|
|
54977
|
+
"props": {
|
|
54978
|
+
"title": "Copy",
|
|
54979
|
+
"style": "plain",
|
|
54980
|
+
"size": "sm",
|
|
54981
|
+
"fullWidth": false,
|
|
54982
|
+
"leadingIcon": "copy",
|
|
54983
|
+
"action": {
|
|
54984
|
+
"actionId": "tier5.himiOwnerLiteAria.copyMessage"
|
|
54985
|
+
}
|
|
54986
|
+
}
|
|
54987
|
+
}
|
|
54988
|
+
},
|
|
54989
|
+
{
|
|
54990
|
+
"id": "himi_owner_lite_aria.regenerate",
|
|
54991
|
+
"body": {
|
|
54992
|
+
"package": "himalaya",
|
|
54993
|
+
"type": "Button",
|
|
54994
|
+
"props": {
|
|
54995
|
+
"title": "Regenerate",
|
|
54996
|
+
"style": "plain",
|
|
54997
|
+
"size": "sm",
|
|
54998
|
+
"fullWidth": false,
|
|
54999
|
+
"leadingIcon": "refresh",
|
|
55000
|
+
"action": {
|
|
55001
|
+
"actionId": "tier5.himiOwnerLiteAria.regenerate"
|
|
55002
|
+
}
|
|
55003
|
+
}
|
|
55004
|
+
}
|
|
55005
|
+
},
|
|
55006
|
+
{
|
|
55007
|
+
"id": "",
|
|
55008
|
+
"body": {
|
|
55009
|
+
"package": "himalaya",
|
|
55010
|
+
"type": "Text",
|
|
55011
|
+
"props": {
|
|
55012
|
+
"text": "$service:himiOwnerLiteAria.copyNote",
|
|
55013
|
+
"style": "footnote",
|
|
55014
|
+
"color": "textSecondary",
|
|
55015
|
+
"maxLines": 1,
|
|
55016
|
+
"collapseWhenEmpty": true
|
|
55017
|
+
}
|
|
55018
|
+
}
|
|
55019
|
+
},
|
|
55020
|
+
{
|
|
55021
|
+
"id": "",
|
|
55022
|
+
"body": {
|
|
55023
|
+
"package": "himalaya",
|
|
55024
|
+
"type": "Spacer",
|
|
55025
|
+
"props": {}
|
|
55026
|
+
}
|
|
55027
|
+
}
|
|
55028
|
+
]
|
|
55029
|
+
}
|
|
55030
|
+
},
|
|
55031
|
+
"modifiers": {
|
|
55032
|
+
"padding": {
|
|
55033
|
+
"leading": 0,
|
|
55034
|
+
"trailing": 0,
|
|
55035
|
+
"top": 0,
|
|
55036
|
+
"bottom": 4
|
|
55037
|
+
},
|
|
55038
|
+
"hiddenPath": "$service:himiOwnerLiteAria.replyActionsHidden"
|
|
55039
|
+
}
|
|
55040
|
+
},
|
|
54964
55041
|
{
|
|
54965
55042
|
"id": "himi_owner_lite_aria.prompts.bound",
|
|
54966
55043
|
"body": {
|
|
@@ -58340,7 +58417,8 @@ const SCREENS = runtimeSetting({
|
|
|
58340
58417
|
"contentType": "application/javascript+ts-bundle",
|
|
58341
58418
|
"declaredCapabilities": [
|
|
58342
58419
|
"wix_api",
|
|
58343
|
-
"open_url"
|
|
58420
|
+
"open_url",
|
|
58421
|
+
"clipboard"
|
|
58344
58422
|
],
|
|
58345
58423
|
"minBinaryVersion": "0.1.0",
|
|
58346
58424
|
"bundleVersion": "0.0.1"
|