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>
This commit is contained in:
parent
44e82e8473
commit
598d425c28
@ -13,6 +13,7 @@ pub struct TopicSummary {
|
|||||||
pub topic_id: String,
|
pub topic_id: String,
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
pub message_count: i64,
|
pub message_count: i64,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub last_active_at: i64,
|
pub last_active_at: i64,
|
||||||
@ -73,6 +74,7 @@ async fn handle_list_topics(
|
|||||||
topic_id: t.id,
|
topic_id: t.id,
|
||||||
session_id: t.session_id,
|
session_id: t.session_id,
|
||||||
title: t.title,
|
title: t.title,
|
||||||
|
description: t.description.filter(|d| !d.is_empty()),
|
||||||
message_count: t.message_count,
|
message_count: t.message_count,
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
last_active_at: t.last_active_at,
|
last_active_at: t.last_active_at,
|
||||||
|
|||||||
@ -107,6 +107,7 @@ async fn handle_create_session(
|
|||||||
topic_id: t.id,
|
topic_id: t.id,
|
||||||
session_id: t.session_id,
|
session_id: t.session_id,
|
||||||
title: t.title,
|
title: t.title,
|
||||||
|
description: t.description.filter(|d| !d.is_empty()),
|
||||||
message_count: t.message_count,
|
message_count: t.message_count,
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
last_active_at: t.last_active_at,
|
last_active_at: t.last_active_at,
|
||||||
|
|||||||
@ -32,6 +32,8 @@ pub struct TopicSummary {
|
|||||||
pub message_count: i64,
|
pub message_count: i64,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub last_active_at: i64,
|
pub last_active_at: i64,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -579,6 +579,13 @@ impl SessionStore {
|
|||||||
params![session_id, now, if is_user_message { 1 } else { 0 }],
|
params![session_id, now, if is_user_message { 1 } else { 0 }],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
if let Some(tid) = topic_id {
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE topics SET message_count = message_count + 1, last_active_at = ?2 WHERE id = ?1",
|
||||||
|
params![tid, now],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ChevronDown, ChevronRight, Play, Check, AlertTriangle, Terminal } from 'lucide-react'
|
import { ChevronDown, ChevronRight, Play, Check, AlertTriangle, Terminal } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import type { ChatMessage } from '../../types/protocol'
|
import type { ChatMessage } from '../../types/protocol'
|
||||||
|
|
||||||
interface ToolPanelProps {
|
interface ToolPanelProps {
|
||||||
@ -7,25 +7,54 @@ interface ToolPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ToolCallItem {
|
interface ToolCallItem {
|
||||||
id: string
|
toolCallId: string
|
||||||
toolName: string
|
toolName: string
|
||||||
status: 'calling' | 'result' | 'pending'
|
status: 'calling' | 'result' | 'pending'
|
||||||
arguments?: unknown
|
arguments?: unknown
|
||||||
content: string
|
resultContent: string
|
||||||
|
callContent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeToolMessages(messages: ChatMessage[]): ToolCallItem[] {
|
||||||
|
const map = new Map<string, ToolCallItem>()
|
||||||
|
|
||||||
|
for (const m of messages) {
|
||||||
|
if (m.role !== 'tool' || !m.type?.startsWith('tool_')) continue
|
||||||
|
|
||||||
|
const key = m.toolCallId || m.id
|
||||||
|
let entry = map.get(key)
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
entry = {
|
||||||
|
toolCallId: key,
|
||||||
|
toolName: m.toolName || 'Unknown',
|
||||||
|
status: 'calling',
|
||||||
|
arguments: undefined,
|
||||||
|
resultContent: '',
|
||||||
|
callContent: '',
|
||||||
|
}
|
||||||
|
map.set(key, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.type === 'tool_call') {
|
||||||
|
entry.arguments = m.arguments
|
||||||
|
entry.callContent = m.content
|
||||||
|
} else if (m.type === 'tool_result') {
|
||||||
|
entry.status = 'result'
|
||||||
|
entry.resultContent = m.content
|
||||||
|
} else if (m.type === 'tool_pending') {
|
||||||
|
entry.status = 'pending'
|
||||||
|
entry.resultContent = m.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolPanel({ messages }: ToolPanelProps) {
|
export function ToolPanel({ messages }: ToolPanelProps) {
|
||||||
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set())
|
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const toolCalls: ToolCallItem[] = messages
|
const toolCalls = useMemo(() => mergeToolMessages(messages), [messages])
|
||||||
.filter((m) => m.role === 'tool' && m.type && m.type.startsWith('tool_'))
|
|
||||||
.map((m) => ({
|
|
||||||
id: m.id,
|
|
||||||
toolName: m.toolName || 'Unknown',
|
|
||||||
status: m.type === 'tool_call' ? 'calling' : m.type === 'tool_result' ? 'result' : 'pending',
|
|
||||||
arguments: m.arguments,
|
|
||||||
content: m.content,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const toggleExpand = (id: string) => {
|
const toggleExpand = (id: string) => {
|
||||||
setExpandedTools((prev) => {
|
setExpandedTools((prev) => {
|
||||||
@ -42,7 +71,7 @@ export function ToolPanel({ messages }: ToolPanelProps) {
|
|||||||
const getStatusIcon = (status: ToolCallItem['status']) => {
|
const getStatusIcon = (status: ToolCallItem['status']) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'calling':
|
case 'calling':
|
||||||
return <Play className="h-3 w-3 text-amber-400" />
|
return <Play className="h-3 w-3 text-amber-400 animate-pulse" />
|
||||||
case 'result':
|
case 'result':
|
||||||
return <Check className="h-3 w-3 text-emerald-400" />
|
return <Check className="h-3 w-3 text-emerald-400" />
|
||||||
case 'pending':
|
case 'pending':
|
||||||
@ -91,11 +120,11 @@ export function ToolPanel({ messages }: ToolPanelProps) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{toolCalls.map((tool) => (
|
{toolCalls.map((tool) => (
|
||||||
<div
|
<div
|
||||||
key={tool.id}
|
key={tool.toolCallId}
|
||||||
className="rounded-xl border border-white/8 bg-[#1a1a25]/50 text-sm overflow-hidden"
|
className="rounded-xl border border-white/8 bg-[#1a1a25]/50 text-sm overflow-hidden"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleExpand(tool.id)}
|
onClick={() => toggleExpand(tool.toolCallId)}
|
||||||
className="flex w-full items-center justify-between px-3 py-2.5 hover:bg-white/5 transition-colors"
|
className="flex w-full items-center justify-between px-3 py-2.5 hover:bg-white/5 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -105,13 +134,13 @@ export function ToolPanel({ messages }: ToolPanelProps) {
|
|||||||
{getStatusText(tool.status)}
|
{getStatusText(tool.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{expandedTools.has(tool.id) ? (
|
{expandedTools.has(tool.toolCallId) ? (
|
||||||
<ChevronDown className="h-4 w-4 text-zinc-500" />
|
<ChevronDown className="h-4 w-4 text-zinc-500" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-4 w-4 text-zinc-500" />
|
<ChevronRight className="h-4 w-4 text-zinc-500" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{expandedTools.has(tool.id) && (
|
{expandedTools.has(tool.toolCallId) && (
|
||||||
<div className="border-t border-white/8 px-3 py-2 bg-black/20">
|
<div className="border-t border-white/8 px-3 py-2 bg-black/20">
|
||||||
{tool.arguments ? (
|
{tool.arguments ? (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
@ -122,7 +151,7 @@ export function ToolPanel({ messages }: ToolPanelProps) {
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-zinc-500 mb-1">结果:</div>
|
<div className="text-xs font-medium text-zinc-500 mb-1">结果:</div>
|
||||||
<div className="max-h-32 overflow-y-auto rounded-lg bg-black/40 p-2 text-xs whitespace-pre-wrap text-zinc-400 font-mono">
|
<div className="max-h-32 overflow-y-auto rounded-lg bg-black/40 p-2 text-xs whitespace-pre-wrap text-zinc-400 font-mono">
|
||||||
{tool.content}
|
{tool.resultContent || tool.callContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export function TopicList({
|
|||||||
<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.description || topic.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-1.5">
|
<div className="flex items-center gap-3 mt-1.5">
|
||||||
<span className="text-xs text-zinc-500 flex items-center gap-1">
|
<span className="text-xs text-zinc-500 flex items-center gap-1">
|
||||||
|
|||||||
@ -125,6 +125,7 @@ export function useChat(): UseChatReturn {
|
|||||||
id: t.topic_id,
|
id: t.topic_id,
|
||||||
session_id: t.session_id,
|
session_id: t.session_id,
|
||||||
title: t.title,
|
title: t.title,
|
||||||
|
description: t.description || undefined,
|
||||||
message_count: Number(t.message_count),
|
message_count: Number(t.message_count),
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
updated_at: t.last_active_at,
|
updated_at: t.last_active_at,
|
||||||
@ -165,6 +166,7 @@ export function useChat(): UseChatReturn {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
type: 'tool_call',
|
type: 'tool_call',
|
||||||
toolName: msg.tool_name,
|
toolName: msg.tool_name,
|
||||||
|
toolCallId: msg.tool_call_id,
|
||||||
arguments: msg.arguments,
|
arguments: msg.arguments,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@ -182,6 +184,7 @@ export function useChat(): UseChatReturn {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
toolName: msg.tool_name,
|
toolName: msg.tool_name,
|
||||||
|
toolCallId: msg.tool_call_id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
break
|
break
|
||||||
@ -198,6 +201,7 @@ export function useChat(): UseChatReturn {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
type: 'tool_pending',
|
type: 'tool_pending',
|
||||||
toolName: msg.tool_name,
|
toolName: msg.tool_name,
|
||||||
|
toolCallId: msg.tool_call_id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
break
|
break
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export interface TopicSummary {
|
|||||||
topic_id: string
|
topic_id: string
|
||||||
session_id: string
|
session_id: string
|
||||||
title: string
|
title: string
|
||||||
|
description?: string
|
||||||
message_count: number
|
message_count: number
|
||||||
created_at: number
|
created_at: number
|
||||||
last_active_at: number
|
last_active_at: number
|
||||||
@ -249,6 +250,7 @@ export interface ChatMessage {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
type?: 'message' | 'tool_call' | 'tool_result' | 'tool_pending'
|
type?: 'message' | 'tool_call' | 'tool_result' | 'tool_pending'
|
||||||
toolName?: string
|
toolName?: string
|
||||||
|
toolCallId?: string
|
||||||
arguments?: unknown
|
arguments?: unknown
|
||||||
attachments?: Attachment[]
|
attachments?: Attachment[]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user