@elabs-ai/components-ai 4.0.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 (180) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/_audio-player-media-chrome-KA5DY54G.js +81 -0
  4. package/dist/_audio-player-media-chrome-KA5DY54G.js.map +1 -0
  5. package/dist/_flow-boundary-D63PJ65S.js +186 -0
  6. package/dist/_flow-boundary-D63PJ65S.js.map +1 -0
  7. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +11 -0
  8. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +1 -0
  9. package/dist/_persona-rive-RFR2EUWP.js +164 -0
  10. package/dist/_persona-rive-RFR2EUWP.js.map +1 -0
  11. package/dist/index.d.ts +4079 -0
  12. package/dist/index.js +11770 -0
  13. package/dist/index.js.map +1 -0
  14. package/package.json +87 -0
  15. package/src/_audio-player-media-chrome.tsx +146 -0
  16. package/src/_chat-shell-rail.tsx +18 -0
  17. package/src/_code-block-theme.test.ts +134 -0
  18. package/src/_code-block-theme.ts +184 -0
  19. package/src/_flow-boundary.tsx +237 -0
  20. package/src/_flow-lazy.ts +34 -0
  21. package/src/_interactive-terminal-xterm.ts +32 -0
  22. package/src/_lazy-mermaid.test.ts +101 -0
  23. package/src/_lazy-mermaid.ts +117 -0
  24. package/src/_persona-rive.tsx +252 -0
  25. package/src/_streamdown-i18n.ts +119 -0
  26. package/src/agent-timeline.stories.tsx +66 -0
  27. package/src/agent-timeline.test.tsx +74 -0
  28. package/src/agent-timeline.tsx +106 -0
  29. package/src/agent.tsx +113 -0
  30. package/src/agentic-workspace.stories.tsx +1099 -0
  31. package/src/artifact.stories.tsx +62 -0
  32. package/src/artifact.test.tsx +112 -0
  33. package/src/artifact.tsx +167 -0
  34. package/src/asset-preview.stories.tsx +100 -0
  35. package/src/asset-preview.test.tsx +126 -0
  36. package/src/asset-preview.tsx +218 -0
  37. package/src/attachments.tsx +393 -0
  38. package/src/audio-player.stories.tsx +99 -0
  39. package/src/audio-player.tsx +170 -0
  40. package/src/blocks-ai-composer.stories.tsx +83 -0
  41. package/src/canvas.stories.tsx +142 -0
  42. package/src/canvas.tsx +29 -0
  43. package/src/chain-of-thought.tsx +193 -0
  44. package/src/chat-greeting.stories.tsx +96 -0
  45. package/src/chat-greeting.test.tsx +91 -0
  46. package/src/chat-greeting.tsx +91 -0
  47. package/src/chat-shell.stories.tsx +123 -0
  48. package/src/chat-shell.test.tsx +113 -0
  49. package/src/chat-shell.tsx +97 -0
  50. package/src/chat.stories.tsx +99 -0
  51. package/src/checkpoint.tsx +54 -0
  52. package/src/code-block.stories.tsx +81 -0
  53. package/src/code-block.test.tsx +191 -0
  54. package/src/code-block.tsx +673 -0
  55. package/src/commit.tsx +343 -0
  56. package/src/composer.stories.tsx +188 -0
  57. package/src/composer.test.tsx +182 -0
  58. package/src/composer.tsx +202 -0
  59. package/src/confirmation.stories.tsx +61 -0
  60. package/src/confirmation.test.tsx +136 -0
  61. package/src/confirmation.tsx +273 -0
  62. package/src/connection.tsx +23 -0
  63. package/src/context-panel.stories.tsx +137 -0
  64. package/src/context-panel.test.tsx +198 -0
  65. package/src/context-panel.tsx +609 -0
  66. package/src/context.stories.tsx +16 -0
  67. package/src/context.tsx +363 -0
  68. package/src/controls.tsx +20 -0
  69. package/src/conversation.stories.tsx +26 -0
  70. package/src/conversation.test.tsx +123 -0
  71. package/src/conversation.tsx +143 -0
  72. package/src/dark-theme-variant.test.ts +47 -0
  73. package/src/edge.tsx +33 -0
  74. package/src/environment-variables.tsx +304 -0
  75. package/src/file-tree.stories.tsx +55 -0
  76. package/src/file-tree.test.tsx +101 -0
  77. package/src/file-tree.tsx +390 -0
  78. package/src/gallery.stories.tsx +182 -0
  79. package/src/gallery.test.tsx +150 -0
  80. package/src/gallery.tsx +737 -0
  81. package/src/grouped-parts.stories.tsx +104 -0
  82. package/src/grouped-parts.test.tsx +167 -0
  83. package/src/grouped-parts.tsx +247 -0
  84. package/src/image.stories.tsx +67 -0
  85. package/src/image.test.tsx +105 -0
  86. package/src/image.tsx +115 -0
  87. package/src/index.ts +93 -0
  88. package/src/inline-citation.stories.tsx +91 -0
  89. package/src/inline-citation.test.tsx +96 -0
  90. package/src/inline-citation.tsx +314 -0
  91. package/src/interactive-terminal.stories.tsx +165 -0
  92. package/src/interactive-terminal.test.tsx +448 -0
  93. package/src/interactive-terminal.tsx +444 -0
  94. package/src/jsx-preview.stories.tsx +265 -0
  95. package/src/jsx-preview.test.tsx +75 -0
  96. package/src/jsx-preview.tsx +409 -0
  97. package/src/markdown-view.stories.tsx +57 -0
  98. package/src/markdown-view.test.tsx +36 -0
  99. package/src/markdown-view.tsx +113 -0
  100. package/src/message-edit.stories.tsx +150 -0
  101. package/src/message-edit.test.tsx +115 -0
  102. package/src/message-edit.tsx +336 -0
  103. package/src/message-feedback.stories.tsx +53 -0
  104. package/src/message-feedback.test.tsx +62 -0
  105. package/src/message-feedback.tsx +117 -0
  106. package/src/message-form-spec.ts +349 -0
  107. package/src/message-form.stories.tsx +182 -0
  108. package/src/message-form.test.tsx +227 -0
  109. package/src/message-form.tsx +845 -0
  110. package/src/message-table-spec.ts +281 -0
  111. package/src/message-table.stories.tsx +176 -0
  112. package/src/message-table.test.tsx +144 -0
  113. package/src/message-table.tsx +366 -0
  114. package/src/message.stories.tsx +262 -0
  115. package/src/message.test.tsx +508 -0
  116. package/src/message.tsx +619 -0
  117. package/src/mic-selector.tsx +339 -0
  118. package/src/microcopy.test.tsx +123 -0
  119. package/src/model-selector.stories.tsx +73 -0
  120. package/src/model-selector.test.tsx +55 -0
  121. package/src/model-selector.tsx +225 -0
  122. package/src/motion-config.tsx +49 -0
  123. package/src/node.tsx +71 -0
  124. package/src/open-in-chat.tsx +340 -0
  125. package/src/package-info.tsx +205 -0
  126. package/src/panel.tsx +20 -0
  127. package/src/part-groups.ts +258 -0
  128. package/src/persona-sources.ts +59 -0
  129. package/src/persona.stories.tsx +111 -0
  130. package/src/persona.test.tsx +67 -0
  131. package/src/persona.tsx +148 -0
  132. package/src/plan.tsx +135 -0
  133. package/src/prompt-input.stories.tsx +261 -0
  134. package/src/prompt-input.test.tsx +392 -0
  135. package/src/prompt-input.tsx +1668 -0
  136. package/src/queue.tsx +237 -0
  137. package/src/reasoning.stories.tsx +81 -0
  138. package/src/reasoning.test.tsx +50 -0
  139. package/src/reasoning.tsx +229 -0
  140. package/src/sandbox.stories.tsx +62 -0
  141. package/src/sandbox.test.tsx +42 -0
  142. package/src/sandbox.tsx +149 -0
  143. package/src/schema-display.tsx +412 -0
  144. package/src/selection-toolbar.stories.tsx +86 -0
  145. package/src/selection-toolbar.test.tsx +89 -0
  146. package/src/selection-toolbar.tsx +192 -0
  147. package/src/shimmer.stories.tsx +10 -0
  148. package/src/shimmer.tsx +81 -0
  149. package/src/snippet.stories.tsx +19 -0
  150. package/src/snippet.test.tsx +116 -0
  151. package/src/snippet.tsx +138 -0
  152. package/src/sources.stories.tsx +34 -0
  153. package/src/sources.test.tsx +129 -0
  154. package/src/sources.tsx +93 -0
  155. package/src/speech-input.tsx +312 -0
  156. package/src/stack-trace.tsx +479 -0
  157. package/src/streamdown-i18n.test.tsx +163 -0
  158. package/src/suggestion.stories.tsx +75 -0
  159. package/src/suggestion.test.tsx +72 -0
  160. package/src/suggestion.tsx +120 -0
  161. package/src/task.stories.tsx +22 -0
  162. package/src/task.test.tsx +36 -0
  163. package/src/task.tsx +89 -0
  164. package/src/templates-ai-assistant.stories.tsx +142 -0
  165. package/src/terminal.tsx +244 -0
  166. package/src/test-results.stories.tsx +177 -0
  167. package/src/test-results.test.tsx +241 -0
  168. package/src/test-results.tsx +408 -0
  169. package/src/tool-result-card.stories.tsx +143 -0
  170. package/src/tool-result-card.test.tsx +72 -0
  171. package/src/tool-result-card.tsx +86 -0
  172. package/src/tool.stories.tsx +60 -0
  173. package/src/tool.test.tsx +63 -0
  174. package/src/tool.tsx +235 -0
  175. package/src/toolbar.tsx +44 -0
  176. package/src/transcription.tsx +118 -0
  177. package/src/voice-selector.tsx +469 -0
  178. package/src/web-preview.stories.tsx +67 -0
  179. package/src/web-preview.test.tsx +117 -0
  180. package/src/web-preview.tsx +283 -0
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import {
4
+ Collapsible,
5
+ CollapsibleContent,
6
+ CollapsibleTrigger,
7
+ Skeleton,
8
+ Tabs,
9
+ TabsContent,
10
+ TabsList,
11
+ TabsTrigger,
12
+ useLocale,
13
+ } from "@elabs-ai/components-ui";
14
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
15
+ import type { ToolUIPart } from "ai";
16
+ import { ChevronDownIcon, Code } from "lucide-react";
17
+ import type { ComponentProps } from "react";
18
+ import { createContext, use, useMemo } from "react";
19
+
20
+ import { getStatusBadge } from "./tool";
21
+
22
+ /**
23
+ * Shared loading state (composition-patterns — lifted into the provider),
24
+ * mirroring Artifact: Sandbox is the only place that knows whether the run
25
+ * body has arrived; SandboxContent reads the interface and renders a
26
+ * skeleton, while SandboxHeader (an ordinary child of Sandbox) keeps
27
+ * rendering as passed.
28
+ */
29
+ const SandboxContext = createContext<{ loading: boolean }>({ loading: false });
30
+
31
+ export type SandboxProps = ComponentProps<typeof Collapsible> & {
32
+ /**
33
+ * The run output has not arrived yet (loading-states.md loading signal).
34
+ * SandboxContent renders a layout-shaped skeleton instead of its children;
35
+ * SandboxHeader keeps rendering as passed.
36
+ * @default false
37
+ */
38
+ loading?: boolean;
39
+ };
40
+
41
+ /** @deprecated Renamed to `SandboxProps`. */
42
+ export type SandboxRootProps = SandboxProps;
43
+
44
+ export const Sandbox = ({ className, loading = false, children, ...props }: SandboxProps) => {
45
+ const contextValue = useMemo(() => ({ loading }), [loading]);
46
+
47
+ return (
48
+ <SandboxContext.Provider value={contextValue}>
49
+ <Collapsible
50
+ className={cn("not-prose group mb-4 w-full overflow-hidden rounded-md border", className)}
51
+ defaultOpen
52
+ {...props}
53
+ >
54
+ {children}
55
+ </Collapsible>
56
+ </SandboxContext.Provider>
57
+ );
58
+ };
59
+
60
+ export interface SandboxHeaderProps {
61
+ title?: string;
62
+ state: ToolUIPart["state"];
63
+ className?: string;
64
+ }
65
+
66
+ export const SandboxHeader = ({ className, title, state, ...props }: SandboxHeaderProps) => (
67
+ <CollapsibleTrigger
68
+ className={cn("flex w-full items-center justify-between gap-4 p-3", className)}
69
+ {...props}
70
+ >
71
+ <div className="flex items-center gap-2">
72
+ <Code className="size-4 text-muted-foreground" />
73
+ <span className="font-medium text-sm">{title}</span>
74
+ {getStatusBadge(state)}
75
+ </div>
76
+ <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
77
+ </CollapsibleTrigger>
78
+ );
79
+
80
+ export type SandboxContentProps = ComponentProps<typeof CollapsibleContent>;
81
+
82
+ export const SandboxContent = ({ className, children, ...props }: SandboxContentProps) => {
83
+ const { loading } = use(SandboxContext);
84
+ const { t } = useLocale();
85
+
86
+ return (
87
+ <CollapsibleContent
88
+ className={cn(
89
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
90
+ className,
91
+ )}
92
+ {...props}
93
+ >
94
+ {loading ? (
95
+ <div className="space-y-2 p-3" role="status" aria-live="polite">
96
+ <span className="sr-only">{t("loading")}</span>
97
+ <Skeleton className="h-4 w-1/3" />
98
+ <Skeleton className="h-4 w-full" />
99
+ <Skeleton className="h-4 w-full" />
100
+ <Skeleton className="h-4 w-2/3" />
101
+ </div>
102
+ ) : (
103
+ children
104
+ )}
105
+ </CollapsibleContent>
106
+ );
107
+ };
108
+
109
+ export type SandboxTabsProps = ComponentProps<typeof Tabs>;
110
+
111
+ export const SandboxTabs = ({ className, ...props }: SandboxTabsProps) => (
112
+ <Tabs className={cn("w-full gap-0", className)} {...props} />
113
+ );
114
+
115
+ export type SandboxTabsBarProps = ComponentProps<"div">;
116
+
117
+ export const SandboxTabsBar = ({ className, ...props }: SandboxTabsBarProps) => (
118
+ <div
119
+ className={cn("flex w-full items-center border-border border-t border-b", className)}
120
+ {...props}
121
+ />
122
+ );
123
+
124
+ export type SandboxTabsListProps = ComponentProps<typeof TabsList>;
125
+
126
+ export const SandboxTabsList = ({ className, ...props }: SandboxTabsListProps) => (
127
+ <TabsList
128
+ className={cn("h-auto rounded-none border-0 bg-transparent p-0", className)}
129
+ {...props}
130
+ />
131
+ );
132
+
133
+ export type SandboxTabsTriggerProps = ComponentProps<typeof TabsTrigger>;
134
+
135
+ export const SandboxTabsTrigger = ({ className, ...props }: SandboxTabsTriggerProps) => (
136
+ <TabsTrigger
137
+ className={cn(
138
+ "rounded-none border-0 border-transparent border-b-2 px-4 py-2 font-medium text-muted-foreground text-sm transition-colors data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:text-foreground data-[state=active]:shadow-none",
139
+ className,
140
+ )}
141
+ {...props}
142
+ />
143
+ );
144
+
145
+ export type SandboxTabContentProps = ComponentProps<typeof TabsContent>;
146
+
147
+ export const SandboxTabContent = ({ className, ...props }: SandboxTabContentProps) => (
148
+ <TabsContent className={cn("mt-0 text-sm", className)} {...props} />
149
+ );
@@ -0,0 +1,412 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@elabs-ai/components-ui";
4
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
5
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
6
+ import { ChevronRightIcon } from "lucide-react";
7
+ import type { ComponentProps, HTMLAttributes } from "react";
8
+ import { createContext, useContext, useMemo } from "react";
9
+
10
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
11
+
12
+ interface SchemaParameter {
13
+ name: string;
14
+ type: string;
15
+ required?: boolean;
16
+ description?: string;
17
+ location?: "path" | "query" | "header";
18
+ }
19
+
20
+ interface SchemaProperty {
21
+ name: string;
22
+ type: string;
23
+ required?: boolean;
24
+ description?: string;
25
+ properties?: SchemaProperty[];
26
+ items?: SchemaProperty;
27
+ }
28
+
29
+ interface SchemaDisplayContextType {
30
+ method: HttpMethod;
31
+ path: string;
32
+ description?: string;
33
+ parameters?: SchemaParameter[];
34
+ requestBody?: SchemaProperty[];
35
+ responseBody?: SchemaProperty[];
36
+ }
37
+
38
+ const SchemaDisplayContext = createContext<SchemaDisplayContextType>({
39
+ method: "GET",
40
+ path: "",
41
+ });
42
+
43
+ const methodStyles: Record<HttpMethod, string> = {
44
+ DELETE: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
45
+ GET: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
46
+ PATCH: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
47
+ POST: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
48
+ PUT: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
49
+ };
50
+
51
+ export type SchemaDisplayHeaderProps = HTMLAttributes<HTMLDivElement>;
52
+
53
+ export const SchemaDisplayHeader = ({
54
+ className,
55
+ children,
56
+ ...props
57
+ }: SchemaDisplayHeaderProps) => (
58
+ <div className={cn("flex items-center gap-3 border-b px-4 py-3", className)} {...props}>
59
+ {children}
60
+ </div>
61
+ );
62
+
63
+ export type SchemaDisplayMethodProps = ComponentProps<typeof Badge>;
64
+
65
+ export const SchemaDisplayMethod = ({
66
+ className,
67
+ children,
68
+ ...props
69
+ }: SchemaDisplayMethodProps) => {
70
+ const { method } = useContext(SchemaDisplayContext);
71
+
72
+ return (
73
+ <Badge
74
+ className={cn("font-mono text-xs", methodStyles[method], className)}
75
+ variant="secondary"
76
+ {...props}
77
+ >
78
+ {children ?? method}
79
+ </Badge>
80
+ );
81
+ };
82
+
83
+ export type SchemaDisplayPathProps = HTMLAttributes<HTMLSpanElement>;
84
+
85
+ export const SchemaDisplayPath = ({ className, children, ...props }: SchemaDisplayPathProps) => {
86
+ const { path } = useContext(SchemaDisplayContext);
87
+
88
+ // Highlight path parameters
89
+ const highlightedPath = path.replaceAll(
90
+ /\{([^}]+)\}/g,
91
+ '<span class="text-blue-600 dark:text-blue-400">{$1}</span>',
92
+ );
93
+
94
+ return (
95
+ <span
96
+ className={cn("font-mono text-sm", className)}
97
+ // oxlint-disable-next-line eslint-plugin-react(no-danger)
98
+ dangerouslySetInnerHTML={{ __html: children ?? highlightedPath }}
99
+ {...props}
100
+ />
101
+ );
102
+ };
103
+
104
+ export type SchemaDisplayDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
105
+
106
+ export const SchemaDisplayDescription = ({
107
+ className,
108
+ children,
109
+ ...props
110
+ }: SchemaDisplayDescriptionProps) => {
111
+ const { description } = useContext(SchemaDisplayContext);
112
+
113
+ return (
114
+ <p className={cn("border-b px-4 py-3 text-muted-foreground text-sm", className)} {...props}>
115
+ {children ?? description}
116
+ </p>
117
+ );
118
+ };
119
+
120
+ export type SchemaDisplayContentProps = HTMLAttributes<HTMLDivElement>;
121
+
122
+ export const SchemaDisplayContent = ({
123
+ className,
124
+ children,
125
+ ...props
126
+ }: SchemaDisplayContentProps) => (
127
+ <div className={cn("divide-y", className)} {...props}>
128
+ {children}
129
+ </div>
130
+ );
131
+
132
+ export type SchemaDisplayParameterProps = HTMLAttributes<HTMLDivElement> & SchemaParameter;
133
+
134
+ export const SchemaDisplayParameter = ({
135
+ name,
136
+ type,
137
+ required,
138
+ description,
139
+ location,
140
+ className,
141
+ ...props
142
+ }: SchemaDisplayParameterProps) => (
143
+ <div className={cn("px-4 py-3 ps-10", className)} {...props}>
144
+ <div className="flex items-center gap-2">
145
+ <span className="font-mono text-sm">{name}</span>
146
+ <Badge className="text-xs" variant="outline">
147
+ {type}
148
+ </Badge>
149
+ {location && (
150
+ <Badge className="text-xs" variant="secondary">
151
+ {location}
152
+ </Badge>
153
+ )}
154
+ {required && (
155
+ <Badge
156
+ className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
157
+ variant="secondary"
158
+ >
159
+ required
160
+ </Badge>
161
+ )}
162
+ </div>
163
+ {description && <p className="mt-1 text-muted-foreground text-sm">{description}</p>}
164
+ </div>
165
+ );
166
+
167
+ export type SchemaDisplayParametersProps = ComponentProps<typeof Collapsible>;
168
+
169
+ export const SchemaDisplayParameters = ({
170
+ className,
171
+ children,
172
+ ...props
173
+ }: SchemaDisplayParametersProps) => {
174
+ const { parameters } = useContext(SchemaDisplayContext);
175
+
176
+ return (
177
+ <Collapsible className={cn(className)} defaultOpen {...props}>
178
+ <CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-start transition-colors hover:bg-muted/50">
179
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
180
+ <span className="font-medium text-sm">Parameters</span>
181
+ <Badge className="ms-auto text-xs" variant="secondary">
182
+ {parameters?.length}
183
+ </Badge>
184
+ </CollapsibleTrigger>
185
+ <CollapsibleContent>
186
+ <div className="divide-y border-t">
187
+ {children ??
188
+ parameters?.map((param) => <SchemaDisplayParameter key={param.name} {...param} />)}
189
+ </div>
190
+ </CollapsibleContent>
191
+ </Collapsible>
192
+ );
193
+ };
194
+
195
+ export type SchemaDisplayPropertyProps = HTMLAttributes<HTMLDivElement> &
196
+ SchemaProperty & {
197
+ depth?: number;
198
+ };
199
+
200
+ export const SchemaDisplayProperty = ({
201
+ name,
202
+ type,
203
+ required,
204
+ description,
205
+ properties,
206
+ items,
207
+ depth = 0,
208
+ className,
209
+ ...props
210
+ }: SchemaDisplayPropertyProps) => {
211
+ const hasChildren = properties || items;
212
+ const paddingLeft = 40 + depth * 16;
213
+
214
+ if (hasChildren) {
215
+ return (
216
+ <Collapsible defaultOpen={depth < 2}>
217
+ <CollapsibleTrigger
218
+ className={cn(
219
+ "group flex w-full items-center gap-2 py-3 text-start transition-colors hover:bg-muted/50",
220
+ className,
221
+ )}
222
+ style={{ paddingLeft }}
223
+ >
224
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
225
+ <span className="font-mono text-sm">{name}</span>
226
+ <Badge className="text-xs" variant="outline">
227
+ {type}
228
+ </Badge>
229
+ {required && (
230
+ <Badge
231
+ className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
232
+ variant="secondary"
233
+ >
234
+ required
235
+ </Badge>
236
+ )}
237
+ </CollapsibleTrigger>
238
+ {description && (
239
+ <p
240
+ className="pb-2 text-muted-foreground text-sm"
241
+ style={{ paddingLeft: paddingLeft + 24 }}
242
+ >
243
+ {description}
244
+ </p>
245
+ )}
246
+ <CollapsibleContent>
247
+ <div className="divide-y border-t">
248
+ {properties?.map((prop) => (
249
+ <SchemaDisplayProperty key={prop.name} {...prop} depth={depth + 1} />
250
+ ))}
251
+ {items && <SchemaDisplayProperty {...items} depth={depth + 1} name={`${name}[]`} />}
252
+ </div>
253
+ </CollapsibleContent>
254
+ </Collapsible>
255
+ );
256
+ }
257
+
258
+ return (
259
+ <div className={cn("py-3 pe-4", className)} style={{ paddingLeft }} {...props}>
260
+ <div className="flex items-center gap-2">
261
+ {/* Spacer for alignment */}
262
+ <span className="size-4" />
263
+ <span className="font-mono text-sm">{name}</span>
264
+ <Badge className="text-xs" variant="outline">
265
+ {type}
266
+ </Badge>
267
+ {required && (
268
+ <Badge
269
+ className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
270
+ variant="secondary"
271
+ >
272
+ required
273
+ </Badge>
274
+ )}
275
+ </div>
276
+ {description && <p className="mt-1 ps-6 text-muted-foreground text-sm">{description}</p>}
277
+ </div>
278
+ );
279
+ };
280
+
281
+ export type SchemaDisplayRequestProps = ComponentProps<typeof Collapsible>;
282
+
283
+ export const SchemaDisplayRequest = ({
284
+ className,
285
+ children,
286
+ ...props
287
+ }: SchemaDisplayRequestProps) => {
288
+ const { requestBody } = useContext(SchemaDisplayContext);
289
+
290
+ return (
291
+ <Collapsible className={cn(className)} defaultOpen {...props}>
292
+ <CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-start transition-colors hover:bg-muted/50">
293
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
294
+ <span className="font-medium text-sm">Request Body</span>
295
+ </CollapsibleTrigger>
296
+ <CollapsibleContent>
297
+ <div className="border-t">
298
+ {children ??
299
+ requestBody?.map((prop) => (
300
+ <SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
301
+ ))}
302
+ </div>
303
+ </CollapsibleContent>
304
+ </Collapsible>
305
+ );
306
+ };
307
+
308
+ export type SchemaDisplayResponseProps = ComponentProps<typeof Collapsible>;
309
+
310
+ export const SchemaDisplayResponse = ({
311
+ className,
312
+ children,
313
+ ...props
314
+ }: SchemaDisplayResponseProps) => {
315
+ const { responseBody } = useContext(SchemaDisplayContext);
316
+
317
+ return (
318
+ <Collapsible className={cn(className)} defaultOpen {...props}>
319
+ <CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-start transition-colors hover:bg-muted/50">
320
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
321
+ <span className="font-medium text-sm">Response</span>
322
+ </CollapsibleTrigger>
323
+ <CollapsibleContent>
324
+ <div className="border-t">
325
+ {children ??
326
+ responseBody?.map((prop) => (
327
+ <SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
328
+ ))}
329
+ </div>
330
+ </CollapsibleContent>
331
+ </Collapsible>
332
+ );
333
+ };
334
+
335
+ export type SchemaDisplayProps = HTMLAttributes<HTMLDivElement> & {
336
+ method: HttpMethod;
337
+ path: string;
338
+ description?: string;
339
+ parameters?: SchemaParameter[];
340
+ requestBody?: SchemaProperty[];
341
+ responseBody?: SchemaProperty[];
342
+ };
343
+
344
+ export const SchemaDisplay = ({
345
+ method,
346
+ path,
347
+ description,
348
+ parameters,
349
+ requestBody,
350
+ responseBody,
351
+ className,
352
+ children,
353
+ ...props
354
+ }: SchemaDisplayProps) => {
355
+ const contextValue = useMemo(
356
+ () => ({
357
+ description,
358
+ method,
359
+ parameters,
360
+ path,
361
+ requestBody,
362
+ responseBody,
363
+ }),
364
+ [description, method, parameters, path, requestBody, responseBody],
365
+ );
366
+
367
+ return (
368
+ <SchemaDisplayContext.Provider value={contextValue}>
369
+ <div className={cn("overflow-hidden rounded-lg border bg-background", className)} {...props}>
370
+ {children ?? (
371
+ <>
372
+ <SchemaDisplayHeader>
373
+ <div className="flex items-center gap-3">
374
+ <SchemaDisplayMethod />
375
+ <SchemaDisplayPath />
376
+ </div>
377
+ </SchemaDisplayHeader>
378
+ {description && <SchemaDisplayDescription />}
379
+ <SchemaDisplayContent>
380
+ {parameters && parameters.length > 0 && <SchemaDisplayParameters />}
381
+ {requestBody && requestBody.length > 0 && <SchemaDisplayRequest />}
382
+ {responseBody && responseBody.length > 0 && <SchemaDisplayResponse />}
383
+ </SchemaDisplayContent>
384
+ </>
385
+ )}
386
+ </div>
387
+ </SchemaDisplayContext.Provider>
388
+ );
389
+ };
390
+
391
+ export type SchemaDisplayBodyProps = HTMLAttributes<HTMLDivElement>;
392
+
393
+ export const SchemaDisplayBody = ({ className, children, ...props }: SchemaDisplayBodyProps) => (
394
+ <div className={cn("divide-y", className)} {...props}>
395
+ {children}
396
+ </div>
397
+ );
398
+
399
+ export type SchemaDisplayExampleProps = HTMLAttributes<HTMLPreElement>;
400
+
401
+ export const SchemaDisplayExample = ({
402
+ className,
403
+ children,
404
+ ...props
405
+ }: SchemaDisplayExampleProps) => (
406
+ <pre
407
+ className={cn("mx-4 mb-4 overflow-auto rounded-md bg-muted p-4 font-mono text-sm", className)}
408
+ {...props}
409
+ >
410
+ {children}
411
+ </pre>
412
+ );
@@ -0,0 +1,86 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import { Textarea } from "@elabs-ai/components-ui";
4
+ import { SelectionToolbar } from "./selection-toolbar";
5
+ import { Message, MessageContent } from "./message";
6
+
7
+ const meta = {
8
+ title: "AI/SelectionToolbar",
9
+ component: SelectionToolbar,
10
+ tags: ["autodocs"],
11
+ parameters: { layout: "padded" },
12
+ } satisfies Meta<typeof SelectionToolbar>;
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ /** Default — select any text in the transcript to reveal the Quote toolbar. */
17
+ export const Default: Story = {
18
+ render: () => (
19
+ <SelectionToolbar onQuote={(text) => console.info("quote", text)}>
20
+ <div className="flex flex-col gap-3">
21
+ <Message from="assistant">
22
+ <MessageContent>
23
+ Q3 revenue totalled $4.2M across four regions. EMEA led at $1.8M, followed by the
24
+ Americas at $1.4M. Growth was strongest in APAC, up 22% quarter over quarter.
25
+ </MessageContent>
26
+ </Message>
27
+ <Message from="assistant">
28
+ <MessageContent>
29
+ The biggest risk to Q4 is renewal concentration: three enterprise accounts make up 31%
30
+ of ARR and all renew in the same month.
31
+ </MessageContent>
32
+ </Message>
33
+ </div>
34
+ </SelectionToolbar>
35
+ ),
36
+ };
37
+
38
+ /** Wired to a composer — quoting prepends a `>`-prefixed blockquote into the draft. */
39
+ export const QuoteIntoComposer: Story = {
40
+ render: function QuoteIntoComposer() {
41
+ const [draft, setDraft] = useState("");
42
+ const quote = (text: string) => {
43
+ const block = text
44
+ .split("\n")
45
+ .map((line) => `> ${line}`)
46
+ .join("\n");
47
+ setDraft((d) => (d ? `${block}\n\n${d}` : `${block}\n\n`));
48
+ };
49
+ return (
50
+ <div className="flex flex-col gap-4">
51
+ <SelectionToolbar onQuote={quote}>
52
+ <Message from="assistant">
53
+ <MessageContent>
54
+ Select this sentence and click Quote to pull it into the composer as a blockquote.
55
+ </MessageContent>
56
+ </Message>
57
+ </SelectionToolbar>
58
+ <Textarea
59
+ value={draft}
60
+ onChange={(e) => setDraft(e.target.value)}
61
+ placeholder="Your reply…"
62
+ aria-label="Reply"
63
+ className="min-h-24"
64
+ />
65
+ </div>
66
+ );
67
+ },
68
+ };
69
+
70
+ /** Dark theme. */
71
+ export const DarkTheme: Story = {
72
+ render: () => (
73
+ <SelectionToolbar onQuote={(text) => console.info("quote", text)}>
74
+ <Message from="assistant">
75
+ <MessageContent>Select this text to see the Quote toolbar in dark theme.</MessageContent>
76
+ </Message>
77
+ </SelectionToolbar>
78
+ ),
79
+ decorators: [
80
+ (Story) => (
81
+ <div data-theme="dark" className="rounded-lg bg-background p-6 text-foreground">
82
+ <Story />
83
+ </div>
84
+ ),
85
+ ],
86
+ };