├── .npmrc ├── bun.lockb ├── .vscode └── settings.json ├── public └── favicon.ico ├── postcss.config.js ├── types └── index.ts ├── next.config.js ├── config ├── site.ts └── fonts.ts ├── .eslintignore ├── pages └── api │ └── announcement.ts ├── styles └── globals.css ├── app ├── page.tsx ├── error.tsx ├── providers.tsx └── layout.tsx ├── content └── markdown │ ├── styles.tsx │ └── announcement.md ├── .gitignore ├── components ├── login-button.tsx ├── user-button.tsx ├── background.tsx ├── primitives.ts ├── navbar.tsx ├── sidebar.tsx ├── login-modal.tsx ├── model-select.tsx ├── file-upload-button.tsx ├── theme-switch.tsx ├── chat-input-1.tsx ├── announcement.tsx ├── icon.tsx └── user-popover.tsx ├── tsconfig.json ├── LICENSE ├── server └── api │ └── announcement.ts ├── tailwind.config.js ├── README.md ├── package.json └── .eslintrc.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/EchoAI/main/bun.lockb -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/EchoAI/main/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig; 2 | 3 | export const siteConfig = { 4 | name: "Echo AI", 5 | description: "Make easiest AI.", 6 | navItems: [ 7 | 8 | ], 9 | navMenuItems: [ 10 | 11 | ], 12 | links: { 13 | 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .now/* 2 | *.css 3 | .changeset 4 | dist 5 | esm/* 6 | public/* 7 | tests/* 8 | scripts/* 9 | *.config.js 10 | .DS_Store 11 | node_modules 12 | coverage 13 | .next 14 | build 15 | !.commitlintrc.cjs 16 | !.lintstagedrc.cjs 17 | !jest.config.js 18 | !plopfile.js 19 | !react-shim.js 20 | !tsup.config.ts -------------------------------------------------------------------------------- /config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google"; 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-sans", 6 | }); 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ["latin"], 10 | variable: "--font-mono", 11 | }); 12 | -------------------------------------------------------------------------------- /pages/api/announcement.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { GET } from '@/server/api/announcement'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method === 'GET') { 6 | return GET(req, res); 7 | } 8 | 9 | return res.status(405).json({ error: 'Method not allowed' }); 10 | } -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | /* 渐变色背景浅色模式默认值 */ 7 | --gradient-from: rgba(0, 0, 0, 0.08); 8 | --gradient-to: rgba(0, 0, 0, 0.02); 9 | } 10 | 11 | /* 渐变色背景深色模式的值 */ 12 | :root[class~="dark"] { 13 | --gradient-from: rgba(255, 255, 255, 0.15); 14 | --gradient-to: rgba(255, 255, 255, 0.06); 15 | } 16 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChatInput1 } from "@/components/chat-input-1"; 2 | import { Background } from "@/components/background"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | 9 | {/* 输入框 */} 10 |
11 | 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /content/markdown/styles.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | import remarkGfm from 'remark-gfm'; 3 | 4 | interface MarkdownContentProps { 5 | content: string; 6 | className?: string; 7 | } 8 | 9 | export const MarkdownContent = ({ content, className = "" }: MarkdownContentProps) => { 10 | return ( 11 |
12 | 13 | {content} 14 | 15 |
16 | ); 17 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | /* eslint-disable no-console */ 15 | console.error(error); 16 | }, [error]); 17 | 18 | return ( 19 |
20 |

Something went wrong!

21 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/login-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@nextui-org/button"; 4 | import { UserIcon } from "@heroicons/react/24/solid"; 5 | import { useDisclosure } from "@nextui-org/modal"; 6 | import { LoginModal } from "./login-modal"; 7 | 8 | export const LoginButton = () => { 9 | const {isOpen, onOpen, onOpenChange} = useDisclosure(); 10 | 11 | return ( 12 | <> 13 | 21 | 22 | 23 | 24 | ); 25 | }; -------------------------------------------------------------------------------- /components/user-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Avatar } from "@nextui-org/avatar"; 4 | import { UserIcon as UserRegularIcon } from "@heroicons/react/24/outline"; 5 | import { UserPopover } from "./user-popover"; 6 | 7 | export const UserButton = () => { 8 | // TODO: 这里应该从用户认证状态中获取用户信息 9 | const user = { 10 | avatar: "https://api.dicebear.com/7.x/adventurer/svg?seed=Felix&backgroundColor=ffdfbf,ffd5dc,c0aede&radius=50", 11 | }; 12 | 13 | return ( 14 | 15 | 21 | } 22 | /> 23 | 24 | ); 25 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ThemeProviderProps } from "next-themes"; 4 | 5 | import * as React from "react"; 6 | import { NextUIProvider } from "@nextui-org/system"; 7 | import { useRouter } from "next/navigation"; 8 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 9 | 10 | export interface ProvidersProps { 11 | children: React.ReactNode; 12 | themeProps?: ThemeProviderProps; 13 | } 14 | 15 | declare module "@react-types/shared" { 16 | interface RouterConfig { 17 | routerOptions: NonNullable< 18 | Parameters["push"]>[1] 19 | >; 20 | } 21 | } 22 | 23 | export function Providers({ children, themeProps }: ProvidersProps) { 24 | const router = useRouter(); 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/background.tsx: -------------------------------------------------------------------------------- 1 | export const Background = () => { 2 | return ( 3 | <> 4 |
8 | 有咩帮到你? 25 | 26 |
27 | 28 | ); 29 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Next UI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/api/announcement.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | // 从 Markdown 内容中提取第一个标题 6 | function extractTitle(content: string): string { 7 | // 匹配第一个 # 开头的标题行 8 | const titleMatch = content.match(/^#\s+(.+)$/m); 9 | return titleMatch ? titleMatch[1] : '系统公告'; 10 | } 11 | 12 | interface AnnouncementResponse { 13 | title: string; 14 | content: string; 15 | } 16 | 17 | interface ErrorResponse { 18 | error: string; 19 | } 20 | 21 | export async function GET( 22 | req: NextApiRequest, 23 | res: NextApiResponse 24 | ) { 25 | try { 26 | // 读取 Markdown 文件 27 | const filePath = path.join(process.cwd(), 'content/markdown/announcement.md'); 28 | const content = fs.readFileSync(filePath, 'utf-8'); 29 | 30 | // 从内容中提取标题 31 | const title = extractTitle(content); 32 | 33 | return res.status(200).json({ 34 | title, 35 | content, 36 | }); 37 | } catch (error) { 38 | console.error('Failed to read announcement:', error); 39 | return res.status(500).json({ error: 'Failed to load announcement' }); 40 | } 41 | } -------------------------------------------------------------------------------- /content/markdown/announcement.md: -------------------------------------------------------------------------------- 1 | # 🌟 系统公告 v3.0 2 | 3 | 亲爱的用户: 4 | 5 | 感谢您一直以来对我们的支持与信任!我们很高兴地宣布,系统已完成重大升级,带来诸多激动人心的新功能与优化。 6 | 7 | # 🎉 重磅功能更新 8 | 9 | ### 1. 全新AI助手升级 10 | 11 | - 支持更多场景的智能对话 12 | - 新增文档分析与数据可视化能力 13 | - 支持多语言实时翻译 14 | - 集成GPT-4模型,响应更精准 15 | 16 | ### 2. 界面与交互优化 17 | 18 | - 全新的深色模式主题 19 | - 自定义主题配色 20 | - 响应式布局优化 21 | - 新增多种动画效果 22 | 23 | ### 3. 协作功能增强 24 | 25 | - 实时多人协作编辑 26 | - 团队空间管理 27 | - 项目进度追踪 28 | - 智能任务分配 29 | 30 | # ⚡ 性能优化 31 | 32 | - 页面加载速度提升 **50%** 33 | - 数据同步延迟降低至 **300ms** 34 | - 内存占用减少 **40%** 35 | - 移动端性能显著提升 36 | 37 | # ⚠️ 系统维护计划 38 | 39 | > **维护时间**:每周日 02:00-04:00 40 | > 41 | > **注意事项**: 42 | > 1. 维护期间服务将暂时中断 43 | > 2. 请提前保存重要数据 44 | > 3. 可通过系统通知查看具体维护进度 45 | 46 | # 📱 版本详情 47 | 48 | ```yaml 49 | 版本号: v3.0.0 50 | 发布日期: 2024-02-20 51 | 兼容性: iOS 13+ / Android 8.0+ 52 | 大小: 45MB 53 | ``` 54 | 55 | # 🛠️ 问题修复 56 | 57 | 1. 修复了在特定情况下数据同步失败的问题 58 | 2. 解决了深色模式下部分界面显示异常的问题 59 | 3. 优化了移动端键盘弹出时的界面适配 60 | 4. 修复了通知推送延迟的问题 61 | 62 | # 💡 使用建议 63 | 64 | - 建议所有用户及时更新到最新版本 65 | - 如遇问题请通过以下方式联系我们: 66 | * 邮箱:support@example.com 67 | * 电话:400-888-8888 68 | * 在线客服:7x24小时 69 | 70 | # 🔮 未来规划 71 | 72 | 1. **Q2 2024**: AR/VR功能支持 73 | 2. **Q3 2024**: 区块链集成 74 | 3. **Q4 2024**: 全新数据分析平台 75 | 76 | --- 77 | 78 | *感谢您的耐心阅读!我们将继续努力为您提供更好的服务。* -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const {nextui} = require("@nextui-org/theme"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | './content/**/*.{js,ts,jsx,tsx,mdx,md}', 10 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', 11 | ], 12 | theme: { 13 | extend: { 14 | fontFamily: { 15 | sans: ["var(--font-sans)"], 16 | mono: ["var(--font-mono)"], 17 | }, 18 | }, 19 | }, 20 | darkMode: "class", 21 | plugins: [ 22 | nextui({ 23 | themes: { 24 | light: { 25 | layout: { 26 | radius: { 27 | "xl": "0.75rem", // 12px 28 | "2xl": "1rem", // 16px 29 | "3xl": "1.5rem", // 24px 30 | }, 31 | } 32 | }, 33 | dark: { 34 | layout: { 35 | radius: { 36 | "xl": "0.75rem", // 12px 37 | "2xl": "1rem", // 16px 38 | "3xl": "1.5rem", // 24px 39 | } 40 | } 41 | } 42 | } 43 | }), 44 | require('@tailwindcss/typography'), 45 | ], 46 | } 47 | -------------------------------------------------------------------------------- /components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from "tailwind-variants"; 2 | 3 | export const title = tv({ 4 | base: "tracking-tight inline font-semibold", 5 | variants: { 6 | color: { 7 | violet: "from-[#FF1CF7] to-[#b249f8]", 8 | yellow: "from-[#FF705B] to-[#FFB457]", 9 | blue: "from-[#5EA2EF] to-[#0072F5]", 10 | cyan: "from-[#00b7fa] to-[#01cfea]", 11 | green: "from-[#6FEE8D] to-[#17c964]", 12 | pink: "from-[#FF72E1] to-[#F54C7A]", 13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", 14 | }, 15 | size: { 16 | sm: "text-3xl lg:text-4xl", 17 | md: "text-[2.3rem] lg:text-5xl leading-9", 18 | lg: "text-4xl lg:text-6xl", 19 | }, 20 | fullWidth: { 21 | true: "w-full block", 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: "md", 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | "violet", 31 | "yellow", 32 | "blue", 33 | "cyan", 34 | "green", 35 | "pink", 36 | "foreground", 37 | ], 38 | class: "bg-clip-text text-transparent bg-gradient-to-b", 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", 45 | variants: { 46 | fullWidth: { 47 | true: "!w-full", 48 | }, 49 | }, 50 | defaultVariants: { 51 | fullWidth: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Navbar as NextUINavbar, 3 | NavbarContent, 4 | NavbarBrand, 5 | } from "@nextui-org/navbar"; 6 | import NextLink from "next/link"; 7 | import { LoginButton } from "@/components/login-button"; 8 | import { UserButton } from "@/components/user-button"; 9 | import { Icon } from "@/components/icon"; 10 | export const Navbar = () => { 11 | // const searchInput = ( 12 | // 20 | // K 21 | // 22 | // } 23 | // labelPlacement="outside" 24 | // placeholder="Search..." 25 | // startContent={ 26 | // 27 | // } 28 | // type="search" 29 | // /> 30 | // ); 31 | 32 | // TODO: 这里应该从用户认证状态中获取登录状态 33 | const isLoggedIn = true; 34 | 35 | return ( 36 | 37 | 38 | {/*logo*/} 39 | 40 | 41 | 42 | 43 |

