chore(craft): add storybook stories for input bar and queued-message bar (#11561)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
WenxiandClaude Opus 4.8 authored and GitHub committed 2026-05-30 02:48:59 +00:00
1 parent 7651cb458a
commit 7f9f682847
3 files changed
+241

No files matched your search

+1
View File
@@ -6,6 +6,7 @@ const config: StorybookConfig = {
"./*.mdx",
"../lib/opal/src/**/*.stories.@(ts|tsx)",
"../src/refresh-components/**/*.stories.@(ts|tsx)",
"../src/sections/**/*.stories.@(ts|tsx)",
"../src/app/craft/**/*.stories.@(ts|tsx)",
],
addons: ["@storybook/addon-essentials", "@storybook/addon-themes"],
@@ -0,0 +1,151 @@
import { useRef, useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { SWRConfig } from "swr";
import { Button } from "@opal/components";
import InputBar from "@/app/craft/components/InputBar";
import {
UploadFilesProvider,
BuildFile,
} from "@/app/craft/contexts/UploadFilesContext";
import { UserProvider } from "@/providers/UserProvider";
import { QueuedMessage } from "@/app/app/interfaces";
// InputBar mounts UserProvider/UploadFilesContext and calls useUserSkills,
// which fetch /api/me, /api/auth/type and /api/skills. There's no backend in
// standalone Storybook, so disable SWR revalidation (with an isolated cache) to
// keep the stories self-contained — hooks fall back to their empty defaults.
const SWR_NO_FETCH = {
provider: () => new Map(),
revalidateOnMount: false,
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
const meta: Meta<typeof InputBar> = {
title: "Apps/Craft/Input Bar",
component: InputBar,
tags: ["autodocs"],
decorators: [
(Story) => (
<SWRConfig value={SWR_NO_FETCH}>
<UserProvider>
<UploadFilesProvider>
<div className="w-[640px]">
<Story />
</div>
</UploadFilesProvider>
</UserProvider>
</SWRConfig>
),
],
args: {
onSubmit: (message: string, files: BuildFile[]) =>
console.log("onSubmit", { message, files }),
isRunning: false,
placeholder: "Continue the conversation...",
},
};
export default meta;
type Story = StoryObj<typeof InputBar>;
/** Idle input — typing + Enter (or the send button) submits immediately. */
export const Default: Story = {};
/** While a response streams, the send button is disabled and nothing submits. */
export const Running: Story = {
args: {
isRunning: true,
},
};
export const Disabled: Story = {
args: {
disabled: true,
},
};
/** Shows the loading send button used while the sandbox spins up. */
export const SandboxInitializing: Story = {
args: {
sandboxInitializing: true,
},
};
/** Used when the input sits flush against a panel below it. */
export const NoBottomRounding: Story = {
args: {
noBottomRounding: true,
},
};
/**
* Interactive demo of the queued-message flow. Typing while "running" enqueues
* the message (rendered as pills above the input) instead of submitting. Click
* a pill (or press ↑ from an empty input) to highlight it, then Enter to edit,
* Delete/Backspace to remove. "Finish run" simulates a completed response,
* which dequeues and submits the next message FIFO — mirroring the auto-send
* effect in BuildChatPanel.
*/
function QueuedMessagesDemo() {
const [isRunning, setIsRunning] = useState(true);
const [queued, setQueued] = useState<QueuedMessage[]>([
{ id: 1, text: "Add a dark mode toggle to the settings page" },
{ id: 2, text: "Then write tests for it" },
]);
const nextIdRef = useRef(3);
function enqueue(text: string) {
setQueued((prev) => [...prev, { id: nextIdRef.current++, text }]);
}
function removeAt(index: number) {
setQueued((prev) => prev.filter((_, i) => i !== index));
}
// Simulate a run completing: send the head of the queue, then keep
// "running" while messages remain (each completion drains one).
function finishRun() {
setQueued((prev) => {
const [head, ...rest] = prev;
if (head) console.log("auto-sent", head.text);
setIsRunning(rest.length > 0);
return rest;
});
}
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<Button
prominence="secondary"
onClick={() => setIsRunning((prev) => !prev)}
>
{`${isRunning ? "Streaming…" : "Idle"} — toggle`}
</Button>
<Button
prominence="secondary"
disabled={queued.length === 0}
onClick={finishRun}
>
Finish run (auto-send next)
</Button>
</div>
<InputBar
onSubmit={(message, files) =>
console.log("onSubmit", { message, files })
}
isRunning={isRunning}
placeholder="Continue the conversation..."
queuedMessages={queued}
onQueueMessage={enqueue}
onRemoveQueuedMessage={removeAt}
/>
</div>
);
}
export const QueuedMessages: Story = {
render: () => <QueuedMessagesDemo />,
};
@@ -0,0 +1,89 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import QueuedMessageBar from "@/sections/input/QueuedMessageBar";
import { QueuedMessage } from "@/app/app/interfaces";
const SAMPLE_MESSAGES: QueuedMessage[] = [
{ id: 1, text: "Add a dark mode toggle to the settings page" },
{ id: 2, text: "Then write tests for it" },
{
id: 3,
text: "And update the changelog with a short summary of the change",
},
];
// Pills shown above the chat input for messages queued while a response
// streams. Shared by the main chat and Craft input bars.
const meta: Meta<typeof QueuedMessageBar> = {
title: "Sections/Input/Queued Message Bar",
component: QueuedMessageBar,
tags: ["autodocs"],
decorators: [
(Story) => (
<div className="w-[640px]">
<Story />
</div>
),
],
args: {
messages: SAMPLE_MESSAGES,
highlightedIndex: null,
awaitingPreferredSelection: false,
onDiscard: (index: number) => console.log("onDiscard", index),
onHighlight: (index: number | null) => console.log("onHighlight", index),
},
};
export default meta;
type Story = StoryObj<typeof QueuedMessageBar>;
export const Default: Story = {};
/** A highlighted pill reveals the "↵ edit · ⌫ remove" keyboard hint. */
export const Highlighted: Story = {
args: {
highlightedIndex: 1,
},
};
/**
* When the chat is waiting for the user to pick a response, the head of the
* queue shows a "Select a response to continue" label instead.
*/
export const AwaitingPreferredSelection: Story = {
args: {
awaitingPreferredSelection: true,
},
};
export const SingleMessage: Story = {
args: {
messages: [SAMPLE_MESSAGES[0]!],
},
};
/**
* Interactive: click a pill to highlight it, click the trash icon to discard.
* State is owned here to mirror how the input bars wire up the bar.
*/
function InteractiveDemo() {
const [messages, setMessages] = useState<QueuedMessage[]>(SAMPLE_MESSAGES);
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
return (
<QueuedMessageBar
messages={messages}
highlightedIndex={highlightedIndex}
awaitingPreferredSelection={false}
onHighlight={setHighlightedIndex}
onDiscard={(index) => {
setMessages((prev) => prev.filter((_, i) => i !== index));
setHighlightedIndex(null);
}}
/>
);
}
export const Interactive: Story = {
render: () => <InteractiveDemo />,
};