CodeBot Wiki 是一款基于大语言模型的智能文档生成工具,能够自动解析项目代码,生成结构清晰、内容专业的项目 Wiki。它不仅覆盖项目概览、核心模块、关键逻辑等信息,还支持交互式理解,更适合团队协作与持续迭代,是理解开源项目或内部系统的理想助手!

本期解读项目地址:https://github.com/dyad-sh/dyad
项目概览
本项目是一个基于 Electron 和 React 的桌面应用程序,旨在提供一个集成开发环境(IDE)或应用生成平台。它支持多种功能,如应用创建、代码编辑、版本管理、AI 集成、GitHub 集成、Supabase 集成等。项目通过端到端测试(E2E)确保各个功能模块的稳定性和正确性。
架构与核心组件
Electron 主进程与渲染进程
项目使用 Electron 构建桌面应用,主进程负责应用的初始化、窗口管理、IPC 通信、自动更新等核心功能。渲染进程则负责 UI 的展示和用户交互。
- 主进程入口:
src/main.ts
是 Electron 应用的主进程入口文件,负责初始化应用、创建窗口、处理 IPC 消息、设置自动更新、管理深链接和用户首次运行逻辑。 - 预加载脚本:
src/preload.ts
是 Electron 应用的预加载脚本,用于在渲染进程中安全地暴露与主进程通信的能力。 - 渲染进程入口:
src/renderer.tsx
是 React 应用的入口点,负责初始化并渲染整个应用,使用@tanstack/react-router
进行路由管理,并通过PostHogProvider
集成 PostHog 进行用户行为追踪和分析。
路由管理
项目使用 @tanstack/react-router
进行路由管理,路由配置位于 src/router.ts
。通过导入多个路由模块(如首页、聊天页、设置页等),并利用 rootRoute.addChildren()
方法将它们组合成一个完整的路由树 routeTree
。
虚拟文件系统
项目中定义了一个虚拟文件系统(Virtual File System, VFS)的实现,用于在内存中模拟和管理文件系统的状态。核心是 BaseVirtualFileSystem
抽象类,提供了路径标准化、文件增删改查、重命名等基础逻辑,并通过 virtualFiles
和 deletedFiles
数据结构维护虚拟文件的状态。
备份管理
项目中实现了备份管理模块,主要职责是自动或手动备份用户设置和 SQLite 数据库,并在版本升级时触发备份操作。核心类为 BackupManager
,其通过 Electron 提供的 app.getPath("userData")
获取用户数据目录,并在其中创建一个名为 backups
的子目录作为备份存储路径。
组件选择器
项目中实现了一个用于浏览器中选择和标记 DOM 元素的组件选择器工具,通过监听鼠标移动和点击事件,识别带有特定数据属性(如 data-dyad-id
和 data-dyad-name
)的 HTML 元素,并在用户交互时显示一个半透明的高亮框(overlay)以及标签信息。
代理服务器
项目中实现了一个基于 Node.js Worker 线程的轻量级 HTTP/WS 代理服务器,用于接收客户端请求,将请求转发到预设的目标地址,并返回目标服务器的响应。
脚本与工具
项目中包含多个脚本和工具,用于自动化任务和辅助开发。例如,scripts/verify-release-assets.js
用于验证 GitHub 发布版本中是否包含所有预期二进制资产,scripts/clear_console_logs.py
用于从指定目录下的 .ts
和 .tsx
文件中移除所有的 console.log()
调用语句。
配置文件
项目中包含多个配置文件,用于定义项目的构建、样式、依赖等信息。例如,scaffold/package.json
定义了基于 Vite、React 和 TypeScript 的项目配置,scaffold/tailwind.config.ts
定义了 Tailwind CSS 的配置,scaffold/vite.config.ts
定义了 Vite 构建工具的配置。
数据流与交互
用户交互
用户通过 UI 与应用进行交互,UI 通过 IPC 与主进程通信,主进程负责处理业务逻辑并与文件系统、网络等进行交互。
数据持久化
项目中使用 SQLite 数据库进行数据持久化,备份管理模块负责在版本升级时自动备份用户设置和数据库文件。
版本管理
项目中实现了版本管理功能,支持创建快照、切换版本、恢复版本等操作。通过 Git 进行版本控制,支持原生 Git 和 Git isomorphic 实现。
AI 集成
项目中集成了多种 AI 模型,如 Ollama、LM Studio、Claude 等,支持通过 AI 进行代码生成、错误修复、问题检测等操作。
GitHub 集成
项目中实现了 GitHub 集成功能,支持通过设备流连接 GitHub、创建并同步新仓库、断开仓库连接、以及连接并同步已有仓库。
Supabase 集成
项目中实现了 Supabase 集成功能,支持生成 Supabase 客户端、配置数据库迁移等操作。
Mermaid 图
项目架构图

数据流图

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
Electron 主进程 | 负责应用的初始化、窗口管理、IPC 通信、自动更新等核心功能 |
Electron 渲染进程 | 负责 UI 的展示和用户交互 |
React 应用 | 使用 @tanstack/react-router 进行路由管理,并通过 PostHogProvider 集成 PostHog 进行用户行为追踪和分析 |
虚拟文件系统 | 在内存中模拟和管理文件系统的状态 |
备份管理 | 自动或手动备份用户设置和 SQLite 数据库,并在版本升级时触发备份操作 |
组件选择器 | 用于浏览器中选择和标记 DOM 元素的组件选择器工具 |
代理服务器 | 基于 Node.js Worker 线程的轻量级 HTTP/WS 代理服务器 |
脚本与工具 | 用于自动化任务和辅助开发的脚本和工具 |
配置文件 | 定义项目的构建、样式、依赖等信息 |
API 接口参数、类型和描述
接口名称 | 描述 | 参数 |
---|---|---|
IPC 通信 | 渲染进程与主进程之间的通信机制 | channel , args |
数据库操作 | 对 SQLite 数据库进行增删改查操作 | query , params |
文件系统操作 | 对文件系统进行增删改查操作 | path , content |
网络请求 | 发送 HTTP 请求 | url , options |
配置选项及其类型和默认值
配置项 | 类型 | 默认值 | 描述 |
---|---|---|---|
darkMode | string[] | ["class"] | 暗色模式支持 |
content | string[] | ["./pages/**/*.{ts,tsx}"] | 内容扫描路径 |
plugins | object[] | [tailwindcss, autoprefixer] | PostCSS 插件列表 |
host | string | "::" | 开发服务器主机地址 |
port | number | 8080 | 开发服务器端口 |
数据模型字段、类型、约束和描述
字段名 | 类型 | 约束 | 描述 |
---|---|---|---|
path | string | 必填 | 文件路径 |
content | string | 可选 | 文件内容 |
deleted | boolean | 必填 | 文件是否被删除 |
version | string | 必填 | 版本号 |
timestamp | Date | 必填 | 时间戳 |
reason | string | 可选 | 备份原因 |
checksum | string | 可选 | 文件校验和 |
代码片段
Electron 主进程入口
// src/main.ts
import { app, BrowserWindow } from 'electron';
import { registerIpcHandlers } from './ipc';
import { initializeDatabase } from './database';
import { BackupManager } from './backup_manager';
async function onReady() {
registerIpcHandlers();
initializeDatabase();
const backupManager = new BackupManager();
await backupManager.initialize();
createWindow();
}
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
mainWindow.loadFile('index.html');
}
虚拟文件系统实现
// shared/VirtualFilesystem.ts
abstract class BaseVirtualFileSystem {
protected virtualFiles: Map<string, string> = new Map();
protected deletedFiles: Set<string> = new Set();
abstract exists(path: string): boolean;
abstract read(path: string): string;
abstract write(path: string, content: string): void;
abstract delete(path: string): void;
abstract rename(oldPath: string, newPath: string): void;
}
备份管理实现
// src/backup_manager.ts
class BackupManager {
private backupDir: string;
constructor() {
this.backupDir = path.join(app.getPath('userData'), 'backups');
}
async initialize() {
const currentVersion = app.getVersion();
const lastVersion = this.getLastVersion();
if (lastVersion && lastVersion !== currentVersion) {
await this.createBackup(`Upgrade from ${lastVersion} to ${currentVersion}`);
}
this.setLastVersion(currentVersion);
}
async createBackup(reason: string) {
const backupName = `backup-${Date.now()}`;
const backupPath = path.join(this.backupDir, backupName);
await fs.ensureDir(backupPath);
const metadata = {
version: app.getVersion(),
timestamp: new Date().toISOString(),
reason,
checksums: {} as Record<string, string>,
};
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
const dbPath = path.join(app.getPath('userData'), 'sqlite.db');
if (await fs.pathExists(settingsPath)) {
const settingsDest = path.join(backupPath, 'settings.json');
await fs.copy(settingsPath, settingsDest);
metadata.checksums['settings.json'] = await this.calculateChecksum(settingsDest);
}
if (await fs.pathExists(dbPath)) {
const dbDest = path.join(backupPath, 'sqlite.db');
await this.safeCopyDatabase(dbPath, dbDest);
metadata.checksums['sqlite.db'] = await this.calculateChecksum(dbDest);
}
const metadataPath = path.join(backupPath, 'metadata.json');
await fs.writeJson(metadataPath, metadata);
}
async listBackups(): Promise<Backup[]> {
const backups: Backup[] = [];
const backupDirs = await fs.readdir(this.backupDir);
for (const dir of backupDirs) {
const metadataPath = path.join(this.backupDir, dir, 'metadata.json');
if (await fs.pathExists(metadataPath)) {
const metadata = await fs.readJson(metadataPath);
backups.push({ name: dir, ...metadata });
}
}
return backups.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
async cleanupOldBackups(maxBackups: number = 3) {
const backups = await this.listBackups();
if (backups.length > maxBackups) {
const backupsToDelete = backups.slice(maxBackups);
for (const backup of backupsToDelete) {
await this.deleteBackup(backup.name);
}
}
}
async deleteBackup(name: string) {
const backupPath = path.join(this.backupDir, name);
await fs.remove(backupPath);
}
async getBackupSize(): Promise<number> {
return await this.getDirectorySize(this.backupDir);
}
}
组件选择器实现
// worker/dyad-component-selector-client.js
function initializeComponentSelector() {
const overlay = makeOverlay();
let state = 'inactive';
function onMouseMove(event) {
if (state === 'inspecting') {
updateOverlay(event.target, overlay);
}
}
function onClick(event) {
if (state === 'inspecting') {
window.postMessage({ type: 'component-selected', target: event.target }, '*');
state = 'selected';
}
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('click', onClick);
window.addEventListener('message', (event) => {
if (event.data.type === 'activate-selector') {
state = 'inspecting';
} else if (event.data.type === 'deactivate-selector') {
state = 'inactive';
overlay.style.display = 'none';
}
});
}
代理服务器实现
// worker/proxy_server.js
const http = require('http');
const { workerData } = require('worker_threads');
function buildTargetURL(req) {
// 构建目标 URL
}
function injectHTML(html) {
// 注入脚本内容
}
const server = http.createServer((req, res) => {
const targetURL = buildTargetURL(req);
// 转发请求到目标地址并返回响应
});
server.listen(workerData.port, () => {
console.log(`Proxy server listening on port ${workerData.port}`);
});
结论
本项目通过 Electron 和 React 构建了一个功能丰富的桌面应用,支持多种开发工具和集成服务。通过虚拟文件系统、备份管理、组件选择器、代理服务器等模块,实现了高效的数据管理和用户交互。项目通过端到端测试确保各个功能模块的稳定性和正确性,为开发者提供了一个强大的开发环境。
共享工具模块
简介
共享工具模块是项目中用于提供通用功能和数据结构定义的核心部分。它包含两个主要文件:problem_prompt.ts
和 templates.ts
。前者用于生成 TypeScript 错误修复提示,后者定义了模板相关的数据结构和默认配置。这些工具在整个项目中被复用,以确保一致性和可维护性。
详细章节
TypeScript 错误修复提示生成
problem_prompt.ts
文件定义了一个函数 createProblemFixPrompt
,该函数根据 ProblemReport
生成 TypeScript 错误修复提示。如果没有任何问题,它将返回 “No TypeScript problems detected.”。如果有问题,它将统计问题总数,并格式化每个问题的路径、行号、列号、错误信息及代码片段(如果有的话),最后添加总结指令 “Please fix all errors in a concise way.”。
关键函数
createProblemFixPrompt
: 根据ProblemReport
生成 TypeScript 错误修复提示。
模板数据结构与配置
templates.ts
文件定义了模板相关的数据结构和默认配置。它包括 Template
和 ApiTemplate
接口,分别表示本地和 API 模板信息。此外,它还定义了默认模板 ID、特殊模板标识符、依赖 Neon 的模板集合,以及多个本地模板对象,为模板系统提供统一的数据结构与初始配置。
关键数据结构
Template
: 表示本地模板信息。ApiTemplate
: 表示 API 模板信息。
关键配置元素
- 默认模板 ID
- 特殊模板标识符
- 依赖 Neon 的模板集合
- 多个本地模板对象
Mermaid图
TypeScript 错误修复提示生成流程

模板数据结构关系

表格
主要功能或组件及其描述
组件 | 描述 |
---|---|
createProblemFixPrompt | 根据 ProblemReport 生成 TypeScript 错误修复提示 |
Template | 表示本地模板信息 |
ApiTemplate | 表示 API 模板信息 |
配置选项及其类型和默认值
配置项 | 类型 | 默认值 |
---|---|---|
默认模板 ID | string | 未指定 |
特殊模板标识符 | string | 未指定 |
依赖 Neon 的模板集合 | string[] | 未指定 |
代码片段
createProblemFixPrompt
函数
function createProblemFixPrompt(problemReport: ProblemReport): string {
// ...
}
Template
接口
interface Template {
id: string;
name: string;
description: string;
}
ApiTemplate
接口
interface ApiTemplate extends Template {
apiEndpoint: string;
apiKey: string;
}
结论/总结
共享工具模块通过提供通用功能和数据结构定义,为项目中的其他部分提供了坚实的基础。problem_prompt.ts
和 templates.ts
文件分别处理了 TypeScript 错误修复提示生成和模板数据结构与配置,确保了项目的一致性和可维护性。
组件标记工具
简介
组件标记工具(@dyad-sh/react-vite-component-tagger
)是一个 Vite 插件,用于在 React 项目开发过程中自动为 JSX 元素添加 data-dyad-id
和 data-dyad-name
属性。这些属性可用于测试、分析或调试,帮助识别组件实例及其来源。该插件在开发服务器运行时生效,仅处理 .jsx
和 .tsx
文件,并跳过 node_modules
中的内容。
该工具通过 Babel 解析组件的 AST(抽象语法树),识别未标记的 JSX 元素,并使用 MagicString
修改源码以注入唯一标识符和组件名称,同时保留 sourcemap 以支持调试。项目基于 TypeScript 构建,支持 CJS 和 ESM 模块格式,通过 tsup
进行构建。
详细章节
插件核心功能
组件标记工具的核心功能是为 React 组件自动注入数据属性,便于在运行时识别组件。插件通过 Vite 的构建钩子介入,在开发阶段修改源码,注入 data-dyad-id
和 data-dyad-name
属性。
数据属性说明
data-dyad-id
:为每个组件实例生成的唯一标识符。data-dyad-name
:组件的名称,通常来源于组件函数或类的名称。
插件处理流程
- 插件监听 Vite 的
transform
钩子,处理.jsx
和.tsx
文件。 - 使用 Babel 解析文件内容为 AST。
- 遍历 AST,查找未标记的 JSX 元素。
- 为这些元素注入
data-dyad-id
和data-dyad-name
属性。 - 使用
MagicString
修改源码并生成 sourcemap。

关键函数与模块
dyadTagger
插件入口
插件的入口函数为 dyadTagger
,定义在 src/index.ts
中。该函数返回一个 Vite 插件对象,包含 name
、apply
、enforce
和 transform
属性。
name
: 插件名称,用于标识插件。apply
: 指定插件仅在开发服务器中运行。enforce
: 指定插件在其他插件之前执行。transform
: 处理文件内容的钩子函数。
// packages/@dyad-sh/react-vite-component-tagger/src/index.ts
import { parse } from "@babel/parser";
import MagicString from "magic-string";
import path from "node:path";
import { walk } from "estree-walker";
import type { Plugin } from "vite";
const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
export default function dyadTagger(): Plugin {
return {
name: "vite-plugin-dyad-tagger",
apply: "serve",
enforce: "pre",
async transform(code: string, id: string) {
try {
if (
!VALID_EXTENSIONS.has(path.extname(id)) ||
id.includes("node_modules")
)
return null;
const ast = parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
const ms = new MagicString(code);
const fileRelative = path.relative(process.cwd(), id);
walk(ast as any, {
enter(node: any) {
try {
if (node.type !== "JSXOpeningElement") return;
if (node.name?.type !== "JSXIdentifier") return;
const tagName = node.name.name as string;
// Check if the element already has data-dyad-id attribute
const hasDyadId = node.attributes?.some(
(attr: any) =>
attr.name?.type === "JSXIdentifier" &&
attr.name.name === "data-dyad-id"
);
if (hasDyadId) return;
// Generate unique ID and component name
const dyadId = generateDyadId();
const componentName = extractComponentName(this as any, tagName, fileRelative);
// Inject attributes
const insertPosition = node.end - 1;
ms.appendRight(
insertPosition,
` data-dyad-id="${dyadId}" data-dyad-name="${componentName}"`
);
} catch (e) {
console.warn("Error processing JSX element:", e);
}
},
});
return {
code: ms.toString(),
map: ms.generateMap({ hires: true }),
};
} catch (e) {
console.warn("Error transforming file:", id, e);
return null;
}
}
};
}
属性生成逻辑
插件通过 generateDyadId()
函数生成唯一的 data-dyad-id
,通过 extractComponentName()
函数确定 data-dyad-name
的值。
// packages/@dyad-sh/react-vite-component-tagger/src/index.ts
let dyadIdCounter = 0;
function generateDyadId(): string {
return `dyad-${++dyadIdCounter}`;
}
function extractComponentName(walkerContext: any, tagName: string, fileRelative: string): string {
// For HTML elements, use the tag name
if (tagName[0] === tagName[0].toLowerCase()) {
return tagName;
}
// For custom components, try to find the component name from the AST context
try {
let scope = walkerContext.scope;
while (scope) {
if (scope.declarations?.[tagName]) {
return tagName;
}
scope = scope.parent;
}
} catch (e) {
// If we can't determine the component name from the AST, fall back to the file name
}
// Fallback to file name
return path.basename(fileRelative, path.extname(fileRelative));
}
依赖与构建配置
项目依赖
项目依赖包括以下关键库:
- @babel/parser: 用于解析 JSX 语法为 AST。
- magic-string: 用于修改源码并生成 sourcemap。
- estree-walker: 用于遍历 AST 节点。
- Vite: 作为插件运行的平台。
构建工具
项目使用 tsup
进行构建,支持 CJS 和 ESM 模块格式。构建脚本定义在 package.json
中,包括构建、监视和清理命令。
{
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"clean": "rm -rf dist"
}
}
配置与使用
安装与配置
插件可通过 npm 安装,并在 Vite 配置文件中引入:
npm install @dyad-sh/react-vite-component-tagger
在 vite.config.ts
中添加插件:
import { defineConfig } from 'vite';
import dyadTagger from '@dyad-sh/react-vite-component-tagger';
export default defineConfig({
plugins: [dyadTagger()]
});
使用说明
插件在开发模式下自动生效,无需额外配置。生成的 data-dyad-id
和 data-dyad-name
属性可用于测试工具或分析工具中,帮助识别组件实例。
表格总结
主要功能与组件
功能/组件 | 描述 |
---|---|
dyadTagger | 插件入口函数,返回 Vite 插件对象 |
transform | Vite 钩子函数,用于处理文件内容并注入属性 |
generateDyadId | 生成唯一标识符的函数 |
extractComponentName | 提取组件名称的函数 |
data-dyad-id | 为组件实例生成的唯一标识符 |
data-dyad-name | 组件的名称,来源于组件函数或类的名称 |
项目依赖
依赖项 | 类型 | 描述 |
---|---|---|
@babel/parser | 主依赖 | 用于解析 JSX 语法为 AST |
magic-string | 主依赖 | 用于修改源码并生成 sourcemap |
estree-walker | 主依赖 | 用于遍历 AST 节点 |
vite | 开发依赖 | 插件运行的平台 |
typescript | 开发依赖 | TypeScript 支持 |
tsup | 开发依赖 | 构建工具,支持 CJS 和 ESM 模块 |
插件配置选项
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
apply | string | serve | 指定插件在开发服务器中运行 |
enforce | string | pre | 指定插件在其他插件之前执行 |
结论
组件标记工具通过为 React 组件自动注入数据属性,简化了测试和分析流程。其核心功能依赖于 Babel 解析 JSX 语法为 AST,通过 estree-walker 遍历节点,并使用 MagicString 修改源码同时保持 sourcemap。该工具在项目中扮演了重要角色,特别是在需要识别组件实例的场景中。通过生成唯一的 data-dyad-id
和准确的 data-dyad-name
,该插件为开发者提供了强大的组件识别能力。
测试服务器模块
简介
测试服务器模块是一个本地服务,用于模拟大型语言模型(LLM)和 GitHub API 的行为。它主要用于开发和测试阶段,以替代真实的外部服务,从而提高开发效率并减少对外部依赖的耦合。该模块支持多种 LLM 接口风格(如 Ollama、LM Studio、OpenAI),并提供流式和非流式响应选项。此外,它还实现了 GitHub API 的核心功能,包括设备授权、用户信息获取、仓库管理和 Git 操作模拟。
详细章节
LLM 模拟服务
LLM 模拟服务通过 chatCompletionHandler.ts
文件实现,提供了一个 Express 路由处理器,用于模拟 LLM 的聊天接口。该服务支持流式输出和预设测试用例加载,并能根据特定标记触发限流、调试写入等行为。
关键函数和变量
CANNED_MESSAGE
: 默认回复内容。generateDump(req)
: 写入调试数据。globalCounter
: 计数器。createStreamChunk()
: 构建流式响应块。
API 接口
接口名称 | 描述 | 参数 |
---|---|---|
/chat/completions | 模拟 LLM 聊天接口 | messages , stream |
功能详解
LLM 模拟服务的核心功能包括:
- 默认响应:当没有特殊指令时,服务返回预定义的
CANNED_MESSAGE
内容:
export const CANNED_MESSAGE = `
<dyad-write path="file1.txt">
A file (2)
</dyad-write>
More
EOM`;
- 流式响应支持:服务能够根据请求中的
stream
标志返回流式或非流式响应。流式响应通过createStreamChunk
函数构建:
export function createStreamChunk(
content: string,
role: string = "assistant",
isLast: boolean = false,
) {
const chunk = {
id: `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "fake-model",
choices: [
{
index: 0,
delta: isLast ? {} : { content, role },
finish_reason: isLast ? "stop" : null,
},
],
};
return `data: ${JSON.stringify(chunk)}\n\n${isLast ? "data: [DONE]\n\n" : ""}`;
}
- 限流模拟:当最后一条消息的内容为 “[429]” 时,服务器会模拟限流错误:
if (lastMessage && lastMessage.content === "[429]") {
return res.status(429).json({
error: {
message: "Too many requests. Please try again later.",
type: "rate_limit_error",
param: null,
code: "rate_limit_exceeded",
},
});
}
- 测试用例加载:支持通过 “tc=” 前缀加载预设的测试用例文件:
if (
lastMessage &&
lastMessage.content &&
typeof lastMessage.content === "string" &&
lastMessage.content.startsWith("tc=")
) {
const testCaseName = lastMessage.content.slice(3);
const testFilePath = path.join(
__dirname,
"..",
"..",
"..",
"e2e-tests",
"fixtures",
prefix,
`${testCaseName}.md`,
);
try {
if (fs.existsSync(testFilePath)) {
messageContent = fs.readFileSync(testFilePath, "utf-8");
console.log(`* Loaded test case: ${testCaseName}`);
} else {
console.log(`* Test case file not found: ${testFilePath}`);
messageContent = `Error: Test case file not found: ${testCaseName}.md`;
}
} catch (error) {
console.error(`* Error reading test case file: ${error}`);
messageContent = `Error: Could not read test case file: ${testCaseName}.md`;
}
}
- 调试数据写入:
generateDump
函数负责将请求数据写入调试文件:
function generateDump(req: Request) {
const timestamp = Date.now();
const generatedDir = path.join(__dirname, "generated");
if (!fs.existsSync(generatedDir)) {
fs.mkdirSync(generatedDir, { recursive: true });
}
const dumpFilePath = path.join(generatedDir, `${timestamp}.json`);
try {
fs.writeFileSync(
dumpFilePath,
JSON.stringify(
{
body: req.body,
headers: { authorization: req.headers["authorization"] },
},
null,
2,
).replace(/\r\n/g, "\n"),
"utf-8",
);
console.log(`* Dumped messages to: ${dumpFilePath}`);
return `[[dyad-dump-path=${dumpFilePath}]]`;
} catch (error) {
console.error(`* Error writing dump file: ${error}`);
return `Error: Could not write dump file: ${error}`;
}
}
- 计数器功能:
globalCounter
计数器在收到 “[increment]” 指令时递增:
let globalCounter = 0;
if (lastMessage && lastMessage.content === "[increment]") {
globalCounter++;
messageContent = `counter=${globalCounter}`;
}
- TypeScript 错误修复:支持特定的 TypeScript 错误修复指令:
if (
lastMessage &&
typeof lastMessage.content === "string" &&
lastMessage.content.startsWith(
"Fix these 2 TypeScript compile-time error",
)
) {
messageContent = `
<dyad-write path="src/bad-file.ts" description="Fix 2 errors and introduce a new error.">
// Import doesn't exist
// import NonExistentClass from 'non-existent-class';
const x = new Object();
x.nonExistentMethod2();
</dyad-write>
`;
}
GitHub API 模拟服务
GitHub API 模拟服务通过 githubHandler.ts
文件实现,支持设备授权、用户信息、仓库管理、分支操作及 Git 协议交互。
关键函数
handleDeviceCode
handleAccessToken
handleUserRepos
handleGitPush
API 接口
接口名称 | 描述 | 参数 |
---|---|---|
/device/code | 设备码处理 | 无 |
/access/token | 访问令牌处理 | 无 |
/user/repos | 用户仓库列表 | 无 |
/git/push | Git 推送模拟 | 无 |
功能详解
GitHub API 模拟服务实现了以下核心功能:
- 设备授权:
handleDeviceCode
处理设备码请求,生成并返回设备码和用户验证 URL。 - 访问令牌:
handleAccessToken
处理访问令牌请求,验证设备码并返回访问令牌。 - 用户仓库:
handleUserRepos
返回用户仓库列表。 - Git 推送:
handleGitPush
模拟 Git 推送操作,并记录推送事件。
服务入口和配置
服务入口文件 index.ts
负责注册多个 LLM 和 GitHub API 路由,并启动 HTTP 服务监听 3500 端口。它还设置了 JSON 解析和最大请求体大小。
关键函数
createChatCompletionHandler(provider)
createStreamChunk(content, role, isLast)
配置选项
选项 | 类型 | 默认值 |
---|---|---|
端口 | number | 3500 |
功能详解
服务入口文件的主要职责包括:
- 路由注册:注册 LLM 和 GitHub API 的路由。
- 服务启动:启动 HTTP 服务并监听指定端口。
- 中间件配置:设置 JSON 解析和请求体大小限制。
依赖管理
项目配置文件 package.json
和 package-lock.json
定义了项目的依赖项和脚本命令。使用 TypeScript 编写,构建为 JavaScript 运行。
主要依赖
express
cors
body-parser
stream
Mermaid图
LLM 模拟服务流程

GitHub API 模拟服务流程

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
LLM 模拟服务 | 模拟 LLM 聊天接口,支持流式和非流式响应 |
GitHub API 模拟服务 | 模拟 GitHub API 核心功能 |
服务入口 | 注册路由并启动 HTTP 服务 |
依赖管理 | 管理项目依赖项和脚本命令 |
API 接口参数、类型和描述
接口 | 参数 | 类型 | 描述 |
---|---|---|---|
/chat/completions | messages | array | 消息列表 |
/chat/completions | stream | boolean | 是否流式响应 |
/device/code | 无 | 无 | 无 |
/access/token | 无 | 无 | 无 |
/user/repos | 无 | 无 | 无 |
/git/push | 无 | 无 | 无 |
配置选项及其类型和默认值
选项 | 类型 | 默认值 |
---|---|---|
端口 | number | 3500 |
代码片段
chatCompletionHandler.ts
中的 createStreamChunk
函数
function createStreamChunk(content: string, role: string, isLast: boolean) {
const chunk = {
id: `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "fake-model",
choices: [
{
index: 0,
delta: isLast ? {} : { content, role },
finish_reason: isLast ? "stop" : null,
},
],
};
return `data: ${JSON.stringify(chunk)}\n\n${isLast ? "data: [DONE]\n\n" : ""}`;
}
index.ts
中的 createChatCompletionHandler
函数
function createChatCompletionHandler(provider: string) {
return (req: Request, res: Response) => {
const { messages, stream } = req.body;
const lastMessage = messages[messages.length - 1];
if (lastMessage && lastMessage.content === "[429]") {
return res.status(429).json({
error: {
message: "Too many requests. Please try again later.",
type: "rate_limit_error",
param: null,
code: "rate_limit_exceeded",
},
});
}
if (stream) {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
let messageContent = CANNED_MESSAGE;
let counter = 0;
const interval = setInterval(() => {
if (counter < messageContent.length) {
const chunk = messageContent.slice(counter, counter + 10);
res.write(createStreamChunk(chunk, "assistant", false));
counter += 10;
} else {
res.write(createStreamChunk("", "assistant", true));
clearInterval(interval);
res.end();
}
}, 100);
} else {
res.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: "fake-model",
choices: [
{
index: 0,
message: {
role: "assistant",
content: CANNED_MESSAGE,
},
finish_reason: "stop",
},
],
});
}
};
}
githubHandler.ts
中的 handleDeviceCode
函数
function handleDeviceCode(req: Request, res: Response) {
const deviceCode = `device_${Math.random().toString(36).substr(2, 9)}`;
const userCode = `user_${Math.random().toString(36).substr(2, 9)}`;
const verificationUri = "http://localhost:3500/oauth/authorize";
deviceCodes.set(deviceCode, {
userCode,
verificationUri,
expiresIn: 900,
interval: 5,
});
res.json({
device_code: deviceCode,
user_code: userCode,
verification_uri: verificationUri,
expires_in: 900,
interval: 5,
});
}
结论/总结
测试服务器模块通过模拟 LLM 和 GitHub API 的行为,为开发和测试提供了极大的便利。它不仅支持多种接口风格和响应模式,还实现了核心的 GitHub 功能,使得开发者能够在不依赖外部服务的情况下进行高效开发。该模块的架构清晰,易于扩展和维护,是项目中不可或缺的一部分。
语言模型辅助模块
简介
语言模型辅助模块 (language_model_helpers.ts
) 是一个核心工具模块,旨在统一管理项目中使用的语言模型及其提供商信息。它通过整合数据库配置和硬编码的默认设置,为上层应用提供一致的接口来获取语言模型和提供商的元数据。
该模块的主要职责包括:
- 提供所有可用语言模型提供商的列表。
- 根据提供商 ID 获取其支持的语言模型。
- 并发获取非本地提供商的语言模型并按 ID 分组。
- 判断某个提供商是否为自定义类型。
通过这些功能,模块增强了系统的灵活性和可扩展性,允许开发者轻松集成新的语言模型或提供商。
详细章节
常量与接口定义
模块中定义了关键的接口和常量,用于描述语言模型的结构和预设配置。
ModelOption
接口
这是描述语言模型选项的核心接口,包含了模型的标识、名称、提供商等信息。
export interface ModelOption {
name: string;
displayName: string;
description: string;
tag?: string;
maxOutputTokens?: number;
contextWindow?: number;
}
预设模型列表与环境变量映射
模块中包含多个预设模型列表和环境变量映射,用于快速获取常用的模型配置和提供商信息。
export const PROVIDER_TO_ENV_VAR: Record<string, string> = {
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GEMINI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
};
export const CLOUD_PROVIDERS: Record<string, { ... }> = { ... };
const LOCAL_PROVIDERS: Record<string, { ... }> = { ... };
export const MODEL_OPTIONS: Record<string, ModelOption[]> = {
openai: [
{
name: "gpt-4o",
displayName: "GPT-4o",
description: "OpenAI's flagship model",
maxOutputTokens: 16384,
contextWindow: 128000,
},
// 其他模型...
],
anthropic: [
{
name: "claude-3-5-sonnet-latest",
displayName: "Claude 3.5 Sonnet",
description: "Anthropic's most intelligent model",
maxOutputTokens: 8192,
contextWindow: 200000,
},
// 其他模型...
],
// 其他提供商...
};
核心函数功能
模块提供了多个核心函数,用于获取语言模型和提供商信息。
getLanguageModelProviders()
返回所有可用的语言模型提供商,包括从数据库获取的自定义提供商和硬编码的云提供商。
export async function getLanguageModelProviders(): Promise<LanguageModelProvider[]> {
// Fetch custom providers from the database
const customProvidersDb = await db.select().from(languageModelProvidersSchema);
const customProvidersMap = new Map<string, LanguageModelProvider>();
for (const cp of customProvidersDb) {
customProvidersMap.set(cp.id, {
id: cp.id,
name: cp.name,
apiBaseUrl: cp.api_base_url,
envVarName: cp.env_var_name ?? undefined,
type: "custom",
});
}
// Get hardcoded cloud providers
const hardcodedProviders: LanguageModelProvider[] = [];
for (const [id, provider] of Object.entries(CLOUD_PROVIDERS)) {
if (!customProvidersMap.has(id)) {
hardcodedProviders.push({
id,
name: provider.name,
apiBaseUrl: provider.apiBaseUrl,
envVarName: PROVIDER_TO_ENV_VAR[id],
type: "cloud",
});
}
}
// Add local providers
for (const [id, provider] of Object.entries(LOCAL_PROVIDERS)) {
if (!customProvidersMap.has(id)) {
hardcodedProviders.push({
id,
name: provider.name,
apiBaseUrl: provider.apiBaseUrl,
type: "local",
});
}
}
return [...hardcodedProviders, ...customProvidersMap.values()];
}
getLanguageModels({ providerId })
根据提供商 ID 获取其支持的语言模型,包括硬编码模型和自定义模型。
export async function getLanguageModels({
providerId,
}: {
providerId: string;
}): Promise<LanguageModel[]> {
const allProviders = await getLanguageModelProviders();
const provider = allProviders.find((p) => p.id === providerId);
// 获取自定义模型
let customModels: LanguageModel[] = [];
try {
const customModelsDb = await db
.select({
id: languageModelsSchema.id,
displayName: languageModelsSchema.displayName,
apiName: languageModelsSchema.apiName,
description: languageModelsSchema.description,
maxOutputTokens: languageModelsSchema.max_output_tokens,
contextWindow: languageModelsSchema.context_window,
})
.from(languageModelsSchema)
.where(
isCustomProvider({ providerId })
? eq(languageModelsSchema.customProviderId, providerId)
: eq(languageModelsSchema.builtinProviderId, providerId),
);
customModels = customModelsDb.map((model) => ({
...model,
description: model.description ?? "",
tag: undefined,
maxOutputTokens: model.maxOutputTokens ?? undefined,
contextWindow: model.contextWindow ?? undefined,
type: "custom",
}));
} catch (error) {
console.error(
`Error fetching custom models for provider "${providerId}" from DB:`,
error,
);
}
// 如果是云提供商,也获取硬编码模型
let hardcodedModels: LanguageModel[] = [];
if (provider.type === "cloud") {
if (providerId in MODEL_OPTIONS) {
const models = MODEL_OPTIONS[providerId] || [];
hardcodedModels = models.map((model) => ({
...model,
apiName: model.name,
type: "cloud",
}));
}
}
return [...hardcodedModels, ...customModels];
}
getLanguageModelsByProviders()
并发获取非本地提供商的语言模型并按 ID 分组。
export async function getLanguageModelsByProviders(): Promise<
Record<string, LanguageModel[]>
> {
const providers = await getLanguageModelProviders();
// 并发获取所有模型
const modelPromises = providers
.filter((p) => p.type !== "local")
.map(async (provider) => {
const models = await getLanguageModels({ providerId: provider.id });
return { providerId: provider.id, models };
});
// 等待所有请求完成
const results = await Promise.all(modelPromises);
// 转换为记录格式
const record: Record<string, LanguageModel[]> = {};
for (const result of results) {
record[result.providerId] = result.models;
}
return record;
}
isCustomProvider({ providerId })
判断给定的 providerId 是否为自定义提供商。
export function isCustomProvider({ providerId }: { providerId: string }) {
return providerId.startsWith(CUSTOM_PROVIDER_PREFIX);
}
export const CUSTOM_PROVIDER_PREFIX = "custom::";
Mermaid 图
语言模型辅助模块功能流程图

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
getLanguageModelProviders | 返回所有可用的语言模型提供商 |
getLanguageModels | 根据提供商 ID 获取其支持的语言模型 |
getLanguageModelsByProviders | 并发获取非本地提供商的语言模型并按 ID 分组 |
isCustomProvider | 判断给定的 providerId 是否为自定义提供商 |
ModelOption
接口字段
字段 | 类型 | 描述 |
---|---|---|
name | string | 模型的API名称 |
displayName | string | 模型的显示名称 |
description | string | 模型的描述信息 |
tag | string (可选) | 模型的标签 |
maxOutputTokens | number (可选) | 最大输出token数 |
contextWindow | number (可选) | 上下文窗口大小 |
提供商环境变量映射
提供商 | 环境变量 |
---|---|
openai | OPENAI_API_KEY |
anthropic | ANTHROPIC_API_KEY |
GEMINI_API_KEY | |
openrouter | OPENROUTER_API_KEY |
代码片段
getLanguageModelProviders
函数
export async function getLanguageModelProviders(): Promise<LanguageModelProvider[]> {
const customProvidersDb = await db.select().from(languageModelProvidersSchema);
const customProvidersMap = new Map<string, LanguageModelProvider>();
for (const cp of customProvidersDb) {
customProvidersMap.set(cp.id, {
id: cp.id,
name: cp.name,
apiBaseUrl: cp.api_base_url,
envVarName: cp.env_var_name ?? undefined,
type: "custom",
});
}
const hardcodedProviders: LanguageModelProvider[] = [];
for (const [id, provider] of Object.entries(CLOUD_PROVIDERS)) {
if (!customProvidersMap.has(id)) {
hardcodedProviders.push({
id,
name: provider.name,
apiBaseUrl: provider.apiBaseUrl,
envVarName: PROVIDER_TO_ENV_VAR[id],
type: "cloud",
});
}
}
for (const [id, provider] of Object.entries(LOCAL_PROVIDERS)) {
if (!customProvidersMap.has(id)) {
hardcodedProviders.push({
id,
name: provider.name,
apiBaseUrl: provider.apiBaseUrl,
type: "local",
});
}
}
return [...hardcodedProviders, ...customProvidersMap.values()];
}
getLanguageModels
函数
export async function getLanguageModels({
providerId,
}: {
providerId: string;
}): Promise<LanguageModel[]> {
const allProviders = await getLanguageModelProviders();
const provider = allProviders.find((p) => p.id === providerId);
let customModels: LanguageModel[] = [];
try {
const customModelsDb = await db
.select({
id: languageModelsSchema.id,
displayName: languageModelsSchema.displayName,
apiName: languageModelsSchema.apiName,
description: languageModelsSchema.description,
maxOutputTokens: languageModelsSchema.max_output_tokens,
contextWindow: languageModelsSchema.context_window,
})
.from(languageModelsSchema)
.where(
isCustomProvider({ providerId })
? eq(languageModelsSchema.customProviderId, providerId)
: eq(languageModelsSchema.builtinProviderId, providerId),
);
customModels = customModelsDb.map((model) => ({
...model,
description: model.description ?? "",
tag: undefined,
maxOutputTokens: model.maxOutputTokens ?? undefined,
contextWindow: model.contextWindow ?? undefined,
type: "custom",
}));
} catch (error) {
console.error(
`Error fetching custom models for provider "${providerId}" from DB:`,
error,
);
}
let hardcodedModels: LanguageModel[] = [];
if (provider.type === "cloud") {
if (providerId in MODEL_OPTIONS) {
const models = MODEL_OPTIONS[providerId] || [];
hardcodedModels = models.map((model) => ({
...model,
apiName: model.name,
type: "cloud",
}));
}
}
return [...hardcodedModels, ...customModels];
}
getLanguageModelsByProviders
函数
export async function getLanguageModelsByProviders(): Promise<
Record<string, LanguageModel[]>
> {
const providers = await getLanguageModelProviders();
const modelPromises = providers
.filter((p) => p.type !== "local")
.map(async (provider) => {
const models = await getLanguageModels({ providerId: provider.id });
return { providerId: provider.id, models };
});
const results = await Promise.all(modelPromises);
const record: Record<string, LanguageModel[]> = {};
for (const result of results) {
record[result.providerId] = result.models;
}
return record;
}
isCustomProvider
函数
export function isCustomProvider({ providerId }: { providerId: string }) {
return providerId.startsWith(CUSTOM_PROVIDER_PREFIX);
}
export const CUSTOM_PROVIDER_PREFIX = "custom::";
结论/总结
语言模型辅助模块通过提供统一的接口和灵活的配置选项,极大地简化了语言模型的管理和使用。它不仅支持预设模型,还允许开发者轻松集成自定义模型,从而增强了系统的可扩展性和适应性。该模块是项目中语言模型相关功能的核心基础,为上层应用提供了稳定可靠的支持。通过数据库和硬编码配置的结合,模块实现了灵活的模型管理机制,支持云提供商、本地提供商和自定义提供商等多种类型。
IPC通信模块
简介
IPC(Inter-Process Communication,进程间通信)模块是 Electron 应用中前后端通信的核心机制。它通过主进程和渲染进程之间的消息传递,实现对应用生命周期、数据管理、系统操作等功能的控制。该模块涵盖了从应用管理、聊天交互、环境变量配置到版本控制、语言模型管理等多个方面,为整个应用提供了统一的通信接口和数据结构。
详细章节
1. IPC 处理器注册与管理
IPC 处理器通过 ipc_host.ts
文件集中注册,调用各个模块的 registerXxxHandlers()
函数,覆盖了聊天、设置、部署、数据库、窗口控制等多个方面。这种集中管理的方式便于统一管理和扩展。
1.1 处理器注册流程

