@usereq/widget 0.2.24 → 1.0.0-experimental.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/README.md +35 -145
- package/dist/embed.js +38 -0
- package/dist/embed.js.map +1 -0
- package/dist/embed.mjs +2839 -0
- package/dist/embed.mjs.map +1 -0
- package/package.json +35 -81
- package/dist/widget.js +0 -255
- package/src/chat-widget/chat-widget-appearance.ts +0 -55
- package/src/chat-widget/message-reveal.ts +0 -52
- package/src/chat-widget/stop-confirmation.ts +0 -288
- package/src/custom-element/agent-widget-element.tsx +0 -676
- package/src/embed.ts +0 -8
- package/src/index.ts +0 -9
- package/src/register.ts +0 -15
- package/src/renderer/index.ts +0 -6
- package/src/renderer/widget-runtime.tsx +0 -385
- package/src/runtime/api-origin.ts +0 -29
- package/src/runtime/api.ts +0 -123
- package/src/runtime/bootstrap.ts +0 -366
- package/src/runtime/debug.ts +0 -104
- package/src/runtime/messages.ts +0 -12
- package/src/runtime/session-storage.ts +0 -32
- package/src/runtime/trigger-rule.ts +0 -182
- package/src/shared/shadow-css.ts +0 -15
- package/src/shared/shadow-theme.ts +0 -115
- package/src/shared/stop-confirmation.ts +0 -20
- package/src/shared/widget-config.ts +0 -171
- package/src/styles/widget.css.ts +0 -83
- package/src/types.ts +0 -98
- package/src/vite-env.d.ts +0 -4
|
@@ -1,676 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_WIDGET_APPEARANCE,
|
|
5
|
-
DEFAULT_WIDGET_PRESET,
|
|
6
|
-
normalizeWidgetPlacement,
|
|
7
|
-
normalizeWidgetPreset,
|
|
8
|
-
normalizeWidgetVariant,
|
|
9
|
-
type WidgetAppearance,
|
|
10
|
-
type WidgetMessage,
|
|
11
|
-
type WidgetPlacement,
|
|
12
|
-
type WidgetPreset,
|
|
13
|
-
type WidgetSessionConfig,
|
|
14
|
-
type WidgetSessionState,
|
|
15
|
-
type WidgetVariant,
|
|
16
|
-
} from "../types";
|
|
17
|
-
import {
|
|
18
|
-
normalizeWidgetMobileHeight,
|
|
19
|
-
widgetTriggerRuleKey,
|
|
20
|
-
} from "../shared/widget-config";
|
|
21
|
-
import { WidgetRuntime, type WidgetRuntimeStatus } from "../renderer/index";
|
|
22
|
-
import { buildAssistantMessageRevealSteps } from "../chat-widget/message-reveal";
|
|
23
|
-
import { DEFAULT_STOP_CONVERSATION_PROMPT } from "../shared/stop-confirmation";
|
|
24
|
-
import {
|
|
25
|
-
bootstrapWidget,
|
|
26
|
-
resetWidgetSession,
|
|
27
|
-
submitWidgetStopConfirmation,
|
|
28
|
-
submitWidgetMessage,
|
|
29
|
-
} from "../runtime/bootstrap";
|
|
30
|
-
import { getWidgetShadowCss } from "../shared/shadow-css";
|
|
31
|
-
import { WIDGET_SHADOW_THEME_CSS } from "../shared/shadow-theme";
|
|
32
|
-
|
|
33
|
-
type WidgetAppearanceAttributes = Partial<WidgetAppearance>;
|
|
34
|
-
|
|
35
|
-
export class AgentWidgetElement extends HTMLElement {
|
|
36
|
-
static observedAttributes = [
|
|
37
|
-
"agent-id",
|
|
38
|
-
"mode",
|
|
39
|
-
"variant",
|
|
40
|
-
"preset",
|
|
41
|
-
"placement",
|
|
42
|
-
"avatar-orb-color-1",
|
|
43
|
-
"avatar-orb-color-2",
|
|
44
|
-
"action-text",
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
private shadowRootRef: ShadowRoot;
|
|
48
|
-
private hostStyleElement: HTMLStyleElement;
|
|
49
|
-
private shadowStyleElement: HTMLStyleElement;
|
|
50
|
-
private mountElement: HTMLDivElement;
|
|
51
|
-
private root: Root;
|
|
52
|
-
private agentId = "";
|
|
53
|
-
private mode: "embed" | "preview" = "embed";
|
|
54
|
-
private variant: string | null = null;
|
|
55
|
-
private preset: string | null = null;
|
|
56
|
-
private placement: string | null = null;
|
|
57
|
-
private appearanceAttributes: WidgetAppearanceAttributes = {};
|
|
58
|
-
private open = false;
|
|
59
|
-
private status: WidgetRuntimeStatus = "idle";
|
|
60
|
-
private sessionConfig: WidgetSessionConfig | null = null;
|
|
61
|
-
private sessionState: WidgetSessionState | null = null;
|
|
62
|
-
private messages: WidgetMessage[] = [];
|
|
63
|
-
private widgetTitle = "Assistant";
|
|
64
|
-
private welcomeMessage: string | null = null;
|
|
65
|
-
private errorMessage: string | null = null;
|
|
66
|
-
private bootstrapPromise: Promise<void> | null = null;
|
|
67
|
-
private bootstrapVersion = 0;
|
|
68
|
-
private sendVersion = 0;
|
|
69
|
-
private confirmationVersion = 0;
|
|
70
|
-
private bootstrapPhase: "idle" | "booting" | "ready" | "blocked" = "idle";
|
|
71
|
-
// Back-to-back send support. The composer stays enabled while the agent
|
|
72
|
-
// is replying; new submissions land here and are drained sequentially by
|
|
73
|
-
// `sendMessage`. Each entry carries the optimistic bubble id so failures
|
|
74
|
-
// can yank exactly the right row.
|
|
75
|
-
private pendingQueue: Array<{ content: string; optimisticId: string }> = [];
|
|
76
|
-
private draining = false;
|
|
77
|
-
|
|
78
|
-
constructor() {
|
|
79
|
-
super();
|
|
80
|
-
this.shadowRootRef = this.attachShadow({ mode: "open" });
|
|
81
|
-
this.hostStyleElement = document.createElement("style");
|
|
82
|
-
this.hostStyleElement.textContent = `
|
|
83
|
-
${WIDGET_SHADOW_THEME_CSS}
|
|
84
|
-
|
|
85
|
-
:host {
|
|
86
|
-
display: block;
|
|
87
|
-
box-sizing: border-box;
|
|
88
|
-
width: 100%;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
:host([mode="preview"]) {
|
|
92
|
-
position: relative;
|
|
93
|
-
min-height: 720px;
|
|
94
|
-
}
|
|
95
|
-
`;
|
|
96
|
-
this.shadowStyleElement = document.createElement("style");
|
|
97
|
-
this.shadowStyleElement.textContent = getWidgetShadowCss() ?? "";
|
|
98
|
-
this.mountElement = document.createElement("div");
|
|
99
|
-
this.mountElement.style.display = "block";
|
|
100
|
-
this.mountElement.style.width = "100%";
|
|
101
|
-
this.mountElement.style.height = "100%";
|
|
102
|
-
this.shadowRootRef.append(
|
|
103
|
-
this.hostStyleElement,
|
|
104
|
-
this.shadowStyleElement,
|
|
105
|
-
this.mountElement,
|
|
106
|
-
);
|
|
107
|
-
this.root = createRoot(this.mountElement);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
connectedCallback() {
|
|
111
|
-
this.syncAttributes();
|
|
112
|
-
this.renderWidget();
|
|
113
|
-
if (this.agentId) {
|
|
114
|
-
void this.ensureBootstrap();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
disconnectedCallback() {
|
|
119
|
-
this.bootstrapPromise = null;
|
|
120
|
-
this.root.unmount();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
attributeChangedCallback(
|
|
124
|
-
name?: string,
|
|
125
|
-
oldValue?: string | null,
|
|
126
|
-
newValue?: string | null,
|
|
127
|
-
) {
|
|
128
|
-
this.syncAttributes();
|
|
129
|
-
if (name === "agent-id" && oldValue !== newValue) {
|
|
130
|
-
this.clearAgentState();
|
|
131
|
-
}
|
|
132
|
-
this.renderWidget();
|
|
133
|
-
if (name === "agent-id" && this.agentId) {
|
|
134
|
-
void this.ensureBootstrap();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private syncAttributes() {
|
|
139
|
-
this.agentId = this.getAttribute("agent-id")?.trim() ?? "";
|
|
140
|
-
this.mode = normalizeWidgetMode(this.getAttribute("mode"));
|
|
141
|
-
this.variant = this.getAttribute("variant");
|
|
142
|
-
this.preset = this.getAttribute("preset");
|
|
143
|
-
this.placement = this.getAttribute("placement");
|
|
144
|
-
this.appearanceAttributes = parseAppearanceAttributes(this);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private getResolvedPlacement(): WidgetPlacement {
|
|
148
|
-
return normalizeWidgetPlacement(
|
|
149
|
-
this.placement ?? this.sessionConfig?.widgetConfig.widgetPlacement ?? "bottom-right",
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private getResolvedVariant(): WidgetVariant {
|
|
154
|
-
return normalizeWidgetVariant(
|
|
155
|
-
this.variant ?? this.sessionConfig?.widgetConfig.widgetVariant ?? "chatbar",
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private getResolvedPreset(): WidgetPreset {
|
|
160
|
-
return normalizeWidgetPreset(
|
|
161
|
-
this.preset ??
|
|
162
|
-
this.sessionConfig?.widgetConfig.widgetPreset ??
|
|
163
|
-
DEFAULT_WIDGET_PRESET,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private getResolvedAppearance(): WidgetAppearance {
|
|
168
|
-
const base =
|
|
169
|
-
this.sessionConfig?.widgetConfig.widgetAppearance ?? DEFAULT_WIDGET_APPEARANCE;
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
avatarOrbColor1:
|
|
173
|
-
this.appearanceAttributes.avatarOrbColor1?.trim() ||
|
|
174
|
-
base.avatarOrbColor1,
|
|
175
|
-
avatarOrbColor2:
|
|
176
|
-
this.appearanceAttributes.avatarOrbColor2?.trim() ||
|
|
177
|
-
base.avatarOrbColor2,
|
|
178
|
-
actionText:
|
|
179
|
-
this.appearanceAttributes.actionText?.trim() || base.actionText,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private getWidgetTitle(): string {
|
|
184
|
-
return this.sessionConfig?.widgetConfig.name ?? this.widgetTitle;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private getAgentAvatarUrl(): string | null {
|
|
188
|
-
return this.sessionConfig?.widgetConfig.agentAvatarUrl ?? null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private getWelcomeMessage(): string | null {
|
|
192
|
-
return this.sessionConfig?.widgetConfig.welcomeMessage ?? this.welcomeMessage;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private getSpotlights(): string[] {
|
|
196
|
-
return this.sessionConfig?.widgetConfig.spotlightMessages ?? [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private getCtaLink(): string | null {
|
|
200
|
-
return this.sessionConfig?.widgetConfig.ctaLink ?? null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private clearAgentState() {
|
|
204
|
-
this.sendVersion += 1;
|
|
205
|
-
this.confirmationVersion += 1;
|
|
206
|
-
this.bootstrapVersion += 1;
|
|
207
|
-
this.bootstrapPhase = "idle";
|
|
208
|
-
this.bootstrapPromise = null;
|
|
209
|
-
this.sessionConfig = null;
|
|
210
|
-
this.sessionState = null;
|
|
211
|
-
this.messages = [];
|
|
212
|
-
this.widgetTitle = "Assistant";
|
|
213
|
-
this.welcomeMessage = null;
|
|
214
|
-
this.errorMessage = null;
|
|
215
|
-
this.status = "idle";
|
|
216
|
-
// Bumping sendVersion above invalidates the in-flight drain; reset
|
|
217
|
-
// the queue + flag so the next session starts fresh.
|
|
218
|
-
this.pendingQueue = [];
|
|
219
|
-
this.draining = false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private renderWidget() {
|
|
223
|
-
if (
|
|
224
|
-
this.bootstrapPhase !== "ready" ||
|
|
225
|
-
this.sessionConfig == null ||
|
|
226
|
-
this.sessionState == null
|
|
227
|
-
) {
|
|
228
|
-
this.root.render(null);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const appearance = this.getResolvedAppearance();
|
|
233
|
-
const placement = this.getResolvedPlacement();
|
|
234
|
-
const variant = this.getResolvedVariant();
|
|
235
|
-
const preset = this.getResolvedPreset();
|
|
236
|
-
const behavior = this.sessionConfig?.widgetConfig.widgetBehavior;
|
|
237
|
-
const alwaysOpen = behavior?.alwaysOpen === true;
|
|
238
|
-
|
|
239
|
-
this.root.render(
|
|
240
|
-
React.createElement(WidgetRuntime, {
|
|
241
|
-
key: `${this.agentId}:${widgetTriggerRuleKey(
|
|
242
|
-
this.sessionConfig?.widgetConfig.triggerRule,
|
|
243
|
-
)}`,
|
|
244
|
-
mode: this.mode,
|
|
245
|
-
agentId: this.agentId,
|
|
246
|
-
variant,
|
|
247
|
-
preset,
|
|
248
|
-
placement,
|
|
249
|
-
appearance,
|
|
250
|
-
agentAvatarUrl: this.getAgentAvatarUrl(),
|
|
251
|
-
widgetTitle: this.getWidgetTitle(),
|
|
252
|
-
welcomeMessage: this.getWelcomeMessage(),
|
|
253
|
-
spotlights: this.getSpotlights(),
|
|
254
|
-
ctaLink: this.getCtaLink(),
|
|
255
|
-
mobileHeight: normalizeWidgetMobileHeight(
|
|
256
|
-
this.sessionConfig?.widgetConfig.mobileHeight,
|
|
257
|
-
),
|
|
258
|
-
open: this.open,
|
|
259
|
-
alwaysOpen,
|
|
260
|
-
status: this.status,
|
|
261
|
-
messages: this.messages,
|
|
262
|
-
errorMessage: this.errorMessage,
|
|
263
|
-
triggerRule: this.sessionConfig?.widgetConfig.triggerRule ?? null,
|
|
264
|
-
// Lazy session: no "Start a chat" gate. The composer is live from
|
|
265
|
-
// the moment the panel opens; the conversation is created on the
|
|
266
|
-
// server the first time the user actually sends something.
|
|
267
|
-
emptyAction: undefined,
|
|
268
|
-
onStopConversation:
|
|
269
|
-
this.sessionState?.conversationId != null
|
|
270
|
-
? () => void this.handleStopConversation()
|
|
271
|
-
: undefined,
|
|
272
|
-
onOpenChange: (nextOpen: boolean) => void this.handleOpenChange(nextOpen),
|
|
273
|
-
onSendMessage: (content: string) => void this.sendMessage(content),
|
|
274
|
-
onConfirmationDecision: (confirmationId: string, accepted: boolean) =>
|
|
275
|
-
void this.handleConfirmationDecision(confirmationId, accepted),
|
|
276
|
-
// Portal directly into the shadow root — matches the legacy
|
|
277
|
-
// `agents-monorepo` widget exactly. ShadowRoot is a DocumentFragment,
|
|
278
|
-
// which Radix Dialog accepts as `container`. The earlier `portalElement`
|
|
279
|
-
// wrapper added a `pointer-events: none` ancestor that interfered with
|
|
280
|
-
// the Dialog overlay/content's own event handling.
|
|
281
|
-
portalContainer: this.shadowRootRef,
|
|
282
|
-
}),
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private async ensureBootstrap() {
|
|
287
|
-
if (this.bootstrapPromise) {
|
|
288
|
-
return this.bootstrapPromise;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const runVersion = ++this.bootstrapVersion;
|
|
292
|
-
this.bootstrapPromise = (async () => {
|
|
293
|
-
if (!this.agentId) {
|
|
294
|
-
this.status = "error";
|
|
295
|
-
this.errorMessage = "Missing agent id";
|
|
296
|
-
this.renderWidget();
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this.bootstrapPhase = "booting";
|
|
301
|
-
this.status = "booting";
|
|
302
|
-
this.errorMessage = null;
|
|
303
|
-
this.renderWidget();
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
const state = await bootstrapWidget({
|
|
307
|
-
agentId: this.agentId,
|
|
308
|
-
pageUrl: window.location.href,
|
|
309
|
-
referrer: document.referrer || undefined,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (runVersion !== this.bootstrapVersion) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
this.sessionConfig = {
|
|
317
|
-
sessionToken: state.session.sessionToken,
|
|
318
|
-
expiresAt: state.session.expiresAt,
|
|
319
|
-
widgetConfig: state.session.widgetConfig,
|
|
320
|
-
};
|
|
321
|
-
this.sessionState = state.session;
|
|
322
|
-
this.messages = state.messages;
|
|
323
|
-
this.widgetTitle = state.session.widgetConfig.name;
|
|
324
|
-
this.welcomeMessage = state.session.widgetConfig.welcomeMessage;
|
|
325
|
-
this.status = "ready";
|
|
326
|
-
this.bootstrapPhase = "ready";
|
|
327
|
-
// Lazy session: when the visitor doesn't already have a running
|
|
328
|
-
// conversation, seed a client-side welcome bubble from the boot
|
|
329
|
-
// config so the panel doesn't open on a blank empty state. The
|
|
330
|
-
// real conversation is only created server-side once they type.
|
|
331
|
-
this.seedClientWelcomeIfNeeded();
|
|
332
|
-
// Apply the agent's configured open-state on mount. `alwaysOpen`
|
|
333
|
-
// wins (the panel is pinned regardless), and `defaultOpen` opens it
|
|
334
|
-
// once if the visitor hasn't already interacted with the launcher.
|
|
335
|
-
const behavior = state.session.widgetConfig.widgetBehavior;
|
|
336
|
-
if (behavior?.alwaysOpen || behavior?.defaultOpen) {
|
|
337
|
-
this.open = true;
|
|
338
|
-
}
|
|
339
|
-
this.renderWidget();
|
|
340
|
-
} catch (error) {
|
|
341
|
-
if (runVersion !== this.bootstrapVersion) {
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
this.status = "error";
|
|
346
|
-
this.bootstrapPhase = "blocked";
|
|
347
|
-
this.errorMessage =
|
|
348
|
-
error instanceof Error ? error.message : "Failed to start widget";
|
|
349
|
-
this.renderWidget();
|
|
350
|
-
}
|
|
351
|
-
})().finally(() => {
|
|
352
|
-
if (runVersion === this.bootstrapVersion) {
|
|
353
|
-
this.bootstrapPromise = null;
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
return this.bootstrapPromise;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Entry point for user-typed sends. Drops the optimistic bubble in
|
|
362
|
-
* immediately so the user sees their own backlog, then either kicks off
|
|
363
|
-
* the drain loop (if idle) or appends to `pendingQueue` (if a prior send
|
|
364
|
-
* is still mid-stream). The drain loop runs at most one POST in flight
|
|
365
|
-
* at a time — the server-side session memory expects sequential turns,
|
|
366
|
-
* and the AI engineer confirmed parallel requests are *possible* but
|
|
367
|
-
* the answers would interleave out-of-order on the wire.
|
|
368
|
-
*/
|
|
369
|
-
private async sendMessage(content: string) {
|
|
370
|
-
const trimmedContent = content.trim();
|
|
371
|
-
// Lazy session: no `conversationId` check here. If the visitor is
|
|
372
|
-
// sending their very first message, `submitWidgetMessage` will call
|
|
373
|
-
// POST /conversations/public/start under the hood before posting.
|
|
374
|
-
if (!trimmedContent || !this.sessionState) return;
|
|
375
|
-
|
|
376
|
-
const optimisticMessageId = `optimistic:${Date.now()}:${Math.random()
|
|
377
|
-
.toString(36)
|
|
378
|
-
.slice(2, 8)}`;
|
|
379
|
-
const optimisticMessage: WidgetMessage = {
|
|
380
|
-
id: optimisticMessageId,
|
|
381
|
-
// Empty when no conversation exists yet — the real id lands on the
|
|
382
|
-
// server row we swap in from `submitWidgetMessage`'s response.
|
|
383
|
-
conversationId: this.sessionState.conversationId ?? "",
|
|
384
|
-
role: "user",
|
|
385
|
-
content: trimmedContent,
|
|
386
|
-
createdAt: new Date().toISOString(),
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
this.errorMessage = null;
|
|
390
|
-
this.messages = [...this.messages, optimisticMessage];
|
|
391
|
-
this.renderWidget();
|
|
392
|
-
|
|
393
|
-
// Drain already running → just join the queue. The drainer will
|
|
394
|
-
// consume this entry as soon as the in-flight POST + reveal settle.
|
|
395
|
-
if (this.draining) {
|
|
396
|
-
this.pendingQueue.push({
|
|
397
|
-
content: trimmedContent,
|
|
398
|
-
optimisticId: optimisticMessageId,
|
|
399
|
-
});
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
this.draining = true;
|
|
404
|
-
this.status = "sending";
|
|
405
|
-
this.renderWidget();
|
|
406
|
-
|
|
407
|
-
const runVersion = this.sendVersion;
|
|
408
|
-
let succeeded = true;
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
await this.processSend(trimmedContent, optimisticMessageId, runVersion);
|
|
412
|
-
while (this.pendingQueue.length > 0 && runVersion === this.sendVersion) {
|
|
413
|
-
const next = this.pendingQueue.shift()!;
|
|
414
|
-
await this.processSend(next.content, next.optimisticId, runVersion);
|
|
415
|
-
}
|
|
416
|
-
} catch {
|
|
417
|
-
succeeded = false;
|
|
418
|
-
if (runVersion === this.sendVersion) {
|
|
419
|
-
// Drop the rest of the backlog — each queued message expects the
|
|
420
|
-
// server to have already consumed the prior turn, which didn't
|
|
421
|
-
// happen. Leaving them queued would silently fire mid-failure.
|
|
422
|
-
const failedIds = new Set(
|
|
423
|
-
this.pendingQueue.map((entry) => entry.optimisticId),
|
|
424
|
-
);
|
|
425
|
-
this.pendingQueue = [];
|
|
426
|
-
this.messages = this.messages.filter((m) => !failedIds.has(m.id));
|
|
427
|
-
// `processSend` already set status="error" + errorMessage; render
|
|
428
|
-
// once more so the cleared queue is visible.
|
|
429
|
-
this.renderWidget();
|
|
430
|
-
}
|
|
431
|
-
} finally {
|
|
432
|
-
this.draining = false;
|
|
433
|
-
if (succeeded && runVersion === this.sendVersion) {
|
|
434
|
-
this.status = "ready";
|
|
435
|
-
this.renderWidget();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* One round-trip: POST → swap optimistic → stagger assistant reveal.
|
|
442
|
-
* Throws on failure so the outer drainer can clear the backlog. The
|
|
443
|
-
* `runVersion` guard short-circuits late state writes if a hard reset
|
|
444
|
-
* (agent-id change, conversation reset) bumped `sendVersion` mid-drain.
|
|
445
|
-
*/
|
|
446
|
-
private async processSend(
|
|
447
|
-
content: string,
|
|
448
|
-
optimisticId: string,
|
|
449
|
-
runVersion: number,
|
|
450
|
-
) {
|
|
451
|
-
try {
|
|
452
|
-
const result = await submitWidgetMessage({
|
|
453
|
-
agentId: this.agentId,
|
|
454
|
-
content,
|
|
455
|
-
});
|
|
456
|
-
if (runVersion !== this.sendVersion) return;
|
|
457
|
-
this.sessionState = result.session;
|
|
458
|
-
|
|
459
|
-
// Lazy-start swap: when this was the first send, `submitWidgetMessage`
|
|
460
|
-
// created the conversation server-side and the server inserted its
|
|
461
|
-
// own welcome message row. Replace the placeholder client-welcome
|
|
462
|
-
// bubble with the real server row so ids/timestamps match the DB.
|
|
463
|
-
if (result.welcomeMessage) {
|
|
464
|
-
const clientWelcomeId = this.getClientWelcomeId();
|
|
465
|
-
this.messages = this.messages.map((m) =>
|
|
466
|
-
m.id === clientWelcomeId
|
|
467
|
-
? {
|
|
468
|
-
id: result.welcomeMessage!.id,
|
|
469
|
-
conversationId: result.conversationId,
|
|
470
|
-
role: "assistant",
|
|
471
|
-
content: result.welcomeMessage!.content,
|
|
472
|
-
createdAt: result.welcomeMessage!.createdAt,
|
|
473
|
-
}
|
|
474
|
-
: m,
|
|
475
|
-
);
|
|
476
|
-
this.renderWidget();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const revealSteps = buildAssistantMessageRevealSteps(
|
|
480
|
-
result.assistantMessages.map((message) => ({
|
|
481
|
-
id: message.id,
|
|
482
|
-
content: message.content,
|
|
483
|
-
createdAt: message.createdAt,
|
|
484
|
-
})),
|
|
485
|
-
{
|
|
486
|
-
initialDelayMs: 0,
|
|
487
|
-
typingHoldMs: 1260,
|
|
488
|
-
interMessageDelayMs: 1520,
|
|
489
|
-
interMessageDelayJitterMs: 100,
|
|
490
|
-
},
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
for (const step of revealSteps) {
|
|
494
|
-
await new Promise((resolve) =>
|
|
495
|
-
window.setTimeout(resolve, step.delayMs),
|
|
496
|
-
);
|
|
497
|
-
if (runVersion !== this.sendVersion) return;
|
|
498
|
-
const dto = step.message;
|
|
499
|
-
this.messages = [
|
|
500
|
-
...this.messages,
|
|
501
|
-
{
|
|
502
|
-
id: dto.id,
|
|
503
|
-
conversationId: result.conversationId,
|
|
504
|
-
role: "assistant",
|
|
505
|
-
content: dto.content,
|
|
506
|
-
createdAt: dto.createdAt,
|
|
507
|
-
},
|
|
508
|
-
];
|
|
509
|
-
this.renderWidget();
|
|
510
|
-
}
|
|
511
|
-
} catch (error) {
|
|
512
|
-
if (runVersion !== this.sendVersion) {
|
|
513
|
-
// The hard reset path already cleared state — swallow.
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
this.messages = this.messages.filter((m) => m.id !== optimisticId);
|
|
517
|
-
this.status = "error";
|
|
518
|
-
this.errorMessage =
|
|
519
|
-
error instanceof Error ? error.message : "Failed to send message";
|
|
520
|
-
this.renderWidget();
|
|
521
|
-
throw error;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
private async handleOpenChange(nextOpen: boolean) {
|
|
526
|
-
this.open = nextOpen;
|
|
527
|
-
this.renderWidget();
|
|
528
|
-
|
|
529
|
-
if (this.open && (this.status === "idle" || !this.sessionState)) {
|
|
530
|
-
await this.ensureBootstrap();
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private getClientWelcomeId(): string {
|
|
535
|
-
return `client-welcome:${this.agentId}`;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Lazy session seeding. When bootstrap finishes with no live conversation
|
|
540
|
-
* and no rehydrated transcript, drop a synthetic assistant bubble carrying
|
|
541
|
-
* the boot-config welcome copy into `this.messages`. Skipped when there's
|
|
542
|
-
* already a transcript (rehydrated session) or no welcome copy configured.
|
|
543
|
-
* The bubble's id is stable (`client-welcome:{agentId}`) so `processSend`
|
|
544
|
-
* can swap it for the real server row after the first successful send.
|
|
545
|
-
*/
|
|
546
|
-
private seedClientWelcomeIfNeeded() {
|
|
547
|
-
if (this.messages.length > 0) return;
|
|
548
|
-
if (this.sessionState?.conversationId) return;
|
|
549
|
-
const welcome = this.getWelcomeMessage();
|
|
550
|
-
if (!welcome) return;
|
|
551
|
-
this.messages = [
|
|
552
|
-
{
|
|
553
|
-
id: this.getClientWelcomeId(),
|
|
554
|
-
conversationId: "",
|
|
555
|
-
role: "assistant",
|
|
556
|
-
content: welcome,
|
|
557
|
-
createdAt: new Date().toISOString(),
|
|
558
|
-
},
|
|
559
|
-
];
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
private async handleStopConversation() {
|
|
563
|
-
if (!this.sessionState?.conversationId) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const confirmationId = crypto.randomUUID();
|
|
568
|
-
const runVersion = ++this.confirmationVersion;
|
|
569
|
-
|
|
570
|
-
this.status = "sending";
|
|
571
|
-
this.errorMessage = null;
|
|
572
|
-
this.renderWidget();
|
|
573
|
-
|
|
574
|
-
try {
|
|
575
|
-
const result = await submitWidgetStopConfirmation({
|
|
576
|
-
agentId: this.agentId,
|
|
577
|
-
confirmationId,
|
|
578
|
-
prompt: DEFAULT_STOP_CONVERSATION_PROMPT,
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
if (runVersion !== this.confirmationVersion) {
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
this.sessionState = result.session;
|
|
586
|
-
this.messages = [...this.messages, result.confirmationMessage];
|
|
587
|
-
this.status = "ready";
|
|
588
|
-
this.renderWidget();
|
|
589
|
-
} catch (error) {
|
|
590
|
-
if (runVersion !== this.confirmationVersion) {
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
this.status = "error";
|
|
594
|
-
this.errorMessage =
|
|
595
|
-
error instanceof Error ? error.message : "Failed to stop conversation";
|
|
596
|
-
this.renderWidget();
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
private async handleConfirmationDecision(
|
|
601
|
-
confirmationId: string,
|
|
602
|
-
accepted: boolean,
|
|
603
|
-
) {
|
|
604
|
-
if (!this.sessionState?.conversationId) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const runVersion = ++this.confirmationVersion;
|
|
609
|
-
this.status = "sending";
|
|
610
|
-
this.errorMessage = null;
|
|
611
|
-
this.renderWidget();
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
const result = await submitWidgetStopConfirmation({
|
|
615
|
-
agentId: this.agentId,
|
|
616
|
-
confirmationId,
|
|
617
|
-
decision: accepted ? "accepted" : "rejected",
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
if (runVersion !== this.confirmationVersion) {
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
this.sessionState = result.session;
|
|
625
|
-
this.messages = [...this.messages, result.confirmationMessage];
|
|
626
|
-
this.renderWidget();
|
|
627
|
-
|
|
628
|
-
if (accepted) {
|
|
629
|
-
window.setTimeout(() => {
|
|
630
|
-
if (runVersion !== this.confirmationVersion) {
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
void this.handleReset();
|
|
634
|
-
}, 0);
|
|
635
|
-
} else {
|
|
636
|
-
this.status = "ready";
|
|
637
|
-
this.renderWidget();
|
|
638
|
-
}
|
|
639
|
-
} catch (error) {
|
|
640
|
-
if (runVersion !== this.confirmationVersion) {
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
this.status = "error";
|
|
644
|
-
this.errorMessage =
|
|
645
|
-
error instanceof Error ? error.message : "Failed to update confirmation";
|
|
646
|
-
this.renderWidget();
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
private handleReset = async () => {
|
|
651
|
-
this.sendVersion += 1;
|
|
652
|
-
await resetWidgetSession(this.agentId);
|
|
653
|
-
this.clearAgentState();
|
|
654
|
-
this.open = true;
|
|
655
|
-
this.renderWidget();
|
|
656
|
-
await this.ensureBootstrap();
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function parseAppearanceAttributes(
|
|
661
|
-
element: HTMLElement,
|
|
662
|
-
): WidgetAppearanceAttributes {
|
|
663
|
-
const avatarOrbColor1 = element.getAttribute("avatar-orb-color-1");
|
|
664
|
-
const avatarOrbColor2 = element.getAttribute("avatar-orb-color-2");
|
|
665
|
-
const actionText = element.getAttribute("action-text");
|
|
666
|
-
|
|
667
|
-
return {
|
|
668
|
-
avatarOrbColor1: avatarOrbColor1?.trim() || undefined,
|
|
669
|
-
avatarOrbColor2: avatarOrbColor2?.trim() || undefined,
|
|
670
|
-
actionText: actionText?.trim() || undefined,
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function normalizeWidgetMode(value: string | null): "embed" | "preview" {
|
|
675
|
-
return value === "preview" ? "preview" : "embed";
|
|
676
|
-
}
|
package/src/embed.ts
DELETED
package/src/index.ts
DELETED
package/src/register.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { AgentWidgetElement } from "./custom-element/agent-widget-element";
|
|
2
|
-
|
|
3
|
-
declare global {
|
|
4
|
-
interface HTMLElementTagNameMap {
|
|
5
|
-
"usereq-agent-widget": AgentWidgetElement;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function registerWidgetElement() {
|
|
10
|
-
if (!customElements.get("usereq-agent-widget")) {
|
|
11
|
-
customElements.define("usereq-agent-widget", AgentWidgetElement);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export { AgentWidgetElement };
|