Compare commits

...

10 Commits

Author SHA1 Message Date
gaoziman
bc1ad129d0 chore(deps): 添加项目依赖
- clsx: CSS 类名合并工具
- lucide-react: React 图标库
2025-12-17 22:55:38 +08:00
gaoziman
9356c87180 refactor(app): 重构应用入口和首页
- layout.tsx: 更新元数据,设置中文语言,简化布局结构
- page.tsx: 重构首页为 AI 聊天界面,集成欢迎、输入框和快捷操作
2025-12-17 22:55:22 +08:00
gaoziman
01777b3786 feat(pages): 添加聊天页面和设置页面
- chat/[id]/page.tsx: 动态路由聊天页面,支持消息展示和发送
- settings/page.tsx: 设置页面,包含模型、主题、语言等配置项
2025-12-17 22:55:03 +08:00
gaoziman
d055ec7473 style(globals): 重构全局样式配置
- 定义 CSS 变量:品牌色、背景色、文字色、边框色
- 添加阴影、布局、圆角、过渡等设计令牌
- 配置全局字体和抗锯齿渲染
- 添加按钮、输入框、下拉框等通用组件样式
- 添加自定义滚动条样式
- 参考原型图 https://openclaude.me/chat 设计
2025-12-17 22:54:45 +08:00
gaoziman
c2a48986b4 feat(features): 添加核心功能组件
- ModelSelector: 模型选择下拉框组件
- ToolsDropdown: 工具管理下拉框组件
- MessageBubble: 聊天消息气泡组件
- QuickActions: 快捷操作按钮组件
- Welcome: 欢迎页问候组件
- ChatInput: 聊天输入框组件,集成模型选择和工具管理
2025-12-17 22:54:26 +08:00
gaoziman
5347bc7c2f feat(layout): 添加应用布局组件
- AppLayout: 主应用布局,包含侧边栏和主内容区
- Sidebar: 侧边栏组件,包含新建对话、聊天历史、用户信息
2025-12-17 22:54:08 +08:00
gaoziman
ee9dc67708 feat(ui): 添加基础 UI 组件
- Avatar: 用户头像组件,支持图片和文字头像
- Toggle: 开关切换组件,用于设置项
- AILogo: AI 助手 Logo 组件,品牌标识
2025-12-17 22:53:52 +08:00
gaoziman
fefacff0d1 feat(data): 添加应用模拟数据
- 添加 Claude 模型列表配置
- 添加工具列表(网络搜索、代码执行等)
- 添加聊天历史记录模拟数据
- 添加当前用户信息
- 添加快捷操作列表
- 添加根据时间返回问候语的函数
2025-12-17 22:53:34 +08:00
gaoziman
05fd8e17f5 feat(utils): 添加通用工具函数
- 添加 cn 函数用于合并 Tailwind CSS 类名
- 集成 clsx 和 tailwind-merge 库
2025-12-17 22:53:05 +08:00
gaoziman
db418d0f0d feat(types): 添加应用核心类型定义
- 定义 Model 模型类型
- 定义 Tool 工具类型
- 定义 ChatHistory 聊天记录类型
- 定义 Message 消息类型
- 定义 User 用户类型
- 定义 Settings 设置类型
- 定义 QuickAction 快捷操作类型
2025-12-17 22:52:44 +08:00
21 changed files with 1725 additions and 91 deletions

View File

@ -9,6 +9,8 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.561.0",
"next": "16.0.10", "next": "16.0.10",
"react": "19.2.1", "react": "19.2.1",
"react-dom": "19.2.1" "react-dom": "19.2.1"

View File

@ -8,6 +8,12 @@ importers:
.: .:
dependencies: dependencies:
clsx:
specifier: ^2.1.1
version: 2.1.1
lucide-react:
specifier: ^0.561.0
version: 0.561.0(react@19.2.1)
next: next:
specifier: 16.0.10 specifier: 16.0.10
version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
@ -799,6 +805,10 @@ packages:
client-only@0.0.1: client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -1450,6 +1460,11 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.561.0:
resolution: {integrity: sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -2649,6 +2664,8 @@ snapshots:
client-only@0.0.1: {} client-only@0.0.1: {}
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@ -3427,6 +3444,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.561.0(react@19.2.1):
dependencies:
react: 19.2.1
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5

151
src/app/chat/[id]/page.tsx Normal file
View 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>
);
}