2. 应用管理
应用管理相关的 IPC 处理器位于 app_handlers.ts
文件中,负责应用的创建、复制、运行、停止、删除等操作,并支持 Git 初始化、提交、分支重命名等功能。
2.1 关键函数
executeApp
: 执行应用copyDir
: 复制目录killProcessOnPort
: 终止指定端口的进程
3. 聊天交互
聊天交互相关的 IPC 处理器分布在 chat_handlers.ts
和 chat_stream_handlers.ts
文件中,负责处理聊天的创建、获取、删除以及流式响应等操作。
3.1 关键函数
create-chat
: 创建聊天get-chats
: 获取聊天列表delete-messages
: 删除消息chat:stream
: 处理流式响应chat:cancel
: 取消流式响应
4. 环境变量管理
环境变量管理相关的 IPC 处理器位于 app_env_vars_handlers.ts
文件中,负责根据 appId
查询数据库并读取 .env.local
文件内容,以及接收 appId
和新变量对象,写入格式化后的 .env.local
文件。
4.1 关键函数
get-app-env-vars
: 获取应用环境变量set-app-env-vars
: 设置应用环境变量
5. 语言模型管理
语言模型管理相关的 IPC 处理器位于 language_model_handlers.ts
文件中,支持创建/删除模型、获取列表、分类返回模型信息等功能。
5.1 关键函数
getLanguageModelProviders
: 获取语言模型提供商getLanguageModels
: 获取语言模型列表findLanguageModel
: 查找语言模型
6. 版本控制
版本控制相关的 IPC 处理器位于 version_handlers.ts
文件中,实现版本的列出、回滚、检出等操作,并依赖 Git 和 Neon 数据库分支管理,确保代码与数据库状态一致。
6.1 关键函数
switchPostgresToDevelopmentBranch
: 切换到开发分支
7. 窗口控制
窗口控制相关的 IPC 处理器位于 window_handlers.ts
文件中,负责窗口的最小化、最大化、关闭等操作。
7.1 关键函数
minimize
: 最小化窗口maximize
: 最大化窗口close
: 关闭窗口
8. 其他功能
其他功能包括但不限于:
github_handlers.ts
: GitHub 集成supabase_handlers.ts
: Supabase 集成vercel_handlers.ts
: Vercel 集成template_handlers.ts
: 模板管理settings_handlers.ts
: 用户设置管理session_handlers.ts
: 会话数据清除shell_handler.ts
: 系统操作(打开 URL、显示文件路径)
Mermaid图
IPC 处理器注册流程

应用管理流程

