| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- /**
- * 工具调用面板组件 - 显示 MCP 工具调用过程
- * 移动端优化:默认折叠,点击展开
- */
- 'use client';
- import { useState } from 'react';
- interface ToolCall {
- tool: string;
- result: unknown;
- }
- interface ToolCallPanelProps {
- toolCalls: ToolCall[];
- isLoading: boolean;
- }
- export default function ToolCallPanel({ toolCalls, isLoading }: ToolCallPanelProps) {
- const [isExpanded, setIsExpanded] = useState(false);
- const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set());
- const toggleItem = (idx: number) => {
- setExpandedItems((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(idx)) {
- newSet.delete(idx);
- } else {
- newSet.add(idx);
- }
- return newSet;
- });
- };
- // 移动端:显示为底部可折叠面板
- // 桌面端:显示为右侧固定面板
- const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
- if (isMobile) {
- return (
- <div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700 shadow-lg rounded-t-2xl z-50 max-h-[50vh] overflow-hidden flex flex-col">
- {/* 折叠头部 */}
- <button
- onClick={() => setIsExpanded(!isExpanded)}
- className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900 border-b dark:border-gray-700 w-full text-left"
- >
- <div className="flex items-center gap-2">
- <div className="w-2 h-2 rounded-full bg-purple-500"></div>
- <span className="font-medium text-gray-800 dark:text-gray-200 text-sm">
- 工具调用 ({toolCalls.length})
- </span>
- {isLoading && (
- <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
- )}
- </div>
- <svg
- className={`w-5 h-5 text-gray-500 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
- </svg>
- </button>
- {/* 可折叠内容 */}
- {isExpanded && (
- <div className="flex-1 overflow-y-auto p-3 space-y-2">
- {toolCalls.length === 0 && isLoading && (
- <div className="text-center text-gray-400 py-4">
- <p className="text-sm">AI 正在调用工具...</p>
- </div>
- )}
- {toolCalls.map((call, idx) => {
- const isItemExpanded = expandedItems.has(idx);
- return (
- <div key={idx} className="bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden">
- <button
- onClick={() => toggleItem(idx)}
- className="w-full flex items-center justify-between p-2 text-left"
- >
- <span className="font-medium text-purple-700 dark:text-purple-400 text-sm truncate flex-1">
- {call.tool}
- </span>
- <svg
- className={`w-4 h-4 text-gray-500 transition-transform flex-shrink-0 ml-2 ${isItemExpanded ? 'rotate-180' : ''}`}
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
- </svg>
- </button>
- {isItemExpanded && (
- <div className="p-2 border-t dark:border-gray-700">
- <pre className="text-[11px] bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap break-all">
- {JSON.stringify(call.result, null, 2)}
- </pre>
- </div>
- )}
- </div>
- );
- })}
- {isLoading && toolCalls.length > 0 && (
- <div className="text-center text-gray-400 py-2">
- <p className="text-xs">处理中...</p>
- </div>
- )}
- </div>
- )}
- </div>
- );
- }
- // 桌面端布局
- return (
- <div className="w-80 bg-white dark:bg-gray-800 border-l dark:border-gray-700 overflow-y-auto">
- <div className="p-4 border-b dark:border-gray-700">
- <h3 className="font-semibold text-gray-800 dark:text-white">
- 工具调用
- </h3>
- </div>
- <div className="p-4 space-y-3">
- {toolCalls.length === 0 && isLoading && (
- <div className="text-center text-gray-400 py-8">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
- <p>AI 正在思考...</p>
- </div>
- )}
- {toolCalls.map((call, idx) => (
- <div key={idx} className="tool-call">
- <div className="flex items-center justify-between mb-1">
- <span className="font-medium text-purple-700 dark:text-purple-400">{call.tool}</span>
- <span className="text-xs text-gray-500">#{idx + 1}</span>
- </div>
- <pre className="text-xs bg-gray-50 dark:bg-gray-900 p-2 rounded overflow-x-auto">
- {JSON.stringify(call.result, null, 2)}
- </pre>
- </div>
- ))}
- {isLoading && toolCalls.length > 0 && (
- <div className="text-center text-gray-400 py-4">
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto"></div>
- <p className="text-sm mt-2">处理中...</p>
- </div>
- )}
- </div>
- </div>
- );
- }
|