@usereq/widget 0.1.4 → 0.1.5

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.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -10,25 +10,46 @@ import {
10
10
  } from "@usereq/ui/components/avatar";
11
11
  import { Button } from "@usereq/ui/components/button";
12
12
  import { Textarea } from "@usereq/ui/components/textarea";
13
- import { X, SendHorizonal, Phone, Video, Info, ThumbsUp } from "lucide-react";
13
+ import {
14
+ ChevronDown,
15
+ SendHorizonal,
16
+ Phone,
17
+ Video,
18
+ Info,
19
+ } from "lucide-react";
14
20
  import {
15
21
  DEFAULT_AGENT_NAME_BUBBLE,
16
- DEFAULT_COMPOSER_PLACEHOLDER,
17
22
  DEFAULT_POWERED_BY,
18
23
  DEFAULT_SPOTLIGHT_MESSAGES,
19
24
  } from "../chat-widget-defaults";
20
25
  import {
21
- ChatWidgetMessages,
22
26
  ChatWidgetPanel,
23
27
  ChatWidgetPoweredBy,
28
+ ChatWidgetTyping,
24
29
  useIsChatWidgetFullscreenViewport,
25
30
  useSpotlightFade,
26
31
  type ChatWidgetDataProps,
32
+ type ChatWidgetPreviewMessage,
27
33
  } from "../components/chat-widget-primitives";
34
+ import {
35
+ Conversation,
36
+ ConversationContent,
37
+ ConversationEmptyState,
38
+ ConversationScrollButton,
39
+ } from "../components/conversation";
40
+ import { foldStopConfirmationMessages } from "../stop-confirmation";
41
+ import { getChatWidgetMessagesLayoutClasses } from "../components/chat-widget-layout";
42
+ import {
43
+ Message,
44
+ MessageContent,
45
+ MessageResponse,
46
+ } from "../components/message";
28
47
 
29
48
  const MESSENGER_GRADIENT =
30
49
  "linear-gradient(135deg, #00B2FF 0%, #006AFF 50%, #8E5BFF 100%)";
31
50
 
51
+ const MESSENGER_BRAND_COLOR = "#0084FF";
52
+
32
53
  const PANEL_WIDTH = 372;
33
54
  const PANEL_HEIGHT = 580;
34
55
  const LAUNCHER_SIZE = 60;
