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:
1 parent
9b2793c9d3
commit
1359758fbf
81 files changed
+8152
-191
No files matched your search
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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**.
|
||||
@@ -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
|
||||
@@ -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 可以在系统托盘模式下运行,即使窗口关闭也能保持桥接活跃
|
||||
|
||||
## 桥接模式
|
||||
|
||||
- **交互模式** — 完整的双向消息传递
|
||||
- **系统托盘模式** — 最小化到托盘,桥接保持活跃
|
||||
- **仅通知** — 只接收通知,不转发消息
|
||||
@@ -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 版本
|
||||
- 复现步骤
|
||||
- 日志中的相关错误信息
|
||||
@@ -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 内置自动更新功能。有新版本可用时,你会在应用中看到通知。也可以在**设置**的**通用**部分手动检查更新。
|
||||
@@ -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 服务器有权访问所需资源
|
||||
@@ -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 才能生效。
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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,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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export const i18n = defineI18n({
|
||||
defaultLanguage: 'en',
|
||||
languages: ['en', 'zh'],
|
||||
hideLocale: 'default-locale',
|
||||
parser: 'dir',
|
||||
});
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -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|.*\\..*).*)'],
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mdx",
|
||||
".source",
|
||||
"next-env.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 感知)
|
||||
@@ -14,6 +14,9 @@ const eslintConfig = defineConfig([
|
||||
"release/**",
|
||||
"dist-electron/**",
|
||||
"next-env.d.ts",
|
||||
// apps/site generated artifacts
|
||||
"apps/site/.next/**",
|
||||
"apps/site/.source/**",
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
Generated
+3901
-190
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,7 @@
|
||||
"name": "codepilot",
|
||||
"version": "0.30.0",
|
||||
"private": true,
|
||||
"workspaces": ["apps/*", "packages/*"],
|
||||
"author": {
|
||||
"name": "op7418",
|
||||
"email": "7418@openclaw.ai"
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user