@usereq/widget 0.1.6 → 0.2.0-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usereq/widget",
3
- "version": "0.1.6",
3
+ "version": "0.2.0-rc.0",
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": "latest",
79
+ "@usereq/ui": "^0.2.0",
80
80
  "lucide-react": "^0.553.0",
81
81
  "motion": "^12.23.24",
82
82
  "react": "^19.2.0",
@@ -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
- import {
3
- ChatWidgetBubble,
4
- ChatWidgetBox,
5
- ChatWidgetChatBar,
6
- ChatWidgetMessenger,
7
- } from "../chat-widget";
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 type { WidgetTriggerRule } from "../shared/widget-config";
17
- import { getPendingStopConfirmationId } from "../shared/stop-confirmation";
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 = "idle" | "booting" | "ready" | "sending" | "error";
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 VARIANT_COMPONENTS = {
58
- default: ChatWidgetBubble,
59
- box: ChatWidgetBox,
60
- chatbar: ChatWidgetChatBar,
61
- } as const;
62
-
63
- const PRESET_COMPONENTS = {
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
- <Component
139
- agentName={widgetTitle}
140
- agentAvatarUrl={resolvedAvatarUrl}
141
- appearance={appearance}
142
- spotlightMessages={spotlights}
143
- messages={messages}
144
- status={status}
145
- errorMessage={errorMessage}
146
- emptyAction={emptyAction}
147
- sendDisabled={sendDisabledResolved}
148
- onSendMessage={onSendMessage}
149
- open={open}
150
- onOpenChange={onOpenChange}
151
- onStopConversation={onStopConversation}
152
- stopDisabled={stopDisabledResolved}
153
- onConfirmationDecision={onConfirmationDecision}
154
- onLauncherClick={onLauncherClick}
155
- onLinkClick={onLinkClick}
156
- portalContainer={portalContainer}
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, { left: "50%", top: "24px", transform: "translateX(-50%)" });
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, { left: "50%", bottom: "24px", transform: "translateX(-50%)" });
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" });