@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,75 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useEffect, useState } from "react";
3
+ import { StreamingSuggestions, Suggestion, Suggestions } from "./suggestion";
4
+ const meta = {
5
+ title: "AI/Suggestion",
6
+ component: Suggestions,
7
+ tags: ["autodocs"],
8
+ parameters: { layout: "padded" },
9
+ } satisfies Meta<typeof Suggestions>;
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+ // Default = the soft tinted pill (#194): borderless `bg-secondary rounded-full`.
13
+ export const Default: Story = {
14
+ render: () => (
15
+ <Suggestions>
16
+ {["Summarize the deploys", "Explain the billing error", "Write a regression test"].map(
17
+ (s) => (
18
+ <Suggestion key={s} suggestion={s} onClick={() => {}} />
19
+ ),
20
+ )}
21
+ </Suggestions>
22
+ ),
23
+ };
24
+ // A caller can still opt into an outlined chip; reach for the quiet
25
+ // `outline-subtle` rung. (Since the ADR 0010 Amendment, 2026-06-20, `--input` ==
26
+ // `--border` so `outline`/`outline-subtle` render identically by default;
27
+ // `outline-subtle` remains the semantically correct choice for a quiet chip.)
28
+ export const OutlineSubtle: Story = {
29
+ render: () => (
30
+ <Suggestions>
31
+ {["Compare to last quarter", "Draft the follow-up email"].map((s) => (
32
+ <Suggestion key={s} suggestion={s} variant="outline-subtle" onClick={() => {}} />
33
+ ))}
34
+ </Suggestions>
35
+ ),
36
+ };
37
+
38
+ // Streaming trailing-loader — chips are present and a shimmering trailing chip
39
+ // is shown while the generator is still producing suggestions.
40
+ export const Streaming: Story = {
41
+ render: () => (
42
+ <StreamingSuggestions
43
+ loading
44
+ suggestions={["Summarize the deploys", "Explain the billing error"]}
45
+ onSuggestionClick={() => {}}
46
+ />
47
+ ),
48
+ };
49
+
50
+ // Live demo — suggestions appear progressively, then the trailing loader
51
+ // disappears once the set settles.
52
+ export const StreamingLive: Story = {
53
+ render: function StreamingLive() {
54
+ const all = [
55
+ "Summarize the deploys",
56
+ "Explain the billing error",
57
+ "Write a regression test",
58
+ "Draft the follow-up email",
59
+ ];
60
+ const [count, setCount] = useState(0);
61
+ const done = count >= all.length;
62
+ useEffect(() => {
63
+ if (done) return;
64
+ const t = setTimeout(() => setCount((c) => c + 1), 700);
65
+ return () => clearTimeout(t);
66
+ }, [count, done]);
67
+ return (
68
+ <StreamingSuggestions
69
+ loading={!done}
70
+ suggestions={all.slice(0, count)}
71
+ onSuggestionClick={() => {}}
72
+ />
73
+ );
74
+ },
75
+ };
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { StreamingSuggestions, Suggestion, SuggestionLoading } from "./suggestion";
5
+
6
+ describe("Suggestion", () => {
7
+ it("renders the suggestion text and fires onClick with it", async () => {
8
+ const onClick = vi.fn();
9
+ render(<Suggestion suggestion="Summarize the deploy log" onClick={onClick} />);
10
+
11
+ const button = screen.getByRole("button", { name: "Summarize the deploy log" });
12
+ expect(button).toBeInTheDocument();
13
+
14
+ await userEvent.click(button);
15
+ expect(onClick).toHaveBeenCalledWith("Summarize the deploy log");
16
+ });
17
+
18
+ // #194 (research 02 §3a BTN-2): the default is the soft tinted pill —
19
+ // borderless `bg-secondary` + `rounded-full` — never the strong `--input`
20
+ // outline. A caller-supplied variant still wins.
21
+ it("defaults to the borderless secondary pill", () => {
22
+ render(<Suggestion suggestion="Soft pill" />);
23
+ const button = screen.getByRole("button", { name: "Soft pill" });
24
+ const classes = button.className.split(" ");
25
+ expect(classes).toContain("bg-secondary");
26
+ expect(classes).toContain("rounded-full");
27
+ expect(classes).not.toContain("border");
28
+ expect(classes).not.toContain("border-input");
29
+ });
30
+ });
31
+
32
+ describe("SuggestionLoading", () => {
33
+ it("renders a status chip with the shimmering label", () => {
34
+ render(<SuggestionLoading />);
35
+ const status = screen.getByRole("status");
36
+ expect(status).toHaveTextContent("Thinking…");
37
+ });
38
+
39
+ it("accepts a custom label", () => {
40
+ render(<SuggestionLoading label="Generating…" />);
41
+ expect(screen.getByRole("status")).toHaveTextContent("Generating…");
42
+ });
43
+ });
44
+
45
+ describe("StreamingSuggestions", () => {
46
+ it("renders chips and a trailing loader while loading", () => {
47
+ render(
48
+ <StreamingSuggestions
49
+ suggestions={["First", "Second"]}
50
+ loading
51
+ onSuggestionClick={vi.fn()}
52
+ />,
53
+ );
54
+ expect(screen.getByRole("button", { name: "First" })).toBeInTheDocument();
55
+ expect(screen.getByRole("button", { name: "Second" })).toBeInTheDocument();
56
+ expect(screen.getByRole("status")).toHaveTextContent("Thinking…");
57
+ });
58
+
59
+ it("hides the trailing loader once the set settles", () => {
60
+ render(<StreamingSuggestions suggestions={["First", "Second"]} loading={false} />);
61
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
62
+ });
63
+
64
+ it("fires onSuggestionClick with the chosen suggestion", async () => {
65
+ const onSuggestionClick = vi.fn();
66
+ render(
67
+ <StreamingSuggestions suggestions={["Pick me"]} onSuggestionClick={onSuggestionClick} />,
68
+ );
69
+ await userEvent.click(screen.getByRole("button", { name: "Pick me" }));
70
+ expect(onSuggestionClick).toHaveBeenCalledWith("Pick me");
71
+ });
72
+ });
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { Button } from "@elabs-ai/components-ui";
4
+ import { ScrollArea, ScrollBar } from "@elabs-ai/components-ui";
5
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
6
+ import type { ComponentProps, HTMLAttributes } from "react";
7
+ import { useCallback } from "react";
8
+
9
+ import { Shimmer } from "./shimmer";
10
+
11
+ export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
12
+
13
+ export const Suggestions = ({ className, children, ...props }: SuggestionsProps) => (
14
+ <ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
15
+ <div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>{children}</div>
16
+ <ScrollBar className="hidden" orientation="horizontal" />
17
+ </ScrollArea>
18
+ );
19
+
20
+ export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
21
+ suggestion: string;
22
+ onClick?: (suggestion: string) => void;
23
+ };
24
+
25
+ export const Suggestion = ({
26
+ suggestion,
27
+ onClick,
28
+ className,
29
+ // The soft tinted pill (#194, research 02 §3a BTN-2 / 08 §C.1): `secondary`
30
+ // = borderless `bg-secondary` fill; the pill shape comes from `rounded-full`
31
+ // below. The harsh `outline` (strong `--input` boundary) is for form-adjacent
32
+ // controls, not chips.
33
+ variant = "secondary",
34
+ size = "sm",
35
+ children,
36
+ ...props
37
+ }: SuggestionProps) => {
38
+ const handleClick = useCallback(() => {
39
+ onClick?.(suggestion);
40
+ }, [onClick, suggestion]);
41
+
42
+ return (
43
+ <Button
44
+ className={cn("cursor-pointer rounded-full px-4", className)}
45
+ onClick={handleClick}
46
+ size={size}
47
+ type="button"
48
+ variant={variant}
49
+ {...props}
50
+ >
51
+ {children || suggestion}
52
+ </Button>
53
+ );
54
+ };
55
+
56
+ export type SuggestionLoadingProps = HTMLAttributes<HTMLDivElement> & {
57
+ /** Shimmering label shown while the suggestion set is still generating. */
58
+ label?: string;
59
+ };
60
+
61
+ /**
62
+ * A non-interactive trailing chip that shimmers while suggestions stream in.
63
+ * Shaped like a `Suggestion` pill; composes `Shimmer` (motion-reduce aware) and
64
+ * announces via `role="status"`. Render it after the chips while loading.
65
+ */
66
+ export const SuggestionLoading = ({
67
+ label = "Thinking…",
68
+ className,
69
+ ...props
70
+ }: SuggestionLoadingProps) => (
71
+ <div
72
+ role="status"
73
+ aria-live="polite"
74
+ className={cn(
75
+ "inline-flex h-8 shrink-0 items-center rounded-full bg-secondary px-4 text-body",
76
+ className,
77
+ )}
78
+ {...props}
79
+ >
80
+ <Shimmer as="span" className="text-secondary-foreground">
81
+ {label}
82
+ </Shimmer>
83
+ </div>
84
+ );
85
+
86
+ export type StreamingSuggestionsProps = Omit<SuggestionsProps, "children"> & {
87
+ /** The suggestions to render. As this array grows, new chips appear in place. */
88
+ suggestions: string[];
89
+ /** While true, a shimmering trailing chip is shown until the set settles. */
90
+ loading?: boolean;
91
+ /** Click handler for a suggestion chip. */
92
+ onSuggestionClick?: (suggestion: string) => void;
93
+ /** Label for the trailing loading chip. @default "Thinking…" */
94
+ loadingLabel?: string;
95
+ };
96
+
97
+ /**
98
+ * Suggestions that appear progressively while a generator streams. Each chip
99
+ * fades in on mount (existing chips don't re-animate — keys are the suggestion
100
+ * text); a shimmering trailing chip shows until `loading` flips false.
101
+ */
102
+ export const StreamingSuggestions = ({
103
+ suggestions,
104
+ loading = false,
105
+ onSuggestionClick,
106
+ loadingLabel,
107
+ ...props
108
+ }: StreamingSuggestionsProps) => (
109
+ <Suggestions {...props}>
110
+ {suggestions.map((s) => (
111
+ <Suggestion
112
+ key={s}
113
+ suggestion={s}
114
+ onClick={onSuggestionClick}
115
+ className="animate-in fade-in-0 slide-in-from-bottom-1 duration-fast ease-entrance motion-reduce:animate-none"
116
+ />
117
+ ))}
118
+ {loading && <SuggestionLoading label={loadingLabel} />}
119
+ </Suggestions>
120
+ );
@@ -0,0 +1,22 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger } from "./task";
3
+ const meta = { title: "AI/Task", component: Task, parameters: { layout: "padded" } } satisfies Meta<
4
+ typeof Task
5
+ >;
6
+ export default meta;
7
+ type Story = StoryObj<typeof meta>;
8
+ // Collapsed by default since #192; `defaultOpen` here only so the docs render
9
+ // the body — the items ride the canonical AgentTimeline rail.
10
+ export const Default: Story = {
11
+ render: () => (
12
+ <Task defaultOpen className="max-w-prose">
13
+ <TaskTrigger title="Searched the codebase" />
14
+ <TaskContent>
15
+ <TaskItem>
16
+ Read <TaskItemFile>app.tsx</TaskItemFile> and <TaskItemFile>router.ts</TaskItemFile>
17
+ </TaskItem>
18
+ <TaskItem>Found 3 matching call sites</TaskItem>
19
+ </TaskContent>
20
+ </Task>
21
+ ),
22
+ };
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger } from "./task";
4
+
5
+ describe("Task (#192, research 10 §B.6)", () => {
6
+ it("is collapsed by default (defaultOpen flipped true → false)", () => {
7
+ render(
8
+ <Task>
9
+ <TaskTrigger title="Compiled the board note" />
10
+ <TaskContent>
11
+ <TaskItem>Reconciled 8 rows</TaskItem>
12
+ </TaskContent>
13
+ </Task>,
14
+ );
15
+ expect(screen.getByText("Compiled the board note")).toBeInTheDocument();
16
+ expect(screen.queryByText("Reconciled 8 rows")).not.toBeInTheDocument();
17
+ });
18
+
19
+ it("renders items on the canonical rail with unbordered file chips when open", () => {
20
+ render(
21
+ <Task defaultOpen>
22
+ <TaskTrigger title="Compiled the board note" />
23
+ <TaskContent>
24
+ <TaskItem>
25
+ Wrote <TaskItemFile data-testid="chip">board-note.md</TaskItemFile>
26
+ </TaskItem>
27
+ </TaskContent>
28
+ </Task>,
29
+ );
30
+ const li = screen.getByRole("listitem");
31
+ expect(li).toHaveAttribute("data-status", "complete");
32
+ const chip = screen.getByTestId("chip");
33
+ expect(chip.tagName).toBe("SPAN");
34
+ expect(chip.className.split(" ")).not.toContain("border");
35
+ });
36
+ });
package/src/task.tsx ADDED
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Task — a collapsed "what got done" run summary (#192, research 10 §B.6).
5
+ *
6
+ * The body rides the canonical execution-trace rail (`AgentTimeline` /
7
+ * `AgentStep`, research 10 §B.3) instead of the pre-#192 hand-rolled
8
+ * `border-s-2` content rail. Collapsed by default — in a settled transcript
9
+ * only the focal surfaces (the produced artifact + the final answer) stay
10
+ * open, not every trace.
11
+ */
12
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
13
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
14
+ import { ChevronDownIcon } from "lucide-react";
15
+ import type { ComponentProps, ReactNode } from "react";
16
+
17
+ import { AgentStep, AgentTimeline, type AgentStepProps } from "./agent-timeline";
18
+
19
+ export type TaskItemFileProps = ComponentProps<"span">;
20
+
21
+ /**
22
+ * A quiet inline file chip. Unbordered since #192 (the fill is the gesture;
23
+ * a border on a filled chip is the redundant-border anti-pattern). For a
24
+ * navigable list of produced files, prefer the asset-tree idiom (research 09).
25
+ */
26
+ export const TaskItemFile = ({ children, className, ...props }: TaskItemFileProps) => (
27
+ <span
28
+ className={cn(
29
+ "inline-flex items-center gap-1 rounded-md bg-secondary px-1.5 py-0.5 text-meta text-secondary-foreground",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ </span>
36
+ );
37
+
38
+ export type TaskItemProps = Omit<AgentStepProps, "name" | "children"> & {
39
+ children?: ReactNode;
40
+ };
41
+
42
+ /**
43
+ * One line of the run summary — an `AgentStep` on the canonical rail.
44
+ * Defaults to `status="complete"` (the task reports finished work) with the
45
+ * badge hidden (the rail node carries the status; a badge per log line would
46
+ * shout).
47
+ */
48
+ export const TaskItem = ({
49
+ children,
50
+ status = "complete",
51
+ hideBadge = true,
52
+ ...props
53
+ }: TaskItemProps) => <AgentStep hideBadge={hideBadge} name={children} status={status} {...props} />;
54
+
55
+ export type TaskProps = ComponentProps<typeof Collapsible>;
56
+
57
+ /** Collapsed by default since #192 (`defaultOpen` flipped `true`→`false`). */
58
+ export const Task = ({ className, ...props }: TaskProps) => (
59
+ <Collapsible className={cn(className)} {...props} />
60
+ );
61
+
62
+ export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
63
+ title: string;
64
+ };
65
+
66
+ export const TaskTrigger = ({ children, className, title, ...props }: TaskTriggerProps) => (
67
+ <CollapsibleTrigger asChild className={cn("group", className)} {...props}>
68
+ {children ?? (
69
+ <div className="flex w-full cursor-pointer items-center gap-2 text-body text-muted-foreground transition-colors hover:text-foreground">
70
+ <p>{title}</p>
71
+ <ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
72
+ </div>
73
+ )}
74
+ </CollapsibleTrigger>
75
+ );
76
+
77
+ export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
78
+
79
+ export const TaskContent = ({ children, className, ...props }: TaskContentProps) => (
80
+ <CollapsibleContent
81
+ className={cn(
82
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]",
83
+ className,
84
+ )}
85
+ {...props}
86
+ >
87
+ <AgentTimeline className="mt-4">{children}</AgentTimeline>
88
+ </CollapsibleContent>
89
+ );
@@ -0,0 +1,142 @@
1
+ /**
2
+ * AI assistant template — the canonical full-screen AI assistant composition
3
+ * (app-shell + chat shell: Conversation + PromptInput). This story is the
4
+ * single source of truth: `pnpm gen:templates` derives the consumer template
5
+ * source (`docs/playbooks/templates/ai-assistant.tsx`) from it.
6
+ * Verify across both themes with globals=theme:<slug>.
7
+ */
8
+ import type { Meta, StoryObj } from "@storybook/react-vite";
9
+ import { useState } from "react";
10
+ import { AppIcon } from "@elabs-ai/components-icons";
11
+ import {
12
+ Sidebar,
13
+ SidebarContent,
14
+ SidebarGroup,
15
+ SidebarGroupContent,
16
+ SidebarHeader,
17
+ SidebarInset,
18
+ SidebarMenu,
19
+ SidebarMenuButton,
20
+ SidebarMenuItem,
21
+ SidebarProvider,
22
+ SidebarTrigger,
23
+ } from "@elabs-ai/components-ui";
24
+ import {
25
+ Composer,
26
+ Conversation,
27
+ ConversationContent,
28
+ ConversationEmptyState,
29
+ ConversationScrollButton,
30
+ Message,
31
+ MessageContent,
32
+ MessageResponse,
33
+ } from "./index";
34
+ import { Bot, History, Home, Settings } from "lucide-react";
35
+
36
+ interface ChatMessage {
37
+ id: string;
38
+ role: "user" | "assistant";
39
+ text: string;
40
+ }
41
+
42
+ const nav = [
43
+ { id: "chat", label: "Chat", icon: Bot },
44
+ { id: "history", label: "History", icon: History },
45
+ { id: "home", label: "Home", icon: Home },
46
+ { id: "settings", label: "Settings", icon: Settings },
47
+ ];
48
+
49
+ function AiAssistantTemplate() {
50
+ const [active, setActive] = useState("chat");
51
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
52
+ const [status, setStatus] = useState<"ready" | "submitted">("ready");
53
+
54
+ function send(text: string) {
55
+ setMessages((m) => [...m, { id: crypto.randomUUID(), role: "user", text }]);
56
+ setStatus("submitted");
57
+ const reply = "This is a placeholder response. Connect this to your model runtime.";
58
+ setMessages((m) => [...m, { id: crypto.randomUUID(), role: "assistant", text: reply }]);
59
+ setStatus("ready");
60
+ }
61
+
62
+ return (
63
+ <SidebarProvider>
64
+ <Sidebar collapsible="icon">
65
+ <SidebarHeader className="px-3 py-2">
66
+ <div className="flex items-center gap-2">
67
+ <AppIcon height={20} aria-hidden />
68
+ <span className="truncate font-semibold group-data-[collapsible=icon]:hidden">
69
+ Assistant
70
+ </span>
71
+ </div>
72
+ </SidebarHeader>
73
+ <SidebarContent>
74
+ <SidebarGroup>
75
+ <SidebarGroupContent>
76
+ <SidebarMenu>
77
+ {nav.map((n) => (
78
+ <SidebarMenuItem key={n.id}>
79
+ <SidebarMenuButton
80
+ isActive={active === n.id}
81
+ tooltip={n.label}
82
+ onClick={() => setActive(n.id)}
83
+ >
84
+ <n.icon aria-hidden="true" />
85
+ <span>{n.label}</span>
86
+ </SidebarMenuButton>
87
+ </SidebarMenuItem>
88
+ ))}
89
+ </SidebarMenu>
90
+ </SidebarGroupContent>
91
+ </SidebarGroup>
92
+ </SidebarContent>
93
+ </Sidebar>
94
+ <SidebarInset className="flex flex-col overflow-hidden">
95
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
96
+ <SidebarTrigger />
97
+ <h1 className="text-body font-medium">AI Assistant</h1>
98
+ </header>
99
+ <div className="flex flex-1 flex-col overflow-hidden">
100
+ <Conversation className="flex-1">
101
+ <ConversationContent>
102
+ {messages.length === 0 ? (
103
+ <ConversationEmptyState
104
+ title="Start the conversation"
105
+ description="Ask anything to begin."
106
+ />
107
+ ) : (
108
+ messages.map((m) => (
109
+ <Message from={m.role} key={m.id}>
110
+ <MessageContent>
111
+ <MessageResponse>{m.text}</MessageResponse>
112
+ </MessageContent>
113
+ </Message>
114
+ ))
115
+ )}
116
+ </ConversationContent>
117
+ <ConversationScrollButton />
118
+ </Conversation>
119
+ <Composer
120
+ placeholder="Send a message…"
121
+ status={status === "submitted" ? "Generating…" : "Awaiting your input"}
122
+ sendStatus={status}
123
+ onSubmit={(message) => {
124
+ const text = message.text?.trim();
125
+ if (text) send(text);
126
+ }}
127
+ />
128
+ </div>
129
+ </SidebarInset>
130
+ </SidebarProvider>
131
+ );
132
+ }
133
+
134
+ const meta = {
135
+ title: "Patterns/Templates/AI Assistant",
136
+ parameters: { layout: "fullscreen" },
137
+ tags: ["autodocs"],
138
+ } satisfies Meta;
139
+ export default meta;
140
+ type Story = StoryObj<typeof meta>;
141
+
142
+ export const Default: Story = { render: () => <AiAssistantTemplate /> };