feat: add marketing site and docs app (apps/site)

- Homepage with hero section (typewriter animation, ChatDemo, FlickeringGrid),
  features section, open-source section (GitHub stars), FAQ accordion, final CTA
- Docs powered by fumadocs-ui with i18n (en/zh), custom top nav bar with
  search, GitHub link, and language switcher
- Marketing content system with typed en/zh content files
- Custom components: RainbowButton, ScrollNav, ChatDemo animation
- Docs pages: Getting Started, Installation, Providers, MCP, Bridge,
  Assistant Workspace, FAQ
- DownloadButton MDX component linking to GitHub Releases
- Monorepo workspace config (apps/*, packages/*)
- Fix FAQ accordion lint (pure CSS grid-template-rows animation, no useEffect)
- Fix RainbowButton empty interface lint error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-10 20:25:57 +08:00
1 parent 9b2793c9d3
commit 1359758fbf
81 files changed
+8152 -191

No files matched your search

+5
View File
@@ -53,3 +53,8 @@ next-env.d.ts
# user uploads (runtime data)
.codepilot-uploads/
# monorepo: site app build artifacts
apps/site/.next/
apps/site/.source/
apps/site/node_modules/
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
@@ -0,0 +1,47 @@
---
title: Assistant Workspace
description: Inspect and manage Claude's working environment.
---
The Assistant Workspace gives you visibility into what Claude is doing during a conversation. It shows file changes, tool usage, and the current state of Claude's working context.
## Features
### File Inspector
View the files that Claude has read or modified during the conversation:
- **File tree** — Browse the project directory structure
- **Diff view** — See exactly what changes Claude made to each file
- **File preview** — Preview files with syntax highlighting
### Tool Activity
Monitor Claude's tool usage in real time:
- See which tools Claude is calling and why
- View tool input/output for debugging
- Track the sequence of operations
### Context Usage
Track how much of Claude's context window is being used:
- **Token count** — Current tokens used vs. available
- **Usage breakdown** — How tokens are distributed across messages, tools, and system context
- **Cost estimate** — Approximate cost based on the active provider's pricing
## Opening the Workspace
The workspace can be configured from:
- Go to **Settings** and open the **Assistant** section (`/settings#assistant`)
The workspace panel appears as a resizable side panel alongside the chat, and can be toggled from the chat header.
## Use Cases
- **Code Review** — Review all file changes Claude has made before committing
- **Debugging** — Inspect tool calls when something isn't working as expected
- **Learning** — Understand how Claude approaches problems by watching its tool usage
- **Cost Management** — Monitor token usage to stay within budget
@@ -0,0 +1,54 @@
---
title: Message Bridge
description: Bridge Claude conversations to messaging platforms.
---
CodePilot's Message Bridge lets you interact with Claude from external messaging platforms. Start a conversation on your desktop, then continue it from your phone via Discord, Telegram, Feishu (Lark), or QQ.
## Supported Platforms
| Platform | Features |
|----------|----------|
| **Discord** | Full message bridge with bot integration |
| **Telegram** | Direct message bridge via bot |
| **Feishu/Lark** | Enterprise messaging bridge |
| **QQ** | Bridge via QQ bot platform |
## Setup
### Discord
1. Open **Bridge** from the sidebar, or go to **Settings** and configure the Discord bridge
2. Create a Discord bot at [discord.com/developers](https://discord.com/developers/applications)
3. Copy the bot token and paste it into CodePilot
4. Invite the bot to your Discord server
5. Specify the channel ID where the bridge should operate
6. Click **Enable**
### Telegram
1. Open **Bridge** from the sidebar, or go to **Settings** and configure the Telegram bridge
2. Create a bot via [@BotFather](https://t.me/BotFather) on Telegram
3. Copy the bot token and paste it into CodePilot
4. Click **Enable**
5. Send a message to your bot to start bridging
### Feishu/Lark
1. Open **Bridge** from the sidebar, or go to **Settings** and configure the Feishu bridge
2. Create an app in the [Feishu Open Platform](https://open.feishu.cn)
3. Configure the app credentials (App ID and App Secret)
4. Set up event subscriptions
5. Click **Enable**
## How It Works
1. **Desktop to Mobile**: Messages you send in CodePilot are forwarded to the configured platform
2. **Mobile to Desktop**: Messages from the platform are sent to Claude, and responses are forwarded back
3. **Background Mode**: CodePilot can run in system tray mode, keeping the bridge active even when the window is closed
## Bridge Modes
- **Interactive Mode** — Full bidirectional messaging
- **System Tray Mode** — Minimize to tray, bridge stays active in background
- **Notification Only** — Only receive notifications, no message forwarding
+70
View File
@@ -0,0 +1,70 @@
---
title: FAQ
description: Frequently asked questions about CodePilot.
---
## General
### What is CodePilot?
CodePilot is a native desktop GUI client for Claude Code. It provides a graphical interface for the Claude Code CLI, with additional features like multi-provider support, a plugin system, message bridging, and theme customization.
### Is CodePilot free?
CodePilot is open source and free to use. You need your own API key from a supported provider (Anthropic, OpenAI, Google, AWS Bedrock, etc.) to use it.
### What platforms are supported?
CodePilot supports macOS (Apple Silicon and Intel) and Windows. Linux support is planned.
## Setup
### I get a "Node.js not found" error
CodePilot requires Node.js 18 or later. The setup wizard will offer to install it for you, or you can install it manually from [nodejs.org](https://nodejs.org).
### How do I get an API key?
- **Anthropic**: Sign up at [console.anthropic.com](https://console.anthropic.com) and create an API key
- **OpenAI**: Sign up at [platform.openai.com](https://platform.openai.com) and create an API key
- **Google AI**: Get a key from [aistudio.google.com](https://aistudio.google.com)
### Can I use a local LLM?
Yes, if it provides an OpenAI-compatible API endpoint. Configure it as an OpenAI provider with your local server's URL.
## Usage
### How do I switch between providers?
Use the provider selector in the chat header. Each conversation remembers its provider, and you can switch mid-conversation.
### How do I use MCP plugins?
Open **MCP** from the sidebar to add MCP servers. Once connected, their tools are automatically available to Claude.
### Can I use CodePilot on my phone?
Not directly, but you can use the [Message Bridge](/docs/bridge) to bridge conversations to Discord, Telegram, Feishu, or QQ and interact from your phone.
## Troubleshooting
### The app won't start
1. Make sure Node.js 18+ is installed
2. Try deleting the app data directory and relaunching
3. Check the logs in the app data directory for error messages
### Claude isn't responding
1. Check that your API key is valid and has sufficient credits
2. Verify your internet connection
3. Try switching to a different provider to isolate the issue
4. Check the **MCP** page in the sidebar for any MCP server errors that might be blocking
### How do I report a bug?
Open an issue on [GitHub](https://github.com/op7418/CodePilot/issues) with:
- Your OS and CodePilot version
- Steps to reproduce the issue
- Any relevant error messages from the logs
+30
View File
@@ -0,0 +1,30 @@
---
title: Getting Started
description: Get started with CodePilot - a native desktop GUI client for Claude Code.
---
# Getting Started
Welcome to CodePilot! This guide will help you get up and running.
## What is CodePilot?
CodePilot is a native desktop GUI client for Claude Code, built with Electron and Next.js.
It provides a rich graphical interface for interacting with Claude, managing projects,
and leveraging AI-powered development tools.
## Quick Start
1. Download CodePilot from the [download page](/download)
2. Install and launch the application
3. Configure your API provider in Settings
4. Start a new conversation and begin coding with Claude!
## Features
- **Multi-provider support** — Anthropic, OpenAI, Google, AWS Bedrock
- **MCP Plugin System** — Extend functionality with Model Context Protocol plugins
- **Message Bridge** — Connect to Discord, Telegram, Feishu, and QQ
- **Assistant Workspace** — Inspect and manage Claude's working environment
- **Theme System** — Customizable themes and visual styles
- **Image Generation** — Built-in AI image generation capabilities
@@ -0,0 +1,61 @@
---
title: Installation
description: How to install and set up CodePilot on your system.
---
## System Requirements
- **macOS** 12+ (Apple Silicon or Intel)
- **Windows** 10+ (64-bit)
- **Node.js** 18 or later
- **Claude Code CLI** (will be installed automatically if not present)
## Download
<DownloadButton label="Download Latest Release" />
Download the latest release for your platform from [GitHub Releases](https://github.com/op7418/CodePilot/releases).
| Platform | Format | Architecture |
|----------|--------|-------------|
| macOS | DMG | arm64 (Apple Silicon) + x64 (Intel) |
| Windows | NSIS Installer | x64 |
## Installation Steps
### macOS
1. Download the `.dmg` file for your architecture
2. Open the DMG and drag CodePilot to your Applications folder
3. Launch CodePilot from Applications
4. On first launch, macOS may ask you to confirm opening an app from an unidentified developer — go to **System Settings > Privacy & Security** and click "Open Anyway"
### Windows
1. Download the `.exe` installer
2. Run the installer and follow the prompts
3. CodePilot will be available in your Start menu
## First Launch
On first launch, CodePilot will:
1. **Check for Node.js** — if not found, it will offer to install it automatically
2. **Check for Claude Code CLI** — if not found, it will install it via npm
3. **Open the Setup Wizard** — configure your API provider and preferences
## Configuring an API Provider
CodePilot needs at least one API provider to function. Go to **Settings** and open the **Providers** section to add one:
- **Anthropic** — Direct API access to Claude models
- **Custom API (OpenAI-compatible)** — Any OpenAI-compatible endpoint, including local LLMs
- **OpenRouter** — Access multiple providers via OpenRouter
- **AWS Bedrock** — Claude via AWS
- **Google Vertex** — Claude/Gemini via Google Cloud
Enter your API key and select a default model. You can configure multiple providers and switch between them.
## Updating
CodePilot includes an auto-updater. When a new version is available, you'll see a notification in the app. You can also manually check for updates in the **General** section of **Settings**.
+57
View File
@@ -0,0 +1,57 @@
---
title: MCP
description: Extend CodePilot with Model Context Protocol servers.
---
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard for connecting AI assistants to external tools and data sources. CodePilot has built-in support for MCP servers.
## What is MCP?
MCP servers give Claude access to additional tools beyond its default capabilities:
- **File system tools** — Read/write files, search directories
- **Database access** — Query databases directly
- **API integrations** — Connect to external services (GitHub, Jira, Slack, etc.)
- **Development tools** — Run tests, lint code, manage dependencies
- **Custom tools** — Build your own tools for any purpose
## Managing MCP Servers
### Adding an MCP Server
1. Open **MCP** from the sidebar
2. Click **Add MCP Server**
3. Configure the server:
- **Name** — A descriptive name for the server
- **Command** — The command to start the MCP server (e.g., `npx @modelcontextprotocol/server-filesystem`)
- **Arguments** — Command-line arguments
- **Environment Variables** — Any required env vars (API keys, etc.)
4. Click **Save** and the server will be started
### Server Status
Each MCP server shows its connection status:
- **Connected** — Server is running and tools are available
- **Disconnected** — Server is not running
- **Error** — Server encountered an error (check logs for details)
## Using MCP Tools
Once an MCP server is connected, its tools automatically become available to Claude in your conversations. Claude will use these tools when relevant to your request.
You can see which tools are available from each server on the **MCP** page by expanding the server details.
## Popular MCP Servers
| Server | Purpose |
|--------|---------|
| `@modelcontextprotocol/server-filesystem` | File system access |
| `@modelcontextprotocol/server-github` | GitHub integration |
| `@modelcontextprotocol/server-slack` | Slack integration |
| `@anthropic-ai/mcp-server-fetch` | Web fetching |
## Troubleshooting
- **Server won't connect**: Check that the command is correct and the required npm package is installed
- **Tools not appearing**: Try disconnecting and reconnecting the server
- **Permission errors**: Ensure the MCP server has access to the resources it needs
+14
View File
@@ -0,0 +1,14 @@
{
"title": "Documentation",
"pages": [
"index",
"installation",
"---Guides---",
"providers",
"mcp",
"bridge",
"assistant-workspace",
"---Help---",
"faq"
]
}
@@ -0,0 +1,77 @@
---
title: Providers
description: Configure LLM providers to power your CodePilot experience.
---
CodePilot supports multiple LLM providers. You can configure one or more providers and switch between them at any time.
## Supported Providers
| Provider | Description | Auth Method |
|----------|-------------|-------------|
| **Anthropic** | Direct access to Claude models (Opus, Sonnet, Haiku) | API Key |
| **Custom API (OpenAI-compatible)** | Any OpenAI-compatible endpoint, including local LLMs | API Key + Base URL |
| **OpenRouter** | Access multiple model providers via OpenRouter | API Key |
| **AWS Bedrock** | Claude via AWS infrastructure | AWS Credentials |
| **Google Vertex** | Claude and Gemini via Google Cloud | Service Account |
| **Google Gemini (Image)** | Gemini image generation | API Key |
| **LiteLLM** | Unified proxy for 100+ LLM providers | API Key + Base URL |
Additional presets are available for Chinese providers: GLM, Kimi/Moonshot, MiniMax, Volcengine Ark, and Aliyun Bailian.
## Adding a Provider
1. Open **Settings** and go to the **Providers** section
2. Click **Add Provider**
3. Select the provider type
4. Enter your credentials:
- For API key providers: paste your API key
- For AWS Bedrock: configure your AWS region and credentials
- For Google Vertex: configure your project ID and region
5. Select a default model
6. Click **Save**
## Switching Providers
You can switch the active provider at any time:
- Use the provider selector in the chat header
- Each conversation remembers which provider was used
- Switching providers mid-conversation is supported
## Provider-Specific Features
### Anthropic
- Supports **extended thinking** mode for complex reasoning
- Token usage tracking with cost estimates
- Full Claude Agent SDK integration
### Custom API (OpenAI-compatible)
- Works with any endpoint that implements the OpenAI chat completions API
- Use this for local LLMs (Ollama, LM Studio, vLLM, etc.) or third-party proxies
- Configure via custom base URL
### OpenRouter
- Access models from Anthropic, OpenAI, Google, Meta, and more through a single API key
- Automatic model routing and fallback
### AWS Bedrock
- Uses your existing AWS IAM credentials
- Supports cross-region inference
- No separate API key needed if AWS CLI is configured
## Environment Variables
Providers can also be configured via environment variables. CodePilot automatically reads your shell environment:
```bash
# Anthropic (auto-detected on startup)
export ANTHROPIC_API_KEY="sk-ant-..."
# AWS Bedrock (uses standard AWS env vars)
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
```
CodePilot auto-detects `ANTHROPIC_API_KEY` from your shell environment on startup. For other providers, configure credentials directly in the Providers settings UI. If you update environment variables, restart CodePilot to pick up the changes.
@@ -0,0 +1,46 @@
---
title: 助手工作区
description: 检查和管理 Claude 的工作环境。
---
助手工作区让你能够查看 Claude 在对话过程中的操作。它展示文件变更、工具使用情况以及 Claude 当前的工作上下文状态。
## 功能
### 文件检查器
查看 Claude 在对话中读取或修改的文件:
- **文件树** — 浏览项目目录结构
- **Diff 视图** — 查看 Claude 对每个文件的具体更改
- **文件预览** — 带语法高亮的文件预览
### 工具活动
实时监控 Claude 的工具使用:
- 查看 Claude 正在调用哪些工具及原因
- 查看工具输入/输出以便调试
- 追踪操作序列
### 上下文使用情况
追踪 Claude 上下文窗口的使用情况:
- **Token 计数** — 当前使用的 token 数 vs. 可用数
- **使用明细** — token 在消息、工具和系统上下文中的分布
- **成本估算** — 基于当前提供商定价的大致费用
## 打开工作区
工作区面板可以通过以下方式切换:
- 前往**设置**,打开**助手**部分(即 Settings #assistant)
- 键盘快捷键(可在设置中配置)
## 使用场景
- **代码审查** — 在提交前审查 Claude 所做的所有文件更改
- **调试** — 当某些功能不正常时检查工具调用
- **学习** — 通过观察工具使用来了解 Claude 如何处理问题
- **成本管理** — 监控 token 使用以控制预算
@@ -0,0 +1,54 @@
---
title: 消息桥接
description: 将 Claude 对话桥接到消息平台。
---
CodePilot 的消息桥接功能让你可以从外部消息平台与 Claude 交互。在桌面上开始对话,然后通过 Discord、Telegram、飞书或 QQ 在手机上继续。
## 支持的平台
| 平台 | 功能 |
|------|------|
| **Discord** | 完整的消息桥接,支持 Bot 集成 |
| **Telegram** | 通过 Bot 进行直接消息桥接 |
| **飞书/Lark** | 企业消息桥接 |
| **QQ** | 通过 QQ Bot 平台桥接 |
## 设置
### Discord
1. 从侧边栏打开 **Bridge**
2. 在 [discord.com/developers](https://discord.com/developers/applications) 创建 Discord Bot
3. 复制 Bot Token 并粘贴到 CodePilot
4. 将 Bot 邀请到你的 Discord 服务器
5. 指定桥接操作的频道 ID
6. 点击**启用**
### Telegram
1. 从侧边栏打开 **Bridge**
2. 在 Telegram 上通过 [@BotFather](https://t.me/BotFather) 创建 Bot
3. 复制 Bot Token 并粘贴到 CodePilot
4. 点击**启用**
5. 向你的 Bot 发送消息开始桥接
### 飞书/Lark
1. 从侧边栏打开 **Bridge**
2. 在[飞书开放平台](https://open.feishu.cn)创建应用
3. 配置应用凭证(App ID 和 App Secret)
4. 设置事件订阅
5. 点击**启用**
## 工作原理
1. **桌面到手机**:你在 CodePilot 中发送的消息会转发到配置的平台
2. **手机到桌面**:来自平台的消息会发送给 Claude,回复会转发回去
3. **后台模式**:CodePilot 可以在系统托盘模式下运行,即使窗口关闭也能保持桥接活跃
## 桥接模式
- **交互模式** — 完整的双向消息传递
- **系统托盘模式** — 最小化到托盘,桥接保持活跃
- **仅通知** — 只接收通知,不转发消息
+70
View File
@@ -0,0 +1,70 @@
---
title: 常见问题
description: 关于 CodePilot 的常见问题解答。
---
## 一般问题
### CodePilot 是什么?
CodePilot 是 Claude Code 的原生桌面 GUI 客户端。它为 Claude Code CLI 提供图形界面,附带多提供商支持、插件系统、消息桥接和主题自定义等额外功能。
### CodePilot 免费吗?
CodePilot 是开源且免费使用的。你需要自己的 API 密钥(来自 Anthropic、OpenAI、Google、AWS Bedrock 等支持的提供商)。
### 支持哪些平台?
CodePilot 支持 macOS(Apple Silicon 和 Intel)和 Windows。Linux 支持正在计划中。
## 设置
### 出现"未找到 Node.js"错误
CodePilot 需要 Node.js 18 或更高版本。设置向导会提供自动安装,你也可以从 [nodejs.org](https://nodejs.org) 手动安装。
### 如何获取 API 密钥?
- **Anthropic**:在 [console.anthropic.com](https://console.anthropic.com) 注册并创建 API 密钥
- **OpenAI**:在 [platform.openai.com](https://platform.openai.com) 注册并创建 API 密钥
- **Google AI**:从 [aistudio.google.com](https://aistudio.google.com) 获取密钥
### 可以使用本地 LLM 吗?
可以,如果它提供 OpenAI 兼容的 API 端点。将其配置为 OpenAI 提供商,使用本地服务器的 URL 即可。
## 使用
### 如何切换提供商?
使用聊天头部的提供商选择器。每个对话会记住其提供商,你可以在对话中途切换。
### 如何使用 MCP 插件?
从侧边栏打开 **MCP** 添加 MCP 服务器。连接后,其工具会自动对 Claude 可用。
### 可以在手机上使用 CodePilot 吗?
不能直接使用,但你可以使用[消息桥接](/zh/docs/bridge)将对话桥接到 Discord、Telegram、飞书或 QQ,在手机上交互。
## 故障排除
### 应用无法启动
1. 确保已安装 Node.js 18+
2. 尝试删除应用数据目录并重新启动
3. 检查应用数据目录中的日志文件
### Claude 没有响应
1. 检查 API 密钥是否有效且有足够额度
2. 验证网络连接
3. 尝试切换到其他提供商以定位问题
4. 检查侧边栏 **MCP** 页面是否有 MCP 服务器错误
### 如何报告 Bug?
在 [GitHub](https://github.com/op7418/CodePilot/issues) 上创建 issue,包含:
- 你的操作系统和 CodePilot 版本
- 复现步骤
- 日志中的相关错误信息
+29
View File
@@ -0,0 +1,29 @@
---
title: 快速开始
description: 开始使用 CodePilot — Claude Code 的原生桌面 GUI 客户端。
---
# 快速开始
欢迎使用 CodePilot!本指南将帮助你快速上手。
## 什么是 CodePilot?
CodePilot 是 Claude Code 的原生桌面 GUI 客户端,基于 Electron 和 Next.js 构建。
它提供了丰富的图形界面,用于与 Claude 交互、管理项目以及利用 AI 驱动的开发工具。
## 快速上手
1. 从[下载页面](/zh/download)下载 CodePilot
2. 安装并启动应用
3. 在设置中配置你的 API 提供商
4. 开始新对话,与 Claude 一起编程!
## 功能特色
- **多提供商支持** — Anthropic、OpenAI、Google、AWS Bedrock
- **MCP 插件系统** — 通过 Model Context Protocol 插件扩展功能
- **消息桥接** — 连接 Discord、Telegram、飞书和 QQ
- **助手工作区** — 检查和管理 Claude 的工作环境
- **主题系统** — 可自定义的主题和视觉风格
- **图片生成** — 内置 AI 图片生成功能
@@ -0,0 +1,61 @@
---
title: 安装指南
description: 如何在你的系统上安装和配置 CodePilot。
---
## 系统要求
- **macOS** 12+(Apple Silicon 或 Intel)
- **Windows** 10+(64 位)
- **Node.js** 18 或更高版本
- **Claude Code CLI**(如未安装会自动安装)
## 下载
<DownloadButton label="下载最新版本" />
从 [GitHub Releases](https://github.com/op7418/CodePilot/releases) 下载适合你平台的最新版本。
| 平台 | 格式 | 架构 |
|------|------|------|
| macOS | DMG | arm64 (Apple Silicon) + x64 (Intel) |
| Windows | NSIS 安装包 | x64 |
## 安装步骤
### macOS
1. 下载对应架构的 `.dmg` 文件
2. 打开 DMG,将 CodePilot 拖到应用程序文件夹
3. 从应用程序中启动 CodePilot
4. 首次启动时,macOS 可能会提示确认打开来自未识别开发者的应用 — 前往**系统设置 > 隐私与安全性**,点击"仍要打开"
### Windows
1. 下载 `.exe` 安装程序
2. 运行安装程序并按提示操作
3. CodePilot 将出现在开始菜单中
## 首次启动
首次启动时,CodePilot 会:
1. **检查 Node.js** — 如未找到,会提供自动安装
2. **检查 Claude Code CLI** — 如未找到,会通过 npm 安装
3. **打开设置向导** — 配置 API 提供商和偏好设置
## 配置 API 提供商
CodePilot 至少需要一个 API 提供商才能工作。前往**设置 > 提供商**添加:
- **Anthropic** — 直接访问 Claude 模型
- **Custom API (OpenAI 兼容)** — 任何 OpenAI 兼容端点,包括本地 LLM
- **OpenRouter** — 通过 OpenRouter 访问多个模型提供商
- **AWS Bedrock** — 通过 AWS 使用 Claude
- **Google Vertex** — 通过 Google Cloud 使用 Claude/Gemini
输入 API 密钥并选择默认模型。你可以配置多个提供商并在它们之间切换。
## 更新
CodePilot 内置自动更新功能。有新版本可用时,你会在应用中看到通知。也可以在**设置**的**通用**部分手动检查更新。
+57
View File
@@ -0,0 +1,57 @@
---
title: MCP 插件
description: 通过 Model Context Protocol 插件扩展 CodePilot 功能。
---
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) 是一个用于连接 AI 助手与外部工具和数据源的开放标准。CodePilot 内置了对 MCP 服务器的支持。
## 什么是 MCP 插件?
MCP 插件让 Claude 能够使用超出默认能力的额外工具:
- **文件系统工具** — 读写文件、搜索目录
- **数据库访问** — 直接查询数据库
- **API 集成** — 连接外部服务(GitHub、Jira、Slack 等)
- **开发工具** — 运行测试、代码检查、管理依赖
- **自定义工具** — 为任何目的构建你自己的工具
## 管理插件
### 添加插件
1. 从侧边栏打开 **MCP**
2. 点击**添加 MCP 服务器**
3. 配置服务器:
- **名称** — 插件的描述性名称
- **命令** — 启动 MCP 服务器的命令(如 `npx @modelcontextprotocol/server-filesystem`)
- **参数** — 命令行参数
- **环境变量** — 所需的环境变量(API 密钥等)
4. 点击**保存**,插件将被启动
### 插件状态
每个插件会显示连接状态:
- **已连接** — 服务器正在运行,工具可用
- **已断开** — 服务器未运行
- **错误** — 服务器遇到错误(查看日志了解详情)
## 使用插件工具
插件连接后,其工具会自动在对话中对 Claude 可用。Claude 会在与你的请求相关时使用这些工具。
你可以在 **MCP** 页面中查看每个插件提供的可用工具。
## 常用 MCP 服务器
| 服务器 | 用途 |
|--------|------|
| `@modelcontextprotocol/server-filesystem` | 文件系统访问 |
| `@modelcontextprotocol/server-github` | GitHub 集成 |
| `@modelcontextprotocol/server-slack` | Slack 集成 |
| `@anthropic-ai/mcp-server-fetch` | 网页抓取 |
## 故障排除
- **插件无法连接**:检查命令是否正确,所需的 npm 包是否已安装
- **工具未出现**:尝试断开并重新连接插件
- **权限错误**:确保 MCP 服务器有权访问所需资源
+14
View File
@@ -0,0 +1,14 @@
{
"title": "文档",
"pages": [
"index",
"installation",
"---指南---",
"providers",
"mcp",
"bridge",
"assistant-workspace",
"---帮助---",
"faq"
]
}
@@ -0,0 +1,77 @@
---
title: 提供商配置
description: 配置 LLM 提供商以驱动你的 CodePilot 体验。
---
CodePilot 支持多个 LLM 提供商。你可以配置一个或多个提供商,随时在它们之间切换。
## 支持的提供商
| 提供商 | 说明 | 认证方式 |
|--------|------|----------|
| **Anthropic** | 直接访问 Claude 模型(Opus、Sonnet、Haiku) | API 密钥 |
| **Custom API (OpenAI 兼容)** | 任何 OpenAI 兼容端点,包括本地 LLM | API 密钥 + Base URL |
| **OpenRouter** | 通过 OpenRouter 访问多个模型提供商 | API 密钥 |
| **AWS Bedrock** | 通过 AWS 基础设施使用 Claude | AWS 凭证 |
| **Google Vertex** | 通过 Google Cloud 使用 Claude 和 Gemini | 服务账号 |
| **Google Gemini (Image)** | Gemini 图片生成 | API 密钥 |
| **LiteLLM** | 统一代理,支持 100+ LLM 提供商 | API 密钥 + Base URL |
另外还提供中国提供商预设:GLM、Kimi/Moonshot、MiniMax、火山引擎 Ark、阿里云百炼。
## 添加提供商
1. 打开**设置 > 提供商**
2. 点击**添加提供商**
3. 选择提供商类型
4. 输入凭证:
- API 密钥类型:粘贴你的 API 密钥
- AWS Bedrock:配置 AWS 区域和凭证
- Google Vertex:配置项目 ID 和区域
5. 选择默认模型
6. 点击**保存**
## 切换提供商
你可以随时切换活跃的提供商:
- 使用聊天头部的提供商选择器
- 每个对话会记住使用的提供商
- 支持在对话中途切换提供商
## 提供商特定功能
### Anthropic
- 支持**扩展思考**模式,用于复杂推理
- 带成本估算的 token 使用追踪
- 完整的 Claude Agent SDK 集成
### Custom API (OpenAI 兼容)
- 支持任何实现 OpenAI Chat Completions API 的端点
- 可用于本地 LLM(Ollama、LM Studio、vLLM 等)或第三方代理
- 通过自定义 Base URL 配置
### OpenRouter
- 通过单个 API 密钥访问 Anthropic、OpenAI、Google、Meta 等多家提供商的模型
- 自动模型路由和故障转移
### AWS Bedrock
- 使用现有的 AWS IAM 凭证
- 支持跨区域推理
- 如果已配置 AWS CLI,无需单独的 API 密钥
## 环境变量
提供商也可以通过环境变量配置。CodePilot 会自动读取你的 shell 环境:
```bash
# Anthropic(启动时自动检测)
export ANTHROPIC_API_KEY="sk-ant-..."
# AWS Bedrock(使用标准 AWS 环境变量)
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
```
CodePilot 启动时会自动检测 `ANTHROPIC_API_KEY` 环境变量。其他提供商请在设置的提供商界面中直接配置凭证。更新环境变量后需要重启 CodePilot 才能生效。
+237
View File
@@ -0,0 +1,237 @@
export interface MarketingContent {
hero: {
title: string;
tagline: string;
cta: string;
secondaryCta: string;
screenshots: { src: string; alt: string; caption: string }[];
};
features: {
title: string;
titleLight: string;
subtitle: string;
items: {
icon: string;
title: string;
description: string;
badge?: string;
}[];
};
openSource: {
title: string;
titleLight: string;
highlights: {
icon: string;
title: string;
description: string;
}[];
githubCta: string;
githubUrl: string;
};
faq: {
title: string;
titleLight: string;
items: { q: string; a: string }[];
};
audience: {
title: string;
subtitle: string;
items: { title: string; description: string }[];
};
quickstart: {
title: string;
steps: { step: string; title: string; description: string }[];
};
docs: {
title: string;
cards: { title: string; description: string; href: string }[];
};
cta: {
title: string;
description: string;
primary: string;
secondary: string;
};
footer: {
copyright: string;
links: { text: string; url: string }[];
};
}
export const en: MarketingContent = {
hero: {
title: 'CodePilot',
tagline: 'Your desktop AI Agent for',
cta: 'Download',
secondaryCta: 'Documentation',
screenshots: [
{ src: '/screenshots/chat.svg', alt: 'Chat interface', caption: 'Multi-session chat with Code, Plan, and Ask modes' },
{ src: '/screenshots/providers.svg', alt: 'Provider management', caption: 'Connect and switch between AI providers' },
{ src: '/screenshots/mcp-skills.svg', alt: 'MCP and Skills', caption: 'Extend with MCP servers and Skills' },
{ src: '/screenshots/workspace.svg', alt: 'Assistant Workspace', caption: 'Inspect files and review changes in real time' },
{ src: '/screenshots/bridge.svg', alt: 'Bridge messaging', caption: 'Continue conversations from your phone' },
],
},
features: {
title: 'Everything you need to work with Claude Code.',
titleLight: 'Conversations, providers, extensions, and project context — in one place.',
subtitle: '',
items: [
{
icon: 'MessageSquare',
title: 'Multi-session chat',
description: 'Run multiple conversations with independent context.',
},
{
icon: 'Layers',
title: 'Code · Plan · Ask',
description: 'Three modes for different workflows.',
},
{
icon: 'Shield',
title: 'Permission control',
description: 'Confirm before Claude modifies files.',
},
{
icon: 'FolderOpen',
title: 'Assistant Workspace',
description: 'Inspect files and review changes live.',
},
{
icon: 'Brain',
title: 'Persona & Memory',
description: 'Consistent behavior across sessions.',
},
{
icon: 'Sparkles',
title: 'Skills',
description: 'Reusable prompt patterns you can share.',
},
{
icon: 'Bookmark',
title: 'Session persistence',
description: 'Pick up where you left off after restart.',
},
{
icon: 'Compass',
title: 'Onboarding',
description: 'Auto-detect project structure on first run.',
},
],
},
openSource: {
title: 'Fully open source.',
titleLight: 'Use your own API key. No middleman, no markup.',
highlights: [
{
icon: 'Code',
title: 'Open Source',
description: 'Every line of code is public on GitHub. Audit, fork, or contribute.',
},
{
icon: 'Key',
title: 'Bring Your Own Key',
description: 'Connect directly to Anthropic, OpenAI, Google, or any provider with your own API key.',
},
{
icon: 'Users',
title: 'Community Driven',
description: 'Built in the open with feedback from developers who use it every day.',
},
],
githubCta: 'Star on GitHub',
githubUrl: 'https://github.com/op7418/CodePilot',
},
faq: {
title: 'Frequently asked questions.',
titleLight: 'Everything you need to know before getting started.',
items: [
{
q: 'Is CodePilot really free?',
a: 'Yes. CodePilot is completely free and open source. You only pay for the API usage from your chosen provider.',
},
{
q: 'Which AI providers are supported?',
a: 'Anthropic, OpenAI, Google, AWS Bedrock, and any OpenAI-compatible endpoint. You can switch between them at any time.',
},
{
q: 'Do I need a Claude Code subscription?',
a: 'No. CodePilot works with your own API key directly — no Claude Code subscription required.',
},
{
q: 'Is my data sent to CodePilot servers?',
a: 'No. All API calls go directly from your machine to the provider. CodePilot never sees your code or conversations.',
},
{
q: 'Can I use it on Windows and Linux?',
a: 'macOS is fully supported today. Windows and Linux builds are on the roadmap.',
},
],
},
audience: {
title: 'Built for daily use.',
subtitle: 'For developers who rely on Claude Code every day.',
items: [
{
title: 'Long-lived codebases',
description: 'Keep project context organized across months of work.',
},
{
title: 'Multiple providers',
description: 'Switch between providers and MCP servers without friction.',
},
{
title: 'Persistent context',
description: 'Build up persona, memory, and onboarding that stick.',
},
{
title: 'Work on the go',
description: 'Continue tasks from your phone while Claude keeps working.',
},
],
},
quickstart: {
title: 'Three steps to start.',
steps: [
{
step: '1',
title: 'Install Claude Code CLI',
description: 'Install and sign in with your Anthropic account.',
},
{
step: '2',
title: 'Download CodePilot',
description: 'Configure your preferred provider.',
},
{
step: '3',
title: 'Start a session',
description: 'Connect Workspace, MCP, or Bridge as needed.',
},
],
},
docs: {
title: 'Documentation',
cards: [
{ title: 'Getting Started', description: 'Installation and first steps.', href: '/docs' },
{ title: 'Providers', description: 'Configure AI providers.', href: '/docs/providers' },
{ title: 'MCP', description: 'Set up MCP servers.', href: '/docs/mcp' },
{ title: 'Bridge', description: 'Connect messaging platforms.', href: '/docs/bridge' },
{ title: 'Workspace', description: 'File inspection and context.', href: '/docs/workspace' },
],
},
cta: {
title: 'Ready to try CodePilot?',
description: 'Download and start working with Claude Code in a desktop workspace.',
primary: 'Download',
secondary: 'Read the docs',
},
footer: {
copyright: '\u00a9 2026 CodePilot',
links: [
{ text: 'GitHub', url: 'https://github.com/op7418/CodePilot' },
{ text: 'Docs', url: '/docs' },
{ text: 'Download', url: '/download' },
],
},
};
+11
View File
@@ -0,0 +1,11 @@
import { en } from './en';
import { zh } from './zh';
import type { MarketingContent } from './en';
const content: Record<string, MarketingContent> = { en, zh };
export function getMarketingContent(locale: string): MarketingContent {
return content[locale] ?? content.en;
}
export type { MarketingContent };
+179
View File
@@ -0,0 +1,179 @@
import type { MarketingContent } from './en';
export const zh: MarketingContent = {
hero: {
title: 'CodePilot',
tagline: '你的桌面 AI Agent,专注',
cta: '下载',
secondaryCta: '查看文档',
screenshots: [
{ src: '/screenshots/chat.svg', alt: '聊天界面', caption: '多会话聊天,支持 Code、Plan、Ask 模式' },
{ src: '/screenshots/providers.svg', alt: 'Provider 管理', caption: '连接并切换多个 AI 提供商' },
{ src: '/screenshots/mcp-skills.svg', alt: 'MCP 和 Skills', caption: '通过 MCP 和 Skills 扩展能力' },
{ src: '/screenshots/workspace.svg', alt: 'Assistant Workspace', caption: '实时检查文件和审查更改' },
{ src: '/screenshots/bridge.svg', alt: 'Bridge 消息', caption: '在手机上继续对话' },
],
},
features: {
title: '使用 Claude Code 所需的一切',
titleLight: '对话、Provider、扩展和项目上下文——集于一处。',
subtitle: '',
items: [
{
icon: 'MessageSquare',
title: '多会话聊天',
description: '多个会话独立运行,各自保持上下文。',
},
{
icon: 'Layers',
title: 'Code · Plan · Ask',
description: '三种模式,适配不同工作流。',
},
{
icon: 'Shield',
title: '权限控制',
description: '修改文件前需你确认。',
},
{
icon: 'FolderOpen',
title: 'Assistant Workspace',
description: '实时查看文件和审查更改。',
},
{
icon: 'Brain',
title: 'Persona 和 Memory',
description: '跨会话保持一致的行为。',
},
{
icon: 'Sparkles',
title: 'Skills',
description: '可复用、可分享的提示模式。',
},
{
icon: 'Bookmark',
title: '会话持久化',
description: '重启后从上次中断处继续。',
},
{
icon: 'Compass',
title: 'Onboarding',
description: '首次运行自动检测项目结构。',
},
],
},
openSource: {
title: '完全开源。',
titleLight: '使用你自己的 API Key,没有中间商,没有加价。',
highlights: [
{
icon: 'Code',
title: '开源',
description: '所有代码公开在 GitHub,可审查、可 fork、可贡献。',
},
{
icon: 'Key',
title: '自带 Key',
description: '直连 Anthropic、OpenAI、Google 或任何 Provider,使用你自己的 API Key。',
},
{
icon: 'Users',
title: '社区驱动',
description: '在开发者社区的反馈中持续迭代。',
},
],
githubCta: '在 GitHub 上 Star',
githubUrl: 'https://github.com/op7418/CodePilot',
},
faq: {
title: '常见问题。',
titleLight: '开始使用前你可能想了解的一切。',
items: [
{
q: 'CodePilot 真的免费吗?',
a: '是的。CodePilot 完全免费且开源,你只需为所选 Provider 的 API 用量付费。',
},
{
q: '支持哪些 AI Provider?',
a: 'Anthropic、OpenAI、Google、AWS Bedrock 以及任何 OpenAI 兼容接口,随时切换。',
},
{
q: '需要 Claude Code 订阅吗?',
a: '不需要。CodePilot 直接使用你自己的 API Key,无需 Claude Code 订阅。',
},
{
q: '我的数据会发送到 CodePilot 服务器吗?',
a: '不会。所有 API 调用从你的电脑直接发送到 Provider,CodePilot 不会接触你的代码或对话。',
},
{
q: '支持 Windows 和 Linux 吗?',
a: '目前完整支持 macOS,Windows 和 Linux 版本在计划中。',
},
],
},
audience: {
title: '为日常使用而设计。',
subtitle: '面向每天依赖 Claude Code 的开发者。',
items: [
{
title: '长期代码库',
description: '在持续数月的开发中保持上下文有序。',
},
{
title: '多 Provider',
description: '在提供商和 MCP 服务器之间无缝切换。',
},
{
title: '持久上下文',
description: '积累 persona、memory 和 onboarding。',
},
{
title: '随时随地',
description: '通过手机继续任务,Claude 持续工作。',
},
],
},
quickstart: {
title: '三步开始使用。',
steps: [
{
step: '1',
title: '安装 Claude Code CLI',
description: '安装并用 Anthropic 账户登录。',
},
{
step: '2',
title: '下载 CodePilot',
description: '配置你偏好的 Provider。',
},
{
step: '3',
title: '开始会话',
description: '按需连接 Workspace、MCP 或 Bridge。',
},
],
},
docs: {
title: '文档',
cards: [
{ title: '快速开始', description: '安装与第一步。', href: '/docs' },
{ title: 'Providers', description: '配置 AI 提供商。', href: '/docs/providers' },
{ title: 'MCP', description: '设置 MCP 服务器。', href: '/docs/mcp' },
{ title: 'Bridge', description: '连接消息平台。', href: '/docs/bridge' },
{ title: 'Workspace', description: '文件检查与上下文。', href: '/docs/workspace' },
],
},
cta: {
title: '准备好试试 CodePilot 了吗?',
description: '下载并开始在桌面工作区中使用 Claude Code。',
primary: '下载',
secondary: '阅读文档',
},
footer: {
copyright: '\u00a9 2026 CodePilot',
links: [
{ text: 'GitHub', url: 'https://github.com/op7418/CodePilot' },
{ text: '文档', url: '/zh/docs' },
{ text: '下载', url: '/zh/download' },
],
},
};
+10
View File
@@ -0,0 +1,10 @@
import { createMDX } from 'fumadocs-mdx/next';
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
};
const withMDX = createMDX();
export default withMDX(config);
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@codepilot/site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start",
"lint": "eslint",
"typecheck": "tsc -p tsconfig.check.json --noEmit"
},
"dependencies": {
"@base-ui/react": "^1.2.0",
"@hugeicons/core-free-icons": "^4.0.0",
"@hugeicons/react": "^1.1.6",
"@types/mdx": "^2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.35.2",
"fumadocs-core": "^15",
"fumadocs-mdx": "^11",
"fumadocs-ui": "^15",
"lucide-react": "^0.563.0",
"next": "15.3.3",
"react": "^19",
"react-dom": "^19",
"shadcn": "^4.0.2",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="348" viewBox="0 0 800 348">
<rect width="800" height="348" fill="#f0f1f4" rx="0"/>
<text x="400" y="174" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="18" fill="#9ca3af">PLACEHOLDER</text>
</svg>

After

Width:  |  Height:  |  Size: 298 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="348" viewBox="0 0 800 348">
<rect width="800" height="348" fill="#f0f1f4" rx="0"/>
<text x="400" y="174" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="18" fill="#9ca3af">PLACEHOLDER</text>
</svg>

After

Width:  |  Height:  |  Size: 298 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="348" viewBox="0 0 800 348">
<rect width="800" height="348" fill="#f0f1f4" rx="0"/>
<text x="400" y="174" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="18" fill="#9ca3af">PLACEHOLDER</text>
</svg>

After

Width:  |  Height:  |  Size: 298 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="348" viewBox="0 0 800 348">
<rect width="800" height="348" fill="#f0f1f4" rx="0"/>
<text x="400" y="174" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="18" fill="#9ca3af">PLACEHOLDER</text>
</svg>

After

Width:  |  Height:  |  Size: 298 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="348" viewBox="0 0 800 348">
<rect width="800" height="348" fill="#f0f1f4" rx="0"/>
<text x="400" y="174" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="18" fill="#9ca3af">PLACEHOLDER</text>
</svg>

After

Width:  |  Height:  |  Size: 298 B

+11
View File
@@ -0,0 +1,11 @@
import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
export const docs = defineDocs({
dir: 'content/docs',
});
export default defineConfig({
mdxOptions: {
// Add any remark/rehype plugins here
},
});
@@ -0,0 +1,33 @@
import { siteConfig } from '@/lib/site.config';
import { buttonVariants } from '@/components/ui/button-variants';
export default async function CommunityPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
return (
<main className="flex min-h-screen flex-col items-center justify-center p-8">
<h1 className="text-3xl font-bold mb-4">
{lang === 'zh' ? '社区' : 'Community'}
</h1>
<p className="text-muted-foreground mb-8 text-center max-w-md">
{lang === 'zh'
? '加入社区,获取帮助和分享经验'
: 'Join the community, get help, and share your experience'}
</p>
<div className="flex gap-4">
<a
href={siteConfig.repo.url}
target="_blank"
rel="noopener noreferrer"
className={buttonVariants({ variant: "outline" })}
>
GitHub
</a>
</div>
</main>
);
}
@@ -0,0 +1,31 @@
import { siteConfig } from '@/lib/site.config';
import { buttonVariants } from '@/components/ui/button-variants';
export default async function DownloadPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
return (
<main className="flex min-h-screen flex-col items-center justify-center p-8">
<h1 className="text-3xl font-bold mb-4">
{lang === 'zh' ? '下载 CodePilot' : 'Download CodePilot'}
</h1>
<p className="text-muted-foreground mb-8 text-center max-w-md">
{lang === 'zh'
? '从 GitHub Releases 下载最新版本'
: 'Download the latest version from GitHub Releases'}
</p>
<a
href={siteConfig.repo.releases}
target="_blank"
rel="noopener noreferrer"
className={buttonVariants({ variant: "default" })}
>
{lang === 'zh' ? '前往 GitHub Releases' : 'Go to GitHub Releases'}
</a>
</main>
);
}
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { homeOptions } from '@/lib/layout.shared';
export default async function MarketingLayout({
params,
children,
}: {
params: Promise<{ lang: string }>;
children: ReactNode;
}) {
const { lang } = await params;
return <HomeLayout {...homeOptions(lang)}>{children}</HomeLayout>;
}
@@ -0,0 +1,29 @@
import { getMarketingContent } from '../../../../content/marketing';
import { ScrollNav } from '@/components/marketing/ScrollNav';
import { HeroSection } from '@/components/marketing/HeroSection';
import { FeaturesSection } from '@/components/marketing/FeaturesSection';
import { IntegrationsSection } from '@/components/marketing/IntegrationsSection';
import { FAQSection } from '@/components/marketing/FAQAccordion';
import { FinalCTA } from '@/components/marketing/FinalCTA';
import { SiteFooter } from '@/components/marketing/SiteFooter';
export default async function HomePage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const content = getMarketingContent(lang);
return (
<main>
<ScrollNav locale={lang} />
<HeroSection content={content.hero} locale={lang} />
<FeaturesSection content={content.features} />
<IntegrationsSection content={content.openSource} />
<FAQSection content={content.faq} />
<FinalCTA content={content.cta} locale={lang} />
<SiteFooter content={content.footer} />
</main>
);
}
@@ -0,0 +1,62 @@
import { source } from '@/lib/source';
import {
DocsPage,
DocsBody,
DocsTitle,
DocsDescription,
} from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { DownloadButton } from '@/components/docs/DownloadButton';
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
interface MDXPageData {
title?: string;
description?: string;
body: (props: { components: Record<string, unknown> }) => ReactNode;
toc: { title: string; url: string; depth: number }[];
}
export default async function Page({
params,
}: {
params: Promise<{ slug?: string[]; lang: string }>;
}) {
const { slug, lang } = await params;
const page = source.getPage(slug, lang);
if (!page) notFound();
// fumadocs-mdx provides body/toc at runtime; typed locally to bridge version gap
const { body: MDXContent, toc } = page.data as unknown as MDXPageData;
return (
<DocsPage toc={toc}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDXContent components={{ ...defaultMdxComponents, DownloadButton }} />
</DocsBody>
</DocsPage>
);
}
export function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[]; lang: string }>;
}): Promise<Metadata> {
const { slug, lang } = await params;
const page = source.getPage(slug, lang);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
};
}
+31
View File
@@ -0,0 +1,31 @@
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import type { ReactNode } from 'react';
import { source } from '@/lib/source';
import { baseOptions } from '@/lib/layout.shared';
import { DocsTopNav } from '@/components/docs/DocsTopNav';
export default async function DocsLayoutWrapper({
params,
children,
}: {
params: Promise<{ lang: string }>;
children: ReactNode;
}) {
const { lang } = await params;
return (
<div className="flex min-h-screen flex-col">
<DocsTopNav locale={lang} />
<DocsLayout
{...baseOptions(lang)}
tree={source.pageTree[lang]}
nav={{ enabled: false }}
links={[]}
githubUrl={undefined}
sidebar={{ collapsible: false }}
>
{children}
</DocsLayout>
</div>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import { i18n } from '@/lib/i18n';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
export default async function LangLayout({
params,
children,
}: {
params: Promise<{ lang: string }>;
children: ReactNode;
}) {
const { lang } = await params;
return (
<html lang={lang} className={inter.variable} suppressHydrationWarning>
<body className="font-sans">
<RootProvider i18n={{
locale: lang,
locales: [
{ locale: 'en', name: 'English' },
{ locale: 'zh', name: '中文' },
],
}}>
{children}
</RootProvider>
</body>
</html>
);
}
export function generateStaticParams() {
return i18n.languages.map((lang) => ({ lang }));
}
+9
View File
@@ -0,0 +1,9 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
localeMap: {
// Orama does not have a Chinese stemmer; use English tokenizer as fallback
zh: 'english',
},
});
+196
View File
@@ -0,0 +1,196 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@source "../../node_modules/fumadocs-ui/dist/**/*.js";
/* Font stack: Inter for Latin, system CJK fonts for Chinese */
@theme {
--font-sans: var(--font-inter), "PingFang SC", "Noto Sans SC",
"Microsoft YaHei", "Hiragino Sans GB", ui-sans-serif, system-ui, sans-serif;
}
:root {
--fd-primary: 217 91% 50%;
--fd-background: 225 15% 97%;
/* Rainbow button colors */
--color-1: 0 100% 63%;
--color-2: 270 100% 63%;
--color-3: 210 100% 63%;
--color-4: 195 100% 63%;
--color-5: 90 100% 63%;
/* Match main CodePilot app's stone neutrals, with blue primary for brand */
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: hsl(217 91% 50%);
--primary-foreground: hsl(0 0% 100%);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.546 0.245 262.881);
--radius: 0.75rem;
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.546 0.245 262.881);
}
.dark {
--fd-primary: 217 80% 62%;
--fd-background: 222 15% 6%;
/* Match main CodePilot app's dark stone palette */
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.147 0.004 49.25);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.147 0.004 49.25);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.623 0.214 259.815);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.623 0.214 259.815);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
/* Tight letter-spacing on headings (Inter style) */
h1, h2, h3 {
letter-spacing: -0.025em;
}
@theme inline {
--font-sans: var(--font-sans);
--color-color-1: hsl(var(--color-1));
--color-color-2: hsl(var(--color-2));
--color-color-3: hsl(var(--color-3));
--color-color-4: hsl(var(--color-4));
--color-color-5: hsl(var(--color-5));
--animate-rainbow: rainbow var(--speed, 2s) infinite linear;
@keyframes rainbow {
0% { background-position: 0%; }
100% { background-position: 200%; }
}
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
/* Docs page: match homepage typography and spacing */
.fd-page article {
font-size: 15px;
line-height: 1.7;
}
/* Docs headings: tighter letter-spacing, match homepage weight */
.fd-page article h2 {
font-weight: 700;
margin-top: 2.5rem;
}
.fd-page article h3 {
font-weight: 600;
margin-top: 2rem;
}
/* Docs links: use primary color */
.fd-page article a:not([class]) {
color: var(--primary);
text-decoration: underline;
text-underline-offset: 2px;
}
.fd-page article a:not([class]):hover {
opacity: 0.8;
}
/* Docs table: cleaner borders */
.fd-page article table {
border-radius: 0;
}
/* Docs sidebar: match homepage font weight */
[data-fd-sidebar] a {
font-weight: 500;
}
+18
View File
@@ -0,0 +1,18 @@
import './global.css';
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { Geist } from "next/font/google";
const geist = Geist({subsets:['latin'],variable:'--font-sans'});
export const metadata: Metadata = {
title: {
default: 'CodePilot',
template: '%s | CodePilot',
},
description: 'A native desktop GUI client for Claude Code',
};
export default function RootLayout({ children }: { children: ReactNode }) {
return children;
}
@@ -0,0 +1,85 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { Globe, Github, Search } from 'lucide-react';
import { useSearchContext } from 'fumadocs-ui/contexts/search';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { siteConfig } from '@/lib/site.config';
export function DocsTopNav({ locale }: { locale: string }) {
const router = useRouter();
const pathname = usePathname();
const { setOpenSearch } = useSearchContext();
const handleLocaleChange = (newLocale: string | null) => {
if (!newLocale) return;
let newPath: string;
if (locale === 'en') {
newPath = `/${newLocale}${pathname}`;
} else {
newPath = newLocale === 'en'
? pathname.replace(`/${locale}`, '') || '/'
: pathname.replace(`/${locale}`, `/${newLocale}`);
}
router.push(newPath);
};
return (
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-16 items-center justify-between px-6 lg:px-10">
{/* Left: Logo + Product name */}
<Link href={`/${locale === 'en' ? '' : locale}`} className="flex items-center gap-3">
<Image
src="/logo.png"
alt="CodePilot"
width={30}
height={30}
className="h-[30px] w-[30px]"
/>
<span className="text-lg font-bold">CodePilot</span>
</Link>
{/* Right: Search + GitHub + Language switcher */}
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => setOpenSearch(true)}
className="inline-flex items-center rounded-md p-2 text-muted-foreground transition-colors hover:text-foreground"
>
<Search className="h-[18px] w-[18px]" />
</button>
<a
href={siteConfig.repo.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center rounded-md p-2 text-muted-foreground transition-colors hover:text-foreground"
>
<Github className="h-[18px] w-[18px]" />
</a>
<Select defaultValue={locale} onValueChange={handleLocaleChange}>
<SelectTrigger size="sm" className="ml-1 h-8 gap-1.5 rounded-md border-border bg-transparent px-2.5 text-sm">
<Globe className="h-4 w-4 text-muted-foreground" />
<SelectValue>
{locale === 'zh' ? '简体中文' : 'English'}
</SelectValue>
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh">简体中文</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</header>
);
}
@@ -0,0 +1,15 @@
import { siteConfig } from '@/lib/site.config';
export function DownloadButton({ label = 'Download Latest Release' }: { label?: string }) {
return (
<a
href={`${siteConfig.repo.releases}/latest`}
target="_blank"
rel="noopener noreferrer"
className="mt-4 mb-6 inline-flex items-center gap-2 rounded-full bg-foreground px-6 py-3 text-sm font-semibold text-background transition-opacity hover:opacity-80"
>
{label}
<span aria-hidden="true">→</span>
</a>
);
}
@@ -0,0 +1,44 @@
'use client';
import { useRouter, usePathname } from 'next/navigation';
import { Globe } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
export function LanguageSwitcher({ locale }: { locale: string }) {
const router = useRouter();
const pathname = usePathname();
const handleChange = (newLocale: string | null) => {
if (!newLocale) return;
let newPath: string;
if (locale === 'en') {
newPath = `/${newLocale}${pathname}`;
} else {
newPath = newLocale === 'en'
? pathname.replace(`/${locale}`, '') || '/'
: pathname.replace(`/${locale}`, `/${newLocale}`);
}
router.push(newPath);
};
return (
<Select defaultValue={locale} onValueChange={handleChange}>
<SelectTrigger size="sm" className="h-7 gap-1.5 rounded-md border-border bg-transparent px-2 text-xs">
<Globe className="h-3.5 w-3.5 text-muted-foreground" />
<SelectValue>
{locale === 'zh' ? '简体中文' : 'English'}
</SelectValue>
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh">简体中文</SelectItem>
</SelectContent>
</Select>
);
}
@@ -0,0 +1,16 @@
import Image from 'next/image';
export function NavTitle() {
return (
<span className="flex items-center gap-2.5">
<Image
src="/logo.png"
alt="CodePilot"
width={24}
height={24}
className="h-6 w-6"
/>
<span className="text-[15px] font-bold">CodePilot</span>
</span>
);
}
@@ -0,0 +1,31 @@
import type { MarketingContent } from '../../../content/marketing/en';
export function AudienceSection({
content,
}: {
content: MarketingContent['audience'];
}) {
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
<h2 className="text-2xl font-bold text-foreground md:text-3xl">
{content.title}
</h2>
<p className="mt-3 text-muted-foreground">{content.subtitle}</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{content.items.map((item) => (
<div
key={item.title}
className="rounded-2xl border border-border bg-card p-5 shadow-sm"
>
<h3 className="font-semibold text-foreground">{item.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{item.description}
</p>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,505 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { HugeiconsIcon } from '@hugeicons/react';
import {
Message02Icon,
ZapIcon,
Plug01Icon,
Image01Icon,
Wifi01Icon,
Settings02Icon,
Search01Icon,
PlusSignIcon,
FolderOpenIcon,
ArrowUp02Icon,
ArrowDown01Icon,
CommandLineIcon,
AiUserIcon,
SecurityCheckIcon,
} from '@hugeicons/core-free-icons';
import { Check, FileText } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
/* ------------------------------------------------------------------ */
/* Chat message data */
/* ------------------------------------------------------------------ */
interface ChatMsg {
id: number;
role: 'user' | 'assistant';
text: string;
delay: number;
badge?: { icon: 'skill' | 'mcp' | 'bridge' | 'agent'; label: string };
tool?: { name: string; status: 'done' };
}
const MESSAGES: ChatMsg[] = [
{
id: 1,
role: 'user',
text: 'Refactor the auth module to use JWT. Follow the team conventions.',
delay: 800,
badge: { icon: 'skill', label: '/refactor' },
},
{
id: 2,
role: 'assistant',
text: 'Reading project conventions from persona memory\u2026',
delay: 1400,
},
{
id: 3,
role: 'assistant',
text: 'Refactored src/auth/ to JWT. Updated 4 files, added refresh-token rotation, kept the existing middleware contract.',
delay: 2000,
tool: { name: 'Edit 4 files', status: 'done' },
},
{
id: 4,
role: 'user',
text: "Connect the Figma MCP server \u2014 I need design tokens.",
delay: 1400,
badge: { icon: 'mcp', label: 'Figma MCP' },
},
{
id: 5,
role: 'assistant',
text: "Connected to Figma MCP. Pulled 48 tokens \u2192 src/theme/tokens.ts updated.",
delay: 1800,
tool: { name: 'figma:pull-tokens', status: 'done' },
},
{
id: 6,
role: 'user',
text: "I'm heading out. Forward replies to Telegram.",
delay: 1400,
badge: { icon: 'bridge', label: 'Telegram' },
},
{
id: 7,
role: 'assistant',
text: "Bridge active \u2014 I'll keep working and send updates to Telegram.",
delay: 1600,
},
{
id: 8,
role: 'user',
text: 'Generate a hero section with the new tokens.',
delay: 1800,
badge: { icon: 'agent', label: 'Design Agent' },
},
{
id: 9,
role: 'assistant',
text: 'Created src/app/hero/ with responsive layout and the new palette. Preview ready.',
delay: 2200,
tool: { name: 'Create 3 files', status: 'done' },
},
];
const SESSIONS = [
{ name: 'Auth JWT refactor', active: true, time: 'now' },
{ name: 'API rate limiting', active: false, time: '2h' },
{ name: 'Dashboard redesign', active: false, time: '5h' },
{ name: 'CI pipeline fix', active: false, time: '1d' },
{ name: 'Onboarding flow', active: false, time: '2d' },
];
const WORKSPACE_FILES = [
{ name: 'src/auth/jwt.ts', status: 'modified' },
{ name: 'src/auth/middleware.ts', status: 'modified' },
{ name: 'src/auth/refresh.ts', status: 'added' },
{ name: 'src/theme/tokens.ts', status: 'modified' },
{ name: 'src/app/hero/page.tsx', status: 'added' },
];
/*
* The demo uses the real product's purple primary color via CSS custom
* properties scoped to the demo container, so it visually matches the
* actual CodePilot app regardless of the site's blue brand primary.
*/
const DEMO_THEME_VARS = {
'--primary': 'oklch(0.546 0.245 262.881)',
'--primary-foreground': 'oklch(0.985 0.001 106.423)',
'--ring': 'oklch(0.546 0.245 262.881)',
} as React.CSSProperties;
/* ------------------------------------------------------------------ */
/* Sub-components */
/* ------------------------------------------------------------------ */
function BadgeIcon({ type }: { type: string }) {
const cls = 'h-3 w-3';
switch (type) {
case 'skill': return <HugeiconsIcon icon={ZapIcon} className={cls} />;
case 'mcp': return <HugeiconsIcon icon={Plug01Icon} className={cls} />;
case 'bridge': return <HugeiconsIcon icon={Wifi01Icon} className={cls} />;
case 'agent': return <HugeiconsIcon icon={AiUserIcon} className={cls} />;
default: return null;
}
}
function Badge({ badge }: { badge: NonNullable<ChatMsg['badge']> }) {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-primary/20 bg-primary/10 px-2 py-[2px] text-[10px] font-medium text-primary">
<BadgeIcon type={badge.icon} />
{badge.label}
</span>
);
}
function TypingDots() {
return (
<span className="inline-flex gap-[3px]">
{[0, 1, 2].map((i) => (
<motion.span
key={i}
className="block h-[4px] w-[4px] rounded-full bg-muted-foreground/40"
animate={{ opacity: [0.3, 1, 0.3] }}
transition={{ duration: 0.9, repeat: Infinity, delay: i * 0.15 }}
/>
))}
</span>
);
}
/* ------------------------------------------------------------------ */
/* Icon sidebar — real: NavRail.tsx w-14, h-9 w-9 buttons, gap-1 */
/* ------------------------------------------------------------------ */
const NAV_ITEMS = [
{ icon: Message02Icon, label: 'Chats', active: true },
{ icon: ZapIcon, label: 'Skills', active: false },
{ icon: Plug01Icon, label: 'MCP', active: false },
{ icon: Image01Icon, label: 'Gallery', active: false },
{ icon: Wifi01Icon, label: 'Bridge', active: false },
];
function IconSidebar() {
return (
<aside className="flex w-14 shrink-0 flex-col items-center bg-sidebar pb-3 pt-10">
<nav className="flex flex-1 flex-col items-center gap-1">
{NAV_ITEMS.map((item) => (
<Button
key={item.label}
variant="ghost"
size="icon"
className={`h-9 w-9 ${
item.active
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-muted-foreground/50'
}`}
>
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
</Button>
))}
</nav>
<div className="mt-auto flex flex-col items-center gap-2">
<div className="relative">
<Button variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground/50">
<HugeiconsIcon icon={Settings02Icon} className="h-4 w-4" />
</Button>
<span className="pointer-events-none absolute right-0.5 top-0.5 h-2 w-2 rounded-full bg-primary" />
</div>
</div>
</aside>
);
}
/* ------------------------------------------------------------------ */
/* Session sidebar — real: ChatListPanel 240px, bg-sidebar */
/* ------------------------------------------------------------------ */
function SessionSidebar() {
return (
<div className="hidden w-[190px] shrink-0 flex-col bg-sidebar sm:flex">
{/* Top spacing for macOS traffic lights area */}
<div className="h-10 mt-3" />
{/* New chat button — real: h-8 outline with PlusSignIcon */}
<div className="px-2.5">
<Button variant="outline" size="sm" className="w-full justify-start gap-1.5 rounded-md pl-2 text-[10px]">
<HugeiconsIcon icon={PlusSignIcon} className="h-3 w-3" />
New Chat
</Button>
</div>
{/* Search */}
<div className="relative mt-2 px-2.5">
<HugeiconsIcon icon={Search01Icon} className="absolute left-[1.15rem] top-1/2 z-10 h-3 w-3 -translate-y-1/2 text-muted-foreground/40" />
<Input className="h-7 rounded-md pl-7 !text-[10px]" placeholder="Search..." readOnly />
</div>
{/* Project folder — real: FolderOpenIcon, text-[13px] font-medium */}
<div className="mt-3 flex items-center gap-1.5 px-3 py-1.5">
<HugeiconsIcon icon={FolderOpenIcon} className="h-4 w-4 text-muted-foreground/60" />
<span className="text-[13px] font-medium text-sidebar-foreground">My-Project</span>
</div>
{/* Session list — real: text-[13px], active=bg-sidebar-accent */}
<div className="mt-1 flex-1 overflow-hidden pl-4 pr-1.5">
{SESSIONS.map((s) => (
<div
key={s.name}
className={`flex items-center justify-between rounded-md px-2 py-2 ${
s.active
? 'bg-sidebar-accent font-medium text-sidebar-accent-foreground'
: 'text-sidebar-foreground/60'
}`}
>
<span className="truncate text-[13px] leading-tight">{s.name}</span>
<span className="ml-2 shrink-0 text-[11px] text-muted-foreground/40">{s.time}</span>
</div>
))}
</div>
<div className="shrink-0" />
</div>
);
}
/* ------------------------------------------------------------------ */
/* Right workspace panel — real: RightPanel 288px, bg-background */
/* ------------------------------------------------------------------ */
function WorkspacePanel({ fileCount }: { fileCount: number }) {
const visible = WORKSPACE_FILES.slice(0, fileCount);
return (
<div className="hidden w-[180px] shrink-0 flex-col bg-background lg:flex">
{/* Section title — real: text-[11px] font-semibold uppercase tracking-wider */}
<div className="px-4 pb-2 pt-4">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Tasks</span>
<div className="mt-2 space-y-2">
<label className="flex items-center gap-2">
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-[3px] border border-foreground bg-foreground text-background">
<Check className="h-2.5 w-2.5" strokeWidth={3} />
</span>
<span className="text-[11px] text-muted-foreground line-through">Auth refactor</span>
</label>
<label className="flex items-center gap-2">
<span className="h-3.5 w-3.5 shrink-0 rounded-[3px] border border-input" />
<span className="text-[11px] text-muted-foreground">Hero generation</span>
</label>
</div>
</div>
{/* Files */}
<div className="px-4 pb-1">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Files</span>
</div>
<div className="flex-1 overflow-hidden px-3">
<AnimatePresence>
{visible.map((f) => (
<motion.div
key={f.name}
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
className="flex items-center gap-1.5 rounded-md px-2 py-1"
>
<FileText className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40" />
<span className="truncate text-[11px] text-muted-foreground">{f.name}</span>
<span className={`ml-auto shrink-0 text-[10px] ${
f.status === 'added' ? 'text-green-500' : 'text-amber-500'
}`}>
{f.status === 'added' ? '+' : 'M'}
</span>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Main ChatDemo */
/* ------------------------------------------------------------------ */
export function ChatDemo() {
const [visibleCount, setVisibleCount] = useState(0);
const [typingId, setTypingId] = useState<number | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const hasStarted = useRef(false);
const containerRef = useRef<HTMLDivElement>(null);
const fileCount = Math.min(
WORKSPACE_FILES.length,
visibleCount >= 9 ? 5 : visibleCount >= 5 ? 4 : visibleCount >= 3 ? 2 : 0,
);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasStarted.current) {
hasStarted.current = true;
playMessages();
}
},
{ threshold: 0.2 },
);
observer.observe(el);
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function playMessages() {
let elapsed = 0;
MESSAGES.forEach((msg, i) => {
elapsed += msg.delay;
if (msg.role === 'assistant') {
setTimeout(() => setTypingId(msg.id), elapsed - 600);
}
setTimeout(() => {
setTypingId(null);
setVisibleCount(i + 1);
}, elapsed);
});
setTimeout(() => {
setVisibleCount(0);
setTypingId(null);
hasStarted.current = false;
const el = containerRef.current;
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasStarted.current) {
hasStarted.current = true;
playMessages();
}
},
{ threshold: 0.2 },
);
obs.observe(el);
}, elapsed + 4000);
}
useEffect(() => {
const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [visibleCount, typingId]);
const visible = MESSAGES.slice(0, visibleCount);
return (
<div
ref={containerRef}
className="flex overflow-hidden rounded-t-2xl bg-background shadow-[0_-2px_20px_0_rgb(0_0_0/0.06),0_4px_24px_0_rgb(0_0_0/0.08)]"
style={{ height: 520, ...DEMO_THEME_VARS }}
>
{/* Left: icon bar — real NavRail */}
<IconSidebar />
{/* Left: session sidebar — real ChatListPanel */}
<SessionSidebar />
{/* Center: chat area */}
<div className="flex min-w-0 flex-1 flex-col bg-background">
{/* Chat header — real: h-11 border-b border-border/50 */}
<div className="flex h-11 items-center justify-between px-4">
<span className="text-[13px] font-medium text-foreground">Auth JWT refactor</span>
</div>
{/* Messages */}
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div className="flex flex-col gap-3">
<AnimatePresence mode="popLayout">
{visible.map((msg) => (
<motion.div
key={msg.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
>
{msg.badge && (
<div className="mb-1">
<Badge badge={msg.badge} />
</div>
)}
<div
className={`max-w-[88%] rounded-2xl px-3.5 py-2 text-[12px] leading-relaxed ${
msg.role === 'user'
? 'bg-accent text-accent-foreground'
: 'text-foreground'
}`}
>
{msg.text}
{msg.tool && (
<div className="mt-1 flex items-center gap-1 text-[10px] text-green-600">
<Check className="h-2.5 w-2.5" />
{msg.tool.name}
</div>
)}
</div>
</motion.div>
))}
{typingId !== null && (
<motion.div
key="typing"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex items-start"
>
<div className="rounded-2xl px-3.5 py-2">
<TypingDots />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* Input bar — real: InputGroup rounded-2xl border-input shadow-md dark:bg-input/30 */}
<div className="px-3 pb-1.5">
<div className="rounded-xl border border-input shadow-sm dark:bg-input/30">
{/* Textarea row */}
<div className="flex items-center px-3.5 py-3">
<span className="flex-1 text-xs text-muted-foreground/50">Message Claude...</span>
<Button size="icon-xs" className="shrink-0 rounded-full bg-primary text-primary-foreground hover:bg-primary/80">
<HugeiconsIcon icon={ArrowUp02Icon} className="h-3.5 w-3.5" />
</Button>
</div>
{/* PromptInputFooter — real: gap-1, icon buttons h-6 w-6 */}
<div className="flex items-center justify-between gap-1 px-2.5 pb-2.5">
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon-xs" className="text-muted-foreground/50">
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon-xs" className="text-muted-foreground/50">
<HugeiconsIcon icon={CommandLineIcon} className="h-3.5 w-3.5" />
</Button>
<div className="flex items-center gap-1 rounded-md px-1.5 py-1 font-mono text-[10px] text-muted-foreground/60">
claude-sonnet-4
<HugeiconsIcon icon={ArrowDown01Icon} className="h-2.5 w-2.5" />
</div>
</div>
</div>
</div>
{/* ChatComposerActionBar — below input */}
<div className="flex items-center justify-between px-2 pb-1 pt-1.5">
<span className="flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] text-muted-foreground/50">
<HugeiconsIcon icon={AiUserIcon} className="h-3.5 w-3.5" />
Design Agent
</span>
<span className="flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] text-muted-foreground/50">
<HugeiconsIcon icon={SecurityCheckIcon} className="h-3.5 w-3.5" />
Default
</span>
</div>
</div>
</div>
{/* Right: workspace panel — real RightPanel */}
<WorkspacePanel fileCount={fileCount} />
</div>
);
}
@@ -0,0 +1,40 @@
import Link from 'next/link';
import { ArrowRight } from 'lucide-react';
import type { MarketingContent } from '../../../content/marketing/en';
export function DocsEntrySection({
content,
locale,
}: {
content: MarketingContent['docs'];
locale: string;
}) {
const prefix = locale === 'en' ? '' : `/${locale}`;
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
<h2 className="mb-8 text-2xl font-bold text-foreground md:text-3xl">
{content.title}
</h2>
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3">
{content.cards.map((card) => (
<Link
key={card.href}
href={`${prefix}${card.href}`}
className="group flex items-start justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-shadow hover:shadow-md"
>
<div>
<h3 className="font-semibold text-foreground">{card.title}</h3>
<p className="mt-1 text-sm text-muted-foreground">
{card.description}
</p>
</div>
<ArrowRight className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
</Link>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,72 @@
'use client';
import { useState } from 'react';
import { Plus, Minus } from 'lucide-react';
import type { MarketingContent } from '../../../content/marketing/en';
function FAQItem({ item, isOpen, onToggle, isLast }: {
item: { q: string; a: string };
isOpen: boolean;
onToggle: () => void;
isLast: boolean;
}) {
return (
<div className={!isLast ? 'border-b border-border' : ''}>
<button
onClick={onToggle}
className="flex w-full items-center justify-between py-6 text-left"
>
<span className="text-base font-semibold text-foreground">
{item.q}
</span>
{isOpen ? (
<Minus className="h-5 w-5 shrink-0 text-muted-foreground" />
) : (
<Plus className="h-5 w-5 shrink-0 text-muted-foreground" />
)}
</button>
<div
className="grid transition-[grid-template-rows] duration-200"
style={{ gridTemplateRows: isOpen ? '1fr' : '0fr' }}
>
<div className="overflow-hidden">
<p className="pb-6 text-base font-semibold leading-relaxed text-muted-foreground">
{item.a}
</p>
</div>
</div>
</div>
);
}
export function FAQSection({
content,
}: {
content: MarketingContent['faq'];
}) {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
{/* Two-tone title */}
<h2 className="max-w-2xl text-2xl font-bold leading-snug md:text-3xl">
<span className="text-foreground">{content.title}</span>{' '}
<span className="text-muted-foreground">{content.titleLight}</span>
</h2>
<div className="mt-10 border border-border px-6">
{content.items.map((item, i) => (
<FAQItem
key={i}
item={item}
isOpen={openIndex === i}
isLast={i === content.items.length - 1}
onToggle={() => setOpenIndex(openIndex === i ? null : i)}
/>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,43 @@
import { CapabilityIcon } from './IconMap';
import type { MarketingContent } from '../../../content/marketing/en';
export function FeaturesSection({
content,
}: {
content: MarketingContent['features'];
}) {
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
{/* Two-tone title: dark + light */}
<h2 className="max-w-2xl text-2xl font-bold leading-snug md:text-3xl">
<span className="text-foreground">{content.title}</span>{' '}
<span className="text-muted-foreground">{content.titleLight}</span>
</h2>
{/* Feature cards — no border, larger description, larger icon without bg */}
<div className="mt-14 grid gap-x-12 gap-y-10 sm:grid-cols-2">
{content.items.map((item) => (
<div key={item.title} className="group">
<CapabilityIcon
name={item.icon}
className="h-7 w-7 text-foreground"
/>
<h3 className="mt-4 text-lg font-semibold text-foreground">
{item.title}
{item.badge && (
<span className="ml-2 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
{item.badge}
</span>
)}
</h3>
<p className="mt-2 text-[17px] font-semibold leading-relaxed text-muted-foreground">
{item.description}
</p>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,38 @@
import Link from 'next/link';
import type { MarketingContent } from '../../../content/marketing/en';
import { RainbowButton } from '@/components/ui/rainbow-button';
export function FinalCTA({
content,
locale,
}: {
content: MarketingContent['cta'];
locale: string;
}) {
const prefix = locale === 'en' ? '' : `/${locale}`;
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
<h2 className="max-w-2xl text-2xl font-bold leading-snug md:text-3xl">
<span className="text-foreground">{content.title}</span>{' '}
<span className="text-muted-foreground">{content.description}</span>
</h2>
<div className="mt-8 flex items-center gap-4">
<Link href={`${prefix}/docs/installation`}>
<RainbowButton className="h-12 rounded-full px-10 text-base">
{content.primary}
</RainbowButton>
</Link>
<Link
href={`${prefix}/docs`}
className="text-base font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
{content.secondary}
</Link>
</div>
</div>
</section>
);
}
@@ -0,0 +1,75 @@
import Link from 'next/link';
import Image from 'next/image';
import type { MarketingContent } from '../../../content/marketing/en';
import { ChatDemo } from './ChatDemo';
import { TypewriterWords } from './TypewriterWords';
import { RainbowButton } from '@/components/ui/rainbow-button';
import { FlickeringGrid } from '@/components/ui/flickering-grid';
export function HeroSection({
content,
locale,
}: {
content: MarketingContent['hero'];
locale: string;
}) {
const prefix = locale === 'en' ? '' : `/${locale}`;
return (
<section className="relative overflow-hidden">
{/* Blue-gray gradient background */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-transparent to-muted" />
<div className="relative">
{/* Logo + Title + CTA */}
<div className="mx-auto max-w-[800px] px-6 pt-8 text-center md:pt-10 lg:pt-12">
<Image
src="/logo.png"
alt="CodePilot"
width={80}
height={80}
className="mx-auto h-24 w-24 md:h-28 md:w-28"
priority
/>
<h1 className="mt-5 text-[28px] font-semibold leading-snug text-foreground md:text-[34px] lg:text-[40px]">
{content.tagline}{' '}
<TypewriterWords locale={locale} />
</h1>
<div className="mt-7 flex items-center justify-center">
<Link href={`${prefix}/docs/installation`}>
<RainbowButton className="h-14 rounded-full px-14 text-lg">
{content.cta}
</RainbowButton>
</Link>
</div>
</div>
{/* Animated chat demo with flickering grid background */}
<div className="relative mx-auto mt-12 max-w-[1000px] px-6 md:mt-14">
{/* FlickeringGrid — shifted down, masked with radial gradient for soft edges */}
<div
className="absolute -inset-x-24 -bottom-32 top-24 z-0"
style={{
maskImage: 'radial-gradient(ellipse at center, black 30%, transparent 75%)',
WebkitMaskImage: 'radial-gradient(ellipse at center, black 30%, transparent 75%)',
}}
>
<FlickeringGrid
className="absolute inset-0 size-full"
squareSize={4}
gridGap={6}
color="#6B7280"
maxOpacity={0.4}
flickerChance={0.1}
/>
</div>
<div className="relative z-10">
<ChatDemo />
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,46 @@
import {
MessageSquare,
Settings,
Plug,
Sparkles,
Radio,
FolderOpen,
Layers,
Shield,
Brain,
Bookmark,
Compass,
Code,
KeyRound,
Users,
type LucideIcon,
} from 'lucide-react';
const iconMap: Record<string, LucideIcon> = {
MessageSquare,
Settings,
Plug,
Sparkles,
Radio,
FolderOpen,
Layers,
Shield,
Brain,
Bookmark,
Compass,
Code,
Key: KeyRound,
Users,
};
export function CapabilityIcon({
name,
className,
}: {
name: string;
className?: string;
}) {
const Icon = iconMap[name];
if (!Icon) return null;
return <Icon className={className} />;
}
@@ -0,0 +1,74 @@
import { Star } from 'lucide-react';
import { CapabilityIcon } from './IconMap';
import type { MarketingContent } from '../../../content/marketing/en';
async function getStarCount(): Promise<string> {
try {
const res = await fetch('https://api.github.com/repos/op7418/CodePilot', {
next: { revalidate: 3600 },
});
if (!res.ok) return '3.4k';
const data = await res.json();
const count = data.stargazers_count;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return String(count);
} catch {
return '3.4k';
}
}
export async function IntegrationsSection({
content,
}: {
content: MarketingContent['openSource'];
locale?: string;
}) {
const stars = await getStarCount();
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
{/* Two-tone title */}
<h2 className="max-w-2xl text-2xl font-bold leading-snug md:text-3xl">
<span className="text-foreground">{content.title}</span>{' '}
<span className="text-muted-foreground">{content.titleLight}</span>
</h2>
{/* GitHub Star button */}
<div className="mt-8">
<a
href={content.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-5 py-2.5 text-sm font-semibold text-foreground shadow-sm transition-shadow hover:shadow-md"
>
<Star className="h-4 w-4 fill-amber-400 text-amber-400" />
{content.githubCta}
<span className="ml-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-bold text-muted-foreground">
{stars}
</span>
</a>
</div>
{/* Highlight cards */}
<div className="mt-14 grid gap-x-12 gap-y-10 sm:grid-cols-3">
{content.highlights.map((item) => (
<div key={item.title}>
<CapabilityIcon
name={item.icon}
className="h-7 w-7 text-foreground"
/>
<h3 className="mt-4 text-lg font-semibold text-foreground">
{item.title}
</h3>
<p className="mt-2 text-[17px] font-semibold leading-relaxed text-muted-foreground">
{item.description}
</p>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,35 @@
import type { MarketingContent } from '../../../content/marketing/en';
export function QuickstartSection({
content,
}: {
content: MarketingContent['quickstart'];
}) {
return (
<section className="py-20 md:py-28">
<div className="mx-auto max-w-[800px] px-6">
<h2 className="text-2xl font-bold text-foreground md:text-3xl">
{content.title}
</h2>
<div className="mt-12 grid gap-8 md:grid-cols-3 md:gap-4">
{content.steps.map((step) => (
<div
key={step.step}
className="flex gap-4 md:flex-col md:gap-0"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-bold text-primary-foreground md:mb-4">
{step.step}
</span>
<div>
<h3 className="font-semibold text-foreground">{step.title}</h3>
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,114 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import type { MarketingContent } from '../../../content/marketing/en';
type ScreenshotItem = MarketingContent['hero']['screenshots'][number];
export function ScreenshotCarousel({
items,
}: {
items: ScreenshotItem[];
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState(0);
const isPaused = useRef(false);
const scrollToIndex = useCallback((index: number) => {
const container = scrollRef.current;
if (!container) return;
const slide = container.children[index] as HTMLElement | undefined;
if (!slide) return;
container.scrollTo({
left: slide.offsetLeft - (container.offsetWidth - slide.offsetWidth) / 2,
behavior: 'smooth',
});
}, []);
useEffect(() => {
const container = scrollRef.current;
if (!container) return;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const idx = Array.from(container.children).indexOf(entry.target as HTMLElement);
if (idx >= 0) setActiveIndex(idx);
}
}
},
{ root: container, threshold: 0.6 },
);
Array.from(container.children).forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, [items.length]);
useEffect(() => {
const interval = setInterval(() => {
if (isPaused.current) return;
setActiveIndex((prev) => {
const next = (prev + 1) % items.length;
scrollToIndex(next);
return next;
});
}, 5500);
return () => clearInterval(interval);
}, [items.length, scrollToIndex]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowLeft') scrollToIndex(Math.max(0, activeIndex - 1));
else if (e.key === 'ArrowRight') scrollToIndex(Math.min(items.length - 1, activeIndex + 1));
};
return (
<div>
<div
ref={scrollRef}
className="flex snap-x snap-mandatory gap-4 overflow-x-auto scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
onMouseEnter={() => { isPaused.current = true; }}
onMouseLeave={() => { isPaused.current = false; }}
onFocus={() => { isPaused.current = true; }}
onBlur={() => { isPaused.current = false; }}
onKeyDown={handleKeyDown}
tabIndex={0}
role="region"
aria-label="Screenshot carousel"
>
{items.map((item, i) => (
<figure
key={item.src}
className="w-full flex-shrink-0 snap-center"
>
<div className="overflow-hidden rounded-t-xl bg-card shadow-sm">
<Image
src={item.src}
alt={item.alt}
width={800}
height={348}
className="h-auto w-full"
priority={i === 0}
loading={i === 0 ? undefined : 'lazy'}
/>
</div>
</figure>
))}
</div>
{/* Dots only — dark gray active */}
<div className="mt-4 flex items-center justify-center gap-1.5 pb-8">
{items.map((item, i) => (
<button
key={item.src}
type="button"
onClick={() => scrollToIndex(i)}
className={`h-1.5 rounded-full transition-all ${
i === activeIndex ? 'w-5 bg-foreground' : 'w-1.5 bg-muted-foreground/25'
}`}
aria-label={`Go to slide ${i + 1}`}
/>
))}
</div>
</div>
);
}
@@ -0,0 +1,100 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Github } from 'lucide-react';
export function ScrollNav({ locale }: { locale: string }) {
const [visible, setVisible] = useState(false);
const [stars, setStars] = useState<string | null>(null);
const prefix = locale === 'en' ? '' : `/${locale}`;
useEffect(() => {
const onScroll = () => setVisible(window.scrollY > 80);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
fetch('https://api.github.com/repos/op7418/CodePilot', { next: { revalidate: 3600 } } as RequestInit)
.then((r) => r.json())
.then((d) => {
if (d.stargazers_count != null) {
const count = Number(d.stargazers_count);
setStars(count >= 1000 ? `${(count / 1000).toFixed(1)}k` : String(count));
}
})
.catch(() => {});
}, []);
return (
<nav
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ${
visible
? 'translate-y-0'
: '-translate-y-full'
}`}
style={{
background: 'linear-gradient(to bottom, var(--color-background) 0%, transparent 100%)',
}}
>
<div className="mx-auto flex h-14 items-center justify-between px-6 lg:px-10">
{/* Left: Logo + Name */}
<Link
href={`${prefix}/`}
className="flex items-center gap-2"
>
<Image
src="/logo.png"
alt="CodePilot"
width={28}
height={28}
className="h-7 w-7"
/>
<span className="text-[15px] font-bold text-foreground">
CodePilot
</span>
</Link>
{/* Right: Links + Download pill */}
<div className="flex items-center gap-5">
<Link
href={`${prefix}/docs`}
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
Docs
</Link>
{/* Language switcher */}
<Link
href={locale === 'en' ? '/zh' : '/en'}
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{locale === 'en' ? '中文' : 'EN'}
</Link>
{/* GitHub + stars */}
<a
href="https://github.com/op7418/CodePilot"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<Github className="h-4 w-4" />
{stars && <span>{stars}</span>}
</a>
{/* Download pill button */}
<Link
href={`${prefix}/docs/installation`}
className="rounded-full bg-foreground px-4 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-80"
>
{locale === 'zh' ? '下载' : 'Download'}
</Link>
</div>
</div>
</nav>
);
}
@@ -0,0 +1,29 @@
import type { MarketingContent } from '../../../content/marketing/en';
export function SiteFooter({
content,
}: {
content: MarketingContent['footer'];
}) {
return (
<footer>
<div className="mx-auto flex max-w-[800px] flex-col items-center justify-between gap-4 px-6 py-8 text-sm text-muted-foreground sm:flex-row">
<span>{content.copyright}</span>
<nav className="flex gap-6">
{content.links.map((link) => (
<a
key={link.url}
href={link.url}
className="transition-colors hover:text-foreground"
{...(link.url.startsWith('http')
? { target: '_blank', rel: 'noopener noreferrer' }
: {})}
>
{link.text}
</a>
))}
</nav>
</div>
</footer>
);
}
@@ -0,0 +1,83 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
interface WordItem {
text: string;
color: string;
}
const WORDS_EN: WordItem[] = [
{ text: 'Development', color: '#3b82f6' }, // blue
{ text: 'Design', color: '#f59e0b' }, // amber
{ text: 'Writing', color: '#10b981' }, // emerald
{ text: 'Research', color: '#8b5cf6' }, // violet
{ text: 'Debugging', color: '#ef4444' }, // red
{ text: 'Prototyping', color: '#06b6d4' }, // cyan
];
const WORDS_ZH: WordItem[] = [
{ text: '开发', color: '#3b82f6' },
{ text: '设计', color: '#f59e0b' },
{ text: '写作', color: '#10b981' },
{ text: '调研', color: '#8b5cf6' },
{ text: '调试', color: '#ef4444' },
{ text: '原型', color: '#06b6d4' },
];
export function TypewriterWords({ locale }: { locale: string }) {
const words = locale === 'zh' ? WORDS_ZH : WORDS_EN;
const [index, setIndex] = useState(0);
const [displayed, setDisplayed] = useState('');
const [isDeleting, setIsDeleting] = useState(false);
const current = words[index];
const tick = useCallback(() => {
const full = current.text;
if (!isDeleting) {
// Typing
const next = full.slice(0, displayed.length + 1);
setDisplayed(next);
if (next === full) {
// Pause then start deleting
setTimeout(() => setIsDeleting(true), 2000);
return;
}
} else {
// Deleting
const next = full.slice(0, displayed.length - 1);
setDisplayed(next);
if (next === '') {
setIsDeleting(false);
setIndex((prev) => (prev + 1) % words.length);
return;
}
}
}, [current.text, displayed, isDeleting, words.length]);
useEffect(() => {
const speed = isDeleting ? 60 : 100;
const timer = setTimeout(tick, speed);
return () => clearTimeout(timer);
}, [tick, isDeleting]);
return (
<span className="inline-flex items-baseline">
<span
className="font-semibold transition-colors duration-300"
style={{ color: current.color }}
>
{displayed}
</span>
<motion.span
className="ml-[1px] inline-block h-[0.85em] w-[2px] translate-y-[0.05em] rounded-full"
style={{ backgroundColor: current.color }}
animate={{ opacity: [1, 0] }}
transition={{ duration: 0.6, repeat: Infinity, repeatType: 'reverse' }}
/>
</span>
);
}
@@ -0,0 +1,38 @@
import { cva } from "class-variance-authority"
export const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
+24
View File
@@ -0,0 +1,24 @@
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import type { VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { buttonVariants } from "./button-variants"
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
@@ -0,0 +1,195 @@
"use client";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
interface FlickeringGridProps {
squareSize?: number;
gridGap?: number;
flickerChance?: number;
color?: string;
width?: number;
height?: number;
className?: string;
maxOpacity?: number;
}
const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4,
gridGap = 6,
flickerChance = 0.3,
color = "rgb(0, 0, 0)",
width,
height,
className,
maxOpacity = 0.3,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return `rgba(0, 0, 0,`;
}
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d");
if (!ctx) return "rgba(255, 0, 0,";
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
return `rgba(${r}, ${g}, ${b},`;
};
return toRGBA(color);
}, [color]);
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const cols = Math.floor(width / (squareSize + gridGap));
const rows = Math.floor(height / (squareSize + gridGap));
const squares = new Float32Array(cols * rows);
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity;
}
return { cols, rows, squares, dpr };
},
[squareSize, gridGap, maxOpacity],
);
const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity;
}
}
},
[flickerChance, maxOpacity],
);
const drawGrid = useCallback(
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cols: number,
rows: number,
squares: Float32Array,
dpr: number,
) => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "transparent";
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j];
ctx.fillStyle = `${memoizedColor}${opacity})`;
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr,
);
}
}
},
[memoizedColor, squareSize, gridGap],
);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let gridParams: ReturnType<typeof setupCanvas>;
const updateCanvasSize = () => {
const newWidth = width || container.clientWidth;
const newHeight = height || container.clientHeight;
setCanvasSize({ width: newWidth, height: newHeight });
gridParams = setupCanvas(canvas, newWidth, newHeight);
};
updateCanvasSize();
let lastTime = 0;
const animate = (time: number) => {
if (!isInView) return;
const deltaTime = (time - lastTime) / 1000;
lastTime = time;
updateSquares(gridParams.squares, deltaTime);
drawGrid(
ctx,
canvas.width,
canvas.height,
gridParams.cols,
gridParams.rows,
gridParams.squares,
gridParams.dpr,
);
animationFrameId = requestAnimationFrame(animate);
};
const resizeObserver = new ResizeObserver(() => {
updateCanvasSize();
});
resizeObserver.observe(container);
const intersectionObserver = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
},
{ threshold: 0 },
);
intersectionObserver.observe(canvas);
if (isInView) {
animationFrameId = requestAnimationFrame(animate);
}
return () => {
cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
return (
<div ref={containerRef} className={`h-full w-full ${className}`}>
<canvas
ref={canvasRef}
className="pointer-events-none"
style={{
width: canvasSize.width,
height: canvasSize.height,
}}
/>
</div>
);
};
export { FlickeringGrid };
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
@@ -0,0 +1,31 @@
import React from "react";
import { cn } from "@/lib/utils";
export function RainbowButton({
children,
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={cn(
"group relative inline-flex h-11 animate-rainbow cursor-pointer items-center justify-center rounded-xl border-0 bg-[length:200%] px-8 py-2 font-medium text-primary-foreground transition-colors [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
// before styles
"before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] before:bg-[length:200%] before:[filter:blur(calc(0.8*1rem))]",
// light mode colors
"bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))]",
// dark mode colors
"dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))]",
className,
)}
{...props}
>
{children}
</button>
);
}
+201
View File
@@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
const Select = SelectPrimitive.Root
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
}
/>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
+25
View File
@@ -0,0 +1,25 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
+66
View File
@@ -0,0 +1,66 @@
"use client"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
)
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+8
View File
@@ -0,0 +1,8 @@
import { defineI18n } from 'fumadocs-core/i18n';
export const i18n = defineI18n({
defaultLanguage: 'en',
languages: ['en', 'zh'],
hideLocale: 'default-locale',
parser: 'dir',
});
+40
View File
@@ -0,0 +1,40 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { siteConfig } from '@/lib/site.config';
/**
* Shared options used by both HomeLayout and DocsLayout.
*/
export function baseOptions(locale: string): BaseLayoutProps {
return {
nav: {
url: `/${locale === 'en' ? '' : locale}`,
},
links: [
{
text: 'Docs',
url: `/${locale === 'en' ? '' : locale + '/'}docs`,
active: 'nested-url',
},
{
text: 'Download',
url: `/${locale === 'en' ? '' : locale + '/'}docs/installation`,
},
],
githubUrl: siteConfig.repo.url,
i18n: false,
themeSwitch: { enabled: false },
};
}
/**
* Homepage-specific overrides: no nav (custom scroll-nav used instead), no search.
*/
export function homeOptions(locale: string): BaseLayoutProps {
return {
...baseOptions(locale),
nav: {
enabled: false,
},
searchToggle: { enabled: false },
};
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Centralized site configuration.
* All public URLs, repo links, and external references should be sourced from here
* to avoid drift across layout, marketing content, and documentation.
*/
export const siteConfig = {
name: 'CodePilot',
description: 'A native desktop GUI client for Claude Code',
// Canonical repository
repo: {
owner: 'op7418',
name: 'CodePilot',
url: 'https://github.com/op7418/CodePilot',
releases: 'https://github.com/op7418/CodePilot/releases',
issues: 'https://github.com/op7418/CodePilot/issues',
},
// External links
links: {
discord: '#', // TODO: replace with actual Discord invite
mcp: 'https://modelcontextprotocol.io',
nodejs: 'https://nodejs.org',
anthropicConsole: 'https://console.anthropic.com',
openaiPlatform: 'https://platform.openai.com',
googleAIStudio: 'https://aistudio.google.com',
discordDev: 'https://discord.com/developers/applications',
telegramBotFather: 'https://t.me/BotFather',
feishuOpen: 'https://open.feishu.cn',
},
} as const;
export type SiteConfig = typeof siteConfig;
+20
View File
@@ -0,0 +1,20 @@
import { docs } from '@/.source';
import { loader } from 'fumadocs-core/source';
import { i18n } from './i18n';
const mdxSource = docs.toFumadocsSource();
// fumadocs-mdx v11 returns `files` as a function at runtime,
// while fumadocs-core v15 expects a plain array. Unwrap at runtime,
// cast via `any` to bridge the version mismatch.
/* eslint-disable @typescript-eslint/no-explicit-any */
const files: any = typeof mdxSource.files === 'function'
? (mdxSource.files as any)()
: mdxSource.files;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const source = loader({
baseUrl: '/docs',
source: { ...mdxSource, files },
i18n,
});
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+8
View File
@@ -0,0 +1,8 @@
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
import { i18n } from './lib/i18n';
export default createI18nMiddleware(i18n);
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'],
};
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.mdx",
".source",
"next-env.d.ts"
],
"exclude": [
"node_modules",
".next"
]
}
+45
View File
@@ -0,0 +1,45 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
],
"@/.source": [
"./.source/index.ts"
]
}
},
"include": [
"**/*.mdx",
"**/*.ts",
"**/*.tsx",
".source",
"next-env.d.ts",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
+1
View File
@@ -45,6 +45,7 @@
| 文件 | 主题 | 状态 |
|------|------|------|
| active/context-storage-migration.md | 上下文共享与存储迁移 | Phase 0 部分完成,Phase 1-3 待开始 |
| active/site-and-docs.md | 官网 + 文档站(apps/site) | Phase 0-1 进行中 |
### Completed
+198
View File
@@ -0,0 +1,198 @@
# 官网 + 文档站(apps/site)
> 创建时间:2026-03-10
> 最后更新:2026-03-10
## 状态
| Phase | 内容 | 状态 | 备注 |
|-------|------|------|------|
| Phase 0 | Monorepo 基础设施 | ✅ 已完成 | npm workspaces + 共享配置 |
| Phase 1 | apps/site 骨架 + Fumadocs 接入 | ✅ 已完成 | Next.js + Fumadocs + i18n,已 CDP 验证 |
| Phase 2 | 官网页面(首页、下载、社区) | ✅ 已完成 | marketing 页面 + 内容源 |
| Phase 3 | 首批文档内容 | ✅ 已完成 | 7 篇 x 2 语言,已 CDP 验证 |
| Phase 4 | packages/ui 抽取 | 📋 待开始 | 共享组件库 |
| Phase 5 | 桌面端适配 + CI/CD | 📋 待开始 | Electron 改 /chat、Vercel 部署 |
## 决策日志
- 2026-03-10: 官网与文档合并为一个 Next.js 应用 apps/site,与桌面应用分开部署
- 2026-03-10: 文档框架选 Fumadocs(支持 Next.js、DocsLayout、i18n、Orama 搜索)
- 2026-03-10: 部署到 Vercel,桌面应用继续 GitHub Releases
- 2026-03-10: 第一阶段只做 zh + en
- 2026-03-10: packages/ui 放 Phase 4,先让 site 跑起来再抽共享组件
- 2026-03-10: next.config.mjs(Fumadocs MDX 是 ESM-only)
- 2026-03-10: fumadocs-mdx v11 的 toFumadocsSource() 返回 files 为函数,fumadocs-core v15.8 期望数组,需手动调用
- 2026-03-10: 根 tsconfig.json 需排除 apps/ 和 packages/ 避免 typecheck 冲突
- 2026-03-10: @/.source 路径需要在 tsconfig paths 中单独映射(因为 src/ 目录结构)
## 目标目录结构
```
repo/
├── apps/
│ ├── desktop/ # 现有 Electron + Next.js(迁移自根目录)
│ └── site/ # 官网 + /docs
│ ├── app/
│ │ ├── [lang]/
│ │ │ ├── (marketing)/
│ │ │ │ ├── page.tsx # 官网首页
│ │ │ │ ├── download/page.tsx # 下载页
│ │ │ │ └── community/page.tsx # 社区页
│ │ │ ├── docs/
│ │ │ │ ├── layout.tsx # DocsLayout
│ │ │ │ └── [[...slug]]/page.tsx # Fumadocs 文档页
│ │ │ └── layout.tsx # [lang] layout + RootProvider
│ │ └── api/
│ │ └── search/route.ts # Orama 搜索 API
│ ├── content/
│ │ ├── docs/
│ │ │ ├── en/ # 英文文档 MDX
│ │ │ └── zh/ # 中文文档 MDX
│ │ └── marketing/
│ │ ├── en.ts # 英文官网文案
│ │ └── zh.ts # 中文官网文案
│ ├── lib/
│ │ ├── source.ts # Fumadocs source loader
│ │ ├── i18n.ts # i18n 配置
│ │ └── layout.shared.tsx # 共享 layout 选项
│ ├── components/ # site 专用组件
│ ├── public/ # 静态资源
│ ├── source.config.ts # Fumadocs MDX 配置
│ ├── next.config.mjs # ESM Next.js 配置
│ ├── tailwind.css # Tailwind v4 + Fumadocs preset
│ ├── tsconfig.json
│ ├── package.json
│ └── middleware.ts # i18n middleware
├── packages/
│ ├── ui/ # 共享 UI 组件(Phase 4)
│ └── config/ # 共享 tsconfig/eslint(Phase 4)
├── package.json # 根 workspace 配置
└── ... (现有根目录文件保持不动,Phase 5 再迁移)
```
**Phase 0-3 策略:** 不动现有根目录结构。apps/site 作为独立新应用添加,用 npm workspaces 管理。现有桌面应用暂时留在根目录,Phase 5 再考虑是否迁移到 apps/desktop。
## 路由设计
| 路径 | 页面 | 说明 |
|------|------|------|
| `/` | 官网首页 | 产品介绍、特性展示、CTA |
| `/download` | 下载页 | 跳转 GitHub Releases |
| `/community` | 社区页 | Discord/GitHub 链接 |
| `/docs` | 文档首页 | Getting Started |
| `/docs/[...slug]` | 文档详情 | Fumadocs MDX 渲染 |
| `/zh/` | 中文首页 | 同上中文版 |
| `/zh/docs/[...slug]` | 中文文档 | 同上中文版 |
| `/api/search` | 搜索 API | Orama 全文搜索 |
i18n 策略:en 为默认语言,URL 无前缀(`hideLocale: 'default-locale'`);zh 有 `/zh/` 前缀。
## Fumadocs 接入点
### 核心依赖
```
fumadocs-core — 内容加载、搜索、页面树、i18n
fumadocs-ui — DocsLayout、DocsPage、RootProvider、SearchDialog
fumadocs-mdx — MDX 构建时加载器、defineDocs、defineConfig
@types/mdx — MDX 类型支持
```
### 关键配置文件
- `source.config.ts` → `defineDocs({ dir: 'content/docs' })`
- `lib/source.ts` → `loader({ baseUrl: '/docs', source: docs.toFumadocsSource() })`
- `lib/i18n.ts` → `defineI18n({ defaultLanguage: 'en', languages: ['en', 'zh'] })`
- `middleware.ts` → `createI18nMiddleware(i18n)`
- `next.config.mjs` → `createMDX()(config)`
### Tailwind v4 集成
```css
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@source "../../node_modules/fumadocs-ui/dist/**/*.js";
```
## Vercel 部署拓扑
- **Project root directory:** `apps/site`
- **Framework:** Next.js
- **Production branch:** main
- **Preview:** 开启,每个 PR 自动预览
- **域名:** codepilot.xxx/ → 官网,codepilot.xxx/docs → 文档
Vercel 只部署 apps/site,桌面应用继续走 GitHub Actions + GitHub Releases。
## CI/CD 分工
| 触发条件 | 平台 | 产物 |
|----------|------|------|
| apps/site/** 变更 | Vercel | 官网 + 文档站 |
| 桌面应用代码变更 | GitHub Actions | DMG / NSIS 安装包 |
| git tag v* | GitHub Actions | Release + 安装包 |
## 首批文档目录
```
content/docs/en/
├── index.mdx # Getting Started
├── installation.mdx # 安装指南
├── providers/
│ └── index.mdx # Provider 配置(Anthropic/OpenAI/Google/Bedrock)
├── mcp/
│ └── index.mdx # MCP 插件系统
├── bridge/
│ └── index.mdx # 消息桥接(Discord/Telegram/飞书/QQ)
├── assistant-workspace/
│ └── index.mdx # 助手工作区
└── faq.mdx # 常见问题
content/docs/zh/
└── (同上,中文版)
```
## 分阶段实施计划
### Phase 0: Monorepo 基础设施
1. 根 package.json 添加 `workspaces: ["apps/*", "packages/*"]`
2. 创建 apps/site 目录和 package.json
3. 确保现有桌面应用不受影响
### Phase 1: apps/site 骨架 + Fumadocs 接入
1. 初始化 Next.js 应用(next.config.mjs)
2. 安装 fumadocs-core、fumadocs-ui、fumadocs-mdx
3. 配置 source.config.ts + lib/source.ts
4. 配置 i18n(lib/i18n.ts + middleware.ts)
5. 配置 Tailwind v4 + Fumadocs CSS preset
6. 创建 [lang] layout + RootProvider
7. 创建 docs layout(DocsLayout)+ docs page(DocsPage)
8. 创建搜索 API(Orama)
9. 放入 1 篇测试文档验证全链路
### Phase 2: 官网页面
1. 首页(产品介绍、特性网格、CTA)
2. 下载页(平台检测、跳转 Releases)
3. 社区页(Discord/GitHub 链接)
4. 共享导航栏 + 页脚
5. marketing 文案内容源(en.ts / zh.ts)
### Phase 3: 首批文档内容
1. Getting Started
2. Installation
3. Providers
4. MCP
5. Bridge
6. Assistant Workspace
7. FAQ
8. 中文翻译
### Phase 4: packages/ui 抽取
1. 从 src/components/ui/ 抽取到 packages/ui
2. 桌面应用和 site 都消费 packages/ui
3. 共享 tsconfig/eslint 配置到 packages/config
### Phase 5: 桌面端适配 + CI/CD
1. Electron loadURL 改为直接打开 /chat
2. 桌面端根路径不再 redirect(或保持 redirect)
3. Vercel 项目配置
4. GitHub Actions 调整(monorepo 感知)
+3
View File
@@ -14,6 +14,9 @@ const eslintConfig = defineConfig([
"release/**",
"dist-electron/**",
"next-env.d.ts",
// apps/site generated artifacts
"apps/site/.next/**",
"apps/site/.source/**",
]),
]);
+3901 -190
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -2,6 +2,7 @@
"name": "codepilot",
"version": "0.30.0",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"author": {
"name": "op7418",
"email": "7418@openclaw.ai"
+1 -1
View File
@@ -30,5 +30,5 @@
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules", "electron", "dist-electron", "scripts", "release"]
"exclude": ["node_modules", "electron", "dist-electron", "scripts", "release", "apps", "packages"]
}