import { useCallback, useEffect, useMemo, useRef } from 'react' import { Zap, Cpu, MessageSquare, ArrowLeft, Bot, Clock } from 'lucide-react' import { ChatContainer } from './components/Chat/ChatContainer' import { TopicList } from './components/Sidebar/TopicList' import { SessionInfo } from './components/Sidebar/SessionInfo' import { SchedulerJobList } from './components/Sidebar/SchedulerJobList' import { ToolPanel } from './components/Panel/ToolPanel' import { ConnectionStatus } from './components/ConnectionStatus' import { useWebSocket } from './hooks/useWebSocket' import { useChat } from './hooks/useChat' import type { ChatMessage, Command, Attachment, SchedulerJobSessionLookup } from './types/protocol' const WS_URL = 'ws://127.0.0.1:19876/ws' function App() { const lastAutoSwitchedTopicRef = useRef(null) const { // 连接状态 connectionId, isConnected, // Session 状态 session, sessionId, chatId, // Topic 状态 topics, selectedTopic, // 消息状态 messages, isLoading, isReadOnly, // 子智能体视图 subAgentView, // 定时任务 schedulerJobs, sidebarTab, setSidebarTab, requestSchedulerJobList, schedulerView, enterSchedulerJobView, exitSchedulerJobView, // 方法 handleMessage, handleCommand, handleServerMessage, selectTopic, createTopic, switchTopic, requestSessionList, requestTopicList, enterSubAgentView, exitSubAgentView, } = useChat() const { status, sendMessage } = useWebSocket({ url: WS_URL, onMessage: handleServerMessage, }) // 连接建立后自动加载 Session useEffect(() => { if (isConnected && status === 'connected') { // 1. 请求 Session 列表(会自动选择第一个) const sessionCmd = requestSessionList() handleCommand(sessionCmd) sendMessage({ type: 'command', payload: JSON.stringify(sessionCmd) }) } }, [isConnected, status, handleCommand, sendMessage, requestSessionList]) // Session 加载后自动加载 Topics useEffect(() => { if (sessionId && status === 'connected') { const topicCmd = requestTopicList() if (topicCmd) { handleCommand(topicCmd) sendMessage({ type: 'command', payload: JSON.stringify(topicCmd) }) } } }, [sessionId, status, handleCommand, sendMessage, requestTopicList]) // Topics 加载后,自动选择第一个并通知后端切换,以便加载历史消息 useEffect(() => { if (topics.length === 0 || status !== 'connected') { return } const firstTopic = topics[0] if (lastAutoSwitchedTopicRef.current === firstTopic.id) { return } lastAutoSwitchedTopicRef.current = firstTopic.id selectTopic(firstTopic.id) const cmd = switchTopic(firstTopic.id) handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [topics, status, selectTopic, switchTopic, handleCommand, sendMessage]); const handleSendMessage = useCallback( (content: string, attachments: Attachment[] = []) => { if (isReadOnly || !sessionId) { return } if (content.startsWith('/')) { const parts = content.slice(1).split(' ') const command = parts[0] const args = parts.slice(1) let cmd: Command switch (command) { case 'new': cmd = { type: 'create_session', title: args.join(' ') || undefined } break case 'list': cmd = { type: 'list_sessions', include_archived: args[0] === 'all' } break case 'use': if (args[0]) { cmd = { type: 'switch_topic', topic_id: args[0] } } else { alert('Usage: /use ') return } break case 'save': cmd = { type: 'save_topic', filepath: args[0] || undefined, include_subagents: false } break default: alert(`Unknown command: /${command}`) return } handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) } else { handleMessage(content, attachments) sendMessage({ type: 'message', content, attachments, chat_id: chatId, }) } }, [sendMessage, handleMessage, handleCommand, sessionId, chatId, isReadOnly] ) const handleCreateTopic = useCallback(() => { if (isReadOnly || !sessionId) { return } const cmd = createTopic() handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [sendMessage, handleCommand, createTopic, sessionId, isReadOnly]) const handleSwitchTopic = useCallback( (topicId: string) => { selectTopic(topicId) const cmd = switchTopic(topicId) handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [sendMessage, handleCommand, switchTopic, selectTopic] ) const handleNavigateToSubAgent = useCallback( (taskId: string, description: string) => { const cmd = enterSubAgentView(taskId, description) handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [enterSubAgentView, handleCommand, sendMessage] ) const handleExitSubAgentView = useCallback(() => { exitSubAgentView() }, [exitSubAgentView]) // 切换到定时任务 tab 时自动获取列表 useEffect(() => { if (sidebarTab === 'scheduler' && status === 'connected') { const cmd = requestSchedulerJobList() handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) } }, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList]) const handleRefreshSchedulerJobs = useCallback(() => { const cmd = requestSchedulerJobList() handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [handleCommand, sendMessage, requestSchedulerJobList]) const handleViewSchedulerJob = useCallback( (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => { const cmd = enterSchedulerJobView(lookup, jobId, description) handleCommand(cmd) sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [enterSchedulerJobView, handleCommand, sendMessage] ) const handleExitSchedulerJobView = useCallback(() => { exitSchedulerJobView() }, [exitSchedulerJobView]) const chatMessages = useMemo(() => { const result: ChatMessage[] = [] const toolCallIndex = new Map() for (const msg of messages) { if (msg.type === 'tool_call') { toolCallIndex.set(msg.toolCallId || msg.id, result.length) result.push({ ...msg, type: 'merged_tool', status: 'calling', callContent: msg.content, resultContent: '', }) } else if (msg.type === 'tool_result') { const idx = toolCallIndex.get(msg.toolCallId || msg.id) if (idx !== undefined) { result[idx] = { ...result[idx], status: 'result', resultContent: msg.content, durationMs: msg.durationMs, } } } else if (msg.type === 'tool_pending') { const idx = toolCallIndex.get(msg.toolCallId || msg.id) if (idx !== undefined) { result[idx] = { ...result[idx], status: 'pending', resultContent: msg.content, } } } else { result.push(msg) } } return result }, [messages]) const toolMessages = messages return (
{/* Header */}

Pico Bot

AI Ready
{session && (
{session.title}
)}
{/* Main Content */}
{/* Left Sidebar */}
{/* Tab 栏 */}
{sidebarTab === 'topics' ? ( ) : ( )}
{/* Center - Chat */}
{/* Scheduler job view back bar */} {schedulerView && (
定时任务: {schedulerView.description}
通道: {schedulerView.channel}
)} {/* Sub-agent back bar */} {subAgentView && (
子智能体: {subAgentView.description}
类型: {subAgentView.subagentType || '...'}
状态: {subAgentView.status === 'completed' ? '已完成' : subAgentView.status === 'failed' ? '失败' : subAgentView.status === 'timeout' ? '超时' : subAgentView.status === 'running' ? '执行中' : subAgentView.status === 'loading' ? '加载中...' : subAgentView.status}
)}
{} : handleSendMessage} onNavigateToSubAgent={handleNavigateToSubAgent} />
{/* Right Sidebar - Tool Panel */}
) } export default App