ECHO

44 |
45 |
46 |
47 | 48 | {/* 根据登录状态显示不同按钮 */} 49 | 50 | {isLoggedIn ? : } 51 | 52 | 53 | 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EchoAI 2 | 3 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/yourusername/echoai) 4 | 5 | EchoAI 是一个使用 Next.js 15 构建的精美 AI 对话界面演示项目,致力于打造流畅优雅的用户体验。采用 Bun 作为高性能运行时,带来极致的开发体验。 6 | 7 | ## 🌟 主要特性 8 | 9 | - 💅 精致设计 10 | - 现代简约的界面风格 11 | - 流畅的过渡动画效果 12 | - 细腻的交互反馈 13 | - 优雅的加载状态展示 14 | 15 | - 🎨 视觉体验 16 | - 精心设计的配色方案 17 | - 平滑的渐变效果 18 | - 细致的阴影层次 19 | - 灵动的微交互设计 20 | 21 | - 🌓 主题切换 22 | - 深浅色模式无缝切换 23 | - 自动适应系统主题 24 | - 主题切换过渡动画 25 | - 定制化的主题色彩 26 | 27 | - 📱 响应式布局 28 | - 完美适配各种设备尺寸 29 | - 智能的布局调整 30 | - 触控优化的交互体验 31 | - 流畅的屏幕适配 32 | 33 | ## 🎯 界面特色 34 | 35 | - 💬 对话界面 36 | - 平滑的消息滚动动画 37 | - 打字机效果的文本显示 38 | - 优雅的加载状态指示 39 | - 丝滑的列表过渡效果 40 | 41 | - ✨ 动画效果 42 | - 基于 Framer Motion 的流畅动画 43 | - 自然的页面切换过渡 44 | - 精致的悬浮反馈 45 | - 连贯的状态变化动画 46 | 47 | ## 🛠️ 技术栈 48 | 49 | - [Next.js 15](https://nextjs.org/) - React 框架 50 | - [NextUI v2](https://nextui.org/) - 现代化 UI 组件库 51 | - [Tailwind CSS](https://tailwindcss.com/) - 原子化 CSS 框架 52 | - [TypeScript](https://www.typescriptlang.org/) - 类型安全 53 | - [Framer Motion](https://www.framer.com/motion/) - 专业级动画库 54 | - [next-themes](https://github.com/pacocoursey/next-themes) - 主题切换 55 | 56 | ## 🚀 快速开始 57 | 58 | 0. 前置要求 59 | 60 | ```bash 61 | # 安装 Bun 62 | curl -fsSL https://bun.sh/install | bash 63 | ``` 64 | 65 | 1. 克隆项目 66 | 67 | ```bash 68 | git clone https://github.com/yourusername/echoai.git 69 | cd echoai 70 | ``` 71 | 72 | 2. 安装依赖 73 | 74 | ```bash 75 | bun install # 比 npm/pnpm 快 20+ 倍 76 | ``` 77 | 78 | 3. 启动开发服务器 79 | 80 | ```bash 81 | bun dev # 毫秒级的启动速度 82 | ``` 83 | 84 | 4. 在浏览器中打开 [http://localhost:3000](http://localhost:3000) 85 | 86 | ## 🌩️ 部署 87 | 88 | ### Vercel 部署 89 | 90 | 点击上方的 "Deploy with Vercel" 按钮,即可一键部署到 Vercel 平台。 91 | 92 | ### 手动构建 93 | 94 | ```bash 95 | bun run build # 构建生产版本 96 | bun run start # 启动生产服务器 97 | ``` 98 | 99 | ## 📝 项目目的 100 | 101 | 这是一个注重用户体验的前端演示项目,主要展示: 102 | - 精美的 AI 对话界面设计与实现 103 | - 丰富的动画效果与交互体验 104 | - Next.js 15 与 NextUI 的最佳实践 105 | - 响应式设计与主题切换的完美实现 106 | 107 | ## 📄 许可证 108 | 109 | 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 110 | 111 | ## 📧 联系方式 112 | 113 | 如有问题或建议,欢迎联系我们! 114 | 115 | - 项目主页:[GitHub](https://github.com/yourusername/echoai) 116 | - 问题反馈:[Issues](https://github.com/yourusername/echoai/issues) 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app-template", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^2.2.0", 13 | "@nextui-org/avatar": "^2.2.4", 14 | "@nextui-org/button": "2.2.3", 15 | "@nextui-org/card": "^2.2.7", 16 | "@nextui-org/chip": "^2.2.4", 17 | "@nextui-org/code": "2.2.3", 18 | "@nextui-org/form": "^2.1.6", 19 | "@nextui-org/input": "2.4.3", 20 | "@nextui-org/kbd": "2.2.3", 21 | "@nextui-org/link": "2.2.3", 22 | "@nextui-org/listbox": "2.3.3", 23 | "@nextui-org/modal": "^2.2.5", 24 | "@nextui-org/navbar": "2.2.3", 25 | "@nextui-org/popover": "^2.3.7", 26 | "@nextui-org/scroll-shadow": "^2.3.3", 27 | "@nextui-org/select": "^2.4.7", 28 | "@nextui-org/snippet": "2.2.4", 29 | "@nextui-org/switch": "2.2.3", 30 | "@nextui-org/system": "2.4.3", 31 | "@nextui-org/tabs": "^2.2.5", 32 | "@nextui-org/theme": "2.4.1", 33 | "@react-aria/ssr": "3.9.7", 34 | "@react-aria/visually-hidden": "3.8.18", 35 | "clsx": "2.1.1", 36 | "framer-motion": "11.13.1", 37 | "intl-messageformat": "^10.5.0", 38 | "next": "15.0.4", 39 | "next-themes": "^0.4.4", 40 | "react": "18.3.1", 41 | "react-dom": "18.3.1", 42 | "react-icons": "^5.4.0", 43 | "react-markdown": "^9.0.1", 44 | "remark-gfm": "^4.0.0" 45 | }, 46 | "devDependencies": { 47 | "@next/eslint-plugin-next": "15.0.4", 48 | "@react-types/shared": "3.25.0", 49 | "@tailwindcss/typography": "^0.5.15", 50 | "@types/node": "20.5.7", 51 | "@types/react": "18.3.3", 52 | "@types/react-dom": "18.3.0", 53 | "@typescript-eslint/eslint-plugin": "8.11.0", 54 | "@typescript-eslint/parser": "8.11.0", 55 | "autoprefixer": "10.4.19", 56 | "eslint": "^8.57.0", 57 | "eslint-config-next": "15.0.4", 58 | "eslint-config-prettier": "9.1.0", 59 | "eslint-plugin-import": "^2.26.0", 60 | "eslint-plugin-jsx-a11y": "^6.4.1", 61 | "eslint-plugin-node": "^11.1.0", 62 | "eslint-plugin-prettier": "5.2.1", 63 | "eslint-plugin-react": "^7.23.2", 64 | "eslint-plugin-react-hooks": "^4.6.0", 65 | "eslint-plugin-unused-imports": "4.1.4", 66 | "postcss": "8.4.49", 67 | "prettier": "3.3.3", 68 | "raw-loader": "^4.0.2", 69 | "tailwind-variants": "0.1.20", 70 | "tailwindcss": "3.4.16", 71 | "typescript": "5.6.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Button } from "@nextui-org/button"; 5 | import { ChevronRightIcon } from "@nextui-org/shared-icons"; 6 | import { motion, AnimatePresence } from "framer-motion"; 7 | 8 | export const Sidebar = () => { 9 | const [isOpen, setIsOpen] = useState(false); 10 | const [isMobile, setIsMobile] = useState(false); 11 | 12 | useEffect(() => { 13 | // 初始化检查 14 | setIsMobile(window.innerWidth < 768); 15 | 16 | // 添加窗口大小变化监听 17 | const handleResize = () => { 18 | setIsMobile(window.innerWidth < 768); 19 | }; 20 | 21 | window.addEventListener("resize", handleResize); 22 | 23 | // 清理监听器 24 | return () => window.removeEventListener("resize", handleResize); 25 | }, []); 26 | 27 | return ( 28 | <> 29 | {/* 模糊背景 */} 30 | 31 | {isOpen && ( 32 | setIsOpen(false)} 39 | /> 40 | )} 41 | 42 | 43 | {/* 主容器 */} 44 | 51 | {/* 侧边栏内容 */} 52 |
53 | 54 | {/* 展开按钮 */} 55 | 82 |
83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc.json", 3 | "env": { 4 | "browser": false, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "plugin:react/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:jsx-a11y/recommended", 13 | "plugin:@next/next/recommended" 14 | ], 15 | "plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": 12, 22 | "sourceType": "module" 23 | }, 24 | "settings": { 25 | "react": { 26 | "version": "detect" 27 | } 28 | }, 29 | "rules": { 30 | "no-console": "warn", 31 | "react/prop-types": "off", 32 | "react/jsx-uses-react": "off", 33 | "react/react-in-jsx-scope": "off", 34 | "react-hooks/exhaustive-deps": "off", 35 | "jsx-a11y/click-events-have-key-events": "warn", 36 | "jsx-a11y/interactive-supports-focus": "warn", 37 | "prettier/prettier": "warn", 38 | "no-unused-vars": "off", 39 | "unused-imports/no-unused-vars": "off", 40 | "unused-imports/no-unused-imports": "warn", 41 | "@typescript-eslint/no-unused-vars": [ 42 | "warn", 43 | { 44 | "args": "after-used", 45 | "ignoreRestSiblings": false, 46 | "argsIgnorePattern": "^_.*?$" 47 | } 48 | ], 49 | "import/order": [ 50 | "warn", 51 | { 52 | "groups": [ 53 | "type", 54 | "builtin", 55 | "object", 56 | "external", 57 | "internal", 58 | "parent", 59 | "sibling", 60 | "index" 61 | ], 62 | "pathGroups": [ 63 | { 64 | "pattern": "~/**", 65 | "group": "external", 66 | "position": "after" 67 | } 68 | ], 69 | "newlines-between": "always" 70 | } 71 | ], 72 | "react/self-closing-comp": "warn", 73 | "react/jsx-sort-props": [ 74 | "warn", 75 | { 76 | "callbacksLast": true, 77 | "shorthandFirst": true, 78 | "noSortAlphabetically": false, 79 | "reservedFirst": true 80 | } 81 | ], 82 | "padding-line-between-statements": [ 83 | "warn", 84 | {"blankLine": "always", "prev": "*", "next": "return"}, 85 | {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"}, 86 | { 87 | "blankLine": "any", 88 | "prev": ["const", "let", "var"], 89 | "next": ["const", "let", "var"] 90 | } 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { Metadata, Viewport } from "next"; 3 | import { Link } from "@nextui-org/link"; 4 | import clsx from "clsx"; 5 | import { Providers } from "./providers"; 6 | import { siteConfig } from "@/config/site"; 7 | import { fontSans } from "@/config/fonts"; 8 | import { Navbar } from "@/components/navbar"; 9 | import { Sidebar } from "@/components/sidebar"; 10 | import { Noto_Sans_SC } from 'next/font/google'; 11 | 12 | const notoSansSC = Noto_Sans_SC({ 13 | subsets: ['latin'], 14 | weight: ['100', '300', '400', '500', '700', '900'], 15 | display: 'swap', 16 | variable: '--font-noto-sans', 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: { 21 | default: siteConfig.name, 22 | template: `%s - ${siteConfig.name}`, 23 | }, 24 | description: siteConfig.description, 25 | icons: { 26 | icon: "/favicon.ico", 27 | }, 28 | }; 29 | 30 | export const viewport: Viewport = { 31 | themeColor: [ 32 | { media: "(prefers-color-scheme: light)", color: "white" }, 33 | { media: "(prefers-color-scheme: dark)", color: "black" }, 34 | ], 35 | }; 36 | 37 | export default function RootLayout({ 38 | children, 39 | }: { 40 | children: React.ReactNode; 41 | }) { 42 | return ( 43 | 44 | 45 | 46 | 53 | 54 | {/* 里面的都可以切换主题 */} 55 | 56 | 57 | {/* 根布局 固定视口高度,不允许滚动,flex 用于侧边栏和右侧界面水平布局 */} 58 |
59 | 60 | {/* 侧边栏 */} 61 | 62 | 63 | {/* 右侧界面 flex-col 用于导航栏,主内容,页脚的垂直布局*/} 64 |
65 | 66 | {/* 导航栏 */} 67 | 68 | 69 | {/* 主内容区域 - 允许内容滚动 */} 70 |
71 | {children} 72 |
73 | 74 | {/* 页脚 - 固定在底部 */} 75 |
76 | 82 | Powered by 83 |