聊天交互流程

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
应用管理 | 负责应用的创建、复制、运行、停止、删除等操作 |
聊天交互 | 处理聊天的创建、获取、删除以及流式响应等操作 |
环境变量管理 | 管理应用的环境变量,读取和写入 .env.local 文件 |
语言模型管理 | 管理语言模型及提供者,支持创建/删除模型、获取列表等 |
版本控制 | 实现版本的列出、回滚、检出等操作 |
窗口控制 | 负责窗口的最小化、最大化、关闭等操作 |
API 接口参数、类型和描述
接口名称 | 参数 | 类型 | 描述 |
---|---|---|---|
get-app-env-vars | appId | string | 根据 appId 查询数据库并读取 .env.local 文件内容 |
set-app-env-vars | appId , vars | string, object | 接收 appId 和新变量对象,写入格式化后的 .env.local 文件 |
create-chat | 无 | 无 | 创建聊天 |
get-chats | 无 | 无 | 获取聊天列表 |
delete-messages | chatId | string | 删除指定聊天的消息 |
chat:stream | input | string | 处理流式响应 |
chat:cancel | streamId | string | 取消流式响应 |
配置选项及其类型和默认值
配置项 | 类型 | 默认值 | 描述 |
---|---|---|---|
LM_STUDIO_BASE_URL | string | http://localhost:1234/v1 | LM Studio 的基础 URL |
数据模型字段、类型、约束和描述
字段名 | 类型 | 约束 | 描述 |
---|---|---|---|
appId | string | 必填 | 应用 ID |
vars | object | 必填 | 环境变量对象 |
chatId | string | 必填 | 聊天 ID |
input | string | 必填 | 用户输入 |
streamId | string | 必填 | 流 ID |
代码片段
app_handlers.ts
async function executeApp(appId: string): Promise<void> {
// 执行应用逻辑
}
chat_stream_handlers.ts
async function handleStream(input: string): Promise<void> {
// 处理流式响应逻辑
}
app_env_vars_handlers.ts
async function getAppEnvVars(appId: string): Promise<object> {
// 获取应用环境变量逻辑
}
language_model_handlers.ts
async function getLanguageModels(): Promise<Array<object>> {
// 获取语言模型列表逻辑
}
version_handlers.ts
async function switchPostgresToDevelopmentBranch(): Promise<void> {
// 切换到开发分支逻辑
}
window_handlers.ts
async function minimize(): Promise<void> {
// 最小化窗口逻辑
}
结论/总结
IPC 通信模块是 Electron 应用中前后端通信的核心机制,通过主进程和渲染进程之间的消息传递,实现了对应用生命周期、数据管理、系统操作等功能的控制。该模块涵盖了从应用管理、聊天交互、环境变量配置到版本控制、语言模型管理等多个方面,为整个应用提供了统一的通信接口和数据结构。通过集中管理和模块化设计,IPC 通信模块具有良好的扩展性和维护性,能够满足复杂应用的需求。
测试模块
简介
测试模块是项目中用于验证系统功能、性能和稳定性的关键部分。它涵盖了从用户界面交互到后端逻辑处理的全面测试,确保每个组件在各种场景下都能按预期工作。测试模块通过模拟真实用户操作和系统响应,帮助开发团队发现潜在问题并及时修复,从而提高整体产品质量。
详细章节
用户界面测试
用户界面测试主要关注前端组件的交互和视觉表现。通过模拟用户操作,验证页面元素的正确性和响应性。
审批流程测试
审批流程测试验证用户在 Index.tsx
页面执行写入操作并生成摘要的流程。测试包括文本输入、图片展示和按钮交互,最终以”Approved”状态结束,涉及”Undo”和”Retry”按钮。
图像附件测试
图像附件测试涵盖聊天界面中的附件图片功能,包括上传和查看附件的过程,并验证”Retry”按钮的交互逻辑。
错误修复测试
错误修复测试模拟用户识别错误并触发自动修复操作的界面行为。测试包括错误信息展示、堆栈跟踪及”Fix error with AI”按钮,验证错误处理与修复流程中的界面内容与交互逻辑。
组件选择测试
组件选择测试验证系统中组件选择和取消选择的功能。测试包括”Select component”和”Deselect component”操作,确保UI元素正确响应用户交互。
上下文管理测试
上下文管理测试关注代码库上下文的配置和管理,确保系统能够正确识别和处理相关文件。
默认上下文配置
默认上下文配置测试验证”Codebase Context”对话框界面,用户可设置处理代码时的上下文范围。测试包括路径输入框、”Add”按钮及说明文字,确保用户能灵活配置上下文路径。
排除路径配置
排除路径配置测试验证用户设置包含和排除路径的功能。通过文本框输入 glob 模式并点击”Add”按钮,系统根据配置决定哪些文件被纳入或排除在上下文中。
智能上下文配置
智能上下文配置测试验证系统自动识别关键文件的能力。测试包括”Codebase Context”和”Smart Context Auto-includes”两部分,减少手动配置工作量。
GitHub 集成测试
GitHub 集成测试验证系统与 GitHub 仓库的连接和同步功能,确保代码能够正确推送到远程仓库。
创建新仓库
创建新仓库测试验证用户创建新 GitHub 仓库并选择默认分支的流程。测试包括仓库名、分支名输入及”Sync to GitHub”和”Disconnect from repo”按钮,确保连接状态正确。
同步到现有仓库
同步到现有仓库测试验证用户连接到已有 GitHub 仓库并支持自定义分支的流程。测试包括仓库名、分支名展示及同步和断开操作,确保仓库连接与同步逻辑正确。
导入应用测试
导入应用测试验证系统导入外部应用的能力,确保导入过程中的 UI 行为符合预期。
基本导入
基本导入测试验证导入应用测试场景的标识,确保 UI 元素或交互符合预期。
AI 规则导入
AI 规则导入测试验证导入应用时的错误处理与重试机制。测试包括 [dump] 和 [[dyad-dump-path=*]] 标记,以及”Retry”按钮和图像元素。
聊天模式测试
聊天模式测试验证聊天模式选择器在不同模式下的行为,确保界面输出符合预期。
Ask 模式
Ask 模式测试验证聊天模式选择器在”Ask 模式”下的 UI 行为。测试包括用户指令(如生成 AI_RULES.md
)、系统响应(如”Approved”)及”Retry”按钮和图像资源。
默认构建模式
默认构建模式测试验证聊天模式选择器在默认构建模式下的行为。测试包括用户输入、系统响应、调试标记 [dump]
及”Retry”按钮。
问题修复测试
问题修复测试验证系统自动修复 TypeScript 编译错误的能力,确保代码质量。
自动修复
自动修复测试验证系统在启用自动修复功能后对 bad-file.ts
的逐步修复。测试记录不同阶段修复状态,第一次修复引入新错误,第二次修复解决剩余问题。
手动修复
手动修复测试验证用户手动修复问题的流程。测试包括错误信息展示、”Recheck”和”Fix All”按钮,确保用户能重新检查或修复问题。
版本切换测试
版本切换测试验证应用在不同版本间的切换功能,确保UI一致性和稳定性。
版本切换流程
版本切换流程测试验证应用启动时的UI布局,包括通知区域”Notifications (F8)”、”Welcome to Your Blank App”标题及Dyad链接。测试确保端到端测试中对比不同版本的UI是否一致。
重试机制测试
重试机制测试验证系统在特定条件下的重试流程与状态更新。
重试按钮功能
重试按钮功能测试验证”重试”按钮及其相关状态,包括图片和计数器 counter=1
,确保系统在特定条件下的重试流程与状态更新。
Mermaid图
审批流程

图像附件流程

错误修复流程

上下文管理流程

GitHub 集成流程

组件选择流程

版本切换流程

重试机制流程

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
审批流程 | 验证用户在 Index.tsx 页面执行写入操作并生成摘要的流程 |
图像附件 | 测试聊天界面中的附件图片功能,包括上传和查看附件的过程 |
错误修复 | 模拟用户识别错误并触发自动修复操作的界面行为 |
上下文管理 | 验证代码库上下文的配置和管理,确保系统能够正确识别和处理相关文件 |
GitHub 集成 | 验证系统与 GitHub 仓库的连接和同步功能 |
导入应用 | 验证系统导入外部应用的能力 |
聊天模式 | 验证聊天模式选择器在不同模式下的行为 |
问题修复 | 验证系统自动修复 TypeScript 编译错误的能力 |
组件选择 | 验证组件选择和取消选择的功能 |
版本切换 | 验证应用在不同版本间的切换功能 |
重试机制 | 验证系统在特定条件下的重试流程与状态更新 |
API 接口参数、类型和描述
接口名称 | 参数 | 类型 | 描述 |
---|---|---|---|
审批流程 | – | – | – |
图像附件 | – | – | – |
错误修复 | – | – | – |
上下文管理 | – | – | – |
GitHub 集成 | – | – | – |
导入应用 | – | – | – |
聊天模式 | – | – | – |
问题修复 | – | – | – |
组件选择 | – | – | – |
版本切换 | – | – | – |
重试机制 | – | – | – |
配置选项及其类型和默认值
配置选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
上下文路径 | 字符串 | src/**/*.tsx | 用于设置处理代码时的上下文范围 |
排除路径 | 字符串 | – | 用于排除特定文件或目录 |
智能上下文 | 布尔值 | false | 启用智能上下文机制自动识别关键文件 |
自动修复 | 布尔值 | false | 启用自动修复功能 |
版本检查 | 布尔值 | true | 启用版本一致性检查 |
数据模型字段、类型、约束和描述
字段 | 类型 | 约束 | 描述 |
---|---|---|---|
审批状态 | 字符串 | Approved/Rejected | 表示审批流程的最终状态 |
图片路径 | 字符串 | – | 表示上传图片的路径 |
错误信息 | 字符串 | – | 表示检测到的错误信息 |
上下文路径 | 字符串 | – | 表示设置的上下文路径 |
仓库信息 | 对象 | – | 包含仓库名、分支名等信息 |
导入状态 | 字符串 | – | 表示导入应用的状态 |
聊天模式 | 字符串 | Ask/Default | 表示当前的聊天模式 |
修复状态 | 字符串 | Success/Failure | 表示问题修复的状态 |
组件状态 | 字符串 | Selected/Deselected | 表示组件的选择状态 |
版本号 | 字符串 | – | 表示当前应用版本 |
重试次数 | 数字 | >=0 | 表示已重试的次数 |
代码片段
审批流程代码片段
function approveProcess() {
writeToIndex();
showSummary();
const userAction = getUserAction();
if (userAction === 'approve') {
setApprovedStatus();
} else {
setRejectedStatus();
}
showUndoRetryButtons();
}
图像附件代码片段
function attachImageProcess() {
const image = selectImage();
uploadImage(image);
displayImage(image);
const userAction = getUserAction();
if (userAction === 'retry') {
attachImageProcess();
} else {
confirmAttachment();
}
}
错误修复代码片段
function fixErrorProcess() {
const error = detectError();
showErrorInfo(error);
const userAction = getUserAction();
if (userAction === 'fix') {
fixError(error);
const isFixed = verifyFix();
if (isFixed) {
completeFix();
} else {
fixErrorProcess();
}
} else {
ignoreError();
}
}
上下文管理代码片段
function manageContextProcess() {
setContext();
const pathPattern = inputPathPattern();
addPath(pathPattern);
const fileCount = showFileCount();
const userAction = getUserAction();
if (userAction === 'confirm') {
saveConfig();
} else {
manageContextProcess();
}
}
GitHub 集成代码片段
function githubIntegrationProcess() {
connectRepository();
const repoInfo = inputRepositoryInfo();
const repository = createOrSelectRepository(repoInfo);
const syncResult = syncCode(repository);
if (syncResult.success) {
completeSync();
} else {
githubIntegrationProcess();
}
}
组件选择代码片段
function selectComponentProcess() {
const component = selectComponent();
displayComponentDetails(component);
const userAction = getUserAction();
if (userAction === 'deselect') {
deselectComponent(component);
} else {
editComponent(component);
}
updateUI();
}
版本切换代码片段
function switchVersionProcess() {
const version = selectVersion();
loadVersion(version);
const isConsistent = verifyUIConsistency();
if (isConsistent) {
completeSwitch();
} else {
rollbackVersion();
}
}
重试机制代码片段
function retryProcess() {
const result = performAction();
if (!result.success) {
showRetryButton();
const userAction = getUserAction();
if (userAction === 'retry') {
retryProcess();
} else {
abandonAction();
}
} else {
completeAction();
}
}
结论/总结
测试模块通过全面的测试覆盖,确保系统在各种场景下都能按预期工作。从用户界面交互到后端逻辑处理,每个组件都经过严格的验证,以提高整体产品质量。通过模拟真实用户操作和系统响应,测试模块帮助开发团队发现潜在问题并及时修复,从而确保系统的稳定性和可靠性。
用户界面测试涵盖了审批流程、图像附件、错误修复和组件选择等关键功能,确保前端交互的正确性。上下文管理测试验证了代码库上下文的配置和管理,包括默认配置、排除路径和智能上下文等功能。GitHub 集成测试确保了系统与远程仓库的连接和同步能力。
导入应用测试和聊天模式测试验证了系统在不同场景下的适应性,而问题修复测试则确保了代码质量。版本切换测试和重试机制测试进一步增强了系统的稳定性和用户体验。
通过这些全面的测试,项目能够在发布前发现并修复潜在问题,确保最终用户获得高质量的产品体验。
工具模块
简介
工具模块是项目中多个独立功能的集合,旨在提供通用、可复用的辅助功能,以增强开发效率和代码质量。这些工具涵盖了类型断言、聊天接口封装、数据结构验证、UI 提示、样式处理、代码库提取等多个方面。模块中的每个文件都专注于特定领域,例如:
assert.ts
提供类型安全的断言机制;chat.ts
封装与聊天相关的 IPC 调用;schemas.ts
定义统一的数据结构和验证逻辑;toast.tsx
实现统一的 UI 提示系统;utils.ts
提供样式合并和名称生成工具;codebase.ts
提供代码库内容提取与格式化功能。
这些工具模块在整个项目中作为基础支撑,为上层业务逻辑提供稳定、可靠的辅助功能。
详细章节
类型断言工具
src/lib/assert.ts
文件提供了一个泛型断言函数 assertExists<T>
,用于确保值不为 null
或 undefined
。若条件不满足则抛出异常,并通过 asserts
实现类型守卫,将类型从 T
狭窄化为 NonNullable<T>
。
关键函数
assertExists<T>(value: T): asserts value is NonNullable<T>
:断言值存在,否则抛出异常。
代码片段
function assertExists<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error("Value is null or undefined");
}
}
聊天接口封装
src/lib/chat.ts
文件封装了与聊天相关的 IPC 调用,提供了两个主要函数:
createApp(params: CreateAppParams): Promise<CreateAppResult>
:创建新应用并初始化聊天内容;getAllChats(appId?: number): Promise<ChatSummary[]>
:获取所有聊天记录,支持按应用过滤。
两个函数在出错时打印日志并抛出异常,确保上层逻辑正确处理错误。
关键函数
createApp
:创建应用并初始化聊天内容。getAllChats
:获取聊天记录。
代码片段
async function createApp(params: CreateAppParams): Promise<CreateAppResult> {
try {
const result = await IpcClient.invoke("createApp", params);
return result;
} catch (error) {
console.error("Failed to create app:", error);
throw error;
}
}
async function getAllChats(appId?: number): Promise<ChatSummary[]> {
try {
const result = await IpcClient.invoke("getAllChats", { appId });
return result;
} catch (error) {
console.error("Failed to get chats:", error);
throw error;
}
}
数据结构验证
src/lib/schemas.ts
文件定义了多个 Zod Schema 和 TypeScript 类型,用于数据结构标准化和验证,包括:
SecretSchema
、ChatSummarySchema
、LargeLanguageModelSchema
、ProviderSettingSchema
、UserSettingsSchema
;- 辅助函数
isDyadProEnabled
、hasDyadProKey
; - 提案接口如
Proposal
、CodeProposal
等。
关键 Schema 和类型
SecretSchema
:用于验证密钥数据。ChatSummarySchema
:用于验证聊天摘要数据。LargeLanguageModelSchema
:用于验证大语言模型配置。ProviderSettingSchema
:用于验证提供者设置。UserSettingsSchema
:用于验证用户设置。
代码片段
const SecretSchema = z.object({
key: z.string(),
});
const ChatSummarySchema = z.object({
id: z.number(),
title: z.string(),
});
const LargeLanguageModelSchema = z.object({
name: z.string(),
provider: z.string(),
});
const ProviderSettingSchema = z.object({
apiKey: z.string(),
endpoint: z.string().optional(),
});
const UserSettingsSchema = z.object({
theme: z.string(),
language: z.string(),
});
UI 提示工具
src/lib/toast.tsx
文件实现了统一的 Toast 提示工具函数,封装了 sonner
库方法,提供了以下函数:
showSuccess
、showError
、showWarning
、showInfo
和showInputRequest
。
部分函数支持复制错误信息或交互式输入,并结合 PostHog
进行事件追踪。所有函数返回 toast ID,便于后续管理。
关键函数
showSuccess
:显示成功提示。showError
:显示错误提示。showWarning
:显示警告提示。showInfo
:显示信息提示。showInputRequest
:显示交互式输入提示。
代码片段
function showSuccess(message: string): string {
return toast.success(message);
}
function showError(message: string): string {
return toast.error(message);
}
function showWarning(message: string): string {
return toast.warning(message);
}
function showInfo(message: string): string {
return toast.info(message);
}
function showInputRequest(message: string): string {
return toast.input(message);
}
样式与名称生成工具
src/lib/utils.ts
文件提供了两个工具函数:
cn(...inputs: ClassValue[])
:合并 Tailwind CSS 类名;generateCuteAppName()
:随机生成可爱风格的应用名称,格式为 “形容词-动物-动词”。
关键函数
cn
:合并 CSS 类名。generateCuteAppName
:生成应用名称。
代码片段
function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
function generateCuteAppName(): string {
const adjectives = ["Cute", "Fluffy", "Sparkly"];
const animals = ["Cat", "Dog", "Bunny"];
const verbs = ["Runs", "Jumps", "Sleeps"];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const animal = animals[Math.floor(Math.random() * animals.length)];
const verb = verbs[Math.floor(Math.random() * verbs.length)];
return `${adjective}-${animal}-${verb}`;
}
代码库提取工具
src/utils/codebase.ts
文件是一个用于提取和格式化代码库内容的工具模块,主要职责是遍历项目目录、过滤文件、缓存读取结果,并根据规则将文件内容整理为适合 LLM 使用的结构化字符串。
核心功能
- 文件过滤与收集:支持多种常见开发语言扩展名,提供白名单/黑名单机制,排除特定目录和敏感文件,支持 Git 忽略规则检查。
- 缓存机制:使用缓存基于修改时间判断是否更新,控制最大缓存数量以避免内存溢出。
- 虚拟文件系统支持:允许传入虚拟文件系统模拟文件变更。
- 格式化输出:将文件封装为
<dyad-file>
标签块,对特殊文件进行内容精简处理。 - 智能上下文提取:支持自动包含指定路径下的文件,可排除特定路径下的文件,输出按修改时间排序。
关键函数
collectFiles
:递归收集文件。readFileWithCache
:带缓存的文件读取。isGitIgnored
:判断文件是否被 Git 忽略。formatFile
:格式化单个文件。extractCodebase
:主入口函数。
代码片段
async function collectFiles(dir: string, baseDir: string): Promise<string[]> {
const files: string[] = [];
try {
await fsAsync.access(dir);
} catch {
return files;
}
try {
const entries = await fsAsync.readdir(dir, { withFileTypes: true });
const promises = entries.map(async (entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && EXCLUDED_DIRS.includes(entry.name)) {
return;
}
if (await isGitIgnored(fullPath, baseDir)) {
return;
}
if (entry.isDirectory()) {
const subDirFiles = await collectFiles(fullPath, baseDir);
files.push(...subDirFiles);
} else if (entry.isFile()) {
if (EXCLUDED_FILES.includes(entry.name)) {
return;
}
try {
const stats = await fsAsync.stat(fullPath);
if (stats.size > MAX_FILE_SIZE) {
return;
}
} catch (error) {
logger.error(`Error checking file size: ${fullPath}`, error);
return;
}
files.push(fullPath);
}
});
await Promise.all(promises);
} catch (error) {
logger.error(`Error reading directory ${dir}:`, error);
}
return files;
}
async function isGitIgnored(
filePath: string,
baseDir: string,
): Promise<boolean> {
try {
let currentDir = baseDir;
const pathParts = path.relative(baseDir, filePath).split(path.sep);
let shouldClearCache = false;
const rootGitIgnorePath = path.join(baseDir, ".gitignore");
try {
const stats = await fsAsync.stat(rootGitIgnorePath);
const lastMtime = gitIgnoreMtimes.get(rootGitIgnorePath) || 0;
if (stats.mtimeMs > lastMtime) {
gitIgnoreMtimes.set(rootGitIgnorePath, stats.mtimeMs);
shouldClearCache = true;
}
} catch {
}
for (let i = 0; i < pathParts.length - 1; i++) {
currentDir = path.join(currentDir, pathParts[i]);
const gitIgnorePath = path.join(currentDir, ".gitignore");
try {
const stats = await fsAsync.stat(gitIgnorePath);
const lastMtime = gitIgnoreMtimes.get(gitIgnorePath) || 0;
if (stats.mtimeMs > lastMtime) {
gitIgnoreMtimes.set(gitIgnorePath, stats.mtimeMs);
shouldClearCache = true;
}
} catch {
}
}
if (shouldClearCache) {
gitIgnoreCache.clear();
}
const cacheKey = `${baseDir}:${filePath}`;
if (gitIgnoreCache.has(cacheKey)) {
return gitIgnoreCache.get(cacheKey)!;
}
const relativePath = path.relative(baseDir, filePath);
const result = await isIgnored({
fs,
dir: baseDir,
filepath: relativePath,
});
gitIgnoreCache.set(cacheKey, result);
return result;
} catch (error) {
logger.error(`Error checking if path is git ignored: ${filePath}`, error);
return false;
}
}
export async function readFileWithCache(
filePath: string,
virtualFileSystem?: AsyncVirtualFileSystem,
): Promise<string | undefined> {
try {
if (virtualFileSystem) {
const virtualContent = await virtualFileSystem.readFile(filePath);
if (virtualContent != null) {
return cleanContent({ content: virtualContent, filePath });
}
}
const stats = await fsAsync.stat(filePath);
const currentMtime = stats.mtimeMs;
if (fileContentCache.has(filePath)) {
const cache = fileContentCache.get(filePath)!;
if (cache.mtime === currentMtime) {
return cache.content;
}
}
const rawContent = await fsAsync.readFile(filePath, "utf-8");
const content = cleanContent({ content: rawContent, filePath });
fileContentCache.set(filePath, {
content,
mtime: currentMtime,
});
if (fileContentCache.size > MAX_FILE_CACHE_SIZE) {
const firstKey = fileContentCache.keys().next().value;
if (firstKey) {
fileContentCache.delete(firstKey);
}
}
return content;
} catch (error) {
logger.error(`Error reading file: ${filePath}`, error);
return undefined;
}
}
async function formatFile({
filePath,
normalizedRelativePath,
virtualFileSystem,
}: {
filePath: string;
normalizedRelativePath: string;
virtualFileSystem?: AsyncVirtualFileSystem;
}): Promise<string> {
try {
if (!shouldReadFileContents({ filePath, normalizedRelativePath })) {
return `<dyad-file path="${normalizedRelativePath}">
${OMITTED_FILE_CONTENT}
</dyad-file>
`;
}
const content = await readFileWithCache(filePath, virtualFileSystem);
if (content == null) {
return `<dyad-file path="${normalizedRelativePath}">
// Error reading file
</dyad-file>
`;
}
return `<dyad-file path="${normalizedRelativePath}">
${content}
</dyad-file>
`;
} catch (error) {
logger.error(`Error reading file: ${filePath}`, error);
return `<dyad-file path="${normalizedRelativePath}">
// Error reading file: ${error}
</dyad-file>
`;
}
}
export async function extractCodebase({
appPath,
chatContext,
virtualFileSystem,
}: {
appPath: string;
chatContext: AppChatContext;
virtualFileSystem?: AsyncVirtualFileSystem;
}): Promise<{
formattedOutput: string;
files: CodebaseFile[];
}> {
const settings = readSettings();
const isSmartContextEnabled =
settings?.enableDyadPro && settings?.enableProSmartFilesContextMode;
try {
await fsAsync.access(appPath);
} catch {
return {
formattedOutput: `# Error: Directory ${appPath} does not exist or is not accessible`,
files: [],
};
}
const startTime = Date.now();
let files = await collectFiles(appPath, appPath);
if (virtualFileSystem) {
const deletedFiles = new Set(
virtualFileSystem
.getDeletedFiles()
.map((relativePath) => path.resolve(appPath, relativePath)),
);
files = files.filter((file) => !deletedFiles.has(file));
const virtualFiles = virtualFileSystem.getVirtualFiles();
for (const virtualFile of virtualFiles) {
const absolutePath = path.resolve(appPath, virtualFile.path);
if (!files.includes(absolutePath)) {
files.push(absolutePath);
}
}
}
const { contextPaths, smartContextAutoIncludes, excludePaths } = chatContext;
const includedFiles = new Set<string>();
const autoIncludedFiles = new Set<string>();
const excludedFiles = new Set<string>();
if (contextPaths && contextPaths.length > 0) {
for (const p of contextPaths) {
const resolvedPath = path.resolve(appPath, p);
try {
const stat = await fsAsync.stat(resolvedPath);
if (stat.isDirectory()) {
const dirFiles = await collectFiles(resolvedPath, appPath);
dirFiles.forEach((file) => includedFiles.add(path.normalize(file)));
} else if (stat.isFile()) {
includedFiles.add(path.normalize(resolvedPath));
}
} catch (error) {
logger.error(`Error accessing context path: ${resolvedPath}`, error);
}
}
}
if (isSmartContextEnabled && smartContextAutoIncludes) {
for (const p of smartContextAutoIncludes) {
const resolvedPath = path.resolve(appPath, p);
try {
const stat = await fsAsync.stat(resolvedPath);
if (stat.isDirectory()) {
const dirFiles = await collectFiles(resolvedPath, appPath);
dirFiles.forEach((file) => autoIncludedFiles.add(path.normalize(file)));
} else if (stat.isFile()) {
autoIncludedFiles.add(path.normalize(resolvedPath));
}
} catch (error) {
logger.error(`Error accessing smart context auto include path: ${resolvedPath}`, error);
}
}
}
if (excludePaths) {
for (const p of excludePaths) {
const resolvedPath = path.resolve(appPath, p);
try {
const stat = await fsAsync.stat(resolvedPath);
if (stat.isDirectory()) {
const dirFiles = await collectFiles(resolvedPath, appPath);
dirFiles.forEach((file) => excludedFiles.add(path.normalize(file)));
} else if (stat.isFile()) {
excludedFiles.add(path.normalize(resolvedPath));
}
} catch (error) {
logger.error(`Error accessing exclude path: ${resolvedPath}`, error);
}
}
}
if (contextPaths && contextPaths.length > 0) {
files = files.filter((file) => includedFiles.has(path.normalize(file)));
}
if (excludedFiles.size > 0) {
files = files.filter((file) => !excludedFiles.has(path.normalize(file)));
}
const sortedFiles = await sortFilesByModificationTime([...new Set(files)]);
const filesArray: CodebaseFile[] = [];
const formatPromises = sortedFiles.map(async (file) => {
const relativePath = path.relative(appPath, file);
const normalizedRelativePath = relativePath.split(path.sep).join("/");
const formattedContent = await formatFile({
filePath: file,
normalizedRelativePath,
virtualFileSystem,
});
filesArray.push({
path: normalizedRelativePath,
content: formattedContent,
});
return formattedContent;
});
const formattedFiles = await Promise.all(formatPromises);
const formattedOutput = formattedFiles.join("");
const endTime = Date.now();
logger.log("extractCodebase: time taken", endTime - startTime);
if (IS_TEST_BUILD) {
filesArray.sort((a, b) => a.path.localeCompare(b.path));
}
return {
formattedOutput,
files: filesArray,
};
}
Mermaid 图
工具模块架构图