@@ -40,7 +61,6 @@ const fadeTransition = { duration: 0.35 };
40
61
  export function ChatWidgetMessenger({
41
62
  agentName = DEFAULT_AGENT_NAME_BUBBLE,
42
63
  agentAvatarUrl,
43
- appearance,
44
64
  spotlightMessages = [],
45
65
  messages = [],
46
66
  poweredBy = DEFAULT_POWERED_BY,
@@ -50,7 +70,6 @@ export function ChatWidgetMessenger({
50
70
  sendDisabled = false,
51
71
  onSendMessage,
52
72
  isTyping,
53
- onSuggestion,
54
73
  onLauncherClick,
55
74
  className,
56
75
  open,
@@ -120,7 +139,7 @@ export function ChatWidgetMessenger({
120
139
  transition={fadeTransition}
121
140
  className="absolute right-0 bottom-[calc(60px+10px)] flex w-full max-w-70 flex-col items-end"
122
141
  >
123
- <div className="rounded-3xl bg-white px-4 py-2.5 text-right shadow-[0_4px_16px_rgba(0,0,0,0.12)]">
142
+ <div className="rounded-3xl bg-background px-4 py-2.5 text-right shadow-[0_4px_16px_rgba(0,0,0,0.12)]">
124
143
  <p className="text-[14px] leading-snug text-foreground">
125
144
  {spotlightText}
126
145
  </p>
@@ -134,7 +153,7 @@ export function ChatWidgetMessenger({
134
153
  open={effectiveOpen}
135
154
  onOpenChange={handleOpenChange}
136
155
  portalContainer={portalContainer}
137
- desktopClassName="absolute right-0 overflow-hidden rounded-3xl bg-background shadow-[0_8px_32px_rgba(0,0,0,0.16)] border border-black/5"
156
+ desktopClassName="absolute right-0 overflow-hidden rounded-3xl bg-background shadow-[0_8px_32px_rgba(0,0,0,0.16)] border border-border"
138
157
  desktopStyle={{
139
158
  width: PANEL_WIDTH,
140
159
  height: PANEL_HEIGHT,
@@ -149,20 +168,16 @@ export function ChatWidgetMessenger({
149
168
  onStopConversation={onStopConversation ?? undefined}
150
169
  stopDisabled={stopDisabled}
151
170
  />
152
- <div className="flex min-h-0 flex-1 flex-col bg-[#F5F5F7] dark:bg-background overflow-hidden">
153
- <ChatWidgetMessages
171
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
172
+ <MessengerMessages
154
173
  messages={messages}
155
174
  isTyping={isTyping ?? status === "sending"}
156
175
  status={status}
157
- suggestions={spotlightForFade}
158
176
  errorMessage={errorMessage}
159
- appearance={appearance}
177
+ agentName={agentName}
178
+ agentAvatarUrl={agentAvatarUrl}
160
179
  onConfirmationDecision={onConfirmationDecision}
161
180
  onLinkClick={onLinkClick}
162
- onSuggestion={(value: string) => {
163
- setDraftValue(value);
164
- onSuggestion?.(value);
165
- }}
166
181
  emptyAction={emptyAction}
167
182
  />
168
183
  </div>
@@ -199,7 +214,7 @@ export function ChatWidgetMessenger({
199
214
  style={{
200
215
  width: LAUNCHER_SIZE,
201
216
  height: LAUNCHER_SIZE,
202
- background: MESSENGER_GRADIENT,
217
+ backgroundColor: MESSENGER_BRAND_COLOR,
203
218
  boxShadow: "0 6px 20px rgba(0, 132, 255, 0.45)",
204
219
  }}
205
220
  aria-label={effectiveOpen ? "Close chat" : "Open chat"}
@@ -209,7 +224,7 @@ export function ChatWidgetMessenger({
209
224
  whileTap={previewOnly ? undefined : { scale: 0.96 }}
210
225
  >
211
226
  {effectiveOpen ? (
212
- <X className="size-7" strokeWidth={2.4} />
227
+ <ChevronDown className="size-7" strokeWidth={2.4} />
213
228
  ) : (
214
229
  <MessengerLogo className="size-8" />
215
230
  )}
@@ -219,6 +234,169 @@ export function ChatWidgetMessenger({
219
234
  );
220
235
  }
221
236
 
237
+ function MessengerMessages({
238
+ messages,
239
+ isTyping,
240
+ status,
241
+ errorMessage,
242
+ agentName,
243
+ agentAvatarUrl,
244
+ onConfirmationDecision,
245
+ onLinkClick,
246
+ emptyAction,
247
+ }: {
248
+ messages: ChatWidgetPreviewMessage[];
249
+ isTyping: boolean;
250
+ status: ChatWidgetDataProps["status"];
251
+ errorMessage?: string | null;
252
+ agentName: string;
253
+ agentAvatarUrl?: string | null;
254
+ onConfirmationDecision?: ChatWidgetDataProps["onConfirmationDecision"];
255
+ onLinkClick?: (linkUrl: string) => void;
256
+ emptyAction?: { label: string; onClick: () => void; disabled?: boolean };
257
+ }) {
258
+ const { scrollClassName, contentClassName } =
259
+ getChatWidgetMessagesLayoutClasses();
260
+ const renderedMessages = React.useMemo(
261
+ () => foldStopConfirmationMessages(messages),
262
+ [messages],
263
+ );
264
+
265
+ if (messages.length === 0 && !isTyping) {
266
+ if (status === "error") {
267
+ return (
268
+ <div className="flex h-full items-center justify-center px-6 py-8 text-center text-sm text-destructive">
269
+ {errorMessage ?? "Something went wrong."}
270
+ </div>
271
+ );
272
+ }
273
+
274
+ return (
275
+ <div className="flex min-h-full flex-1 items-center justify-center px-6 py-8">
276
+ <ConversationEmptyState className="w-full max-w-[280px] gap-4">
277
+ <Avatar
278
+ className="size-16 ring-4 ring-background"
279
+ style={{
280
+ boxShadow: "0 6px 18px rgba(0, 132, 255, 0.25)",
281
+ }}
282
+ >
283
+ {agentAvatarUrl ? (
284
+ <AvatarImage src={agentAvatarUrl} alt={agentName} />
285
+ ) : null}
286
+ <AvatarFallback
287
+ className="text-white text-base font-semibold"
288
+ style={{ backgroundColor: MESSENGER_BRAND_COLOR }}
289
+ >
290
+ {agentName.slice(0, 2).toUpperCase()}
291
+ </AvatarFallback>
292
+ </Avatar>
293
+ <div>
294
+ <p className="text-[15px] font-semibold leading-tight text-foreground">
295
+ {agentName}
296
+ </p>
297
+ <p className="mt-1.5 text-[13px] leading-snug text-muted-foreground">
298
+ Typically replies in a few minutes.
299
+ </p>
300
+ </div>
301
+ {emptyAction && (
302
+ <Button
303
+ size="lg"
304
+ disabled={emptyAction.disabled}
305
+ onClick={emptyAction.onClick}
306
+ className="rounded-full px-6 text-white shadow-[0_4px_12px_rgba(0,132,255,0.35)] hover:opacity-95 disabled:opacity-60"
307
+ style={{ backgroundColor: MESSENGER_BRAND_COLOR }}
308
+ >
309
+ {emptyAction.label}
310
+ </Button>
311
+ )}
312
+ </ConversationEmptyState>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ return (
318
+ <Conversation className="relative size-full">
319
+ <ConversationContent
320
+ scrollClassName={scrollClassName}
321
+ className={cn(contentClassName, "gap-1.5 px-3 py-3")}
322
+ >
323
+ {renderedMessages.map((item) => {
324
+ if (item.type === "confirmation") {
325
+ return (
326
+ <Message key={item.message.id} from="assistant">
327
+ <div className="flex w-fit max-w-[80%] shrink-0 flex-col gap-2">
328
+ <MessageResponse
329
+ confirmation={item.latestEnvelope}
330
+ confirmationPrompt={item.envelope.prompt}
331
+ confirmationActionsDisabled={status === "sending"}
332
+ onConfirmationDecision={onConfirmationDecision}
333
+ />
334
+ </div>
335
+ </Message>
336
+ );
337
+ }
338
+
339
+ const m = item.message;
340
+ const isUser = m.role === "user";
341
+ return (
342
+ <Message key={m.id} from={m.role}>
343
+ <div className="flex w-fit max-w-[80%] shrink-0 flex-col gap-1.5">
344
+ <MessageContent
345
+ className={cn(
346
+ "whitespace-pre-wrap rounded-[18px] px-3.5 py-2 text-[14px] leading-snug",
347
+ isUser
348
+ ? "text-white"
349
+ : "bg-secondary text-secondary-foreground",
350
+ )}
351
+ style={
352
+ isUser ? { background: MESSENGER_GRADIENT } : undefined
353
+ }
354
+ >
355
+ {isUser ? (
356
+ m.content
357
+ ) : (
358
+ <MessageResponse onLinkClick={onLinkClick}>
359
+ {m.content}
360
+ </MessageResponse>
361
+ )}
362
+ </MessageContent>
363
+ {m.actions && m.actions.length > 0 && (
364
+ <div className="flex flex-wrap gap-2">
365
+ {m.actions.map((action, i) => (
366
+ <Button
367
+ key={i}
368
+ size="sm"
369
+ variant={action.variant ?? "outline"}
370
+ onClick={action.onClick}
371
+ className="rounded-full"
372
+ >
373
+ {action.label}
374
+ </Button>
375
+ ))}
376
+ </div>
377
+ )}
378
+ </div>
379
+ </Message>
380
+ );
381
+ })}
382
+ {isTyping && (
383
+ <motion.div
384
+ initial={{ opacity: 0, y: 6 }}
385
+ animate={{ opacity: 1, y: 0 }}
386
+ transition={{ duration: 0.2, ease: "easeOut" }}
387
+ className="flex w-full justify-start"
388
+ >
389
+ <div className="flex max-w-[80%] items-center rounded-[18px] bg-secondary px-3.5 py-2 text-secondary-foreground">
390
+ <ChatWidgetTyping className="bg-transparent! p-0!" />
391
+ </div>
392
+ </motion.div>
393
+ )}
394
+ </ConversationContent>
395
+ <ConversationScrollButton />
396
+ </Conversation>
397
+ );
398
+ }
399
+
222
400
  function MessengerHeader({
223
401
  agentName,
224
402
  agentAvatarUrl,
@@ -237,7 +415,7 @@ function MessengerHeader({
237
415
  return (
238
416
  <div
239
417
  className="flex shrink-0 items-center gap-3 px-4 py-3 text-white"
240
- style={{ background: MESSENGER_GRADIENT }}
418
+ style={{ backgroundColor: MESSENGER_BRAND_COLOR }}
241
419
  >
242
420
  <div className="relative">
243
421
  <Avatar size="lg" className="ring-2 ring-white/30">
@@ -287,11 +465,11 @@ function MessengerHeader({
287
465
  )}
288
466
  <button
289
467
  type="button"
290
- aria-label="Close"
468
+ aria-label="Minimize"
291
469
  onClick={onClose}
292
470
  className="ml-0.5 flex size-8 items-center justify-center rounded-full text-white/90 transition-colors hover:bg-white/15 hover:text-white"
293
471
  >
294
- <X className="size-4.5" strokeWidth={2.4} />
472
+ <ChevronDown className="size-5" strokeWidth={2.4} />
295
473
  </button>
296
474
  </div>
297
475
  </div>
@@ -326,49 +504,47 @@ function MessengerComposer({
326
504
  submit();
327
505
  };
328
506
 
329
- const trimmedValue = value.trim();
330
- const showSend = trimmedValue.length > 0;
507
+ const hasText = value.trim().length > 0;
508
+ const sendDisabled = disabled || !onSubmit || !hasText;
331
509
 
332
510
  return (
333
- <div className="shrink-0 border-t border-black/5 bg-background px-3 py-2.5">
334
- <div className="flex items-end gap-2">
335
- <div className="flex flex-1 items-end rounded-3xl bg-[#F0F0F2] px-3 dark:bg-muted">
511
+ <div className="shrink-0 border-t border-border bg-background px-2 py-2">
512
+ <div className="flex items-end gap-0.5">
513
+ {/* Tool icons (Plus / Camera / Image / Mic) temporarily disabled. */}
514
+ <div className="flex flex-1 items-end rounded-3xl bg-secondary px-3.5">
336
515
  <Textarea
337
516
  ref={textareaRef}
338
- placeholder={DEFAULT_COMPOSER_PLACEHOLDER}
517
+ placeholder="Aa"
339
518
  value={value}
340
519
  onChange={(e) => onChange(e.target.value)}
341
520
  onKeyDown={handleKeyDown}
342
521
  rows={1}
343
522
  disabled={disabled}
344
523
  className={cn(
345
- "min-h-9 max-h-32 flex-1 resize-none border-0 bg-transparent py-2 text-[14px] shadow-none",
524
+ "min-h-9 max-h-32 flex-1 resize-none border-0 bg-transparent py-2 text-[14px] leading-snug shadow-none",
346
525
  "focus-visible:ring-0 focus-visible:border-0",
347
526
  )}
348
527
  />
349
528
  </div>
350
- {showSend ? (
351
- <Button
352
- type="button"
353
- size="icon"
354
- aria-label="Send"
355
- onClick={submit}
356
- disabled={disabled || !onSubmit}
357
- className="h-9 w-9 shrink-0 rounded-full text-white hover:opacity-90 disabled:opacity-60"
358
- style={{ background: MESSENGER_GRADIENT }}
359
- >
360
- <SendHorizonal className="size-4" />
361
- </Button>
362
- ) : (
363
- <button
364
- type="button"
365
- aria-label="Like"
366
- disabled
367
- className="flex size-9 shrink-0 items-center justify-center rounded-full text-[#0084FF] disabled:opacity-100"
368
- >
369
- <ThumbsUp className="size-5" strokeWidth={2.2} />
370
- </button>
371
- )}
529
+ <button
530
+ type="button"
531
+ aria-label="Send"
532
+ onClick={submit}
533
+ disabled={sendDisabled}
534
+ className={cn(
535
+ "ml-1 flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
536
+ "hover:bg-secondary disabled:hover:bg-transparent",
537
+ )}
538
+ style={{ color: MESSENGER_BRAND_COLOR }}
539
+ >
540
+ <SendHorizonal
541
+ className={cn(
542
+ "size-5 transition-opacity",
543
+ sendDisabled && "opacity-40",
544
+ )}
545
+ strokeWidth={2.2}
546
+ />
547
+ </button>
372
548
  </div>
373
549
  </div>
374
550
  );