QuQi AI

84 | 85 |
86 | 87 |
88 |
89 |
90 | 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /components/login-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, ModalContent, ModalBody, ModalFooter} from "@nextui-org/modal"; 2 | import {Tabs, Tab} from "@nextui-org/tabs"; 3 | import { Button } from "@nextui-org/button"; 4 | import { Input } from "@nextui-org/input"; 5 | import { Link } from "@nextui-org/link"; 6 | import { useState } from "react"; 7 | import { ArrowRightStartOnRectangleIcon, UserPlusIcon, RocketLaunchIcon, SparklesIcon } from "@heroicons/react/24/outline"; 8 | 9 | interface LoginModalProps { 10 | isOpen: boolean; // 控制弹窗是否显示的状态 11 | onOpenChange: (isOpen: boolean) => void; // 状态改变时的回调函数 12 | } 13 | 14 | export const LoginModal = ({ isOpen, onOpenChange }: LoginModalProps) => { 15 | const [selected, setSelected] = useState("login"); 16 | 17 | return ( 18 | 25 | 26 | 27 | {/* nextui 自带的 onClose 方法 */} 28 | {(onClose) => ( 29 | <> 30 | 31 | 32 | setSelected(key as string)} 38 | > 39 | {/* 登录 */} 40 | 44 | 45 | 登录 46 | 47 | } 48 | > 49 |
50 | 51 | 52 |

53 | 没账号吗?{" "} 54 | setSelected("sign-up")}>去注册 55 |

