@usereq/widget 0.2.9 → 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,248 +0,0 @@
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
- }