PicoBot/web/src/App.tsx
oudecheng 624d8e8943 feat: 添加 React Web UI 前端界面
- 使用 React 18 + TypeScript + Vite + Tailwind CSS 构建前端
- 实现 WebSocket 实时通信(useWebSocket hook)
- 添加聊天界面组件(MessageList, MessageBubble, MessageInput)
- 集成 Topic 管理(新建、列出、切换)
- 支持 Markdown 渲染(react-markdown + remark-gfm)
- 添加工具调用展示面板
- 实现深色科技主题(Tech Dark)
- 后端集成静态文件服务(tower-http)
- 添加 Makefile 和 build.sh 构建脚本
- 更新 .gitignore 忽略前端构建产物

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:43:15 +08:00

157 lines
4.9 KiB
TypeScript

import { useCallback, useMemo } from 'react'
import { Zap, Cpu, Activity } from 'lucide-react'
import { ChatContainer } from './components/Chat/ChatContainer'
import { TopicList } from './components/Sidebar/TopicList'
import { ToolPanel } from './components/Panel/ToolPanel'
import { ConnectionStatus } from './components/ConnectionStatus'
import { useWebSocket } from './hooks/useWebSocket'
import { useChat } from './hooks/useChat'
import type { Command } from './types/protocol'
const WS_URL = 'ws://127.0.0.1:19876/ws'
function App() {
const {
messages,
currentSessionId,
currentTopicId,
topics,
isLoading,
handleMessage,
handleCommand,
handleServerMessage,
} = useChat()
const { status, sendMessage } = useWebSocket({
url: WS_URL,
onMessage: handleServerMessage,
})
const handleSendMessage = useCallback(
(content: string) => {
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_session', session_id: args[0] }
} else {
alert('Usage: /use <session_id>')
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)
sendMessage({
type: 'message',
content,
chat_id: currentTopicId ?? undefined,
})
}
},
[sendMessage, handleMessage, handleCommand, currentTopicId]
)
const handleCreateTopic = useCallback(() => {
const title = prompt('Enter topic title:')
if (title) {
const cmd: Command = { type: 'create_session', title }
handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
}
}, [sendMessage, handleCommand])
const handleSwitchTopic = useCallback(
(topicId: string) => {
const cmd: Command = { type: 'switch_session', session_id: topicId }
handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
},
[sendMessage, handleCommand]
)
const toolMessages = useMemo(() => messages, [messages])
return (
<div className="flex h-screen flex-col bg-[#0a0a0f] text-white overflow-hidden">
{/* Header */}
<header className="flex items-center justify-between border-b border-white/8 bg-[#12121a]/80 backdrop-blur-md px-6 py-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Zap className="h-6 w-6 text-[#00f0ff]" />
<h1 className="text-xl font-bold tracking-tight">
<span className="text-white">Pico</span>
<span className="text-[#00f0ff] glow-text">Bot</span>
</h1>
</div>
<div className="h-4 w-px bg-white/20" />
<ConnectionStatus status={status} />
</div>
<div className="flex items-center gap-4 text-sm text-zinc-400">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-[#00f0ff]" />
<span>AI Ready</span>
</div>
{currentSessionId && (
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-emerald-400" />
<span className="font-mono text-xs">
{currentSessionId.slice(0, 8)}...
</span>
</div>
)}
</div>
</header>
{/* Main Content */}
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar - Topic List */}
<div className="w-72 shrink-0 border-r border-white/8 bg-[#12121a]/50">
<TopicList
topics={topics}
currentTopicId={currentTopicId}
onCreateTopic={handleCreateTopic}
onSwitchTopic={handleSwitchTopic}
/>
</div>
{/* Center - Chat */}
<div className="flex-1 min-w-0 bg-[#0a0a0f]">
<ChatContainer
messages={messages}
isLoading={isLoading}
onSendMessage={handleSendMessage}
/>
</div>
{/* Right Sidebar - Tool Panel */}
<div className="w-80 shrink-0 border-l border-white/8 bg-[#12121a]/50">
<ToolPanel messages={toolMessages} />
</div>
</div>
</div>
)
}
export default App