diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 459a93d..0f52141 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -1,34 +1,82 @@ 'use client'; -import { useState, useRef, useEffect } from 'react'; -import { useParams } from 'next/navigation'; -import { Share2, MoreHorizontal } from 'lucide-react'; +import { useState, useRef, useEffect, use } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Share2, MoreHorizontal, Loader2, Square, Clock } from 'lucide-react'; import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar'; import { ChatInput } from '@/components/features/ChatInput'; import { MessageBubble } from '@/components/features/MessageBubble'; import { cn } from '@/lib/utils'; -import { - models, - tools as initialTools, - chatHistories, - sampleMessages, - currentUser -} from '@/data/mock'; -import type { Model, Tool, Message } from '@/types'; +import { currentUser } from '@/data/mock'; +import { useConversation, useConversations } from '@/hooks/useConversations'; +import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat'; +import { useModels, useTools, useSettings } from '@/hooks/useSettings'; -export default function ChatPage() { - const params = useParams(); - const chatId = params.id as string; +interface PageProps { + params: Promise<{ id: string }>; +} + +export default function ChatPage({ params }: PageProps) { + const { id: chatId } = use(params); + const router = useRouter(); + const searchParams = useSearchParams(); + const initialMessage = searchParams.get('message'); const [sidebarOpen, setSidebarOpen] = useState(true); - const [selectedModel, setSelectedModel] = useState(models[1]); - const [tools, setTools] = useState(initialTools); - const [messages, setMessages] = useState(sampleMessages); const messagesEndRef = useRef(null); + const [isNewChat, setIsNewChat] = useState(false); + const [initialMessageSent, setInitialMessageSent] = useState(false); - // 获取当前聊天信息 - const currentChat = chatHistories.find(c => c.id === chatId); - const chatTitle = currentChat?.title || '新对话'; + // 获取数据 + const { conversation, loading: conversationLoading, error: conversationError } = useConversation(chatId); + const { createConversation } = useConversations(); + const { models, loading: modelsLoading } = useModels(); + const { tools: availableTools, loading: toolsLoading } = useTools(); + const { settings } = useSettings(); + + // 聊天状态 + const { + messages, + isStreaming, + sendMessage, + stopGeneration, + setInitialMessages, + } = useStreamChat(); + + // 当前选择的模型和工具 + const [selectedModelId, setSelectedModelId] = useState(''); + const [enabledTools, setEnabledTools] = useState([]); + const [enableThinking, setEnableThinking] = useState(false); + + // 初始化模型和工具 + useEffect(() => { + if (conversation) { + setSelectedModelId(conversation.model); + setEnabledTools((conversation.tools as string[]) || []); + setEnableThinking(conversation.enableThinking || false); + setIsNewChat(false); + + // 加载历史消息 + if (conversation.messages && conversation.messages.length > 0) { + const historyMessages: ChatMessage[] = conversation.messages.map((msg) => ({ + id: msg.messageId, + role: msg.role as 'user' | 'assistant', + content: msg.content, + thinkingContent: msg.thinkingContent || undefined, + status: 'completed' as const, + inputTokens: msg.inputTokens || undefined, + outputTokens: msg.outputTokens || undefined, + })); + setInitialMessages(historyMessages); + } + } else if (settings && !conversationLoading) { + // 对话不存在或加载失败,使用默认设置作为新对话 + setSelectedModelId(settings.defaultModel); + setEnabledTools(settings.defaultTools || []); + setEnableThinking(settings.enableThinking); + setIsNewChat(true); + } + }, [conversation, settings, conversationLoading, setInitialMessages]); // 滚动到底部 const scrollToBottom = () => { @@ -39,48 +87,114 @@ export default function ChatPage() { scrollToBottom(); }, [messages]); + // 处理初始消息(从首页跳转过来时) + useEffect(() => { + if ( + initialMessage && + !initialMessageSent && + !conversationLoading && + conversation && + selectedModelId + ) { + setInitialMessageSent(true); + // 清除 URL 中的 message 参数 + router.replace(`/chat/${chatId}`); + // 发送初始消息 + sendMessage({ + conversationId: chatId, + message: initialMessage, + model: selectedModelId, + tools: enabledTools, + enableThinking, + }); + } + }, [initialMessage, initialMessageSent, conversationLoading, conversation, selectedModelId, chatId, enabledTools, enableThinking, sendMessage, router]); + + // 切换工具 const handleToolToggle = (toolId: string) => { - setTools((prev) => - prev.map((tool) => - tool.id === toolId ? { ...tool, enabled: !tool.enabled } : tool - ) + setEnabledTools((prev) => + prev.includes(toolId) + ? prev.filter((t) => t !== toolId) + : [...prev, toolId] ); }; + // 全选/取消全选工具 const handleEnableAllTools = (enabled: boolean) => { - setTools((prev) => - prev.map((tool) => ({ ...tool, enabled })) + if (enabled) { + setEnabledTools(availableTools.map((t) => t.toolId)); + } else { + setEnabledTools([]); + } + }; + + // 发送消息 + const handleSend = async (message: string) => { + let targetConversationId = chatId; + + // 如果是新对话,先创建对话 + if (isNewChat) { + try { + const newConversation = await createConversation({ + model: selectedModelId, + tools: enabledTools, + enableThinking, + }); + targetConversationId = newConversation.conversationId; + setIsNewChat(false); + + // 跳转到新对话页面 + router.replace(`/chat/${targetConversationId}`); + } catch (error) { + console.error('Failed to create conversation:', error); + return; + } + } + + await sendMessage({ + conversationId: targetConversationId, + message, + model: selectedModelId, + tools: enabledTools, + enableThinking, + }); + }; + + // 切换思考模式 + const handleThinkingToggle = () => { + setEnableThinking(!enableThinking); + }; + + // 转换模型格式 + const modelOptions = models.map((m) => ({ + id: m.modelId, + name: m.displayName, + tag: m.supportsThinking ? 'Thinking' : '', + })); + + const selectedModel = modelOptions.find((m) => m.id === selectedModelId) || modelOptions[0]; + + // 转换工具格式 + const toolOptions = availableTools.map((t) => ({ + id: t.toolId, + name: t.displayName, + icon: t.icon || 'Tool', + enabled: enabledTools.includes(t.toolId), + })); + + if (conversationLoading || modelsLoading || toolsLoading) { + return ( +
+ +
); - }; - - const handleSend = (message: string) => { - // 添加用户消息 - const userMessage: Message = { - id: `msg-${Date.now()}`, - role: 'user', - content: message, - timestamp: new Date(), - }; - setMessages((prev) => [...prev, userMessage]); - - // 模拟 AI 回复 - setTimeout(() => { - const aiMessage: Message = { - id: `msg-${Date.now() + 1}`, - role: 'assistant', - content: '感谢您的消息!这是一个模拟的 AI 回复。在实际应用中,这里会调用 AI API 生成真实的回复内容。\n\n您可以继续与我对话,我会尽力帮助您解决问题。', - timestamp: new Date(), - }; - setMessages((prev) => [...prev, aiMessage]); - }, 1000); - }; + } return (
{/* 侧边栏 */} setSidebarOpen(!sidebarOpen)} /> @@ -97,10 +211,25 @@ export default function ChatPage() {
setSidebarOpen(!sidebarOpen)} />