数据验证流程图

代码提取流程图

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
类型断言 | 提供类型安全的断言机制,确保值不为 null 或 undefined 。 |
聊天接口 | 封装与聊天相关的 IPC 调用,提供创建应用和获取聊天记录的功能。 |
数据验证 | 定义多个 Zod Schema 和 TypeScript 类型,用于数据结构标准化和验证。 |
UI 提示 | 实现统一的 Toast 提示工具函数,支持多种提示类型和事件追踪。 |
样式工具 | 提供合并 Tailwind CSS 类名和生成可爱风格应用名称的功能。 |
代码提取 | 提取和格式化代码库内容,支持文件过滤、缓存和虚拟文件系统。 |
API 接口参数、类型和描述
接口名称 | 参数类型 | 描述 |
---|---|---|
createApp | CreateAppParams | 创建新应用并初始化聊天内容。 |
getAllChats | number (可选) | 获取所有聊天记录,支持按应用过滤。 |
配置选项及其类型和默认值
配置项 | 类型 | 默认值 | 描述 |
---|---|---|---|
theme | string | light | 用户界面主题。 |
language | string | en | 用户界面语言。 |
apiKey | string | “” | 提供者 API 密钥。 |
endpoint | string | “” | 提供者 API 端点(可选)。 |
数据模型字段、类型、约束和描述
字段名 | 类型 | 约束 | 描述 |
---|---|---|---|
key | string | 必填 | 密钥数据。 |
id | number | 必填 | 聊天记录 ID。 |
title | string | 必填 | 聊天记录标题。 |
name | string | 必填 | 大语言模型名称。 |
provider | string | 必填 | 大语言模型提供者。 |
结论/总结
工具模块通过提供类型断言、聊天接口封装、数据结构验证、UI 提示、样式处理和代码库提取等功能,为项目提供了稳定、可靠的辅助支持。这些工具不仅增强了代码的可维护性和可读性,还提高了开发效率,是项目中不可或缺的一部分。
前端组件库
简介
本项目前端组件库是构建用户界面的核心模块,基于 React 和 TypeScript 实现,采用模块化设计,提供丰富的 UI 组件和交互逻辑。组件库分为两个主要部分:src/components
和 scaffold/src/components
。前者包含项目核心业务组件,如聊天界面、应用管理、设置面板等;后者则提供通用 UI 组件,如按钮、输入框、对话框等,基于 Radix UI 和 Tailwind CSS 构建,支持高度定制化和响应式设计。
详细章节
核心业务组件
应用管理组件
应用管理组件负责展示和管理用户的应用列表,包括创建、导入、升级等功能。
AppList.tsx
:展示用户应用列表,支持创建新应用。CreateAppDialog.tsx
:创建新应用的对话框。ImportAppDialog.tsx
:导入本地应用的对话框。AppUpgrades.tsx
:展示和执行应用升级操作。
聊天组件
聊天组件是项目的核心交互模块,提供完整的聊天功能,包括消息展示、输入、附件管理等。
ChatPanel.tsx
:聊天面板容器,包含聊天头部、消息列表和输入框。ChatHeader.tsx
:聊天头部,包含版本面板、分支状态提示和新聊天创建。MessagesList.tsx
:聊天消息列表,支持流式控制和版本回滚。ChatInput.tsx
:聊天输入框,支持文本输入、附件上传和提案审批。ChatMessage.tsx
:单条聊天消息展示,支持 Markdown 渲染和动画效果。AttachmentsList.tsx
:聊天附件列表,支持图片预览和删除。FileAttachmentDropdown.tsx
:附件下拉菜单,提供附件上传选项。DragDropOverlay.tsx
:拖拽文件时的提示层。ChatErrorBox.tsx
:聊天错误信息展示。ChatError.tsx
:聊天错误提示。DeleteChatDialog.tsx
:删除聊天对话框。RenameChatDialog.tsx
:重命名聊天对话框。PromoMessage.tsx
:预设提示或推广信息展示。TokenBar.tsx
:token 使用情况展示。VersionPane.tsx
:应用版本历史展示和操作。
聊天组件数据流和状态管理
聊天组件的数据流从用户输入开始,经过 ChatInput
组件处理,通过 IPC 调用发送消息,然后在 MessagesList
中接收并展示消息。状态管理主要依赖于 Jotai 库,通过原子(atom)来管理聊天消息、流式计数器等状态。
ChatPanel.tsx
是聊天面板的容器组件,它接收chatId
作为 props,并使用 Jotai 管理messagesAtom
和streamingCounterAtom
状态。它负责协调布局、数据加载与滚动行为。ChatInput.tsx
组件负责处理用户输入,包括文本输入、附件上传和提案审批。它使用useAttachments
Hook 管理附件,并在提交消息时调用streamMessage
函数。MessagesList.tsx
组件负责渲染聊天消息列表,它接收messages
和streamingCounter
作为 props,并通过messagesEndRef
实现自动滚动到底部。ChatMessage.tsx
组件负责渲染单条聊天消息,它根据角色对齐内容,并使用 Markdown 解析器渲染内容。
关键聊天组件的实现细节
ChatPanel.tsx
:该组件是聊天功能的核心容器,它接收chatId
等 props 并结合 Jotai 管理消息和流式计数器状态。核心功能包括自动滚动到底部并检测用户手动滚动、异步加载指定chatId
的聊天记录、包含ChatHeader
、MessagesList
、ChatInput
和可选VersionPane
。ChatInput.tsx
:该组件支持文本输入、附件上传和提案审批。使用自动扩展的textarea
和useAttachments
Hook 管理附件。提交消息时调用streamMessage
并清空内容,显示提案详情并提供批准或拒绝操作,集成 PostHog 进行行为追踪。MessagesList.tsx
:该组件渲染聊天消息列表,展示ChatMessage
,支持流式控制、版本回滚与模型配置检查。提供 Undo/Retry 操作,显示推广信息,并通过messagesEndRef
实现自动滚动。ChatMessage.tsx
:该组件渲染单条聊天消息,根据角色对齐内容,助手消息显示加载动画。使用 Markdown 解析器渲染内容,末尾添加旋转指示器。若存在审批状态则显示图标和文本,结合framer-motion
实现动画效果。
聊天组件的扩展功能
- 附件管理:
AttachmentsList.tsx
和FileAttachmentDropdown.tsx
组件共同实现了附件的展示和上传功能。DragDropOverlay.tsx
在拖拽文件时提供视觉反馈。 - 错误处理:
ChatErrorBox.tsx
和ChatError.tsx
组件负责在聊天界面中展示错误信息,并提供升级 Dyad Pro 的链接。 - 版本控制:
VersionPane.tsx
组件展示和操作应用版本历史,加载版本数据并实现切换、恢复等操作。
设置组件
设置组件提供用户配置选项,包括 AI 模型选择、上下文文件路径配置、自动更新等。
SettingsList.tsx
:设置选项列表,支持分类和滚动导航。ChatModeSelector.tsx
:聊天模式选择器。ModelPicker.tsx
:语言模型选择器。ProModeSelector.tsx
:Dyad Pro 模式配置。ContextFilesPicker.tsx
:上下文文件路径配置。MaxChatTurnsSelector.tsx
:最大聊天轮次选择器。ThinkingBudgetSelector.tsx
:思考预算级别选择器。AutoApproveSwitch.tsx
:自动批准开关。AutoFixProblemsSwitch.tsx
:自动修复问题开关。AutoUpdateSwitch.tsx
:自动更新开关。ReleaseChannelSelector.tsx
:发布渠道选择器。TelemetrySwitch.tsx
:遥测数据发送开关。ApiKeyConfiguration.tsx
:API 密钥管理。ModelsSection.tsx
:AI 模型列表管理。ProviderSettingsHeader.tsx
:第三方服务提供商设置头部。ProviderSettingsPage.tsx
:语言模型提供商设置页面。ProviderSettings.tsx
:AI 提供商设置管理。
集成组件
集成组件负责与外部服务的连接和管理,如 GitHub、Neon、Supabase 和 Vercel。
GitHubConnector.tsx
:GitHub 连接管理。GitHubIntegration.tsx
:GitHub 集成设置。NeonConnector.tsx
:Neon 连接管理。NeonIntegration.tsx
:Neon 集成设置。SupabaseConnector.tsx
:Supabase 连接管理。SupabaseIntegration.tsx
:Supabase 集成设置。VercelConnector.tsx
:Vercel 连接管理。VercelIntegration.tsx
:Vercel 集成设置。
预览面板组件
预览面板组件提供应用预览、代码编辑、问题查看和发布管理功能。
PreviewPanel.tsx
:预览面板容器,支持模式切换。PreviewHeader.tsx
:预览面板头部,支持模式切换和问题统计。CodeView.tsx
:代码编辑视图。FileEditor.tsx
:文件编辑器,集成 Monaco Editor。FileTree.tsx
:文件树结构展示。Console.tsx
:应用日志展示。Problems.tsx
:TypeScript 项目代码问题展示。ConfigurePanel.tsx
:环境变量管理。NeonConfigure.tsx
:Neon 数据库信息展示。PublishPanel.tsx
:应用发布选项管理。
通用 UI 组件
通用 UI 组件基于 Radix UI 和 Tailwind CSS 构建,提供丰富的 UI 元素和交互组件。
accordion.tsx
:可折叠面板组件。alert-dialog.tsx
:警告对话框组件。alert.tsx
:提示框组件。badge.tsx
:徽章组件。button.tsx
:按钮组件。card.tsx
:卡片组件。checkbox.tsx
:复选框组件。dialog.tsx
:对话框组件。dropdown-menu.tsx
:下拉菜单组件。input.tsx
:输入框组件。label.tsx
:标签组件。LoadingBar.tsx
:加载条组件。popover.tsx
:弹出框组件。scroll-area.tsx
:可滚动区域组件。select.tsx
:下拉选择组件。separator.tsx
:分隔线组件。sheet.tsx
:抽屉式弹窗组件。sidebar.tsx
:侧边栏组件。skeleton.tsx
:骨架屏组件。switch.tsx
:开关组件。toggle-group.tsx
:切换按钮组组件。toggle.tsx
:切换按钮组件。tooltip.tsx
:提示信息组件。
Mermaid 图
组件库架构图

聊天组件数据流图

表格
主要功能或组件及其描述
组件名称 | 描述 |
---|---|
AppList | 展示用户应用列表并提供创建新应用的功能 |
ChatPanel | 展示和管理聊天面板的 React 组件 |
SettingsList | 用于展示设置选项列表的 UI 组件 |
PreviewPanel | 根据 previewModeAtom 动态渲染子组件 |
Button | 定义 Button 按钮组件,支持多种风格及大小 |
Dialog | 封装 Radix UI 的 Dialog 对话框组件 |
配置选项及其类型和默认值
配置项 | 类型 | 默认值 | 描述 |
---|---|---|---|
autoApproveChanges | boolean | false | 自动批准设置 |
enableAutoFixProblems | boolean | false | 自动修复问题功能 |
enableAutoUpdate | boolean | false | 自动更新设置 |
maxChatTurnsInContext | number | 10 | 最大聊天轮次 |
telemetryConsent | string | “opted_out” | 遥测数据发送选项 |
代码片段
AutoApproveSwitch.tsx
组件代码
import { useSettings } from "@/hooks/useSettings";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
export function AutoApproveSwitch() {
const { settings, updateSettings } = useSettings();
const { autoApproveChanges } = settings;
const handleCheckedChange = (checked: boolean) => {
updateSettings({ autoApproveChanges: checked });
};
return (
<div className="flex items-center space-x-2">
<Switch
id="auto-approve"
checked={autoApproveChanges}
onCheckedChange={handleCheckedChange}
/>
<Label htmlFor="auto-approve">Auto-approve changes</Label>
</div>
);
}
ChatInput.tsx
组件代码
import { useState } from "react";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { useAttachments } from "@/hooks/useAttachments";
export function ChatInput() {
const [input, setInput] = useState("");
const { attachments, addAttachment, removeAttachment } = useAttachments();
const handleSubmit = () => {
// 提交消息逻辑
};
return (
<div>
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
/>
<Button onClick={handleSubmit}>Send</Button>
</div>
);
}
结论/总结
本项目前端组件库通过模块化设计和丰富的 UI 组件,为开发者提供了构建复杂用户界面的强大工具。核心业务组件涵盖了应用管理、聊天、设置、集成和预览面板等关键功能,而通用 UI 组件则提供了基础的 UI 元素和交互组件,支持高度定制化和响应式设计。通过合理使用这些组件,可以快速构建出功能完善、用户体验良好的前端应用。特别是聊天组件,其复杂的数据流和状态管理机制,以及丰富的扩展功能,体现了组件库在处理复杂交互方面的强大能力。
数据库模块
简介
数据库模块是项目的核心组件之一,负责管理 SQLite 数据库的连接、初始化和数据迁移。它使用 drizzle-orm
和 better-sqlite3
来实现数据库操作,确保数据的一致性和可扩展性。该模块通过定义表结构和关系,支持应用开发与部署系统中的数据管理需求。
详细章节
数据库连接与初始化
数据库连接与初始化模块位于 src/db/index.ts
,主要功能包括构建数据库路径、初始化数据库并执行迁移脚本、使用单例模式管理数据库连接、提供访问接口 getDb()
以及封装数据库实例为 Proxy。这些功能确保了数据库连接的稳定性和可扩展性。
数据库连接管理实现机制
数据库连接管理采用单例模式设计,确保在整个应用生命周期中只有一个数据库连接实例存在。该机制通过以下方式实现:
- 数据库路径构建:模块首先构建数据库文件的完整路径,确保数据库文件在指定位置创建或访问。
- 连接初始化:使用
better-sqlite3
创建数据库连接实例,并配置相关选项。 - 迁移执行:在初始化过程中自动执行数据库迁移脚本,确保数据库结构与应用需求保持同步。
- 单例模式实现:通过变量缓存数据库连接实例,避免重复创建连接。
- 访问接口封装:提供
getDb()
函数作为统一的数据库访问入口。 - Proxy 封装:将数据库实例封装为 Proxy 对象,可能用于拦截操作或添加额外逻辑。
这种设计模式有效减少了数据库连接开销,提高了应用性能,并确保了数据操作的一致性。
表结构定义
表结构定义模块位于 src/db/schema.ts
,定义了 SQLite 数据库的表结构,支持应用开发与部署系统。核心表包括 apps
、chats
、messages
、versions
,以及语言模型相关表 language_model_providers
和 language_models
。各表包含时间戳字段,并通过外键建立关联关系,确保数据一致性。
核心数据表详细定义
apps 表
存储应用基本信息,包括:
id
(主键):应用唯一标识符name
:应用名称path
:应用路径- 时间戳字段:记录创建和更新时间
- GitHub 相关字段:如
github_repo
- Supabase 配置字段:如
supabase_project
chats 表
记录聊天会话信息:
id
(主键):聊天唯一标识符app_id
(外键):关联到apps
表title
:聊天标题commit_hash
:提交哈希值- 时间戳字段:记录创建和更新时间
- 支持级联删除:当关联的应用被删除时自动删除相关聊天记录
messages 表
存储具体消息内容:
id
(主键):消息唯一标识符chat_id
(外键):关联到chats
表role
:消息角色(如用户或AI)content
:消息内容文本approved
:审批状态布尔值commit_hash
:提交哈希值- 时间戳字段:记录创建和更新时间
- 支持级联删除:当关联的聊天被删除时自动删除相关消息
language_model_providers 表
管理语言模型服务提供商信息:
id
(主键):提供商唯一标识符name
:提供商名称
language_models 表
定义具体模型属性:
id
(主键):模型唯一标识符provider_id
(外键):关联到language_model_providers
表name
:模型名称
versions 表
跟踪应用版本信息:
id
(主键):版本唯一标识符app_id
(外键):关联到apps
表commit_hash
(唯一):提交哈希值,用于标识唯一版本
数据库结构快照
数据库结构快照文件位于 drizzle/meta
目录下,记录了数据库结构的演变过程。每个快照文件定义了特定版本的数据库结构,包括表的字段、类型、约束和关系。这些快照文件支持数据库的迁移、回滚和状态恢复。
数据库结构演变过程
数据库结构经历了多个版本的演进:
- 初始版本 (0000):包含基本的
apps
、chats
和messages
三张表,支持基础的聊天系统管理。 - 增加审批状态 (0001):在
messages
表中添加approved
字段,用于跟踪消息的审批状态。 - 增强级联删除 (0002):为
chats
和messages
表添加级联删除支持,确保数据一致性。 - 完善集成字段 (0003-0004):增加 GitHub 和 Supabase 集成相关字段,并引入
commit_hash
用于跟踪应用变更。 - 引入 AI 模型支持 (0005-0008):添加
language_model_providers
和language_models
表,支持 AI 聊天系统的建模与对话记录管理。 - 版本跟踪功能 (0009):新增
versions
表,用于跟踪应用版本,通过commit_hash
标识唯一性,支持应用程序开发、部署与交互的全生命周期管理。
数据库迁移
数据库迁移功能通过 _journal.json
文件实现,该文件记录了每次更新的索引、版本号、时间戳、标签及是否设置检查点。这些信息用于支持数据库迁移、回滚或状态恢复。
迁移机制详细说明
迁移机制的核心组件包括:
- 版本控制:通过版本号跟踪数据库结构的变化
- 时间戳记录:记录每次迁移的时间,便于审计和回滚
- 标签系统:为特定迁移添加标签,便于识别和管理
- 检查点设置:允许设置检查点以支持部分回滚
当前数据库版本为 “7”,使用 SQLite 方言。entries
数组详细记录了每次迁移的信息,包括:
idx
:迁移索引version
:版本号when
:时间戳tag
:标签breakpoints
:检查点设置
Mermaid图
数据库结构演变

数据库表关系

数据库连接管理流程

表格
主要功能或组件及其描述
功能或组件 | 描述 |
---|---|
数据库连接与初始化 | 构建数据库路径,初始化数据库并执行迁移脚本,使用单例模式管理数据库连接,提供访问接口 getDb() ,封装数据库实例为 Proxy |
表结构定义 | 定义 SQLite 数据库表结构,支持应用开发与部署系统,核心表包括 apps 、chats 、messages 、versions ,以及语言模型相关表 language_model_providers 和 language_models |
数据库结构快照 | 记录数据库结构的演变过程,支持数据库的迁移、回滚和状态恢复 |
数据库迁移 | 通过 _journal.json 文件记录每次更新的索引、版本号、时间戳、标签及是否设置检查点,支持数据库迁移、回滚或状态恢复 |
数据模型字段、类型、约束和描述
表名 | 字段名 | 类型 | 约束 | 描述 |
---|---|---|---|---|
apps | id | TEXT | PRIMARY KEY | 应用ID |
apps | name | TEXT | 应用名称 | |
apps | path | TEXT | 应用路径 | |
apps | github_repo | TEXT | GitHub仓库 | |
apps | supabase_project | TEXT | Supabase项目 | |
chats | id | TEXT | PRIMARY KEY | 聊天ID |
chats | app_id | TEXT | FOREIGN KEY | 应用ID |
chats | title | TEXT | 聊天标题 | |
chats | commit_hash | TEXT | 提交哈希 | |
messages | id | TEXT | PRIMARY KEY | 消息ID |
messages | chat_id | TEXT | FOREIGN KEY | 聊天ID |
messages | role | TEXT | 消息角色 | |
messages | content | TEXT | 消息内容 | |
messages | approved | BOOLEAN | 审批状态 | |
messages | commit_hash | TEXT | 提交哈希 | |
language_model_providers | id | TEXT | PRIMARY KEY | 提供商ID |
language_model_providers | name | TEXT | 提供商名称 | |
language_models | id | TEXT | PRIMARY KEY | 模型ID |
language_models | provider_id | TEXT | FOREIGN KEY | 提供商ID |
language_models | name | TEXT | 模型名称 | |
versions | id | TEXT | PRIMARY KEY | 版本ID |
versions | app_id | TEXT | FOREIGN KEY | 应用ID |
versions | commit_hash | TEXT | UNIQUE | 提交哈希 |
数据库迁移版本信息
版本文件 | 主要变更 |
---|---|
0000_snapshot.json | 初始版本,包含 apps、chats、messages 三表 |
0001_snapshot.json | 在 messages 表中添加 approved 字段 |
0002_snapshot.json | 为 chats 和 messages 表添加级联删除支持 |
0003_snapshot.json | 完善 apps 表的 GitHub 和 Supabase 字段 |
0004_snapshot.json | 在 chats 和 messages 表中添加 commit_hash 字段 |
0005_snapshot.json | 添加 language_model_providers 和 language_models 表 |
0006_snapshot.json | 所有表添加时间戳字段,默认使用 Unix 时间戳自动填充 |
0007_snapshot.json | 完善多模型、多应用聊天系统支持 |
0008_snapshot.json | 优化表间关系,支持集成平台数据管理 |
0009_snapshot.json | 添加 versions 表,支持应用版本跟踪 |
结论/总结
数据库模块通过定义表结构和关系,支持应用开发与部署系统中的数据管理需求。它使用 drizzle-orm
和 better-sqlite3
来实现数据库操作,确保数据的一致性和可扩展性。数据库结构快照文件记录了数据库结构的演变过程,支持数据库的迁移、回滚和状态恢复。数据库迁移功能通过 _journal.json
文件实现,确保数据库的稳定性和可维护性。
该模块采用单例模式管理数据库连接,通过 Proxy 封装提供统一访问接口,有效减少了连接开销并提高了性能。表结构设计考虑了数据完整性和关系约束,通过外键和级联删除机制确保数据一致性。随着版本演进,数据库结构逐步完善,从基础的聊天系统支持扩展到 AI 模型管理和应用版本跟踪,体现了良好的可扩展性和适应性。
应用主界面模块
简介
应用主界面模块是整个 React 应用的入口布局组件,负责定义整体结构和共享逻辑。它通过整合多个上下文提供者(如主题、深度链接和侧边栏状态)来协调 UI 布局与交互行为。该模块主要由两个核心组件构成:layout.tsx
和 TitleBar.tsx
,分别负责整体布局和标题栏渲染。
此模块确保了应用在不同平台和模式下的一致性体验,例如支持预览模式下的刷新操作、窗口控制按钮显示以及主题切换功能。
详细章节
主布局组件 (layout.tsx
)
layout.tsx
是应用的根布局组件,负责注入必要的上下文提供者并渲染共享 UI 元素。其主要职责包括:
- 使用
<ThemeProvider>
、<DeepLinkProvider>
和<SidebarProvider>
来管理主题、深度链接和侧边栏状态。 - 渲染
<TitleBar />
和<AppSidebar />
组件,包裹子路由内容以支持嵌套结构。 - 监听全局键盘事件,捕获
Ctrl+R
或Cmd+R
快捷键,在预览模式下调用refreshAppIframe()
方法进行刷新。 - 通过
previewModeAtom
获取当前是否处于预览模式。
关键函数与组件
RootLayout({ children })
: 主布局组件,接收子元素作为参数。useRunApp()
: 提供refreshAppIframe()
方法用于刷新 iframe。handleKeyDown(event)
: 处理全局键盘事件,实现快捷刷新功能。
Mermaid 图:主布局结构

标题栏组件 (TitleBar.tsx
)
TitleBar.tsx
负责渲染应用顶部标题栏,集成了多种功能以提升用户体验。其主要职责包括:
- 显示当前选中的应用名称,通过
selectedAppIdAtom
和useLoadApps
加载数据。 - 集成 Dyad Pro 功能,展示按钮和 AI 信用额度状态。
- 检测深链接并触发成功提示对话框。
- 在非 macOS 平台上显示窗口控制按钮(最小化、最大化、关闭)。
- 支持暗色/亮色主题切换。
- 提供跳转至应用详情页的功能,并支持预览面板头组件。
关键函数与组件
TitleBar
: 标题栏组件,整合了状态管理和用户交互逻辑。selectedAppIdAtom
: 用于获取当前选中应用 ID 的状态原子。useLoadApps
: 加载应用数据的自定义 Hook。
Mermaid 图:标题栏功能结构

