Compare commits

..

4 Commits

Author SHA1 Message Date
icarus
d8b841c386 Merge branch 'feat/show-enabled-provider-only' of github.com:CherryHQ/cherry-studio into feat/show-enabled-provider-only 2025-10-24 21:09:45 +08:00
icarus
5ee120db7c refactor(provider): change hide disabled providers to show disabled providers
Update the provider settings to use a more intuitive "show disabled" toggle instead of "hide disabled". This includes updating the preference schema, default value, and UI components to reflect this change.
2025-10-24 21:09:22 +08:00
GitHub Action
c22034080c fix(i18n): Auto update translations for PR #10933 2025-10-23 20:13:41 +00:00
icarus
b6fa2583d1 feat(provider-settings): add option to hide disabled providers
- Add hide disabled providers toggle in provider settings
- Update preference schema to store the setting
- Refactor provider list layout to accommodate new toggle
2025-10-24 04:10:02 +08:00
378 changed files with 12393 additions and 9677 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1,5 +1,4 @@
/src/renderer/src/store/ @0xfullex
/src/renderer/src/databases/ @0xfullex
/src/main/services/ConfigManager.ts @0xfullex
/packages/shared/IpcChannel.ts @0xfullex
/src/main/ipc.ts @0xfullex
@@ -10,4 +9,3 @@
/src/renderer/src/data/ @0xfullex
/packages/ui/ @MyPrototypeWhat

View File

@@ -3,18 +3,6 @@
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
-->
<!--
⚠️ Important: Redux/IndexedDB Data-Changing Feature PRs Temporarily On Hold ⚠️
Please note: For our current development cycle, we are not accepting feature Pull Requests that introduce changes to Redux data models or IndexedDB schemas.
While we value your contributions, PRs of this nature will be blocked without merge. We welcome all other contributions (bug fixes, perf enhancements, docs, etc.). Thank you!
Once version 2.0.0 is released, we will resume reviewing feature PRs.
-->
### What this PR does
Before this PR:

View File

@@ -1,10 +1,9 @@
name: Auto I18N
env:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
TRANSLATION_MODEL: ${{ vars.AUTO_I18N_MODEL || 'deepseek/deepseek-v3.1'}}
TRANSLATION_BASE_URL: ${{ vars.AUTO_I18N_BASE_URL || 'https://api.ppinfra.com/openai'}}
TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}}
API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
MODEL: ${{ vars.AUTO_I18N_MODEL || 'deepseek/deepseek-v3.1'}}
BASE_URL: ${{ vars.AUTO_I18N_BASE_URL || 'https://api.ppinfra.com/openai'}}
on:
pull_request:
@@ -30,7 +29,6 @@ jobs:
uses: actions/setup-node@v5
with:
node-version: 20
package-manager-cache: false
- name: 📦 Install dependencies in isolated directory
run: |
@@ -44,7 +42,7 @@ jobs:
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
- name: 🏃‍♀️ Translate
run: npx tsx scripts/sync-i18n.ts && npx tsx scripts/auto-translate-i18n.ts
run: npx tsx scripts/auto-translate-i18n.ts
- name: 🔍 Format
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/

View File