- {chatTitle} + {conversation?.title || '新对话'}

+ {/* 思考模式开关 */} + + +
+ )} setSelectedModelId(model.id)} + tools={toolOptions} onToolToggle={handleToolToggle} onEnableAllTools={handleEnableAllTools} onSend={handleSend} + placeholder="输入您的问题..." />
diff --git a/src/app/page.tsx b/src/app/page.tsx index cf9bd4c..8d7b45b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,39 +1,90 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { AppLayout } from '@/components/layout/AppLayout'; import { Welcome } from '@/components/features/Welcome'; import { ChatInput } from '@/components/features/ChatInput'; import { QuickActions } from '@/components/features/QuickActions'; -import { models, tools as initialTools, quickActions, currentUser, getGreeting } from '@/data/mock'; -import type { Model, Tool, QuickAction } from '@/types'; +import { currentUser, getGreeting } from '@/data/mock'; +import { useConversations } from '@/hooks/useConversations'; +import { useModels, useTools, useSettings } from '@/hooks/useSettings'; +import type { QuickAction } from '@/types'; export default function HomePage() { const router = useRouter(); - const [selectedModel, setSelectedModel] = useState(models[1]); // Sonnet as default - const [tools, setTools] = useState(initialTools); + const { createConversation } = useConversations(); + const { models, loading: modelsLoading } = useModels(); + const { tools: availableTools, loading: toolsLoading } = useTools(); + const { settings } = useSettings(); + + const [selectedModelId, setSelectedModelId] = useState(''); + const [enabledTools, setEnabledTools] = useState([]); + const [isSending, setIsSending] = useState(false); const greeting = getGreeting(currentUser.name); + // 初始化默认设置 + useEffect(() => { + if (settings) { + setSelectedModelId(settings.defaultModel); + setEnabledTools(settings.defaultTools || []); + } + }, [settings]); + + // 转换模型格式 + const modelOptions = models.map((m) => ({ + id: m.modelId, + name: m.displayName, + tag: m.supportsThinking ? 'Thinking' : '', + })); + + const selectedModel = modelOptions.find((m) => m.id === selectedModelId) || modelOptions[0]; + + // 转换工具格式 + const toolOptions = availableTools.map((t) => ({ + id: t.toolId, + name: t.displayName, + icon: t.icon || 'Tool', + enabled: enabledTools.includes(t.toolId), + })); + const handleToolToggle = (toolId: string) => { - setTools((prev) => - prev.map((tool) => - tool.id === toolId ? { ...tool, enabled: !tool.enabled } : tool - ) + setEnabledTools((prev) => + prev.includes(toolId) + ? prev.filter((t) => t !== toolId) + : [...prev, toolId] ); }; const handleEnableAllTools = (enabled: boolean) => { - setTools((prev) => - prev.map((tool) => ({ ...tool, enabled })) - ); + if (enabled) { + setEnabledTools(availableTools.map((t) => t.toolId)); + } else { + setEnabledTools([]); + } }; - const handleSend = (message: string) => { - // 在实际应用中,这里会创建新对话并跳转 - console.log('Sending message:', message); - router.push('/chat/1'); + const handleSend = async (message: string) => { + if (isSending) return; + + setIsSending(true); + try { + // 创建新对话 + const newConversation = await createConversation({ + title: message.slice(0, 50), + model: selectedModelId || 'default', + tools: enabledTools, + enableThinking: settings?.enableThinking || false, + }); + + // 跳转到新对话页面并携带初始消息 + router.push(`/chat/${newConversation.conversationId}?message=${encodeURIComponent(message)}`); + } catch (error) { + console.error('Failed to create conversation:', error); + } finally { + setIsSending(false); + } }; const handleQuickAction = (action: QuickAction) => { @@ -42,6 +93,24 @@ export default function HomePage() { } }; + // 快捷操作 + const quickActions: QuickAction[] = [ + { id: '1', label: '写代码', prompt: '帮我写一段代码', icon: 'Code' }, + { id: '2', label: '解释概念', prompt: '解释一下什么是', icon: 'BookOpen' }, + { id: '3', label: '调试代码', prompt: '帮我调试这段代码', icon: 'Bug' }, + { id: '4', label: '代码审查', prompt: '请审查这段代码', icon: 'Search' }, + ]; + + if (modelsLoading || toolsLoading) { + return ( + +
+
加载中...
+
+
+ ); + } + return (
@@ -50,10 +119,10 @@ export default function HomePage() { {/* 聊天输入框 */} setSelectedModelId(model.id)} + tools={toolOptions} onToolToggle={handleToolToggle} onEnableAllTools={handleEnableAllTools} onSend={handleSend} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 350aa6d..064480c 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,39 +1,190 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; -import { ArrowLeft, Zap, Download } from 'lucide-react'; +import { ArrowLeft, Download, Check, Loader2, Eye, EyeOff, RotateCcw } from 'lucide-react'; import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar'; import { Toggle } from '@/components/ui/Toggle'; import { cn } from '@/lib/utils'; -import { currentUser, chatHistories, models } from '@/data/mock'; -import type { Settings } from '@/types'; +import { currentUser, chatHistories } from '@/data/mock'; +import { useSettings, useModels, useTools } from '@/hooks/useSettings'; + +// 默认系统提示词 +const DEFAULT_SYSTEM_PROMPT = `你是一个专业、友好的 AI 助手。请遵循以下规则来回复用户: + +## 回复风格 +- 使用中文回复,除非用户明确要求其他语言 +- 回复要详细、有深度,不要过于简短 +- 语气友好、专业,像一个耐心的老师 + +## 格式规范 +- 使用 Markdown 格式化回复 +- 对于代码,使用代码块并标明语言,例如 \`\`\`python +- 使用标题(##、###)来组织长回复 +- 使用列表(有序或无序)来列举要点 +- 使用粗体或斜体强调重要内容 + +## 回答结构 +当回答技术问题时,请按以下结构: +1. **简要概述**:先给出简洁的答案 +2. **详细解释**:深入解释原理或概念 +3. **代码示例**:如果适用,提供完整、可运行的代码 +4. **注意事项**:指出常见错误或最佳实践 +5. **延伸阅读**:如果有相关主题,可以简要提及 + +## 代码规范 +- 代码要完整、可运行,不要省略关键部分 +- 添加适当的注释解释关键逻辑 +- 使用有意义的变量名 +- 如果代码较长,先展示完整代码,再逐段解释 + +## 特别注意 +- 如果问题不明确,先确认理解是否正确 +- 如果有多种方案,说明各自的优缺点 +- 承认不确定的地方,不要编造信息`; export default function SettingsPage() { const [sidebarOpen, setSidebarOpen] = useState(true); - const [settings, setSettings] = useState({ - defaultModel: 'sonnet', - theme: 'light', - language: 'zh-CN', - enableWebSearch: true, - enableCodeExecution: false, - saveChatHistory: true, - }); + const { settings, loading, saving, updateSettings } = useSettings(); + const { models, loading: modelsLoading } = useModels(); + const { tools, loading: toolsLoading } = useTools(); - const handleToggle = (key: keyof Settings) => { - setSettings((prev) => ({ - ...prev, - [key]: !prev[key], - })); + // CCH 配置状态 + const [cchUrl, setCchUrl] = useState(''); + const [cchApiKey, setCchApiKey] = useState(''); + const [showApiKey, setShowApiKey] = useState(false); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); + + // AI 行为设置状态 + const [systemPrompt, setSystemPrompt] = useState(''); + const [temperature, setTemperature] = useState('0.7'); + const [promptSaveStatus, setPromptSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); + + // 当设置加载完成后,更新本地状态 + useEffect(() => { + if (settings) { + setCchUrl(settings.cchUrl || ''); + setSystemPrompt(settings.systemPrompt || ''); + setTemperature(settings.temperature || '0.7'); + } + }, [settings]); + + // 保存 CCH 配置 + const handleSaveCchConfig = async () => { + setSaveStatus('saving'); + try { + const updates: Record = { cchUrl }; + if (cchApiKey) { + updates.cchApiKey = cchApiKey; + } + await updateSettings(updates); + setSaveStatus('saved'); + setCchApiKey(''); // 清除输入的 API Key + setTimeout(() => setSaveStatus('idle'), 2000); + } catch { + setSaveStatus('error'); + setTimeout(() => setSaveStatus('idle'), 2000); + } }; - const handleSelectChange = (key: keyof Settings, value: string) => { - setSettings((prev) => ({ - ...prev, - [key]: value, - })); + // 清除 API Key + const handleClearApiKey = async () => { + try { + await updateSettings({ cchApiKey: '' }); + } catch (error) { + console.error('Failed to clear API key:', error); + } }; + // 更新默认模型 + const handleModelChange = async (modelId: string) => { + try { + await updateSettings({ defaultModel: modelId }); + } catch (error) { + console.error('Failed to update model:', error); + } + }; + + // 切换工具 + const handleToolToggle = async (toolId: string) => { + const currentTools = settings?.defaultTools || []; + const newTools = currentTools.includes(toolId) + ? currentTools.filter((t) => t !== toolId) + : [...currentTools, toolId]; + try { + await updateSettings({ defaultTools: newTools }); + } catch (error) { + console.error('Failed to update tools:', error); + } + }; + + // 切换思考模式 + const handleThinkingToggle = async () => { + try { + await updateSettings({ enableThinking: !settings?.enableThinking }); + } catch (error) { + console.error('Failed to toggle thinking:', error); + } + }; + + // 切换聊天历史 + const handleChatHistoryToggle = async () => { + try { + await updateSettings({ saveChatHistory: !settings?.saveChatHistory }); + } catch (error) { + console.error('Failed to toggle chat history:', error); + } + }; + + // 更新主题 + const handleThemeChange = async (theme: string) => { + try { + await updateSettings({ theme }); + } catch (error) { + console.error('Failed to update theme:', error); + } + }; + + // 更新语言 + const handleLanguageChange = async (language: string) => { + try { + await updateSettings({ language }); + } catch (error) { + console.error('Failed to update language:', error); + } + }; + + // 保存 System Prompt 和温度设置 + const handleSavePromptSettings = async () => { + setPromptSaveStatus('saving'); + try { + await updateSettings({ systemPrompt, temperature }); + setPromptSaveStatus('saved'); + setTimeout(() => setPromptSaveStatus('idle'), 2000); + } catch { + setPromptSaveStatus('error'); + setTimeout(() => setPromptSaveStatus('idle'), 2000); + } + }; + + // 重置为默认 System Prompt + const handleResetSystemPrompt = () => { + setSystemPrompt(DEFAULT_SYSTEM_PROMPT); + }; + + // 更新温度(实时显示但不立即保存) + const handleTemperatureChange = (value: string) => { + setTemperature(value); + }; + + if (loading) { + return ( +
+ +
+ ); + } + return (
{/* 侧边栏 */} @@ -58,7 +209,7 @@ export default function SettingsPage() { {/* Body */}
-
+
{/* 返回链接 */}
- {/* 账户设置 */} + {/* CCH 配置 */} - + setCchUrl(e.target.value)} + placeholder="http://localhost:13500" + /> - - {currentUser.plan === 'free' ? '免费计划' : '专业计划'} - + settings?.cchApiKeyConfigured + ? '已配置 API Key' + : '设置访问 CCH 服务的 API Key' } > - +
+ {settings?.cchApiKeyConfigured ? ( + <> + + + 已配置 + + + + ) : ( +
+ setCchApiKey(e.target.value)} + placeholder="输入 API Key" + /> + +
+ )} +
+
+ +
+ + {saveStatus === 'error' && ( + 保存失败 + )} +
+
+ + {/* 模型和工具设置 */} + + + -
-
-
-
+ + + +
+
+ 默认工具 +
+
+ 选择新对话默认启用的工具 +
+
+ {toolsLoading ? ( + 加载中... + ) : ( + tools.map((tool) => ( + + )) + )} +
+
+ + + {/* AI 行为设置 */} + + {/* 温度参数 */} +
+
+
+
+ 温度参数 (Temperature)
-
- 35 / 100 条消息 - 35% +
+ 控制回复的创造性和随机性。较低值更保守精确,较高值更创新多样。
+
+ {temperature} +
- +
+ 精确 + handleTemperatureChange(e.target.value)} + className="flex-1 h-2 bg-[var(--color-bg-tertiary)] rounded-lg appearance-none cursor-pointer accent-[var(--color-primary)]" + /> + 创意 +
+
+ + {/* System Prompt 编辑器 */} +
+
+
+
+ 系统提示词 (System Prompt) +
+
+ 定义 AI 的角色、回复风格和行为规范。留空将使用默认提示词。 +
+
+ +
+