@elabs-ai/components-ai 4.0.0 → 4.2.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/README.md +62 -12
  2. package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
  3. package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
  4. package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
  5. package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
  6. package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
  7. package/dist/_persona-rive-JEG44YHX.js.map +1 -0
  8. package/dist/index.d.ts +1152 -218
  9. package/dist/index.js +5601 -3241
  10. package/dist/index.js.map +1 -1
  11. package/package.json +30 -15
  12. package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
  13. package/src/__contract__/chat-shell.contract.test.tsx +49 -0
  14. package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
  15. package/src/__contract__/image.contract.test.tsx +49 -0
  16. package/src/__contract__/markdown-view.contract.test.tsx +49 -0
  17. package/src/__contract__/message-feedback.contract.test.tsx +49 -0
  18. package/src/__contract__/message-form.contract.test.tsx +49 -0
  19. package/src/__contract__/message-table.contract.test.tsx +49 -0
  20. package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
  21. package/src/__contract__/persona.contract.test.tsx +49 -0
  22. package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
  23. package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
  24. package/src/_audio-player-media-chrome.tsx +102 -15
  25. package/src/_chat-shell-rail.tsx +2 -2
  26. package/src/_flow-boundary.tsx +68 -49
  27. package/src/_lazy-boundary-conformance.ts +38 -0
  28. package/src/_lazy-cjk.test.ts +43 -0
  29. package/src/_lazy-cjk.ts +73 -0
  30. package/src/_lazy-engine-boundary.tsx +61 -0
  31. package/src/_lazy-math.test.ts +63 -0
  32. package/src/_lazy-math.ts +90 -0
  33. package/src/_lazy-mermaid-absent.test.ts +53 -0
  34. package/src/_lazy-mermaid.test.ts +15 -0
  35. package/src/_lazy-mermaid.ts +24 -1
  36. package/src/_mermaid-error-panel.test.tsx +50 -0
  37. package/src/_mermaid-error-panel.tsx +66 -0
  38. package/src/_persona-rive.tsx +62 -10
  39. package/src/_streamdown-i18n.ts +94 -22
  40. package/src/_streamdown-safety.ts +170 -0
  41. package/src/_theme-scope-store.test.ts +83 -0
  42. package/src/_theme-scope-store.ts +103 -0
  43. package/src/agent-event.stories.tsx +97 -0
  44. package/src/agent-event.test.tsx +145 -0
  45. package/src/agent-event.tsx +187 -0
  46. package/src/agent-timeline.stories.tsx +9 -1
  47. package/src/agent.test.tsx +47 -0
  48. package/src/agent.tsx +43 -24
  49. package/src/agentic-workspace.stories.tsx +27 -20
  50. package/src/artifact.tsx +17 -14
  51. package/src/asset-preview.test.tsx +40 -0
  52. package/src/asset-preview.tsx +83 -12
  53. package/src/attachments.tsx +7 -4
  54. package/src/audio-player.test.tsx +78 -0
  55. package/src/audio-player.tsx +243 -56
  56. package/src/audio-visualizer.stories.tsx +126 -0
  57. package/src/audio-visualizer.test.tsx +438 -0
  58. package/src/audio-visualizer.tsx +367 -0
  59. package/src/canvas.stories.tsx +150 -1
  60. package/src/chat-shell.stories.tsx +18 -3
  61. package/src/chat.stories.tsx +16 -2
  62. package/src/code-block.stories.tsx +9 -1
  63. package/src/code-block.test.tsx +100 -1
  64. package/src/code-block.tsx +166 -103
  65. package/src/commit.tsx +30 -41
  66. package/src/composer.stories.tsx +372 -9
  67. package/src/composer.test.tsx +357 -6
  68. package/src/composer.tsx +217 -35
  69. package/src/confirmation.stories.tsx +72 -1
  70. package/src/confirmation.test.tsx +216 -2
  71. package/src/confirmation.tsx +264 -4
  72. package/src/context-panel.stories.tsx +9 -1
  73. package/src/context-panel.tsx +3 -2
  74. package/src/conversation.stories.tsx +86 -2
  75. package/src/conversation.test.tsx +66 -0
  76. package/src/conversation.tsx +52 -9
  77. package/src/diff-view.stories.tsx +196 -0
  78. package/src/diff-view.test.tsx +238 -0
  79. package/src/diff-view.tsx +697 -0
  80. package/src/environment-variables.tsx +20 -37
  81. package/src/file-tree.test.tsx +21 -0
  82. package/src/file-tree.tsx +12 -2
  83. package/src/gallery.tsx +4 -2
  84. package/src/index.ts +14 -4
  85. package/src/inline-citation.tsx +5 -5
  86. package/src/jsx-preview.stories.tsx +2 -2
  87. package/src/jsx-preview.tsx +151 -43
  88. package/src/markdown-view.stories.tsx +92 -1
  89. package/src/markdown-view.test.tsx +237 -2
  90. package/src/markdown-view.tsx +157 -6
  91. package/src/message-compare.stories.tsx +175 -0
  92. package/src/message-compare.test.tsx +207 -0
  93. package/src/message-compare.tsx +453 -0
  94. package/src/message-form.stories.tsx +65 -4
  95. package/src/message-form.test.tsx +8 -2
  96. package/src/message-form.tsx +15 -7
  97. package/src/message-table.stories.tsx +2 -2
  98. package/src/message-table.test.tsx +7 -0
  99. package/src/message-table.tsx +8 -4
  100. package/src/message.stories.tsx +9 -1
  101. package/src/message.test.tsx +247 -1
  102. package/src/message.tsx +125 -15
  103. package/src/microcopy.test.tsx +40 -0
  104. package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
  105. package/src/model-provider-logo.test.tsx +109 -0
  106. package/src/model-provider-logo.tsx +183 -0
  107. package/src/open-in-chat.tsx +50 -29
  108. package/src/package-info.tsx +12 -12
  109. package/src/permission-mode-select.stories.tsx +82 -0
  110. package/src/permission-mode-select.test.tsx +100 -0
  111. package/src/permission-mode-select.tsx +137 -0
  112. package/src/persona-missing-peer.test.tsx +54 -0
  113. package/src/persona.tsx +68 -22
  114. package/src/plan.stories.tsx +166 -0
  115. package/src/plan.test.tsx +267 -0
  116. package/src/plan.tsx +182 -20
  117. package/src/prompt-input-effort.stories.tsx +123 -0
  118. package/src/prompt-input-effort.test.tsx +83 -0
  119. package/src/prompt-input-effort.tsx +136 -0
  120. package/src/prompt-input-mode.stories.tsx +108 -0
  121. package/src/prompt-input-mode.test.tsx +99 -0
  122. package/src/prompt-input-mode.tsx +169 -0
  123. package/src/prompt-input-slash.stories.tsx +211 -0
  124. package/src/prompt-input-slash.test.tsx +262 -0
  125. package/src/prompt-input-slash.tsx +541 -0
  126. package/src/prompt-input.stories.tsx +2 -2
  127. package/src/prompt-input.test.tsx +67 -1
  128. package/src/prompt-input.tsx +42 -4
  129. package/src/queue.tsx +4 -4
  130. package/src/reasoning.tsx +42 -17
  131. package/src/sandbox.stories.tsx +9 -1
  132. package/src/sandbox.tsx +3 -3
  133. package/src/schema-display.test.tsx +56 -0
  134. package/src/schema-display.tsx +71 -37
  135. package/src/selection-toolbar.stories.tsx +9 -1
  136. package/src/session-header.stories.tsx +128 -0
  137. package/src/session-header.test.tsx +138 -0
  138. package/src/session-header.tsx +243 -0
  139. package/src/session-status-bar.stories.tsx +73 -0
  140. package/src/session-status-bar.test.tsx +94 -0
  141. package/src/session-status-bar.tsx +165 -0
  142. package/src/snippet.stories.tsx +9 -1
  143. package/src/snippet.test.tsx +6 -2
  144. package/src/snippet.tsx +14 -34
  145. package/src/speech-input.test.tsx +109 -0
  146. package/src/speech-input.tsx +31 -3
  147. package/src/stack-trace.tsx +24 -36
  148. package/src/streamdown-i18n.test.tsx +24 -1
  149. package/src/task.stories.tsx +13 -3
  150. package/src/templates-ai-assistant.stories.tsx +21 -1
  151. package/src/test-results.tsx +47 -27
  152. package/src/token-usage.stories.tsx +24 -0
  153. package/src/token-usage.test.tsx +92 -0
  154. package/src/{context.tsx → token-usage.tsx} +73 -59
  155. package/src/tool-result-card.stories.tsx +9 -1
  156. package/src/tool.stories.tsx +13 -3
  157. package/src/tool.test.tsx +65 -0
  158. package/src/tool.tsx +73 -30
  159. package/src/transcription.tsx +1 -1
  160. package/src/turn-status.stories.tsx +124 -0
  161. package/src/turn-status.test.tsx +74 -0
  162. package/src/turn-status.tsx +174 -0
  163. package/src/use-audio-level.ts +104 -0
  164. package/src/voice-selector.tsx +5 -5
  165. package/src/web-preview.test.tsx +51 -1
  166. package/src/web-preview.tsx +46 -10
  167. package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
  168. package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
  169. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
  170. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
  171. package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
  172. package/src/_interactive-terminal-xterm.ts +0 -32
  173. package/src/blocks-ai-composer.stories.tsx +0 -83
  174. package/src/context.stories.tsx +0 -16
  175. package/src/interactive-terminal.stories.tsx +0 -165
  176. package/src/interactive-terminal.test.tsx +0 -448
  177. package/src/interactive-terminal.tsx +0 -444
  178. package/src/model-selector.test.tsx +0 -55
  179. package/src/model-selector.tsx +0 -225
  180. package/src/terminal.tsx +0 -244
