refactor(pages): 重构核心页面支持实时数据
首页 (page.tsx): - 集成 useConversations 和 useSettings hooks - 实现快捷操作创建新会话 - 添加加载状态处理 聊天页 (chat/[id]/page.tsx): - 集成 useStreamChat 实现流式对话 - 支持 AI 思考内容展示 - 优化消息发送和模型切换 - 添加错误处理和重试机制 设置页 (settings/page.tsx): - 重构为完整的设置管理界面 - 支持 API 配置(URL、密钥) - 支持默认模型和工具选择 - 支持主题和语言设置 - 添加设置保存和同步功能
This commit is contained in:
parent
bb5996240a
commit
f405f298aa
@ -1,34 +1,82 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect, use } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Share2, MoreHorizontal } from 'lucide-react';
|
import { Share2, MoreHorizontal, Loader2, Square, Clock } from 'lucide-react';
|
||||||
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
||||||
import { ChatInput } from '@/components/features/ChatInput';
|
import { ChatInput } from '@/components/features/ChatInput';
|
||||||
import { MessageBubble } from '@/components/features/MessageBubble';
|
import { MessageBubble } from '@/components/features/MessageBubble';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import { currentUser } from '@/data/mock';
|
||||||
models,
|
import { useConversation, useConversations } from '@/hooks/useConversations';
|
||||||
tools as initialTools,
|
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
||||||
chatHistories,
|
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
|
||||||
sampleMessages,
|
|
||||||
currentUser
|
|
||||||
} from '@/data/mock';
|
|
||||||
import type { Model, Tool, Message } from '@/types';
|
|
||||||
|
|
||||||
export default function ChatPage() {
|
interface PageProps {
|
||||||
const params = useParams();
|
params: Promise<{ id: string }>;
|
||||||
const chatId = params.id as 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 [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 messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isNewChat, setIsNewChat] = useState(false);
|
||||||
|
const [initialMessageSent, setInitialMessageSent] = useState(false);
|
||||||
|
|
||||||
// 获取当前聊天信息
|
// 获取数据
|
||||||
const currentChat = chatHistories.find(c => c.id === chatId);
|
const { conversation, loading: conversationLoading, error: conversationError } = useConversation(chatId);
|
||||||
const chatTitle = currentChat?.title || '新对话';
|
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 = () => {
|
const scrollToBottom = () => {
|
||||||
@ -39,48 +87,114 @@ export default function ChatPage() {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [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) => {
|
const handleToolToggle = (toolId: string) => {
|
||||||
setTools((prev) =>
|
setEnabledTools((prev) =>
|
||||||
prev.map((tool) =>
|
prev.includes(toolId)
|
||||||
tool.id === toolId ? { ...tool, enabled: !tool.enabled } : tool
|
? prev.filter((t) => t !== toolId)
|
||||||
)
|
: [...prev, toolId]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 全选/取消全选工具
|
||||||
const handleEnableAllTools = (enabled: boolean) => {
|
const handleEnableAllTools = (enabled: boolean) => {
|
||||||
setTools((prev) =>
|
if (enabled) {
|
||||||
prev.map((tool) => ({ ...tool, 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 (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
<Sidebar
|
<Sidebar
|
||||||
user={currentUser}
|
user={currentUser}
|
||||||
chatHistories={chatHistories}
|
|
||||||
isOpen={sidebarOpen}
|
isOpen={sidebarOpen}
|
||||||
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||||
/>
|
/>
|
||||||
@ -97,10 +211,25 @@ export default function ChatPage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
|
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
<h1 className="text-base font-medium text-[var(--color-text-primary)] truncate max-w-[300px]">
|
<h1 className="text-base font-medium text-[var(--color-text-primary)] truncate max-w-[300px]">
|
||||||
{chatTitle}
|
{conversation?.title || '新对话'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<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"
|
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="分享对话"
|
title="分享对话"
|
||||||
@ -120,13 +249,32 @@ export default function ChatPage() {
|
|||||||
{/* 消息列表区域 */}
|
{/* 消息列表区域 */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="max-w-[900px] mx-auto px-4 py-6">
|
<div className="max-w-[900px] mx-auto px-4 py-6">
|
||||||
{messages.map((message) => (
|
{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
|
<MessageBubble
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={{
|
||||||
|
id: message.id,
|
||||||
|
role: message.role,
|
||||||
|
content: message.content,
|
||||||
|
timestamp: new Date(),
|
||||||
|
}}
|
||||||
user={message.role === 'user' ? currentUser : undefined}
|
user={message.role === 'user' ? currentUser : undefined}
|
||||||
|
thinkingContent={message.thinkingContent}
|
||||||
|
isStreaming={message.status === 'streaming'}
|
||||||
|
error={message.error}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</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="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">
|
<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
|
<ChatInput
|
||||||
models={models}
|
models={modelOptions}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel || modelOptions[0]}
|
||||||
onModelSelect={setSelectedModel}
|
onModelSelect={(model) => setSelectedModelId(model.id)}
|
||||||
tools={tools}
|
tools={toolOptions}
|
||||||
onToolToggle={handleToolToggle}
|
onToolToggle={handleToolToggle}
|
||||||
onEnableAllTools={handleEnableAllTools}
|
onEnableAllTools={handleEnableAllTools}
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
|
placeholder="输入您的问题..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
107
src/app/page.tsx
107
src/app/page.tsx
@ -1,39 +1,90 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { AppLayout } from '@/components/layout/AppLayout';
|
import { AppLayout } from '@/components/layout/AppLayout';
|
||||||
import { Welcome } from '@/components/features/Welcome';
|
import { Welcome } from '@/components/features/Welcome';
|
||||||
import { ChatInput } from '@/components/features/ChatInput';
|
import { ChatInput } from '@/components/features/ChatInput';
|
||||||
import { QuickActions } from '@/components/features/QuickActions';
|
import { QuickActions } from '@/components/features/QuickActions';
|
||||||
import { models, tools as initialTools, quickActions, currentUser, getGreeting } from '@/data/mock';
|
import { currentUser, getGreeting } from '@/data/mock';
|
||||||
import type { Model, Tool, QuickAction } from '@/types';
|
import { useConversations } from '@/hooks/useConversations';
|
||||||
|
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
|
||||||
|
import type { QuickAction } from '@/types';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [selectedModel, setSelectedModel] = useState<Model>(models[1]); // Sonnet as default
|
const { createConversation } = useConversations();
|
||||||
const [tools, setTools] = useState<Tool[]>(initialTools);
|
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);
|
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) => {
|
const handleToolToggle = (toolId: string) => {
|
||||||
setTools((prev) =>
|
setEnabledTools((prev) =>
|
||||||
prev.map((tool) =>
|
prev.includes(toolId)
|
||||||
tool.id === toolId ? { ...tool, enabled: !tool.enabled } : tool
|
? prev.filter((t) => t !== toolId)
|
||||||
)
|
: [...prev, toolId]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnableAllTools = (enabled: boolean) => {
|
const handleEnableAllTools = (enabled: boolean) => {
|
||||||
setTools((prev) =>
|
if (enabled) {
|
||||||
prev.map((tool) => ({ ...tool, enabled }))
|
setEnabledTools(availableTools.map((t) => t.toolId));
|
||||||
);
|
} else {
|
||||||
|
setEnabledTools([]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSend = (message: string) => {
|
const handleSend = async (message: string) => {
|
||||||
// 在实际应用中,这里会创建新对话并跳转
|
if (isSending) return;
|
||||||
console.log('Sending message:', message);
|
|
||||||
router.push('/chat/1');
|
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) => {
|
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 (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<div className="w-full max-w-[900px] mx-auto">
|
<div className="w-full max-w-[900px] mx-auto">
|
||||||
@ -50,10 +119,10 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* 聊天输入框 */}
|
{/* 聊天输入框 */}
|
||||||
<ChatInput
|
<ChatInput
|
||||||
models={models}
|
models={modelOptions}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
onModelSelect={setSelectedModel}
|
onModelSelect={(model) => setSelectedModelId(model.id)}
|
||||||
tools={tools}
|
tools={toolOptions}
|
||||||
onToolToggle={handleToolToggle}
|
onToolToggle={handleToolToggle}
|
||||||
onEnableAllTools={handleEnableAllTools}
|
onEnableAllTools={handleEnableAllTools}
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
|
|||||||
@ -1,39 +1,190 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
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 { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
||||||
import { Toggle } from '@/components/ui/Toggle';
|
import { Toggle } from '@/components/ui/Toggle';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { currentUser, chatHistories, models } from '@/data/mock';
|
import { currentUser, chatHistories } from '@/data/mock';
|
||||||
import type { Settings } from '@/types';
|
import { useSettings, useModels, useTools } from '@/hooks/useSettings';
|
||||||
|
|
||||||
|
// 默认系统提示词
|
||||||
|
const DEFAULT_SYSTEM_PROMPT = `你是一个专业、友好的 AI 助手。请遵循以下规则来回复用户:
|
||||||
|
|
||||||
|
## 回复风格
|
||||||
|
- 使用中文回复,除非用户明确要求其他语言
|
||||||
|
- 回复要详细、有深度,不要过于简短
|
||||||
|
- 语气友好、专业,像一个耐心的老师
|
||||||
|
|
||||||
|
## 格式规范
|
||||||
|
- 使用 Markdown 格式化回复
|
||||||
|
- 对于代码,使用代码块并标明语言,例如 \`\`\`python
|
||||||
|
- 使用标题(##、###)来组织长回复
|
||||||
|
- 使用列表(有序或无序)来列举要点
|
||||||
|
- 使用粗体或斜体强调重要内容
|
||||||
|
|
||||||
|
## 回答结构
|
||||||
|
当回答技术问题时,请按以下结构:
|
||||||
|
1. **简要概述**:先给出简洁的答案
|
||||||
|
2. **详细解释**:深入解释原理或概念
|
||||||
|
3. **代码示例**:如果适用,提供完整、可运行的代码
|
||||||
|
4. **注意事项**:指出常见错误或最佳实践
|
||||||
|
5. **延伸阅读**:如果有相关主题,可以简要提及
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
- 代码要完整、可运行,不要省略关键部分
|
||||||
|
- 添加适当的注释解释关键逻辑
|
||||||
|
- 使用有意义的变量名
|
||||||
|
- 如果代码较长,先展示完整代码,再逐段解释
|
||||||
|
|
||||||
|
## 特别注意
|
||||||
|
- 如果问题不明确,先确认理解是否正确
|
||||||
|
- 如果有多种方案,说明各自的优缺点
|
||||||
|
- 承认不确定的地方,不要编造信息`;
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const { settings, loading, saving, updateSettings } = useSettings();
|
||||||
defaultModel: 'sonnet',
|
const { models, loading: modelsLoading } = useModels();
|
||||||
theme: 'light',
|
const { tools, loading: toolsLoading } = useTools();
|
||||||
language: 'zh-CN',
|
|
||||||
enableWebSearch: true,
|
|
||||||
enableCodeExecution: false,
|
|
||||||
saveChatHistory: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleToggle = (key: keyof Settings) => {
|
// CCH 配置状态
|
||||||
setSettings((prev) => ({
|
const [cchUrl, setCchUrl] = useState('');
|
||||||
...prev,
|
const [cchApiKey, setCchApiKey] = useState('');
|
||||||
[key]: !prev[key],
|
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) => {
|
// 清除 API Key
|
||||||
setSettings((prev) => ({
|
const handleClearApiKey = async () => {
|
||||||
...prev,
|
try {
|
||||||
[key]: value,
|
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 (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
@ -58,7 +209,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="px-8 pb-8">
|
<div className="px-8 pb-8">
|
||||||
<div className="max-w-[640px] mx-auto">
|
<div className="max-w-[800px] mx-auto">
|
||||||
{/* 返回链接 */}
|
{/* 返回链接 */}
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@ -78,49 +229,240 @@ export default function SettingsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 账户设置 */}
|
{/* CCH 配置 */}
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title="账户"
|
title="CCH 服务配置"
|
||||||
description="管理您的账户信息"
|
description="配置 Claude Code Hub 服务连接"
|
||||||
>
|
>
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
label="邮箱"
|
label="CCH 服务地址"
|
||||||
description={currentUser.email}
|
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>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
label="当前计划"
|
label="API Key"
|
||||||
description={
|
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">
|
settings?.cchApiKeyConfigured
|
||||||
<Zap size={14} />
|
? '已配置 API Key'
|
||||||
{currentUser.plan === 'free' ? '免费计划' : '专业计划'}
|
: '设置访问 CCH 服务的 API Key'
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
label="使用量"
|
label="启用思考模式"
|
||||||
description="本月消息数"
|
description="让 AI 在回答前展示思考过程"
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<Toggle
|
||||||
<div className="mt-3">
|
checked={settings?.enableThinking || false}
|
||||||
<div className="h-2 bg-[var(--color-bg-tertiary)] rounded-full overflow-hidden">
|
onChange={handleThinkingToggle}
|
||||||
<div
|
disabled={saving}
|
||||||
className="h-full bg-[var(--color-primary)] rounded-full transition-all duration-500"
|
|
||||||
style={{ width: '35%' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-2 text-xs text-[var(--color-text-tertiary)]">
|
|
||||||
<span>35 / 100 条消息</span>
|
|
||||||
<span>35%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
</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="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>
|
||||||
|
<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>
|
</SettingsSection>
|
||||||
|
|
||||||
{/* 偏好设置 */}
|
{/* 偏好设置 */}
|
||||||
@ -128,31 +470,15 @@ export default function SettingsPage() {
|
|||||||
title="偏好设置"
|
title="偏好设置"
|
||||||
description="自定义您的体验"
|
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
|
<SettingsItem
|
||||||
label="主题"
|
label="主题"
|
||||||
description="选择您喜欢的主题"
|
description="选择您喜欢的主题"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
className="settings-select"
|
className="settings-select"
|
||||||
value={settings.theme}
|
value={settings?.theme || 'light'}
|
||||||
onChange={(e) => handleSelectChange('theme', e.target.value)}
|
onChange={(e) => handleThemeChange(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
<option value="system">跟随系统</option>
|
<option value="system">跟随系统</option>
|
||||||
<option value="light">浅色</option>
|
<option value="light">浅色</option>
|
||||||
@ -166,8 +492,9 @@ export default function SettingsPage() {
|
|||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
className="settings-select"
|
className="settings-select"
|
||||||
value={settings.language}
|
value={settings?.language || 'zh-CN'}
|
||||||
onChange={(e) => handleSelectChange('language', e.target.value)}
|
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
<option value="en">English</option>
|
<option value="en">English</option>
|
||||||
<option value="zh-CN">简体中文</option>
|
<option value="zh-CN">简体中文</option>
|
||||||
@ -175,26 +502,6 @@ export default function SettingsPage() {
|
|||||||
<option value="ja">日本語</option>
|
<option value="ja">日本語</option>
|
||||||
</select>
|
</select>
|
||||||
</SettingsItem>
|
</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>
|
</SettingsSection>
|
||||||
|
|
||||||
{/* 数据与隐私 */}
|
{/* 数据与隐私 */}
|
||||||
@ -207,8 +514,9 @@ export default function SettingsPage() {
|
|||||||
description="保存您的对话记录"
|
description="保存您的对话记录"
|
||||||
>
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={settings.saveChatHistory}
|
checked={settings?.saveChatHistory || false}
|
||||||
onChange={() => handleToggle('saveChatHistory')}
|
onChange={handleChatHistoryToggle}
|
||||||
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user