PicoBot/web/src/components/Sidebar/TopicList.tsx
oudecheng 598d425c28 feat: 话题添加描述字段,工具消息按 tool_call_id 合并展示
- TopicSummary 新增 description 字段,侧边栏优先显示描述
- ToolPanel 使用 toolCallId 将 tool_call 和 tool_result 配对合并展示
- 保存消息时同步更新 topics 表的 message_count 和 last_active_at
- ChatMessage 新增 toolCallId 字段

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:30:21 +08:00

130 lines
4.7 KiB
TypeScript

import { Plus, MessageSquare, Layers, Hash, Clock } from 'lucide-react'
import type { Topic } from '../../types/protocol'
interface TopicListProps {
sessionId: string | null
sessionTitle: string
topics: Topic[]
currentTopicId: string | null
isReadOnly: boolean
onCreateTopic: () => 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({
sessionId,
sessionTitle,
topics,
currentTopicId,
isReadOnly,
onCreateTopic,
onSwitchTopic,
}: 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.length > 0 && (
<span className="text-xs text-zinc-500">({topics.length})</span>
)}
</h2>
<button
onClick={onCreateTopic}
disabled={isReadOnly || !sessionId}
className={`flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm transition-all ${
isReadOnly || !sessionId
? 'bg-zinc-500/10 text-zinc-500 cursor-not-allowed'
: 'bg-[#00f0ff]/10 text-[#00f0ff] hover:bg-[#00f0ff]/20 border border-[#00f0ff]/30'
}`}
>
<Plus className="h-4 w-4" />
</button>
</div>
{/* Session 标题 */}
{sessionTitle && (
<div className="px-4 py-2 border-b border-white/8 bg-[#00f0ff]/5">
<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" />
<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" />
<p></p>
<p className="text-xs mt-1">"新建"</p>
</div>
) : (
<div className="space-y-1">
{topics.map((topic, index) => (
<button
key={topic.id}
onClick={() => onSwitchTopic(topic.id)}
className={`w-full rounded-xl px-3 py-3 text-left text-sm transition-all ${
topic.id === currentTopicId
? 'bg-gradient-to-r from-[#00f0ff]/20 to-transparent border border-[#00f0ff]/30'
: 'hover:bg-white/5 border border-transparent'
}`}
>
<div className="flex items-start gap-3">
<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.description || topic.title}
</div>
<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-2 w-2 rounded-full bg-[#00f0ff] shadow-lg shadow-[#00f0ff]/50 mt-1.5" />
)}
</div>
</button>
))}
</div>
)}
</div>
</div>
)
}