mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
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:
1 parent
7651cb458a
commit
7f9f682847
3 files changed
+241
No files matched your search
@@ -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 />,
|
||||
};
|
||||
Reference in new issue
Block a user