feat(pages): 添加聊天页面和设置页面
- chat/[id]/page.tsx: 动态路由聊天页面,支持消息展示和发送 - settings/page.tsx: 设置页面,包含模型、主题、语言等配置项
This commit is contained in:
parent
d055ec7473
commit
01777b3786
151
src/app/chat/[id]/page.tsx
Normal file
151
src/app/chat/[id]/page.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { Share2, MoreHorizontal } 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';
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const chatId = params.id as string;
|
||||||
|
|
||||||
|
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 currentChat = chatHistories.find(c => c.id === chatId);
|
||||||
|
const chatTitle = currentChat?.title || '新对话';
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleToolToggle = (toolId: string) => {
|
||||||
|
setTools((prev) =>
|
||||||
|
prev.map((tool) =>
|
||||||
|
tool.id === toolId ? { ...tool, enabled: !tool.enabled } : tool
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnableAllTools = (enabled: boolean) => {
|
||||||
|
setTools((prev) =>
|
||||||
|
prev.map((tool) => ({ ...tool, enabled }))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 主内容区 */}
|
||||||
|
<main
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col min-h-screen transition-all duration-300',
|
||||||
|
sidebarOpen ? 'ml-[var(--sidebar-width)]' : 'ml-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 固定顶部 Header */}
|
||||||
|
<header className="h-[var(--header-height)] px-4 flex items-center justify-between border-b border-[var(--color-border)] bg-white sticky top-0 z-10">
|
||||||
|
<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}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<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="分享对话"
|
||||||
|
>
|
||||||
|
<Share2 size={16} />
|
||||||
|
<span>分享</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-8 h-8 flex items-center justify-center text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] rounded-lg transition-colors"
|
||||||
|
title="更多选项"
|
||||||
|
>
|
||||||
|
<MoreHorizontal size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 消息列表区域 */}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 固定底部输入框 */}
|
||||||
|
<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">
|
||||||
|
<ChatInput
|
||||||
|
models={models}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
onModelSelect={setSelectedModel}
|
||||||
|
tools={tools}
|
||||||
|
onToolToggle={handleToolToggle}
|
||||||
|
onEnableAllTools={handleEnableAllTools}
|
||||||
|
onSend={handleSend}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
317
src/app/settings/page.tsx
Normal file
317
src/app/settings/page.tsx
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArrowLeft, Zap, Download } 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';
|
||||||
|
|
||||||
|
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 handleToggle = (key: keyof Settings) => {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: !prev[key],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (key: keyof Settings, value: string) => {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
<Sidebar
|
||||||
|
user={currentUser}
|
||||||
|
chatHistories={chatHistories}
|
||||||
|
isOpen={sidebarOpen}
|
||||||
|
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 主内容区 */}
|
||||||
|
<main
|
||||||
|
className={cn(
|
||||||
|
'flex-1 min-h-screen transition-all duration-300',
|
||||||
|
sidebarOpen ? 'ml-[var(--sidebar-width)]' : 'ml-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<header className="h-[var(--header-height)] px-4 flex items-center">
|
||||||
|
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="px-8 pb-8">
|
||||||
|
<div className="max-w-[640px] mx-auto">
|
||||||
|
{/* 返回链接 */}
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center gap-2 text-[var(--color-text-secondary)] text-sm mb-4 hover:text-[var(--color-text-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
<span>返回聊天</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-semibold text-[var(--color-text-primary)] mb-2">
|
||||||
|
设置
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||||
|
管理您的账户设置和偏好
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 账户设置 */}
|
||||||
|
<SettingsSection
|
||||||
|
title="账户"
|
||||||
|
description="管理您的账户信息"
|
||||||
|
>
|
||||||
|
<SettingsItem
|
||||||
|
label="邮箱"
|
||||||
|
description={currentUser.email}
|
||||||
|
>
|
||||||
|
<button className="btn-ghost">更改</button>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
label="当前计划"
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button className="btn-primary">升级</button>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
label="使用量"
|
||||||
|
description="本月消息数"
|
||||||
|
>
|
||||||
|
<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%' }}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{/* 偏好设置 */}
|
||||||
|
<SettingsSection
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
<option value="system">跟随系统</option>
|
||||||
|
<option value="light">浅色</option>
|
||||||
|
<option value="dark">深色</option>
|
||||||
|
</select>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
label="语言"
|
||||||
|
description="选择您的首选语言"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className="settings-select"
|
||||||
|
value={settings.language}
|
||||||
|
onChange={(e) => handleSelectChange('language', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="zh-CN">简体中文</option>
|
||||||
|
<option value="zh-TW">繁體中文</option>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 数据与隐私 */}
|
||||||
|
<SettingsSection
|
||||||
|
title="数据与隐私"
|
||||||
|
description="管理您的数据和隐私设置"
|
||||||
|
>
|
||||||
|
<SettingsItem
|
||||||
|
label="聊天历史"
|
||||||
|
description="保存您的对话记录"
|
||||||
|
>
|
||||||
|
<Toggle
|
||||||
|
checked={settings.saveChatHistory}
|
||||||
|
onChange={() => handleToggle('saveChatHistory')}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
label="导出数据"
|
||||||
|
description="下载所有聊天历史"
|
||||||
|
>
|
||||||
|
<button className="btn-ghost inline-flex items-center gap-2">
|
||||||
|
<Download size={16} />
|
||||||
|
导出
|
||||||
|
</button>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
label="清除所有聊天"
|
||||||
|
description="删除所有对话历史"
|
||||||
|
>
|
||||||
|
<button className="btn-ghost text-red-600 hover:text-red-700">
|
||||||
|
清除
|
||||||
|
</button>
|
||||||
|
</SettingsItem>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{/* 危险区域 */}
|
||||||
|
<SettingsSection
|
||||||
|
title="危险区域"
|
||||||
|
description="不可逆操作"
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
<SettingsItem
|
||||||
|
label="删除账户"
|
||||||
|
description="永久删除您的账户和所有数据"
|
||||||
|
>
|
||||||
|
<button className="btn-danger">删除账户</button>
|
||||||
|
</SettingsItem>
|
||||||
|
</SettingsSection>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置区域组件
|
||||||
|
interface SettingsSectionProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSection({ title, description, children, variant = 'default' }: SettingsSectionProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
'bg-white border rounded-xl mb-6 overflow-hidden',
|
||||||
|
variant === 'danger' ? 'border-red-200' : 'border-[var(--color-border)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'px-5 py-4 border-b',
|
||||||
|
variant === 'danger'
|
||||||
|
? 'bg-red-50 border-red-200'
|
||||||
|
: 'border-[var(--color-border-light)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className={cn(
|
||||||
|
'text-base font-semibold',
|
||||||
|
variant === 'danger' ? 'text-red-600' : 'text-[var(--color-text-primary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-[var(--color-text-secondary)] mt-1">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置项组件
|
||||||
|
interface SettingsItemProps {
|
||||||
|
label: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsItem({ label, description, children }: SettingsItemProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center px-5 py-4 border-b border-[var(--color-border-light)] last:border-b-0 hover:bg-[var(--color-bg-tertiary)] transition-colors">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-1">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--color-text-tertiary)]">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user