@usereq/widget 0.2.8 → 0.2.10

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.
@@ -1,258 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { 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 { Button } from "@usereq/ui/components/button";
12
- import { ChevronDown } from "lucide-react";
13
- import {
14
- getChatWidgetAvatarStyle,
15
- getChatWidgetGradientStyle,
16
- } from "../chat-widget-appearance";
17
- import {
18
- DEFAULT_AGENT_NAME_BOX,
19
- DEFAULT_POWERED_BY,
20
- DEFAULT_SPOTLIGHT_MESSAGES,
21
- DEFAULT_START_CHAT_LABEL,
22
- } from "../chat-widget-defaults";
23
- import {
24
- ChatWidgetComposer,
25
- ChatWidgetHeader,
26
- ChatWidgetMessages,
27
- ChatWidgetPanel,
28
- ChatWidgetPoweredBy,
29
- useIsChatWidgetFullscreenViewport,
30
- useSpotlightTyping,
31
- useStableSpotlightMessages,
32
- type ChatWidgetDataProps,
33
- } from "../components/chat-widget-primitives";
34
-
35
- const PANEL_WIDTH = 360;
36
- const PANEL_GAP = 12;
37
- const LAUNCHER_CARD_WIDTH = 280;
38
- const LAUNCHER_CARD_HEIGHT = 120;
39
- /** When panel is open, launcher is just the close button (square). */
40
- const LAUNCHER_BUTTON_SIZE = 56;
41
-
42
- const panelTransition = {
43
- type: "spring" as const,
44
- stiffness: 400,
45
- damping: 35,
46
- };
47
-
48
- /**
49
- * ChatWidgetBox — closed: intro message + Start chat button; open: same panel as Bubble + chevron to close.
50
- */
51
- export function ChatWidgetBox({
52
- agentName = DEFAULT_AGENT_NAME_BOX,
53
- agentAvatarUrl,
54
- appearance,
55
- spotlightMessages = [],
56
- messages = [],
57
- poweredBy = DEFAULT_POWERED_BY,
58
- headerStatus = "Online",
59
- errorMessage,
60
- status = "idle",
61
- sendDisabled = false,
62
- startChatLabel = DEFAULT_START_CHAT_LABEL,
63
- onSendMessage,
64
- isTyping,
65
- onSuggestion,
66
- onLauncherClick,
67
- className,
68
- open,
69
- defaultOpen = false,
70
- onOpenChange,
71
- previewOnly = false,
72
- onStopConversation,
73
- stopDisabled,
74
- onConfirmationDecision,
75
- onLinkClick,
76
- emptyAction,
77
- portalContainer,
78
- }: ChatWidgetDataProps & {
79
- /** Button label on closed card */
80
- startChatLabel?: string;
81
- open?: boolean;
82
- className?: string;
83
- defaultOpen?: boolean;
84
- onOpenChange?: (open: boolean) => void;
85
- /** When true, launcher only (no open state); for layout editor. */
86
- previewOnly?: boolean;
87
- portalContainer?: React.ComponentProps<
88
- typeof ChatWidgetPanel
89
- >["portalContainer"];
90
- }) {
91
- const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
92
- previewOnly ? false : defaultOpen,
93
- );
94
- const [draftValue, setDraftValue] = React.useState("");
95
- const effectiveOpen = previewOnly ? false : (open ?? uncontrolledOpen);
96
- const handleOpenChange = React.useCallback(
97
- (nextOpen: boolean) => {
98
- if (onOpenChange) {
99
- onOpenChange(nextOpen);
100
- return;
101
- }
102
- setUncontrolledOpen(nextOpen);
103
- },
104
- [onOpenChange],
105
- );
106
- const stableSpotlightMessages = useStableSpotlightMessages(spotlightMessages);
107
- const spotlightForTyping =
108
- stableSpotlightMessages.length > 0
109
- ? stableSpotlightMessages
110
- : [...DEFAULT_SPOTLIGHT_MESSAGES];
111
- const introTextTyping = useSpotlightTyping(spotlightForTyping);
112
- const introText = introTextTyping || spotlightForTyping[0] || DEFAULT_SPOTLIGHT_MESSAGES[0];
113
-
114
- const launcherHeight = effectiveOpen
115
- ? LAUNCHER_BUTTON_SIZE
116
- : LAUNCHER_CARD_HEIGHT;
117
- const rootWidth = effectiveOpen ? LAUNCHER_BUTTON_SIZE : LAUNCHER_CARD_WIDTH;
118
- const isFullscreenViewport = useIsChatWidgetFullscreenViewport();
119
- const hideLauncher = isFullscreenViewport && effectiveOpen;
120
- const closeButtonStyle: React.CSSProperties = {
121
- width: LAUNCHER_BUTTON_SIZE,
122
- height: LAUNCHER_BUTTON_SIZE,
123
- ...(appearance ? getChatWidgetGradientStyle(appearance) : {}),
124
- pointerEvents: effectiveOpen ? "auto" : "none",
125
- };
126
-
127
- return (
128
- <div
129
- className={cn("relative", className)}
130
- style={{ width: rootWidth, height: launcherHeight }}
131
- >
132
- {/* Chat panel: full-screen on mobile, floating on desktop (hidden when previewOnly) */}
133
- {!previewOnly && (
134
- <ChatWidgetPanel
135
- open={effectiveOpen}
136
- onOpenChange={handleOpenChange}
137
- portalContainer={portalContainer}
138
- desktopClassName="absolute right-0 overflow-hidden rounded-2xl border bg-background shadow-xl"
139
- desktopStyle={{
140
- width: PANEL_WIDTH,
141
- bottom: launcherHeight + PANEL_GAP,
142
- }}
143
- >
144
- <div className="flex shrink-0 border-b p-4">
145
- <ChatWidgetHeader
146
- agentName={agentName}
147
- agentAvatarUrl={agentAvatarUrl}
148
- appearance={appearance}
149
- statusText={headerStatus}
150
- onStopConversation={onStopConversation ?? undefined}
151
- stopDisabled={stopDisabled}
152
- onClose={() => handleOpenChange(false)}
153
- />
154
- </div>
155
- <div className="flex min-h-0 flex-1 flex-col overflow-hidden p-4 md:max-h-90">
156
- <ChatWidgetMessages
157
- messages={messages}
158
- isTyping={isTyping ?? status === "sending"}
159
- status={status}
160
- errorMessage={errorMessage}
161
- appearance={appearance}
162
- onConfirmationDecision={onConfirmationDecision}
163
- onLinkClick={onLinkClick}
164
- onSuggestion={(value: string) => {
165
- setDraftValue(value);
166
- onSuggestion?.(value);
167
- }}
168
- emptyAction={emptyAction}
169
- />
170
- </div>
171
- {!(emptyAction && messages.length === 0 && !isTyping) && (
172
- <div className="border-t p-3 shrink-0">
173
- <ChatWidgetComposer
174
- value={draftValue}
175
- onChange={setDraftValue}
176
- onSubmit={onSendMessage}
177
- appearance={appearance}
178
- disabled={sendDisabled || status === "booting"}
179
- />
180
- <ChatWidgetPoweredBy text={poweredBy} className="mt-2" />
181
- </div>
182
- )}
183
- </ChatWidgetPanel>
184
- )}
185
-
186
- {/* Closed: intro message + Start chat button */}
187
- {!effectiveOpen && (
188
- <motion.div
189
- className="absolute bottom-0 right-0 flex flex-col gap-4 rounded-2xl border bg-background p-4 shadow-xl"
190
- style={{ width: LAUNCHER_CARD_WIDTH, height: LAUNCHER_CARD_HEIGHT }}
191
- initial={false}
192
- animate={{ opacity: 1 }}
193
- >
194
- <div className="flex flex-1 items-start gap-2">
195
- <Avatar
196
- className="size-9 shrink-0"
197
- style={
198
- appearance ? getChatWidgetAvatarStyle(appearance) : undefined
199
- }
200
- >
201
- {agentAvatarUrl ? (
202
- <AvatarImage src={agentAvatarUrl} alt={agentName} />
203
- ) : null}
204
- <AvatarFallback className="bg-transparent text-white text-xs">
205
- {agentName.slice(0, 2).toUpperCase()}
206
- </AvatarFallback>
207
- </Avatar>
208
- {introText ? (
209
- <p className="min-w-0 flex-1 text-sm leading-snug text-foreground">
210
- {introText}
211
- </p>
212
- ) : null}
213
- </div>
214
- <Button
215
- className={cn(
216
- "w-full rounded-full",
217
- previewOnly && "pointer-events-none",
218
- )}
219
- onClick={
220
- previewOnly
221
- ? undefined
222
- : () => {
223
- onLauncherClick?.();
224
- handleOpenChange(true);
225
- }
226
- }
227
- style={
228
- appearance ? getChatWidgetGradientStyle(appearance) : undefined
229
- }
230
- >
231
- {appearance?.actionText?.trim() || startChatLabel}
232
- </Button>
233
- </motion.div>
234
- )}
235
-
236
- {/* Open: square close button (matches launcher width/height when open) */}
237
- {!previewOnly && !hideLauncher && (
238
- <motion.button
239
- type="button"
240
- onClick={() => handleOpenChange(false)}
241
- className="absolute bottom-0 right-0 flex items-center justify-center rounded-full text-primary-foreground shadow-lg"
242
- style={closeButtonStyle as any}
243
- aria-label="Close chat"
244
- initial={false}
245
- animate={{
246
- opacity: effectiveOpen ? 1 : 0,
247
- visibility: effectiveOpen ? "visible" : "hidden",
248
- }}
249
- transition={panelTransition}
250
- whileHover={effectiveOpen ? { scale: 1.02 } : undefined}
251
- whileTap={effectiveOpen ? { scale: 0.98 } : undefined}
252
- >
253
- <ChevronDown className="size-6" />
254
- </motion.button>
255
- )}
256
- </div>
257
- );
258
- }
@@ -1,238 +0,0 @@
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
- }