View File

@ -1,26 +1,264 @@
@import "tailwindcss"; @import "tailwindcss";
/* ========================================
cchcode UI - 设计令牌
基于原型图: https://openclaude.me/chat
======================================== */
:root { :root {
--background: #ffffff; /* 品牌色 */
--foreground: #171717; --color-primary: #E06B3E;
--color-primary-hover: #D4643E;
--color-primary-light: #FEF3EF;
--color-primary-alpha: rgba(224, 107, 62, 0.1);
/* 背景色 */
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #FAFAF9;
--color-bg-tertiary: #F7F7F8;
--color-bg-hover: #F3F4F6;
/* 文字色 */
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-tertiary: #9CA3AF;
--color-text-placeholder: #A0AEC0;
/* 边框色 */
--color-border: #E5E5E5;
--color-border-light: #F0F0F0;
--color-border-focus: #E06B3E;
/* 消息气泡色 */
--color-message-user: #EDE4DE;
--color-message-assistant-bg: #F7F7F6;
--color-message-assistant-border: #E8E8E8;
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-dropdown: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-input: 0 2px 8px rgba(0, 0, 0, 0.04);
--shadow-input-focus: 0 4px 16px rgba(224, 107, 62, 0.1);
/* 布局 */
--sidebar-width: 260px;
--header-height: 56px;
--input-max-width: 900px;
/* 圆角 */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 18px;
--radius-full: 9999px;
/* 过渡 */
--transition-fast: 0.15s ease;
--transition-base: 0.2s ease;
--transition-slow: 0.3s ease;
} }
@theme inline { @theme inline {
--color-background: var(--background); /* 品牌色 */
--color-foreground: var(--foreground); --color-primary: var(--color-primary);
--font-sans: var(--font-geist-sans); --color-primary-hover: var(--color-primary-hover);
--font-mono: var(--font-geist-mono); --color-primary-light: var(--color-primary-light);
/* 背景色 */
--color-background: var(--color-bg-secondary);
--color-foreground: var(--color-text-primary);
/* 字体 */
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SF Mono', Monaco, 'Courier New', monospace;
} }
@media (prefers-color-scheme: dark) { /* ========================================
:root { 全局样式
--background: #0a0a0a; ======================================== */
--foreground: #ededed; * {
} box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
body { body {
background: var(--background); background-color: var(--color-bg-secondary);
color: var(--foreground); color: var(--color-text-primary);
font-family: Arial, Helvetica, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.5;
min-height: 100vh;
}
/* ========================================
自定义滚动条
======================================== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-tertiary);
}
/* ========================================
动画
======================================== */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn var(--transition-base);
}
.animate-slide-up {
animation: slideUp var(--transition-base);
}
.animate-slide-down {
animation: slideDown var(--transition-fast);
}
/* ========================================
响应式设计
======================================== */
@media (max-width: 768px) {
:root {
--sidebar-width: 0px;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
:root {
--sidebar-width: 220px;
}
}
/* ========================================
按钮样式
======================================== */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
background-color: var(--color-primary);
color: white;
font-size: 14px;
font-weight: 500;
border-radius: var(--radius-md);
border: none;
cursor: pointer;
transition: background-color var(--transition-fast);
}
.btn-primary:hover {
background-color: var(--color-primary-hover);
}
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
background-color: transparent;
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 500;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-ghost:hover {
background-color: var(--color-bg-hover);
color: var(--color-text-primary);
}
.btn-danger {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
background-color: #DC2626;
color: white;
font-size: 14px;
font-weight: 500;
border-radius: var(--radius-md);
border: none;
cursor: pointer;
transition: background-color var(--transition-fast);
}
.btn-danger:hover {
background-color: #B91C1C;
}
/* ========================================
设置页面选择器
======================================== */
.settings-select {
appearance: none;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 8px 32px 8px 12px;
font-size: 14px;
color: var(--color-text-primary);
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
min-width: 140px;
}
.settings-select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: var(--shadow-input-focus);
} }

