refactor(pages): 重构核心页面支持实时数据

首页 (page.tsx):
- 集成 useConversations 和 useSettings hooks
- 实现快捷操作创建新会话
- 添加加载状态处理

聊天页 (chat/[id]/page.tsx):
- 集成 useStreamChat 实现流式对话
- 支持 AI 思考内容展示
- 优化消息发送和模型切换
- 添加错误处理和重试机制

设置页 (settings/page.tsx):
- 重构为完整的设置管理界面
- 支持 API 配置(URL、密钥)
- 支持默认模型和工具选择
- 支持主题和语言设置
- 添加设置保存和同步功能
This commit is contained in:
gaoziman 2025-12-18 11:43:59 +08:00
parent bb5996240a
commit f405f298aa
3 changed files with 709 additions and 172 deletions

View File

@ -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<Model>(models[1]);
const [tools, setTools] = useState<Tool[]>(initialTools);
const [messages, setMessages] = useState<Message[]>(sampleMessages);
const messagesEndRef = useRef<HTMLDivElement>(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<string>('');
const [enabledTools, setEnabledTools] = useState<string[]>([]);
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 (
<div className="flex min-h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-[var(--color-primary)]" />
</div>
);
};
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 (
<div className="flex min-h-screen">
{/* 侧边栏 */}
<Sidebar
user={currentUser}
chatHistories={chatHistories}
isOpen={sidebarOpen}
onToggle={() => setSidebarOpen(!sidebarOpen)}
/>
@ -97,10 +211,25 @@ export default function ChatPage() {
<div className="flex items-center gap-3">
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
<h1 className="text-base font-medium text-[var(--color-text-primary)] truncate max-w-[300px]">
{chatTitle}
{conversation?.title || '新对话'}
</h1>
</div>
<div className="flex items-center gap-2">
{/* 思考模式开关 */}
<button
onClick={handleThinkingToggle}
className={cn(
'flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg transition-colors',
enableThinking
? 'bg-[var(--color-primary-light)] text-[var(--color-primary)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]'
)}
title={enableThinking ? '关闭思考模式' : '开启思考模式'}
>
<Clock size={16} />
<span></span>
</button>
<button
className="flex items-center gap-2 px-3 py-1.5 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] rounded-lg transition-colors"
title="分享对话"
@ -120,13 +249,32 @@ export default function ChatPage() {
{/* 消息列表区域 */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-[900px] mx-auto px-4 py-6">
{messages.map((message) => (
<MessageBubble
key={message.id}
message={message}
user={message.role === 'user' ? currentUser : undefined}
/>
))}
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<h2 className="text-xl font-medium text-[var(--color-text-primary)] mb-2">
</h2>
<p className="text-sm text-[var(--color-text-tertiary)]">
AI
</p>
</div>
) : (
messages.map((message) => (
<MessageBubble
key={message.id}
message={{
id: message.id,
role: message.role,
content: message.content,
timestamp: new Date(),
}}
user={message.role === 'user' ? currentUser : undefined}
thinkingContent={message.thinkingContent}
isStreaming={message.status === 'streaming'}
error={message.error}
/>
))
)}
<div ref={messagesEndRef} />
</div>
</div>
@ -134,14 +282,26 @@ export default function ChatPage() {
{/* 固定底部输入框 */}
<div className="sticky bottom-0 bg-gradient-to-t from-white via-white to-transparent pt-4">
<div className="max-w-[900px] mx-auto px-4 pb-4">
{isStreaming && (
<div className="flex justify-center mb-3">
<button
onClick={stopGeneration}
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] rounded-lg hover:bg-[var(--color-bg-hover)] transition-colors"
>
<Square size={14} />
</button>
</div>
)}
<ChatInput
models={models}
selectedModel={selectedModel}
onModelSelect={setSelectedModel}
tools={tools}
models={modelOptions}
selectedModel={selectedModel || modelOptions[0]}
onModelSelect={(model) => setSelectedModelId(model.id)}
tools={toolOptions}
onToolToggle={handleToolToggle}
onEnableAllTools={handleEnableAllTools}
onSend={handleSend}
placeholder="输入您的问题..."
/>
</div>
</div>

View File

@ -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<Model>(models[1]); // Sonnet as default
const [tools, setTools] = useState<Tool[]>(initialTools);
const { createConversation } = useConversations();
const { models, loading: modelsLoading } = useModels();
const { tools: availableTools, loading: toolsLoading } = useTools();
const { settings } = useSettings();
const [selectedModelId, setSelectedModelId] = useState<string>('');
const [enabledTools, setEnabledTools] = useState<string[]>([]);
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 (
<AppLayout>
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-[var(--color-text-tertiary)]">...</div>
</div>
</AppLayout>
);
}
return (
<AppLayout>
<div className="w-full max-w-[900px] mx-auto">
@ -50,10 +119,10 @@ export default function HomePage() {
{/* 聊天输入框 */}
<ChatInput
models={models}
models={modelOptions}
selectedModel={selectedModel}
onModelSelect={setSelectedModel}
tools={tools}
onModelSelect={(model) => setSelectedModelId(model.id)}
tools={toolOptions}
onToolToggle={handleToolToggle}
onEnableAllTools={handleEnableAllTools}
onSend={handleSend}

View File

@ -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<Settings>({
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<string, string> = { 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 (
<div className="flex min-h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-[var(--color-primary)]" />
</div>
);
}
return (
<div className="flex min-h-screen">
{/* 侧边栏 */}
@ -58,7 +209,7 @@ export default function SettingsPage() {
{/* Body */}
<div className="px-8 pb-8">
<div className="max-w-[640px] mx-auto">
<div className="max-w-[800px] mx-auto">
{/* 返回链接 */}
<Link
href="/"
@ -78,49 +229,240 @@ export default function SettingsPage() {
</p>
</div>
{/* 账户设置 */}
{/* CCH 配置 */}
<SettingsSection
title="账户"
description="管理您的账户信息"
title="CCH 服务配置"
description="配置 Claude Code Hub 服务连接"
>
<SettingsItem
label="邮箱"
description={currentUser.email}
label="CCH 服务地址"
description="Claude Code Hub 服务的 URL"
>
<button className="btn-ghost"></button>
<input
type="text"
className="settings-input w-80"
value={cchUrl}
onChange={(e) => setCchUrl(e.target.value)}
placeholder="http://localhost:13500"
/>
</SettingsItem>
<SettingsItem
label="当前计划"
label="API Key"
description={
<span className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-primary-light)] rounded-full text-sm text-[var(--color-primary)] font-medium">
<Zap size={14} />
{currentUser.plan === 'free' ? '免费计划' : '专业计划'}
</span>
settings?.cchApiKeyConfigured
? '已配置 API Key'
: '设置访问 CCH 服务的 API Key'
}
>
<button className="btn-primary"></button>
<div className="flex items-center gap-2">
{settings?.cchApiKeyConfigured ? (
<>
<span className="inline-flex items-center gap-1 text-sm text-green-600">
<Check size={14} />
</span>
<button
onClick={handleClearApiKey}
className="btn-ghost text-red-600 hover:text-red-700 text-sm"
disabled={saving}
>
</button>
</>
) : (
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
className="settings-input pr-10 w-80"
value={cchApiKey}
onChange={(e) => setCchApiKey(e.target.value)}
placeholder="输入 API Key"
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
)}
</div>
</SettingsItem>
<div className="px-5 py-4 border-t border-[var(--color-border-light)]">
<button
onClick={handleSaveCchConfig}
disabled={saveStatus === 'saving'}
className="btn-primary inline-flex items-center gap-2"
>
{saveStatus === 'saving' ? (
<Loader2 size={16} className="animate-spin" />
) : saveStatus === 'saved' ? (
<Check size={16} />
) : null}
{saveStatus === 'saving' ? '保存中...' : saveStatus === 'saved' ? '已保存' : '保存配置'}
</button>
{saveStatus === 'error' && (
<span className="ml-3 text-sm text-red-600"></span>
)}
</div>
</SettingsSection>
{/* 模型和工具设置 */}
<SettingsSection
title="AI 配置"
description="配置默认模型和工具"
>
<SettingsItem
label="默认模型"
description="为新对话选择默认 AI 模型"
>
<select
className="settings-select"
value={settings?.defaultModel || ''}
onChange={(e) => handleModelChange(e.target.value)}
disabled={modelsLoading || saving}
>
{models.map((model) => (
<option key={model.modelId} value={model.modelId}>
{model.displayName}
</option>
))}
</select>
</SettingsItem>
<SettingsItem
label="使用量"
description="本月消息数"
label="启用思考模式"
description="让 AI 在回答前展示思考过程"
>
<div className="w-full">
<div className="mt-3">
<div className="h-2 bg-[var(--color-bg-tertiary)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-primary)] rounded-full transition-all duration-500"
style={{ width: '35%' }}
/>
<Toggle
checked={settings?.enableThinking || false}
onChange={handleThinkingToggle}
disabled={saving}
/>
</SettingsItem>
<div className="px-5 py-4 border-b border-[var(--color-border-light)]">
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-3">
</div>
<div className="text-xs text-[var(--color-text-tertiary)] mb-4">
</div>
<div className="flex flex-wrap gap-2">
{toolsLoading ? (
<span className="text-sm text-[var(--color-text-tertiary)]">...</span>
) : (
tools.map((tool) => (
<button
key={tool.toolId}
onClick={() => handleToolToggle(tool.toolId)}
disabled={saving}
className={cn(
'inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all',
settings?.defaultTools?.includes(tool.toolId)
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-secondary)]'
)}
>
{settings?.defaultTools?.includes(tool.toolId) && (
<Check size={14} />
)}
{tool.displayName}
</button>
))
)}
</div>
</div>
</SettingsSection>
{/* AI 行为设置 */}
<SettingsSection
title="AI 行为设置"
description="自定义 AI 的回复风格和行为"
>
{/* 温度参数 */}
<div className="px-5 py-4 border-b border-[var(--color-border-light)]">
<div className="flex justify-between items-center mb-3">
<div>
<div className="text-sm font-medium text-[var(--color-text-primary)]">
(Temperature)
</div>
<div className="flex justify-between mt-2 text-xs text-[var(--color-text-tertiary)]">
<span>35 / 100 </span>
<span>35%</span>
<div className="text-xs text-[var(--color-text-tertiary)] mt-1">
</div>
</div>
<div className="text-lg font-mono font-semibold text-[var(--color-primary)]">
{temperature}
</div>
</div>
</SettingsItem>
<div className="flex items-center gap-4">
<span className="text-xs text-[var(--color-text-tertiary)]"></span>
<input
type="range"
min="0"
max="1"
step="0.1"
value={temperature}
onChange={(e) => handleTemperatureChange(e.target.value)}
className="flex-1 h-2 bg-[var(--color-bg-tertiary)] rounded-lg appearance-none cursor-pointer accent-[var(--color-primary)]"
/>
<span className="text-xs text-[var(--color-text-tertiary)]"></span>
</div>
</div>
{/* System Prompt 编辑器 */}
<div className="px-5 py-4 border-b border-[var(--color-border-light)]">
<div className="flex justify-between items-start mb-3">
<div>
<div className="text-sm font-medium text-[var(--color-text-primary)]">
(System Prompt)
</div>
<div className="text-xs text-[var(--color-text-tertiary)] mt-1">
AI 使
</div>
</div>
<button
onClick={handleResetSystemPrompt}
className="btn-ghost text-sm inline-flex items-center gap-1"
title="重置为默认提示词"
>
<RotateCcw size={14} />
</button>
</div>
<textarea
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
placeholder="留空使用默认提示词..."
rows={12}
className="w-full px-3 py-2 text-sm border border-[var(--color-border)] rounded-lg bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent resize-y font-mono"
/>
<div className="mt-2 text-xs text-[var(--color-text-tertiary)]">
使 Markdown {systemPrompt.length}
</div>
</div>
{/* 保存按钮 */}
<div className="px-5 py-4">
<button
onClick={handleSavePromptSettings}
disabled={promptSaveStatus === 'saving'}
className="btn-primary inline-flex items-center gap-2"
>
{promptSaveStatus === 'saving' ? (
<Loader2 size={16} className="animate-spin" />
) : promptSaveStatus === 'saved' ? (
<Check size={16} />
) : null}
{promptSaveStatus === 'saving' ? '保存中...' : promptSaveStatus === 'saved' ? '已保存' : '保存 AI 设置'}
</button>
{promptSaveStatus === 'error' && (
<span className="ml-3 text-sm text-red-600"></span>
)}
</div>
</SettingsSection>
{/* 偏好设置 */}
@ -128,31 +470,15 @@ export default function SettingsPage() {
title="偏好设置"
description="自定义您的体验"
>
<SettingsItem
label="默认模型"
description="为新对话选择默认 AI 模型"
>
<select
className="settings-select"
value={settings.defaultModel}
onChange={(e) => handleSelectChange('defaultModel', e.target.value)}
>
{models.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
</SettingsItem>
<SettingsItem
label="主题"
description="选择您喜欢的主题"
>
<select
className="settings-select"
value={settings.theme}
onChange={(e) => handleSelectChange('theme', e.target.value)}
value={settings?.theme || 'light'}
onChange={(e) => handleThemeChange(e.target.value)}
disabled={saving}
>
<option value="system"></option>
<option value="light"></option>
@ -166,8 +492,9 @@ export default function SettingsPage() {
>
<select
className="settings-select"
value={settings.language}
onChange={(e) => handleSelectChange('language', e.target.value)}
value={settings?.language || 'zh-CN'}
onChange={(e) => handleLanguageChange(e.target.value)}
disabled={saving}
>
<option value="en">English</option>
<option value="zh-CN"></option>
@ -175,26 +502,6 @@ export default function SettingsPage() {
<option value="ja"></option>
</select>
</SettingsItem>
<SettingsItem
label="启用网络搜索"
description="允许 AI 搜索网络获取信息"
>
<Toggle
checked={settings.enableWebSearch}
onChange={() => handleToggle('enableWebSearch')}
/>
</SettingsItem>
<SettingsItem
label="启用代码执行"
description="允许 AI 运行代码片段"
>
<Toggle
checked={settings.enableCodeExecution}
onChange={() => handleToggle('enableCodeExecution')}
/>
</SettingsItem>
</SettingsSection>
{/* 数据与隐私 */}
@ -207,8 +514,9 @@ export default function SettingsPage() {
description="保存您的对话记录"
>
<Toggle
checked={settings.saveChatHistory}
onChange={() => handleToggle('saveChatHistory')}
checked={settings?.saveChatHistory || false}
onChange={handleChatHistoryToggle}
disabled={saving}
/>
</SettingsItem>