'use client' import { useChat } from '@ai-sdk/react' import { DefaultChatTransport, type UIMessage } from 'ai' import { ArrowUp, Bot, Check, CircleAlert, LoaderCircle, Square, Trash2, Wrench, } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import { withPascalBasePath } from '@/lib/pascal-base-path' interface AiDesignAgentPanelProps { sceneId: string } interface AgentStatus { configured: boolean maxSteps: number model: string } type DisplayPart = { errorText?: string state?: string text?: string toolName?: string type: string } const STARTER_PROMPTS = [ '设计一个紧凑的两室一厅住宅', '检查当前模型并修复结构问题', '增加二层,并创建连接楼梯', ] const TOOL_LABELS: Record = { add_door: '添加门', add_window: '添加窗户', apply_patch: '更新场景结构', check_collisions: '检查空间碰撞', create_house_from_brief: '生成建筑方案', create_level: '创建楼层', create_roof: '创建屋顶', create_room: '创建房间', create_stair_between_levels: '创建楼梯', create_story_shell: '创建楼层结构', create_wall: '创建墙体', delete_node: '删除场景元素', find_nodes: '查找场景元素', furnish_room: '布置房间', get_scene: '读取当前场景', place_item: '放置物品', save_scene: '保存设计进度', validate_scene: '验证建筑结构', } function textOnlyHistory(messages: UIMessage[]): UIMessage[] { return messages .map((message) => ({ id: message.id, parts: message.parts.filter((part) => part.type === 'text'), role: message.role, })) .filter((message) => message.parts.length > 0) .slice(-40) as UIMessage[] } function toolNameFromPart(part: DisplayPart): string | null { if (part.toolName) return part.toolName if (part.type.startsWith('tool-')) return part.type.slice(5) return null } function ToolActivity({ part }: { part: DisplayPart }) { const name = toolNameFromPart(part) if (!name) return null const isComplete = part.state === 'output-available' const isError = part.state === 'output-error' return (
{isError ? ( ) : isComplete ? ( ) : ( )}
{TOOL_LABELS[name] ?? name.replaceAll('_', ' ')}
{isError ? part.errorText || '执行失败' : isComplete ? '已完成' : '正在执行'}
) } export function AiDesignAgentPanel({ sceneId }: AiDesignAgentPanelProps) { const historyKey = `pascal-agent-history:${sceneId}` const [input, setInput] = useState('') const [agentStatus, setAgentStatus] = useState(null) const [historyReady, setHistoryReady] = useState(false) const endRef = useRef(null) const transport = useMemo( () => new DefaultChatTransport({ api: withPascalBasePath('/api/agent/chat'), prepareSendMessagesRequest(request) { return { body: { messages: request.messages, sceneId, }, } }, }), [sceneId], ) const { error, messages, sendMessage, setMessages, status, stop } = useChat({ id: `pascal-agent:${sceneId}`, experimental_throttle: 80, transport, }) const isRunning = status === 'submitted' || status === 'streaming' const latestMessageRenderKey = useMemo(() => { const latest = messages.at(-1) if (!latest) return '' return latest.parts .map((part) => { const displayPart = part as DisplayPart return `${displayPart.type}:${displayPart.state ?? ''}:${displayPart.text?.length ?? 0}` }) .join('|') }, [messages]) useEffect(() => { let active = true fetch(withPascalBasePath('/api/agent/chat')) .then(async (response) => { if (!response.ok) throw new Error('status_unavailable') return (await response.json()) as AgentStatus }) .then((next) => { if (active) setAgentStatus(next) }) .catch(() => { if (active) setAgentStatus({ configured: false, maxSteps: 0, model: '' }) }) return () => { active = false } }, []) useEffect(() => { try { const saved = window.localStorage.getItem(historyKey) if (saved) { const parsed = JSON.parse(saved) as UIMessage[] if (Array.isArray(parsed)) setMessages(parsed) } } catch { window.localStorage.removeItem(historyKey) } finally { setHistoryReady(true) } }, [historyKey, setMessages]) useEffect(() => { if (!historyReady) return window.localStorage.setItem(historyKey, JSON.stringify(textOnlyHistory(messages))) }, [historyKey, historyReady, messages]) useEffect(() => { if (!latestMessageRenderKey) return endRef.current?.scrollIntoView({ behavior: isRunning ? 'smooth' : 'auto' }) }, [isRunning, latestMessageRenderKey]) const submit = () => { const text = input.trim() if (!text || isRunning || agentStatus?.configured === false) return sendMessage({ role: 'user', parts: [{ type: 'text', text }] }) setInput('') } return (
AI 设计助手
{agentStatus === null ? '正在检查服务' : agentStatus.configured ? agentStatus.model : '尚未配置模型'}
{messages.length === 0 ? (

从描述开始建模

描述空间、面积、房间和风格要求,助手会直接操作当前场景并自动检查结果。

{STARTER_PROMPTS.map((prompt) => ( ))}
) : (
{messages.map((message) => (
{message.parts.map((rawPart, index) => { const part = rawPart as DisplayPart if (part.type === 'text' && part.text) { return (

{part.text}

) } return })}
))} {status === 'submitted' ? (
正在分析当前场景
) : null}
)}
{agentStatus?.configured === false ? (
请先配置 PASCAL_AGENT_API_KEY、模型和接口地址。
) : null} {error ?
{error.message}
: null}