View File

@ -1,20 +1,12 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "cchcode - AI 智能助手",
description: "Generated by create next app", description: "基于 Claude 的智能对话助手",
icons: {
icon: "/favicon.ico",
},
}; };
export default function RootLayout({ export default function RootLayout({
@ -23,10 +15,8 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="zh-CN">
<body <body className="antialiased">
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children} {children}
</body> </body>
</html> </html>

View File

@ -1,65 +1,70 @@
import Image from "next/image"; 'use client';
import { useState } 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';
export default function HomePage() {
const router = useRouter();
const [selectedModel, setSelectedModel] = useState<Model>(models[1]); // Sonnet as default
const [tools, setTools] = useState<Tool[]>(initialTools);
const greeting = getGreeting(currentUser.name);
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) => {
// 在实际应用中,这里会创建新对话并跳转
console.log('Sending message:', message);
router.push('/chat/1');
};
const handleQuickAction = (action: QuickAction) => {
if (action.prompt) {
handleSend(action.prompt);
}
};
export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <AppLayout>
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <div className="w-full max-w-[900px] mx-auto">
<Image {/* 欢迎区域 */}
className="dark:invert" <Welcome greeting={greeting} className="mb-8" />
src="/next.svg"
alt="Next.js logo" {/* 聊天输入框 */}
width={100} <ChatInput
height={20} models={models}
priority selectedModel={selectedModel}
onModelSelect={setSelectedModel}
tools={tools}
onToolToggle={handleToolToggle}
onEnableAllTools={handleEnableAllTools}
onSend={handleSend}
/> />
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> {/* 快捷操作 */}
To get started, edit the page.tsx file. <QuickActions
</h1> actions={quickActions}
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> onSelect={handleQuickAction}
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/> />
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div> </div>
</AppLayout>
); );
} }

317
src/app/settings/page.tsx Normal file
View 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>
);
}

View File

