import { useEffect, useRef, useState } from 'react'; import { renderKnowledgeMarkdown } from '../markdown'; interface ToolCall { id: string; name: string; arguments: Record; } interface ToolResult { id: string; name: string; output: string; error?: string; } interface AgentActivity { id: string; kind: 'wake' | 'tool' | 'permission' | 'receipt'; label: string; detail: string; status: 'running' | 'completed' | 'pending' | 'failed'; timestamp: string; tool?: string; } interface Message { role: 'user' | 'assistant' | 'system'; content: string; toolCalls?: ToolCall[]; toolResults?: ToolResult[]; activities?: AgentActivity[]; timestamp: string; } interface ConversationSummary { id: string; title: string; createdAt: string; updatedAt: string; messageCount: number; } interface AgentStatus { name: string; role: string; model: string; configured: boolean; operational: boolean; tools: string[]; } interface PendingAction { id: string; tool: string; effect: 'write' | 'delete'; target: string; summary: string; createdAt: string; } interface Props { apiBase: string; runtimeRevision?: number; onClose?: () => void; onWorldChanged?: () => void; } export default function AgentChat({ apiBase, runtimeRevision = 0, onClose, onWorldChanged }: Props) { const [messages, setMessages] = useState([]); const [conversations, setConversations] = useState([]); const [activeConversationId, setActiveConversationId] = useState(''); const [historyOpen, setHistoryOpen] = useState(false); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); const [status, setStatus] = useState(null); const [statusError, setStatusError] = useState(''); const [pendingActions, setPendingActions] = useState([]); const [actionBusy, setActionBusy] = useState(''); const bottomRef = useRef(null); const inputRef = useRef(null); useEffect(() => { void bootstrap(); }, [runtimeRevision]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, sending]); async function bootstrap() { try { const response = await fetch(`${apiBase}/api/agent/conversations`); const data = await response.json(); if (!data.ok) throw new Error(data.error || '无法读取对话'); setConversations(data.conversations || []); const selected = activeConversationId || data.activeConversationId || data.conversations?.[0]?.id; if (selected) await selectConversation(selected); } catch { setStatusError('HoloLake 本地服务未启动'); } } async function selectConversation(id: string) { setActiveConversationId(id); const [historyResponse, statusResponse, actionsResponse] = await Promise.all([ fetch(`${apiBase}/api/agent/conversation?conversationId=${encodeURIComponent(id)}`), fetch(`${apiBase}/api/agent/status?conversationId=${encodeURIComponent(id)}`), fetch(`${apiBase}/api/agent/actions?conversationId=${encodeURIComponent(id)}`), ]); const [history, runtime, actions] = await Promise.all([historyResponse.json(), statusResponse.json(), actionsResponse.json()]); if (history.ok) setMessages(history.messages || []); if (actions.ok) setPendingActions(actions.actions || []); if (runtime.ok) { setStatusError(''); setStatus({ name: runtime.persona.name, role: runtime.persona.role, model: runtime.persona.model, configured: runtime.configured, operational: runtime.operational, tools: runtime.tools || [], }); } } async function refreshConversationList() { const response = await fetch(`${apiBase}/api/agent/conversations`); const data = await response.json(); if (data.ok) setConversations(data.conversations || []); } async function createConversation() { const response = await fetch(`${apiBase}/api/agent/conversations`, { method: 'POST' }); const data = await response.json(); if (!data.ok) return; await refreshConversationList(); await selectConversation(data.conversation.id); setHistoryOpen(false); inputRef.current?.focus(); } async function deleteConversation(id: string) { if (!confirm('删除这条对话记录?知识库文档不会被删除。')) return; const response = await fetch(`${apiBase}/api/agent/conversations/${encodeURIComponent(id)}`, { method: 'DELETE' }); const data = await response.json(); if (!data.ok) return; await refreshConversationList(); await selectConversation(data.activeConversationId); } async function sendMessage() { const text = input.trim(); if (!text || sending || !activeConversationId) return; setInput(''); setSending(true); setMessages(previous => [...previous, { role: 'user', content: text, timestamp: new Date().toISOString() }]); try { const response = await fetch(`${apiBase}/api/agent/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, conversationId: activeConversationId }), }); const data = await response.json(); if (!data.ok) throw new Error(data.error || '调用失败'); setMessages(previous => [...previous, { role: 'assistant', content: data.reply, toolCalls: data.toolCalls || [], toolResults: data.toolResults || [], activities: data.activities || [], timestamp: new Date().toISOString(), }]); setPendingActions(data.pendingActions || []); await refreshConversationList(); } catch (error) { setMessages(previous => [...previous, { role: 'assistant', content: `错误:${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString() }]); } finally { setSending(false); inputRef.current?.focus(); } } async function resolveAction(action: PendingAction, decision: 'confirm' | 'reject') { setActionBusy(action.id); try { const response = await fetch(`${apiBase}/api/agent/actions/${encodeURIComponent(action.id)}/${decision}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversationId: activeConversationId }), }); const data = await response.json(); if (!data.ok) throw new Error(data.error || '动作处理失败'); setPendingActions(data.actions || []); setMessages(data.messages || messages); await refreshConversationList(); if (decision === 'confirm') onWorldChanged?.(); } catch (error) { setMessages(previous => [...previous, { role: 'assistant', content: `动作未执行:${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString() }]); } finally { setActionBusy(''); } } return (

{status?.name || 'HoloLake'}

{status?.role || '语言操作入口'}
{statusError || (status?.operational ? '语言操作入口可用' : '请在设置中验证模型')}{status?.tools?.length || 0} 项受控能力 · {status?.model || 'offline'}
{historyOpen && ( )}
{messages.length === 0 &&

语言操作入口

人格体唤醒时,HoloLake 会先查找该人格体自己的路径页面。活动栏只显示真实发生的读取、工具、权限与回执,不使用固定步骤模板。

} {messages.map((message, index) => (
{message.role === 'user' ?

{message.content}

: (
{message.activities && message.activities.length > 0 && (
运行活动 · {message.activities.length} 项
    {message.activities.map(activity =>
  1. {activity.label}{activity.detail}
    {activity.status === 'completed' ? '完成' : activity.status === 'pending' ? '待确认' : activity.status === 'failed' ? '失败' : '进行中'}
  2. )}
)}
)}
))} {pendingActions.length > 0 &&
待确认动作
{pendingActions.map(action =>
{action.effect === 'delete' ? '删除' : '写入'} · {action.target}{action.summary}
)}
} {sending &&
}