键盘事件处理机制详解
handleKeyDown
函数实现细节
在 src/app/layout.tsx
文件中,通过 useEffect
钩子注册了一个全局键盘事件监听器,用于处理快捷刷新功能。以下是其完整实现:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "r" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
if (previewMode === "preview") {
refreshAppIframe();
}
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [refreshAppIframe, previewMode]);
关键机制分析
- 事件监听注册:
- 通过
document.addEventListener("keydown", handleKeyDown)
在组件挂载时注册全局键盘事件监听器 - 监听范围是整个文档(document)级别的keydown事件
- 快捷键检测逻辑:
- 检测
event.key === "r"
确定按下了R键 - 检测
event.ctrlKey || event.metaKey
确定同时按下了Ctrl键(Windows/Linux)或Cmd键(macOS) - 使用
event.preventDefault()
阻止浏览器默认的刷新行为
- 预览模式条件判断:
- 通过
previewMode === "preview"
判断当前是否处于预览模式 - 只有在预览模式下才会执行
refreshAppIframe()
方法
- 事件监听器清理:
- 通过
useEffect
的清理函数document.removeEventListener("keydown", handleKeyDown)
在组件卸载时移除事件监听器 - 避免内存泄漏和重复监听
- 依赖项管理:
useEffect
的依赖数组为[refreshAppIframe, previewMode]
,确保当这两个值变化时重新注册事件监听器
相关变量和方法
previewModeAtom
:
- 从
@/atoms/appAtoms
导入的状态原子 - 用于获取当前应用的预览模式状态
refreshAppIframe
:
- 通过
useRunApp
Hook 获取的方法 - 实现自定义的iframe刷新功能,替代浏览器默认刷新
表格:主要功能与组件
功能/组件 | 描述 |
---|---|
RootLayout | 主布局组件,协调上下文提供者和共享 UI 元素 |
TitleBar | 标题栏组件,显示应用名称、集成 Dyad Pro 功能并处理用户交互 |
ThemeProvider | 提供主题上下文,支持暗色/亮色模式切换 |
DeepLinkProvider | 管理深度链接状态,检测并响应外部链接 |
SidebarProvider | 管理侧边栏状态,支持动态显示和隐藏 |
AppSidebar | 应用侧边栏组件,提供导航功能 |
refreshAppIframe | 刷新 iframe 的方法,在预览模式下通过快捷键调用 |
handleKeyDown | 全局键盘事件监听器,处理 Ctrl+R / Cmd+R 快捷刷新 |
useRunApp | 自定义 Hook,提供运行和刷新应用的方法 |
selectedAppIdAtom | 状态原子,存储当前选中应用的 ID |
useLoadApps | 自定义 Hook,加载应用数据 |
结论/总结
应用主界面模块通过 layout.tsx
和 TitleBar.tsx
两个核心组件,构建了一个结构清晰、功能丰富的主界面。它不仅协调了主题、导航和刷新等基础功能,还集成了 Dyad Pro 和深链接等高级特性,提升了用户体验。该模块的设计体现了良好的分层架构和状态管理实践,为整个应用提供了稳定且可扩展的基础。特别是其键盘事件处理机制,通过精确的快捷键检测和预览模式判断,实现了流畅的刷新体验,同时避免了与浏览器默认行为的冲突。
路由与页面模块
简介
路由与页面模块负责定义和管理应用程序中的导航结构和页面渲染逻辑。通过声明式路由配置,将 URL 路径映射到对应的 React 页面组件,实现页面跳转、参数传递和状态管理。该模块基于 createRoute
和 createRootRoute
等 API 构建,结合 Jotai 和 React Query 实现状态与数据的响应式更新。
项目使用了统一的路由创建模式,基于 @tanstack/react-router
库。所有路由都通过 createRoute
函数创建,并挂载到根路由 rootRoute
上。页面组件广泛使用了 Jotai 和 React Query 进行状态管理,并通过 @tanstack/react-router
的 hooks 获取路由信息。
详细章节
路由结构
项目采用分层路由结构,所有路由均挂载于根路由 rootRoute
,并通过 createRoute
创建具体路径。路由配置文件位于 src/routes/
目录下,页面组件位于 src/pages/
目录下。
根路由
根路由 rootRoute
是所有子路由的父级,定义在 src/routes/root.tsx
中。它返回一个包裹在 Layout
组件中的 React 元素,并通过 <Outlet />
渲染子路由内容。
import { createRootRoute, Outlet } from "@tanstack/react-router";
import Layout from "../app/layout";
export const rootRoute = createRootRoute({
component: () => (
<Layout>
<Outlet />
</Layout>
),
});
页面路由
- 首页路由 (
/
)
定义在src/routes/home.tsx
,挂载于rootRoute
,渲染HomePage
组件。支持校验 URL 参数中的可选数字appId
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import HomePage from "../pages/home";
import { z } from "zod";
export const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: HomePage,
validateSearch: z.object({
appId: z.number().optional(),
}),
});
- 应用详情路由 (
/app-details
)
定义在src/routes/app-details.tsx
,挂载于rootRoute
,指向组件AppDetailsPage
。通过validateSearch
校验 URL 查询参数中的可选数字类型appId
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import AppDetailsPage from "../pages/app-details";
import { z } from "zod";
export const appDetailsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/app-details",
component: AppDetailsPage,
validateSearch: z.object({
appId: z.number().optional(),
}),
});
- 聊天路由 (
/chat
)
定义在src/routes/chat.tsx
,挂载于rootRoute
,关联组件ChatPage
。使用validateSearch
校验 URL 中的可选数字参数id
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import ChatPage from "../pages/chat";
import { z } from "zod";
export const chatRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/chat",
component: ChatPage,
validateSearch: z.object({
id: z.number().optional(),
}),
});
- Hub 路由 (
/hub
)
定义在src/routes/hub.ts
,挂载于rootRoute
,绑定组件HubPage
。用于页面跳转和加载,通过getParentRoute
确定父级路由关系。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import HubPage from "../pages/hub";
export const hubRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hub",
component: HubPage,
});
- 设置路由 (
/settings
)
定义在src/routes/settings.tsx
,挂载于rootRoute
,渲染组件SettingsPage
,提供设置页面的路由入口。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import SettingsPage from "../pages/settings";
export const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/settings",
component: SettingsPage,
});
- 提供商设置路由 (
/settings/providers/$provider
)
定义在src/routes/settings/providers/$provider.tsx
,动态匹配不同提供商。提取 URL 参数为ProviderSettingsParams
类型,通过useParams
获取当前provider
值并传递给ProviderSettingsPage
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "../../root";
import ProviderSettingsPage from "../../../pages/settings/providers";
import { z } from "zod";
interface ProviderSettingsParams {
provider: string;
}
export const providerSettingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/providers/$provider",
params: {
parse: (params: { provider: string }): ProviderSettingsParams => ({
provider: params.provider,
}),
stringify: (params: ProviderSettingsParams) => ({
provider: params.provider,
}),
},
component: function ProviderSettingsRouteComponent() {
const { provider } = providerSettingsRoute.useParams();
return <ProviderSettingsPage provider={provider} />;
},
});
页面组件
HomePage
HomePage
是首页组件,提供创建新 App 的入口并引导用户进入聊天或详情页。使用 useAtom
和自定义 Hooks(如 useLoadApps
, useStreamChat
)处理数据加载与设置。包含导入按钮、输入框、灵感提示等功能。提交时触发创建流程并导航至聊天页,同时检查版本更新并弹出 release notes。
import { useAtom } from "jotai";
import { useState } from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useLoadApps, useStreamChat } from "../hooks";
import { useSettings } from "../hooks/useSettings";
import { appsListAtom } from "../state";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { useAppVersion } from "../hooks/useAppVersion";
export default function HomePage() {
const navigate = useNavigate();
const search = useSearch({ strict: false });
const [inputValue, setInputValue] = useAtom(inputValueAtom);
const [apps] = useAtom(appsListAtom);
const { loadApps } = useLoadApps();
const { streamChat } = useStreamChat();
const { settings } = useSettings();
const [isLoading, setIsLoading] = useState(false);
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
const { version, releaseNotes } = useAppVersion();
// 组件实现...
}
AppDetailsPage
AppDetailsPage
是一个 React 页面组件,用于展示和管理单个应用的详细信息。通过 URL 参数获取应用 ID,并从 appsListAtom
中查找对应对象。支持重命名、复制、删除等操作,集成 GitHub、Supabase 等扩展配置组件。使用 Jotai 和 React Query 实现状态与数据缓存控制。
import { useAtom } from "jotai";
import { useSearch } from "@tanstack/react-router";
import { appsListAtom } from "../state";
import { useLoadApps } from "../hooks/useLoadApps";
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDebounce } from "../hooks/useDebounce";
import { useCheckName } from "../hooks/useCheckName";
export default function AppDetailsPage() {
const search = useSearch({ from: appDetailsRoute.id });
const [apps, setApps] = useAtom(appsListAtom);
const { loadApps } = useLoadApps();
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const queryClient = useQueryClient();
const [newName, setNewName] = useState("");
const debouncedName = useDebounce(newName, 500);
const { data: nameCheck } = useCheckName(debouncedName);
// 组件实现...
}
ChatPage
该文件是聊天页面组件,采用分屏结构,左侧为 ChatPanel
,右侧为可折叠的 PreviewPanel
。通过 useState
和 useEffect
控制面板状态与自动跳转逻辑。使用 useChats
Hook 获取聊天列表,联动 isPreviewOpenAtom
控制预览面板展开/折叠,支持拖拽调整大小。
import { useState, useEffect, useRef } from "react";
import { useSearch, useNavigate } from "@tanstack/react-router";
import { useAtom } from "jotai";
import { isPreviewOpenAtom } from "../state";
import { useChats } from "../hooks/useChats";
import ChatPanel from "../components/chat/ChatPanel";
import PreviewPanel from "../components/chat/PreviewPanel";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "../components/ui/resizable";
export default function ChatPage() {
const search = useSearch({ from: chatRoute.id });
const navigate = useNavigate();
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const { chats, isLoading } = useChats();
const [isResizing, setIsResizing] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
// 组件实现...
}
HubPage
HubPage
是模板选择页面,展示官方与社区模板,通过 TemplateCard
渲染并支持选中操作。点击”Create App”打开对话框创建新项目。顶部有返回按钮,底部集成 BackendSection
连接后端服务,是项目初始化的核心页面之一。
import { useState } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useTemplates } from "../hooks/useTemplates";
import { useSettings } from "../hooks/useSettings";
import TemplateCard from "../components/hub/TemplateCard";
import BackendSection from "../components/hub/BackendSection";
import { Button } from "../components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog";
export default function HubPage() {
const navigate = useNavigate();
const { templates, isLoading } = useTemplates();
const { settings, updateSetting } = useSettings();
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
// 组件实现...
}
SettingsPage
该页面用于管理全局设置,包括通用、工作流、AI、遥测、集成等多个模块。每个模块由独立函数组件实现,支持重置所有数据并提供确认对话框防止误操作。使用 useSettings
、useAppVersion
等 Hook 以及 IPC 客户端通信,实现完整的配置与持久化逻辑。
import { useState } from "react";
import { useSettings } from "../hooks/useSettings";
import { useTheme } from "../hooks/useTheme";
import { useAppVersion } from "../hooks/useAppVersion";
import GeneralSettings from "../components/settings/GeneralSettings";
import WorkflowSettings from "../components/settings/WorkflowSettings";
import AISettings from "../components/settings/AISettings";
import TelemetrySettings from "../components/settings/TelemetrySettings";
import IntegrationsSettings from "../components/settings/IntegrationsSettings";
import { Button } from "../components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "../components/ui/dialog";
export default function SettingsPage() {
const { settings, updateSetting, resetSettings } = useSettings();
const { theme, setTheme } = useTheme();
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const { version } = useAppVersion();
// 组件实现...
}
Mermaid 图
路由结构图

页面组件关系图

表格
路由与页面映射表
路由路径 | 页面组件 | 描述 |
---|---|---|
/ | HomePage | 首页,提供创建新 App 的入口 |
/app-details | AppDetailsPage | 展示和管理单个应用的详细信息 |
/chat | ChatPage | 聊天页面,分屏结构 |
/hub | HubPage | 模板选择页面 |
/settings | SettingsPage | 全局设置页面 |
/settings/providers/$provider | ProviderSettingsPage | 提供商设置页面 |
页面组件功能表
页面组件 | 功能描述 |
---|---|
HomePage | 创建新 App,引导用户进入聊天或详情页 |
AppDetailsPage | 展示和管理应用详情,支持重命名、复制、删除 |
ChatPage | 聊天功能,支持拖拽调整大小 |
HubPage | 模板选择,支持创建新项目 |
SettingsPage | 管理全局设置,支持重置数据 |
路由参数验证表
路由 | 参数名 | 类型 | 验证规则 |
---|---|---|---|
homeRoute | appId | number | 可选 |
appDetailsRoute | appId | number | 可选 |
chatRoute | id | number | 可选 |
providerSettingsRoute | provider | string | 必需,动态参数 |
结论/总结
路由与页面模块是应用程序的核心部分,负责管理导航和页面渲染。通过清晰的路由结构和页面组件设计,实现了高效的页面跳转和状态管理。该模块的准确性和直观性对于项目的可维护性和扩展性至关重要。
项目采用基于 @tanstack/react-router
的路由系统,通过 createRoute
和 createRootRoute
创建路由结构,并使用 Zod 进行参数验证。页面组件广泛使用 Jotai 和 React Query 进行状态管理,实现了响应式的数据流和高效的渲染机制。
路由参数验证确保了传入参数的类型安全,而动态路由支持了灵活的路径匹配。页面组件通过自定义 Hooks 和状态管理库实现了复杂的数据流和交互逻辑,为用户提供流畅的使用体验。
状态管理模块
简介
状态管理模块是本项目的核心组成部分,负责集中管理应用的全局状态。通过使用 Jotai 库,模块将状态划分为多个原子(atom),以实现响应式、细粒度的状态更新和共享。这些原子覆盖了应用的多个方面,包括应用数据、聊天交互、模型管理、UI 状态、预览逻辑等。
该模块通过将状态分散到独立的原子中,提升了组件之间的解耦性,使得状态的读取和更新更加高效和可维护。例如,appAtoms.ts
管理核心应用数据,而 chatAtoms.ts
专注于聊天相关状态。这种设计支持模块化开发,并为未来扩展提供了良好的基础。
详细章节
应用核心状态管理
src/atoms/appAtoms.ts
文件定义了多个原子,用于管理应用的核心数据。这些原子包括当前应用、应用列表、版本信息、预览模式和用户设置等。通过这些原子,应用能够实现全局状态的共享和响应式更新。
关键原子
currentAppAtom
: 当前应用的状态。appListAtom
: 应用列表数据。versionInfoAtom
: 版本相关信息。previewModeAtom
: 预览模式的开关状态。userSettingsAtom
: 用户个性化设置。
聊天状态管理
src/atoms/chatAtoms.ts
文件专注于聊天功能的状态管理。它定义了多个原子,用于存储聊天消息、错误信息、选中ID、输入内容和加载状态等。这些原子共同支持聊天交互逻辑的实现。
关键原子
messagesAtom
: 聊天消息列表。chatErrorAtom
: 聊天过程中的错误信息。selectedIdAtom
: 当前选中的聊天项ID。inputContentAtom
: 用户输入的内容。isLoadingAtom
: 聊天加载状态。
本地模型状态管理
src/atoms/localModelsAtoms.ts
文件负责管理本地模型及 LM Studio 模型的状态。它通过定义原子来存储模型列表、加载状态和错误信息,从而实现对模型数据的集中响应式管理。
关键原子
localModelsAtom
: 本地模型列表。lmStudioModelsAtom
: LM Studio 模型列表。modelsLoadingAtom
: 模型加载状态。modelsErrorAtom
: 模型加载错误信息。
预览与视图状态管理
src/atoms/previewAtoms.ts
和 src/atoms/viewAtoms.ts
文件分别管理组件预览和视图相关的状态。previewAtoms.ts
定义了 selectedComponentPreviewAtom
用于存储组件预览状态,而 viewAtoms.ts
则定义了 isPreviewOpenAtom
和 selectedFileAtom
用于管理预览面板的开关和选中文件路径。
关键原子
selectedComponentPreviewAtom
: 组件预览状态。isPreviewOpenAtom
: 预览面板是否打开。selectedFileAtom
: 当前选中文件路径。
提案与 Supabase 状态管理
src/atoms/proposalAtoms.ts
和 src/atoms/supabaseAtoms.ts
文件分别管理提案结果和 Supabase 相关状态。proposalAtoms.ts
定义了 proposalResultAtom
用于存储提案结果数据,而 supabaseAtoms.ts
则管理项目列表、加载状态、错误信息和当前选中项目。
关键原子
proposalResultAtom
: 提案结果数据。supabaseProjectsAtom
: Supabase 项目列表。supabaseLoadingAtom
: Supabase 数据加载状态。supabaseErrorAtom
: Supabase 数据加载错误信息。selectedProjectAtom
: 当前选中的 Supabase 项目。
UI 状态管理
src/atoms/uiAtoms.ts
文件定义了 dropdownOpenAtom
原子,用于跟踪下拉菜单是否打开。这个原子支持 UI 中下拉状态的共享与同步,确保用户交互的一致性。
关键原子
dropdownOpenAtom
: 下拉菜单的开关状态。
版本检出状态管理
src/store/appAtoms.ts
文件定义了与 checkoutVersion
操作相关的状态原子。activeCheckoutCounterAtom
存储当前操作数量,而 isAnyCheckoutVersionInProgressAtom
是其派生原子,用于判断是否有操作正在进行。
关键原子
activeCheckoutCounterAtom
: 当前版本检出操作计数。isAnyCheckoutVersionInProgressAtom
: 是否有版本检出操作正在进行。
Jotai 状态原子实现与使用模式
原子定义方式
项目中所有 Jotai 原子均通过 atom()
函数创建,采用基础原子和派生原子两种模式。基础原子使用初始值进行初始化,而派生原子则通过函数计算得到其值。
基础原子定义
基础原子是状态管理的基本单元,它们存储简单的值,如布尔值、字符串、对象或数组。例如:
// src/atoms/appAtoms.ts
import { atom } from 'jotai';
export const currentAppAtom = atom(null);
export const appListAtom = atom([]);
export const versionInfoAtom = atom(null);
export const previewModeAtom = atom(false);
export const userSettingsAtom = atom({});
// src/atoms/chatAtoms.ts
import { atom } from 'jotai';
export const messagesAtom = atom([]);
export const chatErrorAtom = atom(null);
export const selectedIdAtom = atom(null);
export const inputContentAtom = atom('');
export const isLoadingAtom = atom(false);
// src/atoms/localModelsAtoms.ts
import { atom } from 'jotai';
export const localModelsAtom = atom([]);
export const lmStudioModelsAtom = atom([]);
export const modelsLoadingAtom = atom(false);
export const modelsErrorAtom = atom(null);
// src/atoms/previewAtoms.ts
import { atom } from 'jotai';
export const selectedComponentPreviewAtom = atom(null);
// src/atoms/proposalAtoms.ts
import { atom } from 'jotai';
export const proposalResultAtom = atom(null);
// src/atoms/supabaseAtoms.ts
import { atom } from 'jotai';
export const supabaseProjectsAtom = atom([]);
export const supabaseLoadingAtom = atom(false);
export const supabaseErrorAtom = atom(null);
export const selectedProjectAtom = atom(null);
// src/atoms/uiAtoms.ts
import { atom } from 'jotai';
export const dropdownOpenAtom = atom(false);
// src/atoms/viewAtoms.ts
import { atom } from 'jotai';
export const isPreviewOpenAtom = atom(false);
export const selectedFileAtom = atom(null);
派生原子定义
派生原子通过函数计算得到其值,它们依赖于其他原子的状态。例如:
// src/store/appAtoms.ts
import { atom } from 'jotai';
export const activeCheckoutCounterAtom = atom(0);
export const isAnyCheckoutVersionInProgressAtom = atom(
(get) => get(activeCheckoutCounterAtom) > 0
);
在这个例子中,isAnyCheckoutVersionInProgressAtom
是一个派生原子,它依赖于 activeCheckoutCounterAtom
的值。当 activeCheckoutCounterAtom
的值大于 0 时,isAnyCheckoutVersionInProgressAtom
返回 true
。
原子在组件中的使用方式
在组件中,原子通过 useAtom
钩子进行读取和更新。useAtom
返回一个数组,第一个元素是原子的当前值,第二个元素是更新原子值的函数。
import { useAtom } from 'jotai';
import { messagesAtom } from '../atoms/chatAtoms';
const ChatComponent = () => {
const [messages, setMessages] = useAtom(messagesAtom);
const addMessage = (message) => {
setMessages(prev => [...prev, message]);
};
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg.text}</div>
))}
<button onClick={() => addMessage({text: 'New message'})}>
Add Message
</button>
</div>
);
};
原子间依赖关系和组合模式
原子之间可以建立依赖关系,形成复杂的状态管理网络。例如,一个派生原子可以依赖多个基础原子,或者依赖其他派生原子。
// 示例:复合派生原子
const filteredMessagesAtom = atom(
(get) => {
const messages = get(messagesAtom);
const filter = get(filterAtom);
return messages.filter(msg => msg.type === filter);
}
);
这种组合模式使得状态管理更加灵活和可复用。
Mermaid图
状态管理模块架构

应用核心状态详细流程

聊天状态详细流程

原子依赖关系图