56 | 63 |
64 |
65 | 66 | {/* 注册 */} 67 | 71 | 72 | 注册 73 | 74 | } 75 | > 76 |
77 | 78 | 79 | 80 | 87 |
88 |
89 |
90 |
91 | 92 | )} 93 |
94 |
95 | ); 96 | }; -------------------------------------------------------------------------------- /components/model-select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useState, useCallback } from "react"; 3 | import { Popover, PopoverTrigger, PopoverContent } from "@nextui-org/popover"; 4 | import { Button } from "@nextui-org/button"; 5 | import { CheckIcon } from "@heroicons/react/24/outline"; 6 | import { Icon } from "./icon"; 7 | import dynamic from 'next/dynamic'; 8 | // 定义模型类型 9 | type ModelType = { 10 | key: string; 11 | label: string; 12 | icon: 'gpt' | 'claude' | 'gemini'; 13 | description: string; 14 | }; 15 | 16 | // 整合所有模型信息到 modelList 17 | export const modelList: ModelType[] = [ 18 | { 19 | key: "GPT", 20 | label: "GPT-PLUS", 21 | icon: 'gpt', 22 | description: "OpenAI 出品的模型,擅长对话和创作" 23 | }, 24 | { 25 | key: "Claude", 26 | label: "Claude-PLUS", 27 | icon: 'claude', 28 | description: "Anthropic 出品的模型,擅长分析和推理" 29 | }, 30 | { 31 | key: "Gemini", 32 | label: "Gemini-PLUS", 33 | icon: 'gemini', 34 | description: "Google 出品的模型,擅长多模态理解" 35 | } 36 | ]; 37 | 38 | const LoginModal = dynamic(() => import('./login-modal').then(mod => mod.LoginModal), { 39 | loading: () =>
加载中...
40 | }); 41 | 42 | export const ModelSelect = () => { 43 | const [selectedModel, setSelectedModel] = useState(modelList[0]); 44 | const [isOpen, setIsOpen] = useState(false); 45 | 46 | const handleModelSelect = useCallback((item: ModelType) => { 47 | setSelectedModel(item); 48 | requestAnimationFrame(() => { 49 | setIsOpen(false); 50 | }); 51 | }, []); 52 | 53 | return ( 54 | 60 | 61 | 79 | 80 | 81 | 82 |
83 | {modelList.map((item) => ( 84 | 104 | ))} 105 |
106 |
107 |
108 | ); 109 | }; -------------------------------------------------------------------------------- /components/file-upload-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Popover, PopoverTrigger, PopoverContent } from "@nextui-org/popover"; 4 | import { Button } from "@nextui-org/button"; 5 | import { Card, CardHeader, CardBody, CardFooter } from "@nextui-org/card"; 6 | import { BsFileEarmarkPlusFill } from "react-icons/bs"; 7 | 8 | import { 9 | ArrowUpTrayIcon, // 上传 10 | DocumentTextIcon, // PDF 11 | PresentationChartBarIcon, // PPT 12 | DocumentIcon, // Word 13 | PhotoIcon, // 图片 14 | FolderPlusIcon, // 文件夹 15 | } from "@heroicons/react/24/outline"; 16 | import { useState } from "react"; 17 | 18 | export const FileUploadButton = () => { 19 | const [isDragging, setIsDragging] = useState(false); 20 | 21 | const handleDrop = (e: React.DragEvent) => { 22 | e.preventDefault(); 23 | setIsDragging(false); 24 | const files = e.dataTransfer.files; 25 | console.log(files); 26 | }; 27 | 28 | return ( 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | {/* 文件 */} 41 |
42 |
43 | 44 | Word 45 |
46 |
47 | 48 | PDF 49 |
50 |
51 | 52 | PPT 53 |
54 |
55 | 56 | 图片 57 |
58 |
59 | 60 |
61 | 62 | 63 | 92 | 93 |
94 |
95 |
96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { VisuallyHidden } from "@react-aria/visually-hidden"; 5 | import { SwitchProps, useSwitch } from "@nextui-org/switch"; 6 | import { useTheme } from "next-themes"; 7 | import { useIsSSR } from "@react-aria/ssr"; 8 | import clsx from "clsx"; 9 | import { motion } from "framer-motion"; 10 | 11 | export interface ThemeSwitchProps { 12 | className?: string; 13 | classNames?: SwitchProps["classNames"]; 14 | size?: number; 15 | } 16 | 17 | export const ThemeSwitch: FC = ({ 18 | className, 19 | classNames, 20 | size = 24, 21 | }) => { 22 | const { theme, setTheme } = useTheme(); 23 | const isSSR = useIsSSR(); 24 | 25 | const onChange = () => { 26 | theme === "light" ? setTheme("dark") : setTheme("light"); 27 | }; 28 | 29 | const { 30 | Component, 31 | slots, 32 | isSelected, 33 | getBaseProps, 34 | getInputProps, 35 | getWrapperProps, 36 | } = useSwitch({ 37 | isSelected: theme === "light" || isSSR, 38 | "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`, 39 | onChange, 40 | }); 41 | 42 | return ( 43 | 54 | 55 | 56 | 57 |
76 | 90 | {theme === "light" ? ( 91 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ) : ( 114 | 126 | 127 | 128 | )} 129 | 130 |
131 |
132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /components/chat-input-1.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Textarea } from "@nextui-org/input"; 4 | import { Button } from "@nextui-org/button"; 5 | import { useState } from "react"; 6 | import { motion } from "framer-motion"; 7 | import { FileUploadButton } from "./file-upload-button"; 8 | import { ModelSelect } from "./model-select"; 9 | export const ChatInput1 = () => { 10 | const [inputValue, setInputValue] = useState(""); 11 | const [isFocused, setIsFocused] = useState(false); 12 | 13 | return ( 14 | /* 主容器 */ 15 | 22 | 23 | {/* 选项排 */} 24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 | 34 | 35 | {/* 输入框 */} 36 |