/** * Presentational building blocks. * * Each part is a thin, styling-light view over the data `useConversation` * produces. They render semantic markup with `smooth-chat__*` class names whose * styling lives entirely in `styles.css` and is driven by `--smooth-*` CSS * variables — so retheming is CSS, never prop-drilling. Every part also forwards * `className` so you can layer Tailwind utilities on top, and you can ignore * these entirely and render the hook's state yourself. */ import { useState, type FormEvent, type KeyboardEvent } from 'react'; import { safeHttpUrl } from '../response.js'; import type { ChatMessage, Citation, ConnectionStatus } from '../types.js'; function cx(...parts: (string | false | undefined)[]): string { return parts.filter(Boolean).join(' '); } /** A list of grounding sources rendered under an assistant bubble. */ export function Citations({ citations, className }: { citations: Citation[]; className?: string }) { if (!citations.length) return null; return (
{citations.length} source{citations.length === 1 ? '' : 's'}
    {citations.map((c) => { const href = safeHttpUrl(c.url); return (
  1. {href ? ( {c.title} ) : ( {c.title} )} {c.snippet ? {c.snippet} : null}
  2. ); })}
); } /** A single message bubble (+ its citations + streaming cursor). */ export function MessageBubble({ message, className }: { message: ChatMessage; className?: string }) { return ( <>
{message.text} {message.streaming ?
{message.role === 'assistant' && message.citations?.length ? : null} ); } /** The scrollable message column. Renders an optional greeting before any messages. */ export function MessageList({ messages, greeting, className }: { messages: ChatMessage[]; greeting?: string; className?: string }) { return (
{messages.length === 0 && greeting ?
{greeting}
: null} {messages.map((m) => ( ))}
); } /** The input row. Calls `onSend` on submit / Enter (Shift+Enter inserts a newline). */ export function Composer({ onSend, disabled, placeholder = 'Type a message…', className, }: { onSend: (text: string) => void; disabled?: boolean; placeholder?: string; className?: string; }) { const [value, setValue] = useState(''); const submit = () => { const text = value.trim(); if (!text || disabled) return; setValue(''); onSend(text); }; const onSubmit = (e: FormEvent) => { e.preventDefault(); submit(); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); } }; return (