import type { Meta, StoryObj } from '@storybook/nextjs-vite' import React, { useState } from 'react' import { ChatContainer, ChatContent, ChatFooter, ChatHeader, } from '../components/chat/chat-container' import { ChatInput } from '../components/chat/chat-input' import { ChatMessageList } from '../components/chat/chat-message-list' import { QuickActionWall } from '../components/chat/quick-action-wall' import type { QuickActionChip } from '../components/chat/quick-action-chip' import { ModelDisplay, ModelDisplaySkeleton } from '../components/chat/model-display' import type { Message } from '../components/chat/types/message.types' import { cn } from '../utils/cn' const SAMPLE_QUICK_ACTIONS: QuickActionChip[] = [ { id: 'slow', label: 'Computer slow' }, { id: 'updates', label: 'Check updates' }, { id: 'internet', label: 'Internet problems' }, ] // ============================================================================= // Mock messages — sample client-facing Fae conversation // ============================================================================= const FAE_MESSAGES: Message[] = [ { id: '1', role: 'assistant', name: 'Fae', assistantType: 'fae', content: "Hi! I'm Fae, your personal assistant. How can I help you today?", timestamp: new Date('2026-05-28T10:00:00Z'), }, { id: '2', role: 'user', name: 'You', content: "My printer stopped working this morning — can you open a ticket?", timestamp: new Date('2026-05-28T10:00:30Z'), }, { id: '3', role: 'assistant', name: 'Fae', assistantType: 'fae', content: "Of course — I'll get that opened right away. Could you tell me which printer it is (model or location) and what happens when you try to print?", timestamp: new Date('2026-05-28T10:00:45Z'), }, { id: '4', role: 'user', name: 'You', content: "It's the HP LaserJet by the front desk. It blinks orange and nothing prints.", timestamp: new Date('2026-05-28T10:01:10Z'), }, ] // ============================================================================= // StoryShell — wires up controlled `ChatInput` and `ChatMessageList` so the // story is interactive: typing & sending pushes a new user bubble into the // message list. No transport — the assistant doesn't reply. // ============================================================================= function FaeChatShell({ withTicketInfo = false, withMspOrganization = false, mspLoading = false, bare = false, fullWidth = false, modelLoading = false, }: { withTicketInfo?: boolean withMspOrganization?: boolean mspLoading?: boolean bare?: boolean fullWidth?: boolean modelLoading?: boolean }) { const [messages, setMessages] = useState(FAE_MESSAGES) const handleSend = (text: string) => { if (!text.trim()) return setMessages((prev) => [ ...prev, { id: `local-${prev.length + 1}`, role: 'user', name: 'You', content: text, timestamp: new Date(), }, ]) } const handleNewChat = () => setMessages([FAE_MESSAGES[0]]) return ( { // eslint-disable-next-line no-console console.log('[story] close clicked') }} fullWidth={fullWidth} bare={bare} ticketInfo={ withTicketInfo ? { title: 'Printer offline — Ticket #4821', meta: 'Assigned to: J. Smith', status: 'in-progress', } : undefined } mspOrganization={ withMspOrganization ? { name: 'TechFlow Solutions', website: 'www.techflow.com', href: 'https://www.techflow.com', } : undefined } isMspLoading={mspLoading} /> {/* Quick actions above the composer — the shared `QuickActionWall` (its own 1:1 skeleton drives the loading variant, same pattern as ModelDisplay). */} {/* Model row below the composer — its width tracks the composer's, so in `fullWidth` it spans the footer instead of the centered column. Loaded state shows the real `ModelDisplay`; `modelLoading` swaps in the skeleton placeholder. */}
{modelLoading ? ( ) : ( )}
) } // ============================================================================= // Meta // ============================================================================= const meta = { title: 'Chat/ChatContainer', component: ChatContainer, parameters: { layout: 'fullscreen', }, } satisfies Meta export default meta type Story = StoryObj // ============================================================================= // Fae — client-facing chat shell // ============================================================================= /** * The Fae client-app chat: the default `ChatContainer` + `ChatHeader` shell * used by the OpenFrame customer-facing application. Header carries * `Grace "Fae" Meadows` as the assistant persona (pink avatar via the * default `bg-ods-flamingo-pink`); message list renders with * `assistantType="fae"` so AI bubbles use the matching Fae styling. */ export const Fae: Story = { render: () => , } /** * Same Fae shell with `ticketInfo` populated on the header — adds a * secondary row beneath the avatar/name showing the active ticket title, * meta and status tag. This is the in-conversation state once a support * ticket has been opened from the chat. */ export const FaeWithTicket: Story = { render: () => , } /** * Client chat home-screen header with the MSP organization branding bar — * logo, "Your IT is managed by {name}" and the website link rendered inside * the header card beneath the assistant identity row. This is the state of * the main AI Assistant screen (with the chats list); once a chat/ticket is * open, `ticketInfo` takes over the slot (see `FaeWithTicket`). The block * renders only when the host has MSP info — absent it, the header stays a * single identity row. */ export const ClientChatWithMspOrganization: Story = { render: () => , } /** * Client chat MSP organization slot in its loading state — * `MspOrganizationCardSkeleton` holds the space while tenant info resolves, * so the header doesn't shift when the branding bar swaps in. */ export const ClientChatMspOrganizationLoading: Story = { render: () => , } /** * Full-width variant — drops the centered 600px content column on header, * message list and footer. Use when embedding the Fae chat in a narrow * side panel (e.g. mobile, slide-in drawer) where the centered column * would float in the middle of the panel. */ export const FaeFullWidth: Story = { render: () => , } /** * Bare header — drops the card chrome (background, border, shadow, ring) * so the header blends flush with its container. Pair with a host- * supplied floating card / drawer that already provides the chrome. */ export const FaeBareHeader: Story = { render: () => , } /** * Model row in its loading state — `ModelDisplaySkeleton` stands in for the * `ModelDisplay` pill below the composer until the model metadata resolves. * Shows only the left part (provider icon + model name); the "tokens used" * tail is opt-in via `showTokens`. */ export const FaeModelLoading: Story = { render: () => , }