@@ -1,131 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index b3f018730a93639aad7c203f15fb1aeb766c73f4..ade2a43d66e9184799d072153df61ef7be4ea110 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -296,7 +296,14 @@ var HuggingFaceResponsesLanguageModel = class {
metadata: huggingfaceOptions == null ? void 0 : huggingfaceOptions.metadata,
instructions: huggingfaceOptions == null ? void 0 : huggingfaceOptions.instructions,
...preparedTools && { tools: preparedTools },
- ...preparedToolChoice && { tool_choice: preparedToolChoice }
+ ...preparedToolChoice && { tool_choice: preparedToolChoice },
+ ...(huggingfaceOptions?.reasoningEffort != null && {
+ reasoning: {
+ ...(huggingfaceOptions?.reasoningEffort != null && {
+ effort: huggingfaceOptions.reasoningEffort,
+ }),
+ },
+ }),
};
return { args: baseArgs, warnings };
}
@@ -365,6 +372,20 @@ var HuggingFaceResponsesLanguageModel = class {
}
break;
}
+ case 'reasoning': {
+ for (const contentPart of part.content) {
+ content.push({
+ type: 'reasoning',
+ text: contentPart.text,
+ providerMetadata: {
+ huggingface: {
+ itemId: part.id,
+ },
+ },
+ });
+ }
+ break;
+ }
case "mcp_call": {
content.push({
type: "tool-call",
@@ -519,6 +540,11 @@ var HuggingFaceResponsesLanguageModel = class {
id: value.item.call_id,
toolName: value.item.name
});
+ } else if (value.item.type === 'reasoning') {
+ controller.enqueue({
+ type: 'reasoning-start',
+ id: value.item.id,
+ });
}
return;
}
@@ -570,6 +596,22 @@ var HuggingFaceResponsesLanguageModel = class {
});
return;
}
+ if (isReasoningDeltaChunk(value)) {
+ controller.enqueue({
+ type: 'reasoning-delta',
+ id: value.item_id,
+ delta: value.delta,
+ });
+ return;
+ }
+
+ if (isReasoningEndChunk(value)) {
+ controller.enqueue({
+ type: 'reasoning-end',
+ id: value.item_id,
+ });
+ return;
+ }
},
flush(controller) {
controller.enqueue({
@@ -593,7 +635,8 @@ var HuggingFaceResponsesLanguageModel = class {
var huggingfaceResponsesProviderOptionsSchema = z2.object({
metadata: z2.record(z2.string(), z2.string()).optional(),
instructions: z2.string().optional(),
- strictJsonSchema: z2.boolean().optional()
+ strictJsonSchema: z2.boolean().optional(),
+ reasoningEffort: z2.string().optional(),
});
var huggingfaceResponsesResponseSchema = z2.object({
id: z2.string(),
@@ -727,12 +770,31 @@ var responseCreatedChunkSchema = z2.object({
model: z2.string()
})
});
+var reasoningTextDeltaChunkSchema = z2.object({
+ type: z2.literal('response.reasoning_text.delta'),
+ item_id: z2.string(),
+ output_index: z2.number(),
+ content_index: z2.number(),
+ delta: z2.string(),
+ sequence_number: z2.number(),
+});
+
+var reasoningTextEndChunkSchema = z2.object({
+ type: z2.literal('response.reasoning_text.done'),
+ item_id: z2.string(),
+ output_index: z2.number(),
+ content_index: z2.number(),
+ text: z2.string(),
+ sequence_number: z2.number(),
+});
var huggingfaceResponsesChunkSchema = z2.union([
responseOutputItemAddedSchema,
responseOutputItemDoneSchema,
textDeltaChunkSchema,
responseCompletedChunkSchema,
responseCreatedChunkSchema,
+ reasoningTextDeltaChunkSchema,
+ reasoningTextEndChunkSchema,
z2.object({ type: z2.string() }).loose()
// fallback for unknown chunks
]);
@@ -751,6 +813,12 @@ function isResponseCompletedChunk(chunk) {
function isResponseCreatedChunk(chunk) {
return chunk.type === "response.created";
}
+function isReasoningDeltaChunk(chunk) {
+ return chunk.type === 'response.reasoning_text.delta';
+}
+function isReasoningEndChunk(chunk) {
+ return chunk.type === 'response.reasoning_text.done';
+}
// src/huggingface-provider.ts
function createHuggingFace(options = {}) {

View File

@@ -65,28 +65,7 @@ The Test Plan aims to provide users with a more stable application experience an
### Other Suggestions
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.
## Important Contribution Guidelines & Focus Areas
Please review the following critical information before submitting your Pull Request:
### Temporary Restriction on Data-Changing Feature PRs 🚫
**Currently, we are NOT accepting feature Pull Requests that introduce changes to our Redux data models or IndexedDB schemas.**
Our core team is currently focused on significant architectural updates that involve these data structures. To ensure stability and focus during this period, contributions of this nature will be temporarily managed internally.
* **PRs that require changes to Redux state shape or IndexedDB schemas will be closed.**
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (please replace with your actual repo link).
We highly encourage contributions for:
* Bug fixes 🐞
* Performance improvements 🚀
* Documentation updates 📚
* Features that **do not** alter Redux data models or IndexedDB schemas (e.g., UI enhancements, new components, minor refactors). ✨
We appreciate your understanding and continued support during this important development phase. Thank you!
- **Become a Core Developer**: If you contribute to the project consistently, congratulations, you can become a core developer and gain project membership status. Please check our [Membership Guide](https://github.com/CherryHQ/community/blob/main/docs/membership.en.md).
## Contact Us

View File

@@ -37,7 +37,7 @@
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
@@ -45,7 +45,7 @@
</div>
<div align="center">
[![][github-release-shield]][github-release-link]
[![][github-nightly-shield]][github-nightly-link]
[![][github-contributors-shield]][github-contributors-link]
@@ -248,10 +248,10 @@ The Enterprise Edition addresses core challenges in team collaboration by centra
| Feature | Community Edition | Enterprise Edition |
| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee |
| **Admin Backend** | — | ● Centralized **Model** Access<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **Data** Backup |
| **Server** | — | ✅ Dedicated Private Deployment |
| **Server** | — | ✅ Dedicated Private Deployment |
## Get the Enterprise Edition

View File

@@ -69,28 +69,7 @@ git commit --signoff -m "Your commit message"
### 其他建议
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
## 重要贡献指南与关注点
在提交 Pull Request 之前,请务必阅读以下关键信息:
### 🚫 暂时限制涉及数据更改的功能性 PR
**目前,我们不接受涉及 Redux 数据模型或 IndexedDB schema 变更的功能性 Pull Request。**
我们的核心团队目前正专注于涉及这些数据结构的关键架构更新和基础工作。为确保在此期间的稳定性与专注,此类贡献将暂时由内部进行管理。
* **需要更改 Redux 状态结构或 IndexedDB schema 的 PR 将会被关闭。**
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (请替换为您的实际仓库链接) 跟踪 `v2.0.0` 的进展及相关讨论。
我们非常鼓励以下类型的贡献:
* 错误修复 🐞
* 性能改进 🚀
* 文档更新 📚
* 不改变 Redux 数据模型或 IndexedDB schema 的功能例如UI 增强、新组件、小型重构)。✨
感谢您在此重要开发阶段的理解与持续支持。谢谢!
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。请查看我们的[成员指南](https://github.com/CherryHQ/community/blob/main/membership.md)
## 联系我们

View File

@@ -1,260 +0,0 @@
> [!NOTE]
> This technical documentation was automatically generated by Claude Code based on analysis of the current OCR implementation in the codebase. The content reflects the architecture as of the current branch state.
# OCR Architecture
## Overview
Cherry Studio's OCR (Optical Character Recognition) system is a modular, extensible architecture designed to support multiple OCR providers and file types. The architecture follows a layered approach with clear separation of concerns between data access, business logic, and provider implementations.
## Architecture Layers
The OCR architecture follows a layered approach where data interactions occur through RESTful APIs, while IPC serves as part of the API layer, allowing the renderer to interact directly with the business layer:
### 1. API Layer
**Location**: `src/main/data/api/handlers/`, `src/main/ipc.ts`, `src/preload/index.ts`
- **IPC Bridge**: Serves as API layer connecting renderer to main process
- **Request Routing**: Routes IPC calls to appropriate service methods
- **Type Safety**: Zod schemas for request/response validation
- **Error Handling**: Centralized error propagation across process boundaries
- **Security**: Secure communication sandbox between renderer and main processes
### 2. OCR Service Layer (Business Layer)
**Location**: `src/main/services/ocr/`
- **OcrService**: Main business logic orchestrator and central coordinator
- **Provider Registry**: Manages registered OCR providers
- **Data Integration**: Direct interaction with data layer for provider management
- **Lifecycle Management**: Handles provider initialization and disposal
- **Validation**: Ensures provider availability and data integrity
- **Orchestration**: Coordinates between providers and data services
- **Direct IPC Access**: Renderer can directly invoke business layer methods via IPC
### 3. Provider Services Layer
**Location**: `src/main/services/ocr/builtin/`
- **Base Service**: Abstract `OcrBaseService` defines common interface
- **Data Independence**: No direct database interactions, relies on injected data
- **Built-in Providers**:
- `TesseractService`: Local Tesseract.js implementation
- `SystemOcrService`: Platform-specific system OCR
- `PpocrService`: PaddleOCR integration
- `OvOcrService`: Intel OpenVINO (NPU) OCR
- **Pure OCR Logic**: Focus solely on OCR processing capabilities
### 4. Data Layer
**Location**: `src/main/data/db/schemas/ocr/`, `src/main/data/repositories/`
- **Database Schema**: Uses Drizzle ORM with SQLite database
- **Repository Pattern**: `OcrProviderRepository` handles all database operations
- **Provider Storage**: Stores provider configurations in `ocr_provider` table
- **JSON Configuration**: Polymorphic `config` field stores provider-specific settings
- **Data Access**: Exclusively accessed by OCR Service layer
### 5. Frontend Layer
**Location**: `src/renderer/src/services/ocr/`, `src/renderer/src/hooks/ocr/`
- **Direct IPC Communication**: Direct interaction with business layer via IPC
- **React Hooks**: Custom hooks for OCR operations and state management
- **Configuration UI**: Settings pages for provider configuration
- **State Management**: Frontend state synchronization with backend data
## Data Flow
```mermaid
graph TD
A[Frontend UI] --> B[Frontend OCR Service]
B --> C[API Layer - IPC Bridge]
C --> D[OCR Service Layer - Business Logic]
D --> E[Data Layer - Provider Repository]
D --> F[Provider Services Layer]
F --> G[OCR Processing]
G --> H[Result]
H --> F
F --> D
D --> C
C --> B
B --> A
style D fill:#e1f5fe
style F fill:#f3e5f5
style E fill:#e8f5e8
style C fill:#fff3e0
```
**Key Flow Characteristics:**
- **Direct Business Access**: Frontend communicates directly with OCR Service layer via IPC
- **IPC as API Gateway**: IPC bridge functions as the API layer, handling routing and validation
- **Data Isolation**: Only business layer interacts with data persistence
- **Provider Independence**: OCR providers remain isolated from data concerns
## Provider System
### Provider Registration
- **Built-in Providers**: Automatically registered on service initialization
- **Custom Providers**: Support for extensible provider system
- **Configuration**: Each provider has its own configuration schema
### Provider Capabilities
```typescript
interface OcrProviderCapabilityRecord {
image?: boolean // Image file OCR support
pdf?: boolean // PDF file OCR support (future)
}
```
### Configuration Architecture
- **Polymorphic Config**: JSON-based configuration adapts to provider needs
- **Type Safety**: Zod schemas validate provider-specific configurations
- **Runtime Validation**: Configuration validation before OCR operations
## Type System
### Core Types
- **`OcrProvider`**: Base provider interface
- **`OcrParams`**: OCR operation parameters
- **`OcrResult`**: Standardized OCR result format
- **`SupportedOcrFile`**: File types supported for OCR
### Business Types
- **`OcrProviderBusiness`**: Domain-level provider representation
- **Operations**: Create, Update, Replace, Delete operations
- **Queries**: List providers with filtering options
### Provider-Specific Types
- **TesseractConfig**: Language selection, model paths
- **SystemOcrConfig**: Language preferences
- **PaddleOCRConfig**: API endpoints, authentication
- **OpenVINOConfig**: Device selection, model paths
## Built-in Providers
### Tesseract OCR
- **Engine**: Tesseract.js
- **Languages**: Multi-language support with automatic download
- **Configuration**: Language selection, cache management
- **Performance**: Worker pooling for concurrent processing
### System OCR
- **Windows**: Windows Media Foundation OCR
- **macOS**: Vision framework OCR
- **Linux**: Platform-specific implementations
- **Features**: Native performance, system integration
### PaddleOCR
- **Deployment**: Remote API integration
- **Languages**: Chinese, English, and mixed language support
- **Configuration**: API endpoints and authentication
### Intel OpenVINO OCR
- **Hardware**: NPU acceleration support
- **Performance**: Optimized for Intel hardware
- **Use Case**: High-performance OCR scenarios
## Configuration Management
### Database Schema
```sql
CREATE TABLE ocr_provider (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
capabilities TEXT NOT NULL, -- JSON
config TEXT NOT NULL, -- JSON
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### Provider Defaults
- **Initial Configuration**: Defined in `packages/shared/config/ocr.ts`
- **Migration System**: Automatic provider initialization on startup
- **User Customization**: Runtime configuration updates
## Error Handling
### Error Categories
- **Provider Errors**: OCR engine failures, missing dependencies
- **Configuration Errors**: Invalid settings, missing parameters
- **File Errors**: Unsupported formats, corrupted files
- **System Errors**: Resource exhaustion, permissions
### Error Propagation
- **Logging**: Centralized logging with context
- **User Feedback**: Translated error messages
- **Recovery**: Graceful fallback options
## Performance Considerations
### Resource Management
- **Worker Disposal**: Proper cleanup of OCR workers
- **Memory Management**: Limits on file sizes and concurrent operations
- **Caching**: Model and result caching where applicable
### Optimization
- **Lazy Loading**: Providers initialized on demand
- **Concurrent Processing**: Multiple workers for parallel operations
- **Hardware Acceleration**: NPU and GPU support where available
## Security
### Input Validation
- **File Type Checking**: Strict validation of supported formats
- **Size Limits**: Protection against resource exhaustion
- **Path Validation**: Prevention of path traversal attacks
### Configuration Security
- **API Key Storage**: Secure storage of sensitive configuration
- **Validation**: Runtime validation of configuration parameters
- **Sandboxing**: Isolated execution of OCR operations
## Extension Points
### Custom Providers
- **Interface**: Implement `OcrBaseService` for new providers
- **Registration**: Dynamic provider registration system
- **Configuration**: Extensible configuration schemas
### File Type Support
- **Handlers**: Modular file type processors
- **Capabilities**: Declarative provider capabilities
- **Future Support**: PDF, document formats planned
## Migration Strategy
### Legacy System
- **Data Migration**: Automatic migration from old configuration formats
- **Compatibility**: Backward compatibility during transition
- **Testing**: Comprehensive test coverage for migration paths
### Future Enhancements
- **PDF Support**: Planned extension to document OCR
- **Cloud Providers**: API-based OCR services integration
- **AI Enhancement**: Post-processing and accuracy improvements
## Development Guidelines
### Adding New Providers
1. Create provider service extending `OcrBaseService`
2. Define provider-specific configuration schema
3. Register provider in `OcrService`
4. Add configuration UI components
5. Include comprehensive tests
> [!WARNING]
> Provider services should never directly access the data layer. All data operations must go through the OCR Service layer to maintain proper separation of concerns.
### Configuration Changes
1. Update provider configuration schema
2. Add migration logic for existing configurations
3. Update UI validation and error handling
4. Test with various configuration scenarios
> [!WARNING]
> Always validate configuration changes before saving to the database. Use Zod schemas for runtime validation to prevent corrupted provider configurations.
### Testing
- **Unit Tests**: Provider implementation testing
- **Integration Tests**: End-to-end OCR workflows
- **Performance Tests**: Resource usage and timing
- **Error Scenarios**: Comprehensive error handling testing

View File

@@ -1,260 +0,0 @@
> [!NOTE]
> 本技术文档由 Claude Code 基于对当前代码库中 OCR 实现的分析自动生成。内容反映了当前分支状态的架构设计。
# OCR 架构文档
## 概述
Cherry Studio 的 OCR光学字符识别系统是一个模块化、可扩展的架构旨在支持多个 OCR 提供商和文件类型。该架构采用分层设计,在数据访问、业务逻辑和提供商实现之间有明确的关注点分离。
## 架构分层
OCR 架构采用分层方法,其中数据交互通过 RESTful API 进行,而 IPC 作为 API 层的一部分,允许 Renderer 直接与业务层交互:
### 1. API 层
**位置**: `src/main/data/api/handlers/`, `src/main/ipc.ts`, `src/preload/index.ts`
- **IPC 桥接**: 作为 API 层连接 Renderer 到主进程
- **请求路由**: 将 IPC 调用路由到相应的服务方法
- **类型安全**: 使用 Zod 模式进行请求/响应验证
- **错误处理**: 跨进程边界的集中式错误传播
- **安全**: Renderer 和主进程之间的安全通信沙盒
### 2. OCR 服务层(业务层)
**位置**: `src/main/services/ocr/`
- **OcrService**: 主要业务逻辑协调器和中央协调器
- **提供商注册表**: 管理已注册的 OCR 提供商
- **数据集成**: 与数据层直接交互进行提供商管理
- **生命周期管理**: 处理提供商初始化和销毁
- **验证**: 确保提供商可用性和数据完整性
- **协调**: 协调提供商和数据服务之间的交互
- **直接 IPC 访问**: Renderer 可通过 IPC 直接调用业务层方法
### 3. 提供商服务层
**位置**: `src/main/services/ocr/builtin/`
- **基础服务**: 抽象的 `OcrBaseService` 定义通用接口
- **数据独立性**: 无直接数据库交互,依赖外部传入的数据
- **内置提供商**:
- `TesseractService`: 本地 Tesseract.js 实现
- `SystemOcrService`: 平台特定的系统 OCR
- `PpocrService`: PaddleOCR 集成
- `OvOcrService`: Intel OpenVINO (NPU) OCR
- **纯 OCR 逻辑**: 专注于 OCR 处理能力
### 4. 数据层
**位置**: `src/main/data/db/schemas/ocr/`, `src/main/data/repositories/`
- **数据库架构**: 使用 Drizzle ORM 和 SQLite 数据库
- **仓储模式**: `OcrProviderRepository` 处理所有数据库操作
- **提供商存储**: 在 `ocr_provider` 表中存储提供商配置
- **JSON 配置**: 多态的 `config` 字段存储提供商特定的设置
- **数据访问**: 仅由 OCR 服务层访问
### 5. Renderer 层
**位置**: `src/renderer/src/services/ocr/`, `src/renderer/src/hooks/ocr/`
- **直接 IPC 通信**: 通过 IPC 与业务层直接交互
- **React Hooks**: 用于 OCR 操作和状态管理的自定义钩子
- **配置 UI**: 提供商配置的设置页面
- **状态管理**: Renderer 状态与后端数据同步
## 数据流
```mermaid
graph TD
A[Renderer UI] --> B[Renderer OCR 服务]
B --> C[API 层 - IPC 桥接]
C --> D[OCR 服务层 - 业务逻辑]
D --> E[数据层 - 提供商仓储]
D --> F[提供商服务层]
F --> G[OCR 处理]
G --> H[结果]
H --> F
F --> D
D --> C
C --> B
B --> A
style D fill:#e1f5fe
style F fill:#f3e5f5
style E fill:#e8f5e8
style C fill:#fff3e0
```
**关键流程特征**:
- **直接业务访问**: Renderer 通过 IPC 与 OCR 服务层直接通信
- **IPC 作为 API 网关**: IPC 桥接作为 API 层,处理路由和验证
- **数据隔离**: 只有业务层与数据持久化交互
- **提供商独立性**: OCR 提供商保持与数据关注点的隔离
## 提供商系统
### 提供商注册
- **内置提供商**: 在服务初始化时自动注册
- **自定义提供商**: 支持可扩展的提供商系统
- **配置**: 每个提供商都有自己的配置模式
### 提供商能力
```typescript
interface OcrProviderCapabilityRecord {
image?: boolean // 图像文件 OCR 支持
pdf?: boolean // PDF 文件 OCR 支持(未来)
}
```
### 配置架构
- **多态配置**: 基于 JSON 的配置适应提供商需求
- **类型安全**: Zod 模式验证提供商特定的配置
- **运行时验证**: OCR 操作前的配置验证
## 类型系统
### 核心类型
- **`OcrProvider`**: 基础提供商接口
- **`OcrParams`**: OCR 操作参数
- **`OcrResult`**: 标准化的 OCR 结果格式
- **`SupportedOcrFile`**: 支持 OCR 的文件类型
### 业务类型
- **`OcrProviderBusiness`**: 域级别的提供商表示
- **操作**: 创建、更新、替换、删除操作
- **查询**: 带过滤选项的提供商列表
### 提供商特定类型
- **TesseractConfig**: 语言选择、模型路径
- **SystemOcrConfig**: 语言偏好
- **PaddleOCRConfig**: API 端点、认证
- **OpenVINOConfig**: 设备选择、模型路径
## 内置提供商
### Tesseract OCR
- **引擎**: Tesseract.js
- **语言**: 支持多语言,自动下载
- **配置**: 语言选择、缓存管理
- **性能**: 工作池用于并发处理
### 系统 OCR
- **Windows**: Windows Media Foundation OCR
- **macOS**: Vision 框架 OCR
- **Linux**: 平台特定实现
- **特性**: 原生性能、系统集成
### PaddleOCR
- **部署**: 远程 API 集成
- **语言**: 中文、英文和混合语言支持
- **配置**: API 端点和认证
### Intel OpenVINO OCR
- **硬件**: NPU 加速支持
- **性能**: 为 Intel 硬件优化
- **用例**: 高性能 OCR 场景
## 配置管理
### 数据库架构
```sql
CREATE TABLE ocr_provider (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
capabilities TEXT NOT NULL, -- JSON
config TEXT NOT NULL, -- JSON
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### 提供商默认值
- **初始配置**: 在 `packages/shared/config/ocr.ts` 中定义
- **迁移系统**: 启动时自动提供商初始化
- **用户自定义**: 运行时配置更新
## 错误处理
### 错误类别
- **提供商错误**: OCR 引擎故障、缺少依赖
- **配置错误**: 无效设置、缺少参数
- **文件错误**: 不支持的格式、损坏的文件
- **系统错误**: 资源耗尽、权限问题
### 错误传播
- **日志**: 带上下文的集中日志记录
- **用户反馈**: 翻译的错误消息
- **恢复**: 优雅的回退选项
## 性能考虑
### 资源管理
- **工作器销毁**: OCR 工作器的适当清理
- **内存管理**: 文件大小和并发操作限制
- **缓存**: 模型和结果缓存(如适用)
### 优化
- **延迟加载**: 按需初始化提供商
- **并发处理**: 多工作器用于并行操作
- **硬件加速**: NPU 和 GPU 支持(如可用)
## 安全
### 输入验证
- **文件类型检查**: 严格验证支持的格式
- **大小限制**: 防止资源耗尽
- **路径验证**: 防止路径遍历攻击
### 配置安全
- **API 密钥存储**: 敏感配置的安全存储
- **验证**: 配置参数的运行时验证
- **沙盒**: OCR 操作的隔离执行
## 扩展点
### 自定义提供商
- **接口**: 为新提供商实现 `OcrBaseService`
- **注册**: 动态提供商注册系统
- **配置**: 可扩展的配置模式
### 文件类型支持
- **处理器**: 模块化文件类型处理器
- **能力**: 声明式提供商能力
- **未来支持**: PDF、文档格式计划中
## 迁移策略
### 遗留系统
- **数据迁移**: 从旧配置格式自动迁移
- **兼容性**: 过渡期间的向后兼容性
- **测试**: 迁移路径的全面测试覆盖
### 未来增强
- **PDF 支持**: 计划扩展到文档 OCR
- **云提供商**: 基于 API 的 OCR 服务集成
- **AI 增强**: 后处理和准确性改进
## 开发指南
### 添加新提供商
1. 创建扩展 `OcrBaseService` 的提供商服务
2. 定义提供商特定的配置模式
3.`OcrService` 中注册提供商
4. 添加配置 UI 组件
5. 包含全面的测试
> [!WARNING]
> 提供商服务绝不应直接访问数据层。所有数据操作必须通过 OCR 服务层进行,以保持适当的关注点分离。
### 配置更改
1. 更新提供商配置模式
2. 为现有配置添加迁移逻辑
3. 更新 UI 验证和错误处理
4. 测试各种配置场景
> [!WARNING]
> 在保存到数据库之前,务必验证配置更改。使用 Zod 模式进行运行时验证,防止提供商配置损坏。
### 测试
- **单元测试**: 提供商实现测试
- **集成测试**: 端到端 OCR 工作流
- **性能测试**: 资源使用和时间
- **错误场景**: 全面的错误处理测试

View File

@@ -1,10 +0,0 @@
CREATE TABLE `ocr_provider` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`capabilities` text NOT NULL,
`config` text NOT NULL,
`created_at` integer,
`updated_at` integer
);
--> statement-breakpoint
CREATE INDEX `name` ON `ocr_provider` (`name`);

View File

@@ -1,172 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "64f7ad88-7111-4574-988c-d7ef429e375d",
"prevId": "de8009d7-95b9-4f99-99fa-4b8795708f21",
"tables": {
"app_state": {
"name": "app_state",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"ocr_provider": {
"name": "ocr_provider",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"capabilities": {
"name": "capabilities",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"config": {
"name": "config",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"name": {
"name": "name",
"columns": ["name"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"preference": {
"name": "preference",
"columns": {
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"scope_name_idx": {
"name": "scope_name_idx",
"columns": ["scope", "key"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -7,13 +7,6 @@
"tag": "0000_solid_lord_hawal",
"version": "6",
"when": 1754745234572
},
{
"idx": 1,
"version": "6",
"when": 1760969721294,
"tag": "0001_previous_sir_ram",
"breakpoints": true
}
],
"version": "7"

View File

@@ -85,7 +85,6 @@
"@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.4.7",
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
"@radix-ui/react-tabs": "^1.1.13",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"express": "^5.1.0",
"font-list": "^2.0.0",
@@ -95,7 +94,7 @@
"officeparser": "^4.2.0",
"os-proxy-config": "^1.1.2",
"selection-hook": "^1.0.12",
"sharp": "0.34.4",
"sharp": "^0.34.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
@@ -107,7 +106,6 @@
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.35",
"@ai-sdk/google-vertex": "^3.0.40",
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
"@ai-sdk/mistral": "^2.0.19",
"@ai-sdk/perplexity": "^2.0.13",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
@@ -131,8 +129,8 @@
"@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@cherrystudio/extension-table-plus": "workspace:^",
"@cherrystudio/openai": "^6.5.0",
"@cherrystudio/ui": "workspace:*",
"@cherrystudio/openai": "^6.5.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@@ -153,7 +151,7 @@
"@modelcontextprotocol/sdk": "^1.17.5",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@openrouter/ai-sdk-provider": "^1.2.0",
"@openrouter/ai-sdk-provider": "^1.1.2",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "2.0.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
@@ -391,13 +389,12 @@
"vite": "npm:rolldown-vite@7.1.5",
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
"@ai-sdk/google@npm:2.0.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch",
"@img/sharp-darwin-arm64": "0.34.4",
"@img/sharp-darwin-x64": "0.34.4",
"@img/sharp-linux-arm": "0.34.4",
"@img/sharp-linux-arm64": "0.34.4",
"@img/sharp-linux-x64": "0.34.4",
"@img/sharp-win32-x64": "0.34.4",
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0"
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
},
"packageManager": "yarn@4.9.1",
"lint-staged": {

View File

@@ -7,7 +7,6 @@ import { createAzure } from '@ai-sdk/azure'
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
import { createDeepSeek } from '@ai-sdk/deepseek'
import { createGoogleGenerativeAI } from '@ai-sdk/google'
import { createHuggingFace } from '@ai-sdk/huggingface'
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
import type { LanguageModelV2 } from '@ai-sdk/provider'
@@ -30,8 +29,7 @@ export const baseProviderIds = [
'azure',
'azure-responses',
'deepseek',
'openrouter',
'huggingface'
'openrouter'
] as const
/**
@@ -135,12 +133,6 @@ export const baseProviders = [
name: 'OpenRouter',
creator: createOpenRouter,
supportsImageGeneration: true
},
{
id: 'huggingface',
name: 'HuggingFace',
creator: createHuggingFace,
supportsImageGeneration: true
}
] as const satisfies BaseProvider[]

View File

@@ -138,7 +138,6 @@ export enum IpcChannel {
Windows_Close = 'window:close',
Windows_IsMaximized = 'window:is-maximized',
Windows_MaximizedChanged = 'window:maximized-changed',
Windows_NavigateToAbout = 'window:navigate-to-about',
KnowledgeBase_Create = 'knowledge-base:create',
KnowledgeBase_Reset = 'knowledge-base:reset',
@@ -369,7 +368,8 @@ export enum IpcChannel {
CodeTools_RemoveCustomTerminalPath = 'code-tools:remove-custom-terminal-path',
// OCR
OCR_Ocr = 'ocr:ocr',
OCR_ocr = 'ocr:ocr',
OCR_ListProviders = 'ocr:list-providers',
// OVMS
Ovms_AddModel = 'ovms:add-model',

View File

@@ -1,176 +0,0 @@
import type {
BuiltinOcrProvider,
BuiltinOcrProviderId,
OcrOvProvider,
OcrPpocrProvider,
OcrSystemProvider,
OcrTesseractProvider,
TesseractLangCode
} from '@types'
import type { TranslateLanguageCode } from '../../../src/renderer/src/types/translate'
export const tesseract: OcrTesseractProvider = {
id: 'tesseract',
name: 'Tesseract',
capabilities: {
image: true
},
config: {
langs: {
chi_sim: true,
chi_tra: true,
eng: true
},
enabled: false
}
} as const
export const systemOcr: OcrSystemProvider = {
id: 'system',
name: 'System',
capabilities: {
image: true
// pdf: true
},
config: {
langs: ['en-us'],
enabled: false
}
} as const satisfies OcrSystemProvider
export const ppocrOcr: OcrPpocrProvider = {
id: 'paddleocr',
name: 'PaddleOCR',
capabilities: {
image: true
// pdf: true
},
config: { apiUrl: '', enabled: false }
} as const
export const ovOcr: OcrOvProvider = {
id: 'ovocr',
name: 'Intel OV(NPU) OCR',
capabilities: {
image: true
// pdf: true
},
config: {
enabled: false
}
} as const satisfies OcrOvProvider
export const INITIAL_BUILTIN_OCR_PROVIDER_MAP = {
tesseract,
system: systemOcr,
paddleocr: ppocrOcr,
ovocr: ovOcr
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(INITIAL_BUILTIN_OCR_PROVIDER_MAP)
export const TESSERACT_LANG_MAP: Record<TranslateLanguageCode, TesseractLangCode> = {
'af-za': 'afr',
'am-et': 'amh',
'ar-sa': 'ara',
'as-in': 'asm',
'az-az': 'aze',
'az-cyrl-az': 'aze_cyrl',
'be-by': 'bel',
'bn-bd': 'ben',
'bo-cn': 'bod',
'bs-ba': 'bos',
'bg-bg': 'bul',
'ca-es': 'cat',
'ceb-ph': 'ceb',
'cs-cz': 'ces',
'zh-cn': 'chi_sim',
'zh-tw': 'chi_tra',
'chr-us': 'chr',
'cy-gb': 'cym',
'da-dk': 'dan',
'de-de': 'deu',
'dz-bt': 'dzo',
'el-gr': 'ell',
'en-us': 'eng',
'enm-gb': 'enm',
'eo-world': 'epo',
'et-ee': 'est',
'eu-es': 'eus',
'fa-ir': 'fas',
'fi-fi': 'fin',
'fr-fr': 'fra',
'frk-de': 'frk',
'frm-fr': 'frm',
'ga-ie': 'gle',
'gl-es': 'glg',
'grc-gr': 'grc',
'gu-in': 'guj',
'ht-ht': 'hat',
'he-il': 'heb',
'hi-in': 'hin',
'hr-hr': 'hrv',
'hu-hu': 'hun',
'iu-ca': 'iku',
'id-id': 'ind',
'is-is': 'isl',
'it-it': 'ita',
'ita-it': 'ita_old',
'jv-id': 'jav',
'ja-jp': 'jpn',
'kn-in': 'kan',
'ka-ge': 'kat',
'kat-ge': 'kat_old',
'kk-kz': 'kaz',
'km-kh': 'khm',
'ky-kg': 'kir',
'ko-kr': 'kor',
'ku-tr': 'kur',
'la-la': 'lao',
'la-va': 'lat',
'lv-lv': 'lav',
'lt-lt': 'lit',
'ml-in': 'mal',
'mr-in': 'mar',
'mk-mk': 'mkd',
'mt-mt': 'mlt',
'ms-my': 'msa',
'my-mm': 'mya',
'ne-np': 'nep',
'nl-nl': 'nld',
'no-no': 'nor',
'or-in': 'ori',
'pa-in': 'pan',
'pl-pl': 'pol',
'pt-pt': 'por',
'ps-af': 'pus',
'ro-ro': 'ron',
'ru-ru': 'rus',
'sa-in': 'san',
'si-lk': 'sin',
'sk-sk': 'slk',
'sl-si': 'slv',
'es-es': 'spa',
'spa-es': 'spa_old',
'sq-al': 'sqi',
'sr-rs': 'srp',
'sr-latn-rs': 'srp_latn',
'sw-tz': 'swa',
'sv-se': 'swe',
'syr-sy': 'syr',
'ta-in': 'tam',
'te-in': 'tel',
'tg-tj': 'tgk',
'tl-ph': 'tgl',
'th-th': 'tha',
'ti-er': 'tir',
'tr-tr': 'tur',
'ug-cn': 'uig',
'uk-ua': 'ukr',
'ur-pk': 'urd',
'uz-uz': 'uzb',
'uz-cyrl-uz': 'uzb_cyrl',
'vi-vn': 'vie',
'yi-us': 'yid'
}

View File

@@ -1,18 +1,5 @@
// NOTE: Types are defined inline in the schema for simplicity
// If needed, specific types can be imported from './apiModels'
import type {
CreateOcrProviderRequest,
CreateOcrProviderResponse,
GetOcrProviderResponse,
ListOcrProvidersQuery,
ListOcrProvidersResponse,
OcrProviderId,
ReplaceOcrProviderRequest,
ReplaceOcrProviderResponse,
UpdateOcrProviderRequest,
UpdateOcrProviderResponse
} from '@types'
import type { BodyForPath, ConcreteApiPaths, QueryParamsForPath, ResponseForPath } from './apiPaths'
import type { HttpMethod, PaginatedResponse, PaginationParams } from './apiTypes'
@@ -358,38 +345,6 @@ export interface ApiSchemas {
}>
}
}
'/ocr/providers': {
GET: {
query: ListOcrProvidersQuery
response: ListOcrProvidersResponse
}
POST: {
body: CreateOcrProviderRequest
response: CreateOcrProviderResponse
}
}
'/ocr/providers/:id': {
GET: {
params: { id: OcrProviderId }
response: GetOcrProviderResponse
}
PATCH: {
params: { id: OcrProviderId }
body: UpdateOcrProviderRequest
response: UpdateOcrProviderResponse
}
PUT: {
params: { id: OcrProviderId }
body: ReplaceOcrProviderRequest
response: ReplaceOcrProviderResponse
}
DELETE: {
params: { id: OcrProviderId }
response: void
}
}
}
/**

View File

@@ -351,8 +351,6 @@ export interface PreferenceSchemas {
'feature.translate.model_prompt': string
// redux/settings/targetLanguage
'feature.translate.target_language': string
// redux/ocr/imageProviderId
'ocr.settings.image_provider_id': string | null
// redux/shortcuts/shortcuts.exit_fullscreen
'shortcut.app.exit_fullscreen': Record<string, unknown>
// redux/shortcuts/shortcuts.search_message
@@ -401,6 +399,7 @@ export interface PreferenceSchemas {
'ui.custom_css': string
// redux/settings/navbarPosition
'ui.navbar.position': 'left' | 'top'
'ui.provider.show_disabled': boolean
// redux/settings/sidebarIcons.disabled
'ui.sidebar.icons.invisible': PreferenceTypes.SidebarIcon[]
// redux/settings/sidebarIcons.visible
@@ -614,7 +613,6 @@ export const DefaultPreferences: PreferenceSchemas = {
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
'feature.translate.model_prompt': TRANSLATE_PROMPT,
'feature.translate.target_language': 'en-us',
'ocr.settings.image_provider_id': null,
'shortcut.app.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true },
'shortcut.app.search_message': {
editable: true,
@@ -659,6 +657,7 @@ export const DefaultPreferences: PreferenceSchemas = {
'topic.tab.show_time': false,
'ui.custom_css': '',
'ui.navbar.position': 'top',
'ui.provider.show_disabled': true,
'ui.sidebar.icons.invisible': [],
'ui.sidebar.icons.visible': [
'assistants',

View File

@@ -1,2 +0,0 @@
export * from './json'
export * from './net'

View File

@@ -1,7 +0,0 @@
export function safeParseJson(text: string): unknown | null {
try {
return JSON.parse(text)
} catch {
return null
}
}

View File

@@ -1,151 +1,152 @@
# UI Component Library Migration Status
# UI 组件库迁移状态
## Usage Example
## 使用示例
```typescript
// Import components from @cherrystudio/ui
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
// @cherrystudio/ui 导入组件
import { Spinner, DividerWithText, InfoTooltip, CustomTag } from '@cherrystudio/ui'
// Use in components
// 在组件中使用
function MyComponent() {
return (
<div>
<Spinner size={24} />
<DividerWithText text="Divider Text" />
<InfoTooltip content="Tooltip message" />
<DividerWithText text="分隔文本" />
<InfoTooltip content="提示信息" />
<CustomTag color="var(--color-primary)"></CustomTag>
</div>
)
}
```
## Directory Structure
## 目录结构说明
```text
@packages/ui/
├── src/
│ ├── components/ # Main components directory
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
│ │ ├── icons/ # Icon components
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
│ │ └── composite/ # Composite components (made from multiple base components)
│ ├── hooks/ # Custom React Hooks
│ └── types/ # TypeScript type definitions
│ ├── components/ # 组件主目录
│ │ ├── base/ # 基础组件(按钮、输入框、标签等)
│ │ ├── display/ # 显示组件(卡片、列表、表格等)
│ │ ├── layout/ # 布局组件(容器、网格、间距等)
│ │ ├── icons/ # 图标组件
│ │ ├── interactive/ # 交互组件(弹窗、提示、下拉等)
│ │ └── composite/ # 复合组件(多个基础组件组合而成)
│ ├── hooks/ # 自定义 React Hooks
│ └── types/ # TypeScript 类型定义
```
### Component Classification Guide
### 组件分类指南
When submitting PRs, please place components in the correct directory based on their function:
提交 PR 时,请根据组件功能将其放入正确的目录:
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
- **icons**: All icon-related components
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
- **composite**: Composite components made from multiple base components
- **base**: 最基础的 UI 元素,如按钮、输入框、开关、标签等
- **display**: 用于展示内容的组件,如卡片、列表、表格、标签页等
- **layout**: 用于页面布局的组件,如容器、网格系统、分隔符等
- **icons**: 所有图标相关的组件
- **interactive**: 需要用户交互的组件,如模态框、抽屉、提示框、下拉菜单等
- **composite**: 复合组件,由多个基础组件组合而成
## Migration Overview
## 迁移概览
- **Total Components**: 236
- **Migrated**: 34
- **Refactored**: 18
- **Pending Migration**: 184
- **总组件数**: 236
- **已迁移**: 34
- **已重构**: 18
- **待迁移**: 184
## Component Status Table
## 组件状态表
| Category | Component Name | Migration Status | Refactoring Status | Description |
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **base** | | | | Base components |
| | CopyButton | ✅ | ✅ | Copy button |
| | CustomTag | ✅ | ✅ | Custom tag |
| | DividerWithText | ✅ | ✅ | Divider with text |
| | EmojiIcon | ✅ | ✅ | Emoji icon |
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner |
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | ✅ | Text ellipsis |
| | ExpandableText | ✅ | ✅ | Expandable text |
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
| | ListItem | ✅ | ✅ | List item |
| | MaxContextCount | ✅ | ✅ | Max context count display |
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
| | Preview/* | ❌ | ❌ | Preview components |
| **layout** | | | | Layout components |
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
| | Scrollbar | ✅ | ❌ | Scrollbar |
| | Layout/* | ✅ | ✅ | Layout components |
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
| **icons** | | | | Icon components |
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
| | EditableNumber | ✅ | ❌ | Editable number |
| | InfoPopover | ✅ | ❌ | Info popover |
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
| | ImageToolButton | ✅ | ❌ | Image tool button |
| | DraggableList | ✅ | ❌ | Draggable list |
| | CodeEditor | ✅ | ❌ | Code editor |
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
| | LanguageSelect | ❌ | ❌ | Language select |
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
| **composite** | | | | Composite components |
| | - | - | - | No composite components yet |
| **Uncategorized** | | | | Components needing categorization |
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
| | Avatar/* | ❌ | ❌ | Avatar components |
| | ActionTools/* | ❌ | ❌ | Action tools |
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
| Category | Component Name | Migration Status | Refactoring Status | Description |
| --------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **base** | | | | 基础组件 |
| | CopyButton | ✅ | ✅ | 复制按钮 |
| | CustomTag | ✅ | ✅ | 自定义标签 |
| | DividerWithText | ✅ | ✅ | 带文本的分隔线 |
| | EmojiIcon | ✅ | ✅ | 表情图标 |
| | ErrorBoundary | ✅ | ✅ | 错误边界 (通过 props 解耦) |
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTagSuccessTagWarnTagInfoTag |
| | IndicatorLight | ✅ | ✅ | 指示灯 |
| | Spinner | ✅ | ✅ | 加载动画 |
| | TextBadge | ✅ | ✅ | 文本徽标 |
| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 |
| **display** | | | | 显示组件 |
| | Ellipsis | ✅ | ✅ | 文本省略 |
| | ExpandableText | ✅ | ✅ | 可展开文本 |
| | ThinkingEffect | ✅ | ✅ | 思考效果动画 |
| | EmojiAvatar | ✅ | ✅ | 表情头像 |
| | ListItem | ✅ | ✅ | 列表项 |
| | MaxContextCount | ✅ | ✅ | 最大上下文数显示 |
| | ProviderAvatar | ✅ | ✅ | 提供者头像 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
| | Preview/* | ❌ | ❌ | 预览组件 |
| **layout** | | | | 布局组件 |
| | HorizontalScrollContainer | ✅ | ❌ | 水平滚动容器 |
| | Scrollbar | ✅ | ❌ | 滚动条 |
| | Layout/* | ✅ | ✅ | 布局组件 |
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
| **icons** | | | | 图标组件 |
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIconDeleteIconEditIconRefreshIconResetIconToolIconVisionIconWebSearchIconWrapIconUnWrapIconOcrIcon |
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIconFilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
| **interactive** | | | | 交互组件 |
| | InfoTooltip | ✅ | ❌ | 信息提示 |
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
| | WarnTooltip | ✅ | ❌ | 警告提示 |
| | EditableNumber | ✅ | ❌ | 可编辑数字 |
| | InfoPopover | ✅ | ❌ | 信息弹出框 |
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
| | LanguageSelect | ❌ | ❌ | 语言选择 |
| | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) |
| **composite** | | | | 复合组件 |
| | - | - | - | 暂无复合组件 |
| **未分类** | | | | 需要分类的组件 |
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
| | Avatar/* | ❌ | ❌ | 头像组件 |
| | ActionTools/* | ❌ | ❌ | 操作工具 |
| | CodeBlockView/* | ❌ | ❌ | 代码块视图 (window.api 依赖) |
| | ContextMenu | ❌ | ❌ | 右键菜单 (Electron API) |
| | WindowControls | ❌ | ❌ | 窗口控制 (Electron API) |
| | ErrorBoundary | ❌ | ❌ | 错误边界 (window.api 依赖) |
## Migration Steps
## 迁移步骤
### Phase 1: Copy Migration (Current Phase)
### 第一阶段:复制迁移(当前阶段)
- Copy components as-is to @packages/ui
- Retain original dependencies (antd, styled-components, etc.)
- Add original path comment at file top
- 将组件原样复制到 @packages/ui
- 保留原有依赖(antdstyled-components 等)
- 在文件顶部添加原路径注释
### Phase 2: Refactor and Optimize
### 第二阶段:重构优化
- Remove antd dependencies, replace with HeroUI
- Remove styled-components, replace with Tailwind CSS
- Optimize component APIs and type definitions
- 移除 antd 依赖,替换为 HeroUI
- 移除 styled-components,替换为 Tailwind CSS
- 优化组件 API 和类型定义
## Notes
## 注意事项
1. **Do NOT migrate** components with these dependencies (can be migrated after decoupling):
- window.api calls
- Redux (useSelector, useDispatch, etc.)
- Other external data sources
1. **不迁移**包含以下依赖的组件(解耦后可迁移):
- window.api 调用
- ReduxuseSelectoruseDispatch 等)
- 其他外部数据源
2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props)
- Components using antd (replace with HeroUI later)
2. **可迁移**但需要后续解耦的组件:
- 使用 i18n 的组件(将 i18n 改为 props 传入)
- 使用 antd 的组件(后续替换为 HeroUI
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Update migration status in this document
3. **提交规范**
- 每次 PR 专注于一个类别的组件
- 确保所有迁移的组件都有导出
- 更新此文档的迁移状态

View File

@@ -0,0 +1,151 @@
# UI Component Library Migration Status
## Usage Example
```typescript
// Import components from @cherrystudio/ui
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
// Use in components
function MyComponent() {
return (
<div>
<Spinner size={24} />
<DividerWithText text="Divider Text" />
<InfoTooltip content="Tooltip message" />
</div>
)
}
```
## Directory Structure
```text
@packages/ui/
├── src/
│ ├── components/ # Main components directory
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
│ │ ├── icons/ # Icon components
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
│ │ └── composite/ # Composite components (made from multiple base components)
│ ├── hooks/ # Custom React Hooks
│ └── types/ # TypeScript type definitions
```
### Component Classification Guide
When submitting PRs, please place components in the correct directory based on their function:
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
- **icons**: All icon-related components
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
- **composite**: Composite components made from multiple base components
## Migration Overview
- **Total Components**: 236
- **Migrated**: 34
- **Refactored**: 18
- **Pending Migration**: 184
## Component Status Table
| Category | Component Name | Migration Status | Refactoring Status | Description |
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **base** | | | | Base components |
| | CopyButton | ✅ | ✅ | Copy button |
| | CustomTag | ✅ | ✅ | Custom tag |
| | DividerWithText | ✅ | ✅ | Divider with text |
| | EmojiIcon | ✅ | ✅ | Emoji icon |
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner |
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | ✅ | Text ellipsis |
| | ExpandableText | ✅ | ✅ | Expandable text |
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
| | ListItem | ✅ | ✅ | List item |
| | MaxContextCount | ✅ | ✅ | Max context count display |
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
| | Preview/* | ❌ | ❌ | Preview components |
| **layout** | | | | Layout components |
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
| | Scrollbar | ✅ | ❌ | Scrollbar |
| | Layout/* | ✅ | ✅ | Layout components |
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
| **icons** | | | | Icon components |
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
| | EditableNumber | ✅ | ❌ | Editable number |
| | InfoPopover | ✅ | ❌ | Info popover |
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
| | ImageToolButton | ✅ | ❌ | Image tool button |
| | DraggableList | ✅ | ❌ | Draggable list |
| | CodeEditor | ✅ | ❌ | Code editor |
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
| | LanguageSelect | ❌ | ❌ | Language select |
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
| **composite** | | | | Composite components |
| | - | - | - | No composite components yet |
| **Uncategorized** | | | | Components needing categorization |
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
| | Avatar/* | ❌ | ❌ | Avatar components |
| | ActionTools/* | ❌ | ❌ | Action tools |
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
## Migration Steps
### Phase 1: Copy Migration (Current Phase)
- Copy components as-is to @packages/ui
- Retain original dependencies (antd, styled-components, etc.)
- Add original path comment at file top
### Phase 2: Refactor and Optimize
- Remove antd dependencies, replace with HeroUI
- Remove styled-components, replace with Tailwind CSS
- Optimize component APIs and type definitions
## Notes
1. **Do NOT migrate** components with these dependencies (can be migrated after decoupling):
- window.api calls
- Redux (useSelector, useDispatch, etc.)
- Other external data sources
2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props)
- Components using antd (replace with HeroUI later)
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Update migration status in this document

View File

@@ -4,7 +4,7 @@
"components": "@cherrystudio/ui/components",
"hooks": "@cherrystudio/ui/hooks",
"lib": "@cherrystudio/ui/lib",
"ui": "@cherrystudio/ui/components/primitives",
"ui": "@cherrystudio/ui/components/ui",
"utils": "@cherrystudio/ui/utils"
},
"iconLibrary": "lucide",

View File

@@ -1,7 +1,6 @@
import { cn } from '@heroui/react'
import React, { memo } from 'react'
import { cn } from '../../../utils'
interface EmojiAvatarProps {
children: string
size?: number

View File

@@ -1,7 +1,6 @@
import type { AvatarProps as HeroUIAvatarProps } from '@heroui/react'
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup } from '@heroui/react'
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup, cn } from '@heroui/react'
import { cn } from '../../../utils'
import EmojiAvatar from './EmojiAvatar'
export interface AvatarProps extends Omit<HeroUIAvatarProps, 'size'> {

View File

@@ -0,0 +1,12 @@
import type { ButtonProps as HeroUIButtonProps } from '@heroui/react'
import { Button as HeroUIButton } from '@heroui/react'
export interface ButtonProps extends HeroUIButtonProps {}
const Button = ({ ...props }: ButtonProps) => {
return <HeroUIButton {...props} />
}
Button.displayName = 'Button'
export default Button

View File

@@ -1,9 +1,8 @@
// Original path: src/renderer/src/components/CopyButton.tsx
import { Tooltip } from '@heroui/react'
import { Copy } from 'lucide-react'
import type { FC } from 'react'
import { Tooltip } from './tooltip'
interface CopyButtonProps {
tooltip?: string
label?: string

View File

@@ -0,0 +1,46 @@
import { Accordion, AccordionItem, type AccordionItemProps, type AccordionProps } from '@heroui/react'
import type { FC } from 'react'
import { memo } from 'react'
// 重新导出 HeroUI 的组件,方便直接使用
export { Accordion, AccordionItem } from '@heroui/react'
interface CustomCollapseProps {
children: React.ReactNode
accordionProps?: Omit<AccordionProps, 'children'>
accordionItemProps?: Omit<AccordionItemProps, 'children'>
}
const CustomCollapse: FC<CustomCollapseProps> = ({ children, accordionProps = {}, accordionItemProps = {} }) => {
// 解构 Accordion 的 props
const {
defaultExpandedKeys = ['1'],
variant = 'bordered',
className = '',
isDisabled = false,
...restAccordionProps
} = accordionProps
// 解构 AccordionItem 的 props
const { title = 'Collapse Panel', ...restAccordionItemProps } = accordionItemProps
return (
<Accordion
defaultExpandedKeys={defaultExpandedKeys}
variant={variant}
className={className}
isDisabled={isDisabled}
selectionMode="multiple"
{...restAccordionProps}>
<AccordionItem
key="1"
aria-label={typeof title === 'string' ? title : 'collapse-item'}
title={title}
{...restAccordionItemProps}>
{children}
</AccordionItem>
</Accordion>
)
}
export default memo(CustomCollapse)

View File

@@ -1,10 +1,9 @@
// Original path: src/renderer/src/components/Tags/CustomTag.tsx
import { Tooltip } from '@heroui/react'
import { X } from 'lucide-react'
import type { CSSProperties, FC, MouseEventHandler } from 'react'
import { memo, useMemo } from 'react'
import { Tooltip } from './tooltip'
export interface CustomTagProps {
icon?: React.ReactNode
children?: React.ReactNode | string

View File

@@ -1,10 +1,10 @@
// Original path: src/renderer/src/components/ErrorBoundary.tsx
import { Button } from '@heroui/react'
import { AlertTriangle } from 'lucide-react'
import type { ComponentType, ReactNode } from 'react'
import type { FallbackProps } from 'react-error-boundary'
import { ErrorBoundary } from 'react-error-boundary'
import { Button } from '../button'
import { formatErrorMessage } from './utils'
interface CustomFallbackProps extends FallbackProps {
@@ -35,12 +35,12 @@ const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallba
<p className="text-red-700 dark:text-red-300 text-sm mb-3">{formatErrorMessage(error)}</p>
<div className="flex gap-2">
{onDebugClick && (
<Button size="sm" variant="destructive" onClick={onDebugClick}>
<Button size="sm" variant="flat" color="danger" onPress={onDebugClick}>
{debugButtonText}
</Button>
)}
{onReloadClick && (
<Button size="sm" variant="destructive" onClick={onReloadClick}>
<Button size="sm" variant="flat" color="danger" onPress={onReloadClick}>
{reloadButtonText}
</Button>
)}

View File

@@ -0,0 +1,333 @@
# Selector 组件
基于 HeroUI Select 封装的下拉选择组件,简化了 Set 和 Selection 的转换逻辑。
## 核心特性
-**类型安全**: 单选和多选自动推断回调类型
-**智能转换**: 自动处理 `Set<Key>` 和原始值的转换
-**HeroUI 风格**: 保持与 HeroUI 生态一致的 API
-**支持数字和字符串**: 泛型支持,自动识别值类型
## 基础用法
### 单选模式(默认)
```tsx
import { Selector } from '@cherrystudio/ui'
import { useState } from 'react'
function Example() {
const [language, setLanguage] = useState('zh-CN')
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' }
]
return (
<Selector
selectedKeys={language}
onSelectionChange={(value) => {
// value 类型自动推断为 string
setLanguage(value)
}}
items={languageOptions}
placeholder="选择语言"
/>
)
}
```
### 多选模式
```tsx
import { Selector } from '@cherrystudio/ui'
import { useState } from 'react'
function Example() {
const [languages, setLanguages] = useState(['zh-CN', 'en-US'])
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' },
{ label: 'Français', value: 'fr-FR' }
]
return (
<Selector
selectionMode="multiple"
selectedKeys={languages}
onSelectionChange={(values) => {
// values 类型自动推断为 string[]
setLanguages(values)
}}
items={languageOptions}
placeholder="选择语言"
/>
)
}
```
### 数字类型值
```tsx
import { Selector } from '@cherrystudio/ui'
function Example() {
const [priority, setPriority] = useState<number>(1)
const priorityOptions = [
{ label: '低', value: 1 },
{ label: '中', value: 2 },
{ label: '高', value: 3 }
]
return (
<Selector<number>
selectedKeys={priority}
onSelectionChange={(value) => {
// value 类型为 number
setPriority(value)
}}
items={priorityOptions}
/>
)
}
```
### 禁用选项
```tsx
const options = [
{ label: '选项 1', value: '1' },
{ label: '选项 2 (禁用)', value: '2', disabled: true },
{ label: '选项 3', value: '3' }
]
<Selector
selectedKeys="1"
onSelectionChange={handleChange}
items={options}
/>
```
### 自定义 Label
```tsx
import { Flex } from '@cherrystudio/ui'
const options = [
{
label: (
<Flex className="items-center gap-2">
<span>🇨🇳</span>
<span></span>
</Flex>
),
value: 'zh-CN'
},
{
label: (
<Flex className="items-center gap-2">
<span>🇺🇸</span>
<span>English</span>
</Flex>
),
value: 'en-US'
}
]
<Selector
selectedKeys="zh-CN"
onSelectionChange={handleChange}
items={options}
/>
```
## API
### SelectorProps
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `items` | `SelectorItem<V>[]` | - | 必填,选项列表 |
| `selectedKeys` | `V` \| `V[]` | - | 受控的选中值(单选为单个值,多选为数组) |
| `onSelectionChange` | `(key: V) => void` \| `(keys: V[]) => void` | - | 选择变化回调(类型根据 selectionMode 自动推断) |
| `selectionMode` | `'single'` \| `'multiple'` | `'single'` | 选择模式 |
| `placeholder` | `string` | - | 占位文本 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `isRequired` | `boolean` | `false` | 是否必填 |
| `label` | `ReactNode` | - | 标签文本 |
| `description` | `ReactNode` | - | 描述文本 |
| `errorMessage` | `ReactNode` | - | 错误提示 |
| ...rest | `SelectProps` | - | 其他 HeroUI Select 属性 |
### SelectorItem
```tsx
interface SelectorItem<V = string | number> {
label: string | ReactNode // 显示文本或自定义内容
value: V // 选项值
disabled?: boolean // 是否禁用
[key: string]: any // 其他自定义属性
}
```
## 类型安全
组件使用 TypeScript 条件类型,根据 `selectionMode` 自动推断回调类型:
```tsx
// 单选模式
<Selector
selectionMode="single" // 或省略(默认单选)
selectedKeys={value} // 类型: V
onSelectionChange={(v) => ...} // v 类型: V
/>
// 多选模式
<Selector
selectionMode="multiple"
selectedKeys={values} // 类型: V[]
onSelectionChange={(vs) => ...} // vs 类型: V[]
/>
```
## 与 HeroUI Select 的区别
| 特性 | HeroUI Select | Selector (本组件) |
|------|---------------|------------------|
| `selectedKeys` | `Set<Key> \| 'all'` | `V` \| `V[]` (自动转换) |
| `onSelectionChange` | `(keys: Selection) => void` | `(key: V) => void` \| `(keys: V[]) => void` |
| 单选回调 | 返回 `Set` (需手动提取) | 直接返回单个值 |
| 多选回调 | 返回 `Set` (需转数组) | 直接返回数组 |
| 类型推断 | 无 | 根据 selectionMode 自动推断 |
## 最佳实践
### 1. 显式声明 selectionMode
虽然单选是默认模式,但建议显式声明以提高代码可读性:
```tsx
// ✅ 推荐
<Selector selectionMode="single" ... />
// ⚠️ 可以但不够清晰
<Selector ... />
```
### 2. 使用泛型指定值类型
当值类型为数字或联合类型时,使用泛型获得更好的类型提示:
```tsx
// ✅ 推荐
<Selector<number> selectedKeys={priority} ... />
// ✅ 推荐(联合类型)
type Status = 'pending' | 'approved' | 'rejected'
<Selector<Status> selectedKeys={status} ... />
```
### 3. 避免在渲染时创建 items
```tsx
// ❌ 不推荐(每次渲染都创建新数组)
<Selector items={[{ label: 'A', value: '1' }]} />
// ✅ 推荐(在组件外或使用 useMemo
const items = [{ label: 'A', value: '1' }]
<Selector items={items} />
```
## 迁移指南
### 从 antd Select 迁移
```tsx
// antd Select
import { Select } from 'antd'
<Select
value={value}
onChange={(value) => onChange(value)}
options={[
{ label: 'A', value: '1' },
{ label: 'B', value: '2' }
]}
/>
// 迁移到 Selector
import { Selector } from '@cherrystudio/ui'
<Selector
selectedKeys={value} // value → selectedKeys
onSelectionChange={(value) => onChange(value)} // onChange → onSelectionChange
items={[ // options → items
{ label: 'A', value: '1' },
{ label: 'B', value: '2' }
]}
/>
```
### 从旧版 Selector 迁移
```tsx
// 旧版 Selector (返回数组)
<Selector
onSelectionChange={(values) => {
const value = values[0] // 需要手动提取
onChange(value)
}}
/>
// 新版 Selector (直接返回值)
<Selector
selectionMode="single"
onSelectionChange={(value) => {
onChange(value) // 直接使用
}}
/>
```
## 常见问题
### Q: 为什么单选模式下还需要 selectedKeys 而不是 selectedKey
A: 为了保持与 HeroUI API 命名的一致性,同时简化组件实现。组件内部会自动处理单个值和 Set 的转换。
### Q: 如何清空选择?
```tsx
// 单选模式
<Selector
selectedKeys={value}
onSelectionChange={setValue}
isClearable // 添加清空按钮
/>
// 或手动设置为 undefined
setValue(undefined)
```
### Q: 支持异步加载选项吗?
支持,配合 `isLoading` 属性使用:
```tsx
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchItems().then(data => {
setItems(data)
setLoading(false)
})
}, [])
<Selector items={items} isLoading={loading} />
```

View File

@@ -0,0 +1,53 @@
import type { LucideIcon } from 'lucide-react'
import { AlertTriangleIcon, CheckIcon, CircleXIcon, InfoIcon } from 'lucide-react'
import React from 'react'
import CustomTag from '../CustomTag'
export type StatusType = 'success' | 'error' | 'warning' | 'info'
export interface StatusTagProps {
type: StatusType
message: string
iconSize?: number
icon?: React.ReactNode
color?: string
className?: string
}
const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
success: { Icon: CheckIcon, color: '#10B981' }, // green-500
error: { Icon: CircleXIcon, color: '#EF4444' }, // red-500
warning: { Icon: AlertTriangleIcon, color: '#F59E0B' }, // amber-500
info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500
}
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color, className }) => {
const config = statusConfig[type]
const Icon = config.Icon
const finalColor = color || config.color
const finalIcon = icon || <Icon size={iconSize} color={finalColor} />
return (
<CustomTag icon={finalIcon} color={finalColor} className={className}>
{message}
</CustomTag>
)
}
// 保留原有的导出以保持向后兼容
export const SuccessTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="success" iconSize={iconSize} message={message} />
)
export const ErrorTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="error" iconSize={iconSize} message={message} />
)
export const WarnTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="warning" iconSize={iconSize} message={message} />
)
export const InfoTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="info" iconSize={iconSize} message={message} />
)

View File

@@ -0,0 +1,20 @@
// Original: src/renderer/src/components/TextBadge.tsx
import type { FC } from 'react'
interface TextBadgeProps {
text: string
style?: React.CSSProperties
className?: string
}
const TextBadge: FC<TextBadgeProps> = ({ text, style, className = '' }) => {
return (
<span
className={`text-xs text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 px-1.5 py-0.5 rounded font-medium ${className}`}
style={style}>
{text}
</span>
)
}
export default TextBadge

View File

@@ -1,26 +1,33 @@
// Primitive Components
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './primitives/Avatar'
export { default as CopyButton } from './primitives/copyButton'
export { default as CustomTag } from './primitives/customTag'
export { default as DividerWithText } from './primitives/dividerWithText'
export { default as EmojiIcon } from './primitives/emojiIcon'
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './primitives/ErrorBoundary'
export { ErrorBoundary } from './primitives/ErrorBoundary'
export { default as IndicatorLight } from './primitives/indicatorLight'
export { default as Spinner } from './primitives/spinner'
export { DescriptionSwitch, Switch } from './primitives/switch'
export { getToastUtilities, type ToastUtilities } from './primitives/toast'
export { Tooltip, type TooltipProps } from './primitives/tooltip'
// Base Components
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './base/Avatar'
export { default as Button, type ButtonProps } from './base/Button'
export { default as CopyButton } from './base/CopyButton'
export { default as CustomCollapse } from './base/CustomCollapse'
export { default as CustomTag } from './base/CustomTag'
export { default as DividerWithText } from './base/DividerWithText'
export { default as EmojiIcon } from './base/EmojiIcon'
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './base/ErrorBoundary'
export { ErrorBoundary } from './base/ErrorBoundary'
export { default as IndicatorLight } from './base/IndicatorLight'
export { default as Spinner } from './base/Spinner'
export type { StatusTagProps, StatusType } from './base/StatusTag'
export { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from './base/StatusTag'
export { DescriptionSwitch, Switch } from './base/Switch'
export { default as TextBadge } from './base/TextBadge'
export { getToastUtilities, type ToastUtilities } from './base/Toast'
export { Tooltip, type TooltipProps } from './base/Tooltip'
// Composite Components
export { default as Ellipsis } from './composites/Ellipsis'
export { default as ExpandableText } from './composites/ExpandableText'
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './composites/Flex'
export { default as HorizontalScrollContainer } from './composites/HorizontalScrollContainer'
export { default as ListItem } from './composites/ListItem'
export { default as MaxContextCount } from './composites/MaxContextCount'
export { default as Scrollbar } from './composites/Scrollbar'
export { default as ThinkingEffect } from './composites/ThinkingEffect'
// Display Components
export { default as Ellipsis } from './display/Ellipsis'
export { default as ExpandableText } from './display/ExpandableText'
export { default as ListItem } from './display/ListItem'
export { default as MaxContextCount } from './display/MaxContextCount'
export { default as ThinkingEffect } from './display/ThinkingEffect'
// Layout Components
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './layout/Flex'
export { default as HorizontalScrollContainer } from './layout/HorizontalScrollContainer'
export { default as Scrollbar } from './layout/Scrollbar'
// Icon Components
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
@@ -42,9 +49,11 @@ export {
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
/* Selector Components */
export { default as Selector } from './primitives/Selector'
export { default as SearchableSelector } from './primitives/Selector/SearchableSelector'
/* Interactive Components */
// Selector / SearchableSelector
export { default as Selector } from './base/Selector'
export { default as SearchableSelector } from './base/Selector/SearchableSelector'
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
@@ -54,9 +63,7 @@ export type {
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
} from './primitives/Selector/types'
/* Additional Composite Components */
} from './base/Selector/types'
// CodeEditor
export {
default as CodeEditor,
@@ -65,27 +72,28 @@ export {
type CodeMirrorTheme,
getCmThemeByName,
getCmThemeNames
} from './composites/CodeEditor'
} from './interactive/CodeEditor'
// CollapsibleSearchBar
export { default as CollapsibleSearchBar } from './composites/CollapsibleSearchBar'
export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar'
// DraggableList
export { DraggableList, useDraggableReorder } from './composites/DraggableList'
export { DraggableList, useDraggableReorder } from './interactive/DraggableList'
// EditableNumber
export type { EditableNumberProps } from './composites/EditableNumber'
export { default as EditableNumber } from './composites/EditableNumber'
export type { EditableNumberProps } from './interactive/EditableNumber'
// EditableNumber
export { default as EditableNumber } from './interactive/EditableNumber'
// Tooltip variants
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './composites/IconTooltips'
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './interactive/IconTooltips'
// ImageToolButton
export { default as ImageToolButton } from './composites/ImageToolButton'
export { default as ImageToolButton } from './interactive/ImageToolButton'
// Sortable
export { Sortable } from './composites/Sortable'
export { Sortable } from './interactive/Sortable'
/* Shadcn Primitive Components */
export * from './primitives/button'
export * from './primitives/command'
export * from './primitives/dialog'
export * from './primitives/popover'
export * from './primitives/radioGroup'
export * from './primitives/shadcn-io/dropzone'
export * from './primitives/shadcn-io/skeleton'
export * from './primitives/shadcn-io/tabs'
// Composite Components (复合组件)
// 暂无复合组件
// UI Components (shadcn)
export * from './ui/button'
export * from './ui/command'
export * from './ui/dialog'
export * from './ui/popover'
export * from './ui/shadcn-io/dropzone'

View File

@@ -5,7 +5,7 @@ import { Search } from 'lucide-react'
import { motion } from 'motion/react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { Tooltip } from '../../primitives/tooltip'
import { Tooltip } from '../../base/Tooltip'
interface CollapsibleSearchBarProps {
onSearch: (text: string) => void

View File

@@ -9,7 +9,7 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { type ScrollToOptions, useVirtualizer, type VirtualItem } from '@tanstack/react-virtual'
import { type Key, memo, useCallback, useImperativeHandle, useRef } from 'react'
import Scrollbar from '../Scrollbar'
import Scrollbar from '../../layout/Scrollbar'
import { droppableReorder } from './sort'
export interface DraggableVirtualListRef {

View File

@@ -1,7 +1,7 @@
// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx
import { HelpCircle } from 'lucide-react'
import { Tooltip } from '../../primitives/tooltip'
import { Tooltip } from '../../base/Tooltip'
import type { IconTooltipProps } from './types'
export const HelpTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {

View File

@@ -1,7 +1,7 @@
// Original: src/renderer/src/components/TooltipIcons/InfoTooltip.tsx
import { Info } from 'lucide-react'
import { Tooltip } from '../../primitives/tooltip'
import { Tooltip } from '../../base/Tooltip'
import type { IconTooltipProps } from './types'
export const InfoTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {

View File

@@ -1,7 +1,7 @@
// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx
import { AlertTriangle } from 'lucide-react'
import { Tooltip } from '../../primitives/tooltip'
import { Tooltip } from '../../base/Tooltip'
import type { IconTooltipProps } from './types'
export const WarnTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {

View File

@@ -1,6 +1,6 @@
import type { LucideProps } from 'lucide-react'
import type { TooltipProps } from '../../primitives/tooltip'
import type { TooltipProps } from '../../base/Tooltip'
export interface IconTooltipProps extends TooltipProps {
iconProps?: LucideProps

View File

@@ -1,8 +1,8 @@
// Original path: src/renderer/src/components/Preview/ImageToolButton.tsx
import { memo } from 'react'
import { Button } from '../../primitives/button'
import { Tooltip } from '../../primitives/tooltip'
import Button from '../../base/Button'
import { Tooltip } from '../../base/Tooltip'
interface ImageToolButtonProps {
tooltip: string
@@ -13,7 +13,7 @@ interface ImageToolButtonProps {
const ImageToolButton = ({ tooltip, icon, onPress }: ImageToolButtonProps) => {
return (
<Tooltip content={tooltip} delay={500} closeDelay={0}>
<Button size="icon" className="rounded-full" onClick={onPress} aria-label={tooltip}>
<Button radius="full" isIconOnly onPress={onPress} aria-label={tooltip}>
{icon}
</Button>
</Tooltip>

View File

@@ -1,7 +0,0 @@
import { cn } from '@cherrystudio/ui/utils'
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="skeleton" className={cn('bg-accent animate-pulse rounded-md', className)} {...props} />
}
export { Skeleton }

View File

@@ -1,39 +0,0 @@
import { cn } from '@cherrystudio/ui/utils'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import * as React from 'react'
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return <TabsPrimitive.Root data-slot="tabs" className={cn('flex flex-col gap-2', className)} {...props} />
}
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
className
)}
{...props}
/>
)
}
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return <TabsPrimitive.Content data-slot="tabs-content" className={cn('flex-1 outline-none', className)} {...props} />
}
export { Tabs, TabsContent, TabsList, TabsTrigger }

View File

@@ -4,7 +4,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle
} from '@cherrystudio/ui/components/primitives/dialog'
} from '@cherrystudio/ui/components/ui/dialog'
import { cn } from '@cherrystudio/ui/utils'
import { Command as CommandPrimitive } from 'cmdk'
import { SearchIcon } from 'lucide-react'

View File

@@ -1,6 +1,6 @@
'use client'
import { Button } from '@cherrystudio/ui/components/primitives/button'
import { Button } from '@cherrystudio/ui/components/ui/button'
import { cn } from '@cherrystudio/ui/utils/index'
import { UploadIcon } from 'lucide-react'
import type { ReactNode } from 'react'

View File

@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import { Button } from '../../../src/components'
const meta: Meta<typeof Button> = {
title: 'Components/Primitives/Button',
title: 'Components/Base/Button',
component: Button,
parameters: {
layout: 'centered'
@@ -12,16 +12,30 @@ const meta: Meta<typeof Button> = {
argTypes: {
variant: {
control: { type: 'select' },
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']
options: ['solid', 'bordered', 'light', 'flat', 'faded', 'shadow', 'ghost']
},
color: {
control: { type: 'select' },
options: ['default', 'primary', 'secondary', 'success', 'warning', 'danger']
},
size: {
control: { type: 'select' },
options: ['default', 'sm', 'lg', 'icon', 'icon-sm', 'icon-lg']
options: ['sm', 'md', 'lg']
},
disabled: {
radius: {
control: { type: 'select' },
options: ['none', 'sm', 'md', 'lg', 'full']
},
isDisabled: {
control: { type: 'boolean' }
},
asChild: {
isLoading: {
control: { type: 'boolean' }
},
fullWidth: {
control: { type: 'boolean' }
},
isIconOnly: {
control: { type: 'boolean' }
}
}
@@ -41,12 +55,27 @@ export const Default: Story = {
export const Variants: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="solid">Solid</Button>
<Button variant="bordered">Bordered</Button>
<Button variant="light">Light</Button>
<Button variant="flat">Flat</Button>
<Button variant="faded">Faded</Button>
<Button variant="shadow">Shadow</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
)
}
// 不同颜色
export const Colors: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button color="default">Default</Button>
<Button color="primary">Primary</Button>
<Button color="secondary">Secondary</Button>
<Button color="success">Success</Button>
<Button color="warning">Warning</Button>
<Button color="danger">Danger</Button>
</div>
)
}
@@ -54,21 +83,23 @@ export const Variants: Story = {
// 不同尺寸
export const Sizes: Story = {
render: () => (
<div className="flex gap-2 items-center flex-wrap">
<div className="flex gap-2 items-center">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
)
}
// 图标按钮
export const IconButtons: Story = {
// 不同圆角
export const Radius: Story = {
render: () => (
<div className="flex gap-2 items-center flex-wrap">
<Button size="icon-sm">🔍</Button>
<Button size="icon">🔍</Button>
<Button size="icon-lg">🔍</Button>
<div className="flex gap-2 flex-wrap">
<Button radius="none">None</Button>
<Button radius="sm">Small</Button>
<Button radius="md">Medium</Button>
<Button radius="lg">Large</Button>
<Button radius="full">Full</Button>
</div>
)
}
@@ -78,7 +109,8 @@ export const States: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button>Normal</Button>
<Button disabled>Disabled</Button>
<Button isDisabled>Disabled</Button>
<Button isLoading>Loading</Button>
</div>
)
}
@@ -87,15 +119,9 @@ export const States: Story = {
export const WithIcons: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button>
<span className="mr-2">📧</span>
Email
</Button>
<Button>
Next
<span className="ml-2"></span>
</Button>
<Button size="icon">🔍</Button>
<Button startContent={<span>📧</span>}>Email</Button>
<Button endContent={<span></span>}>Next</Button>
<Button isIconOnly>🔍</Button>
</div>
)
}
@@ -104,7 +130,9 @@ export const WithIcons: Story = {
export const FullWidth: Story = {
render: () => (
<div className="w-96">
<Button className="w-full">Full Width Button</Button>
<Button fullWidth color="primary">
Full Width Button
</Button>
</div>
)
}
@@ -113,45 +141,10 @@ export const FullWidth: Story = {
export const Interactive: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button onClick={() => alert('Button clicked!')}>Click Me</Button>
<Button onClick={() => console.log('Primary action')} variant="default">
<Button onPress={() => alert('Button pressed!')}>Click Me</Button>
<Button onPress={() => console.log('Primary action')} color="primary" variant="solid">
Primary Action
</Button>
</div>
)
}
// 组合示例
export const Combinations: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex gap-2 flex-wrap">
<Button variant="default" size="sm">
Small Default
</Button>
<Button variant="destructive" size="sm">
Small Destructive
</Button>
<Button variant="outline" size="sm">
Small Outline
</Button>
</div>
<div className="flex gap-2 flex-wrap">
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
</div>
<div className="flex gap-2 flex-wrap">
<Button variant="default" size="lg">
Large Default
</Button>
<Button variant="destructive" size="lg">
Large Destructive
</Button>
<Button variant="outline" size="lg">
Large Outline
</Button>
</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import CopyButton from '../../../src/components/primitives/copyButton'
import CopyButton from '../../../src/components/base/CopyButton'
const meta: Meta<typeof CopyButton> = {
title: 'Base/CopyButton',

View File

@@ -0,0 +1,459 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { AlertTriangle, CreditCard, Info, Monitor, Settings, Shield } from 'lucide-react'
import { useState } from 'react'
import CustomCollapse, { Accordion, AccordionItem } from '../../../src/components/base/CustomCollapse'
const meta: Meta<typeof CustomCollapse> = {
title: 'Base/CustomCollapse',
component: CustomCollapse,
parameters: {
layout: 'padded'
},
tags: ['autodocs'],
argTypes: {
children: {
control: false,
description: '面板内容'
},
accordionProps: {
control: false,
description: 'Accordion 组件的属性'
},
accordionItemProps: {
control: false,
description: 'AccordionItem 组件的属性'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// 基础用法
export const Default: Story = {
args: {
accordionItemProps: {
title: '默认折叠面板'
},
children: (
<div className="p-4">
<p></p>
<p></p>
</div>
)
}
}
// 带副标题
export const WithSubtitle: Story = {
args: {
accordionProps: {
defaultExpandedKeys: ['1']
},
accordionItemProps: {
title: '带副标题的折叠面板',
subtitle: <span className="text-sm text-gray-500"></span>
},
children: (
<div className="p-4">
<p></p>
<p> subtitle </p>
</div>
)
}
}
// HeroUI 样式变体
export const VariantLight: Story = {
args: {
accordionProps: {
variant: 'light'
},
accordionItemProps: {
title: 'Light 变体'
},
children: (
<div className="p-4">
<p> HeroUI Light </p>
</div>
)
}
}
export const VariantShadow: Story = {
args: {
accordionProps: {
variant: 'shadow',
className: 'p-2'
},
accordionItemProps: {
title: 'Shadow 变体',
subtitle: '带阴影的面板样式'
},
children: (
<div className="p-4">
<p> HeroUI Shadow </p>
</div>
)
}
}
export const VariantBordered: Story = {
args: {
accordionProps: {
variant: 'bordered'
},
accordionItemProps: {
title: 'Bordered 变体(默认)'
},
children: (
<div className="p-4">
<p> HeroUI Bordered </p>
</div>
)
}
}
export const VariantSplitted: Story = {
args: {
accordionProps: {
variant: 'splitted'
},
accordionItemProps: {
title: 'Splitted 变体'
},
children: (
<div className="p-4">
<p> HeroUI Splitted </p>
</div>
)
}
}
// 富内容标题
export const RichLabel: Story = {
args: {
accordionItemProps: {
title: (
<div className="flex items-center gap-2">
<Settings className="text-default-500" size={20} />
<span></span>
</div>
)
},
children: (
<div className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span></span>
<Button size="sm" variant="flat">
</Button>
</div>
<div className="flex items-center justify-between">
<span></span>
<Button size="sm" variant="flat">
</Button>
</div>
</div>
</div>
)
}
}
// 带警告提示
export const WithWarning: Story = {
args: {
accordionItemProps: {
title: (
<div className="flex items-center gap-2">
<Monitor className="text-primary" size={20} />
<span></span>
</div>
),
subtitle: (
<p className="flex">
2<span className="text-primary ml-1"></span>
</p>
)
},
children: (
<div className="p-4">
<p className="text-small"></p>
<ul className="list-disc list-inside mt-2 text-small space-y-1">
<li></li>
<li></li>
</ul>
</div>
)
}
}
// 禁用状态
export const Disabled: Story = {
args: {
accordionProps: {
isDisabled: true,
defaultExpandedKeys: ['1']
},
accordionItemProps: {
title: '禁用的折叠面板'
},
children: (
<div className="p-4">
<p></p>
</div>
)
}
}
// 受控模式
export const ControlledMode: Story = {
render: function ControlledMode() {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['1']))
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button size="sm" onPress={() => setSelectedKeys(new Set(['1']))} color="primary">
</Button>
<Button size="sm" onPress={() => setSelectedKeys(new Set())} color="default">
</Button>
</div>
<CustomCollapse
accordionProps={{
selectedKeys,
onSelectionChange: (keys) => {
if (keys !== 'all') {
setSelectedKeys(keys as Set<string>)
}
}
}}
accordionItemProps={{
title: '受控的折叠面板'
}}>
<div className="p-4">
<p></p>
<p></p>
</div>
</CustomCollapse>
<div className="text-sm text-gray-600">{selectedKeys.size > 0 ? '展开' : '收起'}</div>
</div>
)
}
}
// 多个单面板组合
export const MultipleSinglePanels: Story = {
render: () => (
<div className="space-y-4">
<CustomCollapse accordionProps={{ defaultExpandedKeys: ['1'] }} accordionItemProps={{ title: '第一个面板' }}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
<CustomCollapse
accordionItemProps={{
title: '第二个面板',
subtitle: '带副标题'
}}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
<CustomCollapse accordionProps={{ isDisabled: true }} accordionItemProps={{ title: '第三个面板(禁用)' }}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
</div>
)
}
// 使用原生 HeroUI Accordion 的多面板示例
export const NativeAccordionMultiple: Story = {
render: () => (
<div className="max-w-lg">
<h3 className="text-lg font-medium mb-4"> HeroUI Accordion </h3>
<Accordion variant="shadow" className="p-2 flex flex-col gap-1" defaultExpandedKeys={['1']}>
<AccordionItem
key="1"
title="连接的设备"
startContent={<Monitor className="text-primary" size={20} />}
subtitle={
<p className="flex">
2<span className="text-primary ml-1"></span>
</p>
}>
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="2"
title="应用权限"
startContent={<Shield className="text-default-500" size={20} />}
subtitle="3个应用有读取权限">
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="3"
title="待办任务"
startContent={<Info className="text-warning" size={20} />}
subtitle={<span className="text-warning"></span>}>
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="4"
title={
<p className="flex gap-1 items-center">
<span className="text-default-400 text-small">*4812</span>
</p>
}
startContent={<CreditCard className="text-danger" size={20} />}
subtitle={<span className="text-danger"></span>}>
<div className="p-4">
<p className="text-small text-danger"></p>
</div>
</AccordionItem>
</Accordion>
</div>
)
}
// 富内容面板
export const RichContent: Story = {
args: {
accordionItemProps: {
title: (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<Info className="text-default-500" size={20} />
<span></span>
</div>
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
<Button size="sm" variant="flat" color="primary">
</Button>
<Button size="sm" variant="flat">
</Button>
</div>
</div>
)
},
children: (
<div className="p-4 space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="请输入名称"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md">
<option></option>
<option> A</option>
<option> B</option>
</select>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} placeholder="请输入描述" />
</div>
</div>
)
}
}
// 自定义样式
export const CustomStyles: Story = {
args: {
accordionProps: {
style: {
backgroundColor: 'rgba(255, 193, 7, 0.1)',
borderColor: 'var(--color-warning)'
}
},
accordionItemProps: {
title: (
<div className="flex items-center gap-2">
<AlertTriangle className="text-warning" size={16} />
<span></span>
</div>
)
},
children: (
<div className="p-4 bg-warning-50 dark:bg-warning-900/20">
<p className="text-warning-800 dark:text-warning-200"></p>
</div>
)
}
}
// 原生 HeroUI Accordion 多面板受控模式
export const NativeAccordionControlled: Story = {
render: function NativeAccordionControlled() {
const [activeKeys, setActiveKeys] = useState<Set<string>>(new Set(['1']))
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button size="sm" onPress={() => setActiveKeys(new Set(['1', '2', '3']))} color="primary">
</Button>
<Button size="sm" onPress={() => setActiveKeys(new Set())} color="default">
</Button>
<Button size="sm" onPress={() => setActiveKeys(new Set(['2']))} color="default">
</Button>
</div>
<Accordion
selectedKeys={activeKeys}
onSelectionChange={(keys) => {
if (keys !== 'all') {
setActiveKeys(keys as Set<string>)
}
}}>
<AccordionItem key="1" title="受控面板 1">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
<AccordionItem key="2" title="受控面板 2">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
<AccordionItem key="3" title="受控面板 3">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
</Accordion>
<div className="text-sm text-gray-600">
{activeKeys.size > 0 ? Array.from(activeKeys).join(', ') : '无'}
</div>
</div>
)
}
}

View File

@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import { AlertTriangleIcon, StarIcon } from 'lucide-react'
import { action } from 'storybook/actions'
import CustomTag from '../../../src/components/primitives/customTag'
import CustomTag from '../../../src/components/base/CustomTag'
const meta: Meta<typeof CustomTag> = {
title: 'Base/CustomTag',

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import DividerWithText from '../../../src/components/primitives/dividerWithText'
import DividerWithText from '../../../src/components/base/DividerWithText'
const meta: Meta<typeof DividerWithText> = {
title: 'Base/DividerWithText',

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import EmojiAvatar from '../../../src/components/primitives/Avatar/EmojiAvatar'
import EmojiAvatar from '../../../src/components/base/Avatar/EmojiAvatar'
const meta: Meta<typeof EmojiAvatar> = {
title: 'Display/EmojiAvatar',

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import EmojiIcon from '../../../src/components/primitives/emojiIcon'
import EmojiIcon from '../../../src/components/base/EmojiIcon'
const meta: Meta<typeof EmojiIcon> = {
title: 'Base/EmojiIcon',

View File

@@ -2,8 +2,8 @@ import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import type { CustomFallbackProps } from '../../../src/components/primitives/ErrorBoundary'
import { ErrorBoundary } from '../../../src/components/primitives/ErrorBoundary'
import type { CustomFallbackProps } from '../../../src/components/base/ErrorBoundary'
import { ErrorBoundary } from '../../../src/components/base/ErrorBoundary'
// 错误组件 - 用于触发错误
const ThrowErrorComponent = ({ shouldThrow = false, errorMessage = '这是一个模拟错误' }) => {

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import IndicatorLight from '../../../src/components/primitives/indicatorLight'
import IndicatorLight from '../../../src/components/base/IndicatorLight'
const meta: Meta<typeof IndicatorLight> = {
title: 'Base/IndicatorLight',

View File

@@ -2,7 +2,7 @@ import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import Spinner from '../../../src/components/primitives/spinner'
import Spinner from '../../../src/components/base/Spinner'
const meta: Meta<typeof Spinner> = {
title: 'Base/Spinner',

View File

@@ -0,0 +1,176 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from '../../../src/components/base/StatusTag'
const meta: Meta<typeof StatusTag> = {
title: 'Base/StatusTag',
component: StatusTag,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
type: {
control: { type: 'select' },
options: ['success', 'error', 'warning', 'info']
},
iconSize: { control: { type: 'range', min: 10, max: 24, step: 1 } },
message: { control: 'text' },
color: { control: 'color' }
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
args: {
type: 'success',
message: 'Success'
}
}
// All Types
export const AllTypes: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusTag type="success" message="Success message" />
<StatusTag type="error" message="Error message" />
<StatusTag type="warning" message="Warning message" />
<StatusTag type="info" message="Info message" />
</div>
)
}
// Convenience Components
export const ConvenienceComponents: Story = {
render: () => (
<div className="flex flex-col gap-3">
<SuccessTag message="Operation completed" />
<ErrorTag message="Operation failed" />
<WarnTag message="Please check this" />
<InfoTag message="Additional information" />
</div>
)
}
// Different Icon Sizes
export const IconSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<StatusTag type="success" iconSize={10} message="Small icon" />
<StatusTag type="success" iconSize={14} message="Default icon" />
<StatusTag type="success" iconSize={18} message="Large icon" />
<StatusTag type="success" iconSize={24} message="Extra large icon" />
</div>
<div className="flex items-center gap-4">
<ErrorTag iconSize={10} message="Small icon" />
<ErrorTag iconSize={14} message="Default icon" />
<ErrorTag iconSize={18} message="Large icon" />
<ErrorTag iconSize={24} message="Extra large icon" />
</div>
</div>
)
}
// Custom Colors
export const CustomColors: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusTag type="success" message="Custom purple" color="#8B5CF6" />
<StatusTag type="error" message="Custom blue" color="#3B82F6" />
<StatusTag type="warning" message="Custom green" color="#10B981" />
<StatusTag type="info" message="Custom pink" color="#EC4899" />
</div>
)
}
// In Context
export const InContext: Story = {
render: () => (
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">Form Submission</h3>
<p className="mb-3 text-sm text-gray-600">Your form has been processed.</p>
<SuccessTag message="Form submitted successfully" />
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">Validation Error</h3>
<p className="mb-3 text-sm text-gray-600">Please fix the following issues:</p>
<ErrorTag message="Invalid email format" />
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold">System Status</h3>
<div className="space-y-2">
<SuccessTag message="Database connected" />
<WarnTag message="High memory usage" />
<ErrorTag message="Email service down" />
<InfoTag message="Last backup: 2 hours ago" />
</div>
</div>
</div>
)
}
// Use Cases
export const UseCases: Story = {
render: () => (
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<h4 className="font-medium">Success States</h4>
<div className="space-y-2">
<SuccessTag message="Saved" />
<SuccessTag message="Published" />
<SuccessTag message="Deployed" />
<SuccessTag message="Verified" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Error States</h4>
<div className="space-y-2">
<ErrorTag message="Failed" />
<ErrorTag message="Timeout" />
<ErrorTag message="Not found" />
<ErrorTag message="Access denied" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Warning States</h4>
<div className="space-y-2">
<WarnTag message="Deprecated" />
<WarnTag message="Limited" />
<WarnTag message="Expiring soon" />
<WarnTag message="Low balance" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Info States</h4>
<div className="space-y-2">
<InfoTag message="New" />
<InfoTag message="Beta" />
<InfoTag message="Preview" />
<InfoTag message="Optional" />
</div>
</div>
</div>
)
}
// Long Messages
export const LongMessages: Story = {
render: () => (
<div className="max-w-md space-y-3">
<SuccessTag message="Your request has been successfully processed and saved to the database" />
<ErrorTag message="Unable to connect to the server. Please check your network connection and try again" />
<WarnTag message="This feature will be deprecated in the next major version. Please migrate to the new API" />
<InfoTag message="Additional information about this feature can be found in the documentation at docs.example.com" />
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More