feat(api): 实现消息合并 API,支持工具调用显示
- 新增 MergedMessage、ToolCallInfo 类型定义 - 创建 message-merger.ts 消息合并工具 - 更新 sessions 路由使用合并后的消息格式 - 前端新增 ToolCallsDisplay 组件展示工具调用 - 工具调用显示状态、时长,可展开查看参数和结果
This commit is contained in:
@@ -97,9 +97,14 @@ export async function listBySession(sessionId: string): Promise<MessageInfo[]> {
|
||||
}
|
||||
}
|
||||
|
||||
// 消息 ID 是降序的,所以排序后最新的在后面(时间升序)
|
||||
// 实际上按 ID 字符串排序即可,因为降序 ID 自然会让旧消息在前
|
||||
return messages.sort((a, b) => b.id.localeCompare(a.id)).reverse();
|
||||
// 按创建时间升序排列(最旧的在前)
|
||||
// 当时间相同时,按 ID 降序排列(ID 使用降序时间戳,所以 ID 大的更旧)
|
||||
return messages.sort((a, b) => {
|
||||
const timeDiff = a.createdAt - b.createdAt;
|
||||
if (timeDiff !== 0) return timeDiff;
|
||||
// ID 降序时间戳:数字大 = 时间旧,所以用 b - a 让旧的在前
|
||||
return b.id.localeCompare(a.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -150,12 +150,12 @@ export async function createTool(
|
||||
messageId: string,
|
||||
toolCallId: string,
|
||||
toolName: string,
|
||||
args: Record<string, unknown>
|
||||
args?: Record<string, unknown>
|
||||
): Promise<ToolPart> {
|
||||
return create<ToolPart>(messageId, 'tool', {
|
||||
toolCallId,
|
||||
toolName,
|
||||
args,
|
||||
args: args ?? {},
|
||||
status: 'pending',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { getSessionManager } from '../session/manager.js';
|
||||
import { CreateSessionInputSchema } from '../types.js';
|
||||
import { mergeMessages, type MessageWithParts, type RawPart } from '../utils/message-merger.js';
|
||||
|
||||
export const sessionsRouter = new Hono();
|
||||
|
||||
@@ -100,7 +101,14 @@ sessionsRouter.delete('/:id', async (c) => {
|
||||
/**
|
||||
* GET /sessions/:id/messages - 获取会话消息
|
||||
*
|
||||
* 从 Core 存储读取完整的消息历史(包含 tool-call 和 tool-result)
|
||||
* 从 Core 存储读取消息,合并为用户视角的对话轮次
|
||||
*
|
||||
* 合并规则:
|
||||
* - 用户/系统消息:直接返回
|
||||
* - 助手消息:将连续的 assistant + tool 消息合并为一条
|
||||
* - content: 所有文本内容合并
|
||||
* - toolCalls: 工具调用列表(含参数、状态、结果)
|
||||
* - reasoning: 推理内容(如果有)
|
||||
*/
|
||||
sessionsRouter.get('/:id/messages', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
@@ -115,44 +123,51 @@ sessionsRouter.get('/:id/messages', async (c) => {
|
||||
);
|
||||
}
|
||||
|
||||
// 从 Core 存储读取消息
|
||||
const sessionData = await sessionManager.loadSessionData(id);
|
||||
try {
|
||||
// 动态导入 Core 存储 API
|
||||
const corePath = '@ai-assistant/core';
|
||||
const { MessageStorage, PartStorage } = (await import(/* webpackIgnore: true */ corePath)) as {
|
||||
MessageStorage: {
|
||||
listBySession(sessionId: string): Promise<
|
||||
Array<{ id: string; sessionId: string; role: string; createdAt: number; partIds: string[] }>
|
||||
>;
|
||||
};
|
||||
PartStorage: {
|
||||
getByIds(messageId: string, partIds: string[]): Promise<RawPart[]>;
|
||||
};
|
||||
};
|
||||
|
||||
if (!sessionData) {
|
||||
// 获取消息列表(按创建时间排序)
|
||||
const messageInfos = await MessageStorage.listBySession(id);
|
||||
|
||||
// 获取每个消息的 Parts
|
||||
const messagesWithParts: MessageWithParts[] = [];
|
||||
for (const msgInfo of messageInfos) {
|
||||
const parts = await PartStorage.getByIds(msgInfo.id, msgInfo.partIds);
|
||||
messagesWithParts.push({
|
||||
info: {
|
||||
id: msgInfo.id,
|
||||
sessionId: msgInfo.sessionId,
|
||||
role: msgInfo.role as 'user' | 'assistant' | 'system' | 'tool',
|
||||
createdAt: msgInfo.createdAt,
|
||||
partIds: msgInfo.partIds,
|
||||
},
|
||||
parts,
|
||||
});
|
||||
}
|
||||
|
||||
// 合并消息
|
||||
const mergedMessages = mergeMessages(messagesWithParts);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: mergedMessages,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Sessions] Failed to load messages:', error);
|
||||
return c.json({
|
||||
success: true,
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 为消息添加 ID 并转换内容格式(AI SDK 格式 -> 字符串)
|
||||
const messagesWithId = sessionData.messages.map(
|
||||
(msg: { role: string; content: unknown }, index: number) => {
|
||||
// 转换 AI SDK 内容格式为字符串
|
||||
let content: string;
|
||||
if (typeof msg.content === 'string') {
|
||||
content = msg.content;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// AI SDK 格式: [{type: "text", text: "..."}, ...]
|
||||
content = msg.content
|
||||
.filter((block: { type?: string }) => block.type === 'text')
|
||||
.map((block: { text?: string }) => block.text || '')
|
||||
.join('');
|
||||
} else {
|
||||
content = String(msg.content);
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${msg.role}-${id}-${index}`,
|
||||
role: msg.role,
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: messagesWithId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +188,48 @@ export interface SSEEvent {
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 合并消息相关 (API 层消息合并) ============
|
||||
|
||||
/**
|
||||
* 工具调用状态
|
||||
*/
|
||||
export type ToolCallStatus = 'pending' | 'running' | 'completed' | 'error';
|
||||
|
||||
/**
|
||||
* 工具调用信息(合并后)
|
||||
*/
|
||||
export interface ToolCallInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
status: ToolCallStatus;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
duration?: number; // 执行时长 ms
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并后的消息格式
|
||||
*
|
||||
* 将 AI SDK 产生的多条消息(user → assistant → tool → assistant)
|
||||
* 合并为用户视角的对话轮次
|
||||
*/
|
||||
export interface MergedMessage {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
toolCalls?: ToolCallInfo[];
|
||||
hasReasoning?: boolean;
|
||||
reasoning?: string;
|
||||
metadata?: {
|
||||
model?: string;
|
||||
stepCount?: number;
|
||||
totalTokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============ API 响应 ============
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 消息合并工具
|
||||
*
|
||||
* 将 AI SDK 产生的多条原始消息合并为用户视角的对话轮次
|
||||
*
|
||||
* AI SDK 消息流:
|
||||
* user → assistant(text+tool) → tool(result) → assistant(final)
|
||||
*
|
||||
* 合并后:
|
||||
* user → assistant (含 content + toolCalls[])
|
||||
*/
|
||||
|
||||
import type { MergedMessage, ToolCallInfo, ToolCallStatus } from '../types.js';
|
||||
|
||||
/**
|
||||
* 原始消息信息(来自 Core 存储)
|
||||
*/
|
||||
export interface RawMessageInfo {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
role: 'user' | 'assistant' | 'system' | 'tool';
|
||||
createdAt: number;
|
||||
partIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 类型(简化版,仅包含合并需要的字段)
|
||||
*/
|
||||
export interface RawPart {
|
||||
id: string;
|
||||
type: string;
|
||||
createdAt: number;
|
||||
// TextPart
|
||||
text?: string;
|
||||
// ToolPart
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
args?: Record<string, unknown>;
|
||||
status?: ToolCallStatus;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
startedAt?: number;
|
||||
completedAt?: number;
|
||||
// ReasoningPart
|
||||
// (uses text field)
|
||||
}
|
||||
|
||||
/**
|
||||
* 带 Parts 的消息
|
||||
*/
|
||||
export interface MessageWithParts {
|
||||
info: RawMessageInfo;
|
||||
parts: RawPart[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用临时收集器
|
||||
*/
|
||||
interface ToolCallCollector {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
status: ToolCallStatus;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
startedAt?: number;
|
||||
completedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并消息
|
||||
*
|
||||
* @param messagesWithParts 带 Parts 的消息列表(按时间升序)
|
||||
* @returns 合并后的消息列表
|
||||
*/
|
||||
export function mergeMessages(messagesWithParts: MessageWithParts[]): MergedMessage[] {
|
||||
const result: MergedMessage[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < messagesWithParts.length) {
|
||||
const current = messagesWithParts[i];
|
||||
|
||||
// 用户消息或系统消息:直接输出
|
||||
if (current.info.role === 'user' || current.info.role === 'system') {
|
||||
result.push(createMergedMessage(current));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assistant 消息:收集连续的 assistant + tool 消息
|
||||
if (current.info.role === 'assistant') {
|
||||
const merged = mergeAssistantTurn(messagesWithParts, i);
|
||||
result.push(merged.message);
|
||||
i = merged.nextIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tool 消息单独出现(理论上不应该发生,但做容错处理)
|
||||
if (current.info.role === 'tool') {
|
||||
// 跳过孤立的 tool 消息
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并一个 Assistant 对话轮次
|
||||
*
|
||||
* 从当前位置开始,收集连续的 assistant 和 tool 消息
|
||||
*/
|
||||
function mergeAssistantTurn(
|
||||
messages: MessageWithParts[],
|
||||
startIndex: number
|
||||
): { message: MergedMessage; nextIndex: number } {
|
||||
const sessionId = messages[startIndex].info.sessionId;
|
||||
const firstMessage = messages[startIndex];
|
||||
|
||||
// 收集所有文本内容
|
||||
const textContents: string[] = [];
|
||||
// 收集所有推理内容
|
||||
const reasoningContents: string[] = [];
|
||||
// 收集所有工具调用(按 toolCallId 去重)
|
||||
const toolCallMap = new Map<string, ToolCallCollector>();
|
||||
// 记录最早的时间戳
|
||||
let earliestTimestamp = firstMessage.info.createdAt;
|
||||
// 记录最早的消息 ID
|
||||
let earliestMessageId = firstMessage.info.id;
|
||||
|
||||
let i = startIndex;
|
||||
|
||||
// 收集连续的 assistant 和 tool 消息
|
||||
while (i < messages.length) {
|
||||
const msg = messages[i];
|
||||
|
||||
// 遇到 user 或 system 消息,结束收集
|
||||
if (msg.info.role === 'user' || msg.info.role === 'system') {
|
||||
break;
|
||||
}
|
||||
|
||||
// 更新最早时间戳
|
||||
if (msg.info.createdAt < earliestTimestamp) {
|
||||
earliestTimestamp = msg.info.createdAt;
|
||||
earliestMessageId = msg.info.id;
|
||||
}
|
||||
|
||||
// 处理 Parts
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === 'text' && part.text) {
|
||||
textContents.push(part.text);
|
||||
} else if (part.type === 'reasoning' && part.text) {
|
||||
reasoningContents.push(part.text);
|
||||
} else if (part.type === 'tool' && part.toolCallId) {
|
||||
// 合并工具调用信息
|
||||
const existing = toolCallMap.get(part.toolCallId);
|
||||
if (existing) {
|
||||
// 更新已有的工具调用(用更新的状态覆盖)
|
||||
if (part.status && isMoreRecentStatus(part.status, existing.status)) {
|
||||
existing.status = part.status;
|
||||
}
|
||||
if (part.result !== undefined) {
|
||||
existing.result = part.result;
|
||||
}
|
||||
if (part.error !== undefined) {
|
||||
existing.error = part.error;
|
||||
}
|
||||
if (part.startedAt !== undefined) {
|
||||
existing.startedAt = part.startedAt;
|
||||
}
|
||||
if (part.completedAt !== undefined) {
|
||||
existing.completedAt = part.completedAt;
|
||||
}
|
||||
} else {
|
||||
// 新的工具调用
|
||||
toolCallMap.set(part.toolCallId, {
|
||||
id: part.toolCallId,
|
||||
name: part.toolName || 'unknown',
|
||||
arguments: part.args || {},
|
||||
status: part.status || 'pending',
|
||||
result: part.result,
|
||||
error: part.error,
|
||||
startedAt: part.startedAt,
|
||||
completedAt: part.completedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// 构建工具调用列表
|
||||
const toolCalls: ToolCallInfo[] = [];
|
||||
for (const collector of toolCallMap.values()) {
|
||||
const toolCall: ToolCallInfo = {
|
||||
id: collector.id,
|
||||
name: collector.name,
|
||||
arguments: collector.arguments,
|
||||
status: collector.status,
|
||||
};
|
||||
|
||||
if (collector.result !== undefined) {
|
||||
toolCall.result = collector.result;
|
||||
}
|
||||
if (collector.error !== undefined) {
|
||||
toolCall.error = collector.error;
|
||||
}
|
||||
if (collector.startedAt && collector.completedAt) {
|
||||
toolCall.duration = collector.completedAt - collector.startedAt;
|
||||
}
|
||||
|
||||
toolCalls.push(toolCall);
|
||||
}
|
||||
|
||||
// 构建合并后的消息
|
||||
const merged: MergedMessage = {
|
||||
id: earliestMessageId,
|
||||
sessionId,
|
||||
role: 'assistant',
|
||||
content: textContents.join(''),
|
||||
timestamp: new Date(earliestTimestamp).toISOString(),
|
||||
};
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
merged.toolCalls = toolCalls;
|
||||
}
|
||||
|
||||
if (reasoningContents.length > 0) {
|
||||
merged.hasReasoning = true;
|
||||
merged.reasoning = reasoningContents.join('\n');
|
||||
}
|
||||
|
||||
return {
|
||||
message: merged,
|
||||
nextIndex: i,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建简单的合并消息(用户消息或系统消息)
|
||||
*/
|
||||
function createMergedMessage(messageWithParts: MessageWithParts): MergedMessage {
|
||||
const { info, parts } = messageWithParts;
|
||||
|
||||
// 提取文本内容
|
||||
const textContent = parts
|
||||
.filter((p) => p.type === 'text' && p.text)
|
||||
.map((p) => p.text!)
|
||||
.join('');
|
||||
|
||||
return {
|
||||
id: info.id,
|
||||
sessionId: info.sessionId,
|
||||
role: info.role as 'user' | 'assistant' | 'system',
|
||||
content: textContent,
|
||||
timestamp: new Date(info.createdAt).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断状态是否更新
|
||||
*
|
||||
* 状态优先级:pending < running < completed/error
|
||||
*/
|
||||
function isMoreRecentStatus(newStatus: ToolCallStatus, oldStatus: ToolCallStatus): boolean {
|
||||
const priority: Record<ToolCallStatus, number> = {
|
||||
pending: 0,
|
||||
running: 1,
|
||||
completed: 2,
|
||||
error: 2,
|
||||
};
|
||||
return priority[newStatus] >= priority[oldStatus];
|
||||
}
|
||||
@@ -11,11 +11,46 @@ export interface Session {
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用状态
|
||||
*/
|
||||
export type ToolCallStatus = 'pending' | 'running' | 'completed' | 'error';
|
||||
|
||||
/**
|
||||
* 工具调用信息
|
||||
*/
|
||||
export interface ToolCallInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
status: ToolCallStatus;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
duration?: number; // 执行时长 ms
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息(合并后的格式)
|
||||
*
|
||||
* 助手消息可能包含工具调用信息,将多个原始消息合并为一条
|
||||
*/
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
/** 工具调用列表 */
|
||||
toolCalls?: ToolCallInfo[];
|
||||
/** 是否包含推理过程 */
|
||||
hasReasoning?: boolean;
|
||||
/** 推理内容 */
|
||||
reasoning?: string;
|
||||
/** 元数据 */
|
||||
metadata?: {
|
||||
model?: string;
|
||||
stepCount?: number;
|
||||
totalTokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
* Chat Message Component
|
||||
*/
|
||||
|
||||
import { User, Bot, Copy, Check } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
User,
|
||||
Bot,
|
||||
Copy,
|
||||
Check,
|
||||
Wrench,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, forwardRef } from 'react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { fadeInUp, smoothTransition } from '../utils/animations';
|
||||
import { Markdown } from './Markdown';
|
||||
import type { Message } from '../api/client.js';
|
||||
import type { Message, ToolCallInfo, ToolCallStatus } from '../api/types.js';
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
@@ -59,6 +71,10 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
{/* 工具调用显示 */}
|
||||
{!isUser && message.toolCalls && message.toolCalls.length > 0 && (
|
||||
<ToolCallsDisplay toolCalls={message.toolCalls} />
|
||||
)}
|
||||
<div className="message-content text-gray-200">
|
||||
{isUser ? (
|
||||
// 用户消息:保持原样显示
|
||||
@@ -135,3 +151,137 @@ export function TypingIndicator() {
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 工具调用显示组件 ============
|
||||
|
||||
/**
|
||||
* 获取工具状态图标
|
||||
*/
|
||||
function getStatusIcon(status: ToolCallStatus) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Clock size={14} className="text-yellow-500" />;
|
||||
case 'running':
|
||||
return <Loader2 size={14} className="text-blue-500 animate-spin" />;
|
||||
case 'completed':
|
||||
return <CheckCircle2 size={14} className="text-green-500" />;
|
||||
case 'error':
|
||||
return <AlertCircle size={14} className="text-red-500" />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化执行时长
|
||||
*/
|
||||
function formatDuration(ms?: number): string {
|
||||
if (!ms) return '';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用列表容器
|
||||
*/
|
||||
interface ToolCallsDisplayProps {
|
||||
toolCalls: ToolCallInfo[];
|
||||
}
|
||||
|
||||
function ToolCallsDisplay({ toolCalls }: ToolCallsDisplayProps) {
|
||||
return (
|
||||
<div className="mb-3 space-y-2">
|
||||
{toolCalls.map((toolCall) => (
|
||||
<ToolCallItem key={toolCall.id} toolCall={toolCall} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个工具调用项
|
||||
*/
|
||||
interface ToolCallItemProps {
|
||||
toolCall: ToolCallInfo;
|
||||
}
|
||||
|
||||
function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasDetails =
|
||||
Object.keys(toolCall.arguments).length > 0 ||
|
||||
toolCall.result !== undefined ||
|
||||
toolCall.error !== undefined;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-700 rounded-lg overflow-hidden bg-gray-800/30">
|
||||
{/* 头部:工具名称、状态、时长 */}
|
||||
<button
|
||||
onClick={() => hasDetails && setExpanded(!expanded)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-2 text-sm',
|
||||
hasDetails && 'hover:bg-gray-700/50 cursor-pointer',
|
||||
!hasDetails && 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
|
||||
<span className="font-mono text-gray-200 flex-1 text-left truncate">
|
||||
{toolCall.name}
|
||||
</span>
|
||||
{getStatusIcon(toolCall.status)}
|
||||
{toolCall.duration && (
|
||||
<span className="text-xs text-gray-500">{formatDuration(toolCall.duration)}</span>
|
||||
)}
|
||||
{hasDetails && (
|
||||
<span className="text-gray-500">
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 展开的详情 */}
|
||||
<AnimatePresence>
|
||||
{expanded && hasDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border-t border-gray-700 overflow-hidden"
|
||||
>
|
||||
<div className="px-3 py-2 space-y-2 text-xs">
|
||||
{/* 参数 */}
|
||||
{Object.keys(toolCall.arguments).length > 0 && (
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Arguments:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 结果 */}
|
||||
{toolCall.result !== undefined && (
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Result:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
|
||||
{typeof toolCall.result === 'string'
|
||||
? toolCall.result
|
||||
: JSON.stringify(toolCall.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误 */}
|
||||
{toolCall.error && (
|
||||
<div>
|
||||
<div className="text-red-400 mb-1">Error:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
|
||||
{toolCall.error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user