2
0

index.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>AI MCP Web UI</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <style>
  9. .chat-container {
  10. height: calc(100vh - 200px);
  11. }
  12. .message-user {
  13. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  14. }
  15. .message-assistant {
  16. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  17. }
  18. .typing-indicator span {
  19. animation: bounce 1.4s infinite ease-in-out both;
  20. }
  21. .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
  22. .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
  23. @keyframes bounce {
  24. 0%, 80%, 100% { transform: scale(0); }
  25. 40% { transform: scale(1); }
  26. }
  27. /* 工具调用面板样式 */
  28. .tool-call-panel {
  29. background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  30. border-radius: 8px;
  31. margin: 12px 0;
  32. overflow: hidden;
  33. border: 1px solid #3a3a5a;
  34. }
  35. .tool-panel-header {
  36. background: rgba(255, 255, 255, 0.05);
  37. padding: 10px 14px;
  38. cursor: pointer;
  39. display: flex;
  40. align-items: center;
  41. justify-content: space-between;
  42. transition: background 0.2s;
  43. }
  44. .tool-panel-header:hover {
  45. background: rgba(255, 255, 255, 0.08);
  46. }
  47. .tool-panel-title {
  48. display: flex;
  49. align-items: center;
  50. gap: 8px;
  51. color: #e0e0e0;
  52. font-size: 13px;
  53. font-weight: 500;
  54. }
  55. .tool-panel-content {
  56. padding: 0;
  57. max-height: 0;
  58. overflow: hidden;
  59. transition: max-height 0.3s ease-out, padding 0.3s ease-out;
  60. }
  61. .tool-panel-content.expanded {
  62. max-height: 800px;
  63. padding: 12px;
  64. overflow-y: auto;
  65. }
  66. .tool-panel-content::-webkit-scrollbar {
  67. width: 6px;
  68. }
  69. .tool-panel-content::-webkit-scrollbar-track {
  70. background: rgba(255, 255, 255, 0.05);
  71. }
  72. .tool-panel-content::-webkit-scrollbar-thumb {
  73. background: rgba(255, 255, 255, 0.2);
  74. border-radius: 3px;
  75. }
  76. /* 工具调用项样式 */
  77. .tool-call-item {
  78. background: rgba(255, 255, 255, 0.03);
  79. border-radius: 6px;
  80. padding: 10px 12px;
  81. margin-bottom: 8px;
  82. border-left: 3px solid #555;
  83. transition: all 0.2s;
  84. }
  85. .tool-call-item:last-child {
  86. margin-bottom: 0;
  87. }
  88. .tool-call-item.pending {
  89. border-left-color: #f59e0b;
  90. animation: pulse-border 1.5s infinite;
  91. }
  92. .tool-call-item.success {
  93. border-left-color: #10b981;
  94. }
  95. .tool-call-item.error {
  96. border-left-color: #ef4444;
  97. }
  98. @keyframes pulse-border {
  99. 0%, 100% { border-left-color: #f59e0b; }
  100. 50% { border-left-color: #fbbf24; }
  101. }
  102. .tool-call-header {
  103. display: flex;
  104. align-items: center;
  105. justify-content: space-between;
  106. margin-bottom: 6px;
  107. }
  108. .tool-name {
  109. color: #e0e0e0;
  110. font-size: 13px;
  111. font-weight: 500;
  112. display: flex;
  113. align-items: center;
  114. gap: 6px;
  115. }
  116. .tool-status {
  117. font-size: 11px;
  118. padding: 2px 8px;
  119. border-radius: 10px;
  120. font-weight: 500;
  121. }
  122. .tool-status.pending {
  123. background: rgba(245, 158, 11, 0.2);
  124. color: #fbbf24;
  125. }
  126. .tool-status.success {
  127. background: rgba(16, 185, 129, 0.2);
  128. color: #34d399;
  129. }
  130. .tool-status.error {
  131. background: rgba(239, 68, 68, 0.2);
  132. color: #f87171;
  133. }
  134. .tool-params {
  135. background: rgba(0, 0, 0, 0.3);
  136. border-radius: 4px;
  137. padding: 8px;
  138. margin-top: 6px;
  139. font-size: 11px;
  140. color: #9ca3af;
  141. overflow-x: auto;
  142. }
  143. .tool-params::-webkit-scrollbar {
  144. height: 4px;
  145. }
  146. .tool-params::-webkit-scrollbar-thumb {
  147. background: rgba(255, 255, 255, 0.2);
  148. border-radius: 2px;
  149. }
  150. .tool-result {
  151. background: rgba(16, 185, 129, 0.1);
  152. border-radius: 4px;
  153. padding: 8px;
  154. margin-top: 6px;
  155. font-size: 11px;
  156. color: #6ee7b7;
  157. }
  158. .tool-result.error {
  159. background: rgba(239, 68, 68, 0.1);
  160. color: #fca5a5;
  161. }
  162. .tool-result-content {
  163. max-height: 100px;
  164. overflow-y: auto;
  165. word-break: break-all;
  166. }
  167. .tool-result-content::-webkit-scrollbar {
  168. width: 4px;
  169. }
  170. .tool-result-content::-webkit-scrollbar-thumb {
  171. background: rgba(255, 255, 255, 0.2);
  172. border-radius: 2px;
  173. }
  174. .expand-icon {
  175. transition: transform 0.3s;
  176. }
  177. .expand-icon.expanded {
  178. transform: rotate(180deg);
  179. }
  180. /* 加载动画 */
  181. .spinner {
  182. width: 14px;
  183. height: 14px;
  184. border: 2px solid rgba(251, 191, 36, 0.3);
  185. border-top-color: #fbbf24;
  186. border-radius: 50%;
  187. animation: spin 0.8s linear infinite;
  188. }
  189. @keyframes spin {
  190. to { transform: rotate(360deg); }
  191. }
  192. /* 状态指示器 */
  193. .process-indicator {
  194. display: flex;
  195. align-items: center;
  196. gap: 6px;
  197. padding: 6px 10px;
  198. background: rgba(245, 158, 11, 0.1);
  199. border-radius: 6px;
  200. font-size: 12px;
  201. color: #fbbf24;
  202. }
  203. .process-indicator.success {
  204. background: rgba(16, 185, 129, 0.1);
  205. color: #34d399;
  206. }
  207. .process-indicator .spinner {
  208. width: 12px;
  209. height: 12px;
  210. }
  211. pre {
  212. background: #1e1e1e;
  213. color: #d4d4d4;
  214. padding: 1rem;
  215. border-radius: 0.5rem;
  216. overflow-x: auto;
  217. }
  218. code {
  219. font-family: 'Courier New', monospace;
  220. }
  221. </style>
  222. </head>
  223. <body class="bg-gray-100 min-h-screen">
  224. <div class="container mx-auto px-4 py-6 max-w-4xl">
  225. <!-- Header -->
  226. <header class="bg-white rounded-lg shadow-md p-4 mb-4">
  227. <div class="flex items-center justify-between">
  228. <div class="flex items-center space-x-3">
  229. <div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-lg flex items-center justify-center">
  230. <span class="text-white font-bold">AI</span>
  231. </div>
  232. <div>
  233. <h1 class="text-xl font-bold text-gray-800">AI MCP Web UI</h1>
  234. <p class="text-sm text-gray-500">通用 MCP 服务器 Web 界面</p>
  235. </div>
  236. </div>
  237. <div id="status-indicator" class="flex items-center space-x-2">
  238. <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
  239. <span class="text-sm text-gray-600">已连接</span>
  240. </div>
  241. </div>
  242. </header>
  243. <!-- Chat Container -->
  244. <div class="bg-white rounded-lg shadow-md">
  245. <!-- Messages -->
  246. <div id="chat-messages" class="chat-container overflow-y-auto p-4 space-y-4">
  247. <!-- Welcome Message -->
  248. <div class="flex justify-start">
  249. <div class="message-assistant rounded-lg p-4 max-w-[80%] shadow-sm">
  250. <p class="text-gray-800">你好!我是 AI MCP Web UI 助手。我可以帮助你:</p>
  251. <ul class="mt-2 text-gray-700 list-disc list-inside">
  252. <li>与 Claude AI 进行对话</li>
  253. <li>调用 MCP 服务器工具</li>
  254. <li>自动化各种任务</li>
  255. </ul>
  256. <p class="mt-2 text-gray-600 text-sm">请问有什么可以帮助你的?</p>
  257. </div>
  258. </div>
  259. </div>
  260. <!-- Input Area -->
  261. <div class="border-t border-gray-200 p-4">
  262. <form id="chat-form" class="flex space-x-3">
  263. <input
  264. type="text"
  265. id="user-input"
  266. placeholder="输入你的消息..."
  267. class="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
  268. autocomplete="off"
  269. >
  270. <button
  271. type="submit"
  272. id="send-button"
  273. class="px-6 py-3 bg-gradient-to-r from-purple-500 to-indigo-600 text-white rounded-lg hover:from-purple-600 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
  274. >
  275. 发送
  276. </button>
  277. </form>
  278. </div>
  279. </div>
  280. <!-- MCP Servers Status -->
  281. <div class="mt-4 bg-white rounded-lg shadow-md p-4">
  282. <h2 class="text-lg font-semibold text-gray-800 mb-3">MCP 服务器状态</h2>
  283. <div id="mcp-servers" class="grid grid-cols-1 sm:grid-cols-3 gap-3">
  284. <!-- MCP server cards will be inserted here -->
  285. </div>
  286. </div>
  287. </div>
  288. <script>
  289. // State
  290. let conversationHistory = [];
  291. let isTyping = false;
  292. // DOM Elements
  293. const chatMessages = document.getElementById('chat-messages');
  294. const chatForm = document.getElementById('chat-form');
  295. const userInput = document.getElementById('user-input');
  296. const sendButton = document.getElementById('send-button');
  297. const mcpServers = document.getElementById('mcp-servers');
  298. const statusIndicator = document.getElementById('status-indicator');
  299. // API Base URL
  300. const API_BASE = window.location.origin;
  301. // Format message content (handle code blocks, etc.)
  302. function formatMessage(content) {
  303. // Simple markdown-like formatting
  304. let formatted = content
  305. .replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
  306. .replace(/`([^`]+)`/g, '<code class="bg-gray-200 px-1 rounded">$1</code>')
  307. .replace(/\n/g, '<br>');
  308. return formatted;
  309. }
  310. // Add message to chat
  311. function addMessage(role, content) {
  312. const messageDiv = document.createElement('div');
  313. messageDiv.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
  314. const bubbleClass = role === 'user' ? 'message-user text-white' : 'message-assistant text-gray-800';
  315. messageDiv.innerHTML = `
  316. <div class="${bubbleClass} rounded-lg p-4 max-w-[80%] shadow-sm">
  317. <div>${formatMessage(content)}</div>
  318. </div>
  319. `;
  320. chatMessages.appendChild(messageDiv);
  321. chatMessages.scrollTop = chatMessages.scrollHeight;
  322. }
  323. // Add typing indicator
  324. function addTypingIndicator() {
  325. const typingDiv = document.createElement('div');
  326. typingDiv.id = 'typing-indicator';
  327. typingDiv.className = 'flex justify-start';
  328. typingDiv.innerHTML = `
  329. <div class="message-assistant rounded-lg p-4 shadow-sm">
  330. <div class="typing-indicator flex space-x-1">
  331. <span class="w-2 h-2 bg-gray-500 rounded-full"></span>
  332. <span class="w-2 h-2 bg-gray-500 rounded-full"></span>
  333. <span class="w-2 h-2 bg-gray-500 rounded-full"></span>
  334. </div>
  335. </div>
  336. `;
  337. chatMessages.appendChild(typingDiv);
  338. chatMessages.scrollTop = chatMessages.scrollHeight;
  339. }
  340. // Remove typing indicator
  341. function removeTypingIndicator() {
  342. const indicator = document.getElementById('typing-indicator');
  343. if (indicator) indicator.remove();
  344. }
  345. // 创建工具调用面板
  346. function createToolCallPanel() {
  347. const panel = document.createElement('div');
  348. panel.className = 'tool-call-panel';
  349. panel.innerHTML = `
  350. <div class="tool-panel-header" onclick="toggleToolPanel(this)">
  351. <div class="tool-panel-title">
  352. <span class="expand-icon">▼</span>
  353. <span>🔧 工具调用过程</span>
  354. <span class="tool-count">(0)</span>
  355. </div>
  356. <div class="process-indicator">
  357. <div class="spinner"></div>
  358. <span>处理中...</span>
  359. </div>
  360. </div>
  361. <div class="tool-panel-content expanded">
  362. <div class="tool-calls-container"></div>
  363. </div>
  364. `;
  365. return panel;
  366. }
  367. // 切换工具面板展开/收起
  368. function toggleToolPanel(header) {
  369. const content = header.nextElementSibling;
  370. const icon = header.querySelector('.expand-icon');
  371. content.classList.toggle('expanded');
  372. icon.classList.toggle('expanded');
  373. }
  374. // 添加工具调用项
  375. function addToolCallItem(container, toolName, params) {
  376. const item = document.createElement('div');
  377. item.className = 'tool-call-item pending';
  378. item.id = `tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  379. const paramsStr = params ? JSON.stringify(params, null, 2) : '{}';
  380. item.innerHTML = `
  381. <div class="tool-call-header">
  382. <div class="tool-name">
  383. <span>🔧</span>
  384. <span>${escapeHtml(toolName)}</span>
  385. </div>
  386. <div class="tool-status pending">
  387. <div class="spinner"></div>
  388. <span>执行中</span>
  389. </div>
  390. </div>
  391. ${params ? `<div class="tool-params"><strong>参数:</strong> <pre style="margin:0;padding:0;background:transparent;font-size:10px;">${escapeHtml(paramsStr)}</pre></div>` : ''}
  392. `;
  393. container.appendChild(item);
  394. return item.id;
  395. }
  396. // 更新工具调用状态
  397. function updateToolCallStatus(itemId, status, result = null) {
  398. const item = document.getElementById(itemId);
  399. if (!item) return;
  400. const statusEl = item.querySelector('.tool-status');
  401. item.classList.remove('pending', 'success', 'error');
  402. if (status === 'success') {
  403. item.classList.add('success');
  404. statusEl.className = 'tool-status success';
  405. statusEl.innerHTML = '✅ 成功';
  406. if (result) {
  407. const resultDiv = document.createElement('div');
  408. resultDiv.className = 'tool-result';
  409. const resultText = typeof result === 'string' ? result : JSON.stringify(result);
  410. resultDiv.innerHTML = `<strong>结果:</strong><div class="tool-result-content">${escapeHtml(resultText.substring(0, 500))}${resultText.length > 500 ? '...' : ''}</div>`;
  411. item.appendChild(resultDiv);
  412. }
  413. } else if (status === 'error') {
  414. item.classList.add('error');
  415. statusEl.className = 'tool-status error';
  416. statusEl.innerHTML = '❌ 失败';
  417. const errorDiv = document.createElement('div');
  418. errorDiv.className = 'tool-result error';
  419. errorDiv.innerHTML = `<strong>错误:</strong> ${escapeHtml(result || '未知错误')}`;
  420. item.appendChild(errorDiv);
  421. }
  422. }
  423. // 更新工具面板状态
  424. function updateToolPanelStatus(panel, status, totalCalls = 0) {
  425. const indicator = panel.querySelector('.process-indicator');
  426. const countEl = panel.querySelector('.tool-count');
  427. countEl.textContent = `(${totalCalls})`;
  428. if (status === 'complete') {
  429. indicator.className = 'process-indicator success';
  430. indicator.innerHTML = `<span>✅ 完成 - ${totalCalls} 个工具调用</span>`;
  431. } else if (status === 'error') {
  432. indicator.className = 'process-indicator';
  433. indicator.style.background = 'rgba(239, 68, 68, 0.1)';
  434. indicator.style.color = '#f87171';
  435. indicator.innerHTML = `<span>❌ 部分失败</span>`;
  436. }
  437. }
  438. // HTML 转义
  439. function escapeHtml(text) {
  440. const div = document.createElement('div');
  441. div.textContent = text;
  442. return div.innerHTML;
  443. }
  444. function scrollToBottom() {
  445. chatMessages.scrollTop = chatMessages.scrollHeight;
  446. }
  447. // ========== 流式聊天功能 ==========
  448. async function chatStream(message, onEvent) {
  449. const events = [];
  450. try {
  451. const response = await fetch('/api/chat/stream', {
  452. method: 'POST',
  453. headers: { 'Content-Type': 'application/json' },
  454. body: JSON.stringify({ message, history: conversationHistory })
  455. });
  456. if (!response.ok) {
  457. throw new Error(`HTTP error! status: ${response.status}`);
  458. }
  459. const reader = response.body.getReader();
  460. const decoder = new TextDecoder();
  461. let buffer = '';
  462. while (true) {
  463. const { done, value } = await reader.read();
  464. if (done) break;
  465. buffer += decoder.decode(value, { stream: true });
  466. // 处理 SSE 格式: "event: xxx\ndata: {...}\n\n"
  467. const lines = buffer.split('\n');
  468. buffer = lines.pop() || ''; // 保留未完成的行
  469. for (let i = 0; i < lines.length; i++) {
  470. const line = lines[i].trim();
  471. if (!line) continue;
  472. if (line.startsWith('event:')) {
  473. const eventType = line.substring(6).trim();
  474. events.push({ type: eventType, data: null });
  475. } else if (line.startsWith('data:')) {
  476. const data = line.substring(5).trim();
  477. if (events.length > 0) {
  478. events[events.length - 1].data = data;
  479. }
  480. }
  481. // 处理完整的事件
  482. while (events.length > 0 && events[0].data) {
  483. const event = events.shift();
  484. try {
  485. const data = JSON.parse(event.data);
  486. onEvent(event.type, data);
  487. } catch (e) {
  488. console.error('Parse error:', e, event.data);
  489. }
  490. }
  491. }
  492. }
  493. } catch (error) {
  494. onEvent('error', { error: error.message });
  495. }
  496. }
  497. async function sendMessageStream(message) {
  498. if (isTyping) return;
  499. isTyping = true;
  500. sendButton.disabled = true;
  501. // 添加用户消息
  502. addMessage('user', message);
  503. conversationHistory.push({ role: 'user', content: message });
  504. // 创建助手消息容器
  505. const assistantMsgDiv = document.createElement('div');
  506. assistantMsgDiv.className = 'flex justify-start';
  507. assistantMsgDiv.innerHTML = `
  508. <div class="message-assistant rounded-lg p-4 max-w-[80%] shadow-sm" style="max-width: 90%;">
  509. <div class="response-content">
  510. <div class="text" id="currentResponse"></div>
  511. </div>
  512. <div id="toolCallContainer"></div>
  513. </div>
  514. `;
  515. chatMessages.appendChild(assistantMsgDiv);
  516. const responseDiv = assistantMsgDiv.querySelector('#currentResponse');
  517. const toolContainer = assistantMsgDiv.querySelector('#toolCallContainer');
  518. let currentText = '';
  519. let toolCallsCount = 0;
  520. let toolCallItems = {};
  521. let finalResponse = '';
  522. let toolCallPanel = null;
  523. let toolCallsContainer = null;
  524. await chatStream(message, (eventType, data) => {
  525. switch (eventType) {
  526. case 'start':
  527. responseDiv.innerHTML = '<span style="color: #888; font-style: italic;">🤔 AI 正在思考...</span>';
  528. break;
  529. case 'token':
  530. // 移除思考提示
  531. if (currentText === '') {
  532. responseDiv.innerHTML = '';
  533. }
  534. currentText += data.text || '';
  535. responseDiv.innerHTML = formatMessage(currentText);
  536. scrollToBottom();
  537. break;
  538. case 'tool_call':
  539. // 首次工具调用时创建面板
  540. if (!toolCallPanel) {
  541. toolCallPanel = createToolCallPanel();
  542. toolContainer.appendChild(toolCallPanel);
  543. toolCallsContainer = toolCallPanel.querySelector('.tool-calls-container');
  544. // 清空思考提示
  545. if (currentText === '' || currentText.includes('正在思考')) {
  546. responseDiv.innerHTML = '<span style="color: #888; font-style: italic;">🔧 正在调用工具...</span>';
  547. }
  548. }
  549. // 添加工具调用项
  550. const toolName = data.tool || 'unknown';
  551. const params = data.args || data.params || null;
  552. const itemId = addToolCallItem(toolCallsContainer, toolName, params);
  553. toolCallItems[data.tool_id || toolName] = itemId;
  554. toolCallsCount++;
  555. updateToolPanelStatus(toolCallPanel, 'running', toolCallsCount);
  556. scrollToBottom();
  557. break;
  558. case 'tools_start':
  559. // 批量工具开始
  560. if (!toolCallPanel) {
  561. toolCallPanel = createToolCallPanel();
  562. toolContainer.appendChild(toolCallPanel);
  563. toolCallsContainer = toolCallPanel.querySelector('.tool-calls-container');
  564. }
  565. if (currentText === '' || currentText.includes('正在思考')) {
  566. responseDiv.innerHTML = '<span style="color: #888; font-style: italic;">🔧 正在调用工具...</span>';
  567. }
  568. scrollToBottom();
  569. break;
  570. case 'tool_done':
  571. // 工具执行完成
  572. const toolId = data.tool_id || data.tool;
  573. const itemId2 = toolCallItems[toolId];
  574. if (itemId2) {
  575. const result = data.result || '';
  576. updateToolCallStatus(itemId2, 'success', result);
  577. }
  578. scrollToBottom();
  579. break;
  580. case 'tool_error':
  581. // 工具执行错误
  582. const errorItemId = toolCallItems[data.tool_id || data.tool];
  583. if (errorItemId) {
  584. updateToolCallStatus(errorItemId, 'error', data.error);
  585. }
  586. if (toolCallPanel) {
  587. updateToolPanelStatus(toolCallPanel, 'error', toolCallsCount);
  588. }
  589. scrollToBottom();
  590. break;
  591. case 'complete':
  592. // 完成
  593. if (toolCallPanel) {
  594. updateToolPanelStatus(toolCallPanel, 'complete', toolCallsCount);
  595. }
  596. // 清空临时提示并显示最终响应
  597. if (data.response || currentText) {
  598. finalResponse = data.response || currentText;
  599. responseDiv.innerHTML = formatMessage(finalResponse);
  600. }
  601. conversationHistory.push({ role: 'assistant', content: finalResponse });
  602. scrollToBottom();
  603. break;
  604. case 'error':
  605. responseDiv.innerHTML = `<span style="color: #ef4444;">❌ 错误: ${escapeHtml(data.error || '未知错误')}</span>`;
  606. if (toolCallPanel) {
  607. updateToolPanelStatus(toolCallPanel, 'error', toolCallsCount);
  608. }
  609. scrollToBottom();
  610. break;
  611. }
  612. });
  613. // 重置状态
  614. isTyping = false;
  615. sendButton.disabled = false;
  616. userInput.focus();
  617. }
  618. // Load MCP servers
  619. async function loadMCPServers() {
  620. try {
  621. const response = await fetch(`${API_BASE}/api/mcp/servers`);
  622. const data = await response.json();
  623. mcpServers.innerHTML = data.servers.map(server => `
  624. <div class="flex items-center space-x-2 p-3 rounded-lg ${server.enabled ? 'bg-green-50' : 'bg-gray-50'}">
  625. <span class="w-2 h-2 ${server.enabled ? 'bg-green-500' : 'bg-gray-400'} rounded-full"></span>
  626. <div>
  627. <p class="font-medium text-gray-800">${server.name}</p>
  628. <p class="text-xs text-gray-500 truncate">${server.url}</p>
  629. <p class="text-xs text-purple-600">${server.auth_type === 'jwt' ? '🔒 JWT 认证' : '🔓 无需认证'}</p>
  630. </div>
  631. </div>
  632. `).join('');
  633. } catch (error) {
  634. console.error('Failed to load MCP servers:', error);
  635. }
  636. }
  637. // Check health
  638. async function checkHealth() {
  639. try {
  640. const response = await fetch(`${API_BASE}/api/health`);
  641. if (response.ok) {
  642. statusIndicator.innerHTML = `
  643. <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
  644. <span class="text-sm text-gray-600">已连接</span>
  645. `;
  646. } else {
  647. throw new Error('Health check failed');
  648. }
  649. } catch (error) {
  650. statusIndicator.innerHTML = `
  651. <span class="w-2 h-2 bg-red-500 rounded-full"></span>
  652. <span class="text-sm text-gray-600">连接失败</span>
  653. `;
  654. }
  655. }
  656. // Event listeners
  657. chatForm.addEventListener('submit', (e) => {
  658. e.preventDefault();
  659. const message = userInput.value.trim();
  660. if (message) {
  661. sendMessageStream(message);
  662. userInput.value = '';
  663. }
  664. });
  665. // Initialize
  666. loadMCPServers();
  667. checkHealth();
  668. userInput.focus();
  669. // Make toggle function global
  670. window.toggleToolPanel = toggleToolPanel;
  671. </script>
  672. </body>
  673. </html>