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:
parent
e9e1439428
commit
10fb67320a
@ -353,6 +353,11 @@ async fn handle_inbound(
|
||||
"Updating current_topic_id"
|
||||
);
|
||||
*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 {
|
||||
tracing::warn!(
|
||||
|
||||
164
web/src/App.tsx
164
web/src/App.tsx
@ -1,9 +1,8 @@
|
||||
import { useCallback, useMemo, useEffect } from 'react'
|
||||
import { Zap, Cpu, Activity } from 'lucide-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { Zap, Cpu, MessageSquare } from 'lucide-react'
|
||||
import { ChatContainer } from './components/Chat/ChatContainer'
|
||||
import { TopicList } from './components/Sidebar/TopicList'
|
||||
import { ChannelSelector } from './components/Sidebar/ChannelSelector'
|
||||
import { SessionSelector } from './components/Sidebar/SessionSelector'
|
||||
import { SessionInfo } from './components/Sidebar/SessionInfo'
|
||||
import { ToolPanel } from './components/Panel/ToolPanel'
|
||||
import { ConnectionStatus } from './components/ConnectionStatus'
|
||||
import { useWebSocket } from './hooks/useWebSocket'
|
||||
@ -14,24 +13,29 @@ const WS_URL = 'ws://127.0.0.1:19876/ws'
|
||||
|
||||
function App() {
|
||||
const {
|
||||
// 消息
|
||||
messages,
|
||||
isLoading,
|
||||
// 三级状态
|
||||
channels,
|
||||
selectedChannel,
|
||||
sessions,
|
||||
selectedSession,
|
||||
// 连接状态
|
||||
connectionId,
|
||||
isConnected,
|
||||
// Session 状态
|
||||
session,
|
||||
sessionId,
|
||||
chatId,
|
||||
// Topic 状态
|
||||
topics,
|
||||
selectedTopic,
|
||||
// 消息状态
|
||||
messages,
|
||||
isLoading,
|
||||
isReadOnly,
|
||||
// 方法
|
||||
handleMessage,
|
||||
handleCommand,
|
||||
handleServerMessage,
|
||||
selectChannel,
|
||||
selectSession,
|
||||
selectTopic,
|
||||
createTopic,
|
||||
switchTopic,
|
||||
requestSessionList,
|
||||
requestTopicList,
|
||||
} = useChat()
|
||||
|
||||
const { status, sendMessage } = useWebSocket({
|
||||
@ -39,61 +43,30 @@ function App() {
|
||||
onMessage: handleServerMessage,
|
||||
})
|
||||
|
||||
// 获取选中通道的 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
|
||||
// 连接建立后自动加载 Session
|
||||
useEffect(() => {
|
||||
if (selectedChannel && status === 'connected') {
|
||||
const cmd: Command = {
|
||||
type: 'list_sessions_by_channel',
|
||||
channel_name: selectedChannel,
|
||||
include_archived: false,
|
||||
if (isConnected && status === 'connected') {
|
||||
// 1. 请求 Session 列表(会自动选择第一个)
|
||||
const sessionCmd = requestSessionList()
|
||||
handleCommand(sessionCmd)
|
||||
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(() => {
|
||||
if (selectedSession && status === 'connected') {
|
||||
// 1. 加载 Session 信息
|
||||
const cmd: Command = {
|
||||
type: 'load_session',
|
||||
session_id: selectedSession,
|
||||
if (sessionId && status === 'connected') {
|
||||
const topicCmd = requestTopicList()
|
||||
if (topicCmd) {
|
||||
handleCommand(topicCmd)
|
||||
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(
|
||||
(content: string) => {
|
||||
if (isReadOnly) {
|
||||
if (isReadOnly || !sessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -112,9 +85,9 @@ function App() {
|
||||
break
|
||||
case 'use':
|
||||
if (args[0]) {
|
||||
cmd = { type: 'load_session', session_id: args[0] }
|
||||
cmd = { type: 'switch_session', session_id: args[0] }
|
||||
} else {
|
||||
alert('Usage: /use <session_id>')
|
||||
alert('Usage: /use <topic_id>')
|
||||
return
|
||||
}
|
||||
break
|
||||
@ -133,54 +106,37 @@ function App() {
|
||||
sendMessage({
|
||||
type: 'message',
|
||||
content,
|
||||
chat_id: selectedSession ?? undefined,
|
||||
chat_id: chatId,
|
||||
})
|
||||
}
|
||||
},
|
||||
[sendMessage, handleMessage, handleCommand, selectedSession, isReadOnly]
|
||||
[sendMessage, handleMessage, handleCommand, sessionId, chatId, isReadOnly]
|
||||
)
|
||||
|
||||
const handleCreateTopic = useCallback(() => {
|
||||
if (isReadOnly || !selectedSession) {
|
||||
if (isReadOnly || !sessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
const title = prompt('Enter topic title:')
|
||||
if (title) {
|
||||
// TODO: 实现 create_topic 命令
|
||||
// 目前 Session 和 Topic 是同一个概念,简化处理
|
||||
const cmd: Command = { type: 'create_session', title }
|
||||
const cmd = createTopic(title)
|
||||
handleCommand(cmd)
|
||||
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
|
||||
}
|
||||
}, [sendMessage, handleCommand, selectedSession, isReadOnly])
|
||||
}, [sendMessage, handleCommand, createTopic, sessionId, isReadOnly])
|
||||
|
||||
const handleSwitchTopic = useCallback(
|
||||
(topicId: string) => {
|
||||
selectTopic(topicId)
|
||||
// Topic 切换时重新加载
|
||||
const cmd: Command = { type: 'load_session', session_id: topicId }
|
||||
const cmd = switchTopic(topicId)
|
||||
handleCommand(cmd)
|
||||
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
|
||||
},
|
||||
[sendMessage, handleCommand, selectTopic]
|
||||
[sendMessage, handleCommand, switchTopic, selectTopic]
|
||||
)
|
||||
|
||||
const handleSelectChannel = useCallback(
|
||||
(channelId: string) => {
|
||||
selectChannel(channelId)
|
||||
},
|
||||
[selectChannel]
|
||||
)
|
||||
|
||||
const handleSelectSession = useCallback(
|
||||
(sessionId: string) => {
|
||||
selectSession(sessionId)
|
||||
},
|
||||
[selectSession]
|
||||
)
|
||||
|
||||
const toolMessages = useMemo(() => messages, [messages])
|
||||
const toolMessages = messages
|
||||
|
||||
return (
|
||||
<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]" />
|
||||
<span>AI Ready</span>
|
||||
</div>
|
||||
{selectedSession && (
|
||||
{session && (
|
||||
<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">
|
||||
{selectedSession.slice(0, 8)}...
|
||||
{session.title}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@ -215,24 +171,12 @@ function App() {
|
||||
|
||||
{/* Main Content */}
|
||||
<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">
|
||||
{/* Channel Selector */}
|
||||
<ChannelSelector
|
||||
channels={channels}
|
||||
selectedChannel={selectedChannel}
|
||||
onSelectChannel={handleSelectChannel}
|
||||
/>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-b border-white/8" />
|
||||
|
||||
{/* Session Selector */}
|
||||
<SessionSelector
|
||||
sessions={channelSessions}
|
||||
selectedSession={selectedSession}
|
||||
channelId={selectedChannel || ''}
|
||||
onSelectSession={handleSelectSession}
|
||||
{/* Session Info */}
|
||||
<SessionInfo
|
||||
session={session}
|
||||
connectionId={connectionId}
|
||||
/>
|
||||
|
||||
{/* Divider */}
|
||||
@ -241,8 +185,8 @@ function App() {
|
||||
{/* Topic List */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<TopicList
|
||||
sessionId={selectedSession}
|
||||
sessionTitle={selectedSessionTitle}
|
||||
sessionId={sessionId}
|
||||
sessionTitle={session?.title ?? ''}
|
||||
topics={topics}
|
||||
currentTopicId={selectedTopic}
|
||||
isReadOnly={isReadOnly}
|
||||
@ -258,7 +202,7 @@ function App() {
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
isReadOnly={isReadOnly}
|
||||
channelName={currentChannelName}
|
||||
channelName={session?.title ?? 'PicoBot'}
|
||||
onSendMessage={handleSendMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
50
web/src/components/Sidebar/SessionInfo.tsx
Normal file
50
web/src/components/Sidebar/SessionInfo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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'
|
||||
|
||||
interface TopicListProps {
|
||||
@ -11,6 +11,22 @@ interface TopicListProps {
|
||||
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({
|
||||
sessionId,
|
||||
sessionTitle,
|
||||
@ -22,15 +38,13 @@ export function TopicList({
|
||||
}: TopicListProps) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<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">
|
||||
<Layers className="h-4 w-4 text-[#00f0ff]" />
|
||||
Topics
|
||||
{isReadOnly && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-500/20 text-zinc-400 flex items-center gap-1">
|
||||
<Eye className="h-3 w-3" />
|
||||
只读
|
||||
</span>
|
||||
话题列表
|
||||
{topics.length > 0 && (
|
||||
<span className="text-xs text-zinc-500">({topics.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
<button
|
||||
@ -47,24 +61,26 @@ export function TopicList({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Session 信息 */}
|
||||
{sessionId && (
|
||||
{/* Session 标题 */}
|
||||
{sessionTitle && (
|
||||
<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-sm text-zinc-300 truncate">{sessionTitle}</p>
|
||||
<p className="text-xs text-zinc-500 mb-1">所属会话</p>
|
||||
<p className="text-sm text-zinc-300 font-medium truncate">{sessionTitle}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Topics 列表 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
{!sessionId ? (
|
||||
<div className="p-4 text-center text-sm text-zinc-500">
|
||||
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
请先选择 Session
|
||||
<p>等待连接...</p>
|
||||
</div>
|
||||
) : topics.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-zinc-500">
|
||||
<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 className="space-y-1">
|
||||
@ -79,23 +95,30 @@ export function TopicList({
|
||||
}`}
|
||||
>
|
||||
<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={`truncate font-medium ${
|
||||
topic.id === currentTopicId ? 'text-[#00f0ff]' : 'text-zinc-300'
|
||||
}`}>
|
||||
{topic.title}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-zinc-500">
|
||||
<div className="flex items-center gap-3 mt-1.5">
|
||||
<span className="text-xs text-zinc-500 flex items-center gap-1">
|
||||
<Hash className="h-3 w-3" />
|
||||
{topic.message_count} 条消息
|
||||
</span>
|
||||
<span className="text-xs text-zinc-600 flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{formatTime(topic.updated_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{topic.id === currentTopicId && (
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[#00f0ff] shadow-lg shadow-[#00f0ff]/50" />
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-[#00f0ff] shadow-lg shadow-[#00f0ff]/50 mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -9,42 +9,30 @@ import type {
|
||||
ToolResult,
|
||||
ToolPending,
|
||||
SessionEstablished,
|
||||
SessionCreated,
|
||||
SessionList,
|
||||
SessionLoaded,
|
||||
Channel,
|
||||
ChannelList,
|
||||
TopicList,
|
||||
TopicSummary,
|
||||
Session,
|
||||
} 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 {
|
||||
// 连接状态
|
||||
connectionId: string | null
|
||||
isConnected: boolean
|
||||
|
||||
// 简化的层级状态
|
||||
session: Session | null
|
||||
sessionId: string | null
|
||||
chatId: string
|
||||
topics: Topic[]
|
||||
selectedTopic: string | null
|
||||
|
||||
// 消息
|
||||
messages: ChatMessage[]
|
||||
isLoading: boolean
|
||||
|
||||
// 三级选择状态
|
||||
channels: Channel[]
|
||||
selectedChannel: string | null
|
||||
|
||||
sessions: Session[]
|
||||
selectedSession: string | null
|
||||
|
||||
topics: Topic[]
|
||||
selectedTopic: string | null
|
||||
|
||||
// 是否只读
|
||||
// 是否只读(WebSocket 通道始终可写)
|
||||
isReadOnly: boolean
|
||||
|
||||
// 方法
|
||||
@ -53,23 +41,26 @@ interface UseChatReturn {
|
||||
clearMessages: () => void
|
||||
handleServerMessage: (message: WsOutbound) => void
|
||||
|
||||
// 三级选择方法
|
||||
selectChannel: (channelId: string) => void
|
||||
selectSession: (sessionId: string) => void
|
||||
// Topic 方法
|
||||
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 {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 三级选择状态
|
||||
const [channels, setChannels] = useState<Channel[]>([])
|
||||
const [selectedChannel, setSelectedChannel] = useState<string | null>(null)
|
||||
|
||||
const [sessions, setSessions] = useState<Session[]>([])
|
||||
const [selectedSession, setSelectedSession] = useState<string | null>(null)
|
||||
|
||||
// 简化的状态管理
|
||||
const [connectionId, setConnectionId] = useState<string | null>(null)
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [topics, setTopics] = useState<Topic[]>([])
|
||||
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
||||
|
||||
@ -80,69 +71,47 @@ export function useChat(): UseChatReturn {
|
||||
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) => {
|
||||
console.log('Received message:', message) // 调试日志
|
||||
console.log('Received message:', message)
|
||||
|
||||
switch (message.type) {
|
||||
case 'session_established': {
|
||||
const msg = message as SessionEstablished
|
||||
// 不在这里自动选择,等 channel_list 和 session_list
|
||||
console.log('Session 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)
|
||||
}
|
||||
setConnectionId(msg.session_id)
|
||||
console.log('Connection established:', msg.session_id)
|
||||
break
|
||||
}
|
||||
|
||||
case 'session_list': {
|
||||
const msg = message as SessionList
|
||||
console.log('Session list received:', msg) // 调试日志
|
||||
// 按通道筛选 Session
|
||||
const newSessions = msg.sessions.map((s) => ({
|
||||
id: s.session_id,
|
||||
title: s.title,
|
||||
channel_name: s.channel_name || msg.channel_name || 'unknown',
|
||||
message_count: Number(s.message_count),
|
||||
created_at: s.last_active_at,
|
||||
updated_at: s.last_active_at,
|
||||
}))
|
||||
console.log('Parsed sessions:', newSessions) // 调试日志
|
||||
setSessions(newSessions)
|
||||
// 默认选中最新的 Session
|
||||
if (!selectedSession && newSessions.length > 0) {
|
||||
setSelectedSession(newSessions[0].id)
|
||||
console.log('Session list received:', msg)
|
||||
|
||||
// 自动选择第一个 Session(WebSocket 通道只有一个)
|
||||
if (msg.sessions.length > 0) {
|
||||
const firstSession = msg.sessions[0]
|
||||
setSession({
|
||||
id: firstSession.session_id,
|
||||
title: firstSession.title,
|
||||
channel_name: firstSession.channel_name,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
})
|
||||
}
|
||||
setIsLoading(false)
|
||||
break
|
||||
}
|
||||
|
||||
case 'session_created': {
|
||||
const msg = message as SessionCreated
|
||||
// 添加到 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)
|
||||
// 创建新 Topic 后更新列表
|
||||
setIsLoading(false)
|
||||
break
|
||||
}
|
||||
|
||||
case 'session_loaded': {
|
||||
const msg = message as SessionLoaded
|
||||
setSelectedSession(msg.session_id)
|
||||
setIsLoading(false)
|
||||
setMessages([])
|
||||
break
|
||||
@ -151,6 +120,7 @@ export function useChat(): UseChatReturn {
|
||||
case 'topic_list': {
|
||||
const msg = message as TopicList
|
||||
console.log('Topic list received:', msg)
|
||||
|
||||
// 转换 topics 格式
|
||||
const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({
|
||||
id: t.topic_id,
|
||||
@ -161,7 +131,8 @@ export function useChat(): UseChatReturn {
|
||||
updated_at: t.last_active_at,
|
||||
}))
|
||||
setTopics(newTopics)
|
||||
// 默认选中第一个 Topic
|
||||
|
||||
// 默认选中第一个 Topic(如果没有选中)
|
||||
if (newTopics.length > 0 && !selectedTopic) {
|
||||
setSelectedTopic(newTopics[0].id)
|
||||
}
|
||||
@ -248,8 +219,13 @@ export function useChat(): UseChatReturn {
|
||||
setIsLoading(false)
|
||||
break
|
||||
}
|
||||
|
||||
case 'channel_list':
|
||||
case 'pong':
|
||||
// 忽略这些消息
|
||||
break
|
||||
}
|
||||
}, [selectedChannel, selectedSession])
|
||||
}, [selectedTopic])
|
||||
|
||||
const handleMessage = useCallback((content: string) => {
|
||||
setMessages((prev) => [
|
||||
@ -268,23 +244,13 @@ export function useChat(): UseChatReturn {
|
||||
const handleCommand = useCallback((command: Command) => {
|
||||
switch (command.type) {
|
||||
case 'create_session':
|
||||
setIsLoading(true)
|
||||
break
|
||||
case 'list_sessions':
|
||||
case 'list_sessions_by_channel':
|
||||
setIsLoading(true)
|
||||
break
|
||||
case 'switch_session':
|
||||
case 'load_session':
|
||||
setIsLoading(true)
|
||||
setMessages([])
|
||||
break
|
||||
case 'list_sessions':
|
||||
case 'list_sessions_by_channel':
|
||||
case 'list_topics':
|
||||
setIsLoading(true)
|
||||
break
|
||||
case 'list_channels':
|
||||
setIsLoading(true)
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -292,53 +258,65 @@ export function useChat(): UseChatReturn {
|
||||
setMessages([])
|
||||
}, [])
|
||||
|
||||
// 三级选择方法
|
||||
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([])
|
||||
}, [])
|
||||
|
||||
// Topic 操作方法
|
||||
const selectTopic = useCallback((topicId: string) => {
|
||||
setSelectedTopic(topicId)
|
||||
setMessages([])
|
||||
}, [])
|
||||
|
||||
// 计算是否只读
|
||||
const isReadOnly = useMemo(() => {
|
||||
if (!selectedChannel) return true
|
||||
const channel = channels.find((c) => c.id === selectedChannel)
|
||||
return !channel?.isWritable
|
||||
}, [selectedChannel, channels])
|
||||
const createTopic = useCallback((title: string): Command => {
|
||||
return {
|
||||
type: 'create_session',
|
||||
title,
|
||||
}
|
||||
}, [])
|
||||
|
||||
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 {
|
||||
messages,
|
||||
isLoading,
|
||||
channels,
|
||||
selectedChannel,
|
||||
sessions,
|
||||
selectedSession,
|
||||
connectionId,
|
||||
isConnected,
|
||||
session,
|
||||
sessionId,
|
||||
chatId,
|
||||
topics,
|
||||
selectedTopic,
|
||||
messages,
|
||||
isLoading,
|
||||
isReadOnly,
|
||||
handleMessage,
|
||||
handleCommand,
|
||||
clearMessages,
|
||||
handleServerMessage,
|
||||
selectChannel,
|
||||
selectSession,
|
||||
selectTopic,
|
||||
createTopic,
|
||||
switchTopic,
|
||||
requestSessionList,
|
||||
requestTopicList,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user