@wix/himalaya-cli 0.1036.0 → 0.1038.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 +119 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/portfolio/src/index.d.ts +46 -9
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-genie-chat.mjs +51 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-portfolio.mjs +14 -2
- package/dist/cli.mjs +1323 -492
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +51 -2
- package/guides/headless-loop.md +7 -0
- package/guides/himi-test.md +66 -1
- package/guides/sdk-reference.json +90 -12
- package/guides/trim-a-template.md +5 -0
- package/guides/wix-business-context.md +13 -1
- package/guides/wix-site-to-himi-app.md +10 -1
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/beauty-salon-owner/dev/index.ts +2148 -1262
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/community-club-owner/dev/index.ts +2151 -1265
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/community-club-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/events-meetups-owner/dev/index.ts +2151 -1265
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/events-meetups-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/gym-fitness-owner/dev/index.ts +2148 -1262
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +2148 -1262
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/owner-lite/dev/index.ts +2145 -1259
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/owner-lite/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/podcast-creator-owner/dev/index.ts +2151 -1265
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
- package/templates/portfolio-creative-owner/dev/index.ts +2148 -1262
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/restaurant-owner/dev/index.ts +2148 -1262
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/restaurant-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/retail-store-owner/dev/index.ts +2151 -1265
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/retail-store-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/yoga-studio-owner/dev/index.ts +2148 -1262
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
|
@@ -37,8 +37,13 @@ import {
|
|
|
37
37
|
functionCallsOf,
|
|
38
38
|
getConversationMessages,
|
|
39
39
|
getConversations,
|
|
40
|
+
itemsOf,
|
|
41
|
+
listInstalledProviders,
|
|
42
|
+
mentionReference,
|
|
40
43
|
messageText,
|
|
41
44
|
messagesOf,
|
|
45
|
+
providersOf,
|
|
46
|
+
queryItems,
|
|
42
47
|
sendFunctionCallResults,
|
|
43
48
|
sendMessage,
|
|
44
49
|
triggerAssistantMessage,
|
|
@@ -47,6 +52,8 @@ import {
|
|
|
47
52
|
type Attachment,
|
|
48
53
|
type ConversationMessage,
|
|
49
54
|
type ConversationSummary,
|
|
55
|
+
type ItemsQuery,
|
|
56
|
+
type MessageReference,
|
|
50
57
|
type SendMessageResponse,
|
|
51
58
|
} from "@himalaya/wix-genie-chat";
|
|
52
59
|
import {
|
|
@@ -68,6 +75,8 @@ import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
|
68
75
|
import { NO_WIDGET, activeWidgetOf, asyncAdvanced, type SubtreeSlot, type WidgetOptionRow, type WidgetView }
|
|
69
76
|
from "./aria-widget-view.js";
|
|
70
77
|
import { substituteSelection } from "./aria-widget-model.js";
|
|
78
|
+
import { MENTION_PAGE_SIZE, allowlisted, emptyMentionModel, mentionView,
|
|
79
|
+
type MentionItemRow, type MentionLabels, type MentionModel, type MentionProvider, type MentionView } from "./aria-mentions.js";
|
|
71
80
|
import { isTransportFailure, ownerTransportFailureText } from "./owner-load-error.js";
|
|
72
81
|
import { LOCAL_ID_PREFIX, createRowMemo, mintLocalId, transcriptKeys, type RowMemo } from "./aria-hot-path.js";
|
|
73
82
|
|
|
@@ -102,6 +111,11 @@ export interface AriaBubble {
|
|
|
102
111
|
* that right-aligns the owner renders two templates and collapses one. */
|
|
103
112
|
notUser: boolean;
|
|
104
113
|
notAria: boolean;
|
|
114
|
+
/** What the turn MENTIONED — "@Blue Dress · @Maya Chen" — read off the
|
|
115
|
+
* references stored beside the text. Empty (and `refsHidden`) on every turn
|
|
116
|
+
* that mentioned nothing, which is every assistant turn. */
|
|
117
|
+
refs: string;
|
|
118
|
+
refsHidden: boolean;
|
|
105
119
|
/** What a screen reader says for the row: the author, then the words. A
|
|
106
120
|
* screen that draws the speaker only as a bubble and a side of the screen
|
|
107
121
|
* (`messageStyle: 'userBubble'`) has no visible author line to read, and
|
|
@@ -161,6 +175,10 @@ export interface AriaChatState {
|
|
|
161
175
|
/** Text of the optimistic user bubble awaiting reconciliation with the server
|
|
162
176
|
* transcript. Empty when nothing is in flight. */
|
|
163
177
|
_pending: string;
|
|
178
|
+
/** The optimistic bubble's mention line, so a tick that re-appends the
|
|
179
|
+
* bubble before the server echoes the turn does not drop what it mentioned.
|
|
180
|
+
* Read only while `_pending` is set. */
|
|
181
|
+
_pendingRefs: string;
|
|
164
182
|
/** The file names the in-flight message was sent with, so the optimistic
|
|
165
183
|
* bubble keeps its attachment line across every tick until the server's own
|
|
166
184
|
* copy of the turn replaces it. */
|
|
@@ -223,6 +241,10 @@ export interface AriaChatState {
|
|
|
223
241
|
* every time the transcript is, which is what makes "one active widget"
|
|
224
242
|
* true by construction rather than by remembering to clear it. */
|
|
225
243
|
widget: WidgetView;
|
|
244
|
+
/** The mention picker and the picks waiting above the composer, flattened
|
|
245
|
+
* for binding (`aria-mentions.ts`). Derived from the core's mention model on
|
|
246
|
+
* the way out of EVERY action — see `gated`. */
|
|
247
|
+
mention: MentionView;
|
|
226
248
|
/** The photos waiting in the composer to go with the next message.
|
|
227
249
|
*
|
|
228
250
|
* A PROJECTION, not a field the reducers maintain: the worker holds the chips
|
|
@@ -295,6 +317,18 @@ export interface AriaChatLabels {
|
|
|
295
317
|
* all — it carries no Wix app binding to sign the chat-service call with.
|
|
296
318
|
* See `isWixAccountUnreachable`, and note what it is NOT: a signed-out user. */
|
|
297
319
|
unavailable?: Resolvable<string>;
|
|
320
|
+
/** The mention picker's copy. See `MentionLabels`. */
|
|
321
|
+
mentionTitle?: Resolvable<string>;
|
|
322
|
+
mentionNoProviders?: Resolvable<string>;
|
|
323
|
+
mentionNoItems?: Resolvable<string>;
|
|
324
|
+
mentionFailed?: Resolvable<string>;
|
|
325
|
+
/** Shown when THIS host cannot reach the provider service at all — the
|
|
326
|
+
* service is native-only (see the chat client), so on the web preview the
|
|
327
|
+
* picker says so instead of pretending the site has nothing to mention. */
|
|
328
|
+
mentionUnavailable?: Resolvable<string>;
|
|
329
|
+
/** Shown when one provider's items could not be fetched. Takes the
|
|
330
|
+
* category's label. */
|
|
331
|
+
mentionItemsFailed?: (category: string) => string;
|
|
298
332
|
/** Attachment copy — see `AriaChatOptions.attachments`. Chip states: */
|
|
299
333
|
attachUploading?: Resolvable<string>;
|
|
300
334
|
attachFailed?: Resolvable<string>;
|
|
@@ -674,6 +708,20 @@ export function withSpoken(bubbles: AriaBubble[]): AriaBubble[] {
|
|
|
674
708
|
: bubbles.map((b) => (typeof b.spoken === "string" ? b : { ...b, spoken: spokenOf(b.who, b.content) }));
|
|
675
709
|
}
|
|
676
710
|
|
|
711
|
+
/** The line a bubble shows for what its turn mentioned: each MENTION
|
|
712
|
+
* reference's label, `@`-prefixed. Only `source: "mention"` counts — the
|
|
713
|
+
* service also stores references of other kinds (a reply-to, page context),
|
|
714
|
+
* and those are not something the owner picked. */
|
|
715
|
+
export function refsLine(refs: MessageReference[] | undefined): string {
|
|
716
|
+
const labels: string[] = [];
|
|
717
|
+
for (const r of refs ?? []) {
|
|
718
|
+
if (r?.source !== "mention") continue;
|
|
719
|
+
const label = typeof r.label === "string" ? r.label.trim() : "";
|
|
720
|
+
if (label) labels.push(`@${label}`);
|
|
721
|
+
}
|
|
722
|
+
return labels.join(" · ");
|
|
723
|
+
}
|
|
724
|
+
|
|
677
725
|
/** A bubble's words and its attachment line, with its collapse flag.
|
|
678
726
|
*
|
|
679
727
|
* A turn that is ONLY attachments carries the line as its content instead of
|
|
@@ -688,10 +736,14 @@ function withAttachments(content: string, names: readonly string[]): { content:
|
|
|
688
736
|
}
|
|
689
737
|
|
|
690
738
|
/** A greeting or optimistic bubble: no links, all slots defaulted. `attached`
|
|
691
|
-
* names the files an optimistic USER bubble is being sent with
|
|
692
|
-
|
|
739
|
+
* names the files an optimistic USER bubble is being sent with, and `refs` is
|
|
740
|
+
* its mention line, already formatted. */
|
|
741
|
+
export function plainBubble(
|
|
742
|
+
id: string, role: string, who: string, content: string, attached: readonly string[] = [], refs = "",
|
|
743
|
+
): AriaBubble {
|
|
693
744
|
const a = withAttachments(content, attached);
|
|
694
|
-
return { id, role, who, notUser: role !== "USER", notAria: role === "USER", spoken: spokenOf(who, a.content),
|
|
745
|
+
return { id, role, who, notUser: role !== "USER", notAria: role === "USER", spoken: spokenOf(who, a.content),
|
|
746
|
+
refs, refsHidden: !refs, ...linkFieldsOf("", role), ...a };
|
|
695
747
|
}
|
|
696
748
|
|
|
697
749
|
/** Server transcript → bubbles. USER text is literal; ASSISTANT replies are
|
|
@@ -732,11 +784,14 @@ export function toBubbles(
|
|
|
732
784
|
// not something the service is known to send, and naming files on it
|
|
733
785
|
// would claim Aria sent them.
|
|
734
786
|
const attached = role === "USER" ? attachmentsOf(m).map((a) => a.fileName) : [];
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
787
|
+
// A USER turn only: an owner mentions things, the assistant does not.
|
|
788
|
+
const refs = role === "USER" ? refsLine(m.references) : "";
|
|
789
|
+
const build = () => bubbleOf(id, role, who, raw, writing, attached, refs);
|
|
790
|
+
// Attachments and mentions are part of the key: neither is in the text, so
|
|
791
|
+
// a row keyed on text alone could serve a bubble naming the wrong files or
|
|
792
|
+
// the wrong picks.
|
|
738
793
|
const b = memo
|
|
739
|
-
? memo.row(id, raw, `${role}\u0000${who}\u0000${writing ? "w" : ""}\u0000${attached.join("\u0001")}`, build)
|
|
794
|
+
? memo.row(id, raw, `${role}\u0000${who}\u0000${writing ? "w" : ""}\u0000${attached.join("\u0001")}\u0000${refs}`, build)
|
|
740
795
|
: build();
|
|
741
796
|
if (b) out.push(b);
|
|
742
797
|
}
|
|
@@ -761,7 +816,7 @@ export type BubbleMemo = RowMemo<AriaBubble | null>;
|
|
|
761
816
|
export const createBubbleMemo = (): BubbleMemo => createRowMemo<AriaBubble | null>();
|
|
762
817
|
|
|
763
818
|
function bubbleOf(
|
|
764
|
-
id: string, role: string, who: string, raw: string, writing: boolean, attached: readonly string[] = [],
|
|
819
|
+
id: string, role: string, who: string, raw: string, writing: boolean, attached: readonly string[] = [], refs = "",
|
|
765
820
|
): AriaBubble | null {
|
|
766
821
|
// Only assistant turns carry envelopes; a user's own text is literal, and
|
|
767
822
|
// scanning it would let an owner who typed `<<…>>` erase their own message.
|
|
@@ -786,6 +841,8 @@ function bubbleOf(
|
|
|
786
841
|
notUser: role !== "USER",
|
|
787
842
|
notAria: role === "USER",
|
|
788
843
|
spoken: spokenOf(who, a.content),
|
|
844
|
+
refs,
|
|
845
|
+
refsHidden: !refs,
|
|
789
846
|
...linkFieldsOf(prose, role),
|
|
790
847
|
...a,
|
|
791
848
|
};
|
|
@@ -838,14 +895,14 @@ export function argText(arg: AriaActionArg): string {
|
|
|
838
895
|
* vanish mid-turn. */
|
|
839
896
|
export function reconcile(
|
|
840
897
|
server: AriaBubble[], pending: string, pendingAfter: number, youName = "You", pendingId = "",
|
|
841
|
-
pendingAttached: readonly string[] = [],
|
|
898
|
+
pendingAttached: readonly string[] = [], pendingRefs = "",
|
|
842
899
|
): { messages: AriaBubble[]; pending: string } {
|
|
843
900
|
if (!pending) return { messages: server, pending: "" };
|
|
844
901
|
if (countRole(server, "USER") > pendingAfter) return { messages: server, pending: "" };
|
|
845
902
|
// `pendingId` is empty only for state persisted before ids were minted; a
|
|
846
903
|
// fixed fallback still keeps that one row stable across ticks.
|
|
847
904
|
const id = pendingId || `${LOCAL_ID_PREFIX}pending`;
|
|
848
|
-
return { messages: [...server, plainBubble(id, "USER", youName, pending, pendingAttached)], pending };
|
|
905
|
+
return { messages: [...server, plainBubble(id, "USER", youName, pending, pendingAttached, pendingRefs)], pending };
|
|
849
906
|
}
|
|
850
907
|
|
|
851
908
|
/** How many turns of one role the transcript holds. Counted over BUBBLES rather
|
|
@@ -1178,6 +1235,23 @@ export interface AriaChatCore {
|
|
|
1178
1235
|
widgetPick(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
|
|
1179
1236
|
/** Answer one QUESTIONNAIRE question. */
|
|
1180
1237
|
widgetAnswer(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
|
|
1238
|
+
/** Open the mention picker at the category level, listing the site's
|
|
1239
|
+
* installed providers the first time (per business). `undefined` when the
|
|
1240
|
+
* business switched while the list was being fetched. */
|
|
1241
|
+
mentionOpen(s: AriaChatState): Promise<AriaChatState | undefined>;
|
|
1242
|
+
/** Close it. Picks already made stay above the composer. */
|
|
1243
|
+
mentionClose(s: AriaChatState): AriaChatState;
|
|
1244
|
+
/** From a provider's items back to the categories. */
|
|
1245
|
+
mentionBack(s: AriaChatState): AriaChatState;
|
|
1246
|
+
/** Browse one provider (`arg.id` / `arg.value` is its key). Resolves to
|
|
1247
|
+
* `undefined` when a later picker action superseded it — see `mentionSeq`. */
|
|
1248
|
+
mentionProvider(s: AriaChatState, arg?: AriaActionArg): Promise<AriaChatState | undefined>;
|
|
1249
|
+
/** Search the provider being browsed. Same `undefined` rule. */
|
|
1250
|
+
mentionQuery(s: AriaChatState, arg?: AriaActionArg): Promise<AriaChatState | undefined>;
|
|
1251
|
+
/** Pick one item (`arg.id` is its row id) — a chip above the composer. */
|
|
1252
|
+
mentionPick(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
|
|
1253
|
+
/** Drop one chip (`arg.id` is the chip id). */
|
|
1254
|
+
mentionRemove(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
|
|
1181
1255
|
/** Pick (or take) a photo and start uploading it. Returns as soon as the chip
|
|
1182
1256
|
* exists — the upload reports back through `attachmentSettled`. The source is
|
|
1183
1257
|
* the arg (`"camera"`, or `{ source: "camera" }`); anything else is the
|
|
@@ -1255,12 +1329,112 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1255
1329
|
const historyKeyFor = (site: string) => scopedKey(o.historyKey, site);
|
|
1256
1330
|
const localHistory = o.localHistory !== false;
|
|
1257
1331
|
|
|
1332
|
+
// -------------------------------------------------------------------------
|
|
1333
|
+
// Mentions
|
|
1334
|
+
// -------------------------------------------------------------------------
|
|
1335
|
+
|
|
1336
|
+
const mentionLabels = (): MentionLabels => ({
|
|
1337
|
+
title: res(o.labels?.mentionTitle ?? "Mention"),
|
|
1338
|
+
noProviders: res(o.labels?.mentionNoProviders ?? "There's nothing on this site to mention yet."),
|
|
1339
|
+
noItems: res(o.labels?.mentionNoItems ?? "Nothing found."),
|
|
1340
|
+
providersFailed: res(o.labels?.mentionFailed ?? "Couldn't load what you can mention."),
|
|
1341
|
+
unavailable: res(o.labels?.mentionUnavailable ?? "Mentions aren't available here."),
|
|
1342
|
+
});
|
|
1343
|
+
const itemsFailedText = (category: string): string =>
|
|
1344
|
+
o.labels?.mentionItemsFailed?.(category) ?? `Couldn't load ${category}.`;
|
|
1345
|
+
|
|
1346
|
+
/** The picker, and the picks waiting to be sent — kept HERE, outside the
|
|
1347
|
+
* reducer state, and painted onto every action's result by `gated`.
|
|
1348
|
+
*
|
|
1349
|
+
* A latch for the reason `draft` is one. Reducers are pure functions of the
|
|
1350
|
+
* state handed to them, and the poll ladder keeps a `tick` in the air for
|
|
1351
|
+
* much of a screen's life: a tick that read its snapshot BEFORE the owner
|
|
1352
|
+
* opened the picker would, on resolving, write the closed picker back —
|
|
1353
|
+
* the panel snapping shut under their finger. Painting the model on the way
|
|
1354
|
+
* out of every action, the tick included, makes the latest pick win no
|
|
1355
|
+
* matter which action carries it.
|
|
1356
|
+
*
|
|
1357
|
+
* What that costs: the model is not persisted, so a worker that is torn
|
|
1358
|
+
* down and re-booted comes back with the picker closed and no chips. A pick
|
|
1359
|
+
* is a draft the owner has not sent, exactly like the composer text. */
|
|
1360
|
+
let mentionModel: MentionModel = emptyMentionModel();
|
|
1361
|
+
let mentionViewNow: MentionView = mentionView(mentionModel, mentionLabels());
|
|
1362
|
+
const setMention = (next: MentionModel): void => {
|
|
1363
|
+
mentionModel = next;
|
|
1364
|
+
mentionViewNow = mentionView(next, mentionLabels());
|
|
1365
|
+
};
|
|
1366
|
+
/** Bumped by every picker transition that makes an in-flight item query
|
|
1367
|
+
* stale: a newer search, another provider, Back, Close, a pick, a new chat,
|
|
1368
|
+
* a business switch. A query compares on resolving and stands down. */
|
|
1369
|
+
let mentionSeq = 0;
|
|
1370
|
+
/** Chip ids. Minted rather than derived from the provider key and item id,
|
|
1371
|
+
* neither of which is guaranteed to be safe inside a component id. */
|
|
1372
|
+
let pickSeq = 0;
|
|
1373
|
+
|
|
1374
|
+
/** A business switch drops the providers AND the picks: a product of the
|
|
1375
|
+
* previous business is not something to mention to this one. */
|
|
1376
|
+
const mentionSite = (): void => {
|
|
1377
|
+
const site = siteOf();
|
|
1378
|
+
if (mentionModel.site === site) return;
|
|
1379
|
+
mentionSeq += 1;
|
|
1380
|
+
setMention({ ...emptyMentionModel(), site });
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
/** Which sentence a failed provider call gets. The service is native-only, so
|
|
1384
|
+
* a host that cannot reach it is told so plainly — never shown an empty
|
|
1385
|
+
* list, which would read as "this site has nothing to mention". */
|
|
1386
|
+
const mentionFailure = (e: unknown, fallback: string): string => {
|
|
1387
|
+
const msg = String((e as Error)?.message ?? e ?? "");
|
|
1388
|
+
return isWixAccountUnreachable(e) || msg.includes("absolute_urls_not_allowed")
|
|
1389
|
+
? mentionLabels().unavailable
|
|
1390
|
+
: fallback;
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const mentionArg = (arg?: AriaActionArg): string =>
|
|
1394
|
+
typeof arg === "string" ? arg.trim() : firstString(arg?.id, arg?.optionValue, arg?.value, arg?.text);
|
|
1395
|
+
|
|
1396
|
+
/** One provider page into rows. Row ids are positional — the item id is
|
|
1397
|
+
* kept beside them for the pick — and a picture is only shown from a Wix
|
|
1398
|
+
* media host, the same rule the widget images follow. */
|
|
1399
|
+
const itemRows = (provider: MentionProvider, items: ReturnType<typeof itemsOf>): MentionItemRow[] => {
|
|
1400
|
+
const seen = new Set<string>();
|
|
1401
|
+
const rows: MentionItemRow[] = [];
|
|
1402
|
+
for (const it of items) {
|
|
1403
|
+
if (seen.has(it.id)) continue;
|
|
1404
|
+
seen.add(it.id);
|
|
1405
|
+
const pic = provider.showsImage && isWixMediaHost(hostOf(it.imageUrl)) ? it.imageUrl : "";
|
|
1406
|
+
rows.push({ id: `i${rows.length}`, itemId: it.id, title: it.title, subtitle: it.subtitle,
|
|
1407
|
+
subtitleHidden: !it.subtitle, pictureUrl: pic, pictureHidden: !pic });
|
|
1408
|
+
}
|
|
1409
|
+
return rows;
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
/** Ask one provider for a page. Resolves to `undefined` — leave the state
|
|
1413
|
+
* alone — when a later picker transition superseded it. */
|
|
1414
|
+
const fetchItems = async (s: AriaChatState, provider: MentionProvider, q: string, seq: number): Promise<AriaChatState | undefined> => {
|
|
1415
|
+
const query: ItemsQuery = {
|
|
1416
|
+
...(q ? { search: q } : {}),
|
|
1417
|
+
...(provider.paging === "CURSOR" ? { cursorPaging: { limit: MENTION_PAGE_SIZE } }
|
|
1418
|
+
: provider.paging === "OFFSET" ? { paging: { limit: MENTION_PAGE_SIZE, offset: 0 } } : {}),
|
|
1419
|
+
};
|
|
1420
|
+
let items: MentionItemRow[] = [];
|
|
1421
|
+
let failed = "";
|
|
1422
|
+
try {
|
|
1423
|
+
items = itemRows(provider, itemsOf(await queryItems(provider.key, query)));
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
failed = mentionFailure(e, itemsFailedText(provider.label));
|
|
1426
|
+
}
|
|
1427
|
+
if (seq !== mentionSeq) return undefined;
|
|
1428
|
+
setMention({ ...mentionModel, items, itemsLoading: false, itemsFailed: failed });
|
|
1429
|
+
return committed ?? s;
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1258
1432
|
const initialState: AriaChatState = {
|
|
1259
1433
|
status: "loading", statusText: "", error: "", input: "", copyNote: "", replyActionsHidden: true,
|
|
1260
|
-
messages: [], prompts: [], _convId: "", _siteId: "", _pending: "", _pendingAttach: [],
|
|
1434
|
+
messages: [], prompts: [], _convId: "", _siteId: "", _pending: "", _pendingRefs: "", _pendingAttach: [],
|
|
1261
1435
|
_pendingAfter: 0, _pendingId: "", _replyAfter: 0, _srvUsers: 0, _srvAssistants: 0,
|
|
1262
1436
|
_abortId: "", _turnId: "", stopHidden: true, ok: false,
|
|
1263
|
-
widget: NO_WIDGET,
|
|
1437
|
+
widget: NO_WIDGET, mention: mentionViewNow,
|
|
1264
1438
|
attachments: [], attachmentsHidden: true,
|
|
1265
1439
|
};
|
|
1266
1440
|
|
|
@@ -1422,8 +1596,11 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1422
1596
|
* A latch rather than state, for the same reason `inFlightSend` is one: it is
|
|
1423
1597
|
* a transient recovery hint about one failed attempt, not something that
|
|
1424
1598
|
* should survive a reload or be persisted with the transcript. Keyed by
|
|
1425
|
-
* conversation
|
|
1426
|
-
* was recorded for
|
|
1599
|
+
* conversation, content AND the references (mentions) the turn was stored
|
|
1600
|
+
* with, so it can only ever re-trigger the exact turn it was recorded for: the
|
|
1601
|
+
* same words retried with other picks are a new turn, and re-triggering the
|
|
1602
|
+
* stored one would answer about the old picks and then clear the new chips as
|
|
1603
|
+
* if they had been sent.
|
|
1427
1604
|
*
|
|
1428
1605
|
* It also has to EXPIRE, and cannot do so on its own clock. A trigger whose
|
|
1429
1606
|
* RESPONSE was lost — a timeout, a reset — failed here while succeeding at the
|
|
@@ -1432,7 +1609,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1432
1609
|
* owner's next identical question instead, which is quieter and worse. The
|
|
1433
1610
|
* expiry is therefore a question asked of the SERVER at reuse time — see
|
|
1434
1611
|
* `turnAnswered` — rather than anything inferred from local counts. */
|
|
1435
|
-
let stalledTurn: { convId: string; content: string; userMessageId: string } | null = null;
|
|
1612
|
+
let stalledTurn: { convId: string; content: string; refsKey: string; userMessageId: string } | null = null;
|
|
1436
1613
|
|
|
1437
1614
|
/** A monotonic turn epoch, bumped by every TURN BOUNDARY: a successful `stop`,
|
|
1438
1615
|
* the start of a `send`, and a `newChat`.
|
|
@@ -1792,12 +1969,25 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1792
1969
|
const withDraft = (next: AriaChatState, since: number): AriaChatState =>
|
|
1793
1970
|
draftSeq === since || draft === next.input ? next : { ...next, input: draft };
|
|
1794
1971
|
|
|
1795
|
-
async function send(
|
|
1972
|
+
async function send(
|
|
1973
|
+
s: AriaChatState, content: string, attached: readonly AriaAttachmentChip[] = [], withPicks = true,
|
|
1974
|
+
): Promise<AriaChatState> {
|
|
1796
1975
|
// A new turn is a turn boundary: any tick still parked on the PREVIOUS
|
|
1797
1976
|
// turn's transcript read must stand down rather than write its snapshot over
|
|
1798
1977
|
// this one. Bump before the first await so an in-flight tick is already stale
|
|
1799
1978
|
// by the time it resolves.
|
|
1800
1979
|
turnSeq += 1;
|
|
1980
|
+
// The picks above the composer ride WITH this turn, as references beside
|
|
1981
|
+
// the text — never as tokens inside it. Read once, here: a chip the owner
|
|
1982
|
+
// adds while this send is in the air belongs to the NEXT turn, and only the
|
|
1983
|
+
// ones actually sent are cleared below.
|
|
1984
|
+
mentionSite();
|
|
1985
|
+
const sentPicks = withPicks ? mentionModel.picks : [];
|
|
1986
|
+
const references = sentPicks.map((p) => mentionReference({ id: p.itemId, title: p.label }, p.category));
|
|
1987
|
+
// What this turn's references ARE, for `stalledTurn`'s identity. `data` names
|
|
1988
|
+
// the item and its category; the order is the owner's, and so is the turn's.
|
|
1989
|
+
const refsKey = references.map((r) => r.data ?? "").join("\u0000");
|
|
1990
|
+
const refs = refsLine(references);
|
|
1801
1991
|
// Only chips whose bytes have LANDED travel; `submit` refuses to get here
|
|
1802
1992
|
// with any other kind, and this filter is the second lock on that door.
|
|
1803
1993
|
const riding = attached.filter((c) => c.state === "ready" && !!c.ref);
|
|
@@ -1824,13 +2014,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1824
2014
|
localId = minted.id;
|
|
1825
2015
|
}
|
|
1826
2016
|
const optimistic: AriaBubble[] = [
|
|
1827
|
-
...s.messages.filter((b) => b.id !== localId), plainBubble(localId, "USER", you(), content, attachedNames),
|
|
2017
|
+
...s.messages.filter((b) => b.id !== localId), plainBubble(localId, "USER", you(), content, attachedNames, refs),
|
|
1828
2018
|
];
|
|
1829
2019
|
const base: AriaChatState = {
|
|
1830
2020
|
// `copyNote: ""` — a Copy confirmation belongs to the moment, and a new
|
|
1831
2021
|
// turn ends it. Left in place it would sit under the next reply saying
|
|
1832
2022
|
// "Copied" about text the owner copied several turns ago.
|
|
1833
|
-
...s, prompts: [], input: "", error: "", copyNote: "",
|
|
2023
|
+
...s, prompts: [], input: "", error: "", copyNote: "", _pendingRefs: refs,
|
|
1834
2024
|
// The PREVIOUS turn's handles must not survive into this one: aborting by
|
|
1835
2025
|
// a stale id would abandon a reply that has already been read, and waiting
|
|
1836
2026
|
// on a stale turn id would look for an answer that already exists. Both
|
|
@@ -1891,7 +2081,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1891
2081
|
// send is about (`supersedes`)? And if so, is that turn still worth
|
|
1892
2082
|
// re-triggering (`stalled`)?
|
|
1893
2083
|
latch = stalledTurn;
|
|
1894
|
-
supersedes = !!latch && latch.convId === convId && latch.content === content;
|
|
2084
|
+
supersedes = !!latch && latch.convId === convId && latch.content === content && latch.refsKey === refsKey;
|
|
1895
2085
|
// …and only while it is still UNANSWERED, which is ASKED rather than
|
|
1896
2086
|
// inferred. A trigger whose response was lost failed here while succeeding
|
|
1897
2087
|
// at the service, so the answer arrives anyway and the id now names a
|
|
@@ -1937,9 +2127,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1937
2127
|
} else {
|
|
1938
2128
|
answer = await sendMessage(convId, content, {
|
|
1939
2129
|
origin,
|
|
1940
|
-
//
|
|
1941
|
-
// is byte-for-byte what it was before attachments
|
|
2130
|
+
// Each omitted rather than `[]` when there are none, so a plain text
|
|
2131
|
+
// turn is byte-for-byte what it was before attachments and mentions.
|
|
1942
2132
|
...(attachments.length ? { attachments } : {}),
|
|
2133
|
+
...(references.length ? { messageReferences: references } : {}),
|
|
1943
2134
|
});
|
|
1944
2135
|
abortId = userMessageIdOf(answer);
|
|
1945
2136
|
}
|
|
@@ -1988,6 +2179,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1988
2179
|
// away a turn this send never touched.
|
|
1989
2180
|
if (supersedes && stalledTurn === latch) stalledTurn = null;
|
|
1990
2181
|
retriedId = "";
|
|
2182
|
+
// Delivered, so its chips are spent — the stored turn carries them now,
|
|
2183
|
+
// and on a re-trigger it already did. Only THESE chips: one added during
|
|
2184
|
+
// the send is waiting for the next turn. A FAILED send keeps them all,
|
|
2185
|
+
// so the retry sends what the owner picked.
|
|
2186
|
+
if (sentPicks.length) {
|
|
2187
|
+
setMention({ ...mentionModel, picks: mentionModel.picks.filter((p) => !sentPicks.includes(p)) });
|
|
2188
|
+
}
|
|
1991
2189
|
// A conversation minted just now starts empty, so the marks taken above
|
|
1992
2190
|
// from the PREVIOUS conversation's counts would be ahead of it and its
|
|
1993
2191
|
// first reply would never read as new — the same stranded "typing…" this
|
|
@@ -2043,7 +2241,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2043
2241
|
const storedId = (e as { storedUserMessageId?: unknown })?.storedUserMessageId;
|
|
2044
2242
|
const keptId = typeof storedId === "string" && storedId ? storedId : retriedId;
|
|
2045
2243
|
stalledTurn = convId && keptId
|
|
2046
|
-
? { convId, content, userMessageId: keptId }
|
|
2244
|
+
? { convId, content, refsKey, userMessageId: keptId }
|
|
2047
2245
|
// Nothing stored by this attempt. Drop the latch only if it is the one
|
|
2048
2246
|
// this send was working on and is still the one we captured; otherwise it
|
|
2049
2247
|
// belongs to another turn and must survive.
|
|
@@ -2089,7 +2287,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2089
2287
|
* action rather than maintained inside each of them: `stopHidden` is a pure
|
|
2090
2288
|
* function of two other fields, and eight reducers each remembering to set it
|
|
2091
2289
|
* is eight chances for the control to be shown when there is nothing to stop
|
|
2092
|
-
* — or, worse, hidden while a reply the owner wants to abandon runs on.
|
|
2290
|
+
* — or, worse, hidden while a reply the owner wants to abandon runs on.
|
|
2291
|
+
*
|
|
2292
|
+
* It also paints the mention picker (see `mentionModel`), and for the same
|
|
2293
|
+
* reason: every action's result passes through here, so this is the one
|
|
2294
|
+
* place a stale snapshot cannot sneak an old picker back onto the screen.
|
|
2295
|
+
* Both edits are made only when something changed, so a reducer that
|
|
2296
|
+
* returns the state untouched still returns the SAME object. */
|
|
2093
2297
|
let committed: AriaChatState | null = null;
|
|
2094
2298
|
let commits = 0;
|
|
2095
2299
|
/** A result as the runtime stores it: the one-shot directives it strips on
|
|
@@ -2102,6 +2306,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2102
2306
|
return rest as AriaChatState;
|
|
2103
2307
|
};
|
|
2104
2308
|
const gated = (s: AriaChatState): AriaChatState => {
|
|
2309
|
+
mentionSite();
|
|
2105
2310
|
const hidden = !(turnInFlight(s) && !!s._abortId);
|
|
2106
2311
|
// The reply actions follow the same rule for the same reason: a derived
|
|
2107
2312
|
// flag, recomputed on every action's way out, so no reducer can forget it.
|
|
@@ -2118,9 +2323,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2118
2323
|
seedChips(actioned);
|
|
2119
2324
|
for (const id of [...chipBytes.keys()]) if (!chips.some((c) => c.id === id)) chipBytes.delete(id);
|
|
2120
2325
|
const attachmentsHidden = chips.length === 0;
|
|
2121
|
-
|
|
2326
|
+
let out = actioned.attachments === chips && actioned.attachmentsHidden === attachmentsHidden
|
|
2122
2327
|
? actioned
|
|
2123
2328
|
: { ...actioned, attachments: chips, attachmentsHidden };
|
|
2329
|
+
if (out.mention !== mentionViewNow) out = { ...out, mention: mentionViewNow };
|
|
2124
2330
|
// Every state the runtime commits passes through here (an `undefined`
|
|
2125
2331
|
// result commits nothing and skips it), so this is the newest committed
|
|
2126
2332
|
// state — what `copyMessage` puts its note on when other actions landed
|
|
@@ -2387,6 +2593,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2387
2593
|
// A fresh conversation is a turn boundary too: a tick parked on the OLD
|
|
2388
2594
|
// conversation's transcript must not reconcile it over the new greeting.
|
|
2389
2595
|
turnSeq += 1;
|
|
2596
|
+
// A fresh chat drops its drafts, the picks included — the same way it
|
|
2597
|
+
// empties the composer. The provider list is kept: it belongs to the
|
|
2598
|
+
// business, not to the conversation.
|
|
2599
|
+
mentionSeq += 1;
|
|
2600
|
+
setMention({ ...mentionModel, open: false, provider: null, query: "", items: [], itemsLoading: false,
|
|
2601
|
+
itemsFailed: "", picks: [] });
|
|
2390
2602
|
// ➕ discards the draft (`input: ""` below), and the photos are part of the
|
|
2391
2603
|
// draft. An upload still running settles into nothing: its chip is gone.
|
|
2392
2604
|
chipsSeeded = true;
|
|
@@ -2459,7 +2671,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2459
2671
|
// reply go unnoticed and pinned the ladder fast forever. Safe to return
|
|
2460
2672
|
// `s` here: the guard above proved this tick is still current.
|
|
2461
2673
|
if (!raw.length) return s;
|
|
2462
|
-
const { messages, pending } = reconcile(server, s._pending, s._pendingAfter, you(), s._pendingId, s._pendingAttach ?? []);
|
|
2674
|
+
const { messages, pending } = reconcile(server, s._pending, s._pendingAfter, you(), s._pendingId, s._pendingAttach ?? [], s._pendingRefs);
|
|
2463
2675
|
// …but still never PAINT nothing over something. `messages` is only
|
|
2464
2676
|
// adopted when it has content: a transcript that renders empty leaves
|
|
2465
2677
|
// the screen as it was (the greeting, or the last good turn) while the
|
|
@@ -2563,6 +2775,97 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2563
2775
|
};
|
|
2564
2776
|
},
|
|
2565
2777
|
|
|
2778
|
+
// --- Mentions -----------------------------------------------------------
|
|
2779
|
+
//
|
|
2780
|
+
// Every one of these edits the mention MODEL and returns a state for
|
|
2781
|
+
// `gated` to paint it onto — see `mentionModel` for why the model lives
|
|
2782
|
+
// outside the reducer state.
|
|
2783
|
+
|
|
2784
|
+
mentionOpen: async (s) => {
|
|
2785
|
+
mentionSite();
|
|
2786
|
+
const m = mentionModel;
|
|
2787
|
+
mentionSeq += 1;
|
|
2788
|
+
const atCategories = { ...m, open: true, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" };
|
|
2789
|
+
// Listed once per business. A failed list is asked again: the failure
|
|
2790
|
+
// may have been the network, and the next open is the retry.
|
|
2791
|
+
if (m.providers !== null) { setMention(atCategories); return s; }
|
|
2792
|
+
if (m.providersLoading) { setMention(atCategories); return s; }
|
|
2793
|
+
const site = m.site;
|
|
2794
|
+
setMention({ ...atCategories, providersLoading: true, providersFailed: "" });
|
|
2795
|
+
let providers: MentionProvider[] | null = null;
|
|
2796
|
+
let failed = "";
|
|
2797
|
+
try {
|
|
2798
|
+
providers = allowlisted(providersOf(await listInstalledProviders()));
|
|
2799
|
+
} catch (e) {
|
|
2800
|
+
failed = mentionFailure(e, mentionLabels().providersFailed);
|
|
2801
|
+
}
|
|
2802
|
+
// A switch during the request already reset the model for the new
|
|
2803
|
+
// business; this answer belongs to the old one.
|
|
2804
|
+
if (mentionModel.site !== site) return undefined;
|
|
2805
|
+
setMention({ ...mentionModel, providers, providersLoading: false, providersFailed: failed });
|
|
2806
|
+
return committed ?? s;
|
|
2807
|
+
},
|
|
2808
|
+
|
|
2809
|
+
mentionClose: (s) => {
|
|
2810
|
+
if (!mentionModel.open) return s;
|
|
2811
|
+
mentionSeq += 1;
|
|
2812
|
+
setMention({ ...mentionModel, open: false, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" });
|
|
2813
|
+
return s;
|
|
2814
|
+
},
|
|
2815
|
+
|
|
2816
|
+
mentionBack: (s) => {
|
|
2817
|
+
if (!mentionModel.provider) return s;
|
|
2818
|
+
mentionSeq += 1;
|
|
2819
|
+
setMention({ ...mentionModel, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" });
|
|
2820
|
+
return s;
|
|
2821
|
+
},
|
|
2822
|
+
|
|
2823
|
+
mentionProvider: async (s, arg) => {
|
|
2824
|
+
mentionSite();
|
|
2825
|
+
const key = mentionArg(arg);
|
|
2826
|
+
const provider = mentionModel.open ? (mentionModel.providers ?? []).find((p) => p.key === key) : undefined;
|
|
2827
|
+
if (!provider) return s;
|
|
2828
|
+
const seq = ++mentionSeq;
|
|
2829
|
+
setMention({ ...mentionModel, provider, query: "", items: [], itemsLoading: true, itemsFailed: "" });
|
|
2830
|
+
return fetchItems(s, provider, "", seq);
|
|
2831
|
+
},
|
|
2832
|
+
|
|
2833
|
+
mentionQuery: async (s, arg) => {
|
|
2834
|
+
const provider = mentionModel.provider;
|
|
2835
|
+
if (!mentionModel.open || !provider) return s;
|
|
2836
|
+
// Trimmed: a search, where surrounding space is noise. Unlike the
|
|
2837
|
+
// composer, nothing is lost — the field keeps what was typed.
|
|
2838
|
+
const q = typeof arg === "string" ? arg.trim() : firstString(arg?.value, arg?.text);
|
|
2839
|
+
// The same search again is only worth re-asking when it failed.
|
|
2840
|
+
if (q === mentionModel.query && !mentionModel.itemsFailed && !mentionModel.itemsLoading) return s;
|
|
2841
|
+
const seq = ++mentionSeq;
|
|
2842
|
+
setMention({ ...mentionModel, query: q, itemsLoading: true, itemsFailed: "" });
|
|
2843
|
+
return fetchItems(s, provider, q, seq);
|
|
2844
|
+
},
|
|
2845
|
+
|
|
2846
|
+
mentionPick: (s, arg) => {
|
|
2847
|
+
const provider = mentionModel.provider;
|
|
2848
|
+
const itemId = mentionArg(arg);
|
|
2849
|
+
const item = provider ? mentionModel.items.find((i) => i.itemId === itemId) : undefined;
|
|
2850
|
+
if (!provider || !item) return s;
|
|
2851
|
+
const already = mentionModel.picks.some((p) => p.providerKey === provider.key && p.itemId === item.itemId);
|
|
2852
|
+
const picks = already ? mentionModel.picks : [...mentionModel.picks,
|
|
2853
|
+
{ id: `m${++pickSeq}`, providerKey: provider.key, itemId: item.itemId, label: item.title, category: provider.label }];
|
|
2854
|
+
// A pick closes the picker: the owner came to point at one thing, and
|
|
2855
|
+
// the chip above the composer is where they now look.
|
|
2856
|
+
mentionSeq += 1;
|
|
2857
|
+
setMention({ ...mentionModel, picks, open: false, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" });
|
|
2858
|
+
return s;
|
|
2859
|
+
},
|
|
2860
|
+
|
|
2861
|
+
mentionRemove: (s, arg) => {
|
|
2862
|
+
const id = mentionArg(arg);
|
|
2863
|
+
const picks = mentionModel.picks.filter((p) => p.id !== id);
|
|
2864
|
+
if (picks.length === mentionModel.picks.length) return s;
|
|
2865
|
+
setMention({ ...mentionModel, picks });
|
|
2866
|
+
return s;
|
|
2867
|
+
},
|
|
2868
|
+
|
|
2566
2869
|
widgetToggle: (s, arg) => {
|
|
2567
2870
|
const value = optionValue(arg);
|
|
2568
2871
|
if (!value) return s;
|
|
@@ -2900,7 +3203,9 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2900
3203
|
try {
|
|
2901
3204
|
// `send` directly: it is the one send path, and `submit` with no
|
|
2902
3205
|
// argument would read the composer instead of sending the retry.
|
|
2903
|
-
|
|
3206
|
+
// Without the picks: they are a draft for the owner's NEXT message,
|
|
3207
|
+
// not part of asking for this answer again.
|
|
3208
|
+
return await send(s, REGENERATE_TEXT, [], false);
|
|
2904
3209
|
} finally {
|
|
2905
3210
|
regenerating = false;
|
|
2906
3211
|
}
|
|
@@ -2930,6 +3235,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2930
3235
|
widgetRespond: (s, arg) => impl.widgetRespond(s, arg).then(gated),
|
|
2931
3236
|
widgetPrimary: (s) => impl.widgetPrimary(s).then(gated),
|
|
2932
3237
|
widgetSecondary: (s) => impl.widgetSecondary(s).then(gated),
|
|
3238
|
+
mentionOpen: (s) => impl.mentionOpen(s).then((r) => (r ? gated(r) : r)),
|
|
3239
|
+
mentionClose: (s) => gated(impl.mentionClose(s)),
|
|
3240
|
+
mentionBack: (s) => gated(impl.mentionBack(s)),
|
|
3241
|
+
mentionProvider: (s, arg) => impl.mentionProvider(s, arg).then((r) => (r ? gated(r) : r)),
|
|
3242
|
+
mentionQuery: (s, arg) => impl.mentionQuery(s, arg).then((r) => (r ? gated(r) : r)),
|
|
3243
|
+
mentionPick: (s, arg) => gated(impl.mentionPick(s, arg)),
|
|
3244
|
+
mentionRemove: (s, arg) => gated(impl.mentionRemove(s, arg)),
|
|
2933
3245
|
attach: (s, arg) => impl.attach(s, arg).then(gated),
|
|
2934
3246
|
attachmentSettled: (s, arg) => gated(impl.attachmentSettled(s, arg)),
|
|
2935
3247
|
removeAttachment: (s, arg) => gated(impl.removeAttachment(s, arg)),
|