diff --git a/frontend-v2/app/page.tsx b/frontend-v2/app/page.tsx index 7b56728..931b11f 100644 --- a/frontend-v2/app/page.tsx +++ b/frontend-v2/app/page.tsx @@ -1,19 +1,21 @@ /** * 主页面 - 聊天界面 * - * 使用官方 useChatUI hook 集成 json-render 生成式 UI + * 使用 Vercel AI SDK useChat hook * 支持动态渲染 MCP 工具调用结果 * 支持组件交互事件(ActionProvider) + * 支持 MCP Token 自动传递 */ 'use client'; import React, { useCallback } from 'react'; -import { useChatUI, Renderer } from '@json-render/react'; +import { useChat } from 'ai/react'; import ChatInput from '@/components/ChatInput'; +import ChatMessage from '@/components/ChatMessage'; import Header from '@/components/Header'; import JsonRenderer from '@/components/JsonRenderer'; import { ActionProvider } from '@/lib/action-context'; -import { registry } from '@/lib/registry'; +import { McpTokenManager } from '@/lib/mcp-token-manager'; import type { ComponentSpec } from '@/lib/json-render-catalog'; // 静态欢迎组件 spec @@ -95,13 +97,43 @@ const WELCOME_SPEC: ComponentSpec = { }; export default function Home() { - const { messages, isStreaming, send, clear } = useChatUI({ + const mcpTokenManager = McpTokenManager.getInstance(); + + const { messages, input, handleInputChange, handleSubmit, isLoading, stop } = useChat({ api: '/api/chat', + headers: async () => { + const tokens = mcpTokenManager.getAllTokens(); + const enabledMcpList = mcpTokenManager.getEnabledMcpList(); + return { + 'X-MCP-Tokens': JSON.stringify(tokens), + 'X-MCP-Enabled': JSON.stringify(enabledMcpList), + }; + }, }); + // 兼容旧代码的 send 函数 + const send = async (text: string) => { + // 设置输入值并提交表单 + const formEvent = new Event('submit', { bubbles: true, cancelable: true }); + await handleSubmit(formEvent); + }; + const actionContext = useCallback(() => ({ - sendMessage: send, - }), [send]); + sendMessage: (text: string) => { + // 直接设置 input 值并提交 + const form = document.querySelector('form') as HTMLFormElement; + if (form) { + const textarea = form.querySelector('textarea') as HTMLTextAreaElement; + if (textarea) { + textarea.value = text; + const event = new Event('input', { bubbles: true }); + textarea.dispatchEvent(event); + } + const submitEvent = new Event('submit', { bubbles: true, cancelable: true }); + form.dispatchEvent(submitEvent); + } + }, + }), []); return (
@@ -113,37 +145,34 @@ export default function Home() {
- { if (action === "sendMessage") { send(params?.message || ""); } }} /> + { + if (action === "sendMessage") { + actionContext().sendMessage(params?.message || ""); + } + }} />
) : ( - -
- {messages.map((msg) => ( -
- {msg.role === 'user' ? ( -
-
- {msg.text} -
-
- ) : ( -
- {msg.text &&
{msg.text}
} - {msg.spec && } -
- )} -
- ))} - {isStreaming &&
思考中...
} -
-
+
+ {messages.map((msg, idx) => ( + + ))} +
)}
-
- -
+ diff --git a/frontend-v2/components/ChatInput.tsx b/frontend-v2/components/ChatInput.tsx index 5086ae1..63f5f58 100644 --- a/frontend-v2/components/ChatInput.tsx +++ b/frontend-v2/components/ChatInput.tsx @@ -1,33 +1,69 @@ /** * 聊天输入组件 - 移动端优化 + * + * 支持两种模式: + * 1. 受控模式:传入 value, onChange, onSubmit + * 2. 非受控模式:传入 onSend (兼容旧代码) */ 'use client'; import { useState, FormEvent, KeyboardEvent, useRef, useEffect } from 'react'; interface ChatInputProps { - onSend: (message: string) => void; - isLoading: boolean; - onAbort: () => void; + // 受控模式 props (useChat) + value?: string; + onChange?: (e: React.ChangeEvent) => void; + onSubmit?: (e: React.FormEvent) => void; + isLoading?: boolean; + onStop?: () => void; + + // 非受控模式 props (兼容旧代码) + onSend?: (message: string) => void; + onAbort?: () => void; } -export default function ChatInput({ onSend, isLoading, onAbort }: ChatInputProps) { - const [input, setInput] = useState(''); +export default function ChatInput({ + // 受控模式 props + value, onChange, onSubmit, isLoading, onStop, + // 非受控模式 props + onSend, onAbort +}: ChatInputProps) { + // 检测是否为受控模式 + const isControlled = value !== undefined && onChange !== undefined; + + const [localInput, setLocalInput] = useState(''); const textareaRef = useRef(null); + // 使用受控或非受控的值 + const inputValue = isControlled ? value : localInput; + const loading = isLoading ?? false; + const abortHandler = onStop ?? onAbort; + // 自动调整 textarea 高度 useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`; } - }, [input]); + }, [inputValue]); + + const handleChange = (e: React.ChangeEvent) => { + if (isControlled) { + onChange(e); + } else { + setLocalInput(e.target.value); + } + }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); - if (input.trim() && !isLoading) { - onSend(input.trim()); - setInput(''); + if (onSubmit) { + // 受控模式:直接调用 onSubmit + onSubmit(e); + } else if (onSend && inputValue.trim() && !loading) { + // 非受控模式:调用 onSend 并清空输入 + onSend(inputValue.trim()); + setLocalInput(''); // 重置 textarea 高度 if (textareaRef.current) { textareaRef.current.style.height = 'auto'; @@ -47,28 +83,28 @@ export default function ChatInput({ onSend, isLoading, onAbort }: ChatInputProps