From 194282e0290d4f3fcb13b399c22e1b16626c8c67 Mon Sep 17 00:00:00 2001 From: adrian Date: Wed, 30 Apr 2025 00:52:29 +0800 Subject: [PATCH] add shadcn ui --- frontend/app/chat/page.tsx | 254 ++++++-- frontend/app/layout.tsx | 2 +- frontend/components.json | 21 + frontend/components/ui/button.tsx | 59 ++ frontend/components/ui/dialog.tsx | 135 +++++ frontend/components/ui/form.tsx | 167 ++++++ frontend/components/ui/input.tsx | 21 + frontend/components/ui/label.tsx | 24 + frontend/components/ui/select.tsx | 185 ++++++ frontend/components/ui/slider.tsx | 63 ++ frontend/components/ui/sonner.tsx | 25 + frontend/components/ui/textarea.tsx | 18 + frontend/lib/utils.ts | 6 + frontend/package.json | 16 +- frontend/pnpm-lock.yaml | 875 ++++++++++++++++++++++++++-- frontend/styles/globals.css | 217 +++++++ 16 files changed, 1989 insertions(+), 99 deletions(-) create mode 100644 frontend/components.json create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/form.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/slider.tsx create mode 100644 frontend/components/ui/sonner.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/lib/utils.ts diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index 3d7eb89..7c0be71 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -1,11 +1,11 @@ -// File: frontend/app/chat/page.tsx (重构布局) -// Description: AI 聊天界面组件,包含左侧助手面板、中间聊天区、右侧会话面板 +// File: frontend/app/chat/page.tsx (更新会话逻辑) +// Description: AI 聊天界面,实现助手关联会话、临时新对话和发送时创建会话 'use client'; -import React, { useState, useRef, useEffect } from 'react'; -import { SendHorizontal, Loader2, PanelRightOpen, PanelRightClose, UserPlus, Settings2 } from 'lucide-react'; // 添加图标 -import { sendChatMessage } from '@/lib/api'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { SendHorizontal, Loader2, PanelRightOpen, PanelRightClose, UserPlus, Settings2 } from 'lucide-react'; +import { sendChatMessage } from '@/lib/api'; // 假设 sendChatMessage 只需 message // --- 数据接口定义 --- interface Message { @@ -16,98 +16,188 @@ interface Message { } interface ChatSession { - id: string; + id: string; // 唯一 ID,例如 'session-uuid-123' 或 'temp-new-chat' title: string; createdAt: Date; + assistantId: string; // 关联的助手 ID + isTemporary?: boolean; // 标记是否为临时会话 } -// 定义助手接口 (示例) interface Assistant { id: string; name: string; description: string; - avatar?: string; // 可选头像 URL 或 emoji - systemPrompt: string; // 核心:系统提示 - model: string; // 例如 'gpt-3.5-turbo', 'gemini-pro' + avatar?: string; + systemPrompt: string; + model: string; temperature: number; - // 可以添加 top_p, max_tokens 等其他参数 } +// --- Helper Function --- +// 查找助手的最新会话 (非临时) +const findLastSession = (sessions: ChatSession[], assistantId: string): ChatSession | undefined => { + return sessions + .filter(s => s.assistantId === assistantId && !s.isTemporary) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]; +}; + export default function ChatPage() { // --- State Variables --- const [inputMessage, setInputMessage] = useState(''); - const [messages, setMessages] = useState([ - { id: 'init-1', text: '你好!我是默认助手,有什么可以帮你的吗?', sender: 'ai' }, - ]); + const [messages, setMessages] = useState([]); // 初始为空,由选中助手/会话决定 const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [isSessionPanelOpen, setIsSessionPanelOpen] = useState(true); // 右侧会话面板状态 + const [isSessionPanelOpen, setIsSessionPanelOpen] = useState(true); - // --- Mock Data --- - // 助手列表 Mock 数据 + // --- Mock Data & State --- const [assistants, setAssistants] = useState([ { id: 'asst-default', name: '默认助手', description: '通用聊天助手', avatar: '🤖', systemPrompt: '你是一个乐于助人的 AI 助手。', model: 'gpt-3.5-turbo', temperature: 0.7 }, - { id: 'asst-coder', name: '代码助手', description: '帮助编写和解释代码', avatar: '💻', systemPrompt: '你是一个专业的代码助手,精通多种编程语言。请提供清晰、准确的代码示例和解释。', model: 'gpt-4', temperature: 0.5 }, - { id: 'asst-writer', name: '写作助手', description: '协助进行创意写作', avatar: '✍️', systemPrompt: '你是一位富有创意的写作伙伴,擅长构思情节、润色文字。', model: 'gemini-pro', temperature: 0.9 }, + { id: 'asst-coder', name: '代码助手', description: '帮助编写和解释代码', avatar: '💻', systemPrompt: '你是一个专业的代码助手...', model: 'gpt-4', temperature: 0.5 }, + { id: 'asst-writer', name: '写作助手', description: '协助进行创意写作', avatar: '✍️', systemPrompt: '你是一位富有创意的写作伙伴...', model: 'gemini-pro', temperature: 0.9 }, ]); - const [currentAssistantId, setCurrentAssistantId] = useState('asst-default'); // 当前选中的助手 + const [currentAssistantId, setCurrentAssistantId] = useState('asst-default'); - // 会话列表 Mock 数据 - const [sessions, setSessions] = useState([ - {id: 'session-1', title: '关于 RAG 技术的讨论', createdAt: new Date(Date.now() - 3600000)}, - {id: 'session-2', title: 'Python FastAPI 项目构思', createdAt: new Date(Date.now() - 7200000)}, - {id: 'session-3', title: '如何学习 LangChain', createdAt: new Date()}, + // 所有会话存储在一起,通过 assistantId 过滤 + const [allSessions, setAllSessions] = useState([ + {id: 'session-1', title: '默认助手的讨论', createdAt: new Date(Date.now() - 3600000), assistantId: 'asst-default'}, + {id: 'session-2', title: '代码调试记录', createdAt: new Date(Date.now() - 7200000), assistantId: 'asst-coder'}, + {id: 'session-3', title: '默认助手的学习 LangChain', createdAt: new Date(), assistantId: 'asst-default'}, ]); - const [currentSessionId, setCurrentSessionId] = useState('session-3'); // 当前选中的会话 + // 当前激活的会话 ID,可能是真实 ID 或 'temp-new-chat' + const [currentSessionId, setCurrentSessionId] = useState(null); // --- Refs --- const messagesEndRef = useRef(null); // --- Effects --- + // 自动滚动 useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); + // 根据当前助手 ID 过滤出会话列表 + const currentAssistantSessions = React.useMemo(() => { + return allSessions.filter(s => s.assistantId === currentAssistantId); + }, [allSessions, currentAssistantId]); + + // 当助手切换时,决定加载哪个会话 + useEffect(() => { + const lastSession = findLastSession(allSessions, currentAssistantId); + if (lastSession) { + setCurrentSessionId(lastSession.id); + // TODO: 在实际应用中,这里需要调用 API 加载 lastSession.id 的历史消息 + console.log(`加载助手 ${currentAssistantId} 的最后一个会话: ${lastSession.id}`); + // 模拟加载消息 + const selectedAssistant = assistants.find(a => a.id === currentAssistantId); + setMessages([ + { id: `init-${lastSession.id}-1`, text: `继续与 ${selectedAssistant?.name || '助手'} 的对话: ${lastSession.title}`, sender: 'ai' }, + // ... 应该加载真实历史消息 + ]); + } else { + // 没有历史会话,进入临时新对话状态 + setCurrentSessionId('temp-new-chat'); + console.log(`助手 ${currentAssistantId} 没有历史会话,创建临时新对话`); + const selectedAssistant = assistants.find(a => a.id === currentAssistantId); + setMessages([ + { id: `init-temp-${currentAssistantId}`, text: `开始与 ${selectedAssistant?.name || '助手'} 的新对话吧!`, sender: 'ai' }, + ]); + // 不将会话添加到 allSessions 列表 + } + }, [currentAssistantId, allSessions, assistants]); // 依赖助手 ID 和所有会话 + + // --- Event Handlers --- const handleSendMessage = async (e?: React.FormEvent) => { e?.preventDefault(); const trimmedMessage = inputMessage.trim(); if (!trimmedMessage || isLoading) return; setError(null); + setIsLoading(true); // 先设置加载状态 const userMessage: Message = { id: Date.now().toString(), text: trimmedMessage, sender: 'user', }; - setMessages((prevMessages) => [...prevMessages, userMessage]); + + let targetSessionId = currentSessionId; + let newSessionCreated = false; + + // 检查是否是临时新对话的第一条消息 + if (currentSessionId === 'temp-new-chat') { + console.log("发送临时新对话的第一条消息,准备创建会话..."); + try { + // --- 模拟后端创建会话并生成标题 --- + // TODO: 调用后端 API (e.g., POST /api/v1/sessions) + // const { newSessionId, newTitle } = await createSessionOnBackend(currentAssistantId, trimmedMessage); + await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟 + const newSessionId = `session-${Date.now()}`; // 模拟生成的新 ID + const newTitle = `关于 "${trimmedMessage.substring(0, 15)}..." 的讨论`; // 模拟 AI 生成的标题 + // --- 模拟结束 --- + + const newSession: ChatSession = { + id: newSessionId, + title: newTitle, + createdAt: new Date(), + assistantId: currentAssistantId, + isTemporary: false, // 不再是临时的 + }; + + // 将新创建的会话添加到全局列表,并设为当前会话 + setAllSessions(prev => [...prev, newSession]); + setCurrentSessionId(newSessionId); + targetSessionId = newSessionId; // 更新目标会话 ID + newSessionCreated = true; // 标记已创建 + console.log(`新会话创建成功: ID=${newSessionId}, Title=${newTitle}`); + + // 更新消息列表(先添加用户消息) + // 注意:因为 setMessages 是异步的,这里先更新,后续 AI 回复再更新 + setMessages(prev => [...prev, userMessage]); + + + } catch (creationError: any) { + console.error("创建新会话失败:", creationError); + setError(`无法创建新会话: ${creationError.message}`); + setIsLoading(false); + return; // 创建失败则停止后续操作 + } + } else { + // 不是新会话,直接将用户消息添加到当前消息列表 + setMessages(prev => [...prev, userMessage]); + } + + + // 清空输入框 setInputMessage(''); - setIsLoading(true); + // 发送消息到后端 (无论是否新创建会话,都需要发送) try { - // TODO: 调用后端时需要传递 currentAssistantId 和 currentSessionId - // const aiReply = await sendChatMessage(trimmedMessage, currentSessionId, currentAssistantId); - const aiReply = await sendChatMessage(trimmedMessage); // 暂时保持不变 + console.log(`发送消息到会话: ${targetSessionId}, 助手: ${currentAssistantId}`); + // TODO: 调用后端 API 时需要传递 targetSessionId 和 currentAssistantId + // const aiReply = await sendChatMessage(trimmedMessage, targetSessionId, currentAssistantId); + const aiReply = await sendChatMessage(trimmedMessage); // 暂时保持不变 - const aiMessage: Message = { - id: Date.now().toString() + '_ai', - text: aiReply, - sender: 'ai', - }; - setMessages((prevMessages) => [...prevMessages, aiMessage]); - } catch (err: any) { - console.error("Failed to send message:", err); - const errorMessageText = err.message || 'An unknown error occurred.'; - setError(errorMessageText); - const errorMessage: Message = { - id: Date.now().toString() + '_err', - text: `错误: ${errorMessageText}`, - sender: 'ai', - isError: true, - }; - setMessages((prevMessages) => [...prevMessages, errorMessage]); + const aiMessage: Message = { + id: Date.now().toString() + '_ai', + text: aiReply, + sender: 'ai', + }; + // 使用函数式更新,确保基于最新的消息列表添加回复 + setMessages((prevMessages) => [...prevMessages, aiMessage]); + + } catch (sendError: any) { + console.error("发送消息失败:", sendError); + const errorMessageText = sendError.message || 'An unknown error occurred.'; + setError(errorMessageText); // 更新错误状态 + const errorMessage: Message = { + id: Date.now().toString() + '_err', + text: `错误: ${errorMessageText}`, + sender: 'ai', + isError: true, + }; + setMessages((prevMessages) => [...prevMessages, errorMessage]); } finally { - setIsLoading(false); + setIsLoading(false); // 无论成功失败,结束加载 } }; @@ -126,17 +216,40 @@ export default function ChatPage() { setIsSessionPanelOpen(!isSessionPanelOpen); }; + // 点击助手列表项的处理函数 const handleSelectAssistant = (assistantId: string) => { - setCurrentAssistantId(assistantId); - // TODO: 可能需要清空当前消息或开始新会话 - console.log("Selected assistant:", assistantId); - // 可以在这里加载助手的初始消息或更新聊天标题 - const selectedAssistant = assistants.find(a => a.id === assistantId); - setMessages([ - { id: `init-${assistantId}`, text: `你好!我是${selectedAssistant?.name || '助手'}。${selectedAssistant?.description || ''}`, sender: 'ai' }, - ]); - setCurrentSessionId(`new-${Date.now()}`); // 切换助手时通常开始新会话 - setSessions(prev => [...prev, {id: `new-${Date.now()}`, title: `与 ${selectedAssistant?.name} 的新对话`, createdAt: new Date()}]); // 添加新会话到列表 + if (assistantId !== currentAssistantId) { + setCurrentAssistantId(assistantId); + // 后续逻辑由 useEffect 处理 + } + } + + // 点击会话列表项的处理函数 + const handleSelectSession = (sessionId: string) => { + if (sessionId !== currentSessionId) { + setCurrentSessionId(sessionId); + // TODO: 调用 API 加载该会话的历史消息 + console.log(`切换到会话: ${sessionId}`); + // 模拟加载消息 + const session = allSessions.find(s => s.id === sessionId); + const assistant = assistants.find(a => a.id === session?.assistantId); + setMessages([ + { id: `init-${sessionId}-1`, text: `继续与 ${assistant?.name || '助手'} 的对话: ${session?.title || ''}`, sender: 'ai' }, + // ... 加载真实历史消息 + ]); + } + } + + // 点击新建话题按钮 + const handleNewTopic = () => { + if (currentSessionId !== 'temp-new-chat') { + setCurrentSessionId('temp-new-chat'); + const selectedAssistant = assistants.find(a => a.id === currentAssistantId); + setMessages([ + { id: `init-temp-${currentAssistantId}`, text: `开始与 ${selectedAssistant?.name || '助手'} 的新对话吧!`, sender: 'ai' }, + ]); + console.log("手动创建临时新对话"); + } } // --- JSX Rendering --- @@ -263,19 +376,26 @@ export default function ChatPage() { {/* 右侧会话管理面板 */} -