PicoBot/web/src/hooks/useChat.ts
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

206 lines
5.0 KiB
TypeScript

import { useState, useCallback, useRef } from 'react'
import type {
Command,
ChatMessage,
Topic,
WsOutbound,
AssistantResponse,
ToolCall,
ToolResult,
ToolPending,
SessionEstablished,
SessionCreated,
SessionList,
} from '../types/protocol'
interface UseChatReturn {
messages: ChatMessage[]
currentSessionId: string | null
currentTopicId: string | null
topics: Topic[]
isLoading: boolean
handleMessage: (content: string) => void
handleCommand: (command: Command) => void
clearMessages: () => void
handleServerMessage: (message: WsOutbound) => void
}
export function useChat(): UseChatReturn {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null)
const [currentTopicId, setCurrentTopicId] = useState<string | null>(null)
const [topics, setTopics] = useState<Topic[]>([])
const [isLoading, setIsLoading] = useState(false)
// Message ID generator
const messageIdCounter = useRef(0)
const generateMessageId = () => {
messageIdCounter.current += 1
return `msg_${Date.now()}_${messageIdCounter.current}`
}
const handleServerMessage = useCallback((message: WsOutbound) => {
switch (message.type) {
case 'session_established': {
const msg = message as SessionEstablished
setCurrentSessionId(msg.session_id)
break
}
case 'session_created': {
const msg = message as SessionCreated
setCurrentTopicId(msg.session_id)
setIsLoading(false)
break
}
case 'session_list': {
const msg = message as SessionList
// Convert sessions to topics format
const newTopics = msg.sessions.map((s) => ({
id: s.session_id,
session_id: s.session_id,
title: s.title,
message_count: Number(s.message_count),
created_at: s.last_active_at,
updated_at: s.last_active_at,
}))
setTopics(newTopics)
if (msg.current_session_id) {
setCurrentTopicId(msg.current_session_id)
}
break
}
case 'assistant_response': {
const msg = message as AssistantResponse
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'assistant',
content: msg.content,
timestamp: Date.now(),
type: 'message',
},
])
setIsLoading(false)
break
}
case 'tool_call': {
const msg = message as ToolCall
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'tool',
content: msg.content,
timestamp: Date.now(),
type: 'tool_call',
toolName: msg.tool_name,
arguments: msg.arguments,
},
])
break
}
case 'tool_result': {
const msg = message as ToolResult
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'tool',
content: msg.content,
timestamp: Date.now(),
type: 'tool_result',
toolName: msg.tool_name,
},
])
break
}
case 'tool_pending': {
const msg = message as ToolPending
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'tool',
content: `${msg.content}\n\n${msg.resume_hint}`,
timestamp: Date.now(),
type: 'tool_pending',
toolName: msg.tool_name,
},
])
break
}
case 'error': {
setMessages((prev) => [
...prev,
{
id: generateMessageId(),
role: 'assistant',
content: `Error: ${message.message}`,
timestamp: Date.now(),
type: 'message',
},
])
setIsLoading(false)
break
}
}
}, [])
const handleMessage = useCallback((content: string) => {
// Add user message to list
setMessages((prev) => [
...prev,
{
id: generateMessageId(),
role: 'user',
content,
timestamp: Date.now(),
type: 'message',
},
])
setIsLoading(true)
}, [])
const handleCommand = useCallback((command: Command) => {
// Handle local state updates for commands
switch (command.type) {
case 'create_session':
// Optimistically update
setIsLoading(true)
break
case 'list_sessions':
setIsLoading(true)
break
case 'switch_session':
setCurrentTopicId(command.session_id)
// Clear messages when switching topic
setMessages([])
break
}
}, [])
const clearMessages = useCallback(() => {
setMessages([])
}, [])
return {
messages,
currentSessionId,
currentTopicId,
topics,
isLoading,
handleMessage,
handleCommand,
clearMessages,
handleServerMessage,
}
}