@adres/mgda-ui 0.0.0-stage → 0.0.1
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/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- package/README.md +0 -3
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronDown } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
interface StepContextValue {
|
|
8
|
+
open: boolean
|
|
9
|
+
setOpen: (open: boolean) => void
|
|
10
|
+
contentId: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const StepContext = React.createContext<StepContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
function useStep(part: string) {
|
|
16
|
+
const context = React.useContext(StepContext)
|
|
17
|
+
if (!context) throw new Error(`${part} must be used inside ChainOfThoughtStep`)
|
|
18
|
+
return context
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ChainOfThoughtProps = React.HTMLAttributes<HTMLDivElement>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ChainOfThought - the reasoning behind an assistant answer as a vertical
|
|
25
|
+
* chain of steps. Each step is a one-line trigger that expands to its detail,
|
|
26
|
+
* and a thin rule joins one step to the next.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <ChainOfThought>
|
|
30
|
+
* <ChainOfThoughtStep>
|
|
31
|
+
* <ChainOfThoughtTrigger>Searching Yas Island listings</ChainOfThoughtTrigger>
|
|
32
|
+
* <ChainOfThoughtContent>
|
|
33
|
+
* <ChainOfThoughtItem>Found 42 townhouses under AED 5M</ChainOfThoughtItem>
|
|
34
|
+
* </ChainOfThoughtContent>
|
|
35
|
+
* </ChainOfThoughtStep>
|
|
36
|
+
* </ChainOfThought>
|
|
37
|
+
*/
|
|
38
|
+
export const ChainOfThought = React.forwardRef<HTMLDivElement, ChainOfThoughtProps>(
|
|
39
|
+
({ className, ...props }, ref) => (
|
|
40
|
+
<div ref={ref} data-slot="chain-of-thought" className={cn("ax:flex ax:flex-col", className)} {...props} />
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
ChainOfThought.displayName = "ChainOfThought"
|
|
44
|
+
|
|
45
|
+
export interface ChainOfThoughtStepProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
46
|
+
open?: boolean
|
|
47
|
+
defaultOpen?: boolean
|
|
48
|
+
onOpenChange?: (open: boolean) => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One step in the chain. Closed by default; control it with `open` and `onOpenChange`. */
|
|
52
|
+
export const ChainOfThoughtStep = React.forwardRef<HTMLDivElement, ChainOfThoughtStepProps>(
|
|
53
|
+
({ open: openProp, defaultOpen = false, onOpenChange, className, children, ...props }, ref) => {
|
|
54
|
+
const contentId = React.useId()
|
|
55
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultOpen)
|
|
56
|
+
const open = openProp ?? uncontrolled
|
|
57
|
+
const setOpen = React.useCallback(
|
|
58
|
+
(next: boolean) => {
|
|
59
|
+
if (openProp === undefined) setUncontrolled(next)
|
|
60
|
+
onOpenChange?.(next)
|
|
61
|
+
},
|
|
62
|
+
[openProp, onOpenChange],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<StepContext.Provider value={{ open, setOpen, contentId }}>
|
|
67
|
+
<div
|
|
68
|
+
ref={ref}
|
|
69
|
+
data-slot="chain-of-thought-step"
|
|
70
|
+
data-state={open ? "open" : "closed"}
|
|
71
|
+
className={cn("ax:group/cot-step", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
<div className="ax:flex ax:w-4 ax:justify-center ax:group-last/cot-step:hidden" aria-hidden>
|
|
76
|
+
<div className="ax:h-sm ax:w-px ax:bg-border" />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</StepContext.Provider>
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
ChainOfThoughtStep.displayName = "ChainOfThoughtStep"
|
|
84
|
+
|
|
85
|
+
export interface ChainOfThoughtTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
86
|
+
/** Replaces the default dot. Swaps to a chevron on hover and focus unless `swapIconOnHover` is false. */
|
|
87
|
+
icon?: React.ReactNode
|
|
88
|
+
swapIconOnHover?: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The step's one-line label; toggles its content. */
|
|
92
|
+
export const ChainOfThoughtTrigger = React.forwardRef<HTMLButtonElement, ChainOfThoughtTriggerProps>(
|
|
93
|
+
({ icon, swapIconOnHover = true, className, children, onClick, ...props }, ref) => {
|
|
94
|
+
const { open, setOpen, contentId } = useStep("ChainOfThoughtTrigger")
|
|
95
|
+
const swap = Boolean(icon) && swapIconOnHover
|
|
96
|
+
const chevron = cn("ax:size-4 ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
ref={ref}
|
|
101
|
+
type="button"
|
|
102
|
+
aria-expanded={open}
|
|
103
|
+
aria-controls={contentId}
|
|
104
|
+
onClick={(event) => {
|
|
105
|
+
onClick?.(event)
|
|
106
|
+
if (!event.defaultPrevented) setOpen(!open)
|
|
107
|
+
}}
|
|
108
|
+
className={cn(
|
|
109
|
+
"ax:group/cot-trigger ax:flex ax:items-center ax:gap-sm ax:rounded-xs ax:text-start ax:t-body-sm ax:text-muted-foreground ax:transition-colors",
|
|
110
|
+
"ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
>
|
|
115
|
+
<span className="ax:relative ax:inline-flex ax:size-4 ax:shrink-0 ax:items-center ax:justify-center ax:[&_svg]:size-4" aria-hidden>
|
|
116
|
+
{icon ? (
|
|
117
|
+
<span className={cn("ax:inline-flex ax:transition-opacity", swap && "ax:group-hover/cot-trigger:opacity-0 ax:group-focus-visible/cot-trigger:opacity-0")}>
|
|
118
|
+
{icon}
|
|
119
|
+
</span>
|
|
120
|
+
) : (
|
|
121
|
+
<span className="ax:size-2 ax:rounded-full ax:bg-current" />
|
|
122
|
+
)}
|
|
123
|
+
{swap && (
|
|
124
|
+
<ChevronDown
|
|
125
|
+
className={cn(chevron, "ax:absolute ax:opacity-0 ax:group-hover/cot-trigger:opacity-100 ax:group-focus-visible/cot-trigger:opacity-100")}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
</span>
|
|
129
|
+
<span className="ax:min-w-0">{children}</span>
|
|
130
|
+
{!swap && <ChevronDown className={cn(chevron, "ax:shrink-0")} aria-hidden />}
|
|
131
|
+
</button>
|
|
132
|
+
)
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
ChainOfThoughtTrigger.displayName = "ChainOfThoughtTrigger"
|
|
136
|
+
|
|
137
|
+
export type ChainOfThoughtContentProps = React.HTMLAttributes<HTMLDivElement>
|
|
138
|
+
|
|
139
|
+
/** The step's detail, beside a rule that carries on to the next step. Rendered only while open. */
|
|
140
|
+
export const ChainOfThoughtContent = React.forwardRef<HTMLDivElement, ChainOfThoughtContentProps>(
|
|
141
|
+
({ className, children, ...props }, ref) => {
|
|
142
|
+
const { open, contentId } = useStep("ChainOfThoughtContent")
|
|
143
|
+
if (!open) return null
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
ref={ref}
|
|
147
|
+
id={contentId}
|
|
148
|
+
data-slot="chain-of-thought-content"
|
|
149
|
+
className={cn("ax:grid ax:grid-cols-[1rem_minmax(0,1fr)] ax:gap-x-sm", className)}
|
|
150
|
+
{...props}
|
|
151
|
+
>
|
|
152
|
+
<div className="ax:flex ax:justify-center" aria-hidden>
|
|
153
|
+
<div className="ax:w-px ax:bg-border ax:group-last/cot-step:bg-transparent" />
|
|
154
|
+
</div>
|
|
155
|
+
<div className="ax:mt-sm ax:flex ax:min-w-0 ax:flex-col ax:gap-sm">{children}</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
ChainOfThoughtContent.displayName = "ChainOfThoughtContent"
|
|
161
|
+
|
|
162
|
+
export type ChainOfThoughtItemProps = React.HTMLAttributes<HTMLDivElement>
|
|
163
|
+
|
|
164
|
+
/** One line of detail inside a step: a finding, a source, or a short note. */
|
|
165
|
+
export const ChainOfThoughtItem = React.forwardRef<HTMLDivElement, ChainOfThoughtItemProps>(
|
|
166
|
+
({ className, ...props }, ref) => (
|
|
167
|
+
<div ref={ref} data-slot="chain-of-thought-item" className={cn("ax:t-body-sm ax:text-muted-foreground", className)} {...props} />
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
ChainOfThoughtItem.displayName = "ChainOfThoughtItem"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { ChatContainerContent, ChatContainerRoot, ChatScrollButton } from "./chat-container"
|
|
4
|
+
import { ChatMessage } from "./chat-message"
|
|
5
|
+
import { Button } from "./button"
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof ChatContainerRoot> = {
|
|
8
|
+
title: "Components/ChatContainer",
|
|
9
|
+
component: ChatContainerRoot,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof ChatContainerRoot>
|
|
16
|
+
|
|
17
|
+
const TURNS = [
|
|
18
|
+
"Any townhouses on Yas Island under AED 5M?",
|
|
19
|
+
"Yes. 42 townhouses in Yas Acres and Noya are under AED 5M, and 11 have a private garden.",
|
|
20
|
+
"Which is cheapest?",
|
|
21
|
+
"The lowest asking price is AED 4.2M for a 3-bed in Yas Acres.",
|
|
22
|
+
"How does that compare with recent sales?",
|
|
23
|
+
"The median registered sale in Yas Acres over the last 12 months is AED 4.6M, so it is priced below recent sales.",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function Conversation() {
|
|
27
|
+
const [turns, setTurns] = React.useState(TURNS)
|
|
28
|
+
return (
|
|
29
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
30
|
+
<ChatContainerRoot className="ax:h-80 ax:rounded-md ax:border">
|
|
31
|
+
<ChatContainerContent className="ax:gap-lg ax:p-md">
|
|
32
|
+
{turns.map((text, index) => (
|
|
33
|
+
<ChatMessage key={index} from={index % 2 === 0 ? "user" : "assistant"}>{text}</ChatMessage>
|
|
34
|
+
))}
|
|
35
|
+
</ChatContainerContent>
|
|
36
|
+
<ChatScrollButton className="ax:absolute ax:bottom-sm ax:left-1/2 ax:-translate-x-1/2" />
|
|
37
|
+
</ChatContainerRoot>
|
|
38
|
+
<div>
|
|
39
|
+
<Button variant="outline" size="sm" onClick={() => setTurns((prev) => [...prev, "Another reply from the assistant."])}>
|
|
40
|
+
Add reply
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Default: Story = { render: () => <Conversation /> }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { ChatContainerContent, ChatContainerRoot, ChatScrollButton, useChatContainer } from "./chat-container"
|
|
5
|
+
|
|
6
|
+
describe("ChatContainer", () => {
|
|
7
|
+
it("renders a focusable log named Conversation by default", () => {
|
|
8
|
+
const html = renderToStaticMarkup(
|
|
9
|
+
<ChatContainerRoot>
|
|
10
|
+
<ChatContainerContent>Hello</ChatContainerContent>
|
|
11
|
+
</ChatContainerRoot>,
|
|
12
|
+
)
|
|
13
|
+
expect(html).toContain('role="log"')
|
|
14
|
+
expect(html).toContain('aria-label="Conversation"')
|
|
15
|
+
expect(html).toContain('tabindex="0"')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("lets the caller rename the log", () => {
|
|
19
|
+
const html = renderToStaticMarkup(
|
|
20
|
+
<ChatContainerRoot>
|
|
21
|
+
<ChatContainerContent aria-label="Chat with Adres">Hello</ChatContainerContent>
|
|
22
|
+
</ChatContainerRoot>,
|
|
23
|
+
)
|
|
24
|
+
expect(html).toContain('aria-label="Chat with Adres"')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("starts at the bottom, so the scroll button is hidden and out of the tab order", () => {
|
|
28
|
+
const html = renderToStaticMarkup(
|
|
29
|
+
<ChatContainerRoot>
|
|
30
|
+
<ChatScrollButton />
|
|
31
|
+
</ChatContainerRoot>,
|
|
32
|
+
)
|
|
33
|
+
expect(html).toContain('aria-hidden="true"')
|
|
34
|
+
expect(html).toContain('tabindex="-1"')
|
|
35
|
+
expect(html).toContain('aria-label="Scroll to latest"')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("exposes the scroll state through useChatContainer", () => {
|
|
39
|
+
function Probe() {
|
|
40
|
+
const { isAtBottom, hasUnseen } = useChatContainer()
|
|
41
|
+
return <span>{`${isAtBottom}-${hasUnseen}`}</span>
|
|
42
|
+
}
|
|
43
|
+
expect(renderToStaticMarkup(<ChatContainerRoot><Probe /></ChatContainerRoot>)).toContain("true-false")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("throws when a part is used outside the root", () => {
|
|
47
|
+
expect(() => renderToStaticMarkup(<ChatScrollButton />)).toThrow(/ChatContainerRoot/)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ArrowDown } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { Button, type ButtonProps } from "./button"
|
|
7
|
+
|
|
8
|
+
/** How close to the bottom, in px, still counts as reading the latest. */
|
|
9
|
+
const STICK_THRESHOLD = 48
|
|
10
|
+
|
|
11
|
+
export interface ChatContainerState {
|
|
12
|
+
/** True while the log follows new content: the reader is at the bottom or jumped back to it. */
|
|
13
|
+
isAtBottom: boolean
|
|
14
|
+
/** True when content arrived while the reader was scrolled up. Clears at the bottom. */
|
|
15
|
+
hasUnseen: boolean
|
|
16
|
+
/** Scrolls to the latest and resumes following. Call it when the reader sends a message. */
|
|
17
|
+
scrollToBottom: (behavior?: ScrollBehavior) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ChatContainerContextValue extends ChatContainerState {
|
|
21
|
+
scrollRef: React.RefObject<HTMLDivElement | null>
|
|
22
|
+
onScroll: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ChatContainerContext = React.createContext<ChatContainerContextValue | null>(null)
|
|
26
|
+
|
|
27
|
+
function useChatContainerContext(part: string) {
|
|
28
|
+
const context = React.useContext(ChatContainerContext)
|
|
29
|
+
if (!context) throw new Error(`${part} must be used inside ChatContainerRoot`)
|
|
30
|
+
return context
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Scroll state for the nearest ChatContainerRoot, for custom controls and composers. */
|
|
34
|
+
export function useChatContainer(): ChatContainerState {
|
|
35
|
+
const { isAtBottom, hasUnseen, scrollToBottom } = useChatContainerContext("useChatContainer")
|
|
36
|
+
return { isAtBottom, hasUnseen, scrollToBottom }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function distanceFromBottom(el: HTMLElement) {
|
|
40
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ChatContainerRootProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ChatContainer - a conversation log that sticks to the latest message. It
|
|
47
|
+
* follows new and streaming content while the reader is at the bottom, stays
|
|
48
|
+
* put once they scroll up, and offers a button back down.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <ChatContainerRoot className="ax:h-[480px]">
|
|
52
|
+
* <ChatContainerContent className="ax:gap-lg ax:p-md">{messages}</ChatContainerContent>
|
|
53
|
+
* <ChatScrollButton className="ax:absolute ax:bottom-sm ax:left-1/2 ax:-translate-x-1/2" />
|
|
54
|
+
* </ChatContainerRoot>
|
|
55
|
+
*/
|
|
56
|
+
export const ChatContainerRoot = React.forwardRef<HTMLDivElement, ChatContainerRootProps>(
|
|
57
|
+
({ className, children, ...props }, ref) => {
|
|
58
|
+
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
59
|
+
const following = React.useRef(true)
|
|
60
|
+
const lastScrollTop = React.useRef(0)
|
|
61
|
+
const lastScrollHeight = React.useRef(0)
|
|
62
|
+
const [isAtBottom, setIsAtBottom] = React.useState(true)
|
|
63
|
+
const [hasUnseen, setHasUnseen] = React.useState(false)
|
|
64
|
+
|
|
65
|
+
const scrollToBottom = React.useCallback((behavior: ScrollBehavior = "smooth") => {
|
|
66
|
+
const el = scrollRef.current
|
|
67
|
+
if (!el) return
|
|
68
|
+
following.current = true
|
|
69
|
+
setIsAtBottom(true)
|
|
70
|
+
setHasUnseen(false)
|
|
71
|
+
el.scrollTo({ top: el.scrollHeight, behavior })
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
// Only a scroll up stops following, so a smooth scroll down never flickers the button back.
|
|
75
|
+
const onScroll = React.useCallback(() => {
|
|
76
|
+
const el = scrollRef.current
|
|
77
|
+
if (!el) return
|
|
78
|
+
const near = distanceFromBottom(el) < STICK_THRESHOLD
|
|
79
|
+
if (near) {
|
|
80
|
+
following.current = true
|
|
81
|
+
setHasUnseen(false)
|
|
82
|
+
} else if (el.scrollTop < lastScrollTop.current) {
|
|
83
|
+
following.current = false
|
|
84
|
+
}
|
|
85
|
+
lastScrollTop.current = el.scrollTop
|
|
86
|
+
setIsAtBottom(following.current)
|
|
87
|
+
}, [])
|
|
88
|
+
|
|
89
|
+
React.useLayoutEffect(() => {
|
|
90
|
+
const el = scrollRef.current
|
|
91
|
+
if (!el) return
|
|
92
|
+
el.scrollTop = el.scrollHeight
|
|
93
|
+
lastScrollTop.current = el.scrollTop
|
|
94
|
+
lastScrollHeight.current = el.scrollHeight
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
// Mutations catch new turns and streamed text; resizes catch the viewport changing.
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
const el = scrollRef.current
|
|
100
|
+
if (!el) return
|
|
101
|
+
const update = () => {
|
|
102
|
+
const grew = el.scrollHeight > lastScrollHeight.current
|
|
103
|
+
lastScrollHeight.current = el.scrollHeight
|
|
104
|
+
if (following.current) {
|
|
105
|
+
el.scrollTop = el.scrollHeight
|
|
106
|
+
lastScrollTop.current = el.scrollTop
|
|
107
|
+
} else if (grew) {
|
|
108
|
+
setHasUnseen(true)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mutations = new MutationObserver(update)
|
|
112
|
+
mutations.observe(el, { childList: true, subtree: true, characterData: true })
|
|
113
|
+
const resizes = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update)
|
|
114
|
+
resizes?.observe(el)
|
|
115
|
+
return () => {
|
|
116
|
+
mutations.disconnect()
|
|
117
|
+
resizes?.disconnect()
|
|
118
|
+
}
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
const value = React.useMemo(
|
|
122
|
+
() => ({ isAtBottom, hasUnseen, scrollToBottom, scrollRef, onScroll }),
|
|
123
|
+
[isAtBottom, hasUnseen, scrollToBottom, onScroll],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<ChatContainerContext.Provider value={value}>
|
|
128
|
+
<div
|
|
129
|
+
ref={ref}
|
|
130
|
+
data-slot="chat-container"
|
|
131
|
+
className={cn("ax:relative ax:flex ax:min-h-0 ax:flex-col", className)}
|
|
132
|
+
{...props}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</div>
|
|
136
|
+
</ChatContainerContext.Provider>
|
|
137
|
+
)
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
ChatContainerRoot.displayName = "ChatContainerRoot"
|
|
141
|
+
|
|
142
|
+
export interface ChatContainerContentProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
143
|
+
|
|
144
|
+
/** The scrolling log that holds the turns. Named "Conversation" unless you pass `aria-label`. */
|
|
145
|
+
export const ChatContainerContent = React.forwardRef<HTMLDivElement, ChatContainerContentProps>(
|
|
146
|
+
({ className, onScroll: onScrollProp, ...props }, ref) => {
|
|
147
|
+
const { scrollRef, onScroll } = useChatContainerContext("ChatContainerContent")
|
|
148
|
+
const setRefs = React.useCallback(
|
|
149
|
+
(node: HTMLDivElement | null) => {
|
|
150
|
+
scrollRef.current = node
|
|
151
|
+
if (typeof ref === "function") ref(node)
|
|
152
|
+
else if (ref) ref.current = node
|
|
153
|
+
},
|
|
154
|
+
[ref, scrollRef],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
ref={setRefs}
|
|
160
|
+
data-slot="chat-container-content"
|
|
161
|
+
role="log"
|
|
162
|
+
aria-label="Conversation"
|
|
163
|
+
tabIndex={0}
|
|
164
|
+
onScroll={(event) => {
|
|
165
|
+
onScroll()
|
|
166
|
+
onScrollProp?.(event)
|
|
167
|
+
}}
|
|
168
|
+
className={cn(
|
|
169
|
+
"ax:flex ax:min-h-0 ax:flex-1 ax:flex-col ax:overflow-y-auto",
|
|
170
|
+
"ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-inset",
|
|
171
|
+
className,
|
|
172
|
+
)}
|
|
173
|
+
{...props}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
ChatContainerContent.displayName = "ChatContainerContent"
|
|
179
|
+
|
|
180
|
+
export interface ChatScrollButtonProps extends ButtonProps {}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Jumps back to the latest. Shown only while the reader is scrolled up: an
|
|
184
|
+
* arrow on its own, or "New messages" once content arrives. Position it yourself.
|
|
185
|
+
*/
|
|
186
|
+
export const ChatScrollButton = React.forwardRef<HTMLButtonElement, ChatScrollButtonProps>(
|
|
187
|
+
({ className, variant = "outline", size, children, onClick, ...props }, ref) => {
|
|
188
|
+
const { isAtBottom, hasUnseen, scrollToBottom } = useChatContainerContext("ChatScrollButton")
|
|
189
|
+
const label = hasUnseen ? "New messages" : null
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<Button
|
|
193
|
+
ref={ref}
|
|
194
|
+
type="button"
|
|
195
|
+
data-slot="chat-scroll-button"
|
|
196
|
+
variant={variant}
|
|
197
|
+
size={size ?? (children || label ? "sm" : "icon-sm")}
|
|
198
|
+
aria-label={children || label ? undefined : "Scroll to latest"}
|
|
199
|
+
aria-hidden={isAtBottom || undefined}
|
|
200
|
+
tabIndex={isAtBottom ? -1 : undefined}
|
|
201
|
+
onClick={(event) => {
|
|
202
|
+
onClick?.(event)
|
|
203
|
+
if (!event.defaultPrevented) scrollToBottom()
|
|
204
|
+
}}
|
|
205
|
+
className={cn(
|
|
206
|
+
"ax:rounded-full ax:bg-background ax:shadow-sm ax:transition-[opacity,translate] ax:duration-150 ax:motion-reduce:transition-none",
|
|
207
|
+
isAtBottom ? "ax:pointer-events-none ax:translate-y-sm ax:opacity-0" : "ax:opacity-100",
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
{...props}
|
|
211
|
+
>
|
|
212
|
+
{children ?? (
|
|
213
|
+
<>
|
|
214
|
+
<ArrowDown />
|
|
215
|
+
{label}
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
</Button>
|
|
219
|
+
)
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
ChatScrollButton.displayName = "ChatScrollButton"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { ChatAvatar, ChatDivider, ChatMessage } from "./chat-message"
|
|
4
|
+
import { MessageActions } from "./message-actions"
|
|
5
|
+
import { Text } from "./text"
|
|
6
|
+
import { TooltipProvider } from "./tooltip"
|
|
7
|
+
|
|
8
|
+
const QUESTION = "Any townhouses on Yas Island under AED 5M with a private garden?"
|
|
9
|
+
const ANSWER = "Yes. Yas Acres has three-bed townhouses with private gardens from AED 4.2M, and two are listed this week."
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof ChatMessage> = {
|
|
12
|
+
title: "Components/ChatMessage",
|
|
13
|
+
component: ChatMessage,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
decorators: [(Story) => <TooltipProvider><div style={{ maxWidth: 640 }}><Story /></div></TooltipProvider>],
|
|
16
|
+
argTypes: {
|
|
17
|
+
from: { control: "inline-radio", options: ["assistant", "user"] },
|
|
18
|
+
status: { control: "inline-radio", options: ["sent", "failed"] },
|
|
19
|
+
onRetry: { control: false },
|
|
20
|
+
},
|
|
21
|
+
args: { from: "assistant", children: ANSWER, meta: "10:42" },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof ChatMessage>
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {}
|
|
28
|
+
|
|
29
|
+
export const Conversation: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
|
32
|
+
<ChatDivider label="Today" />
|
|
33
|
+
<ChatMessage from="user" meta="Delivered · 10:41">{QUESTION}</ChatMessage>
|
|
34
|
+
<ChatMessage from="assistant" actions={<MessageActions copyText={ANSWER} onRegenerate={() => undefined} revealOnHover />} meta="10:42">
|
|
35
|
+
{ANSWER}
|
|
36
|
+
</ChatMessage>
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const FailedUserMessage: Story = {
|
|
42
|
+
args: { from: "user", status: "failed", children: QUESTION, onRetry: () => undefined },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const WithAvatar: Story = { args: { avatar: <ChatAvatar /> } }
|
|
46
|
+
|
|
47
|
+
export const CustomContent: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
children: <Text size="2" color="muted">Searched 3 sources, compared 12 listings</Text>,
|
|
50
|
+
meta: undefined,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { ChatAvatar, ChatDivider, ChatMessage } from "./chat-message"
|
|
5
|
+
|
|
6
|
+
describe("ChatMessage", () => {
|
|
7
|
+
it("names each turn by who wrote it", () => {
|
|
8
|
+
expect(renderToStaticMarkup(<ChatMessage from="assistant">Hi</ChatMessage>)).toContain('aria-label="Assistant message"')
|
|
9
|
+
expect(renderToStaticMarkup(<ChatMessage from="user">Hi</ChatMessage>)).toContain('aria-label="Your message"')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it("replaces the meta line with a retry action when a user message failed", () => {
|
|
13
|
+
const html = renderToStaticMarkup(
|
|
14
|
+
<ChatMessage from="user" status="failed" meta="Delivered · 10:41" onRetry={() => undefined}>Hi</ChatMessage>,
|
|
15
|
+
)
|
|
16
|
+
expect(html).toContain("Not delivered.")
|
|
17
|
+
expect(html).toMatch(/<button type="button"[^>]*>Retry<\/button>/)
|
|
18
|
+
expect(html).not.toContain("Delivered · 10:41")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("shows no avatar on assistant turns unless one is passed", () => {
|
|
22
|
+
expect(renderToStaticMarkup(<ChatMessage from="assistant">Hi</ChatMessage>)).not.toContain("<svg")
|
|
23
|
+
expect(renderToStaticMarkup(<ChatMessage from="assistant" avatar={<ChatAvatar />}>Hi</ChatMessage>)).toContain("<svg")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("puts assistant text on the page and user text in a bubble", () => {
|
|
27
|
+
expect(renderToStaticMarkup(<ChatMessage from="assistant">Hi</ChatMessage>)).not.toContain("ax:bg-muted")
|
|
28
|
+
expect(renderToStaticMarkup(<ChatMessage from="user">Hi</ChatMessage>)).toContain("ax:bg-muted")
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("ChatDivider", () => {
|
|
33
|
+
it("is a labelled separator", () => {
|
|
34
|
+
expect(renderToStaticMarkup(<ChatDivider label="Today" />)).toMatch(/role="separator" aria-label="Today"/)
|
|
35
|
+
})
|
|
36
|
+
})
|