@ -0,0 +1,126 @@
'use client';
import { useState } from 'react';
import { Plus, Clock, ArrowUp } from 'lucide-react';
import { ModelSelector } from './ModelSelector';
import { ToolsDropdown } from './ToolsDropdown';
import { cn } from '@/lib/utils';
import type { Model, Tool } from '@/types';
interface ChatInputProps {
models: Model[];
selectedModel: Model;
onModelSelect: (model: Model) => void;
tools: Tool[];
onToolToggle: (toolId: string) => void;
onEnableAllTools: (enabled: boolean) => void;
onSend: (message: string) => void;
placeholder?: string;
className?: string;
}
export function ChatInput({
models,
selectedModel,
onModelSelect,
tools,
onToolToggle,
onEnableAllTools,
onSend,
placeholder = 'How can I help you today?',
className,
}: ChatInputProps) {
const [message, setMessage] = useState('');
const handleSend = () => {
if (message.trim()) {
onSend(message);
setMessage('');
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className={cn('w-full max-w-[var(--input-max-width)] mx-auto', className)}>
<div
className={cn(
'flex flex-col bg-white border border-[var(--color-border)] rounded-[18px] p-4 shadow-[var(--shadow-input)]',
'transition-all duration-150',
'focus-within:border-[var(--color-border-focus)] focus-within:shadow-[var(--shadow-input-focus)]'
)}
>
{/* 第一行:输入区域 */}
<div className="w-full mb-3">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="w-full border-none outline-none text-base text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]"
/>
</div>
{/* 第二行:功能按钮区域 */}
<div className="flex items-center justify-between w-full">
{/* 左侧按钮 */}
<div className="flex items-center gap-1">
{/* 添加附件 */}
<button
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
title="Add attachment"
>
<Plus size={20} />
</button>
{/* 工具下拉 */}
<ToolsDropdown
tools={tools}
onToolToggle={onToolToggle}
onEnableAllToggle={onEnableAllTools}
/>
{/* 历史记录 */}
<button
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
title="History"
>
<Clock size={20} />
</button>
</div>
{/* 右侧按钮 */}
<div className="flex items-center gap-3">
{/* 模型选择器 */}
<ModelSelector
models={models}
selectedModel={selectedModel}
onSelect={onModelSelect}
/>
{/* 发送按钮 */}
<button
onClick={handleSend}
disabled={!message.trim()}
className={cn(
'w-[38px] h-[38px] flex items-center justify-center bg-[var(--color-primary)] text-white rounded-xl transition-all duration-150',
message.trim()
? 'hover:bg-[var(--color-primary-hover)] hover:-translate-y-0.5'
: 'opacity-50 cursor-not-allowed'
)}
title="Send message"
>
<ArrowUp size={18} />
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,140 @@
'use client';
import { Copy, ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react';
import { Avatar } from '@/components/ui/Avatar';
import { AILogo } from '@/components/ui/AILogo';
import { cn } from '@/lib/utils';
import type { Message, User } from '@/types';
interface MessageBubbleProps {
message: Message;
user?: User;
}
export function MessageBubble({ message, user }: MessageBubbleProps) {
const isUser = message.role === 'user';
// 简单的 Markdown 渲染
const renderContent = (content: string) => {
// 处理代码块
const parts = content.split(/(`[^`]+`)/g);
return parts.map((part, index) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code
key={index}
className="bg-red-50 text-red-700 px-1.5 py-0.5 rounded text-sm font-mono"
>
{part.slice(1, -1)}
</code>
);
}
// 处理粗体
const boldParts = part.split(/(\*\*[^*]+\*\*)/g);
return boldParts.map((boldPart, boldIndex) => {
if (boldPart.startsWith('**') && boldPart.endsWith('**')) {
return (
<strong key={`${index}-${boldIndex}`} className="font-semibold">
{boldPart.slice(2, -2)}
</strong>
);
}
return <span key={`${index}-${boldIndex}`}>{boldPart}</span>;
});
});
};
if (isUser) {
return (
<div className="flex justify-end items-start gap-3 mb-8 animate-fade-in">
<div className="max-w-[70%]">
<div className="bg-[var(--color-message-user)] text-[var(--color-text-primary)] px-4 py-3 rounded-[18px] text-base leading-relaxed">
{message.content}
</div>
</div>
{user && <Avatar name={user.name} size="md" />}
</div>
);
}
return (
<div className="flex items-start gap-4 mb-8 animate-fade-in">
{/* AI 图标 */}
<div className="flex-shrink-0 mt-4">
<AILogo size={28} />
</div>
{/* 消息内容 */}
<div className="flex-1 max-w-full">
<div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-2xl px-6 py-5 shadow-sm">
<div className="text-base text-[var(--color-text-primary)] leading-[1.8] whitespace-pre-wrap">
{message.content.split('\n\n').map((paragraph, index) => {
// 处理列表
if (paragraph.startsWith('- ') || paragraph.startsWith('* ')) {
const items = paragraph.split('\n');
return (
<ul key={index} className="list-disc pl-5 my-3 space-y-2">
{items.map((item, i) => (
<li key={i} className="leading-relaxed">
{renderContent(item.replace(/^[-*]\s/, ''))}
</li>
))}
</ul>
);
}
// 处理有序列表
if (/^\d+\.\s/.test(paragraph)) {
const items = paragraph.split('\n');
return (
<ol key={index} className="list-decimal pl-5 my-3 space-y-2">
{items.map((item, i) => (
<li key={i} className="leading-relaxed marker:text-[var(--color-primary)] marker:font-medium">
{renderContent(item.replace(/^\d+\.\s/, ''))}
</li>
))}
</ol>
);
}
return (
<p key={index} className="mb-4 last:mb-0">
{renderContent(paragraph)}
</p>
);
})}
</div>
{/* 操作按钮 */}
<div className="flex items-center gap-1 mt-4 pt-3">
<ActionButton icon={Copy} title="Copy" />
<ActionButton icon={ThumbsUp} title="Good response" />
<ActionButton icon={ThumbsDown} title="Bad response" />
<ActionButton icon={RefreshCw} title="Regenerate" />
</div>
{/* 免责声明 */}
<div className="text-xs text-[var(--color-text-tertiary)] text-right mt-2">
cchcode can make mistakes
</div>
</div>
</div>
</div>
);
}
interface ActionButtonProps {
icon: React.ComponentType<{ size?: number }>;
title: string;
onClick?: () => void;
}
function ActionButton({ icon: Icon, title, onClick }: ActionButtonProps) {
return (
<button
onClick={onClick}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
title={title}
>
<Icon size={16} />
</button>
);
}

View File

@ -0,0 +1,80 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Model } from '@/types';
interface ModelSelectorProps {
models: Model[];
selectedModel: Model;
onSelect: (model: Model) => void;
}
export function ModelSelector({ models, selectedModel, onSelect }: ModelSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// 点击外部关闭
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className={cn('relative', isOpen && 'model-selector--open')} ref={dropdownRef}>
{/* 触发按钮 */}
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 px-2 py-1 text-sm font-medium text-[var(--color-text-secondary)] rounded-lg hover:bg-[var(--color-bg-hover)] transition-colors"
>
<span>{selectedModel.name}</span>
<ChevronDown
size={16}
className={cn('transition-transform duration-150', isOpen && 'rotate-180')}
/>
</button>
{/* 下拉菜单 */}
<div
className={cn(
'absolute bottom-full right-0 mb-2 min-w-[160px] bg-white border border-[var(--color-border)] rounded-xl shadow-lg p-1 z-50',
'transition-all duration-150',
isOpen
? 'opacity-100 visible translate-y-0'
: 'opacity-0 invisible translate-y-2'
)}
>
{models.map((model) => {
const isSelected = model.id === selectedModel.id;
return (
<button
key={model.id}
onClick={() => {
onSelect(model);
setIsOpen(false);
}}
className="flex items-center justify-between w-full px-3 py-2 rounded-lg hover:bg-[var(--color-bg-hover)] transition-colors"
>
<span
className={cn(
'font-medium text-sm',
isSelected ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-primary)]'
)}
>
{model.name}
</span>
<span className="text-xs text-[var(--color-text-tertiary)]">{model.tag}</span>
</button>
);
})}
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
'use client';
import { Code, PenTool, GraduationCap, Home, Sparkles } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { QuickAction } from '@/types';
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
Code,
PenTool,
GraduationCap,
Home,
Sparkles,
};
interface QuickActionsProps {
actions: QuickAction[];
onSelect: (action: QuickAction) => void;
className?: string;
}
export function QuickActions({ actions, onSelect, className }: QuickActionsProps) {
return (
<div className={cn('flex flex-wrap justify-center gap-3 mt-6', className)}>
{actions.map((action) => {
const Icon = iconMap[action.icon];
return (
<button
key={action.id}
onClick={() => onSelect(action)}
className={cn(
'flex items-center gap-2 px-4 py-2',
'bg-white border border-[var(--color-border)] rounded-full',
'text-sm text-[var(--color-text-secondary)]',
'hover:bg-[var(--color-bg-tertiary)] hover:text-[var(--color-text-primary)]',
'transition-all duration-150'
)}
>
{Icon && <Icon size={16} className="quick-action__icon" />}
<span>{action.label}</span>
</button>
);
})}
</div>
);
}

View File

@ -0,0 +1,105 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { Wrench, Search, Terminal, Globe, Check } from 'lucide-react';
import { Toggle } from '@/components/ui/Toggle';
import { cn } from '@/lib/utils';
import type { Tool } from '@/types';
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
Search,
Terminal,
Globe,
};
interface ToolsDropdownProps {
tools: Tool[];
onToolToggle: (toolId: string) => void;
onEnableAllToggle: (enabled: boolean) => void;
}
export function ToolsDropdown({ tools, onToolToggle, onEnableAllToggle }: ToolsDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const enabledCount = tools.filter((t) => t.enabled).length;
const allEnabled = enabledCount > 0;
// 点击外部关闭
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="relative" ref={dropdownRef}>
{/* 触发按钮 */}
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className={cn(
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors',
enabledCount > 0
? 'text-[var(--color-primary)] bg-[var(--color-primary-light)]'
: 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]'
)}
title="Tools"
>
<Wrench size={20} />
</button>
{enabledCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-4 h-4 bg-[var(--color-primary)] text-white text-[10px] font-semibold rounded-full flex items-center justify-center px-1">
{enabledCount}
</span>
)}
</div>
{/* 下拉菜单 */}
<div
className={cn(
'absolute bottom-full left-0 mb-2 min-w-[220px] bg-white border border-[var(--color-border)] rounded-xl shadow-lg p-2 z-50',
'transition-all duration-150',
isOpen
? 'opacity-100 visible translate-y-0'
: 'opacity-0 invisible translate-y-2'
)}
>
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-[var(--color-border-light)] mb-2">
<span className="text-sm font-medium text-[var(--color-text-primary)]">Enable Tools</span>
<Toggle checked={allEnabled} onChange={onEnableAllToggle} />
</div>
{/* 工具列表 */}
{tools.map((tool) => {
const Icon = iconMap[tool.icon];
return (
<button
key={tool.id}
onClick={() => onToolToggle(tool.id)}
className="flex items-center gap-3 w-full px-3 py-2 rounded-lg hover:bg-[var(--color-bg-hover)] transition-colors"
>
{Icon && <Icon size={20} className="text-[var(--color-text-tertiary)]" />}
<span className="flex-1 text-sm text-[var(--color-text-primary)] text-left">
{tool.name}
</span>
<Check
size={16}
className={cn(
'text-[var(--color-primary)] transition-opacity',
tool.enabled ? 'opacity-100' : 'opacity-0'
)}
/>
</button>
);
})}
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
'use client';
import { AILogo } from '@/components/ui/AILogo';
import { cn } from '@/lib/utils';
interface WelcomeProps {
greeting: string;
className?: string;
}
export function Welcome({ greeting, className }: WelcomeProps) {
return (
<div className={cn('text-center animate-fade-in', className)}>
{/* 装饰图标 */}
<div className="flex justify-center mb-6">
<AILogo size={48} />
</div>
{/* 问候语 */}
<h1 className="text-4xl font-normal text-[var(--color-text-primary)]">
{greeting}
</h1>
</div>
);
}

View File

@ -0,0 +1,49 @@
'use client';
import { useState, type ReactNode } from 'react';
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
import { currentUser, chatHistories } from '@/data/mock';
import { cn } from '@/lib/utils';
interface AppLayoutProps {
children: ReactNode;
showHeader?: boolean;
headerContent?: ReactNode;
}
export function AppLayout({ children, showHeader = true, headerContent }: AppLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
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 */}
{showHeader && (
<header className="h-[var(--header-height)] px-4 flex items-center justify-start">
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
{headerContent}
</header>
)}
{/* Body */}
<div className="flex-1 flex flex-col justify-center items-center p-4">
{children}
</div>
</main>
</div>
);
}

View File

@ -0,0 +1,107 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Plus, ChevronDown, PanelLeft } from 'lucide-react';
import { Avatar } from '@/components/ui/Avatar';
import { cn } from '@/lib/utils';
import type { ChatHistory, User } from '@/types';
interface SidebarProps {
user: User;
chatHistories: ChatHistory[];
isOpen?: boolean;
onToggle?: () => void;
}
export function Sidebar({ user, chatHistories, isOpen = true }: SidebarProps) {
const pathname = usePathname();
return (
<>
{/* 侧边栏 */}
<aside
className={cn(
'fixed top-0 left-0 bottom-0 z-50 bg-white border-r border-[var(--color-border-light)] flex flex-col transition-all duration-300 ease-in-out overflow-hidden',
isOpen ? 'w-[var(--sidebar-width)]' : 'w-0 border-r-0'
)}
>
{/* 品牌 Header */}
<header className="p-4">
<h1 className="text-lg font-bold text-[var(--color-text-primary)]">cchcode</h1>
</header>
{/* 新建对话按钮 */}
<div className="px-4 py-2">
<Link
href="/"
className="flex items-center gap-2 text-[var(--color-primary)] text-sm font-medium py-2 hover:opacity-80 transition-opacity"
>
<Plus size={18} />
<span>New chat</span>
</Link>
</div>
{/* Recents 标签 */}
<div className="px-4 py-2">
<h2 className="text-xs font-medium text-[var(--color-text-tertiary)] uppercase tracking-wider mb-2">
Recents
</h2>
</div>
{/* 聊天列表 */}
<nav className="flex-1 overflow-y-auto px-2 flex flex-col gap-1">
{chatHistories.map((chat) => {
const isActive = pathname === `/chat/${chat.id}`;
return (
<Link
key={chat.id}
href={`/chat/${chat.id}`}
className={cn(
'block px-3 py-2 rounded-lg text-sm cursor-pointer transition-colors truncate',
isActive
? 'bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]'
)}
>
{chat.title}
</Link>
);
})}
</nav>
{/* 用户信息 Footer */}
<footer className="p-4 border-t border-[var(--color-border-light)] mt-auto">
<div className="flex items-center gap-3 p-2 rounded-lg cursor-pointer hover:bg-[var(--color-bg-hover)] transition-colors">
<Avatar name={user.name} size="md" />
<div className="flex-1 min-w-0">
<div className="text-sm text-[var(--color-text-primary)] truncate">{user.email}</div>
<div className="text-xs text-[var(--color-text-tertiary)] capitalize">{user.plan} plan</div>
</div>
<ChevronDown size={16} className="text-[var(--color-text-tertiary)]" />
</div>
</footer>
</aside>
{/* 移动端遮罩 */}
{isOpen && (
<div
className="fixed inset-0 bg-black/20 z-40 md:hidden"
/>
)}
</>
);
}
// 侧边栏切换按钮
export function SidebarToggle({ onClick }: { onClick?: () => void }) {
return (
<button
onClick={onClick}
className="w-9 h-9 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
title="Toggle sidebar"
>
<PanelLeft size={20} />
</button>
);
}

View File

@ -0,0 +1,26 @@
'use client';
import { cn } from '@/lib/utils';
interface AILogoProps {
size?: number;
className?: string;
}
export function AILogo({ size = 48, className }: AILogoProps) {
return (
<svg
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
className={cn('text-[var(--color-primary)]', className)}
>
<path
d="M24 4L26.5 18.5L41 16L29.5 24L41 32L26.5 29.5L24 44L21.5 29.5L7 32L18.5 24L7 16L21.5 18.5L24 4Z"
fill="currentColor"
/>
</svg>
);
}

View File

@ -0,0 +1,31 @@
'use client';
import { cn } from '@/lib/utils';
interface AvatarProps {
name: string;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function Avatar({ name, size = 'md', className }: AvatarProps) {
const initial = name.charAt(0).toUpperCase();
const sizeClasses = {
sm: 'w-6 h-6 text-xs',
md: 'w-8 h-8 text-sm',
lg: 'w-10 h-10 text-base',
};
return (
<div
className={cn(
'flex items-center justify-center rounded-full bg-[var(--color-primary)] text-white font-semibold',
sizeClasses[size],
className
)}
>
{initial}
</div>
);
}

View File

@ -0,0 +1,32 @@
'use client';
import { cn } from '@/lib/utils';
interface ToggleProps {
checked: boolean;
onChange: (checked: boolean) => void;
className?: string;
}
export function Toggle({ checked, onChange, className }: ToggleProps) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={cn(
'relative w-11 h-6 rounded-full transition-colors duration-150 ease-in-out',
checked ? 'bg-[var(--color-primary)]' : 'bg-[var(--color-border)]',
className
)}
>
<span
className={cn(
'absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-sm transition-transform duration-150 ease-in-out',
checked && 'translate-x-5'
)}
/>
</button>
);
}

82
src/data/mock.ts Normal file
View File

@ -0,0 +1,82 @@
import type { Model, Tool, ChatHistory, Message, User, QuickAction } from '@/types';
// 模型列表
export const models: Model[] = [
{ id: 'haiku', name: 'Haiku 4.5', tag: 'Fast' },
{ id: 'sonnet', name: 'Sonnet 4.5', tag: 'Balanced' },
{ id: 'opus', name: 'Opus 4.5', tag: 'Powerful' },
];
// 工具列表
export const tools: Tool[] = [
{ id: 'web-search', name: 'Web Search', icon: 'Search', enabled: true },
{ id: 'code-execution', name: 'Code Execution', icon: 'Terminal', enabled: false },
{ id: 'web-fetch', name: 'Web Fetch', icon: 'Globe', enabled: true },
];
// 聊天历史
export const chatHistories: ChatHistory[] = [
{ id: '1', title: '你好', createdAt: new Date(), updatedAt: new Date() },
{ id: '2', title: 'React组件开发', createdAt: new Date(), updatedAt: new Date() },
{ id: '3', title: 'TypeScript类型问题', createdAt: new Date(), updatedAt: new Date() },
];
// 示例消息
export const sampleMessages: Message[] = [
{
id: '1',
role: 'user',
content: '你的模型id',
timestamp: new Date(),
},
{
id: '2',
role: 'assistant',
content: `我是 **Claude 4.5 Sonnet**,由 Anthropic 制作。
cchcode (openclaude.me) Anthropic ID API 使
- \`claude-4.5-sonnet\`(如果这是实际的模型版本)
-
2024 Anthropic **Claude 3.5 Sonnet** ID \`claude-3-5-sonnet-20241022\` 或类似格式)。"Claude 4.5 Sonnet" 这个名称可能是:
1. cchcode
2.
3.
使 cchcode `,
timestamp: new Date(),
},
];
// 当前用户
export const currentUser: User = {
id: '1',
email: 'cisyamgao@gmail.com',
name: 'cisyamgao',
plan: 'free',
};
// 快捷操作
export const quickActions: QuickAction[] = [
{ id: 'code', label: 'Code', icon: 'Code', prompt: '帮我写一段代码...' },
{ id: 'write', label: 'Write', icon: 'PenTool', prompt: '帮我写一篇...' },
{ id: 'learn', label: 'Learn', icon: 'GraduationCap', prompt: '帮我学习...' },
{ id: 'life', label: 'Life stuff', icon: 'Home', prompt: '帮我处理生活中的事情...' },
{ id: 'surprise', label: 'Surprise me', icon: 'Sparkles', prompt: '给我一些惊喜...' },
];
// 获取问候语
export function getGreeting(name: string): string {
const hour = new Date().getHours();
let greeting = 'Good morning';
if (hour >= 12 && hour < 18) {
greeting = 'Good afternoon';
} else if (hour >= 18) {
greeting = 'Good evening';
}
return `${greeting}, ${name}`;
}

5
src/lib/utils.ts Normal file
View File

@ -0,0 +1,5 @@
import { clsx, type ClassValue } from 'clsx';
export function cn(...inputs: ClassValue[]) {
return clsx(inputs);
}

57
src/types/index.ts Normal file
View File

@ -0,0 +1,57 @@
// 模型类型
export interface Model {
id: string;
name: string;
tag: string;
}
// 工具类型
export interface Tool {
id: string;
name: string;
icon: string;
enabled: boolean;
}
// 聊天记录类型
export interface ChatHistory {
id: string;
title: string;
createdAt: Date;
updatedAt: Date;
}
// 消息类型
export interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
// 用户类型
export interface User {
id: string;
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
avatar?: string;
}
// 设置类型
export interface Settings {
defaultModel: string;
theme: 'light' | 'dark' | 'system';
language: string;
webSearchEnabled: boolean;
codeExecutionEnabled: boolean;
chatHistoryEnabled: boolean;
}
// 快捷操作类型
export interface QuickAction {
id: string;
label: string;
icon: string;
prompt?: string;
}