- 使用 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>
128 lines
3.1 KiB
TypeScript
128 lines
3.1 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
import type { WsInbound, WsOutbound, ConnectionStatus } from '../types/protocol'
|
|
|
|
interface UseWebSocketOptions {
|
|
url: string
|
|
onMessage?: (message: WsOutbound) => void
|
|
onConnect?: () => void
|
|
onDisconnect?: () => void
|
|
onError?: (error: Event) => void
|
|
reconnectInterval?: number
|
|
maxReconnectAttempts?: number
|
|
}
|
|
|
|
interface UseWebSocketReturn {
|
|
status: ConnectionStatus
|
|
sendMessage: (message: WsInbound) => boolean
|
|
connect: () => void
|
|
disconnect: () => void
|
|
}
|
|
|
|
export function useWebSocket({
|
|
url,
|
|
onMessage,
|
|
onConnect,
|
|
onDisconnect,
|
|
onError,
|
|
reconnectInterval = 3000,
|
|
maxReconnectAttempts = 5,
|
|
}: UseWebSocketOptions): UseWebSocketReturn {
|
|
const [status, setStatus] = useState<ConnectionStatus>('disconnected')
|
|
const wsRef = useRef<WebSocket | null>(null)
|
|
const reconnectAttemptsRef = useRef(0)
|
|
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const isManualDisconnectRef = useRef(false)
|
|
|
|
const connect = useCallback(() => {
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
return
|
|
}
|
|
|
|
isManualDisconnectRef.current = false
|
|
setStatus('connecting')
|
|
|
|
try {
|
|
const ws = new WebSocket(url)
|
|
wsRef.current = ws
|
|
|
|
ws.onopen = () => {
|
|
setStatus('connected')
|
|
reconnectAttemptsRef.current = 0
|
|
onConnect?.()
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data) as WsOutbound
|
|
onMessage?.(message)
|
|
} catch (error) {
|
|
console.error('Failed to parse message:', error)
|
|
}
|
|
}
|
|
|
|
ws.onerror = (error) => {
|
|
setStatus('error')
|
|
onError?.(error)
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
setStatus('disconnected')
|
|
onDisconnect?.()
|
|
|
|
// Auto reconnect if not manually disconnected
|
|
if (!isManualDisconnectRef.current && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
reconnectAttemptsRef.current += 1
|
|
reconnectTimerRef.current = setTimeout(() => {
|
|
connect()
|
|
}, reconnectInterval)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
setStatus('error')
|
|
console.error('WebSocket connection error:', error)
|
|
}
|
|
}, [url, onMessage, onConnect, onDisconnect, onError, reconnectInterval, maxReconnectAttempts])
|
|
|
|
const disconnect = useCallback(() => {
|
|
isManualDisconnectRef.current = true
|
|
|
|
if (reconnectTimerRef.current) {
|
|
clearTimeout(reconnectTimerRef.current)
|
|
reconnectTimerRef.current = null
|
|
}
|
|
|
|
if (wsRef.current) {
|
|
wsRef.current.close()
|
|
wsRef.current = null
|
|
}
|
|
|
|
setStatus('disconnected')
|
|
}, [])
|
|
|
|
const sendMessage = useCallback((message: WsInbound): boolean => {
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
wsRef.current.send(JSON.stringify(message))
|
|
return true
|
|
}
|
|
console.warn('WebSocket is not connected')
|
|
return false
|
|
}, [])
|
|
|
|
// Auto connect on mount
|
|
useEffect(() => {
|
|
connect()
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
disconnect()
|
|
}
|
|
}, [connect, disconnect])
|
|
|
|
return {
|
|
status,
|
|
sendMessage,
|
|
connect,
|
|
disconnect,
|
|
}
|
|
}
|