page.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /**
  2. * 主页面 - 聊天界面
  3. *
  4. * 集成 json-render 生成式 UI
  5. * 支持动态渲染 MCP 工具调用结果
  6. * 支持组件交互事件(ActionProvider)
  7. *
  8. * 更新:ChatMessage 现在支持 markdown 混排渲染,
  9. * JSON 代码块会就地渲染为 json-render 组件
  10. */
  11. 'use client';
  12. import React, { useState, useCallback, useRef } from 'react';
  13. import { useChat } from '@/lib/hooks';
  14. import ChatInput from '@/components/ChatInput';
  15. import ChatMessage from '@/components/ChatMessage';
  16. import Header from '@/components/Header';
  17. import JsonRenderer from '@/components/JsonRenderer';
  18. import { ActionProvider } from '@/lib/action-context';
  19. import type { ComponentSpec } from '@/lib/json-render-catalog';
  20. // 消息类型
  21. interface Message {
  22. role: 'user' | 'assistant';
  23. content: string;
  24. }
  25. // 静态欢迎组件 spec
  26. const WELCOME_SPEC: ComponentSpec = {
  27. type: 'card',
  28. title: '欢迎使用 AI MCP Web UI',
  29. className: 'bg-gradient-to-br from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border-blue-200 dark:border-blue-800',
  30. children: [
  31. {
  32. type: 'text',
  33. content: '这是一个通用的 AI 助手界面,可以连接多个 MCP 服务器,提供强大的工具调用能力。',
  34. variant: 'body',
  35. className: 'mb-4'
  36. },
  37. {
  38. type: 'heading',
  39. level: 'h3',
  40. text: '可用的 MCP 服务器',
  41. className: 'mb-2 mt-4 text-gray-800 dark:text-gray-200'
  42. },
  43. {
  44. type: 'stack',
  45. direction: 'column',
  46. spacing: 1,
  47. className: 'mb-4',
  48. children: [
  49. {
  50. type: 'text',
  51. content: '🌐 Novel Translator MCP - 文本翻译工具',
  52. variant: 'body',
  53. className: 'text-sm text-gray-600 dark:text-gray-400'
  54. },
  55. {
  56. type: 'text',
  57. content: '📚 Novel Platform User MCP - 小说阅读功能',
  58. variant: 'body',
  59. className: 'text-sm text-gray-600 dark:text-gray-400'
  60. },
  61. {
  62. type: 'text',
  63. content: '⚙️ Novel Platform Admin MCP - 平台管理功能',
  64. variant: 'body',
  65. className: 'text-sm text-gray-600 dark:text-gray-400'
  66. }
  67. ]
  68. },
  69. {
  70. type: 'heading',
  71. level: 'h3',
  72. text: '快速开始',
  73. className: 'mb-3 mt-6 text-gray-800 dark:text-gray-200'
  74. },
  75. {
  76. type: 'suggestion-buttons',
  77. suggestions: [
  78. {
  79. label: '翻译一段文本',
  80. message: '请帮我把以下英文翻译成中文:Hello, how are you?',
  81. icon: '🌐'
  82. },
  83. {
  84. label: '查看小说列表',
  85. message: '请帮我获取小说列表',
  86. icon: '📚'
  87. },
  88. {
  89. label: '获取平台统计',
  90. message: '请帮我获取平台统计数据',
  91. icon: '📊'
  92. },
  93. {
  94. label: '查看 MCP 状态',
  95. message: '请帮我检查所有 MCP 服务器的连接状态',
  96. icon: '🔍'
  97. }
  98. ]
  99. }
  100. ]
  101. };
  102. export default function Home() {
  103. const [history, setHistory] = useState<Message[]>([]);
  104. const { sendMessage, isLoading, response, error, abort, specs } = useChat();
  105. const pendingMessageRef = useRef<string | null>(null);
  106. // 当加载完成时,将完整的助手消息添加到历史记录
  107. React.useEffect(() => {
  108. console.log('[useEffect] Triggered:', {
  109. isLoading,
  110. pendingMessage: pendingMessageRef.current,
  111. responseLength: response?.length || 0,
  112. responsePreview: response?.substring(0, 100),
  113. hasError: !!error,
  114. });
  115. // 添加新消息(加载完成且有待处理消息)
  116. if (!isLoading && pendingMessageRef.current) {
  117. const assistantMessage: Message = {
  118. role: 'assistant',
  119. content: response || (error ? `⚠️ ${error}` : ''),
  120. };
  121. console.log('[useEffect] Adding assistant message to history:', assistantMessage);
  122. setHistory((prev) => [...prev, assistantMessage]);
  123. pendingMessageRef.current = null;
  124. }
  125. }, [isLoading, response, error]);
  126. const handleSend = async (message: string) => {
  127. // 添加用户消息到历史记录
  128. setHistory((prev) => [...prev, { role: 'user', content: message }]);
  129. pendingMessageRef.current = message;
  130. // 开始流式对话(不等待完成,由 useEffect 处理完成后的消息)
  131. sendMessage(message, history);
  132. };
  133. const handleAbort = useCallback(() => {
  134. abort();
  135. // 将当前的部分响应添加到历史记录
  136. if (pendingMessageRef.current && response) {
  137. setHistory((prev) => [
  138. ...prev,
  139. {
  140. role: 'assistant',
  141. content: response,
  142. },
  143. ]);
  144. pendingMessageRef.current = null;
  145. }
  146. }, [abort, response]);
  147. // 创建 action context 用于组件交互
  148. const actionContext = useCallback(() => ({
  149. sendMessage: handleSend,
  150. }), [handleSend]);
  151. return (
  152. <div className="flex flex-col h-screen-mobile min-h-screen bg-gray-50 dark:bg-gray-900">
  153. <Header />
  154. <main className="flex-1 overflow-hidden flex">
  155. {/* 聊天区域 */}
  156. <div className="flex-1 flex flex-col min-w-0">
  157. <div className="flex-1 overflow-y-auto p-3 md:p-4">
  158. {history.length === 0 ? (
  159. // 空状态:显示静态欢迎组件
  160. <ActionProvider actions={actionContext()}>
  161. <div className="flex items-start justify-center h-full pt-4 md:pt-8">
  162. <div className="w-full max-w-2xl px-2">
  163. <JsonRenderer spec={WELCOME_SPEC} />
  164. </div>
  165. </div>
  166. </ActionProvider>
  167. ) : (
  168. // 使用 ActionProvider 包装整个消息区域,使 ChatMessage 内部的 JsonRenderer 可以访问 ActionContext
  169. <ActionProvider actions={actionContext()}>
  170. <div className="space-y-3 md:space-y-4 messages-with-fixed-input">
  171. {history.map((msg, idx) => {
  172. console.log(`[Render] Message ${idx}:`, msg);
  173. return (
  174. <ChatMessage key={idx} role={msg.role} content={msg.content} />
  175. );
  176. })}
  177. {isLoading && response && (
  178. <ChatMessage role="assistant" content={response} specs={specs} isLoading />
  179. )}
  180. {error && (
  181. <div className="bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-300 px-3 py-2 md:px-4 md:py-3 rounded-lg mx-1">
  182. <p className="text-sm">{error}</p>
  183. </div>
  184. )}
  185. </div>
  186. </ActionProvider>
  187. )}
  188. </div>
  189. <div className="fixed-bottom-input md:relative border-t dark:border-gray-700 p-2 md:p-4 bg-white dark:bg-gray-800">
  190. <ChatInput onSend={handleSend} isLoading={isLoading} onAbort={handleAbort} />
  191. </div>
  192. </div>
  193. {/* 工具调用面板 - 仅在桌面端显示侧边栏 */}
  194. {/* 暂时隐藏,因为工具调用结果现在在 ChatMessage 中渲染 */}
  195. {/* {isLoading && (
  196. <ToolCallPanel toolCalls={[]} isLoading={isLoading} />
  197. )} */}
  198. </main>
  199. </div>
  200. );
  201. }