import { useEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEvent } from 'react'; import type { ChatMessage, ComposerSubmission, ModelLoadProgress, ModelId, ModelOption, ShardRetryNotice, StreamState, ToolRun, } from '../../lib/contracts'; import type { CodeTheme } from '../../lib/code-highlighter'; import { TOOL_CATALOG } from '../../lib/agent-tools'; import { segmentsForMessage } from '../../lib/message-segments'; import { ArtifactCard } from './ArtifactCard'; import { ComposerActivity } from './ComposerActivity'; import { MarkdownContent } from './MarkdownContent'; interface ChatSurfaceProps { activeModel: ModelOption; loadedModelId: ModelId | null; messages: ChatMessage[]; streamState: StreamState; modelLoadProgress: ModelLoadProgress; toolsEnabled: boolean; runtimeStatus: string; error: string | null; shardRetry: ShardRetryNotice | null; theme: CodeTheme; onLoadModel: (modelId: ModelId) => void; onDismissError: () => void; onRetryShard: (modelId: ModelId) => void; onSend: (submission: ComposerSubmission) => void; onRegenerate: (messageId: string) => void; onEditUserMessage: (messageId: string, content: string) => void; onCancel: () => void; onToolsEnabledChange: (enabled: boolean) => void; onOpenConversationList: () => void; onOpenSettings: () => void; } export function ChatSurface({ activeModel, loadedModelId, messages, streamState, modelLoadProgress, toolsEnabled, runtimeStatus, error, shardRetry, theme, onLoadModel, onDismissError, onRetryShard, onSend, onRegenerate, onEditUserMessage, onCancel, onToolsEnabledChange, onOpenConversationList, onOpenSettings, }: ChatSurfaceProps) { const [prompt, setPrompt] = useState(''); const isLoading = streamState === 'loading'; const isStreaming = streamState === 'streaming'; const isBusy = isStreaming || isLoading; const isLoaded = loadedModelId === activeModel.id; const runtimeContextLimit = activeModel.id === 'bonsai-27b' ? activeModel.defaultContext : activeModel.contextLimit; const logDate = useMemo(() => new Intl.DateTimeFormat('en-GB', { day: '2-digit', month: 'long', year: 'numeric', }).format(new Date()), []); const submit = () => { const trimmed = prompt.trim(); if (!trimmed || isBusy || !isLoaded) return; onSend({ prompt: trimmed, toolsEnabled }); setPrompt(''); }; const handleSubmit = (event: FormEvent) => { event.preventDefault(); submit(); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); submit(); } }; return (

{activeModel.label}

{activeModel.architectureLabel}
{activeModel.footprint} download {Math.round(runtimeContextLimit / 1024)}K max context {isLoaded ? 'loaded' : 'not loaded'}
{shardRetry && (
Shard {shardRetry.shardIndex + 1}/{shardRetry.shardCount} failed {shardRetry.shardPath} · {shardRetry.error}
)} {error && !shardRetry && (
Runtime stopped safely{error}
)}
{logDate}
{messages.length === 0 && (

Start a chat

Choose a model, keep the backend on Auto, and send a prompt. The model is downloaded once and runs inside this browser.

)} {messages.map((message) => ( ))}
{runtimeStatus}