import type { Meta, StoryObj } from "@storybook/nextjs-vite"; import type React from "react"; import { ChatMessageEnhanced } from "../components/chat/chat-message-enhanced"; import { ChatTypingIndicator } from "../components/chat/chat-typing-indicator"; import type { MessageSegment } from "../components/chat/types"; /** * `` — THE chat message renderer used by * ChatMessageList / EmbeddableChat. Renders the avatar + name + timestamp * header (Fae pink / Mingo cyan / user grey) and every message-segment type: * text (markdown), thinking, tool_execution, approval_request/batch, error, * context_compaction. * * The Fae conversation scenes reproduce the Fae desktop-app flow: greeting → * user request → approval block → typing dots (Figma "fae-approval-block"). */ // Fixed time-of-day so the header always reads "2:47 PM" — messages dated // today render time-only (see formatMessageTimestamp). const at247 = () => { const d = new Date(); d.setHours(14, 47, 0, 0); return d; }; const panelDecorator = (Story: React.ComponentType) => (
); const meta = { title: "Chat/ChatMessageEnhanced", component: ChatMessageEnhanced, tags: ["autodocs"], parameters: { docs: { description: { component: "Single chat message row — avatar + colored author name + timestamp header, plus the full segment renderer (text/thinking/tool/approval/error/compaction). Used by ChatMessageList in both the admin (Mingo) and client (Fae) chats.", }, }, }, decorators: [panelDecorator], } satisfies Meta; export default meta; type Story = StoryObj; /** Assistant (Fae) text message — pink mono name + timestamp. */ export const AssistantFae: Story = { args: { role: "assistant", name: "Fae", content: "Good morning! How can I help you today?", timestamp: at247(), }, }; /** Assistant (Mingo) text message — cyan mono name. */ export const AssistantMingo: Story = { args: { role: "assistant", name: "Mingo", assistantType: "mingo", content: "I've pulled the device list for Acme Corp — 3 endpoints are offline.", timestamp: at247(), }, }; /** User message — grey mono name, no avatar block until user avatars ship. */ export const UserMessage: Story = { args: { role: "user", name: "John Smith", content: "hey can u install slack on my mac", timestamp: at247(), }, }; /** * Fae flow, step 1 — greeting, user request, and the assistant "typing" * row (empty message + ``, as the Fae desktop host * renders it while the reply streams in). */ export const FaeConversationTyping: Story = { args: { role: "assistant", content: "" }, render: () => (
), }; const approvedSegments: MessageSegment[] = [ { type: "text", text: "On it! Just need your quick approval here." }, { type: "approval_request", data: { command: "brew install --cask slack", requestId: "req-1", approvalType: "CLIENT", explanation: "Install Slack via Homebrew Cask on macOS", }, status: "approved", resolvedByName: "John Smith", }, ]; /** * Fae flow, step 2 — the reply carries an `approval_request` segment * rendered as the CLIENT card with the resolved "APPROVED BY JOHN SMITH" * pill (`approvalVariant="client"`), then Fae keeps typing. */ export const FaeConversationApproved: Story = { args: { role: "assistant", content: "" }, render: () => (
), }; /** Pending approval inside a message — Approve/Reject buttons (client card). */ export const PendingApprovalSegment: Story = { args: { role: "assistant", name: "Fae", approvalVariant: "client", timestamp: at247(), content: [ { type: "text", text: "On it! Just need your quick approval here." }, { type: "approval_request", data: { command: "brew install --cask slack", requestId: "req-2", approvalType: "CLIENT", explanation: "Install Slack via Homebrew Cask on macOS", }, status: "pending", onApprove: () => {}, onReject: () => {}, }, ] satisfies MessageSegment[], }, }; /** * `[card://]` markers inside a `guide` segment resolve exactly as they do in a * text segment — the host's `renderEntityCard` node is hoisted out of the * markdown as a sibling block, and an unknown id degrades to the bare id. * Regression guard: the guide body used to skip the marker plan entirely. * * The marker is the ONLY data channel: `renderEntityCard` receives a minimal * `{type, id}` descriptor and hydrates by id (here from a story-local map; * in production from the host's per-object APIs). */ const GUIDE_TITLES_BY_ID: Record = { "first-check": "Create Your First Monitoring Check", "add-devices": "Add Devices to a Policy", }; export const GuideWithEntityCards: Story = { args: { role: "assistant", name: "Mingo", assistantType: "mingo", timestamp: at247(), renderEntityCard: (ref) => ref.type === "onboarding_guide" ? (
{GUIDE_TITLES_BY_ID[ref.id] ?? ref.id} Guide
) : null, content: [ { type: "guide", text: "Here's how to create a monitoring policy (check) in OpenFrame:\n\n[card://onboarding_guide:first-check]\n\nStep-by-step:\n\n1. Go to Monitoring → Policies → Add Policy\n2. Write your osquery SQL query\n3. Save the policy", }, { type: "text", text: "A plain text segment renders its own marker identically:\n\n[card://onboarding_guide:add-devices]" }, ] satisfies MessageSegment[], }, }; /** Every remaining segment type in one message — thinking, guide, tool execution, context compaction, error. */ export const AllSegmentTypes: Story = { args: { role: "assistant", name: "Fae", timestamp: at247(), content: [ { type: "thinking", text: "The user wants Slack installed. Homebrew is present on the endpoint, so `brew install --cask slack` is the cleanest path.", }, { type: "text", text: "Installing Slack now." }, { type: "guide", text: "Here's how to create a monitoring policy (check) in OpenFrame:\n\nStep-by-step:\n\n1. Go to Monitoring → Policies → Add Policy\n2. Give it a Name (something descriptive)\n3. Write your osquery SQL query — the query should return rows for devices that fail the check\n4. Set the interval — how often each device runs the check\n5. Save the policy", }, { type: "tool_execution", data: { type: "EXECUTED_TOOL", integratedToolType: "OPENFRAME_RMM", toolFunction: "run_script", toolTitle: "Install Slack via Homebrew Cask", parameters: { command: "brew install --cask slack" }, result: "🍺 slack was successfully installed!", success: true, }, }, { type: "context_compaction", status: "completed" }, { type: "error", title: "Notification delivery failed", details: "Slack webhook returned 429 — the completion ping was skipped. The install itself succeeded.", }, { type: "text", text: "Done — Slack is installed and ready to use." }, ] satisfies MessageSegment[], }, };