表格
主要功能或组件及其描述
功能/组件 | 描述 |
---|---|
应用核心状态 | 管理应用的核心数据,如当前应用、应用列表、版本信息等。 |
聊天状态 | 管理聊天相关状态,包括消息、错误、选中ID、输入内容、加载状态等。 |
本地模型状态 | 管理本地模型及LM Studio模型的状态,包括模型列表、加载状态和错误信息。 |
预览与视图状态 | 管理组件预览状态和视图状态,如预览面板开关和选中文件路径。 |
提案与Supabase状态 | 管理提案结果和Supabase相关状态,包括项目列表、加载状态、错误信息和当前选中项目。 |
UI状态 | 管理UI相关状态,如下拉菜单的开关状态。 |
版本检出状态 | 管理版本检出操作的状态,包括操作计数和是否正在进行。 |
原子类型及其使用模式
原子类型 | 特征 | 使用场景 |
---|---|---|
基础原子 | 存储简单值(布尔值、字符串、对象、数组等) | 存储基本状态数据 |
派生原子 | 通过函数计算得到值,依赖其他原子 | 计算派生状态、组合多个原子状态 |
代码片段
应用核心状态原子定义
// src/atoms/appAtoms.ts
import { atom } from 'jotai';
export const currentAppAtom = atom(null);
export const appListAtom = atom([]);
export const versionInfoAtom = atom(null);
export const previewModeAtom = atom(false);
export const userSettingsAtom = atom({});
聊天状态原子定义
// src/atoms/chatAtoms.ts
import { atom } from 'jotai';
export const messagesAtom = atom([]);
export const chatErrorAtom = atom(null);
export const selectedIdAtom = atom(null);
export const inputContentAtom = atom('');
export const isLoadingAtom = atom(false);
本地模型状态原子定义
// src/atoms/localModelsAtoms.ts
import { atom } from 'jotai';
export const localModelsAtom = atom([]);
export const lmStudioModelsAtom = atom([]);
export const modelsLoadingAtom = atom(false);
export const modelsErrorAtom = atom(null);
预览与视图状态原子定义
// src/atoms/previewAtoms.ts
import { atom } from 'jotai';
export const selectedComponentPreviewAtom = atom(null);
// src/atoms/viewAtoms.ts
import { atom } from 'jotai';
export const isPreviewOpenAtom = atom(false);
export const selectedFileAtom = atom(null);
提案与Supabase状态原子定义
// src/atoms/proposalAtoms.ts
import { atom } from 'jotai';
export const proposalResultAtom = atom(null);
// src/atoms/supabaseAtoms.ts
import { atom } from 'jotai';
export const supabaseProjectsAtom = atom([]);
export const supabaseLoadingAtom = atom(false);
export const supabaseErrorAtom = atom(null);
export const selectedProjectAtom = atom(null);
UI状态原子定义
// src/atoms/uiAtoms.ts
import { atom } from 'jotai';
export const dropdownOpenAtom = atom(false);
版本检出状态原子定义
// src/store/appAtoms.ts
import { atom } from 'jotai';
export const activeCheckoutCounterAtom = atom(0);
export const isAnyCheckoutVersionInProgressAtom = atom(
(get) => get(activeCheckoutCounterAtom) > 0
);
组件中使用原子示例
import { useAtom } from 'jotai';
import { messagesAtom } from '../atoms/chatAtoms';
const ChatComponent = () => {
const [messages, setMessages] = useAtom(messagesAtom);
const addMessage = (message) => {
setMessages(prev => [...prev, message]);
};
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg.text}</div>
))}
<button onClick={() => addMessage({text: 'New message'})}>
Add Message
</button>
</div>
);
};
派生原子使用示例
// 复合派生原子示例
const filteredMessagesAtom = atom(
(get) => {
const messages = get(messagesAtom);
const filter = get(filterAtom);
return messages.filter(msg => msg.type === filter);
}
);
结论/总结
状态管理模块通过 Jotai 原子化状态管理,实现了应用内各组件间状态的高效共享与响应式更新。模块覆盖了从应用核心数据到UI交互的各个方面,确保了状态的一致性和可维护性。通过将状态分散到独立的原子中,模块提升了组件解耦性,为应用的扩展和维护提供了坚实的基础。
项目中广泛使用了 Jotai 的基础原子和派生原子模式。所有原子均使用 atom()
函数创建,部分原子之间存在依赖关系。这种设计使得状态管理更加灵活和可复用,同时保持了代码的简洁性和可读性。通过 useAtom
钩子,组件可以轻松地读取和更新原子值,实现了状态与UI的无缝集成。
上下文管理模块
简介
上下文管理模块是 React 应用中用于实现全局状态共享和响应式更新的核心机制。该模块通过 React Context API 提供了两个关键上下文:DeepLinkContext
和 ThemeContext
,分别用于处理深度链接事件和用户界面主题管理。
DeepLinkContext
通过 DeepLinkProvider
组件订阅 IPC 客户端事件,确保即使接收到相同的深度链接也能触发状态更新,从而实现组件对深度链接的响应。ThemeContext
则通过 ThemeProvider
组件管理用户界面主题(如浅色、深色模式),并支持持久化和系统主题同步。
这些上下文通过 Provider 组件进行初始化和状态分发,通过自定义 Hook 提供对状态的访问能力,是应用全局状态管理的重要组成部分。
详细章节
深度链接上下文 (DeepLinkContext
)
架构与组件
DeepLinkContext
提供了一个全局状态 lastDeepLink
,用于存储最近一次接收到的深度链接及其时间戳。通过 DeepLinkProvider
组件订阅 IPC 客户端事件,确保即使接收到相同的深度链接也能触发状态更新。
数据流与逻辑
DeepLinkProvider
初始化时订阅 IPC 客户端的深度链接事件。- 当事件触发时,更新
lastDeepLink
状态,包含链接内容和时间戳。 useDeepLink
Hook 提供对lastDeepLink
状态的访问,使组件能够响应深度链接变化。
关键组件
DeepLinkContext
: React 上下文对象。DeepLinkProvider
: 提供者组件,负责状态管理和事件订阅。useDeepLink
: 自定义 Hook,用于访问深度链接状态。
Mermaid 图

表格:主要功能与描述
功能/组件 | 描述 |
---|---|
DeepLinkContext | React 上下文,用于管理深度链接全局状态。 |
DeepLinkProvider | 提供者组件,订阅事件并更新状态。 |
useDeepLink | 自定义 Hook,提供对深度链接状态的访问。 |
主题上下文 (ThemeContext
)
架构与组件
ThemeContext
管理用户的主题偏好,包括系统默认、浅色和深色三种模式。通过 ThemeProvider
组件初始化主题状态,并将其保存到 localStorage
中以实现持久化。组件根据当前主题设置 HTML 的类名,以应用相应的样式。
数据流与逻辑
ThemeProvider
初始化时从localStorage
读取主题偏好,若无则使用系统默认。- 监听系统主题变化,自动同步更新应用主题。
- 用户切换主题时,更新状态并保存到
localStorage
。 useTheme
Hook 提供对当前主题状态的访问和主题切换功能。
关键组件
ThemeContext
: React 上下文对象。ThemeProvider
: 提供者组件,负责主题状态管理和持久化。useTheme
: 自定义 Hook,用于访问和切换主题。
Mermaid 图

表格:主要功能与描述
功能/组件 | 描述 |
---|---|
ThemeContext | React 上下文,用于管理主题全局状态。 |
ThemeProvider | 提供者组件,初始化状态并监听系统变化。 |
useTheme | 自定义 Hook,提供主题访问和切换功能。 |
结论/总结
上下文管理模块通过 DeepLinkContext
和 ThemeContext
实现了应用中关键全局状态的管理。它们通过 Provider 和 Hook 的组合,提供了高效、响应式的状态分发机制,增强了组件间的解耦和可维护性。这些上下文是应用用户体验和功能响应能力的重要基础。
Supabase管理模块
简介
Supabase管理模块负责与 Supabase 平台进行交互,包括配置获取、客户端初始化、身份验证、SQL 执行、函数管理等功能。该模块为前端和后端提供了统一的接口,用于集成和管理 Supabase 服务。模块通过封装 Supabase API 调用,简化了开发者在项目中使用 Supabase 的复杂性。
详细章节
配置与上下文管理
该部分负责从 Supabase 获取配置信息并生成前端集成所需数据。核心函数包括:
getPublishableKey({ projectId })
: 异步获取匿名 API 密钥。getSupabaseClientCode({ projectId })
: 生成初始化客户端代码模板。getSupabaseContext({ supabaseProjectId })
: 返回项目上下文信息,如 ID、密钥、schema 等。
Mermaid图

表格
函数名 | 参数 | 描述 |
---|---|---|
getPublishableKey | projectId | 异步获取匿名 API 密钥 |
getSupabaseClientCode | projectId | 生成初始化客户端代码模板 |
getSupabaseContext | supabaseProjectId | 返回项目上下文信息 |
管理客户端
实现与 Supabase 管理 API 的交互逻辑,支持令牌管理、SQL 执行、函数部署等操作。主要功能包括:
isTokenExpired()
和refreshSupabaseToken()
: 判断和刷新访问令牌。getSupabaseClient()
: 创建认证后的 API 客户端。getSupabaseProjectName()
: 获取项目名称。executeSupabaseSql()
: 执行 SQL 查询。deleteSupabaseFunction()
和deploySupabaseFunctions()
: 管理函数。
Mermaid图

表格
函数名 | 参数 | 描述 |
---|---|---|
isTokenExpired | 无 | 判断访问令牌是否过期 |
refreshSupabaseToken | 无 | 刷新访问令牌 |
getSupabaseClient | 无 | 创建认证后的 API 客户端 |
getSupabaseProjectName | 无 | 获取项目名称 |
executeSupabaseSql | 无 | 执行 SQL 查询 |
deleteSupabaseFunction | 无 | 删除函数 |
deploySupabaseFunctions | 无 | 部署函数 |
OAuth 返回处理
定义 handleSupabaseOAuthReturn
函数,接收 OAuth 登录返回的 token 信息,并调用 writeSettings
将其写入配置中,保存 accessToken
、refreshToken
、expiresIn
和 tokenTimestamp
,用于后续身份验证和刷新。
表格
函数名 | 参数 | 描述 |
---|---|---|
handleSupabaseOAuthReturn | token 信息 | 处理 OAuth 返回的 token 信息 |
数据库模式查询
定义 SUPABASE_SCHEMA_QUERY
SQL 查询语句,通过 CTE 提取数据库模式信息,包括表、策略、函数和触发器的元数据,并统一输出为 JSON 格式,便于展示和处理数据库结构。
表格
变量名 | 描述 |
---|---|
SUPABASE_SCHEMA_QUERY | SQL 查询语句,用于提取数据库模式信息 |
工具函数
定义 isServerFunction(filePath)
函数,判断路径是否以 "supabase/functions/"
开头,用于识别 Supabase 服务器端函数文件。
表格
函数名 | 参数 | 描述 |
---|---|---|
isServerFunction | filePath | 判断路径是否为 Supabase 服务器端函数文件 |
OAuth认证流程详解
OAuth返回处理器实现
handleSupabaseOAuthReturn
函数位于src/supabase_admin/supabase_return_handler.ts
文件中,其具体实现如下:
import { writeSettings } from "../main/settings";
export function handleSupabaseOAuthReturn({
token,
refreshToken,
expiresIn,
}: {
token: string;
refreshToken: string;
expiresIn: number;
}) {
writeSettings({
supabase: {
accessToken: {
value: token,
},
refreshToken: {
value: refreshToken,
},
expiresIn,
tokenTimestamp: Math.floor(Date.now() / 1000),
},
});
}
该函数接收OAuth登录返回的token信息,包括:
token
: 访问令牌refreshToken
: 刷新令牌expiresIn
: 令牌过期时间(秒)
然后调用writeSettings
函数将这些信息写入配置中,并添加一个tokenTimestamp
字段记录当前时间戳。
令牌管理机制
在supabase_management_client.ts
文件中,实现了令牌管理的相关功能:
isTokenExpired
函数用于检查访问令牌是否过期:
function isTokenExpired(expiresIn?: number): boolean {
if (!expiresIn) return true;
// Get when the token was saved (expiresIn is stored at the time of token receipt)
const settings = readSettings();
const tokenTimestamp = settings.supabase?.tokenTimestamp || 0;
const currentTime = Math.floor(Date.now() / 1000);
// Check if the token is expired or about to expire (within 5 minutes)
return currentTime >= tokenTimestamp + expiresIn - 300;
}
该函数会检查令牌是否已过期或即将过期(提前5分钟)。它通过比较当前时间和令牌创建时间加上过期时间来判断。
refreshSupabaseToken
函数用于刷新访问令牌:
export async function refreshSupabaseToken(): Promise<void> {
const settings = readSettings();
const refreshToken = settings.supabase?.refreshToken?.value;
if (!isTokenExpired(settings.supabase?.expiresIn)) {
return;
}
if (!refreshToken) {
throw new Error(
"Supabase refresh token not found. Please authenticate first.",
);
}
try {
// Make request to Supabase refresh endpoint
const response = await fetch(
"https://supabase-oauth.dyad.sh/api/connect-supabase/refresh",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshToken }),
},
);
if (!response.ok) {
throw new Error(
`Supabase token refresh failed. Try going to Settings to disconnect Supabase and then reconnect to Supabase. Error status: ${response.statusText}`,
);
}
const {
accessToken,
refreshToken: newRefreshToken,
expiresIn,
} = await response.json();
// Update settings with new tokens
writeSettings({
supabase: {
accessToken: {
value: accessToken,
},
refreshToken: {
value: newRefreshToken,
},
expiresIn,
tokenTimestamp: Math.floor(Date.now() / 1000), // Store current timestamp
},
});
} catch (error) {
logger.error("Error refreshing Supabase token:", error);
throw error;
}
}
该函数会向特定的刷新端点发送请求,获取新的访问令牌和刷新令牌,并更新设置。
配置存储机制
系统使用JSON文件来存储用户设置,并且对敏感信息(如访问令牌和刷新令牌)进行了加密处理。
Supabase
数据结构定义在schemas.ts
中:
export const SupabaseSchema = z.object({
accessToken: SecretSchema.optional(),
refreshToken: SecretSchema.optional(),
expiresIn: z.number().optional(),
tokenTimestamp: z.number().optional(),
});
export type Supabase = z.infer<typeof SupabaseSchema>;
- 在写入设置时,系统会对敏感信息进行加密:
if (newSettings.supabase) {
if (newSettings.supabase.accessToken) {
newSettings.supabase.accessToken = encrypt(
newSettings.supabase.accessToken.value,
);
}
if (newSettings.supabase.refreshToken) {
newSettings.supabase.refreshToken = encrypt(
newSettings.supabase.refreshToken.value,
);
}
}
- 在读取设置时,系统会解密敏感信息:
const supabase = combinedSettings.supabase;
if (supabase) {
if (supabase.refreshToken) {
const encryptionType = supabase.refreshToken.encryptionType;
if (encryptionType) {
supabase.refreshToken = {
value: decrypt(supabase.refreshToken),
encryptionType,
};
}
}
if (supabase.accessToken) {
const encryptionType = supabase.accessToken.encryptionType;
if (encryptionType) {
supabase.accessToken = {
value: decrypt(supabase.accessToken),
encryptionType,
};
}
}
}
结论/总结
Supabase管理模块通过封装 Supabase API 调用,提供了配置获取、客户端初始化、身份验证、SQL 执行、函数管理等功能,简化了开发者在项目中使用 Supabase 的复杂性。该模块为前端和后端提供了统一的接口,用于集成和管理 Supabase 服务。特别是OAuth认证流程的实现,确保了用户身份的安全验证和令牌的自动刷新,提升了用户体验。
路径管理模块
简介
路径管理模块负责在 Electron 应用中提供跨平台和跨环境的目录路径获取功能。该模块通过封装路径逻辑,确保在开发和生产环境中都能正确解析用户数据、缓存和其他关键目录路径。模块的核心职责是统一处理 Electron 和非 Electron 环境之间的路径差异,从而简化应用的路径管理需求。
详细章节
核心函数
路径管理模块提供了一系列工具函数,用于获取不同场景下的目录路径。这些函数根据运行环境(如是否为测试构建)和平台(如 Windows、macOS、Linux)返回正确的路径。
getDyadAppPath
该函数根据是否为测试构建返回用户数据或主目录的 "dyad-apps"
子路径。其主要用途是为 dyad 应用提供一个统一的存储目录。
export function getDyadAppPath(appPath: string): string {
if (IS_TEST_BUILD) {
const electron = getElectron();
return path.join(electron!.app.getPath("userData"), "dyad-apps", appPath);
}
return path.join(os.homedir(), "dyad-apps", appPath);
}
getTypeScriptCachePath
该函数返回 TypeScript 编译缓存路径。这对于需要编译 TypeScript 代码的应用来说至关重要,因为它确保了编译结果的缓存位置一致。
export function getTypeScriptCachePath(): string {
const electron = getElectron();
return path.join(electron!.app.getPath("sessionData"), "typescript-cache");
}
getUserDataPath
该函数统一处理 Electron 和非 Electron 环境下的用户数据路径。在 Electron 环境中,它使用 app.getPath('userData')
获取路径;在非 Electron 环境中,它使用用户的主目录。
export function getUserDataPath(): string {
const electron = getElectron();
// When running in Electron and app is ready
if (process.env.NODE_ENV !== "development" && electron) {
return electron!.app.getPath("userData");
}
// For development or when the Electron app object isn't available
return path.resolve("./userData");
}
getElectron
该函数安全地引入 Electron 模块。它通过 require
动态加载 Electron,并在加载失败时返回 undefined
,从而避免因 Electron 模块不存在而导致的运行时错误。
export function getElectron(): typeof import("electron") | undefined {
let electron: typeof import("electron") | undefined;
try {
// Check if we're in an Electron environment
if (process.versions.electron) {
electron = require("electron");
}
} catch {
// Not in Electron environment
}
return electron;
}
流程图
以下流程图展示了路径管理模块中各函数的调用关系和路径解析逻辑:

主要功能表
功能名称 | 描述 |
---|---|
getDyadAppPath | 根据是否为测试构建返回 dyad-apps 路径 |
getTypeScriptCachePath | 返回 TypeScript 编译缓存路径 |
getUserDataPath | 统一处理 Electron 和非 Electron 环境的用户数据路径 |
getElectron | 安全引入 Electron 模块 |
结论
路径管理模块通过提供一组工具函数,简化了 Electron 应用中路径的处理逻辑。它确保了在不同环境和平台下路径的一致性,从而提高了应用的可移植性和稳定性。模块的设计考虑了 Electron 和非 Electron 环境的差异,并通过安全加载机制避免了潜在的运行时错误。
客户端逻辑模块
简介
客户端逻辑模块负责初始化和配置应用程序运行所需的环境变量,特别是在与数据库和邮件服务相关的部分。该模块通过调用 neonTemplateHook
函数来创建 Neon 项目并设置关键环境变量,例如数据库连接字符串和安全密钥。此模块在整个项目中扮演着基础配置和初始化的角色,确保应用在启动时具备必要的运行环境。
详细章节
Neon 项目初始化
neonTemplateHook
是客户端逻辑模块中的核心函数,其主要职责是创建一个 Neon 项目并配置应用的环境变量。该函数接收一个包含 appId
和 appName
的对象参数,并使用 IpcClient.getInstance()
调用 createNeonProject
方法来创建项目。创建成功后,它会调用 setAppEnvVars
方法来设置数据库连接、安全密钥、服务器地址和 Gmail 配置等环境变量。
关键函数
neonTemplateHook({ appId, appName })
: 异步函数,用于创建 Neon 项目并设置环境变量。IpcClient.getInstance().createNeonProject
: 通过IpcClient
单例调用,用于创建 Neon 项目。IpcClient.getInstance().setAppEnvVars
: 用于设置应用的环境变量,包括数据库连接字符串和安全密钥。
数据流
- 调用
neonTemplateHook
函数,传入包含appId
和appName
的对象。 - 通过
IpcClient.getInstance()
调用createNeonProject
创建 Neon 项目。 - 调用
IpcClient.getInstance().setAppEnvVars
设置环境变量,包括POSTGRES_URL
和PAYLOAD_SECRET
。
环境变量配置
在 neonTemplateHook
函数中,环境变量的配置是关键步骤之一。POSTGRES_URL
使用 Neon 项目生成的连接字符串,而 PAYLOAD_SECRET
则使用随机生成的 UUID。此外,还配置了服务器地址和 Gmail 相关的环境变量。这些配置确保了应用在启动时能够正确连接到数据库并具备必要的安全配置。
配置元素
POSTGRES_URL
: 数据库连接字符串,由 Neon 项目生成。PAYLOAD_SECRET
: 应用安全密钥,使用随机 UUID 生成。NEXT_PUBLIC_SERVER_URL
: 服务器地址,默认为 “http://localhost:32100″。GMAIL_USER
: Gmail 用户名,默认为 “example@gmail.com”。GOOGLE_APP_PASSWORD
: Gmail 应用密码,需要用户在 Google 账户中生成。
Mermaid图
Neon 项目初始化流程

该流程图展示了 neonTemplateHook
函数的执行流程,从创建 Neon 项目到设置环境变量的完整过程。
表格
主要功能或组件
功能/组件 | 描述 |
---|---|
neonTemplateHook | 异步函数,用于创建 Neon 项目并设置环境变量 |
createNeonProject | 通过 IpcClient 单例调用,用于创建 Neon 项目 |
setAppEnvVars | 用于设置应用的环境变量 |
环境变量配置
变量名 | 类型 | 描述 |
---|---|---|
POSTGRES_URL | string | 数据库连接字符串,由 Neon 生成 |
PAYLOAD_SECRET | string | 应用安全密钥,使用随机 UUID 生成 |
NEXT_PUBLIC_SERVER_URL | string | 服务器地址,默认为 “http://localhost:32100” |
GMAIL_USER | string | Gmail 用户名,默认为 “example@gmail.com” |
GOOGLE_APP_PASSWORD | string | Gmail 应用密码,需要用户生成 |
代码片段
export async function neonTemplateHook({
appId,
appName,
}: {
appId: number;
appName: string;
}) {
console.log("Creating Neon project");
const neonProject = await IpcClient.getInstance().createNeonProject({
name: appName,
appId: appId,
});
console.log("Neon project created", neonProject);
await IpcClient.getInstance().setAppEnvVars({
appId: appId,
envVars: [
{
key: "POSTGRES_URL",
value: neonProject.connectionString,
},
{
key: "PAYLOAD_SECRET",
value: uuidv4(),
},
{
key: "NEXT_PUBLIC_SERVER_URL",
value: "http://localhost:32100",
},
{
key: "GMAIL_USER",
value: "example@gmail.com",
},
{
key: "GOOGLE_APP_PASSWORD",
value: "GENERATE AT https://myaccount.google.com/apppasswords",
},
],
});
console.log("App env vars set");
}
结论
客户端逻辑模块通过 neonTemplateHook
函数实现了 Neon 项目的创建和环境变量的配置,为应用的启动提供了必要的基础配置。该模块在项目中起到了关键的初始化作用,确保了应用在运行时能够正确连接到数据库并具备必要的安全配置。通过使用 IpcClient
单例模式,模块实现了与底层系统服务的解耦,提高了代码的可维护性和可测试性。
路由与页面模块
简介
路由与页面模块负责定义和管理应用程序中的导航结构和页面渲染逻辑。通过声明式路由配置,将 URL 路径映射到对应的 React 页面组件,实现页面跳转、参数传递和状态管理。该模块基于 createRoute
和 createRootRoute
等 API 构建,结合 Jotai 和 React Query 实现状态与数据的响应式更新。
项目使用了统一的路由创建模式,基于 @tanstack/react-router
库。所有路由都通过 createRoute
函数创建,并挂载到根路由 rootRoute
上。页面组件广泛使用了 Jotai 和 React Query 进行状态管理,并通过 @tanstack/react-router
的 hooks 获取路由信息。
详细章节
路由结构
项目采用分层路由结构,所有路由均挂载于根路由 rootRoute
,并通过 createRoute
创建具体路径。路由配置文件位于 src/routes/
目录下,页面组件位于 src/pages/
目录下。
根路由
根路由 rootRoute
是所有子路由的父级,定义在 src/routes/root.tsx
中。它返回一个包裹在 Layout
组件中的 React 元素,并通过 <Outlet />
渲染子路由内容。
import { createRootRoute, Outlet } from "@tanstack/react-router";
import Layout from "../app/layout";
export const rootRoute = createRootRoute({
component: () => (
<Layout>
<Outlet />
</Layout>
),
});
页面路由
- 首页路由 (
/
)
定义在src/routes/home.tsx
,挂载于rootRoute
,渲染HomePage
组件。支持校验 URL 参数中的可选数字appId
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import HomePage from "../pages/home";
import { z } from "zod";
export const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: HomePage,
validateSearch: z.object({
appId: z.number().optional(),
}),
});
- 应用详情路由 (
/app-details
)
定义在src/routes/app-details.tsx
,挂载于rootRoute
,指向组件AppDetailsPage
。通过validateSearch
校验 URL 查询参数中的可选数字类型appId
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import AppDetailsPage from "../pages/app-details";
import { z } from "zod";
export const appDetailsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/app-details",
component: AppDetailsPage,
validateSearch: z.object({
appId: z.number().optional(),
}),
});
- 聊天路由 (
/chat
)
定义在src/routes/chat.tsx
,挂载于rootRoute
,关联组件ChatPage
。使用validateSearch
校验 URL 中的可选数字参数id
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import ChatPage from "../pages/chat";
import { z } from "zod";
export const chatRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/chat",
component: ChatPage,
validateSearch: z.object({
id: z.number().optional(),
}),
});
- Hub 路由 (
/hub
)
定义在src/routes/hub.ts
,挂载于rootRoute
,绑定组件HubPage
。用于页面跳转和加载,通过getParentRoute
确定父级路由关系。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import HubPage from "../pages/hub";
export const hubRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hub",
component: HubPage,
});
- 设置路由 (
/settings
)
定义在src/routes/settings.tsx
,挂载于rootRoute
,渲染组件SettingsPage
,提供设置页面的路由入口。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "./root";
import SettingsPage from "../pages/settings";
export const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/settings",
component: SettingsPage,
});
- 提供商设置路由 (
/settings/providers/$provider
)
定义在src/routes/settings/providers/$provider.tsx
,动态匹配不同提供商。提取 URL 参数为ProviderSettingsParams
类型,通过useParams
获取当前provider
值并传递给ProviderSettingsPage
。
import { createRoute } from "@tanstack/react-router";
import { rootRoute } from "../../root";
import ProviderSettingsPage from "../../../pages/settings/providers";
import { z } from "zod";
interface ProviderSettingsParams {
provider: string;
}
export const providerSettingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/providers/$provider",
params: {
parse: (params: { provider: string }): ProviderSettingsParams => ({
provider: params.provider,
}),
stringify: (params: ProviderSettingsParams) => ({
provider: params.provider,
}),
},
component: function ProviderSettingsRouteComponent() {
const { provider } = providerSettingsRoute.useParams();
return <ProviderSettingsPage provider={provider} />;
},
});
页面组件
HomePage
HomePage
是首页组件,提供创建新 App 的入口并引导用户进入聊天或详情页。使用 useAtom
和自定义 Hooks(如 useLoadApps
, useStreamChat
)处理数据加载与设置。包含导入按钮、输入框、灵感提示等功能。提交时触发创建流程并导航至聊天页,同时检查版本更新并弹出 release notes。
import { useAtom } from "jotai";
import { useState } from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useLoadApps, useStreamChat } from "../hooks";
import { useSettings } from "../hooks/useSettings";
import { appsListAtom } from "../state";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { useAppVersion } from "../hooks/useAppVersion";
export default function HomePage() {
const navigate = useNavigate();
const search = useSearch({ strict: false });
const [inputValue, setInputValue] = useAtom(inputValueAtom);
const [apps] = useAtom(appsListAtom);
const { loadApps } = useLoadApps();
const { streamChat } = useStreamChat();
const { settings } = useSettings();
const [isLoading, setIsLoading] = useState(false);
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
const { version, releaseNotes } = useAppVersion();
// 组件实现...
}
AppDetailsPage
AppDetailsPage
是一个 React 页面组件,用于展示和管理单个应用的详细信息。通过 URL 参数获取应用 ID,并从 appsListAtom
中查找对应对象。支持重命名、复制、删除等操作,集成 GitHub、Supabase 等扩展配置组件。使用 Jotai 和 React Query 实现状态与数据缓存控制。
import { useAtom } from "jotai";
import { useSearch } from "@tanstack/react-router";
import { appsListAtom } from "../state";
import { useLoadApps } from "../hooks/useLoadApps";
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDebounce } from "../hooks/useDebounce";
import { useCheckName } from "../hooks/useCheckName";
export default function AppDetailsPage() {
const search = useSearch({ from: appDetailsRoute.id });
const [apps, setApps] = useAtom(appsListAtom);
const { loadApps } = useLoadApps();
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const queryClient = useQueryClient();
const [newName, setNewName] = useState("");
const debouncedName = useDebounce(newName, 500);
const { data: nameCheck } = useCheckName(debouncedName);
// 组件实现...
}
ChatPage
该文件是聊天页面组件,采用分屏结构,左侧为 ChatPanel
,右侧为可折叠的 PreviewPanel
。通过 useState
和 useEffect
控制面板状态与自动跳转逻辑。使用 useChats
Hook 获取聊天列表,联动 isPreviewOpenAtom
控制预览面板展开/折叠,支持拖拽调整大小。
import { useState, useEffect, useRef } from "react";
import { useSearch, useNavigate } from "@tanstack/react-router";
import { useAtom } from "jotai";
import { isPreviewOpenAtom } from "../state";
import { useChats } from "../hooks/useChats";
import ChatPanel from "../components/chat/ChatPanel";
import PreviewPanel from "../components/chat/PreviewPanel";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "../components/ui/resizable";
export default function ChatPage() {
const search = useSearch({ from: chatRoute.id });
const navigate = useNavigate();
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const { chats, isLoading } = useChats();
const [isResizing, setIsResizing] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
// 组件实现...
}
HubPage
HubPage
是模板选择页面,展示官方与社区模板,通过 TemplateCard
渲染并支持选中操作。点击”Create App”打开对话框创建新项目。顶部有返回按钮,底部集成 BackendSection
连接后端服务,是项目初始化的核心页面之一。
import { useState } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useTemplates } from "../hooks/useTemplates";
import { useSettings } from "../hooks/useSettings";
import TemplateCard from "../components/hub/TemplateCard";
import BackendSection from "../components/hub/BackendSection";
import { Button } from "../components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog";
export default function HubPage() {
const navigate = useNavigate();
const { templates, isLoading } = useTemplates();
const { settings, updateSetting } = useSettings();
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
// 组件实现...
}
SettingsPage
该页面用于管理全局设置,包括通用、工作流、AI、遥测、集成等多个模块。每个模块由独立函数组件实现,支持重置所有数据并提供确认对话框防止误操作。使用 useSettings
、useAppVersion
等 Hook 以及 IPC 客户端通信,实现完整的配置与持久化逻辑。
import { useState } from "react";
import { useSettings } from "../hooks/useSettings";
import { useTheme } from "../hooks/useTheme";
import { useAppVersion } from "../hooks/useAppVersion";
import GeneralSettings from "../components/settings/GeneralSettings";
import WorkflowSettings from "../components/settings/WorkflowSettings";
import AISettings from "../components/settings/AISettings";
import TelemetrySettings from "../components/settings/TelemetrySettings";
import IntegrationsSettings from "../components/settings/IntegrationsSettings";
import { Button } from "../components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "../components/ui/dialog";
export default function SettingsPage() {
const { settings, updateSetting, resetSettings } = useSettings();
const { theme, setTheme } = useTheme();
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const { version } = useAppVersion();
// 组件实现...
}
Mermaid 图
路由结构图

