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"
|
"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!(
|
||||||
|
|||||||
164
web/src/App.tsx
164
web/src/App.tsx
@ -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>
|
||||||
|
|||||||
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'
|
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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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) => ({
|
// 自动选择第一个 Session(WebSocket 通道只有一个)
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user