@usereq/widget 0.1.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.
Files changed (39) hide show
  1. package/README.md +166 -0
  2. package/dist/widget.css +2 -0
  3. package/dist/widget.js +3569 -0
  4. package/package.json +98 -0
  5. package/src/chat-widget/chat-widget-appearance.ts +55 -0
  6. package/src/chat-widget/chat-widget-defaults.tsx +45 -0
  7. package/src/chat-widget/components/chat-widget-frame.tsx +56 -0
  8. package/src/chat-widget/components/chat-widget-layout.ts +6 -0
  9. package/src/chat-widget/components/chat-widget-primitives.tsx +720 -0
  10. package/src/chat-widget/components/confirmation.tsx +155 -0
  11. package/src/chat-widget/components/conversation.tsx +110 -0
  12. package/src/chat-widget/components/message.tsx +325 -0
  13. package/src/chat-widget/index.ts +9 -0
  14. package/src/chat-widget/stop-confirmation.ts +288 -0
  15. package/src/chat-widget/styles/chat-widget-box.tsx +258 -0
  16. package/src/chat-widget/styles/chat-widget-bubble.tsx +238 -0
  17. package/src/chat-widget/styles/chat-widget-chatbar.tsx +248 -0
  18. package/src/custom-element/agent-widget-element.tsx +584 -0
  19. package/src/embed.ts +8 -0
  20. package/src/index.ts +10 -0
  21. package/src/register.ts +15 -0
  22. package/src/renderer/index.ts +6 -0
  23. package/src/renderer/widget-runtime.tsx +205 -0
  24. package/src/runtime/analytics.ts +327 -0
  25. package/src/runtime/api-origin.ts +29 -0
  26. package/src/runtime/api.ts +151 -0
  27. package/src/runtime/bootstrap.ts +309 -0
  28. package/src/runtime/messages.ts +12 -0
  29. package/src/runtime/session-storage.ts +32 -0
  30. package/src/runtime/token-renewal.ts +13 -0
  31. package/src/runtime/trigger-rule.ts +182 -0
  32. package/src/shared/analytics.ts +54 -0
  33. package/src/shared/shadow-css.ts +15 -0
  34. package/src/shared/shadow-theme.ts +85 -0
  35. package/src/shared/stop-confirmation.ts +20 -0
  36. package/src/shared/widget-config.ts +104 -0
  37. package/src/styles/widget.css.ts +27 -0
  38. package/src/types.ts +85 -0
  39. package/src/vite-env.d.ts +4 -0