页面组件关系图

表格
路由与页面映射表
路由路径 | 页面组件 | 描述 |
---|---|---|
/ | HomePage | 首页,提供创建新 App 的入口 |
/app-details | AppDetailsPage | 展示和管理单个应用的详细信息 |
/chat | ChatPage | 聊天页面,分屏结构 |
/hub | HubPage | 模板选择页面 |
/settings | SettingsPage | 全局设置页面 |
/settings/providers/$provider | ProviderSettingsPage | 提供商设置页面 |
页面组件功能表
页面组件 | 功能描述 |
---|---|
HomePage | 创建新 App,引导用户进入聊天或详情页 |
AppDetailsPage | 展示和管理应用详情,支持重命名、复制、删除 |
ChatPage | 聊天功能,支持拖拽调整大小 |
HubPage | 模板选择,支持创建新项目 |
SettingsPage | 管理全局设置,支持重置数据 |
路由参数验证表
路由 | 参数名 | 类型 | 验证规则 |
---|---|---|---|
homeRoute | appId | number | 可选 |
appDetailsRoute | appId | number | 可选 |
chatRoute | id | number | 可选 |
providerSettingsRoute | provider | string | 必需,动态参数 |
结论/总结
路由与页面模块是应用程序的核心部分,负责管理导航和页面渲染。通过清晰的路由结构和页面组件设计,实现了高效的页面跳转和状态管理。该模块的准确性和直观性对于项目的可维护性和扩展性至关重要。
项目采用基于 @tanstack/react-router
的路由系统,通过 createRoute
和 createRootRoute
创建路由结构,并使用 Zod 进行参数验证。页面组件广泛使用 Jotai 和 React Query 进行状态管理,实现了响应式的数据流和高效的渲染机制。
路由参数验证确保了传入参数的类型安全,而动态路由支持了灵活的路径匹配。页面组件通过自定义 Hooks 和状态管理库实现了复杂的数据流和交互逻辑,为用户提供流畅的使用体验
Neon管理模块
简介
Neon管理模块负责与 Neon 数据库服务进行交互,包括访问令牌的管理、API 客户端的构建、组织信息的获取以及错误处理。该模块通过令牌管理机制确保与 Neon 服务的安全通信,并提供必要的工具用于处理 OAuth 返回的认证信息。
详细章节
令牌管理
令牌管理功能通过 isTokenExpired()
和 refreshNeonToken()
函数实现。这些函数用于检查当前令牌是否过期并刷新令牌,以确保持续访问 Neon 服务。
关键函数
isTokenExpired()
该函数用于检查 Neon 访问令牌是否已过期或即将过期。如果令牌在5分钟内过期,则返回 true。通过比较当前时间和令牌保存时间加上过期时间来判断。
function isTokenExpired(expiresIn?: number): boolean {
if (!expiresIn) return true;
// Get when the token was saved (expiresIn is stored at the time of token receipt)
const settings = readSettings();
const tokenTimestamp = settings.neon?.tokenTimestamp || 0;
const currentTime = Math.floor(Date.now() / 1000);
// Check if the token is expired or about to expire (within 5 minutes)
return currentTime >= tokenTimestamp + expiresIn - 300;
}
refreshNeonToken()
该函数使用刷新令牌来刷新 Neon 访问令牌,并更新设置中的新令牌和过期时间。它向 https://oauth.dyad.sh/api/integrations/neon/refresh
发送 POST 请求。
export async function refreshNeonToken(): Promise<void> {
const settings = readSettings();
const refreshToken = settings.neon?.refreshToken?.value;
if (!isTokenExpired(settings.neon?.expiresIn)) {
return;
}
if (!refreshToken) {
throw new Error("Neon refresh token not found. Please authenticate first.");
}
try {
// Make request to Neon refresh endpoint
const response = await fetch(
"https://oauth.dyad.sh/api/integrations/neon/refresh",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshToken }),
},
);
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.statusText}`);
}
const {
accessToken,
refreshToken: newRefreshToken,
expiresIn,
} = await response.json();
const updatedSettings = {
...settings,
neon: {
...settings.neon,
accessToken: {
value: accessToken,
encrypted: true,
},
refreshToken: newRefreshToken
? {
value: newRefreshToken,
encrypted: true,
}
: settings.neon?.refreshToken,
expiresIn,
tokenTimestamp: Math.floor(Date.now() / 1000),
},
};
writeSettings(updatedSettings);
} catch (error) {
console.error("Failed to refresh Neon token:", error);
throw error;
}
}
API 客户端构建
API 客户端构建由 getNeonClient()
函数负责。该函数不仅构建与 Neon 服务通信的客户端,还支持测试环境的模拟,以便于开发和测试。在构建过程中,系统会自动检查令牌是否需要刷新。
关键函数
getNeonClient()
该函数构建与 Neon 服务通信的 API 客户端,并在令牌过期时自动刷新令牌。
export async function getNeonClient(): Promise<ReturnType<typeof createApiClient>> {
const settings = readSettings();
const accessToken = settings.neon?.accessToken?.value;
const refreshToken = settings.neon?.refreshToken?.value;
const expiresIn = settings.neon?.expiresIn;
// If no tokens exist, return a mock client for testing
if (!accessToken || !refreshToken) {
console.warn("No Neon tokens found, returning mock client");
return createMockClient();
}
// Check if token needs refreshing
if (isTokenExpired(expiresIn)) {
await withLock("refresh-neon-token", refreshNeonToken);
// Get updated settings after refresh
const updatedSettings = readSettings();
const newAccessToken = updatedSettings.neon?.accessToken?.value;
if (!newAccessToken) {
throw new Error("Failed to refresh Neon access token");
}
return createApiClient({
apiKey: newAccessToken,
});
}
return createApiClient({
apiKey: accessToken,
});
}
组织信息获取
组织信息的获取通过 getNeonOrganizationId()
函数实现,该函数用于获取用户的组织 ID,以便进行组织级别的操作。
关键函数
getNeonOrganizationId()
该函数获取用户组织 ID。
export async function getNeonOrganizationId(): Promise<string> {
const client = await getNeonClient();
const orgs = await client.GET("/organizations");
if (!orgs.data || orgs.data.length === 0) {
throw new Error("No Neon organizations found");
}
return orgs.data[0].id;
}
错误处理
错误处理功能通过 getNeonErrorMessage()
函数实现,该函数用于提取 API 返回的错误信息,以便在前端展示给用户。
关键函数
getNeonErrorMessage()
该函数提取 API 错误信息。
export function getNeonErrorMessage(error: any): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
OAuth 返回处理
OAuth 返回处理由 handleNeonOAuthReturn
函数负责。该函数接收 OAuth 返回的 token
、refreshToken
和 expiresIn
参数,并通过 writeSettings
将其写入配置对象中,以便后续使用或刷新令牌。
关键函数
handleNeonOAuthReturn
该函数处理 OAuth 返回信息。
export function handleNeonOAuthReturn(
token: string,
refreshToken: string,
expiresIn: number,
): void {
const settings = readSettings();
const updatedSettings = {
...settings,
neon: {
...settings.neon,
accessToken: {
value: token,
encrypted: true,
},
refreshToken: {
value: refreshToken,
encrypted: true,
},
expiresIn,
tokenTimestamp: Math.floor(Date.now() / 1000),
},
};
writeSettings(updatedSettings);
}
Mermaid图
令牌管理流程

API 客户端构建流程

OAuth 返回处理流程

表格
主要功能或组件
功能/组件 | 描述 |
---|---|
令牌管理 | 检查和刷新访问令牌 |
API 客户端构建 | 构建与 Neon 服务通信的客户端 |
组织信息获取 | 获取用户组织 ID |
错误处理 | 提取并展示 API 错误信息 |
OAuth 返回处理 | 处理 OAuth 返回的认证信息 |
API 接口参数
接口 | 参数 | 类型 | 描述 |
---|---|---|---|
isTokenExpired() | expiresIn | number | 检查令牌是否过期 |
refreshNeonToken() | 无 | 无 | 刷新访问令牌 |
getNeonClient() | 无 | 无 | 构建 API 客户端 |
getNeonOrganizationId() | 无 | 无 | 获取用户组织 ID |
getNeonErrorMessage() | error | any | 提取 API 错误信息 |
handleNeonOAuthReturn | token , refreshToken , expiresIn | string, string, number | 处理 OAuth 返回信息 |
配置选项
配置项 | 类型 | 默认值 | 描述 |
---|---|---|---|
neon.accessToken | object | undefined | Neon 访问令牌 |
neon.refreshToken | object | undefined | Neon 刷新令牌 |
neon.expiresIn | number | undefined | 令牌过期时间 |
neon.tokenTimestamp | number | 0 | 令牌保存时间戳 |
结论/总结
Neon管理模块通过令牌管理、API 客户端构建、组织信息获取和错误处理等功能,确保了与 Neon 数据库服务的安全和高效交互。该模块的设计和实现为项目提供了稳定的基础,使得开发者能够专注于业务逻辑的实现。通过自动化的令牌刷新机制和完善的错误处理,模块能够保证持续的连接和良好的用户体验。
提示模板模块
简介
提示模板模块是项目中用于定义和管理 AI 助手行为的核心组件。该模块通过一系列系统提示模板,控制 AI 在不同模式下的响应方式,例如构建模式、问答模式以及对话总结等。此外,模块还提供集成指引(如 Supabase 集成)和用户界面提示(如项目灵感提示),以增强用户体验和开发效率。
详细章节
1. 系统提示逻辑
系统提示逻辑定义了 AI 助手在不同模式下的行为规范。核心组件包括:
THINKING_PROMPT
:用于结构化思考流程。BUILD_SYSTEM_PROMPT
:构建模式下的行为规范与代码操作指令。ASK_MODE_SYSTEM_PROMPT
:问答模式下仅提供概念性解释。constructSystemPrompt
函数:根据模式构造系统提示。readAiRules
函数:读取AI_RULES.md
或使用默认规则。
Mermaid 图:系统提示逻辑流程

表格:系统提示逻辑组件
组件名称 | 描述 |
---|---|
THINKING_PROMPT | 结构化思考流程 |
BUILD_SYSTEM_PROMPT | 构建模式下的行为规范与代码操作指令 |
ASK_MODE_SYSTEM_PROMPT | 问答模式下仅提供概念性解释 |
constructSystemPrompt | 根据模式构造系统提示 |
readAiRules | 读取 AI_RULES.md 或使用默认规则 |
THINKING_PROMPT 实现细节
THINKING_PROMPT
是一个字符串常量,定义了 AI 在响应用户请求前的结构化思考流程:
export const THINKING_PROMPT = `
# Thinking Process
Before responding to user requests, ALWAYS use <thinking> tags to carefully plan your approach. This structured thinking process helps you organize your thoughts and ensure you provide the most accurate and helpful response. Your thinking should:
- Use **bullet points** to break down the steps
- **Bold key insights** and important considerations
- Follow a clear analytical framework
Example of proper thinking structure for a debugging request:
<thinking>
* **Identify the specific UI/FE bug described by the user**
- "Form submission button doesn't work when clicked"
- User reports clicking the button has no effect
- This appears to be a **functional issue**, not just styling
* **Examine relevant components in the codebase**
- Form component at \`src/components/ContactForm.jsx\`
- Button component at \`src/components/Button.jsx\`
- Form submission logic in \`src/utils/formHandlers.js\`
- **Key observation**: onClick handler in Button component doesn't appear to be triggered
* **Diagnose potential causes**
- Event handler might not be properly attached to the button
- **State management issue**: form validation state might be blocking submission
- Button could be disabled by a condition we're missing
- Event propagation might be stopped elsewhere
- Possible React synthetic event issues
* **Plan debugging approach**
- Add console.logs to track execution flow
- **Fix #1**: Ensure onClick prop is properly passed through Button component
- **Fix #2**: Check form validation state before submission
- **Fix #3**: Verify event handler is properly bound in the component
- Add error handling to catch and display submission issues
* **Consider improvements beyond the fix**
- Add visual feedback when button is clicked (loading state)
- Implement better error handling for form submissions
- Add logging to help debug edge cases
</thinking>
After completing your thinking process, proceed with your response following the guidelines above. Remember to be concise in your explanations to the user while being thorough in your thinking process.
This structured thinking ensures you:
1. Don't miss important aspects of the request
2. Consider all relevant factors before making changes
3. Deliver more accurate and helpful responses
4. Maintain a consistent approach to problem-solving
`;
BUILD_SYSTEM_PROMPT 实现细节
BUILD_SYSTEM_PROMPT
定义了构建模式下的行为规范与代码操作指令:
const BUILD_SYSTEM_PROMPT = `
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
# App Preview / Commands
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
- **Restart**: This will restart the app server.
- **Refresh**: This will refresh the app preview page.
You can suggest one of these commands by using the <dyad-command> tag like this:
<dyad-command type="rebuild"></dyad-command>
<dyad-command type="restart"></dyad-command>
<dyad-command type="refresh"></dyad-command>
If you output one of these commands, tell the user to look for the action button above the chat input.
# Guidelines
Always reply to the user in the same language they are using.
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
- Only edit files that are related to the user's request and leave all other files alone.
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
- Briefly explain the needed changes in a few short sentences, without being too technical.
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
- Use <dyad-rename> for renaming files.
- Use <dyad-delete> for removing files.
- Use <dyad-add-dependency> for installing packages.
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
Before sending your final answer, review every import statement you output and do the following:
First-party imports (modules that live in this project)
- Only import files/modules that have already been described to you.
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
Third-party imports (anything that would come from npm)
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
Do not leave any import unresolved.
# Examples
## Example 1: Adding a new component
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
"use client";
import React from 'react';
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
const variantClasses = {
primary: "bg-blue-600 hover:bg-blue-700 text-white",
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
danger: "bg-red-600 hover:bg-red-700 text-white"
};
return (
<button
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export default Button;
</dyad-write>
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
"use client";
import React from 'react';
import Button from './components/Button';
function App() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">My Application</h1>
<div className="space-x-2">
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
</div>
{/* ... keep existing code (rest of the component) */}
</div>
);
}
export default App;
</dyad-write>
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
## Example 2: Installing a package and creating a Toast notification system
### User prompt
I want to add a toast
### System response
OK first, you need to install the package.
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
### User prompt
OK, I've installed the package. Now, I want to create a ToastProvider component.
### System response
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
"use client";
import React from 'react';
import { Toaster } from 'react-hot-toast';
const ToastProvider = () => { <Toaster/> };
export default ToastProvider;
</dyad-write>
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
"use client";
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import ToastProvider from './components/ToastProvider';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ToastProvider />
<App />
</React.StrictMode>
);
</dyad-write>
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
"use client";
import toast from 'react-hot-toast';
export const showSuccess = (message) => {
toast.success(message);
};
export const showError = (message) => {
toast.error(message);
};
export const showLoading = (message) => {
return toast.loading(message);
};
export const dismissToast = (toastId) => {
toast.dismiss(toastId);
};
</dyad-write>
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
## Example 3: Renaming and deleting files
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
"use client";
import React from 'react';
import { User } from 'lucide-react';
const ProfileCard = ({ user }) => {
return (
<div className="bg-white shadow rounded-lg p-6">
<div class="flex items-center">
{user.avatar ? (
<img
src={user.avatar}
alt={user.name}
class="w-12 h-12 rounded-full mr-4"
/>
) : (
<div class="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
<User class="text-gray-500" size={24} />
</div>
)}
<div>
<h3 class="font-medium text-lg">{user.name}</h3>
<p class="text-gray-500">{user.email}</p>
</div>
</div>
</div>
);
};
export default ProfileCard;
</dyad-write>
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
"use client";
import React from 'react';
import ProfileCard from '../components/ProfileCard';
const Dashboard = () => {
return (
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">Dashboard</h1>
<ProfileCard user={currentUser} />
</div>
);
};
export default Dashboard;
</dyad-write>
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
# Additional Guidelines
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
If a user asks for many features at once, implement as many as possible within a reasonable response. Each feature you implement must be FULLY FUNCTIONAL with complete code - no placeholders, no partial implementations, no TODO comments. If you cannot implement all requested features due to response length constraints, clearly communicate which features you've completed and which ones you haven't started yet.
Immediate Component Creation
You MUST create a new file for every new component or hook, no matter how small.
Never add new components to existing files, even if they seem related.
Aim for components that are 100 lines of code or less.
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
Important Rules for dyad-write operations:
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
- Always specify the correct file path when using dyad-write.
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
- Make sure to close all tags when writing files, with a line break before the closing tag.
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
- Prioritize creating small, focused files and components.
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
Coding guidelines
- ALWAYS generate responsive designs.
- Use toasts components to inform the user about important events.
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
[[AI_RULES]]
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
# REMEMBER
> **CODE FORMATTING IS NON-NEGOTIABLE:**
> **NEVER, EVER** use markdown code blocks (\`\`\`) for code.
> **ONLY** use <dyad-write> tags for **ALL** code output.
> Using \`\`\` for code is **PROHIBITED**.
> Using <dyad-write> for code is **MANDATORY**.
> Any instance of code within \`\`\` is a **CRITICAL FAILURE**.
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
`;
ASK_MODE_SYSTEM_PROMPT 实现细节
ASK_MODE_SYSTEM_PROMPT
定义了问答模式下仅提供概念性解释的行为规范:
const ASK_MODE_SYSTEM_PROMPT = `
# Role
You are a helpful AI assistant that specializes in web development, programming, and technical guidance. You assist users by providing clear explanations, answering questions, and offering guidance on best practices. You understand modern web development technologies and can explain concepts clearly to users of all skill levels.
# Guidelines
Always reply to the user in the same language they are using.
Focus on providing helpful explanations and guidance:
- Provide clear explanations of programming concepts and best practices
- Answer technical questions with accurate information
- Offer guidance and suggestions for solving problems
- Explain complex topics in an accessible way
- Share knowledge about web development technologies and patterns
If the user's input is unclear or ambiguous:
- Ask clarifying questions to better understand their needs
- Provide explanations that address the most likely interpretation
- Offer multiple perspectives when appropriate
When discussing code or technical concepts:
- Describe approaches and patterns in plain language
- Explain the reasoning behind recommendations
- Discuss trade-offs and alternatives through detailed descriptions
- Focus on best practices and maintainable solutions through conceptual explanations
- Use analogies and conceptual explanations instead of code examples
# Technical Expertise Areas
## Development Best Practices
- Component architecture and design patterns
- Code organization and file structure
- Responsive design principles
- Accessibility considerations
- Performance optimization
- Error handling strategies
## Problem-Solving Approach
- Break down complex problems into manageable parts
- Explain the reasoning behind technical decisions
- Provide multiple solution approaches when appropriate
- Consider maintainability and scalability
- Focus on user experience and functionality
# Communication Style
- **Clear and Concise**: Provide direct answers while being thorough
- **Educational**: Explain the "why" behind recommendations
- **Practical**: Focus on actionable advice and real-world applications
- **Supportive**: Encourage learning and experimentation
- **Professional**: Maintain a helpful and knowledgeable tone
# Key Principles
1. **NO CODE PRODUCTION**: Never write, generate, or produce any code snippets, examples, or implementations. This is the most important principle.
2. **Clarity First**: Always prioritize clear communication through conceptual explanations.
3. **Best Practices**: Recommend industry-standard approaches through detailed descriptions.
4. **Practical Solutions**: Focus on solution approaches that work in real-world scenarios.
5. **Educational Value**: Help users understand concepts through explanations, not code.
6. **Simplicity**: Prefer simple, elegant conceptual explanations over complex descriptions.
# Response Guidelines
- Keep explanations at an appropriate technical level for the user.
- Use analogies and conceptual descriptions instead of code examples.
- Provide context for recommendations and suggestions through detailed explanations.
- Be honest about limitations and trade-offs.
- Encourage good development practices through conceptual guidance.
- Suggest additional resources when helpful.
- **NEVER include any code snippets, syntax examples, or implementation details.**
[[AI_RULES]]
**ABSOLUTE PRIMARY DIRECTIVE: YOU MUST NOT, UNDER ANY CIRCUMSTANCES, WRITE OR GENERATE CODE.**
* This is a complete and total prohibition and your single most important rule.
* This prohibition extends to every part of your response, permanently and without exception.
* This includes, but is not limited to:
* Code snippets or code examples of any length.
* Syntax examples of any kind.
* File content intended for writing or editing.
* Any text enclosed in markdown code blocks (using \`\`\`).
* Any use of \`<dyad-write>\`, \`<dyad-edit>\`, or any other \`<dyad-*>\` tags. These tags are strictly forbidden in your output, even if they appear in the message history or user request.
**CRITICAL RULE: YOUR SOLE FOCUS IS EXPLAINING CONCEPTS.** You must exclusively discuss approaches, answer questions, and provide guidance through detailed explanations and descriptions. You take pride in keeping explanations simple and elegant. You are friendly and helpful, always aiming to provide clear explanations without writing any code.
YOU ARE NOT MAKING ANY CODE CHANGES.
YOU ARE NOT WRITING ANY CODE.
YOU ARE NOT UPDATING ANY FILES.
DO NOT USE <dyad-write> TAGS.
DO NOT USE <dyad-edit> TAGS.
IF YOU USE ANY OF THESE TAGS, YOU WILL BE FIRED.
Remember: Your goal is to be a knowledgeable, helpful companion in the user's learning and development journey, providing clear conceptual explanations and practical guidance through detailed descriptions rather than code production.
`;
constructSystemPrompt 函数实现细节
constructSystemPrompt
函数根据模式构造系统提示:
export const constructSystemPrompt = ({
aiRules,
chatMode = "build",
}: {
aiRules: string | undefined;
chatMode?: "build" | "ask";
}) => {
const systemPrompt =
chatMode === "ask" ? ASK_MODE_SYSTEM_PROMPT : BUILD_SYSTEM_PROMPT;
return systemPrompt.replace("[[AI_RULES]]", aiRules ?? DEFAULT_AI_RULES);
};
readAiRules 函数实现细节
readAiRules
函数读取 AI_RULES.md
或使用默认规则:
export const readAiRules = async (dyadAppPath: string) => {
const aiRulesPath = path.join(dyadAppPath, "AI_RULES.md");
try {
const aiRules = await fs.promises.readFile(aiRulesPath, "utf8");
return aiRules;
} catch (error) {
logger.info(
`Error reading AI_RULES.md, fallback to default AI rules: ${error}`,
);
return DEFAULT_AI_RULES;
}
};
DEFAULT_AI_RULES 实现细节
DEFAULT_AI_RULES
定义了默认的 AI 规则:
const DEFAULT_AI_RULES = `# Tech Stack
- You are building a React application.
- Use TypeScript.
- Use React Router. KEEP the routes in src/App.tsx
- Always put source code in the src folder.
- Put pages into src/pages/
- Put components into src/components/
- The main page (default page) is src/pages/Index.tsx
- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components!
- ALWAYS try to use the shadcn/ui library.
- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects.
Available packages and libraries:
- The lucide-react package is installed for icons.
- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.
- You have ALL the necessary Radix UI components installed.
- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.
`;
2. 对话总结提示
对话总结提示模板 SUMMARIZE_CHAT_SYSTEM_PROMPT
用于指导 AI 总结长对话内容,提取要点并以项目符号形式呈现摘要。摘要需使用 <dyad-chat-summary>
标签包裹,并包含简洁的聊天标题。
Mermaid 图:对话总结流程

