- 使用 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>
157 lines
4.9 KiB
TypeScript
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
|