refactor(web): 简化 UI 架构,移除三级选择器

- 移除 ChannelSelector 和 SessionSelector 组件
- 新增 SessionInfo 组件显示当前会话信息
- 简化 useChat hook,移除 channels/sessions 状态管理
- 优化 TopicList UI,添加时间格式化显示
- 将废弃组件移至 .deprecated/ 目录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
oudecheng 2026-05-27 15:39:50 +08:00
parent e9e1439428
commit 10fb67320a
7 changed files with 256 additions and 256 deletions

View File

@ -353,6 +353,11 @@ async fn handle_inbound(
"Updating current_topic_id" "Updating current_topic_id"
); );
*current_topic_id = Some(topic_id.clone()); *current_topic_id = Some(topic_id.clone());
// 加载并发送该话题的历史消息
if let Err(e) = send_topic_history(&store, topic_id, sender).await {
tracing::warn!(error = %e, topic_id = %topic_id, "Failed to send topic history");
}
} }
} else if let Some(ref error) = response.error { } else if let Some(ref error) = response.error {
tracing::warn!( tracing::warn!(

View File

@ -1,9 +1,8 @@
import { useCallback, useMemo, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { Zap, Cpu, Activity } from 'lucide-react' import { Zap, Cpu, MessageSquare } from 'lucide-react'
import { ChatContainer } from './components/Chat/ChatContainer' import { ChatContainer } from './components/Chat/ChatContainer'
import { TopicList } from './components/Sidebar/TopicList' import { TopicList } from './components/Sidebar/TopicList'
import { ChannelSelector } from './components/Sidebar/ChannelSelector' import { SessionInfo } from './components/Sidebar/SessionInfo'
import { SessionSelector } from './components/Sidebar/SessionSelector'
import { ToolPanel } from './components/Panel/ToolPanel' import { ToolPanel } from './components/Panel/ToolPanel'
import { ConnectionStatus } from './components/ConnectionStatus' import { ConnectionStatus } from './components/ConnectionStatus'
import { useWebSocket } from './hooks/useWebSocket' import { useWebSocket } from './hooks/useWebSocket'
@ -14,24 +13,29 @@ const WS_URL = 'ws://127.0.0.1:19876/ws'
function App() { function App() {
const { const {
// 消息 // 连接状态
messages, connectionId,
isLoading, isConnected,
// 三级状态 // Session 状态
channels, session,
selectedChannel, sessionId,
sessions, chatId,
selectedSession, // Topic 状态
topics, topics,
selectedTopic, selectedTopic,
// 消息状态
messages,
isLoading,
isReadOnly, isReadOnly,
// 方法 // 方法
handleMessage, handleMessage,
handleCommand, handleCommand,
handleServerMessage, handleServerMessage,
selectChannel,
selectSession,
selectTopic, selectTopic,
createTopic,
switchTopic,
requestSessionList,
requestTopicList,
} = useChat() } = useChat()
const { status, sendMessage } = useWebSocket({ const { status, sendMessage } = useWebSocket({
@ -39,61 +43,30 @@ function App() {
onMessage: handleServerMessage, onMessage: handleServerMessage,
}) })
// 获取选中通道的 Session // 连接建立后自动加载 Session
const channelSessions = useMemo(() => {
if (!selectedChannel) return []
return sessions.filter((s) => s.channel_name === selectedChannel)
}, [sessions, selectedChannel])
// 获取选中 Session 的 title
const selectedSessionTitle = useMemo(() => {
const session = sessions.find((s) => s.id === selectedSession)
return session?.title || ''
}, [sessions, selectedSession])
// 获取当前通道名称
const currentChannelName = useMemo(() => {
const channel = channels.find((c) => c.id === selectedChannel)
return channel?.name || selectedChannel || ''
}, [channels, selectedChannel])
// 通道变化时加载该通道的 Sessions
useEffect(() => { useEffect(() => {
if (selectedChannel && status === 'connected') { if (isConnected && status === 'connected') {
const cmd: Command = { // 1. 请求 Session 列表(会自动选择第一个)
type: 'list_sessions_by_channel', const sessionCmd = requestSessionList()
channel_name: selectedChannel, handleCommand(sessionCmd)
include_archived: false, sendMessage({ type: 'command', payload: JSON.stringify(sessionCmd) })
}
handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
} }
}, [selectedChannel, status, handleCommand, sendMessage]) }, [isConnected, status, handleCommand, sendMessage, requestSessionList])
// Session 变化时加载该 Session 的 Topics // Session 加载后自动加载 Topics
useEffect(() => { useEffect(() => {
if (selectedSession && status === 'connected') { if (sessionId && status === 'connected') {
// 1. 加载 Session 信息 const topicCmd = requestTopicList()
const cmd: Command = { if (topicCmd) {
type: 'load_session', handleCommand(topicCmd)
session_id: selectedSession, sendMessage({ type: 'command', payload: JSON.stringify(topicCmd) })
} }
handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
// 2. 加载 Topics 列表
const topicsCmd: Command = {
type: 'list_topics',
session_id: selectedSession,
}
handleCommand(topicsCmd)
sendMessage({ type: 'command', payload: JSON.stringify(topicsCmd) })
} }
}, [selectedSession, status, handleCommand, sendMessage]) }, [sessionId, status, handleCommand, sendMessage, requestTopicList])
const handleSendMessage = useCallback( const handleSendMessage = useCallback(
(content: string) => { (content: string) => {
if (isReadOnly) { if (isReadOnly || !sessionId) {
return return
} }
@ -112,9 +85,9 @@ function App() {
break break
case 'use': case 'use':
if (args[0]) { if (args[0]) {
cmd = { type: 'load_session', session_id: args[0] } cmd = { type: 'switch_session', session_id: args[0] }
} else { } else {
alert('Usage: /use <session_id>') alert('Usage: /use <topic_id>')
return return
} }
break break
@ -133,54 +106,37 @@ function App() {
sendMessage({ sendMessage({
type: 'message', type: 'message',
content, content,
chat_id: selectedSession ?? undefined, chat_id: chatId,
}) })
} }
}, },
[sendMessage, handleMessage, handleCommand, selectedSession, isReadOnly] [sendMessage, handleMessage, handleCommand, sessionId, chatId, isReadOnly]
) )
const handleCreateTopic = useCallback(() => { const handleCreateTopic = useCallback(() => {
if (isReadOnly || !selectedSession) { if (isReadOnly || !sessionId) {
return return
} }
const title = prompt('Enter topic title:') const title = prompt('Enter topic title:')
if (title) { if (title) {
// TODO: 实现 create_topic 命令 const cmd = createTopic(title)
// 目前 Session 和 Topic 是同一个概念,简化处理
const cmd: Command = { type: 'create_session', title }
handleCommand(cmd) handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
} }
}, [sendMessage, handleCommand, selectedSession, isReadOnly]) }, [sendMessage, handleCommand, createTopic, sessionId, isReadOnly])
const handleSwitchTopic = useCallback( const handleSwitchTopic = useCallback(
(topicId: string) => { (topicId: string) => {
selectTopic(topicId) selectTopic(topicId)
// Topic 切换时重新加载 const cmd = switchTopic(topicId)
const cmd: Command = { type: 'load_session', session_id: topicId }
handleCommand(cmd) handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
}, },
[sendMessage, handleCommand, selectTopic] [sendMessage, handleCommand, switchTopic, selectTopic]
) )
const handleSelectChannel = useCallback( const toolMessages = messages
(channelId: string) => {
selectChannel(channelId)
},
[selectChannel]
)
const handleSelectSession = useCallback(
(sessionId: string) => {
selectSession(sessionId)
},
[selectSession]
)
const toolMessages = useMemo(() => messages, [messages])
return ( return (
<div className="flex h-screen flex-col bg-[#0a0a0f] text-white overflow-hidden"> <div className="flex h-screen flex-col bg-[#0a0a0f] text-white overflow-hidden">
@ -202,11 +158,11 @@ function App() {
<Cpu className="h-4 w-4 text-[#00f0ff]" /> <Cpu className="h-4 w-4 text-[#00f0ff]" />
<span>AI Ready</span> <span>AI Ready</span>
</div> </div>
{selectedSession && ( {session && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-emerald-400" /> <MessageSquare className="h-4 w-4 text-emerald-400" />
<span className="font-mono text-xs"> <span className="font-mono text-xs">
{selectedSession.slice(0, 8)}... {session.title}
</span> </span>
</div> </div>
)} )}
@ -215,24 +171,12 @@ function App() {
{/* Main Content */} {/* Main Content */}
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
{/* Left Sidebar - 三级选择器 */} {/* Left Sidebar - 简化为 Session 信息 + Topic 列表 */}
<div className="w-72 shrink-0 border-r border-white/8 bg-[#12121a]/50 flex flex-col"> <div className="w-72 shrink-0 border-r border-white/8 bg-[#12121a]/50 flex flex-col">
{/* Channel Selector */} {/* Session Info */}
<ChannelSelector <SessionInfo
channels={channels} session={session}
selectedChannel={selectedChannel} connectionId={connectionId}
onSelectChannel={handleSelectChannel}
/>
{/* Divider */}
<div className="border-b border-white/8" />
{/* Session Selector */}
<SessionSelector
sessions={channelSessions}
selectedSession={selectedSession}
channelId={selectedChannel || ''}
onSelectSession={handleSelectSession}
/> />
{/* Divider */} {/* Divider */}
@ -241,8 +185,8 @@ function App() {
{/* Topic List */} {/* Topic List */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<TopicList <TopicList
sessionId={selectedSession} sessionId={sessionId}
sessionTitle={selectedSessionTitle} sessionTitle={session?.title ?? ''}
topics={topics} topics={topics}
currentTopicId={selectedTopic} currentTopicId={selectedTopic}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
@ -258,7 +202,7 @@ function App() {
messages={messages} messages={messages}
isLoading={isLoading} isLoading={isLoading}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
channelName={currentChannelName} channelName={session?.title ?? 'PicoBot'}
onSendMessage={handleSendMessage} onSendMessage={handleSendMessage}
/> />
</div> </div>

View File

@ -0,0 +1,50 @@
import { Wifi, FolderOpen, Hash } from 'lucide-react'
import type { Session } from '../../types/protocol'
interface SessionInfoProps {
session: Session | null
connectionId: string | null
}
export function SessionInfo({ session, connectionId }: SessionInfoProps) {
return (
<div className="p-4">
<div className="flex items-center gap-2 mb-3">
<Wifi className="h-4 w-4 text-[#00f0ff]" />
<span className="text-sm font-medium text-white">WebSocket</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-400">
线
</span>
</div>
<div className="rounded-xl border border-white/10 bg-[#1a1a25]/80 p-3">
<div className="flex items-center gap-2 mb-2">
<FolderOpen className="h-4 w-4 text-zinc-400" />
<span className="text-xs text-zinc-500 uppercase tracking-wider"></span>
</div>
{session ? (
<div className="space-y-1">
<p className="text-sm font-medium text-white truncate">
{session.title}
</p>
<div className="flex items-center gap-1 text-xs text-zinc-500">
<Hash className="h-3 w-3" />
<span className="font-mono">{session.id.slice(0, 8)}...</span>
</div>
</div>
) : (
<p className="text-sm text-zinc-500">...</p>
)}
{connectionId && (
<div className="mt-2 pt-2 border-t border-white/10">
<p className="text-xs text-zinc-600 font-mono">
conn: {connectionId.slice(0, 8)}...
</p>
</div>
)}
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { Plus, MessageSquare, Eye, Layers } from 'lucide-react' import { Plus, MessageSquare, Layers, Hash, Clock } from 'lucide-react'
import type { Topic } from '../../types/protocol' import type { Topic } from '../../types/protocol'
interface TopicListProps { interface TopicListProps {
@ -11,6 +11,22 @@ interface TopicListProps {
onSwitchTopic: (topicId: string) => void onSwitchTopic: (topicId: string) => void
} }
function formatTime(timestamp: number): string {
const date = new Date(timestamp)
const now = new Date()
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays === 0) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
} else if (diffDays === 1) {
return '昨天'
} else if (diffDays < 7) {
return `${diffDays}天前`
} else {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
}
}
export function TopicList({ export function TopicList({
sessionId, sessionId,
sessionTitle, sessionTitle,
@ -22,15 +38,13 @@ export function TopicList({
}: TopicListProps) { }: TopicListProps) {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-white/8 px-4 py-3"> <div className="flex items-center justify-between border-b border-white/8 px-4 py-3">
<h2 className="font-semibold text-white flex items-center gap-2 text-sm"> <h2 className="font-semibold text-white flex items-center gap-2 text-sm">
<Layers className="h-4 w-4 text-[#00f0ff]" /> <Layers className="h-4 w-4 text-[#00f0ff]" />
Topics
{isReadOnly && ( {topics.length > 0 && (
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-500/20 text-zinc-400 flex items-center gap-1"> <span className="text-xs text-zinc-500">({topics.length})</span>
<Eye className="h-3 w-3" />
</span>
)} )}
</h2> </h2>
<button <button
@ -47,24 +61,26 @@ export function TopicList({
</button> </button>
</div> </div>
{/* Session 信息 */} {/* Session 标题 */}
{sessionId && ( {sessionTitle && (
<div className="px-4 py-2 border-b border-white/8 bg-[#00f0ff]/5"> <div className="px-4 py-2 border-b border-white/8 bg-[#00f0ff]/5">
<p className="text-xs text-zinc-500"> Session</p> <p className="text-xs text-zinc-500 mb-1"></p>
<p className="text-sm text-zinc-300 truncate">{sessionTitle}</p> <p className="text-sm text-zinc-300 font-medium truncate">{sessionTitle}</p>
</div> </div>
)} )}
{/* Topics 列表 */}
<div className="flex-1 overflow-y-auto p-3"> <div className="flex-1 overflow-y-auto p-3">
{!sessionId ? ( {!sessionId ? (
<div className="p-4 text-center text-sm text-zinc-500"> <div className="p-4 text-center text-sm text-zinc-500">
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" /> <MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
Session <p>...</p>
</div> </div>
) : topics.length === 0 ? ( ) : topics.length === 0 ? (
<div className="p-4 text-center text-sm text-zinc-500"> <div className="p-4 text-center text-sm text-zinc-500">
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" /> <MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
Session Topics <p></p>
<p className="text-xs mt-1">"新建"</p>
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
@ -79,22 +95,29 @@ export function TopicList({
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="mt-0.5 text-xs text-zinc-500 font-mono">{index + 1}</span> <span className="mt-0.5 text-xs text-zinc-500 font-mono w-4">
{index + 1}
</span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className={`truncate font-medium ${ <div className={`truncate font-medium ${
topic.id === currentTopicId ? 'text-[#00f0ff]' : 'text-zinc-300' topic.id === currentTopicId ? 'text-[#00f0ff]' : 'text-zinc-300'
}`}> }`}>
{topic.title} {topic.title}
</div> </div>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-3 mt-1.5">
<span className="text-xs text-zinc-500"> <span className="text-xs text-zinc-500 flex items-center gap-1">
<Hash className="h-3 w-3" />
{topic.message_count} {topic.message_count}
</span> </span>
{topic.id === currentTopicId && ( <span className="text-xs text-zinc-600 flex items-center gap-1">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[#00f0ff] shadow-lg shadow-[#00f0ff]/50" /> <Clock className="h-3 w-3" />
)} {formatTime(topic.updated_at)}
</span>
</div> </div>
</div> </div>
{topic.id === currentTopicId && (
<span className="inline-block h-2 w-2 rounded-full bg-[#00f0ff] shadow-lg shadow-[#00f0ff]/50 mt-1.5" />
)}
</div> </div>
</button> </button>
))} ))}

View File

@ -9,42 +9,30 @@ import type {
ToolResult, ToolResult,
ToolPending, ToolPending,
SessionEstablished, SessionEstablished,
SessionCreated,
SessionList, SessionList,
SessionLoaded,
Channel,
ChannelList,
TopicList, TopicList,
TopicSummary, TopicSummary,
Session,
} from '../types/protocol' } from '../types/protocol'
// Session 类型 // 简化后的层级状态
export interface Session {
id: string
title: string
channel_name: string
message_count: number
created_at: number
updated_at: number
}
// 三级状态管理
interface UseChatReturn { interface UseChatReturn {
// 连接状态
connectionId: string | null
isConnected: boolean
// 简化的层级状态
session: Session | null
sessionId: string | null
chatId: string
topics: Topic[]
selectedTopic: string | null
// 消息 // 消息
messages: ChatMessage[] messages: ChatMessage[]
isLoading: boolean isLoading: boolean
// 三级选择状态 // 是否只读WebSocket 通道始终可写)
channels: Channel[]
selectedChannel: string | null
sessions: Session[]
selectedSession: string | null
topics: Topic[]
selectedTopic: string | null
// 是否只读
isReadOnly: boolean isReadOnly: boolean
// 方法 // 方法
@ -53,23 +41,26 @@ interface UseChatReturn {
clearMessages: () => void clearMessages: () => void
handleServerMessage: (message: WsOutbound) => void handleServerMessage: (message: WsOutbound) => void
// 三级选择方法 // Topic 方法
selectChannel: (channelId: string) => void
selectSession: (sessionId: string) => void
selectTopic: (topicId: string) => void selectTopic: (topicId: string) => void
createTopic: (title: string) => Command
switchTopic: (topicId: string) => Command
// 初始化方法
requestSessionList: () => Command
requestTopicList: () => Command | null
} }
const DEFAULT_CHANNEL = 'websocket'
const DEFAULT_CHAT_ID = 'default'
export function useChat(): UseChatReturn { export function useChat(): UseChatReturn {
const [messages, setMessages] = useState<ChatMessage[]>([]) const [messages, setMessages] = useState<ChatMessage[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
// 三级选择状态 // 简化的状态管理
const [channels, setChannels] = useState<Channel[]>([]) const [connectionId, setConnectionId] = useState<string | null>(null)
const [selectedChannel, setSelectedChannel] = useState<string | null>(null) const [session, setSession] = useState<Session | null>(null)
const [sessions, setSessions] = useState<Session[]>([])
const [selectedSession, setSelectedSession] = useState<string | null>(null)
const [topics, setTopics] = useState<Topic[]>([]) const [topics, setTopics] = useState<Topic[]>([])
const [selectedTopic, setSelectedTopic] = useState<string | null>(null) const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
@ -80,69 +71,47 @@ export function useChat(): UseChatReturn {
return `msg_${Date.now()}_${messageIdCounter.current}` return `msg_${Date.now()}_${messageIdCounter.current}`
} }
const isConnected = useMemo(() => connectionId !== null, [connectionId])
const sessionId = useMemo(() => session?.id ?? null, [session])
const chatId = useMemo(() => sessionId ?? DEFAULT_CHAT_ID, [sessionId])
const handleServerMessage = useCallback((message: WsOutbound) => { const handleServerMessage = useCallback((message: WsOutbound) => {
console.log('Received message:', message) // 调试日志 console.log('Received message:', message)
switch (message.type) { switch (message.type) {
case 'session_established': { case 'session_established': {
const msg = message as SessionEstablished const msg = message as SessionEstablished
// 不在这里自动选择,等 channel_list 和 session_list setConnectionId(msg.session_id)
console.log('Session established:', msg.session_id) console.log('Connection established:', msg.session_id)
break
}
case 'channel_list': {
const msg = message as ChannelList
setChannels(msg.channels)
// 默认选中第一个可写通道
if (!selectedChannel && msg.channels.length > 0) {
const writableChannel = msg.channels.find((c) => c.isWritable)
const defaultChannel = writableChannel || msg.channels[0]
setSelectedChannel(defaultChannel.id)
}
break break
} }
case 'session_list': { case 'session_list': {
const msg = message as SessionList const msg = message as SessionList
console.log('Session list received:', msg) // 调试日志 console.log('Session list received:', msg)
// 按通道筛选 Session
const newSessions = msg.sessions.map((s) => ({ // 自动选择第一个 SessionWebSocket 通道只有一个)
id: s.session_id, if (msg.sessions.length > 0) {
title: s.title, const firstSession = msg.sessions[0]
channel_name: s.channel_name || msg.channel_name || 'unknown', setSession({
message_count: Number(s.message_count), id: firstSession.session_id,
created_at: s.last_active_at, title: firstSession.title,
updated_at: s.last_active_at, channel_name: firstSession.channel_name,
})) created_at: Date.now(),
console.log('Parsed sessions:', newSessions) // 调试日志 updated_at: Date.now(),
setSessions(newSessions) })
// 默认选中最新的 Session
if (!selectedSession && newSessions.length > 0) {
setSelectedSession(newSessions[0].id)
} }
setIsLoading(false)
break break
} }
case 'session_created': { case 'session_created': {
const msg = message as SessionCreated // 创建新 Topic 后更新列表
// 添加到 Session 列表
const newSession: Session = {
id: msg.session_id,
title: msg.title,
channel_name: selectedChannel || 'websocket',
message_count: 0,
created_at: Date.now(),
updated_at: Date.now(),
}
setSessions((prev) => [newSession, ...prev])
setSelectedSession(msg.session_id)
setIsLoading(false) setIsLoading(false)
break break
} }
case 'session_loaded': { case 'session_loaded': {
const msg = message as SessionLoaded
setSelectedSession(msg.session_id)
setIsLoading(false) setIsLoading(false)
setMessages([]) setMessages([])
break break
@ -151,6 +120,7 @@ export function useChat(): UseChatReturn {
case 'topic_list': { case 'topic_list': {
const msg = message as TopicList const msg = message as TopicList
console.log('Topic list received:', msg) console.log('Topic list received:', msg)
// 转换 topics 格式 // 转换 topics 格式
const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({ const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({
id: t.topic_id, id: t.topic_id,
@ -161,7 +131,8 @@ export function useChat(): UseChatReturn {
updated_at: t.last_active_at, updated_at: t.last_active_at,
})) }))
setTopics(newTopics) setTopics(newTopics)
// 默认选中第一个 Topic
// 默认选中第一个 Topic如果没有选中
if (newTopics.length > 0 && !selectedTopic) { if (newTopics.length > 0 && !selectedTopic) {
setSelectedTopic(newTopics[0].id) setSelectedTopic(newTopics[0].id)
} }
@ -248,8 +219,13 @@ export function useChat(): UseChatReturn {
setIsLoading(false) setIsLoading(false)
break break
} }
case 'channel_list':
case 'pong':
// 忽略这些消息
break
} }
}, [selectedChannel, selectedSession]) }, [selectedTopic])
const handleMessage = useCallback((content: string) => { const handleMessage = useCallback((content: string) => {
setMessages((prev) => [ setMessages((prev) => [
@ -268,23 +244,13 @@ export function useChat(): UseChatReturn {
const handleCommand = useCallback((command: Command) => { const handleCommand = useCallback((command: Command) => {
switch (command.type) { switch (command.type) {
case 'create_session': case 'create_session':
setIsLoading(true)
break
case 'list_sessions':
case 'list_sessions_by_channel':
setIsLoading(true)
break
case 'switch_session': case 'switch_session':
case 'load_session': case 'load_session':
setIsLoading(true) case 'list_sessions':
setMessages([]) case 'list_sessions_by_channel':
break
case 'list_topics': case 'list_topics':
setIsLoading(true) setIsLoading(true)
break break
case 'list_channels':
setIsLoading(true)
break
} }
}, []) }, [])
@ -292,53 +258,65 @@ export function useChat(): UseChatReturn {
setMessages([]) setMessages([])
}, []) }, [])
// 三级选择方法 // Topic 操作方法
const selectChannel = useCallback((channelId: string) => {
setSelectedChannel(channelId)
// 切换通道时重置 Session 和 Topic
setSelectedSession(null)
setSelectedTopic(null)
setSessions([])
setTopics([])
setMessages([])
}, [])
const selectSession = useCallback((sessionId: string) => {
setSelectedSession(sessionId)
// 切换 Session 时重置 Topic
setSelectedTopic(null)
setTopics([])
setMessages([])
}, [])
const selectTopic = useCallback((topicId: string) => { const selectTopic = useCallback((topicId: string) => {
setSelectedTopic(topicId) setSelectedTopic(topicId)
setMessages([]) setMessages([])
}, []) }, [])
// 计算是否只读 const createTopic = useCallback((title: string): Command => {
const isReadOnly = useMemo(() => { return {
if (!selectedChannel) return true type: 'create_session',
const channel = channels.find((c) => c.id === selectedChannel) title,
return !channel?.isWritable }
}, [selectedChannel, channels]) }, [])
const switchTopic = useCallback((topicId: string): Command => {
return {
type: 'switch_session',
session_id: topicId,
}
}, [])
// 初始化方法
const requestSessionList = useCallback((): Command => {
return {
type: 'list_sessions_by_channel',
channel_name: DEFAULT_CHANNEL,
include_archived: false,
}
}, [])
const requestTopicList = useCallback((): Command | null => {
if (!sessionId) return null
return {
type: 'list_topics',
session_id: sessionId,
}
}, [sessionId])
// WebSocket 通道始终可写
const isReadOnly = false
return { return {
messages, connectionId,
isLoading, isConnected,
channels, session,
selectedChannel, sessionId,
sessions, chatId,
selectedSession,
topics, topics,
selectedTopic, selectedTopic,
messages,
isLoading,
isReadOnly, isReadOnly,
handleMessage, handleMessage,
handleCommand, handleCommand,
clearMessages, clearMessages,
handleServerMessage, handleServerMessage,
selectChannel,
selectSession,
selectTopic, selectTopic,
createTopic,
switchTopic,
requestSessionList,
requestTopicList,
} }
} }