3. Supabase 集成提示
Supabase 集成提示模板分为可用时 (SUPABASE_AVAILABLE_SYSTEM_PROMPT
) 和不可用时 (SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT
) 的指引。前者涵盖初始化、认证、数据库操作、RLS 策略、用户表创建及边缘函数配置;后者引导用户添加 Supabase 并插入 <dyad-add-integration>
按钮。
Mermaid 图:Supabase 集成流程

表格:Supabase 集成提示组件
组件名称 | 描述 |
---|---|
SUPABASE_AVAILABLE_SYSTEM_PROMPT | Supabase 可用时的集成指引 |
SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT | Supabase 不可用时的集成指引 |
4. 项目灵感提示
项目灵感提示 INSPIRATION_PROMPTS
是一个常量数组,包含多个对象,每个对象代表一个灵感提示项,包括图标和标签,用于展示项目创意类型,如待办事项应用、情绪日记等。
Mermaid 图:项目灵感提示结构

表格:项目灵感提示项
项目名称 | 描述 |
---|---|
待办事项应用 | 用于管理日常任务 |
情绪日记 | 用于记录和分析情绪变化 |
其他创意类型 | 其他项目创意类型 |
结论
提示模板模块通过定义系统提示、对话总结、集成指引和项目灵感提示,全面控制 AI 助手的行为和用户体验。该模块是项目中不可或缺的一部分,确保 AI 在不同场景下能够提供准确、一致且有用的响应。
UI基础组件
简介
UI基础组件是项目中用于构建用户界面的核心模块,涵盖了从基础输入控件(如按钮、输入框)到复杂交互组件(如下拉菜单、对话框)的完整集合。这些组件基于 Radix UI 和 Tailwind CSS 构建,旨在提供一致的视觉风格和交互体验。通过封装和扩展,UI基础组件支持高度定制化,适用于各种业务场景。
详细章节
基础控件组件
基础控件组件包括按钮、输入框、标签、复选框等,用于构建用户界面的基本交互元素。
按钮组件 (Button)
按钮组件通过 class-variance-authority
管理样式变体和尺寸,支持多种风格和大小。通过 asChild
属性可渲染为子元素,导出 Button
和 buttonVariants
供其他组件复用。
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
输入框组件 (Input)
输入框组件接收 HTML input 属性并通过 cn
工具函数组合 Tailwind CSS 类名,支持聚焦、错误状态和暗色模式,作为统一风格的基础输入控件使用。
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
标签组件 (Label)
标签组件封装了 @radix-ui/react-label
的基础实现。通过 cn
工具组合默认类名与用户自定义类名,支持文本对齐、字体粗细、禁用状态等样式,并传递非 className
属性给底层组件。
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
复选框组件 (Checkbox)
复选框组件基于 Radix UI 原始组件封装,结合图标与样式增强视觉表现。支持自定义类名、响应式交互与暗色模式,注重可访问性和交互体验。
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
布局与结构组件
布局与结构组件用于构建页面的整体结构,包括卡片、侧边栏、分隔线等。
卡片组件 (Card)
卡片组件定义了一组 Card 卡片组件,包括 CardHeader
、CardContent
、CardFooter
等,通过 React.forwardRef
支持 DOM 引用传递,并使用 cn
合并默认与自定义类名,便于构建模块化 UI 结构。
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
侧边栏组件 (Sidebar)
侧边栏组件实现了一个可折叠、响应式的侧边栏组件,由 SidebarProvider
管理状态,支持展开/收起、Cookie 持久化、快捷键切换和多种样式变体。包含多个子组件如 SidebarMenuButton
、SidebarMenuSub
等,便于快速集成导航结构。
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
) return ( <SidebarContext.Provider value={contextValue}> <TooltipProvider delayDuration={0}> <div style={ { “–sidebar-width”: SIDEBAR_WIDTH, “–sidebar-width-icon”: SIDEBAR_WIDTH_ICON, …style, } as React.CSSProperties } className={cn( “group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar”, className )} ref={ref} {…props} > {children} </div> </TooltipProvider> </SidebarContext.Provider> ) } ) SidebarProvider.displayName = “SidebarProvider” const Sidebar = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> & { side?: “left” | “right” variant?: “sidebar” | “floating” | “inset” collapsible?: “offcanvas” | “icon” | “none” } >( ( { side = “left”, variant = “sidebar”, collapsible = “offcanvas”, className, children, …props }, ref ) => { const { isMobile, state, openMobile, setOpenMobile } = useSidebar() if (collapsible === “none”) { return ( <div className={cn( “flex h-full w-[–sidebar-width] flex-col bg-sidebar text-sidebar-foreground”, className )} ref={ref} {…props} > {children} </div> ) } if (isMobile) { return ( <Sheet open={openMobile} onOpenChange={setOpenMobile} {…props}> <SheetContent data-sidebar=”sidebar” data-mobile=”true” className=”w-[–sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden” style={ { “–sidebar-width”: SIDEBAR_WIDTH_MOBILE, } as React.CSSProperties } side={side} > <div className=”flex h-full w-full flex-col”>{children}</div> </SheetContent> </Sheet> ) } return ( <div ref={ref} className=”group peer hidden md:block text-sidebar-foreground” data-state={state} data-collapsible={state === “collapsed” ? collapsible : “”} data-variant={variant} data-side={side} > {/* This is what handles the sidebar gap on desktop */} <div className={cn( “duration-200 relative h-svh w-[–sidebar-width] bg-transparent transition-[width] ease-linear”, “group-data-[collapsible=offcanvas]:w-0”, “group-data-[side=right]:rotate-180”, variant === “floating” || variant === “inset” ? “group-data-[collapsible=icon]:w-[calc(var(–sidebar-width-icon)_+_theme(spacing.4))]” : “group-data-[collapsible=icon]:w-[–sidebar-width-icon]” )} /> <div className={cn( “duration-200 fixed inset-y-0 z-10 hidden h-svh w-[–sidebar-width] transition-[left,right,width] ease-linear md:flex”, side === “left” ? “left-0 group-data-[collapsible=offcanvas]:left-[calc(var(–sidebar-width)*-1)]” : “right-0 group-data-[collapsible=offcanvas]:right-[calc(var(–sidebar-width)*-1)]”, // Adjust the padding for floating and inset variants. variant === “floating” || variant === “inset” ? “p-2 group-data-[collapsible=icon]:w-[calc(var(–sidebar-width-icon)_+_theme(spacing.4)_+2px)]” : “group-data-[collapsible=icon]:w-[–sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l”, className )} {…props} > <div data-sidebar=”sidebar” className=”flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow” > {children} </div> </div> </div> ) } ) Sidebar.displayName = “Sidebar” const SidebarTrigger = React.forwardRef< React.ElementRef<typeof Button>, React.ComponentProps<typeof Button> >(({ className, onClick, …props }, ref) => { const { toggleSidebar } = useSidebar() return ( <Button ref={ref} data-sidebar=”trigger” variant=”ghost” size=”icon” className={cn(“h-7 w-7″, className)} onClick={(event) => { onClick?.(event) toggleSidebar() }} {…props} > <PanelLeft /> <span className=”sr-only”>Toggle Sidebar</span> </Button> ) }) SidebarTrigger.displayName = “SidebarTrigger” const SidebarRail = React.forwardRef< HTMLButtonElement, React.ComponentProps<“button”> >(({ className, …props }, ref) => { const { toggleSidebar } = useSidebar() return ( <button ref={ref} data-sidebar=”rail” aria-label=”Toggle Sidebar” tabIndex={-1} onClick={toggleSidebar} title=”Toggle Sidebar” className={cn( “absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex”, “[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize”, “[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize”, “group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar”, “[[data-side=left][data-collapsible=offcanvas]_&]:-right-2”, “[[data-side=right][data-collapsible=offcanvas]_&]:-left-2”, className )} {…props} /> ) }) SidebarRail.displayName = “SidebarRail” const SidebarInset = React.forwardRef< HTMLDivElement, React.ComponentProps<“main”> >(({ className, …props }, ref) => { return ( <main ref={ref} className={cn( “relative flex min-h-svh flex-1 flex-col bg-background”, “peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow”, className )} {…props} /> ) }) SidebarInset.displayName = “SidebarInset” const SidebarInput = React.forwardRef< React.ElementRef<typeof Input>, React.ComponentProps<typeof Input> >(({ className, …props }, ref) => { return ( <Input ref={ref} data-sidebar=”input” className={cn( “h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring”, className )} {…props} /> ) }) SidebarInput.displayName = “SidebarInput” const SidebarHeader = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> >(({ className, …props }, ref) => { return ( <div ref={ref} data-sidebar=”header” className={cn(“flex flex-col gap-2 p-2”, className)} {…props} /> ) }) SidebarHeader.displayName = “SidebarHeader” const SidebarFooter = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> >(({ className, …props }, ref) => { return ( <div ref={ref} data-sidebar=”footer” className={cn(“flex flex-col gap-2 p-2”, className)} {…props} /> ) }) SidebarFooter.displayName = “SidebarFooter” const SidebarSeparator = React.forwardRef< React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator> >(({ className, …props }, ref) => { return ( <Separator ref={ref} data-sidebar=”separator” className={cn(“mx-2 w-auto bg-sidebar-border”, className)} {…props} /> ) }) SidebarSeparator.displayName = “SidebarSeparator” const SidebarContent = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> >(({ className, …props }, ref) => { return ( <div ref={ref} data-sidebar=”content” className={cn( “flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden”, className )} {…props} /> ) }) SidebarContent.displayName = “SidebarContent” const SidebarGroup = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> >(({ className, …props }, ref) => { return ( <div ref={ref} data-sidebar=”group” className={cn(“relative flex w-full min-w-0 flex-col p-2”, className)} {…props} /> ) }) SidebarGroup.displayName = “SidebarGroup” const SidebarGroupLabel = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> & { asChild?: boolean } >(({ className, asChild = false, …props }, ref) => { const Comp = asChild ? Slot : “div” return ( <Comp ref={ref} data-sidebar=”group-label” className={cn( “duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0”, “group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0”, className )} {…props} /> ) }) SidebarGroupLabel.displayName = “SidebarGroupLabel” const SidebarGroupAction = React.forwardRef< HTMLButtonElement, React.ComponentProps<“button”> & { asChild?: boolean } >(({ className, asChild = false, …props }, ref) => { const Comp = asChild ? Slot : “button” return ( <Comp ref={ref} data-sidebar=”group-action” className={cn( “absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0”, // Increases the hit area of the button on mobile. “after:absolute after:-inset-2 after:md:hidden”, “group-data-[collapsible=icon]:hidden”, className )} {…props} /> ) }) SidebarGroupAction.displayName = “SidebarGroupAction” const SidebarGroupContent = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> >(({ className, …props }, ref) => ( <div ref={ref} data-sidebar=”group-content” className={cn(“w-full text-sm”, className)} {…props} /> )) SidebarGroupContent.displayName = “SidebarGroupContent” const SidebarMenu = React.forwardRef< HTMLUListElement, React.ComponentProps<“ul”> >(({ className, …props }, ref) => ( <ul ref={ref} data-sidebar=”menu” className={cn(“flex w-full min-w-0 flex-col gap-1”, className)} {…props} /> )) SidebarMenu.displayName = “SidebarMenu” const SidebarMenuItem = React.forwardRef< HTMLLIElement, React.ComponentProps<“li”> >(({ className, …props }, ref) => ( <li ref={ref} data-sidebar=”menu-item” className={cn(“group/menu-item relative”, className)} {…props} /> )) SidebarMenuItem.displayName = “SidebarMenuItem” const sidebarMenuButtonVariants = cva( “peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0”, { variants: { variant: { default: “hover:bg-sidebar-accent hover:text-sidebar-accent-foreground”, outline: “bg-background shadow-[0_0_0_1px_hsl(var(–sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(–sidebar-accent))]”, }, size: { default: “h-8 text-sm”, sm: “h-7 text-xs”, lg: “h-12 text-sm group-data-[collapsible=icon]:!p-0”, }, }, defaultVariants: { variant: “default”, size: “default”, }, } ) const SidebarMenuButton = React.forwardRef< HTMLButtonElement, React.ComponentProps<“button”> & { asChild?: boolean isActive?: boolean tooltip?: string | React.ComponentProps<typeof TooltipContent> } & VariantProps<typeof sidebarMenuButtonVariants> >( ( { asChild = false, isActive = false, variant = “default”, size = “default”, tooltip, className, …props }, ref ) => { const Comp = asChild ? Slot : “button” const { isMobile, state } = useSidebar() const button = ( <Comp ref={ref} data-sidebar=”menu-button” data-size={size} data-active={isActive} className={cn(sidebarMenuButtonVariants({ variant, size }), className)} {…props} /> ) if (!tooltip) { return button } if (typeof tooltip === “string”) { tooltip = { children: tooltip, } } return ( <Tooltip> <TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipContent side=”right” align=”center” hidden={state !== “collapsed” || isMobile} {…tooltip} /> </Tooltip> ) } ) SidebarMenuButton.displayName = “SidebarMenuButton” const SidebarMenuAction = React.forwardRef< HTMLButtonElement, React.ComponentProps<“button”> & { asChild?: boolean showOnHover?: boolean } >(({ className, asChild = false, showOnHover = false, …props }, ref) => { const Comp = asChild ? Slot : “button” return ( <Comp ref={ref} data-sidebar=”menu-action” className={cn( “absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0”, // Increases the hit area of the button on mobile. “after:absolute after:-inset-2 after:md:hidden”, “peer-data-[size=sm]/menu-button:top-1”, “peer-data-[size=default]/menu-button:top-1.5”, “peer-data-[size=lg]/menu-button:top-2.5”, “group-data-[collapsible=icon]:hidden”, showOnHover && “group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0”, className )} {…props} /> ) }) SidebarMenuAction.displayName = “SidebarMenuAction” const SidebarMenuBadge = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> >(({ className, …props }, ref) => ( <div ref={ref} data-sidebar=”menu-badge” className={cn( “absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none”, “peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground”, “group-data-[collapsible=icon]:hidden”, className )} {…props} /> )) SidebarMenuBadge.displayName = “SidebarMenuBadge” const SidebarMenuSkeleton = React.forwardRef< HTMLDivElement, React.ComponentProps<“div”> & { showIcon?: boolean } >(({ className, showIcon = false, …props }, ref) => { // Random width between 50 to 90%. const width = React.useMemo(() => { return `${Math.floor(Math.random() * 40) + 50}%` }, []) return ( <div ref={ref} data-sidebar=”menu-skeleton” className={cn(“rounded-md h-8 flex gap-2 px-2 items-center”, className)} {…props} > {showIcon && ( <Skeleton className=”size-4 rounded-md” data-sidebar=”menu-skeleton-icon” /> )} <Skeleton className=”h-4 flex-1 max-w-[–skeleton-width]” data-sidebar=”menu-skeleton-text” style={ { “–skeleton-width”: width, } as React.CSSProperties } /> </div> ) }) SidebarMenuSkeleton.displayName = “SidebarMenuSkeleton” const SidebarMenuSub = React.forwardRef< HTMLUListElement, React.ComponentProps<“ul”> >(({ className, …props }, ref) => ( <ul ref={ref} data-sidebar=”menu-sub” className={cn( “mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5”, “group-data-[collapsible=icon]:hidden”, className )} {…props} /> )) SidebarMenuSub.displayName = “SidebarMenuSub” const SidebarMenuSubItem = React.forwardRef< HTMLLIElement, React.ComponentProps<“li”> >(({ …props }, ref) => <li ref={ref} {…props} />) SidebarMenuSubItem.displayName = “SidebarMenuSubItem” const SidebarMenuSubButton = React.forwardRef< HTMLAnchorElement, React.ComponentProps<“a”> & { asChild?: boolean size?: “sm” | “md” isActive?: boolean } >(({ asChild = false, size = “md”, isActive, className, …props }, ref) => { const Comp = asChild ? Slot : “a” return ( <Comp ref={ref} data-sidebar=”menu-sub-button” data-size={size} data-active={isActive} className={cn( “flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground”, “data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground”, size === “sm” && “text-xs”, size === “md” && “text-sm”, “group-data-[collapsible=icon]:hidden”, className )} {…props} /> ) }) SidebarMenuSubButton.displayName = “SidebarMenuSubButton” export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar, }
分隔线组件 (Separator)
分隔线组件基于 @radix-ui/react-separator
封装。支持方向、装饰性属性及自定义样式,通过 cn
合并默认与用户类名,适用于构建灵活布局的 UI 界面。
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
交互与反馈组件
交互与反馈组件用于处理用户操作和提供反馈,包括对话框、弹出框、加载条等。
对话框组件 (Dialog)
对话框组件封装了 Radix UI 的 Dialog 对话框组件,提供 Dialog
, DialogContent
, DialogOverlay
等子组件,支持动画过渡和样式控制,适用于构建弹窗、模态框等交互场景。
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
弹出框组件 (Popover)
弹出框组件基于 @radix-ui/react-popover
封装,包含 Popover
、PopoverTrigger
和 PopoverContent
。支持对齐方式和偏移量配置,内容通过 Portal
渲染到根部,结合 cn
实现灵活样式控制,适用于信息展示或交互场景。
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
加载条组件 (LoadingBar)
加载条组件接收布尔属性 isVisible
控制加载条显示。渲染固定高度的水平条和绝对定位子元素,使用 CSS 动画模拟加载效果,通过 cn
支持动态类名切换,适用于页面或操作加载时提供反馈。
import { cn } from "@/lib/utils"
interface LoadingBarProps {
isVisible: boolean
}
export function LoadingBar({ isVisible }: LoadingBarProps) {
return (
<div
className={cn(
"fixed top-0 left-0 w-full h-1 bg-transparent z-50 overflow-hidden",
isVisible ? "block" : "hidden"
)}
>
<div className="h-full w-full bg-blue-500 animate-loading-bar"></div>
</div>
)
}
数据展示组件
数据展示组件用于展示数据和信息,包括表格、图表、骨架屏等。
表格组件 (Table)
表格组件定义了一个可复用的 React 表格组件库,包含 Table
、TableHeader
、TableBody
等子组件。每个组件使用 React.forwardRef
实现引用传递,并通过 cn
合并默认样式与用户类名,支持灵活定制。结构遵循 HTML 语义化标准,具备交互效果和视觉优化,适用于展示结构化数据。
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
图表组件 (Chart)
图表组件封装了基于 Recharts 的图表组件,提供统一主题、样式和交互行为。核心包括 ChartContainer
、ChartStyle
、ChartTooltipContent
和 ChartLegendContent
,支持动态注入 CSS 变量、自定义提示内容和图例样式,增强图表的可定制性与一致性。
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
骨架屏组件 (Skeleton)
骨架屏组件用于页面加载前展示占位效果。接收 className
和其他 div
属性,通过 cn
合并默认样式类名,渲染为带 data-slot="skeleton"
的 <div>
元素,提升用户体验。
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }
Mermaid图
UI基础组件架构图

表格
主要功能或组件及其描述
组件名称 | 描述 |
---|---|
Button | 按钮组件,支持多种风格和大小 |
Input | 输入框组件,支持聚焦、错误状态和暗色模式 |
Label | 标签组件,支持文本对齐、字体粗细、禁用状态等样式 |
Checkbox | 复选框组件,支持自定义类名、响应式交互与暗色模式 |
Card | 卡片组件,用于构建模块化 UI 结构 |
Sidebar | 侧边栏组件,支持展开/收起、Cookie 持久化、快捷键切换 |
Separator | 分隔线组件,支持方向、装饰性属性及自定义样式 |
Dialog | 对话框组件,支持动画过渡和样式控制 |
Popover | 弹出框组件,支持对齐方式和偏移量配置 |
LoadingBar | 加载条组件,用于页面或操作加载时提供反馈 |
Table | 表格组件,用于展示结构化数据 |
Chart | 图表组件,提供统一主题、样式和交互行为 |
Skeleton | 骨架屏组件,用于页面加载前展示占位效果 |
结论/总结
UI基础组件是项目中不可或缺的一部分,提供了从基础控件到复杂交互组件的完整集合。通过封装和扩展,这些组件支持高度定制化,适用于各种业务场景。了解和掌握这些组件的使用方法,有助于提高开发效率和用户体验。
本网站提供的所有AI生成内容均基于人工智能技术和大语言模型算法,根据用户输入指令自动生成。生成内容不代表本网站观点,亦不构成任何形式的专业建议。本公司对生成内容的准确性、完整性、适用性及合法性不作明示或默示的保证,用户应对生成内容自行判断并承担全部使用风险。