import type { Meta, StoryObj } from '@storybook/react' import { ChatMessage } from './chat-message' import { InlineToolItem } from '../run/inline-tool-item' import type { ToolPart } from '../types/parts' const meta: Meta = { title: 'Chat/ChatMessage', component: ChatMessage, parameters: { layout: 'fullscreen', backgrounds: { default: 'dark' }, }, decorators: [ (Story) => (
), ], } export default meta type Story = StoryObj const ts = (offsetMinutes = 0) => new Date(Date.now() - offsetMinutes * 60 * 1000) const readToolPart: ToolPart = { type: 'tool', id: 'chat-read', tool: 'read', state: { status: 'completed', input: { file_path: 'src/hooks/useFetchData.ts' }, output: `export function useFetchData(url: string) {\n const [data, setData] = useState(null)\n const [loading, setLoading] = useState(true)\n // ...`, time: { start: Date.now() - 1200, end: Date.now() - 1152 }, }, } const searchToolPart: ToolPart = { type: 'tool', id: 'chat-search', tool: 'grep', state: { status: 'completed', input: { pattern: 'cache', path: 'src/hooks/useFetchData.ts' }, output: 'No cache layer found. Data fetched on every mount.', time: { start: Date.now() - 900, end: Date.now() - 888 }, }, } export const UserMessage: Story = { args: { role: 'user', content: 'How do I implement a binary search tree in TypeScript?', timestamp: ts(3), }, } export const AssistantSimple: Story = { args: { role: 'assistant', content: 'A binary search tree (BST) is a node-based data structure where each node has at most two children, and all left descendants are less than the node, all right descendants are greater.', timestamp: ts(2), }, } export const AssistantWithCode: Story = { args: { role: 'assistant', content: `Here's a clean BST implementation in TypeScript: \`\`\`typescript interface TreeNode { value: T left: TreeNode | null right: TreeNode | null } class BinarySearchTree { private root: TreeNode | null = null insert(value: T): void { this.root = this.insertNode(this.root, value) } private insertNode(node: TreeNode | null, value: T): TreeNode { if (!node) return { value, left: null, right: null } if (value < node.value) node.left = this.insertNode(node.left, value) else if (value > node.value) node.right = this.insertNode(node.right, value) return node } contains(value: T): boolean { let current = this.root while (current) { if (value === current.value) return true current = value < current.value ? current.left : current.right } return false } inOrder(): T[] { const result: T[] = [] const traverse = (node: TreeNode | null) => { if (!node) return traverse(node.left) result.push(node.value) traverse(node.right) } traverse(this.root) return result } } \`\`\` **Key properties:** - \`insert\` — O(log n) average, O(n) worst case - \`contains\` — O(log n) average, O(n) worst case - \`inOrder\` — O(n) always, yields sorted output For a balanced variant, consider AVL or Red-Black trees.`, timestamp: ts(2), }, } export const AssistantStreaming: Story = { args: { role: 'assistant', content: `I'll analyze the performance bottleneck in your \`fetchData\` hook. The issue is that \`useState(true)\` initializes loading as true before React even checks the cache`, isStreaming: true, timestamp: ts(0), }, } export const AssistantWithToolCalls: Story = { args: { role: 'assistant', content: 'Let me read the current implementation first.', toolCalls: (
), timestamp: ts(1), }, } export const AssistantMarkdownRich: Story = { args: { role: 'assistant', content: `## Rate Limiter Design The **sliding window** algorithm is the right choice here. Here's why: | Algorithm | Burst Handling | Redis Ops | Accuracy | |-----------|---------------|-----------|---------- | | Fixed window | Poor | 1 | Low | | Token bucket | Good | 2-3 | Medium | | **Sliding window** | **Excellent** | **2** | **High** | ### Implementation \`\`\`typescript import Redis from 'ioredis' export async function rateLimitCheck( redis: Redis, userId: string, limit = 100, windowMs = 60_000, ): Promise<{ allowed: boolean; remaining: number }> { const now = Date.now() const key = \`rl:\${userId}\` const windowStart = now - windowMs const pipeline = redis.pipeline() pipeline.zremrangebyscore(key, '-inf', windowStart) pipeline.zadd(key, now, \`\${now}-\${Math.random()}\`) pipeline.zcard(key) pipeline.pexpire(key, windowMs) try { const results = await pipeline.exec() const count = (results?.[2]?.[1] as number) ?? 0 return { allowed: count <= limit, remaining: Math.max(0, limit - count) } } catch { // Fail open on Redis errors return { allowed: true, remaining: limit } } } \`\`\` > **Note:** The \`zremrangebyscore\` + \`zadd\` pair is atomic per key, keeping overhead to ~0.3ms p99 on a local Redis instance.`, timestamp: ts(1), }, } export const SystemMessage: Story = { args: { role: 'system', content: 'Session started. Connected to agent runtime v2.4.1.', timestamp: ts(5), }, } export const Conversation: Story = { render: () => (
), }