ToolCallPanel.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. /**
  2. * 工具调用面板组件 - 显示 MCP 工具调用过程
  3. * 移动端优化:默认折叠,点击展开
  4. */
  5. 'use client';
  6. import { useState } from 'react';
  7. interface ToolCall {
  8. tool: string;
  9. result: unknown;
  10. }
  11. interface ToolCallPanelProps {
  12. toolCalls: ToolCall[];
  13. isLoading: boolean;
  14. }
  15. export default function ToolCallPanel({ toolCalls, isLoading }: ToolCallPanelProps) {
  16. const [isExpanded, setIsExpanded] = useState(false);
  17. const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set());
  18. const toggleItem = (idx: number) => {
  19. setExpandedItems((prev) => {
  20. const newSet = new Set(prev);
  21. if (newSet.has(idx)) {
  22. newSet.delete(idx);
  23. } else {
  24. newSet.add(idx);
  25. }
  26. return newSet;
  27. });
  28. };
  29. // 移动端:显示为底部可折叠面板
  30. // 桌面端:显示为右侧固定面板
  31. const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
  32. if (isMobile) {
  33. return (
  34. <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">
  35. {/* 折叠头部 */}
  36. <button
  37. onClick={() => setIsExpanded(!isExpanded)}
  38. 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"
  39. >
  40. <div className="flex items-center gap-2">
  41. <div className="w-2 h-2 rounded-full bg-purple-500"></div>
  42. <span className="font-medium text-gray-800 dark:text-gray-200 text-sm">
  43. 工具调用 ({toolCalls.length})
  44. </span>
  45. {isLoading && (
  46. <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
  47. )}
  48. </div>
  49. <svg
  50. className={`w-5 h-5 text-gray-500 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
  51. fill="none"
  52. stroke="currentColor"
  53. viewBox="0 0 24 24"
  54. >
  55. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
  56. </svg>
  57. </button>
  58. {/* 可折叠内容 */}
  59. {isExpanded && (
  60. <div className="flex-1 overflow-y-auto p-3 space-y-2">
  61. {toolCalls.length === 0 && isLoading && (
  62. <div className="text-center text-gray-400 py-4">
  63. <p className="text-sm">AI 正在调用工具...</p>
  64. </div>
  65. )}
  66. {toolCalls.map((call, idx) => {
  67. const isItemExpanded = expandedItems.has(idx);
  68. return (
  69. <div key={idx} className="bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden">
  70. <button
  71. onClick={() => toggleItem(idx)}
  72. className="w-full flex items-center justify-between p-2 text-left"
  73. >
  74. <span className="font-medium text-purple-700 dark:text-purple-400 text-sm truncate flex-1">
  75. {call.tool}
  76. </span>
  77. <svg
  78. className={`w-4 h-4 text-gray-500 transition-transform flex-shrink-0 ml-2 ${isItemExpanded ? 'rotate-180' : ''}`}
  79. fill="none"
  80. stroke="currentColor"
  81. viewBox="0 0 24 24"
  82. >
  83. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
  84. </svg>
  85. </button>
  86. {isItemExpanded && (
  87. <div className="p-2 border-t dark:border-gray-700">
  88. <pre className="text-[11px] bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap break-all">
  89. {JSON.stringify(call.result, null, 2)}
  90. </pre>
  91. </div>
  92. )}
  93. </div>
  94. );
  95. })}
  96. {isLoading && toolCalls.length > 0 && (
  97. <div className="text-center text-gray-400 py-2">
  98. <p className="text-xs">处理中...</p>
  99. </div>
  100. )}
  101. </div>
  102. )}
  103. </div>
  104. );
  105. }
  106. // 桌面端布局
  107. return (
  108. <div className="w-80 bg-white dark:bg-gray-800 border-l dark:border-gray-700 overflow-y-auto">
  109. <div className="p-4 border-b dark:border-gray-700">
  110. <h3 className="font-semibold text-gray-800 dark:text-white">
  111. 工具调用
  112. </h3>
  113. </div>
  114. <div className="p-4 space-y-3">
  115. {toolCalls.length === 0 && isLoading && (
  116. <div className="text-center text-gray-400 py-8">
  117. <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
  118. <p>AI 正在思考...</p>
  119. </div>
  120. )}
  121. {toolCalls.map((call, idx) => (
  122. <div key={idx} className="tool-call">
  123. <div className="flex items-center justify-between mb-1">
  124. <span className="font-medium text-purple-700 dark:text-purple-400">{call.tool}</span>
  125. <span className="text-xs text-gray-500">#{idx + 1}</span>
  126. </div>
  127. <pre className="text-xs bg-gray-50 dark:bg-gray-900 p-2 rounded overflow-x-auto">
  128. {JSON.stringify(call.result, null, 2)}
  129. </pre>
  130. </div>
  131. ))}
  132. {isLoading && toolCalls.length > 0 && (
  133. <div className="text-center text-gray-400 py-4">
  134. <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto"></div>
  135. <p className="text-sm mt-2">处理中...</p>
  136. </div>
  137. )}
  138. </div>
  139. </div>
  140. );
  141. }