@@ -1,225 +0,0 @@
1
- import {
2
- Command,
3
- CommandDialog,
4
- CommandEmpty,
5
- CommandGroup,
6
- CommandInput,
7
- CommandItem,
8
- CommandList,
9
- CommandSeparator,
10
- CommandShortcut,
11
- } from "@elabs-ai/components-ui";
12
- import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@elabs-ai/components-ui";
13
- import { cn } from "@elabs-ai/components-ui/lib/cn";
14
- import { BotIcon } from "lucide-react";
15
- import type { ComponentProps, ReactNode } from "react";
16
- import { useState } from "react";
17
-
18
- export type ModelSelectorProps = ComponentProps<typeof Dialog>;
19
-
20
- export const ModelSelector = (props: ModelSelectorProps) => <Dialog {...props} />;
21
-
22
- export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
23
-
24
- export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
25
- <DialogTrigger {...props} />
26
- );
27
-
28
- export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
29
- title?: ReactNode;
30
- };
31
-
32
- export const ModelSelectorContent = ({
33
- className,
34
- children,
35
- title = "Model Selector",
36
- ...props
37
- }: ModelSelectorContentProps) => (
38
- <DialogContent
39
- aria-describedby={undefined}
40
- className={cn("outline! border-none! p-0 outline-border! outline-solid!", className)}
41
- {...props}
42
- >
43
- <DialogTitle className="sr-only">{title}</DialogTitle>
44
- <Command className="**:data-[slot=command-input-wrapper]:h-auto">{children}</Command>
45
- </DialogContent>
46
- );
47
-
48
- export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
49
-
50
- export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
51
- <CommandDialog {...props} />
52
- );
53
-
54
- export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
55
-
56
- export const ModelSelectorInput = ({ className, ...props }: ModelSelectorInputProps) => (
57
- <CommandInput className={cn("h-auto py-3.5", className)} {...props} />
58
- );
59
-
60
- export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
61
-
62
- export const ModelSelectorList = (props: ModelSelectorListProps) => <CommandList {...props} />;
63
-
64
- export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
65
-
66
- export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => <CommandEmpty {...props} />;
67
-
68
- export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
69
-
70
- export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => <CommandGroup {...props} />;
71
-
72
- export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
73
-
74
- export const ModelSelectorItem = (props: ModelSelectorItemProps) => <CommandItem {...props} />;
75
-
76
- export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
77
-
78
- export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
79
- <CommandShortcut {...props} />
80
- );
81
-
82
- export type ModelSelectorSeparatorProps = ComponentProps<typeof CommandSeparator>;
83
-
84
- export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
85
- <CommandSeparator {...props} />
86
- );
87
-
88
- /**
89
- * Where provider logos are fetched from by default.
90
- *
91
- * This is a REMOTE origin: a deployment with a restrictive `img-src` blocks it.
92
- * Self-host the SVGs and pass `src` (or set a `fallback`) — see
93
- * `docs/CSP-AND-NETWORK.md`.
94
- */
95
- export const MODEL_SELECTOR_LOGO_BASE_URL = "https://models.dev/logos";
96
-
97
- export type ModelSelectorLogoProps = Omit<ComponentProps<"img">, "alt"> & {
98
- /**
99
- * Override the logo URL — point at a self-hosted or bundled asset when a CSP
100
- * blocks `models.dev`. Defaults to `${MODEL_SELECTOR_LOGO_BASE_URL}/<provider>.svg`.
101
- */
102
- src?: string;
103
- /**
104
- * Rendered when the logo fails to load (blocked, offline, or unknown
105
- * provider). Defaults to a neutral Lucide glyph, so the row never collapses
106
- * to a broken image.
107
- */
108
- fallback?: ReactNode;
109
- provider:
110
- | "moonshotai-cn"
111
- | "lucidquery"
112
- | "moonshotai"
113
- | "zai-coding-plan"
114
- | "alibaba"
115
- | "xai"
116
- | "vultr"
117
- | "nvidia"
118
- | "upstage"
119
- | "groq"
120
- | "github-copilot"
121
- | "mistral"
122
- | "vercel"
123
- | "nebius"
124
- | "deepseek"
125
- | "alibaba-cn"
126
- | "google-vertex-anthropic"
127
- | "venice"
128
- | "chutes"
129
- | "cortecs"
130
- | "github-models"
131
- | "togetherai"
132
- | "azure"
133
- | "baseten"
134
- | "huggingface"
135
- | "opencode"
136
- | "fastrouter"
137
- | "google"
138
- | "google-vertex"
139
- | "cloudflare-workers-ai"
140
- | "inception"
141
- | "wandb"
142
- | "openai"
143
- | "zhipuai-coding-plan"
144
- | "perplexity"
145
- | "openrouter"
146
- | "zenmux"
147
- | "v0"
148
- | "iflowcn"
149
- | "synthetic"
150
- | "deepinfra"
151
- | "zhipuai"
152
- | "submodel"
153
- | "zai"
154
- | "inference"
155
- | "requesty"
156
- | "morph"
157
- | "lmstudio"
158
- | "anthropic"
159
- | "aihubmix"
160
- | "fireworks-ai"
161
- | "modelscope"
162
- | "llama"
163
- | "scaleway"
164
- | "amazon-bedrock"
165
- | "cerebras"
166
- // oxlint-disable-next-line typescript-eslint(ban-types) -- intentional pattern for autocomplete-friendly string union
167
- | (string & {});
168
- };
169
-
170
- export const ModelSelectorLogo = ({
171
- provider,
172
- className,
173
- src,
174
- fallback,
175
- onError,
176
- ...props
177
- }: ModelSelectorLogoProps) => {
178
- const [failed, setFailed] = useState(false);
179
-
180
- // A blocked/missing logo must not leave a broken-image glyph in the row.
181
- if (failed) {
182
- return (
183
- <>
184
- {fallback ?? (
185
- <BotIcon aria-label={`${provider} logo`} className={cn("size-3", className)} role="img" />
186
- )}
187
- </>
188
- );
189
- }
190
-
191
- return (
192
- <img
193
- {...props}
194
- alt={`${provider} logo`}
195
- className={cn("size-3 dark:invert", className)}
196
- height={12}
197
- onError={(event) => {
198
- setFailed(true);
199
- onError?.(event);
200
- }}
201
- // AFTER the spread, so the caller's `src` wins via the destructured
202
- // default rather than being silently overwritten.
203
- src={src ?? `${MODEL_SELECTOR_LOGO_BASE_URL}/${provider}.svg`}
204
- width={12}
205
- />
206
- );
207
- };
208
-
209
- export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
210
-
211
- export const ModelSelectorLogoGroup = ({ className, ...props }: ModelSelectorLogoGroupProps) => (
212
- <div
213
- className={cn(
214
- "flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
215
- className,
216
- )}
217
- {...props}
218
- />
219
- );
220
-
221
- export type ModelSelectorNameProps = ComponentProps<"span">;
222
-
223
- export const ModelSelectorName = ({ className, ...props }: ModelSelectorNameProps) => (
224
- <span className={cn("flex-1 truncate text-start", className)} {...props} />
225
- );
package/src/terminal.tsx DELETED
@@ -1,244 +0,0 @@
1
- "use client";
2
-
3
- import { Button } from "@elabs-ai/components-ui";
4
- import { cn } from "@elabs-ai/components-ui/lib/cn";
5
- import Ansi from "ansi-to-react";
6
- import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react";
7
- import type { ComponentProps, HTMLAttributes } from "react";
8
- import {
9
- createContext,
10
- useCallback,
11
- useContext,
12
- useEffect,
13
- useMemo,
14
- useRef,
15
- useState,
16
- } from "react";
17
-
18
- interface TerminalContextType {
19
- output: string;
20
- isStreaming: boolean;
21
- autoScroll: boolean;
22
- onClear?: () => void;
23
- }
24
-
25
- const TerminalContext = createContext<TerminalContextType>({
26
- autoScroll: true,
27
- isStreaming: false,
28
- output: "",
29
- });
30
-
31
- export type TerminalHeaderProps = HTMLAttributes<HTMLDivElement>;
32
-
33
- export const TerminalHeader = ({ className, children, ...props }: TerminalHeaderProps) => (
34
- <div
35
- className={cn(
36
- "flex items-center justify-between border-zinc-800 border-b px-4 py-2",
37
- className,
38
- )}
39
- {...props}
40
- >
41
- {children}
42
- </div>
43
- );
44
-
45
- export type TerminalTitleProps = HTMLAttributes<HTMLDivElement>;
46
-
47
- export const TerminalTitle = ({ className, children, ...props }: TerminalTitleProps) => (
48
- <div className={cn("flex items-center gap-2 text-sm text-zinc-400", className)} {...props}>
49
- <TerminalIcon className="size-4" />
50
- {children ?? "Terminal"}
51
- </div>
52
- );
53
-
54
- export type TerminalStatusProps = HTMLAttributes<HTMLDivElement>;
55
-
56
- export const TerminalStatus = ({ className, children, ...props }: TerminalStatusProps) => {
57
- const { isStreaming } = useContext(TerminalContext);
58
-
59
- if (!isStreaming) {
60
- return null;
61
- }
62
-
63
- return (
64
- <div className={cn("flex items-center gap-2 text-xs text-zinc-400", className)} {...props}>
65
- {children}
66
- </div>
67
- );
68
- };
69
-
70
- export type TerminalActionsProps = HTMLAttributes<HTMLDivElement>;
71
-
72
- export const TerminalActions = ({ className, children, ...props }: TerminalActionsProps) => (
73
- <div className={cn("flex items-center gap-1", className)} {...props}>
74
- {children}
75
- </div>
76
- );
77
-
78
- export type TerminalCopyButtonProps = ComponentProps<typeof Button> & {
79
- onCopy?: () => void;
80
- onError?: (error: Error) => void;
81
- timeout?: number;
82
- };
83
-
84
- export const TerminalCopyButton = ({
85
- onCopy,
86
- onError,
87
- timeout = 2000,
88
- children,
89
- className,
90
- ...props
91
- }: TerminalCopyButtonProps) => {
92
- const [isCopied, setIsCopied] = useState(false);
93
- const timeoutRef = useRef<number>(0);
94
- const { output } = useContext(TerminalContext);
95
-
96
- const copyToClipboard = useCallback(async () => {
97
- if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
98
- onError?.(new Error("Clipboard API not available"));
99
- return;
100
- }
101
-
102
- try {
103
- await navigator.clipboard.writeText(output);
104
- setIsCopied(true);
105
- onCopy?.();
106
- timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
107
- } catch (error) {
108
- onError?.(error as Error);
109
- }
110
- }, [output, onCopy, onError, timeout]);
111
-
112
- useEffect(
113
- () => () => {
114
- window.clearTimeout(timeoutRef.current);
115
- },
116
- [],
117
- );
118
-
119
- const Icon = isCopied ? CheckIcon : CopyIcon;
120
-
121
- return (
122
- <Button
123
- className={cn(
124
- "size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
125
- className,
126
- )}
127
- onClick={copyToClipboard}
128
- size="icon"
129
- variant="ghost"
130
- {...props}
131
- >
132
- {children ?? <Icon size={14} />}
133
- </Button>
134
- );
135
- };
136
-
137
- export type TerminalClearButtonProps = ComponentProps<typeof Button>;
138
-
139
- export const TerminalClearButton = ({
140
- children,
141
- className,
142
- ...props
143
- }: TerminalClearButtonProps) => {
144
- const { onClear } = useContext(TerminalContext);
145
-
146
- if (!onClear) {
147
- return null;
148
- }
149
-
150
- return (
151
- <Button
152
- className={cn(
153
- "size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
154
- className,
155
- )}
156
- onClick={onClear}
157
- size="icon"
158
- variant="ghost"
159
- {...props}
160
- >
161
- {children ?? <Trash2Icon size={14} />}
162
- </Button>
163
- );
164
- };
165
-
166
- export type TerminalContentProps = HTMLAttributes<HTMLDivElement>;
167
-
168
- export const TerminalContent = ({ className, children, ...props }: TerminalContentProps) => {
169
- const { output, isStreaming, autoScroll } = useContext(TerminalContext);
170
- const containerRef = useRef<HTMLDivElement>(null);
171
-
172
- useEffect(() => {
173
- if (autoScroll && containerRef.current) {
174
- containerRef.current.scrollTop = containerRef.current.scrollHeight;
175
- }
176
- }, [output, autoScroll]);
177
-
178
- return (
179
- <div
180
- className={cn("max-h-96 overflow-auto p-4 font-mono text-sm leading-relaxed", className)}
181
- ref={containerRef}
182
- {...props}
183
- >
184
- {children ?? (
185
- <pre className="whitespace-pre-wrap break-words">
186
- <Ansi>{output}</Ansi>
187
- {isStreaming && (
188
- <span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-zinc-100" />
189
- )}
190
- </pre>
191
- )}
192
- </div>
193
- );
194
- };
195
-
196
- export type TerminalProps = HTMLAttributes<HTMLDivElement> & {
197
- output: string;
198
- isStreaming?: boolean;
199
- autoScroll?: boolean;
200
- onClear?: () => void;
201
- };
202
-
203
- export const Terminal = ({
204
- output,
205
- isStreaming = false,
206
- autoScroll = true,
207
- onClear,
208
- className,
209
- children,
210
- ...props
211
- }: TerminalProps) => {
212
- const contextValue = useMemo(
213
- () => ({ autoScroll, isStreaming, onClear, output }),
214
- [autoScroll, isStreaming, onClear, output],
215
- );
216
-
217
- return (
218
- <TerminalContext.Provider value={contextValue}>
219
- <div
220
- className={cn(
221
- "flex flex-col overflow-hidden rounded-lg border bg-zinc-950 text-zinc-100",
222
- className,
223
- )}
224
- {...props}
225
- >
226
- {children ?? (
227
- <>
228
- <TerminalHeader>
229
- <TerminalTitle />
230
- <div className="flex items-center gap-1">
231
- <TerminalStatus />
232
- <TerminalActions>
233
- <TerminalCopyButton />
234
- {onClear && <TerminalClearButton />}
235
- </TerminalActions>
236
- </div>
237
- </TerminalHeader>
238
- <TerminalContent />
239
- </>
240
- )}
241
- </div>
242
- </TerminalContext.Provider>
243
- );
244
- };