@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.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js +81 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +1 -0
- package/dist/_flow-boundary-D63PJ65S.js +186 -0
- package/dist/_flow-boundary-D63PJ65S.js.map +1 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +11 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +1 -0
- package/dist/_persona-rive-RFR2EUWP.js +164 -0
- package/dist/_persona-rive-RFR2EUWP.js.map +1 -0
- package/dist/index.d.ts +4079 -0
- package/dist/index.js +11770 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
- package/src/_audio-player-media-chrome.tsx +146 -0
- package/src/_chat-shell-rail.tsx +18 -0
- package/src/_code-block-theme.test.ts +134 -0
- package/src/_code-block-theme.ts +184 -0
- package/src/_flow-boundary.tsx +237 -0
- package/src/_flow-lazy.ts +34 -0
- package/src/_interactive-terminal-xterm.ts +32 -0
- package/src/_lazy-mermaid.test.ts +101 -0
- package/src/_lazy-mermaid.ts +117 -0
- package/src/_persona-rive.tsx +252 -0
- package/src/_streamdown-i18n.ts +119 -0
- package/src/agent-timeline.stories.tsx +66 -0
- package/src/agent-timeline.test.tsx +74 -0
- package/src/agent-timeline.tsx +106 -0
- package/src/agent.tsx +113 -0
- package/src/agentic-workspace.stories.tsx +1099 -0
- package/src/artifact.stories.tsx +62 -0
- package/src/artifact.test.tsx +112 -0
- package/src/artifact.tsx +167 -0
- package/src/asset-preview.stories.tsx +100 -0
- package/src/asset-preview.test.tsx +126 -0
- package/src/asset-preview.tsx +218 -0
- package/src/attachments.tsx +393 -0
- package/src/audio-player.stories.tsx +99 -0
- package/src/audio-player.tsx +170 -0
- package/src/blocks-ai-composer.stories.tsx +83 -0
- package/src/canvas.stories.tsx +142 -0
- package/src/canvas.tsx +29 -0
- package/src/chain-of-thought.tsx +193 -0
- package/src/chat-greeting.stories.tsx +96 -0
- package/src/chat-greeting.test.tsx +91 -0
- package/src/chat-greeting.tsx +91 -0
- package/src/chat-shell.stories.tsx +123 -0
- package/src/chat-shell.test.tsx +113 -0
- package/src/chat-shell.tsx +97 -0
- package/src/chat.stories.tsx +99 -0
- package/src/checkpoint.tsx +54 -0
- package/src/code-block.stories.tsx +81 -0
- package/src/code-block.test.tsx +191 -0
- package/src/code-block.tsx +673 -0
- package/src/commit.tsx +343 -0
- package/src/composer.stories.tsx +188 -0
- package/src/composer.test.tsx +182 -0
- package/src/composer.tsx +202 -0
- package/src/confirmation.stories.tsx +61 -0
- package/src/confirmation.test.tsx +136 -0
- package/src/confirmation.tsx +273 -0
- package/src/connection.tsx +23 -0
- package/src/context-panel.stories.tsx +137 -0
- package/src/context-panel.test.tsx +198 -0
- package/src/context-panel.tsx +609 -0
- package/src/context.stories.tsx +16 -0
- package/src/context.tsx +363 -0
- package/src/controls.tsx +20 -0
- package/src/conversation.stories.tsx +26 -0
- package/src/conversation.test.tsx +123 -0
- package/src/conversation.tsx +143 -0
- package/src/dark-theme-variant.test.ts +47 -0
- package/src/edge.tsx +33 -0
- package/src/environment-variables.tsx +304 -0
- package/src/file-tree.stories.tsx +55 -0
- package/src/file-tree.test.tsx +101 -0
- package/src/file-tree.tsx +390 -0
- package/src/gallery.stories.tsx +182 -0
- package/src/gallery.test.tsx +150 -0
- package/src/gallery.tsx +737 -0
- package/src/grouped-parts.stories.tsx +104 -0
- package/src/grouped-parts.test.tsx +167 -0
- package/src/grouped-parts.tsx +247 -0
- package/src/image.stories.tsx +67 -0
- package/src/image.test.tsx +105 -0
- package/src/image.tsx +115 -0
- package/src/index.ts +93 -0
- package/src/inline-citation.stories.tsx +91 -0
- package/src/inline-citation.test.tsx +96 -0
- package/src/inline-citation.tsx +314 -0
- package/src/interactive-terminal.stories.tsx +165 -0
- package/src/interactive-terminal.test.tsx +448 -0
- package/src/interactive-terminal.tsx +444 -0
- package/src/jsx-preview.stories.tsx +265 -0
- package/src/jsx-preview.test.tsx +75 -0
- package/src/jsx-preview.tsx +409 -0
- package/src/markdown-view.stories.tsx +57 -0
- package/src/markdown-view.test.tsx +36 -0
- package/src/markdown-view.tsx +113 -0
- package/src/message-edit.stories.tsx +150 -0
- package/src/message-edit.test.tsx +115 -0
- package/src/message-edit.tsx +336 -0
- package/src/message-feedback.stories.tsx +53 -0
- package/src/message-feedback.test.tsx +62 -0
- package/src/message-feedback.tsx +117 -0
- package/src/message-form-spec.ts +349 -0
- package/src/message-form.stories.tsx +182 -0
- package/src/message-form.test.tsx +227 -0
- package/src/message-form.tsx +845 -0
- package/src/message-table-spec.ts +281 -0
- package/src/message-table.stories.tsx +176 -0
- package/src/message-table.test.tsx +144 -0
- package/src/message-table.tsx +366 -0
- package/src/message.stories.tsx +262 -0
- package/src/message.test.tsx +508 -0
- package/src/message.tsx +619 -0
- package/src/mic-selector.tsx +339 -0
- package/src/microcopy.test.tsx +123 -0
- package/src/model-selector.stories.tsx +73 -0
- package/src/model-selector.test.tsx +55 -0
- package/src/model-selector.tsx +225 -0
- package/src/motion-config.tsx +49 -0
- package/src/node.tsx +71 -0
- package/src/open-in-chat.tsx +340 -0
- package/src/package-info.tsx +205 -0
- package/src/panel.tsx +20 -0
- package/src/part-groups.ts +258 -0
- package/src/persona-sources.ts +59 -0
- package/src/persona.stories.tsx +111 -0
- package/src/persona.test.tsx +67 -0
- package/src/persona.tsx +148 -0
- package/src/plan.tsx +135 -0
- package/src/prompt-input.stories.tsx +261 -0
- package/src/prompt-input.test.tsx +392 -0
- package/src/prompt-input.tsx +1668 -0
- package/src/queue.tsx +237 -0
- package/src/reasoning.stories.tsx +81 -0
- package/src/reasoning.test.tsx +50 -0
- package/src/reasoning.tsx +229 -0
- package/src/sandbox.stories.tsx +62 -0
- package/src/sandbox.test.tsx +42 -0
- package/src/sandbox.tsx +149 -0
- package/src/schema-display.tsx +412 -0
- package/src/selection-toolbar.stories.tsx +86 -0
- package/src/selection-toolbar.test.tsx +89 -0
- package/src/selection-toolbar.tsx +192 -0
- package/src/shimmer.stories.tsx +10 -0
- package/src/shimmer.tsx +81 -0
- package/src/snippet.stories.tsx +19 -0
- package/src/snippet.test.tsx +116 -0
- package/src/snippet.tsx +138 -0
- package/src/sources.stories.tsx +34 -0
- package/src/sources.test.tsx +129 -0
- package/src/sources.tsx +93 -0
- package/src/speech-input.tsx +312 -0
- package/src/stack-trace.tsx +479 -0
- package/src/streamdown-i18n.test.tsx +163 -0
- package/src/suggestion.stories.tsx +75 -0
- package/src/suggestion.test.tsx +72 -0
- package/src/suggestion.tsx +120 -0
- package/src/task.stories.tsx +22 -0
- package/src/task.test.tsx +36 -0
- package/src/task.tsx +89 -0
- package/src/templates-ai-assistant.stories.tsx +142 -0
- package/src/terminal.tsx +244 -0
- package/src/test-results.stories.tsx +177 -0
- package/src/test-results.test.tsx +241 -0
- package/src/test-results.tsx +408 -0
- package/src/tool-result-card.stories.tsx +143 -0
- package/src/tool-result-card.test.tsx +72 -0
- package/src/tool-result-card.tsx +86 -0
- package/src/tool.stories.tsx +60 -0
- package/src/tool.test.tsx +63 -0
- package/src/tool.tsx +235 -0
- package/src/toolbar.tsx +44 -0
- package/src/transcription.tsx +118 -0
- package/src/voice-selector.tsx +469 -0
- package/src/web-preview.stories.tsx +67 -0
- package/src/web-preview.test.tsx +117 -0
- package/src/web-preview.tsx +283 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@elabs-ai/components-ui";
|
|
4
|
+
import type { CarouselApi } from "@elabs-ai/components-ui";
|
|
5
|
+
import { Carousel, CarouselContent, CarouselItem } from "@elabs-ai/components-ui";
|
|
6
|
+
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@elabs-ai/components-ui";
|
|
7
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
8
|
+
import { useLocale } from "@elabs-ai/components-ui";
|
|
9
|
+
import { ArrowLeftIcon, ArrowRightIcon, BadgeCheckIcon } from "lucide-react";
|
|
10
|
+
import type { ComponentProps } from "react";
|
|
11
|
+
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
12
|
+
|
|
13
|
+
export type InlineCitationProps = ComponentProps<"span">;
|
|
14
|
+
|
|
15
|
+
export const InlineCitation = ({ className, ...props }: InlineCitationProps) => (
|
|
16
|
+
<span className={cn("group inline items-center gap-1", className)} {...props} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type InlineCitationTextProps = ComponentProps<"span">;
|
|
20
|
+
|
|
21
|
+
export const InlineCitationText = ({ className, ...props }: InlineCitationTextProps) => (
|
|
22
|
+
<span className={cn("transition-colors group-hover:bg-accent", className)} {...props} />
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
|
26
|
+
|
|
27
|
+
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
|
28
|
+
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A cited source: a bare string — a URL, or any opaque id / human label — or a
|
|
33
|
+
* labelled object.
|
|
34
|
+
*
|
|
35
|
+
* Non-URL sources are first-class. An agent citing a warehouse table, a document
|
|
36
|
+
* id, an uploaded file or a wiki page has no hostname to show, and must not have
|
|
37
|
+
* to synthesize a fake URL to satisfy the chip.
|
|
38
|
+
*/
|
|
39
|
+
export type InlineCitationSourceRef =
|
|
40
|
+
| string
|
|
41
|
+
| {
|
|
42
|
+
/** Opaque identifier, shown when there is no `label` and no `url`. */
|
|
43
|
+
id?: string;
|
|
44
|
+
/** Human-readable label — wins over any hostname derived from `url`. */
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Optional link; its hostname is the fallback label. */
|
|
47
|
+
url?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The URL's hostname when `value` parses as one, else `value` itself. Never throws. */
|
|
51
|
+
const hostnameOrRaw = (value: string): string => {
|
|
52
|
+
try {
|
|
53
|
+
return new URL(value).hostname || value;
|
|
54
|
+
} catch {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The chip label for a source ref. Exported so consumers can render the same
|
|
61
|
+
* label elsewhere (a footnote list, a tooltip) without re-deriving the rules.
|
|
62
|
+
*/
|
|
63
|
+
export const inlineCitationSourceLabel = (source: InlineCitationSourceRef): string => {
|
|
64
|
+
if (typeof source === "string") {
|
|
65
|
+
return hostnameOrRaw(source);
|
|
66
|
+
}
|
|
67
|
+
// `||` not `??` — an upstream field that is present but EMPTY (`label: ""`)
|
|
68
|
+
// must still fall through, or the chip renders with no label at all.
|
|
69
|
+
return source.label || (source.url ? hostnameOrRaw(source.url) : source.id || "unknown");
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
|
|
73
|
+
sources: InlineCitationSourceRef[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const InlineCitationCardTrigger = ({
|
|
77
|
+
sources,
|
|
78
|
+
className,
|
|
79
|
+
...props
|
|
80
|
+
}: InlineCitationCardTriggerProps) => (
|
|
81
|
+
<HoverCardTrigger asChild>
|
|
82
|
+
<Badge
|
|
83
|
+
// The 08 §C.1 evidence channel: the green wash marks "grounded" — green
|
|
84
|
+
// is reserved for evidence/answer/favourable (research 11 §B.4).
|
|
85
|
+
className={cn(
|
|
86
|
+
"ms-1 rounded-full border-success/40 bg-success/10 text-success-text",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
variant="outline"
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<BadgeCheckIcon aria-hidden="true" className="size-3" />
|
|
93
|
+
{sources[0] ? (
|
|
94
|
+
<>
|
|
95
|
+
{inlineCitationSourceLabel(sources[0])} {sources.length > 1 && `+${sources.length - 1}`}
|
|
96
|
+
</>
|
|
97
|
+
) : (
|
|
98
|
+
"unknown"
|
|
99
|
+
)}
|
|
100
|
+
</Badge>
|
|
101
|
+
</HoverCardTrigger>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
export type EvidenceChipProps = InlineCitationCardTriggerProps;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* EvidenceChip — the named front door for the inline evidence interaction
|
|
108
|
+
* (research 11 §B.4): the re-skinned `InlineCitationCardTrigger` (green
|
|
109
|
+
* "grounded" wash). Use inside an `InlineCitationCard` (HoverCard) — it reuses
|
|
110
|
+
* the citation carousel wholesale; it is not a new primitive.
|
|
111
|
+
*/
|
|
112
|
+
export const EvidenceChip = InlineCitationCardTrigger;
|
|
113
|
+
|
|
114
|
+
export type InlineCitationCardBodyProps = ComponentProps<"div">;
|
|
115
|
+
|
|
116
|
+
export const InlineCitationCardBody = ({ className, ...props }: InlineCitationCardBodyProps) => (
|
|
117
|
+
<HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
|
121
|
+
|
|
122
|
+
const useCarouselApi = () => {
|
|
123
|
+
const context = useContext(CarouselApiContext);
|
|
124
|
+
return context;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
|
128
|
+
|
|
129
|
+
export const InlineCitationCarousel = ({
|
|
130
|
+
className,
|
|
131
|
+
children,
|
|
132
|
+
...props
|
|
133
|
+
}: InlineCitationCarouselProps) => {
|
|
134
|
+
const [api, setApi] = useState<CarouselApi>();
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<CarouselApiContext.Provider value={api}>
|
|
138
|
+
<Carousel className={cn("w-full", className)} setApi={setApi} {...props}>
|
|
139
|
+
{children}
|
|
140
|
+
</Carousel>
|
|
141
|
+
</CarouselApiContext.Provider>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type InlineCitationCarouselContentProps = ComponentProps<"div">;
|
|
146
|
+
|
|
147
|
+
export const InlineCitationCarouselContent = (props: InlineCitationCarouselContentProps) => (
|
|
148
|
+
<CarouselContent {...props} />
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
export type InlineCitationCarouselItemProps = ComponentProps<"div">;
|
|
152
|
+
|
|
153
|
+
export const InlineCitationCarouselItem = ({
|
|
154
|
+
className,
|
|
155
|
+
...props
|
|
156
|
+
}: InlineCitationCarouselItemProps) => (
|
|
157
|
+
<CarouselItem className={cn("w-full space-y-2 p-4 ps-8", className)} {...props} />
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
export type InlineCitationCarouselHeaderProps = ComponentProps<"div">;
|
|
161
|
+
|
|
162
|
+
export const InlineCitationCarouselHeader = ({
|
|
163
|
+
className,
|
|
164
|
+
...props
|
|
165
|
+
}: InlineCitationCarouselHeaderProps) => (
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2",
|
|
169
|
+
className,
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
export type InlineCitationCarouselIndexProps = ComponentProps<"div">;
|
|
176
|
+
|
|
177
|
+
export const InlineCitationCarouselIndex = ({
|
|
178
|
+
children,
|
|
179
|
+
className,
|
|
180
|
+
...props
|
|
181
|
+
}: InlineCitationCarouselIndexProps) => {
|
|
182
|
+
const api = useCarouselApi();
|
|
183
|
+
const [current, setCurrent] = useState(0);
|
|
184
|
+
const [count, setCount] = useState(0);
|
|
185
|
+
|
|
186
|
+
const syncState = useCallback(() => {
|
|
187
|
+
if (!api) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
setCount(api.scrollSnapList().length);
|
|
191
|
+
setCurrent(api.selectedScrollSnap() + 1);
|
|
192
|
+
}, [api]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!api) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
syncState();
|
|
200
|
+
|
|
201
|
+
api.on("select", syncState);
|
|
202
|
+
|
|
203
|
+
return () => {
|
|
204
|
+
api.off("select", syncState);
|
|
205
|
+
};
|
|
206
|
+
}, [api, syncState]);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
className={cn(
|
|
211
|
+
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs",
|
|
212
|
+
className,
|
|
213
|
+
)}
|
|
214
|
+
{...props}
|
|
215
|
+
>
|
|
216
|
+
{children ?? `${current}/${count}`}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export type InlineCitationCarouselPrevProps = ComponentProps<"button">;
|
|
222
|
+
|
|
223
|
+
export const InlineCitationCarouselPrev = ({
|
|
224
|
+
className,
|
|
225
|
+
...props
|
|
226
|
+
}: InlineCitationCarouselPrevProps) => {
|
|
227
|
+
const { t } = useLocale();
|
|
228
|
+
const api = useCarouselApi();
|
|
229
|
+
|
|
230
|
+
const handleClick = useCallback(() => {
|
|
231
|
+
if (api) {
|
|
232
|
+
api.scrollPrev();
|
|
233
|
+
}
|
|
234
|
+
}, [api]);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<button
|
|
238
|
+
aria-label={t("previous")}
|
|
239
|
+
className={cn("shrink-0", className)}
|
|
240
|
+
onClick={handleClick}
|
|
241
|
+
type="button"
|
|
242
|
+
{...props}
|
|
243
|
+
>
|
|
244
|
+
<ArrowLeftIcon className="size-4 text-muted-foreground" data-rtl-flip />
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export type InlineCitationCarouselNextProps = ComponentProps<"button">;
|
|
250
|
+
|
|
251
|
+
export const InlineCitationCarouselNext = ({
|
|
252
|
+
className,
|
|
253
|
+
...props
|
|
254
|
+
}: InlineCitationCarouselNextProps) => {
|
|
255
|
+
const { t } = useLocale();
|
|
256
|
+
const api = useCarouselApi();
|
|
257
|
+
|
|
258
|
+
const handleClick = useCallback(() => {
|
|
259
|
+
if (api) {
|
|
260
|
+
api.scrollNext();
|
|
261
|
+
}
|
|
262
|
+
}, [api]);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<button
|
|
266
|
+
aria-label={t("next")}
|
|
267
|
+
className={cn("shrink-0", className)}
|
|
268
|
+
onClick={handleClick}
|
|
269
|
+
type="button"
|
|
270
|
+
{...props}
|
|
271
|
+
>
|
|
272
|
+
<ArrowRightIcon className="size-4 text-muted-foreground" data-rtl-flip />
|
|
273
|
+
</button>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type InlineCitationSourceProps = ComponentProps<"div"> & {
|
|
278
|
+
title?: string;
|
|
279
|
+
url?: string;
|
|
280
|
+
description?: string;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export const InlineCitationSource = ({
|
|
284
|
+
title,
|
|
285
|
+
url,
|
|
286
|
+
description,
|
|
287
|
+
className,
|
|
288
|
+
children,
|
|
289
|
+
...props
|
|
290
|
+
}: InlineCitationSourceProps) => (
|
|
291
|
+
<div className={cn("space-y-1", className)} {...props}>
|
|
292
|
+
{title && <h4 className="truncate font-medium text-sm leading-tight">{title}</h4>}
|
|
293
|
+
{url && <p className="truncate break-all text-muted-foreground text-xs">{url}</p>}
|
|
294
|
+
{description && (
|
|
295
|
+
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">{description}</p>
|
|
296
|
+
)}
|
|
297
|
+
{children}
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
export type InlineCitationQuoteProps = ComponentProps<"blockquote">;
|
|
302
|
+
|
|
303
|
+
export const InlineCitationQuote = ({
|
|
304
|
+
children,
|
|
305
|
+
className,
|
|
306
|
+
...props
|
|
307
|
+
}: InlineCitationQuoteProps) => (
|
|
308
|
+
<blockquote
|
|
309
|
+
className={cn("border-muted border-s-2 ps-3 text-muted-foreground text-sm italic", className)}
|
|
310
|
+
{...props}
|
|
311
|
+
>
|
|
312
|
+
{children}
|
|
313
|
+
</blockquote>
|
|
314
|
+
);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
4
|
+
import { Button } from "@elabs-ai/components-ui";
|
|
5
|
+
import { InteractiveTerminal, type InteractiveTerminalHandle } from "./interactive-terminal";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "AI/InteractiveTerminal",
|
|
9
|
+
component: InteractiveTerminal,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "padded",
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"A token-themed **xterm.js** terminal — a real interactive PTY surface (ANSI " +
|
|
17
|
+
"output, typed input incl. paste, resize). `Terminal` stays the presentational " +
|
|
18
|
+
"read-only log; this is the wrapper for an actual running process. The " +
|
|
19
|
+
"PTY/process is consumer-owned (D5) — this component only renders ANSI output " +
|
|
20
|
+
"and emits `onData`/`onResize`; it never spawns anything. Press **Escape** or " +
|
|
21
|
+
"**Tab**/**Shift+Tab** at any time to move focus out of the terminal — there is " +
|
|
22
|
+
"no keyboard trap.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
} satisfies Meta<typeof InteractiveTerminal>;
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof meta>;
|
|
30
|
+
|
|
31
|
+
// Exercises all 8 base ANSI colors + their bright variants, so a cross-theme
|
|
32
|
+
// audit can confirm every swatch stays legible in light/dark.
|
|
33
|
+
const ANSI_PALETTE =
|
|
34
|
+
"\x1b[30mblack\x1b[0m \x1b[31mred\x1b[0m \x1b[32mgreen\x1b[0m \x1b[33myellow\x1b[0m " +
|
|
35
|
+
"\x1b[34mblue\x1b[0m \x1b[35mmagenta\x1b[0m \x1b[36mcyan\x1b[0m \x1b[37mwhite\x1b[0m\r\n" +
|
|
36
|
+
"\x1b[1;30mblack\x1b[0m \x1b[1;31mred\x1b[0m \x1b[1;32mgreen\x1b[0m \x1b[1;33myellow\x1b[0m " +
|
|
37
|
+
"\x1b[1;34mblue\x1b[0m \x1b[1;35mmagenta\x1b[0m \x1b[1;36mcyan\x1b[0m \x1b[1;37mwhite\x1b[0m " +
|
|
38
|
+
"(bright)\r\n";
|
|
39
|
+
|
|
40
|
+
async function waitForXtermTextarea(root: HTMLElement): Promise<HTMLTextAreaElement> {
|
|
41
|
+
let textarea: HTMLTextAreaElement | null = null;
|
|
42
|
+
await waitFor(
|
|
43
|
+
() => {
|
|
44
|
+
textarea = root.querySelector("textarea");
|
|
45
|
+
if (!textarea) throw new Error("xterm textarea not mounted yet");
|
|
46
|
+
},
|
|
47
|
+
{ timeout: 10000 },
|
|
48
|
+
);
|
|
49
|
+
return textarea!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A minimal fake "shell": echoes typed characters back and, on Enter, prints a
|
|
54
|
+
* canned response — standing in for the real PTY a consumer (e.g. the
|
|
55
|
+
* agent seam in a consuming workbench app) would drive via `onData`/`write()`.
|
|
56
|
+
*/
|
|
57
|
+
function EchoDemo() {
|
|
58
|
+
const ref = useRef<InteractiveTerminalHandle>(null);
|
|
59
|
+
const lineRef = useRef("");
|
|
60
|
+
const lastCommandRef = useRef<HTMLParagraphElement>(null);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
ref.current?.write(ANSI_PALETTE);
|
|
64
|
+
ref.current?.write("Type a command and press Enter (try “hello”).\r\n$ ");
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const handleData = (data: string) => {
|
|
68
|
+
if (data === "\r") {
|
|
69
|
+
const command = lineRef.current;
|
|
70
|
+
lineRef.current = "";
|
|
71
|
+
ref.current?.write("\r\n");
|
|
72
|
+
if (command.trim().length > 0) {
|
|
73
|
+
ref.current?.write(`\x1b[32m✓\x1b[0m ran: ${command}\r\n`);
|
|
74
|
+
if (lastCommandRef.current) {
|
|
75
|
+
lastCommandRef.current.textContent = `Last command: “${command}”`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
ref.current?.write("$ ");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (data === "\x7f") {
|
|
82
|
+
if (lineRef.current.length > 0) {
|
|
83
|
+
lineRef.current = lineRef.current.slice(0, -1);
|
|
84
|
+
ref.current?.write("\b \b");
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
lineRef.current += data;
|
|
89
|
+
ref.current?.write(data);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="flex h-[360px] w-full flex-col gap-2">
|
|
94
|
+
<div className="min-h-0 flex-1">
|
|
95
|
+
<InteractiveTerminal
|
|
96
|
+
ref={ref}
|
|
97
|
+
aria-label="Demo interactive shell"
|
|
98
|
+
className="h-full w-full"
|
|
99
|
+
onData={handleData}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<p ref={lastCommandRef} className="text-caption text-muted-foreground" data-testid="status">
|
|
103
|
+
No command run yet.
|
|
104
|
+
</p>
|
|
105
|
+
<Button size="sm" variant="outline" className="self-start">
|
|
106
|
+
Next focusable control
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const Default: Story = {
|
|
113
|
+
render: () => <EchoDemo />,
|
|
114
|
+
play: async ({ canvasElement }) => {
|
|
115
|
+
const canvas = within(canvasElement);
|
|
116
|
+
const textarea = await waitForXtermTextarea(canvasElement);
|
|
117
|
+
|
|
118
|
+
// Type a command and press Enter — proves onData receives keystrokes and
|
|
119
|
+
// that write() renders the echoed ANSI response.
|
|
120
|
+
textarea.focus();
|
|
121
|
+
await userEvent.keyboard("hello{enter}");
|
|
122
|
+
await expect(canvas.getByTestId("status")).toHaveTextContent("Last command: “hello”");
|
|
123
|
+
|
|
124
|
+
// No keyboard trap (#285): Tab must move focus OFF the terminal onto the
|
|
125
|
+
// next control, not be swallowed as terminal input.
|
|
126
|
+
await userEvent.tab();
|
|
127
|
+
await expect(canvas.getByRole("button", { name: /next focusable control/i })).toHaveFocus();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `readOnly` degrades the terminal to the existing log use case: `write()`
|
|
133
|
+
* still renders ANSI output, but stdin is disabled and no `onData` fires.
|
|
134
|
+
*/
|
|
135
|
+
export const ReadOnly: Story = {
|
|
136
|
+
name: "Read-only (log)",
|
|
137
|
+
parameters: {
|
|
138
|
+
docs: {
|
|
139
|
+
description: {
|
|
140
|
+
story:
|
|
141
|
+
"`readOnly` disables stdin (xterm's `disableStdin`) — `write()` still renders, " +
|
|
142
|
+
"so this covers the same append-only log use case as `Terminal`, with real ANSI " +
|
|
143
|
+
"cursor/color handling.",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
render: function ReadOnlyStory() {
|
|
148
|
+
const ref = useRef<InteractiveTerminalHandle>(null);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
ref.current?.write("$ pnpm build\r\n");
|
|
152
|
+
ref.current?.write(ANSI_PALETTE);
|
|
153
|
+
ref.current?.write("\x1b[32m✓\x1b[0m Build complete in 4.2s\r\n");
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="h-[280px] w-full">
|
|
158
|
+
<InteractiveTerminal ref={ref} aria-label="Build log" className="h-full w-full" readOnly />
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
play: async ({ canvasElement }) => {
|
|
163
|
+
await waitForXtermTextarea(canvasElement);
|
|
164
|
+
},
|
|
165
|
+
};
|