feat(api): 实现消息合并 API,支持工具调用显示
- 新增 MergedMessage、ToolCallInfo 类型定义 - 创建 message-merger.ts 消息合并工具 - 更新 sessions 路由使用合并后的消息格式 - 前端新增 ToolCallsDisplay 组件展示工具调用 - 工具调用显示状态、时长,可展开查看参数和结果
This commit is contained in:
@@ -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