From 7f9f682847fcdad1952a7cb2a3c797bee33d55fd Mon Sep 17 00:00:00 2001 From: Wenxi Date: Fri, 29 May 2026 19:48:59 -0700 Subject: [PATCH] chore(craft): add storybook stories for input bar and queued-message bar (#11561) Co-authored-by: Claude Opus 4.8 --- web/.storybook/main.ts | 1 + .../app/craft/components/InputBar.stories.tsx | 151 ++++++++++++++++++ .../input/QueuedMessageBar.stories.tsx | 89 +++++++++++ 3 files changed, 241 insertions(+) create mode 100644 web/src/app/craft/components/InputBar.stories.tsx create mode 100644 web/src/sections/input/QueuedMessageBar.stories.tsx diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index 632969f256..d60177ba12 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -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"], diff --git a/web/src/app/craft/components/InputBar.stories.tsx b/web/src/app/craft/components/InputBar.stories.tsx new file mode 100644 index 0000000000..76dceb6bc4 --- /dev/null +++ b/web/src/app/craft/components/InputBar.stories.tsx @@ -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 = { + title: "Apps/Craft/Input Bar", + component: InputBar, + tags: ["autodocs"], + decorators: [ + (Story) => ( + + + +
+ +
+
+
+
+ ), + ], + args: { + onSubmit: (message: string, files: BuildFile[]) => + console.log("onSubmit", { message, files }), + isRunning: false, + placeholder: "Continue the conversation...", + }, +}; + +export default meta; +type Story = StoryObj; + +/** 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([ + { 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 ( +
+
+ + +
+ + console.log("onSubmit", { message, files }) + } + isRunning={isRunning} + placeholder="Continue the conversation..." + queuedMessages={queued} + onQueueMessage={enqueue} + onRemoveQueuedMessage={removeAt} + /> +
+ ); +} + +export const QueuedMessages: Story = { + render: () => , +}; diff --git a/web/src/sections/input/QueuedMessageBar.stories.tsx b/web/src/sections/input/QueuedMessageBar.stories.tsx new file mode 100644 index 0000000000..d97dfafff5 --- /dev/null +++ b/web/src/sections/input/QueuedMessageBar.stories.tsx @@ -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 = { + title: "Sections/Input/Queued Message Bar", + component: QueuedMessageBar, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
+ +
+ ), + ], + 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; + +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(SAMPLE_MESSAGES); + const [highlightedIndex, setHighlightedIndex] = useState(null); + + return ( + { + setMessages((prev) => prev.filter((_, i) => i !== index)); + setHighlightedIndex(null); + }} + /> + ); +} + +export const Interactive: Story = { + render: () => , +};