'use client' import { useState, useCallback, useEffect, useRef } from 'react' import type { AttachmentPayload } from '@standardagents/react' import { apiFetch } from '../lib/api' import { withThreadName, type CreatedThread, type SideBacking, type Thread } from '../hooks/useThreads' import { Logo } from './Logo' /** One entry of `GET /api/agents`. */ interface Agent { id: string name: string title?: string | null description?: string | null primary?: boolean side_a_backing?: SideBacking side_b_backing?: SideBacking } interface PendingAttachment extends AttachmentPayload { id: string preview?: string // object URL for preview } interface EmptyStateProps { selectedAgentId: string onAgentChange: (agentId: string) => void onThreadCreated: (thread: Thread) => void } /** * Agents a person can chat with: primary (not subagent-only) agents with a * human side. Spec 0.2.0 replaced the agent `type` with per-side backings. */ function isChatAgent(agent: Agent): boolean { if (agent.primary === false) return false if (!agent.side_a_backing && !agent.side_b_backing) return true return agent.side_a_backing === 'human' || agent.side_b_backing === 'human' } // Read file as base64 function readFileAsBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => { const result = reader.result as string const base64 = result.split(',')[1] resolve(base64) } reader.onerror = reject reader.readAsDataURL(file) }) } // Get image dimensions function getImageDimensions(file: File): Promise<{ width: number; height: number } | null> { return new Promise((resolve) => { if (!file.type.startsWith('image/')) { resolve(null) return } const img = new Image() img.onload = () => { resolve({ width: img.naturalWidth, height: img.naturalHeight }) URL.revokeObjectURL(img.src) } img.onerror = () => resolve(null) img.src = URL.createObjectURL(file) }) } export function EmptyState({ selectedAgentId, onAgentChange, onThreadCreated, }: EmptyStateProps) { const [message, setMessage] = useState('') const [attachments, setAttachments] = useState([]) const [isCreating, setIsCreating] = useState(false) const [agents, setAgents] = useState([]) const [agentsLoading, setAgentsLoading] = useState(true) const [dropdownOpen, setDropdownOpen] = useState(false) const dropdownRef = useRef(null) const textareaRef = useRef(null) const fileInputRef = useRef(null) useEffect(() => { const fetchAgents = async () => { try { const response = await apiFetch('/api/agents') if (!response.ok) throw new Error('Failed to fetch agents') const data = await response.json() as { agents?: Agent[] } const agentList = (data.agents || []).filter(isChatAgent) setAgents(agentList) if (!selectedAgentId && agentList.length > 0) { onAgentChange(agentList[0].id) } } catch (err) { console.error('Failed to load agents:', err) } finally { setAgentsLoading(false) } } fetchAgents() }, [selectedAgentId, onAgentChange]) useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setDropdownOpen(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, []) useEffect(() => { const textarea = textareaRef.current if (textarea) { textarea.style.height = 'auto' textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px' } }, [message]) const selectedAgent = agents.find(a => a.id === selectedAgentId) const handleFileSelect = useCallback(async (e: React.ChangeEvent) => { const files = e.target.files if (!files || files.length === 0) return const newAttachments: PendingAttachment[] = [] for (const file of Array.from(files)) { try { const base64 = await readFileAsBase64(file) const dimensions = await getImageDimensions(file) const preview = file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined newAttachments.push({ id: crypto.randomUUID(), name: file.name, mimeType: file.type || 'application/octet-stream', data: base64, width: dimensions?.width, height: dimensions?.height, preview, }) } catch (err) { console.error('Failed to read file:', err) } } setAttachments(prev => [...prev, ...newAttachments]) // Reset file input and refocus textarea if (fileInputRef.current) { fileInputRef.current.value = '' } textareaRef.current?.focus() }, []) const removeAttachment = useCallback((id: string) => { setAttachments(prev => { const attachment = prev.find(a => a.id === id) if (attachment?.preview) { URL.revokeObjectURL(attachment.preview) } return prev.filter(a => a.id !== id) }) }, []) const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault() if ((!message.trim() && attachments.length === 0) || !selectedAgentId || isCreating) return setIsCreating(true) const title = message.slice(0, 50) + (message.length > 50 ? '...' : '') || 'New conversation' const currentAttachments = [...attachments] try { // Create the thread already named, then send the first message. const tags = withThreadName([], title) const threadRes = await apiFetch('/api/threads', { method: 'POST', body: JSON.stringify({ agent_id: selectedAgentId, tags }), }) if (!threadRes.ok) throw new Error('Failed to create thread') const { threadId } = await threadRes.json() as CreatedThread const attachmentPayloads: AttachmentPayload[] = currentAttachments.map(a => ({ name: a.name, mimeType: a.mimeType, data: a.data, ...(a.width && a.height ? { width: a.width, height: a.height } : {}), })) const messageRes = await apiFetch(`/api/threads/${threadId}/messages`, { method: 'POST', body: JSON.stringify({ role: 'user', content: message, ...(attachmentPayloads.length > 0 ? { attachments: attachmentPayloads } : {}), }), }) if (!messageRes.ok) throw new Error('Failed to send message') const selectedAgent = agents.find(a => a.id === selectedAgentId) const now = Math.floor(Date.now() / 1000) // API uses seconds onThreadCreated({ id: threadId, agent_id: selectedAgentId, user_id: null, tags, created_at: now, last_activity_at: now, executing: true, agent: { name: selectedAgent?.name, title: selectedAgent?.title ?? undefined, side_a_backing: selectedAgent?.side_a_backing, side_b_backing: selectedAgent?.side_b_backing, }, }) setMessage('') setAttachments([]) } catch (error) { console.error('Failed to create thread:', error) } finally { setIsCreating(false) } }, [message, attachments, selectedAgentId, isCreating, onThreadCreated, agents]) return (
{/* Logo and Greeting */}

What can I help with?

{/* Input box */}
{/* Attachment drawer */} {attachments.length > 0 && (
{attachments.map((attachment) => (
{attachment.preview ? ( {attachment.name} ) : (
{attachment.name}
)}
))}
)} {/* Agent selector row */}
{/* Dropdown menu */} {dropdownOpen && agents.length > 0 && (
{agents.map((agent) => ( ))}
)}
{/* Textarea */}