@usereq/widget 0.1.6 → 0.2.0-rc.1
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/widget.js +815 -722
- package/package.json +2 -2
- package/src/chat-widget/index.ts +9 -0
- package/src/renderer/widget-runtime.tsx +203 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usereq/widget",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.0-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@streamdown/mermaid": "^1.0.2",
|
|
79
|
-
"@usereq/ui": "
|
|
79
|
+
"@usereq/ui": "^0.2.1",
|
|
80
80
|
"lucide-react": "^0.553.0",
|
|
81
81
|
"motion": "^12.23.24",
|
|
82
82
|
"react": "^19.2.0",
|
package/src/chat-widget/index.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Since 0.2.0. The widget runtime now renders `ChatWidget` from
|
|
3
|
+
* `@usereq/ui/components/chat/widget` directly. These re-exports remain for
|
|
4
|
+
* external consumers that imported the per-variant components
|
|
5
|
+
* (`ChatWidgetBubble`, `ChatWidgetBox`, `ChatWidgetChatBar`,
|
|
6
|
+
* `ChatWidgetMessenger`) or the conversation/message primitives, and will be
|
|
7
|
+
* removed in 0.3.0. Migrate to `@usereq/ui/components/chat`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
export * from "./components/chat-widget-frame";
|
|
2
11
|
export * from "./components/chat-widget-primitives";
|
|
3
12
|
export * from "./components/confirmation";
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
|
|
3
|
+
import { ChatWidget } from "@usereq/ui/components/chat/widget";
|
|
4
|
+
import type {
|
|
5
|
+
ChatPanelConfirmation,
|
|
6
|
+
ChatPanelMessage,
|
|
7
|
+
ChatPanelStatus,
|
|
8
|
+
} from "@usereq/ui/components/chat/panel";
|
|
9
|
+
import { ChatFullscreenProvider } from "@usereq/ui/hooks/use-chat-fullscreen";
|
|
10
|
+
import type { ChatAppearance } from "@usereq/ui/lib/chat-appearance";
|
|
11
|
+
import type { ChatWidgetConfig } from "@usereq/ui/lib/chat-widget-config";
|
|
12
|
+
|
|
8
13
|
import type { WidgetMessage } from "../types";
|
|
9
14
|
import {
|
|
10
15
|
splitWidgetPlacement,
|
|
@@ -12,16 +17,26 @@ import {
|
|
|
12
17
|
type WidgetPlacement,
|
|
13
18
|
type WidgetPreset,
|
|
14
19
|
type WidgetVariant,
|
|
20
|
+
type WidgetTriggerRule,
|
|
15
21
|
} from "../shared/widget-config";
|
|
16
|
-
import
|
|
17
|
-
|
|
22
|
+
import {
|
|
23
|
+
getPendingStopConfirmationId,
|
|
24
|
+
parseStopConfirmationEnvelope,
|
|
25
|
+
DEFAULT_STOP_CONVERSATION_PROMPT,
|
|
26
|
+
STOP_CONFIRMATION_KIND,
|
|
27
|
+
} from "../shared/stop-confirmation";
|
|
18
28
|
import { widgetCss } from "../styles/widget.css";
|
|
19
29
|
import { useWidgetTriggerGate } from "../runtime/trigger-rule";
|
|
20
30
|
import type { WidgetTriggerRuleType } from "../shared/analytics";
|
|
21
31
|
|
|
22
32
|
export type WidgetRuntimeMode = "embed" | "preview";
|
|
23
33
|
|
|
24
|
-
export type WidgetRuntimeStatus =
|
|
34
|
+
export type WidgetRuntimeStatus =
|
|
35
|
+
| "idle"
|
|
36
|
+
| "booting"
|
|
37
|
+
| "ready"
|
|
38
|
+
| "sending"
|
|
39
|
+
| "error";
|
|
25
40
|
|
|
26
41
|
export type WidgetRuntimeProps = {
|
|
27
42
|
mode: WidgetRuntimeMode;
|
|
@@ -54,15 +69,13 @@ export type WidgetRuntimeProps = {
|
|
|
54
69
|
onLauncherClick?: () => void;
|
|
55
70
|
};
|
|
56
71
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
facebook_messenger: ChatWidgetMessenger,
|
|
65
|
-
} as const;
|
|
72
|
+
const RUNTIME_TO_PANEL_STATUS: Record<WidgetRuntimeStatus, ChatPanelStatus> = {
|
|
73
|
+
idle: "idle",
|
|
74
|
+
booting: "booting",
|
|
75
|
+
ready: "idle",
|
|
76
|
+
sending: "sending",
|
|
77
|
+
error: "error",
|
|
78
|
+
};
|
|
66
79
|
|
|
67
80
|
export function WidgetRuntime({
|
|
68
81
|
mode,
|
|
@@ -73,6 +86,7 @@ export function WidgetRuntime({
|
|
|
73
86
|
appearance,
|
|
74
87
|
agentAvatarUrl,
|
|
75
88
|
widgetTitle,
|
|
89
|
+
welcomeMessage,
|
|
76
90
|
spotlights,
|
|
77
91
|
open,
|
|
78
92
|
status,
|
|
@@ -92,19 +106,6 @@ export function WidgetRuntime({
|
|
|
92
106
|
}: WidgetRuntimeProps) {
|
|
93
107
|
const { ready } = useWidgetTriggerGate(triggerRule);
|
|
94
108
|
const { horizontal, vertical } = splitWidgetPlacement(placement);
|
|
95
|
-
const PresetComponent =
|
|
96
|
-
preset !== "default"
|
|
97
|
-
? PRESET_COMPONENTS[preset as Exclude<WidgetPreset, "default">]
|
|
98
|
-
: undefined;
|
|
99
|
-
const Component =
|
|
100
|
-
PresetComponent ?? VARIANT_COMPONENTS[variant] ?? ChatWidgetBubble;
|
|
101
|
-
const shellStyle = getWidgetShellStyle(mode, placement);
|
|
102
|
-
const resolvedAvatarUrl = agentAvatarUrl?.trim() || undefined;
|
|
103
|
-
const pendingConfirmationId = getPendingStopConfirmationId(messages);
|
|
104
|
-
const confirmationPending = pendingConfirmationId != null;
|
|
105
|
-
const stopDisabledResolved = confirmationPending || status === "booting" || status === "sending";
|
|
106
|
-
const sendDisabledResolved =
|
|
107
|
-
sendDisabled || confirmationPending || status === "booting";
|
|
108
109
|
const viewTrackedRef = React.useRef(false);
|
|
109
110
|
|
|
110
111
|
React.useEffect(() => {
|
|
@@ -115,10 +116,80 @@ export function WidgetRuntime({
|
|
|
115
116
|
onWidgetVisible?.(triggerRule?.triggerRuleType ?? null);
|
|
116
117
|
}, [mode, onWidgetVisible, ready, triggerRule]);
|
|
117
118
|
|
|
119
|
+
const { panelMessages, confirmation } = React.useMemo(
|
|
120
|
+
() => splitConfirmationFromMessages(messages, onConfirmationDecision),
|
|
121
|
+
[messages, onConfirmationDecision],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const pendingConfirmationId = React.useMemo(
|
|
125
|
+
() => getPendingStopConfirmationId(messages),
|
|
126
|
+
[messages],
|
|
127
|
+
);
|
|
128
|
+
const confirmationPending = pendingConfirmationId != null;
|
|
129
|
+
const stopDisabled =
|
|
130
|
+
confirmationPending || status === "booting" || status === "sending";
|
|
131
|
+
const isSendDisabled =
|
|
132
|
+
sendDisabled || confirmationPending || status === "booting";
|
|
133
|
+
|
|
134
|
+
const chatConfig = React.useMemo<ChatWidgetConfig>(
|
|
135
|
+
() => ({
|
|
136
|
+
agentName: widgetTitle,
|
|
137
|
+
agentAvatarUrl,
|
|
138
|
+
variant: mapVariant(variant),
|
|
139
|
+
preset: mapPreset(preset),
|
|
140
|
+
placement,
|
|
141
|
+
appearance: toChatAppearance(appearance),
|
|
142
|
+
welcomeMessage,
|
|
143
|
+
spotlights,
|
|
144
|
+
actionLabel: appearance.actionText || undefined,
|
|
145
|
+
showStopConversation: Boolean(onStopConversation),
|
|
146
|
+
}),
|
|
147
|
+
[
|
|
148
|
+
agentAvatarUrl,
|
|
149
|
+
appearance,
|
|
150
|
+
onStopConversation,
|
|
151
|
+
placement,
|
|
152
|
+
preset,
|
|
153
|
+
spotlights,
|
|
154
|
+
variant,
|
|
155
|
+
welcomeMessage,
|
|
156
|
+
widgetTitle,
|
|
157
|
+
],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleSend = React.useCallback(
|
|
161
|
+
(value: string) => {
|
|
162
|
+
const trimmed = value.trim();
|
|
163
|
+
if (!trimmed || isSendDisabled) return;
|
|
164
|
+
onSendMessage(trimmed);
|
|
165
|
+
},
|
|
166
|
+
[isSendDisabled, onSendMessage],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const handleStop = React.useCallback(() => {
|
|
170
|
+
if (stopDisabled) return;
|
|
171
|
+
onStopConversation?.();
|
|
172
|
+
}, [onStopConversation, stopDisabled]);
|
|
173
|
+
|
|
174
|
+
const wrappedEmptyAction = React.useMemo(
|
|
175
|
+
() =>
|
|
176
|
+
emptyAction
|
|
177
|
+
? {
|
|
178
|
+
label: emptyAction.label,
|
|
179
|
+
disabled: emptyAction.disabled,
|
|
180
|
+
onClick: emptyAction.onClick,
|
|
181
|
+
}
|
|
182
|
+
: undefined,
|
|
183
|
+
[emptyAction],
|
|
184
|
+
);
|
|
185
|
+
|
|
118
186
|
if (!ready) {
|
|
119
187
|
return null;
|
|
120
188
|
}
|
|
121
189
|
|
|
190
|
+
const shellStyle = getWidgetShellStyle(mode, placement);
|
|
191
|
+
const portalEl = isHtmlElement(portalContainer) ? portalContainer : null;
|
|
192
|
+
|
|
122
193
|
return (
|
|
123
194
|
<>
|
|
124
195
|
<style>{widgetCss}</style>
|
|
@@ -135,32 +206,103 @@ export function WidgetRuntime({
|
|
|
135
206
|
className="widget-shell"
|
|
136
207
|
>
|
|
137
208
|
<div style={{ pointerEvents: "auto" }}>
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
/>
|
|
209
|
+
<ChatFullscreenProvider>
|
|
210
|
+
<ChatWidget
|
|
211
|
+
config={chatConfig}
|
|
212
|
+
messages={panelMessages}
|
|
213
|
+
status={RUNTIME_TO_PANEL_STATUS[status]}
|
|
214
|
+
errorMessage={errorMessage}
|
|
215
|
+
open={open}
|
|
216
|
+
onOpenChange={onOpenChange}
|
|
217
|
+
onSendMessage={handleSend}
|
|
218
|
+
onStopConversation={
|
|
219
|
+
onStopConversation ? handleStop : undefined
|
|
220
|
+
}
|
|
221
|
+
onLauncherClick={onLauncherClick}
|
|
222
|
+
onLinkClick={onLinkClick}
|
|
223
|
+
emptyAction={wrappedEmptyAction}
|
|
224
|
+
confirmation={confirmation}
|
|
225
|
+
portalContainer={portalEl}
|
|
226
|
+
/>
|
|
227
|
+
</ChatFullscreenProvider>
|
|
158
228
|
</div>
|
|
159
229
|
</div>
|
|
160
230
|
</>
|
|
161
231
|
);
|
|
162
232
|
}
|
|
163
233
|
|
|
234
|
+
function mapVariant(variant: WidgetVariant): ChatWidgetConfig["variant"] {
|
|
235
|
+
return variant;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function mapPreset(preset: WidgetPreset): ChatWidgetConfig["preset"] {
|
|
239
|
+
return preset;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toChatAppearance(appearance: WidgetAppearance): ChatAppearance {
|
|
243
|
+
return {
|
|
244
|
+
avatarOrbColor1: appearance.avatarOrbColor1 ?? null,
|
|
245
|
+
avatarOrbColor2: appearance.avatarOrbColor2 ?? null,
|
|
246
|
+
actionText: appearance.actionText ?? null,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isHtmlElement(
|
|
251
|
+
node: HTMLElement | DocumentFragment | null | undefined,
|
|
252
|
+
): node is HTMLElement {
|
|
253
|
+
return (
|
|
254
|
+
node != null && typeof node === "object" && "tagName" in (node as object)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Walks the message stream once: peels off stop-confirmation envelopes (they
|
|
260
|
+
* become the `confirmation` slot of ChatWidget) and leaves plain assistant /
|
|
261
|
+
* user messages in place. Only the latest *pending* confirmation surfaces —
|
|
262
|
+
* accepted/rejected envelopes are dropped because the runtime resets the
|
|
263
|
+
* session right after acceptance.
|
|
264
|
+
*/
|
|
265
|
+
function splitConfirmationFromMessages(
|
|
266
|
+
messages: WidgetMessage[],
|
|
267
|
+
onConfirmationDecision:
|
|
268
|
+
| ((id: string, accepted: boolean) => void | Promise<void>)
|
|
269
|
+
| undefined,
|
|
270
|
+
): {
|
|
271
|
+
panelMessages: ChatPanelMessage[];
|
|
272
|
+
confirmation: ChatPanelConfirmation | null;
|
|
273
|
+
} {
|
|
274
|
+
const panelMessages: ChatPanelMessage[] = [];
|
|
275
|
+
let confirmation: ChatPanelConfirmation | null = null;
|
|
276
|
+
|
|
277
|
+
for (const message of messages) {
|
|
278
|
+
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
279
|
+
if (!envelope) {
|
|
280
|
+
panelMessages.push({
|
|
281
|
+
id: message.id,
|
|
282
|
+
role: message.role,
|
|
283
|
+
content: message.content,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (envelope.kind !== STOP_CONFIRMATION_KIND) continue;
|
|
288
|
+
if (envelope.state !== "pending") continue;
|
|
289
|
+
confirmation = {
|
|
290
|
+
id: envelope.confirmationId,
|
|
291
|
+
prompt: envelope.prompt || DEFAULT_STOP_CONVERSATION_PROMPT,
|
|
292
|
+
state: "pending",
|
|
293
|
+
onDecision: onConfirmationDecision
|
|
294
|
+
? (decision) =>
|
|
295
|
+
void onConfirmationDecision(
|
|
296
|
+
envelope.confirmationId,
|
|
297
|
+
decision === "accept",
|
|
298
|
+
)
|
|
299
|
+
: undefined,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { panelMessages, confirmation };
|
|
304
|
+
}
|
|
305
|
+
|
|
164
306
|
function getWidgetShellStyle(
|
|
165
307
|
mode: WidgetRuntimeMode,
|
|
166
308
|
placement: WidgetPlacement,
|
|
@@ -199,7 +341,11 @@ function getWidgetShellStyle(
|
|
|
199
341
|
Object.assign(style, { left: "24px", top: "24px" });
|
|
200
342
|
break;
|
|
201
343
|
case "top-center":
|
|
202
|
-
Object.assign(style, {
|
|
344
|
+
Object.assign(style, {
|
|
345
|
+
left: "50%",
|
|
346
|
+
top: "24px",
|
|
347
|
+
transform: "translateX(-50%)",
|
|
348
|
+
});
|
|
203
349
|
break;
|
|
204
350
|
case "top-right":
|
|
205
351
|
Object.assign(style, { right: "24px", top: "24px" });
|
|
@@ -208,7 +354,11 @@ function getWidgetShellStyle(
|
|
|
208
354
|
Object.assign(style, { left: "24px", bottom: "24px" });
|
|
209
355
|
break;
|
|
210
356
|
case "bottom-center":
|
|
211
|
-
Object.assign(style, {
|
|
357
|
+
Object.assign(style, {
|
|
358
|
+
left: "50%",
|
|
359
|
+
bottom: "24px",
|
|
360
|
+
transform: "translateX(-50%)",
|
|
361
|
+
});
|
|
212
362
|
break;
|
|
213
363
|
default:
|
|
214
364
|
Object.assign(style, { right: "24px", bottom: "24px" });
|