@@ -0,0 +1,238 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { AnimatePresence, motion } from "motion/react";
5
+ import { cn } from "@usereq/ui/lib/utils";
6
+ import {
7
+ Avatar,
8
+ AvatarFallback,
9
+ AvatarImage,
10
+ } from "@usereq/ui/components/avatar";
11
+ import { ChevronDown } from "lucide-react";
12
+ import {
13
+ DEFAULT_AGENT_NAME_BUBBLE,
14
+ DEFAULT_POWERED_BY,
15
+ DEFAULT_SPOTLIGHT_MESSAGES,
16
+ } from "../chat-widget-defaults";
17
+ import {
18
+ ChatWidgetComposer,
19
+ ChatWidgetHeader,
20
+ ChatWidgetMessages,
21
+ ChatWidgetPanel,
22
+ ChatWidgetPoweredBy,
23
+ useSpotlightFade,
24
+ useIsChatWidgetFullscreenViewport,
25
+ type ChatWidgetDataProps,
26
+ } from "../components/chat-widget-primitives";
27
+ import {
28
+ getChatWidgetAvatarStyle,
29
+ getChatWidgetGradientStyle,
30
+ } from "../chat-widget-appearance";
31
+
32
+ const PANEL_WIDTH = 360;
33
+ const LAUNCHER_SIZE = 56;
34
+ const SPOTLIGHT_GAP = 8;
35
+ /** Gap between launcher and panel for embed-friendly spacing */
36
+ const PANEL_GAP = 12;
37
+
38
+ const fadeTransition = { duration: 0.4 };
39
+
40
+ export function ChatWidgetBubble({
41
+ agentName = DEFAULT_AGENT_NAME_BUBBLE,
42
+ agentAvatarUrl,
43
+ appearance,
44
+ spotlightMessages = [],
45
+ messages = [],
46
+ poweredBy = DEFAULT_POWERED_BY,
47
+ headerStatus = "Online",
48
+ errorMessage,
49
+ status = "idle",
50
+ sendDisabled = false,
51
+ onSendMessage,
52
+ isTyping,
53
+ onSuggestion,
54
+ onLauncherClick,
55
+ className,
56
+ open,
57
+ defaultOpen = false,
58
+ onOpenChange,
59
+ previewOnly = false,
60
+ onStopConversation,
61
+ stopDisabled,
62
+ onConfirmationDecision,
63
+ onLinkClick,
64
+ emptyAction,
65
+ portalContainer,
66
+ }: ChatWidgetDataProps & {
67
+ open?: boolean;
68
+ className?: string;
69
+ defaultOpen?: boolean;
70
+ onOpenChange?: (open: boolean) => void;
71
+ onLauncherClick?: () => void;
72
+ /** When true, launcher only (no open state); for layout editor. */
73
+ previewOnly?: boolean;
74
+ portalContainer?: React.ComponentProps<
75
+ typeof ChatWidgetPanel
76
+ >["portalContainer"];
77
+ }) {
78
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
79
+ previewOnly ? false : defaultOpen,
80
+ );
81
+ const [draftValue, setDraftValue] = React.useState("");
82
+ const spotlightForTyping =
83
+ spotlightMessages.length > 0
84
+ ? spotlightMessages
85
+ : [...DEFAULT_SPOTLIGHT_MESSAGES];
86
+ const spotlightText = useSpotlightFade(spotlightForTyping, 3000);
87
+ const effectiveOpen = previewOnly ? false : (open ?? uncontrolledOpen);
88
+ const handleOpenChange = React.useCallback(
89
+ (nextOpen: boolean) => {
90
+ if (onOpenChange) {
91
+ onOpenChange(nextOpen);
92
+ return;
93
+ }
94
+ setUncontrolledOpen(nextOpen);
95
+ },
96
+ [onOpenChange],
97
+ );
98
+ const isFullscreenViewport = useIsChatWidgetFullscreenViewport();
99
+ const hideLauncher = isFullscreenViewport && effectiveOpen;
100
+
101
+ return (
102
+ <div
103
+ className={cn("relative", className)}
104
+ style={{
105
+ width: spotlightForTyping.length > 0 ? 260 : LAUNCHER_SIZE,
106
+ height:
107
+ spotlightForTyping.length > 0
108
+ ? LAUNCHER_SIZE + SPOTLIGHT_GAP + 56
109
+ : LAUNCHER_SIZE,
110
+ }}
111
+ >
112
+ {/* Spotlight: rotating messages above bubble (fade in/out), only when closed */}
113
+ {spotlightForTyping.length > 0 && !effectiveOpen && (
114
+ <AnimatePresence mode="wait">
115
+ <motion.div
116
+ key={spotlightText}
117
+ initial={{ opacity: 0 }}
118
+ animate={{ opacity: 1 }}
119
+ exit={{ opacity: 0 }}
120
+ transition={fadeTransition}
121
+ className="absolute right-0 bottom-[calc(56px+SPOTLIGHT_GAP)] flex w-full max-w-65 flex-col items-end"
122
+ >
123
+ <div className="rounded-2xl border bg-background/95 px-4 py-3 text-right shadow-lg backdrop-blur-sm dark:bg-background/90">
124
+ <p className="text-sm leading-relaxed text-foreground">
125
+ {spotlightText}
126
+ </p>
127
+ </div>
128
+ <div className="flex w-full justify-end pr-6" aria-hidden>
129
+ <span className="border-[6px] border-transparent border-t-background/95 bg-transparent dark:border-t-background/90" />
130
+ </div>
131
+ </motion.div>
132
+ </AnimatePresence>
133
+ )}
134
+
135
+ {/* Chat panel: full-screen on mobile, floating on desktop (hidden when previewOnly) */}
136
+ {!previewOnly && (
137
+ <ChatWidgetPanel
138
+ open={effectiveOpen}
139
+ onOpenChange={handleOpenChange}
140
+ portalContainer={portalContainer}
141
+ desktopClassName="absolute right-0 overflow-hidden rounded-2xl border bg-background shadow-xl"
142
+ desktopStyle={{
143
+ width: PANEL_WIDTH,
144
+ bottom: LAUNCHER_SIZE + PANEL_GAP,
145
+ }}
146
+ >
147
+ <div className="flex shrink-0 border-b p-4">
148
+ <ChatWidgetHeader
149
+ agentName={agentName}
150
+ agentAvatarUrl={agentAvatarUrl}
151
+ appearance={appearance}
152
+ statusText={headerStatus}
153
+ onStopConversation={onStopConversation ?? undefined}
154
+ stopDisabled={stopDisabled}
155
+ onClose={() => handleOpenChange(false)}
156
+ />
157
+ </div>
158
+ <div className="flex min-h-0 flex-1 flex-col p-4 overflow-hidden">
159
+ <ChatWidgetMessages
160
+ messages={messages}
161
+ isTyping={isTyping ?? status === "sending"}
162
+ status={status}
163
+ suggestions={spotlightForTyping}
164
+ errorMessage={errorMessage}
165
+ appearance={appearance}
166
+ onConfirmationDecision={onConfirmationDecision}
167
+ onLinkClick={onLinkClick}
168
+ onSuggestion={(value: string) => {
169
+ setDraftValue(value);
170
+ onSuggestion?.(value);
171
+ }}
172
+ emptyAction={emptyAction}
173
+ />
174
+ </div>
175
+ {!(emptyAction && messages.length === 0 && !isTyping) && (
176
+ <div className="shrink-0 border-t p-3">
177
+ <ChatWidgetComposer
178
+ value={draftValue}
179
+ onChange={setDraftValue}
180
+ onSubmit={onSendMessage}
181
+ appearance={appearance}
182
+ disabled={sendDisabled || status === "booting"}
183
+ />
184
+ <ChatWidgetPoweredBy text={poweredBy} className="mt-2" />
185
+ </div>
186
+ )}
187
+ </ChatWidgetPanel>
188
+ )}
189
+
190
+ {/* Launcher: absolute bottom-right anchor for embed */}
191
+ {!hideLauncher && (
192
+ <motion.button
193
+ type="button"
194
+ onClick={
195
+ previewOnly
196
+ ? undefined
197
+ : () => {
198
+ onLauncherClick?.();
199
+ handleOpenChange(!effectiveOpen);
200
+ }
201
+ }
202
+ className={cn(
203
+ "absolute bottom-0 right-0 flex size-14 shrink-0 items-center justify-center rounded-full text-white shadow-lg",
204
+ previewOnly && "pointer-events-none",
205
+ )}
206
+ style={
207
+ appearance
208
+ ? (getChatWidgetGradientStyle(appearance) as any)
209
+ : undefined
210
+ }
211
+ aria-label={effectiveOpen ? "Close chat" : "Open chat"}
212
+ aria-expanded={effectiveOpen}
213
+ animate={{ scale: 1 }}
214
+ whileHover={previewOnly ? undefined : { scale: 1.02 }}
215
+ whileTap={previewOnly ? undefined : { scale: 0.98 }}
216
+ >
217
+ {effectiveOpen ? (
218
+ <ChevronDown className="size-6" />
219
+ ) : (
220
+ <Avatar
221
+ className="size-12"
222
+ style={
223
+ appearance ? getChatWidgetAvatarStyle(appearance) : undefined
224
+ }
225
+ >
226
+ {agentAvatarUrl ? (
227
+ <AvatarImage src={agentAvatarUrl} alt={agentName} />
228
+ ) : null}
229
+ <AvatarFallback className="bg-transparent text-white">
230
+ {agentName.slice(0, 2).toUpperCase()}
231
+ </AvatarFallback>
232
+ </Avatar>
233
+ )}
234
+ </motion.button>
235
+ )}
236
+ </div>
237
+ );
238
+ }
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { motion } from "motion/react";
5
+ import { ChevronDown, Search, Sparkles } from "lucide-react";
6
+ import { cn } from "@usereq/ui/lib/utils";
7
+ import { getChatWidgetGradientStyle } from "../chat-widget-appearance";
8
+ import {
9
+ DEFAULT_AGENT_NAME_CHATBAR,
10
+ DEFAULT_LAUNCHER_LABEL,
11
+ DEFAULT_POWERED_BY,
12
+ DEFAULT_SPOTLIGHT_MESSAGES,
13
+ } from "../chat-widget-defaults";
14
+ import {
15
+ ChatWidgetComposer,
16
+ ChatWidgetHeader,
17
+ ChatWidgetMessages,
18
+ ChatWidgetPanel,
19
+ ChatWidgetPoweredBy,
20
+ useIsChatWidgetFullscreenViewport,
21
+ useSpotlightTyping,
22
+ useStableSpotlightMessages,
23
+ type ChatWidgetDataProps,
24
+ } from "../components/chat-widget-primitives";
25
+
26
+ const CHATBAR_WIDTH = 560;
27
+ const CHATBAR_MIN_WIDTH = 320;
28
+ const BAR_HEIGHT = 48;
29
+ /** When open, bar shrinks to this size (full rounded chevron) */
30
+ const BAR_OPEN_SIZE = 48;
31
+ /** Same as Bubble/Box for consistent embed spacing */
32
+ const PANEL_GAP = 10;
33
+
34
+ const barTransition = { type: "spring" as const, stiffness: 400, damping: 35 };
35
+
36
+ export function ChatWidgetChatBar({
37
+ agentName = DEFAULT_AGENT_NAME_CHATBAR,
38
+ agentAvatarUrl,
39
+ appearance,
40
+ spotlightMessages = [],
41
+ messages = [],
42
+ poweredBy = DEFAULT_POWERED_BY,
43
+ headerStatus = "Online",
44
+ errorMessage,
45
+ status = "idle",
46
+ sendDisabled = false,
47
+ launcherLabel = DEFAULT_LAUNCHER_LABEL,
48
+ onSendMessage,
49
+ isTyping,
50
+ onSuggestion,
51
+ onLauncherClick,
52
+ className,
53
+ open,
54
+ defaultOpen = false,
55
+ onOpenChange,
56
+ previewOnly = false,
57
+ onStopConversation,
58
+ stopDisabled,
59
+ onConfirmationDecision,
60
+ onLinkClick,
61
+ emptyAction,
62
+ portalContainer,
63
+ }: ChatWidgetDataProps & {
64
+ /** Label on the closed bar pill (e.g. "Ask") */
65
+ launcherLabel?: string;
66
+ open?: boolean;
67
+ className?: string;
68
+ defaultOpen?: boolean;
69
+ onOpenChange?: (open: boolean) => void;
70
+ /** When true, launcher only (no open state); for layout editor. */
71
+ previewOnly?: boolean;
72
+ portalContainer?: React.ComponentProps<
73
+ typeof ChatWidgetPanel
74
+ >["portalContainer"];
75
+ }) {
76
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
77
+ previewOnly ? false : defaultOpen,
78
+ );
79
+ const [draftValue, setDraftValue] = React.useState("");
80
+ const effectiveOpen = previewOnly ? false : (open ?? uncontrolledOpen);
81
+ const isFullscreenViewport = useIsChatWidgetFullscreenViewport();
82
+ const hideLauncher = isFullscreenViewport && effectiveOpen;
83
+ const handleOpenChange = React.useCallback(
84
+ (nextOpen: boolean) => {
85
+ if (onOpenChange) {
86
+ onOpenChange(nextOpen);
87
+ return;
88
+ }
89
+ setUncontrolledOpen(nextOpen);
90
+ },
91
+ [onOpenChange],
92
+ );
93
+ const stableSpotlightMessages = useStableSpotlightMessages(spotlightMessages);
94
+ const spotlightForTyping =
95
+ stableSpotlightMessages.length > 0
96
+ ? stableSpotlightMessages
97
+ : [...DEFAULT_SPOTLIGHT_MESSAGES];
98
+ const barPlaceholder =
99
+ useSpotlightTyping(spotlightForTyping) ||
100
+ spotlightForTyping[0] ||
101
+ DEFAULT_SPOTLIGHT_MESSAGES[0];
102
+ const barWrapperRef = React.useRef<HTMLDivElement>(null);
103
+ const [barWidth, setBarWidth] = React.useState(CHATBAR_WIDTH);
104
+
105
+ React.useEffect(() => {
106
+ if (effectiveOpen) return;
107
+ const el = barWrapperRef.current;
108
+ if (!el) return;
109
+ const ro = new ResizeObserver((entries) => {
110
+ const w = entries[0]?.contentRect.width;
111
+ if (typeof w === "number") setBarWidth(w);
112
+ });
113
+ ro.observe(el);
114
+ return () => ro.disconnect();
115
+ }, [effectiveOpen]);
116
+
117
+ return (
118
+ <div
119
+ className={cn("relative w-full max-w-full shrink-0", className)}
120
+ style={{
121
+ maxWidth: CHATBAR_WIDTH,
122
+ minWidth: CHATBAR_MIN_WIDTH,
123
+ minHeight: BAR_HEIGHT,
124
+ }}
125
+ >
126
+ {/* Chat panel: full-screen on mobile, floating on desktop (hidden when previewOnly) */}
127
+ {!previewOnly && (
128
+ <ChatWidgetPanel
129
+ open={effectiveOpen}
130
+ onOpenChange={handleOpenChange}
131
+ portalContainer={portalContainer}
132
+ desktopClassName={cn(
133
+ "absolute left-0 right-0 mx-auto overflow-hidden rounded-2xl border bg-white/90 shadow-lg backdrop-blur-xl dark:bg-background/90",
134
+ )}
135
+ desktopStyle={{
136
+ width: "100%",
137
+ minWidth: CHATBAR_MIN_WIDTH,
138
+ maxWidth: CHATBAR_WIDTH,
139
+ bottom: BAR_HEIGHT + PANEL_GAP,
140
+ }}
141
+ >
142
+ <div className="flex shrink-0 border-b p-4">
143
+ <ChatWidgetHeader
144
+ agentName={agentName}
145
+ agentAvatarUrl={agentAvatarUrl}
146
+ appearance={appearance}
147
+ statusText={headerStatus}
148
+ onStopConversation={onStopConversation ?? undefined}
149
+ stopDisabled={stopDisabled}
150
+ onClose={() => handleOpenChange(false)}
151
+ />
152
+ </div>
153
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden p-4 md:p-2 md:max-h-85">
154
+ <ChatWidgetMessages
155
+ messages={messages}
156
+ isTyping={isTyping ?? status === "sending"}
157
+ status={status}
158
+ errorMessage={errorMessage}
159
+ appearance={appearance}
160
+ onConfirmationDecision={onConfirmationDecision}
161
+ onLinkClick={onLinkClick}
162
+ onSuggestion={(value: string) => {
163
+ setDraftValue(value);
164
+ onSuggestion?.(value);
165
+ }}
166
+ emptyAction={emptyAction}
167
+ />
168
+ </div>
169
+ {!(emptyAction && messages.length === 0 && !isTyping) && (
170
+ <div className="border-t p-3 shrink-0">
171
+ <ChatWidgetComposer
172
+ value={draftValue}
173
+ onChange={setDraftValue}
174
+ onSubmit={onSendMessage}
175
+ appearance={appearance}
176
+ disabled={sendDisabled || status === "booting"}
177
+ />
178
+ <ChatWidgetPoweredBy text={poweredBy} className="mt-2" />
179
+ </div>
180
+ )}
181
+ </ChatWidgetPanel>
182
+ )}
183
+
184
+ {/* Bar: full width when closed, shrinks to full-rounded chevron when open */}
185
+ {!hideLauncher && (
186
+ <div className="absolute bottom-0 left-0 right-0 z-0 flex w-full justify-center">
187
+ <div
188
+ ref={barWrapperRef}
189
+ className="flex w-full max-w-full justify-center"
190
+ >
191
+ <motion.button
192
+ type="button"
193
+ onClick={
194
+ previewOnly
195
+ ? undefined
196
+ : () => {
197
+ if (!effectiveOpen) {
198
+ onLauncherClick?.();
199
+ }
200
+ handleOpenChange(!effectiveOpen);
201
+ }
202
+ }
203
+ className={cn(
204
+ "flex shrink-0 items-center gap-2.5 rounded-full border bg-white/80 px-4 py-3 shadow-lg backdrop-blur-xl dark:bg-white/10",
205
+ effectiveOpen && "justify-center",
206
+ previewOnly && "pointer-events-none",
207
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
208
+ )}
209
+ style={{ height: BAR_HEIGHT }}
210
+ initial={false}
211
+ animate={{
212
+ width: effectiveOpen ? BAR_OPEN_SIZE : barWidth,
213
+ borderRadius: 9999,
214
+ }}
215
+ transition={barTransition}
216
+ aria-label={effectiveOpen ? "Close chat" : "Open chat"}
217
+ aria-expanded={effectiveOpen}
218
+ whileHover={previewOnly ? undefined : { scale: 1.02 }}
219
+ whileTap={previewOnly ? undefined : { scale: 0.98 }}
220
+ >
221
+ {effectiveOpen ? (
222
+ <ChevronDown className="size-5 shrink-0 text-foreground" />
223
+ ) : (
224
+ <>
225
+ <Search className="size-5 shrink-0 text-muted-foreground" />
226
+ <span className="min-w-0 flex-1 truncate text-left text-sm text-muted-foreground">
227
+ {barPlaceholder}
228
+ </span>
229
+ <div
230
+ className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium text-white"
231
+ style={
232
+ appearance
233
+ ? getChatWidgetGradientStyle(appearance)
234
+ : undefined
235
+ }
236
+ >
237
+ <Sparkles className="size-4" />
238
+ {appearance?.actionText?.trim() || launcherLabel}
239
+ </div>
240
+ </>
241
+ )}
242
+ </motion.button>
243
+ </div>
244
+ </div>
245
+ )}
246
+ </div>
247
+ );
248
+ }