Compare commits
46 Commits
feat/termi
...
feat/reset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e0c96d118 | ||
|
|
60b7795d16 | ||
|
|
9df850b226 | ||
|
|
44378cc879 | ||
|
|
50edf016f6 | ||
|
|
c49c592175 | ||
|
|
4437871644 | ||
|
|
e59dfdd27d | ||
|
|
ac917e12f2 | ||
|
|
7ecfbce5a5 | ||
|
|
e6e20d2d72 | ||
|
|
5101488d65 | ||
|
|
dc06c103e0 | ||
|
|
7c0b03dbdc | ||
|
|
1f0381aebe | ||
|
|
fb02a61a48 | ||
|
|
562fbb3ff7 | ||
|
|
1018ad87b8 | ||
|
|
82ca35fc29 | ||
|
|
fe53b0914a | ||
|
|
67a379641f | ||
|
|
b3dc2d0422 | ||
|
|
1828ef8997 | ||
|
|
9dbc6fbf67 | ||
|
|
b3f88a7fc2 | ||
|
|
8da43ab794 | ||
|
|
2a06c606e1 | ||
|
|
b6dcf2f5fa | ||
|
|
2c07ea0dd8 | ||
|
|
68e0d8b0f1 | ||
|
|
6042ee8ca8 | ||
|
|
d164d7c8bf | ||
|
|
7f1c234ac1 | ||
|
|
c1fd23742f | ||
|
|
d792bf7fe0 | ||
|
|
f8a599322f | ||
|
|
aa810a7ead | ||
|
|
b586e1796e | ||
|
|
23f7b39753 | ||
|
|
fa2ec69fa9 | ||
|
|
fe188ba8fb | ||
|
|
dd8690b592 | ||
|
|
09e6b9741e | ||
|
|
0767952a6f | ||
|
|
72299f833a | ||
|
|
7badaf02b9 |
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 🐛 Bug Report (English)
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['BUG']
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 💡 Feature Request (English)
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['feature']
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3_others.yml
vendored
2
.github/ISSUE_TEMPLATE/3_others.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 🤔 Other Questions (English)
|
||||
name: 🤔 Other Questions
|
||||
description: Submit questions that don't fit into bug reports or feature requests
|
||||
title: '[Other]: '
|
||||
body:
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
|
||||
index 973b0d0e75aeaf8de579419af31b879b32975413..f23c7caa8b9dc8bd404132725346a4786f6b278b 100644
|
||||
--- a/dist/utils/tiktoken.cjs
|
||||
+++ b/dist/utils/tiktoken.cjs
|
||||
@@ -1,25 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encodingForModel = exports.getEncoding = void 0;
|
||||
-const lite_1 = require("js-tiktoken/lite");
|
||||
const async_caller_js_1 = require("./async_caller.cjs");
|
||||
const cache = {};
|
||||
const caller = /* #__PURE__ */ new async_caller_js_1.AsyncCaller({});
|
||||
async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) {
|
||||
- cache[encoding] = caller
|
||||
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
|
||||
- .then((res) => res.json())
|
||||
- .then((data) => new lite_1.Tiktoken(data))
|
||||
- .catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- }
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
exports.getEncoding = getEncoding;
|
||||
async function encodingForModel(model) {
|
||||
- return getEncoding((0, lite_1.getEncodingNameForModel)(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
exports.encodingForModel = encodingForModel;
|
||||
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
|
||||
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..aa5f314a6349ad0d1c5aea8631a56aad099176e0 100644
|
||||
--- a/dist/utils/tiktoken.js
|
||||
+++ b/dist/utils/tiktoken.js
|
||||
@@ -1,20 +1,9 @@
|
||||
-import { Tiktoken, getEncodingNameForModel, } from "js-tiktoken/lite";
|
||||
import { AsyncCaller } from "./async_caller.js";
|
||||
const cache = {};
|
||||
const caller = /* #__PURE__ */ new AsyncCaller({});
|
||||
export async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) {
|
||||
- cache[encoding] = caller
|
||||
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
|
||||
- .then((res) => res.json())
|
||||
- .then((data) => new Tiktoken(data))
|
||||
- .catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- }
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
export async function encodingForModel(model) {
|
||||
- return getEncoding(getEncodingNameForModel(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
diff --git a/package.json b/package.json
|
||||
index 36072aecf700fca1bc49832a19be832eca726103..90b8922fba1c3d1b26f78477c891b07816d6238a 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -37,7 +37,6 @@
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
- "js-tiktoken": "^1.0.12",
|
||||
"langsmith": ">=0.2.8 <0.4.0",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
68
.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch
vendored
Normal file
68
.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
|
||||
index c5b41f121d2e3d24c3a4969e31fa1acffdcad3b9..ec724489dcae79ee6c61acf2d4d84bd19daef036 100644
|
||||
--- a/dist/utils/tiktoken.cjs
|
||||
+++ b/dist/utils/tiktoken.cjs
|
||||
@@ -1,6 +1,5 @@
|
||||
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
||||
const require_utils_async_caller = require('./async_caller.cjs');
|
||||
-const js_tiktoken_lite = require_rolldown_runtime.__toESM(require("js-tiktoken/lite"));
|
||||
|
||||
//#region src/utils/tiktoken.ts
|
||||
var tiktoken_exports = {};
|
||||
@@ -11,14 +10,10 @@ require_rolldown_runtime.__export(tiktoken_exports, {
|
||||
const cache = {};
|
||||
const caller = /* @__PURE__ */ new require_utils_async_caller.AsyncCaller({});
|
||||
async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) cache[encoding] = caller.fetch(`https://tiktoken.pages.dev/js/${encoding}.json`).then((res) => res.json()).then((data) => new js_tiktoken_lite.Tiktoken(data)).catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
async function encodingForModel(model) {
|
||||
- return getEncoding((0, js_tiktoken_lite.getEncodingNameForModel)(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
|
||||
//#endregion
|
||||
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
|
||||
index 641acca03cb92f04a6fa5c9c31f1880ce635572e..707389970ad957aa0ff20ef37fa8dd2875be737c 100644
|
||||
--- a/dist/utils/tiktoken.js
|
||||
+++ b/dist/utils/tiktoken.js
|
||||
@@ -1,6 +1,5 @@
|
||||
import { __export } from "../_virtual/rolldown_runtime.js";
|
||||
import { AsyncCaller } from "./async_caller.js";
|
||||
-import { Tiktoken, getEncodingNameForModel } from "js-tiktoken/lite";
|
||||
|
||||
//#region src/utils/tiktoken.ts
|
||||
var tiktoken_exports = {};
|
||||
@@ -11,14 +10,10 @@ __export(tiktoken_exports, {
|
||||
const cache = {};
|
||||
const caller = /* @__PURE__ */ new AsyncCaller({});
|
||||
async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) cache[encoding] = caller.fetch(`https://tiktoken.pages.dev/js/${encoding}.json`).then((res) => res.json()).then((data) => new Tiktoken(data)).catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
async function encodingForModel(model) {
|
||||
- return getEncoding(getEncodingNameForModel(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
|
||||
//#endregion
|
||||
diff --git a/package.json b/package.json
|
||||
index a24f8fc61de58526051999260f2ebee5f136354b..e885359e8966e7730c51772533ce37e01edb3046 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -20,7 +20,6 @@
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
- "js-tiktoken": "^1.0.12",
|
||||
"langsmith": "^0.3.64",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
@@ -1,19 +0,0 @@
|
||||
diff --git a/dist/embeddings.js b/dist/embeddings.js
|
||||
index 1f8154be3e9c22442a915eb4b85fa6d2a21b0d0c..dc13ef4a30e6c282824a5357bcee9bd0ae222aab 100644
|
||||
--- a/dist/embeddings.js
|
||||
+++ b/dist/embeddings.js
|
||||
@@ -214,10 +214,12 @@ export class OpenAIEmbeddings extends Embeddings {
|
||||
* @returns Promise that resolves to an embedding for the document.
|
||||
*/
|
||||
async embedQuery(text) {
|
||||
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com')
|
||||
+ const input = this.stripNewLines ? text.replace(/\n/g, ' ') : text
|
||||
const params = {
|
||||
model: this.model,
|
||||
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text,
|
||||
- };
|
||||
+ input: isBaiduCloud ? [input] : input
|
||||
+ }
|
||||
if (this.dimensions) {
|
||||
params.dimensions = this.dimensions;
|
||||
}
|
||||
17
.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch
vendored
Normal file
17
.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
diff --git a/dist/embeddings.js b/dist/embeddings.js
|
||||
index 6f4b928d3e4717309382e1b5c2e31ab5bc6c5af0..bc79429c88a6d27d4997a2740c4d8ae0707f5991 100644
|
||||
--- a/dist/embeddings.js
|
||||
+++ b/dist/embeddings.js
|
||||
@@ -94,9 +94,11 @@ var OpenAIEmbeddings = class extends Embeddings {
|
||||
* @returns Promise that resolves to an embedding for the document.
|
||||
*/
|
||||
async embedQuery(text) {
|
||||
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com');
|
||||
+ const input = this.stripNewLines ? text.replace(/\n/g, " ") : text
|
||||
const params = {
|
||||
model: this.model,
|
||||
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text
|
||||
+ input: isBaiduCloud ? [input] : input
|
||||
};
|
||||
if (this.dimensions) params.dimensions = this.dimensions;
|
||||
if (this.encodingFormat) params.encoding_format = this.encodingFormat;
|
||||
@@ -77,7 +77,7 @@ Please review the following critical information before submitting your Pull Req
|
||||
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).
|
||||
* **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/CherryHQ/cherry-studio/pull/10162).
|
||||
|
||||
We highly encourage contributions for:
|
||||
* Bug fixes 🐞
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**"],
|
||||
"includes": ["**", "!**/.claude/**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
|
||||
@@ -81,7 +81,7 @@ git commit --signoff -m "Your commit message"
|
||||
我们的核心团队目前正专注于涉及这些数据结构的关键架构更新和基础工作。为确保在此期间的稳定性与专注,此类贡献将暂时由内部进行管理。
|
||||
|
||||
* **需要更改 Redux 状态结构或 IndexedDB schema 的 PR 将会被关闭。**
|
||||
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (请替换为您的实际仓库链接) 跟踪 `v2.0.0` 的进展及相关讨论。
|
||||
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162) 跟踪 `v2.0.0` 的进展及相关讨论。
|
||||
|
||||
我们非常鼓励以下类型的贡献:
|
||||
* 错误修复 🐞
|
||||
|
||||
@@ -134,60 +134,116 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.2
|
||||
What's New in v1.7.0-beta.3
|
||||
|
||||
New Features:
|
||||
- Session Settings: Manage session-specific settings and model configurations independently
|
||||
- Notes Full-Text Search: Search across all notes with match highlighting
|
||||
- Built-in DiDi MCP Server: Integration with DiDi ride-hailing services (China only)
|
||||
- Intel OV OCR: Hardware-accelerated OCR using Intel NPU
|
||||
- Auto-start API Server: Automatically starts when agents exist
|
||||
- Enhanced Tool Permission System: Real-time tool approval interface with improved UX
|
||||
- Plugin Management System: Support for Claude Agent plugins (agents, commands, skills)
|
||||
- Skill Tool: Add skill execution capabilities for agents
|
||||
- Mobile App Data Restore: Support restoring data to mobile applications
|
||||
- OpenMinerU Preprocessor: Knowledge base now supports open-source MinerU for document processing
|
||||
- HuggingFace Provider: Added HuggingFace as AI provider
|
||||
- Claude Haiku 4.5: Support for the latest Claude Haiku 4.5 model
|
||||
- Ling Series Models: Added support for Ling-1T and related models
|
||||
- Intel OVMS Painting: New painting provider using Intel OpenVINO Model Server
|
||||
- Automatic Update Checks: Implement periodic update checking with configurable intervals
|
||||
- HuggingChat Mini App: New mini app for HuggingChat integration
|
||||
|
||||
Improvements:
|
||||
- Agent model selection now requires explicit user choice
|
||||
- Added Mistral AI provider support
|
||||
- Added NewAPI generic provider support
|
||||
- Improved navbar layout consistency across different modes
|
||||
- Enhanced chat component responsiveness
|
||||
- Better code block display on small screens
|
||||
- Updated OVMS to 2025.3 official release
|
||||
- Added Greek language support
|
||||
- Agent Creation: New agents are now automatically activated upon creation
|
||||
- Lazy Loading: Optimize page load performance with route lazy loading
|
||||
- UI Enhancements: Improved agent item styling and layout consistency
|
||||
- Navigation: Better navbar layout for fullscreen mode on macOS
|
||||
- Settings Tab: Enhanced context slider consistency
|
||||
- Backup Manager: Unified footer layout for local and S3 backup managers
|
||||
- Menu System: Enhanced application menu with improved help section
|
||||
- Proxy Rules: Comprehensive proxy bypass rule matching
|
||||
- German Language: Added German language support
|
||||
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
|
||||
- Translation: Enhanced translation script with concurrency and validation
|
||||
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
|
||||
|
||||
Claude Code Tool Improvements:
|
||||
- GlobTool: Now counts lines instead of files in output for better clarity
|
||||
- ReadTool: Automatically removes system reminder tags from output
|
||||
- TodoWriteTool: Improved rendering behavior
|
||||
- Environment Variables: Updated model-related environment variable names
|
||||
|
||||
Bug Fixes:
|
||||
- Fixed GitHub Copilot gpt-5-codex streaming issues
|
||||
- Fixed assistant creation failures
|
||||
- Fixed translate auto-copy functionality
|
||||
- Fixed miniapps external link opening
|
||||
- Fixed message layout and overflow issues
|
||||
- Fixed API key parsing to preserve spaces
|
||||
- Fixed agent display in different navbar layouts
|
||||
- Fixed session model not being used when sending messages
|
||||
- Fixed tool approval UI and shared workspace plugin inconsistencies
|
||||
- Fixed API server readiness notification to renderer
|
||||
- Fixed grouped items not respecting saved tag order
|
||||
- Fixed assistant/agent activation when creating new ones
|
||||
- Fixed Dashscope Anthropic API host and migrated old configs
|
||||
- Fixed Qwen3 thinking mode control for Ollama
|
||||
- Fixed disappeared MCP button
|
||||
- Fixed create assistant causing blank screen
|
||||
- Fixed up-down button visibility in some cases
|
||||
- Fixed hooks preventing save on composing enter key
|
||||
- Fixed Azure GPT-image-1 and OpenRouter Gemini-image
|
||||
- Fixed Silicon reasoning issues
|
||||
- Fixed topic branch incomplete copy with two-pass ID mapping
|
||||
- Fixed deep research model search context restrictions
|
||||
- Fixed model capability checking logic
|
||||
- Fixed reranker API error response capture
|
||||
- Fixed right-click paste file content into inputbar
|
||||
- Fixed minimax-m2 support in aiCore
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.2 新特性
|
||||
v1.7.0-beta.3 新特性
|
||||
|
||||
新功能:
|
||||
- 会话设置:独立管理会话特定的设置和模型配置
|
||||
- 笔记全文搜索:跨所有笔记搜索并高亮匹配内容
|
||||
- 内置滴滴 MCP 服务器:集成滴滴打车服务(仅限中国地区)
|
||||
- Intel OV OCR:使用 Intel NPU 的硬件加速 OCR
|
||||
- 自动启动 API 服务器:当存在 Agent 时自动启动
|
||||
- 增强工具权限系统:实时工具审批界面,改进用户体验
|
||||
- 插件管理系统:支持 Claude Agent 插件(agents、commands、skills)
|
||||
- 技能工具:为 Agent 添加技能执行能力
|
||||
- 移动应用数据恢复:支持将数据恢复到移动应用程序
|
||||
- OpenMinerU 预处理器:知识库现支持使用开源 MinerU 处理文档
|
||||
- HuggingFace 提供商:添加 HuggingFace 作为 AI 提供商
|
||||
- Claude Haiku 4.5:支持最新的 Claude Haiku 4.5 模型
|
||||
- Ling 系列模型:添加 Ling-1T 及相关模型支持
|
||||
- Intel OVMS 绘图:使用 Intel OpenVINO 模型服务器的新绘图提供商
|
||||
- 自动更新检查:实现可配置间隔的定期更新检查
|
||||
- HuggingChat 小程序:新增 HuggingChat 集成小程序
|
||||
|
||||
改进:
|
||||
- Agent 模型选择现在需要用户显式选择
|
||||
- 添加 Mistral AI 提供商支持
|
||||
- 添加 NewAPI 通用提供商支持
|
||||
- 改进不同模式下的导航栏布局一致性
|
||||
- 增强聊天组件响应式设计
|
||||
- 优化小屏幕代码块显示
|
||||
- 更新 OVMS 至 2025.3 正式版
|
||||
- 添加希腊语支持
|
||||
- Agent 创建:新创建的 Agent 现在会自动激活
|
||||
- 懒加载:通过路由懒加载优化页面加载性能
|
||||
- UI 增强:改进 Agent 项目样式和布局一致性
|
||||
- 导航:改进 macOS 全屏模式下的导航栏布局
|
||||
- 设置选项卡:增强上下文滑块一致性
|
||||
- 备份管理器:统一本地和 S3 备份管理器的页脚布局
|
||||
- 菜单系统:增强应用菜单,改进帮助部分
|
||||
- 代理规则:全面的代理绕过规则匹配
|
||||
- 德语支持:添加德语语言支持
|
||||
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
|
||||
- 翻译:增强翻译脚本的并发和验证功能
|
||||
- Electron & Vite:更新至 Electron 38 和 Vite 4.0.1
|
||||
|
||||
Claude Code 工具改进:
|
||||
- GlobTool:现在计算行数而不是文件数,提供更清晰的输出
|
||||
- ReadTool:自动从输出中移除系统提醒标签
|
||||
- TodoWriteTool:改进渲染行为
|
||||
- 环境变量:更新模型相关的环境变量名称
|
||||
|
||||
问题修复:
|
||||
- 修复 GitHub Copilot gpt-5-codex 流式传输问题
|
||||
- 修复助手创建失败
|
||||
- 修复翻译自动复制功能
|
||||
- 修复小程序外部链接打开
|
||||
- 修复消息布局和溢出问题
|
||||
- 修复 API 密钥解析以保留空格
|
||||
- 修复不同导航栏布局中的 Agent 显示
|
||||
- 修复发送消息时未使用会话模型
|
||||
- 修复工具审批 UI 和共享工作区插件不一致
|
||||
- 修复 API 服务器就绪通知到渲染器
|
||||
- 修复分组项目不遵守已保存标签顺序
|
||||
- 修复创建新的助手/Agent 时的激活问题
|
||||
- 修复 Dashscope Anthropic API 主机并迁移旧配置
|
||||
- 修复 Ollama 的 Qwen3 思考模式控制
|
||||
- 修复 MCP 按钮消失
|
||||
- 修复创建助手导致空白屏幕
|
||||
- 修复某些情况下上下按钮可见性
|
||||
- 修复钩子在输入法输入时阻止保存
|
||||
- 修复 Azure GPT-image-1 和 OpenRouter Gemini-image
|
||||
- 修复 Silicon 推理问题
|
||||
- 修复主题分支不完整复制,采用两阶段 ID 映射
|
||||
- 修复深度研究模型搜索上下文限制
|
||||
- 修复模型能力检查逻辑
|
||||
- 修复 reranker API 错误响应捕获
|
||||
- 修复右键粘贴文件内容到输入栏
|
||||
- 修复 aiCore 中的 minimax-m2 支持
|
||||
<!--LANG:END-->
|
||||
|
||||
15
package.json
15
package.json
@@ -95,8 +95,10 @@
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.2.0",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"selection-hook": "^1.0.12",
|
||||
"sharp": "^0.34.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"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",
|
||||
@@ -149,7 +151,9 @@
|
||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@langchain/community": "^0.3.50",
|
||||
"@langchain/community": "^1.0.0",
|
||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@mistralai/mistralai": "^1.7.5",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
@@ -375,9 +379,7 @@
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/view": "6.38.1",
|
||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
|
||||
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
|
||||
@@ -401,7 +403,10 @@
|
||||
"@img/sharp-linux-arm64": "0.34.3",
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3",
|
||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0"
|
||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.6.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -354,6 +354,7 @@ export enum IpcChannel {
|
||||
ApiServer_Stop = 'api-server:stop',
|
||||
ApiServer_Restart = 'api-server:restart',
|
||||
ApiServer_GetStatus = 'api-server:get-status',
|
||||
ApiServer_Ready = 'api-server:ready',
|
||||
// NOTE: This api is not be used.
|
||||
ApiServer_GetConfig = 'api-server:get-config',
|
||||
|
||||
@@ -395,5 +396,12 @@ export enum IpcChannel {
|
||||
ClaudeCodePlugin_ListInstalled = 'claudeCodePlugin:list-installed',
|
||||
ClaudeCodePlugin_InvalidateCache = 'claudeCodePlugin:invalidate-cache',
|
||||
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
|
||||
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content'
|
||||
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content',
|
||||
|
||||
// WebSocket
|
||||
WebSocket_Start = 'webSocket:start',
|
||||
WebSocket_Stop = 'webSocket:stop',
|
||||
WebSocket_Status = 'webSocket:status',
|
||||
WebSocket_SendFile = 'webSocket:send-file',
|
||||
WebSocket_GetAllCandidates = 'webSocket:get-all-candidates'
|
||||
}
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
# Cherry Studio Design System 集成方案
|
||||
|
||||
本文档聚焦三个核心问题:
|
||||
|
||||
1. **如何将 todocss.css 集成到 Tailwind CSS v4**
|
||||
2. **如何在项目中使用集成后的设计系统**
|
||||
3. **如何平衡 UI 库和主包的需求**
|
||||
|
||||
---
|
||||
|
||||
## 一、集成策略
|
||||
|
||||
### 1.1 文件架构
|
||||
|
||||
```
|
||||
todocss.css (设计师提供)
|
||||
↓ 转换 & 优化
|
||||
design-tokens.css (--ds-* 变量)
|
||||
↓ @theme inline 映射
|
||||
globals.css (cs-* 工具类)
|
||||
↓ 开发者使用
|
||||
React Components
|
||||
```
|
||||
|
||||
### 1.2 核心转换规则
|
||||
|
||||
#### 变量简化
|
||||
|
||||
```css
|
||||
/* todocss.css */
|
||||
--Brand--Base_Colors--Primary: hsla(84, 81%, 44%, 1);
|
||||
|
||||
/* ↓ 转换为 design-tokens.css */
|
||||
--ds-primary: hsla(84, 81%, 44%, 1);
|
||||
|
||||
/* ↓ 映射到 globals.css */
|
||||
@theme inline {
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
}
|
||||
|
||||
/* ↓ 生成工具类 */
|
||||
bg-cs-primary, text-cs-primary, border-cs-primary
|
||||
```
|
||||
|
||||
#### 去除冗余
|
||||
|
||||
- **间距/尺寸合并**: `--Spacing--md` 和 `--Sizing--md` 值相同 → 统一为 `--ds-size-md`
|
||||
- **透明度废弃**: `--Opacity--Red--Red-80` → 使用 `bg-cs-destructive/80`
|
||||
- **错误修正**: `--Font_weight--Regular: 400px` → `--ds-font-weight-regular: 400`
|
||||
|
||||
### 1.3 命名规范
|
||||
|
||||
| 层级 | 前缀 | 示例 | 用途 |
|
||||
|------|------|------|------|
|
||||
| 设计令牌 | `--ds-*` | `--ds-primary` | 定义值 |
|
||||
| Tailwind 映射 | `--color-cs-*` | `--color-cs-primary` | 生成工具类 |
|
||||
| 工具类 | `cs-*` | `bg-cs-primary` | 开发者使用 |
|
||||
|
||||
#### Tailwind v4 映射规则
|
||||
|
||||
| 变量前缀 | 生成的工具类 |
|
||||
|----------|-------------|
|
||||
| `--color-cs-*` | `bg-*`, `text-*`, `border-*`, `fill-*` |
|
||||
| `--spacing-cs-*` | `p-*`, `m-*`, `gap-*` |
|
||||
| `--size-cs-*` | `w-*`, `h-*`, `size-*` |
|
||||
| `--radius-cs-*` | `rounded-*` |
|
||||
| `--font-size-cs-*` | `text-*` |
|
||||
|
||||
### 1.4 为什么使用 @theme inline
|
||||
|
||||
```css
|
||||
/* ❌ @theme - 静态编译,不支持运行时主题切换 */
|
||||
@theme {
|
||||
--color-primary: var(--ds-primary);
|
||||
}
|
||||
|
||||
/* ✅ @theme inline - 保留变量引用,支持运行时切换 */
|
||||
@theme inline {
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**关键差异**:`@theme inline` 使 CSS 变量在运行时动态解析,实现明暗主题切换。
|
||||
|
||||
---
|
||||
|
||||
## 二、项目使用指南
|
||||
|
||||
### 2.1 在 UI 库中使用
|
||||
|
||||
#### 文件结构
|
||||
|
||||
```
|
||||
packages/ui/
|
||||
├── src/styles/
|
||||
│ ├── design-tokens.css # 核心变量定义
|
||||
│ └── globals.css # Tailwind 集成
|
||||
└── package.json # 导出配置
|
||||
```
|
||||
|
||||
#### globals.css 示例
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
@import './design-tokens.css';
|
||||
|
||||
@theme inline {
|
||||
/* 颜色 */
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
--color-cs-bg: var(--ds-background);
|
||||
--color-cs-fg: var(--ds-foreground);
|
||||
|
||||
/* 间距 */
|
||||
--spacing-cs-xs: var(--ds-size-xs);
|
||||
--spacing-cs-sm: var(--ds-size-sm);
|
||||
--spacing-cs-md: var(--ds-size-md);
|
||||
|
||||
/* 尺寸 */
|
||||
--size-cs-xs: var(--ds-size-xs);
|
||||
--size-cs-sm: var(--ds-size-sm);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-cs-sm: var(--ds-radius-sm);
|
||||
--radius-cs-md: var(--ds-radius-md);
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
```
|
||||
|
||||
#### 组件中使用
|
||||
|
||||
```tsx
|
||||
// packages/ui/src/components/Button.tsx
|
||||
export const Button = ({ children }) => (
|
||||
<button className="
|
||||
bg-cs-primary
|
||||
text-white
|
||||
px-cs-sm
|
||||
py-cs-xs
|
||||
rounded-cs-md
|
||||
hover:bg-cs-primary/90
|
||||
transition-colors
|
||||
">
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
```
|
||||
|
||||
### 2.2 在主项目中使用
|
||||
|
||||
#### 导入 UI 库样式
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
```
|
||||
|
||||
#### 覆盖或扩展变量
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 主项目特定覆盖 */
|
||||
:root {
|
||||
--ds-primary: #custom-color; /* 覆盖 UI 库的主题色 */
|
||||
}
|
||||
```
|
||||
|
||||
#### 在主项目组件中使用
|
||||
|
||||
```tsx
|
||||
// src/renderer/src/pages/Home.tsx
|
||||
export const Home = () => (
|
||||
<div className="
|
||||
bg-cs-bg
|
||||
p-cs-md
|
||||
rounded-cs-lg
|
||||
">
|
||||
<Button>主项目按钮</Button>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
### 2.3 主题切换实现
|
||||
|
||||
```tsx
|
||||
// App.tsx
|
||||
import { useState } from 'react'
|
||||
|
||||
export function App() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
切换主题
|
||||
</button>
|
||||
{/* 所有子组件自动响应主题 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 透明度修饰符
|
||||
|
||||
```tsx
|
||||
<div className="
|
||||
bg-cs-primary/10 /* 10% 透明度 */
|
||||
bg-cs-primary/50 /* 50% 透明度 */
|
||||
bg-cs-primary/[0.15] /* 自定义透明度 */
|
||||
">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 库与主包平衡策略
|
||||
|
||||
### 3.1 UI 库职责
|
||||
|
||||
**目标**:提供可复用、可定制的基础设计系统
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"exports": {
|
||||
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
|
||||
"./styles/globals.css": "./src/styles/globals.css"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:
|
||||
|
||||
- ✅ 定义通用的设计令牌(`--ds-*`)
|
||||
- ✅ 提供默认的 Tailwind 映射(`--color-cs-*`)
|
||||
- ✅ 保持变量语义化,不包含业务逻辑
|
||||
- ❌ 不包含主项目特定的颜色或尺寸
|
||||
|
||||
### 3.2 主包职责
|
||||
|
||||
**目标**:导入 UI 库,根据业务需求扩展或覆盖
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 主项目扩展 */
|
||||
@theme inline {
|
||||
--color-cs-brand-accent: #ff6b6b; /* 新增颜色 */
|
||||
}
|
||||
|
||||
/* 主项目覆盖 */
|
||||
:root {
|
||||
--ds-primary: #custom-primary; /* 覆盖 UI 库的主题色 */
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:
|
||||
|
||||
- ✅ 导入 UI 库的 `globals.css`
|
||||
- ✅ 通过覆盖 `--ds-*` 变量定制主题
|
||||
- ✅ 添加项目特定的 `--color-cs-*` 映射
|
||||
- ✅ 保留向后兼容的旧变量(如 `color.css`)
|
||||
|
||||
### 3.3 向后兼容方案
|
||||
|
||||
#### 保留旧变量
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/color.css */
|
||||
:root {
|
||||
--color-primary: #00b96b; /* 旧变量 */
|
||||
--color-background: #181818; /* 旧变量 */
|
||||
}
|
||||
|
||||
/* 映射到新系统 */
|
||||
:root {
|
||||
--ds-primary: var(--color-primary);
|
||||
--ds-background: var(--color-background);
|
||||
}
|
||||
```
|
||||
|
||||
#### 渐进式迁移
|
||||
|
||||
```tsx
|
||||
// 阶段 1:旧代码继续工作
|
||||
<div style={{ color: 'var(--color-primary)' }}>旧代码</div>
|
||||
|
||||
// 阶段 2:新代码使用工具类
|
||||
<div className="text-cs-primary">新代码</div>
|
||||
|
||||
// 阶段 3:逐步替换旧代码
|
||||
```
|
||||
|
||||
### 3.4 冲突处理
|
||||
|
||||
| 场景 | 策略 |
|
||||
|------|------|
|
||||
| UI 库与 Tailwind 默认类冲突 | 使用 `cs-` 前缀隔离 |
|
||||
| 主包需要覆盖 UI 库颜色 | 覆盖 `--ds-*` 变量 |
|
||||
| 主包需要新增颜色 | 添加新的 `--color-cs-*` 映射 |
|
||||
| 旧变量与新系统共存 | 通过 `var()` 映射到 `--ds-*` |
|
||||
|
||||
### 3.5 独立发布 UI 库
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"name": "@cherrystudio/ui",
|
||||
"exports": {
|
||||
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
|
||||
"./styles/globals.css": "./src/styles/globals.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^4.1.13"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**外部项目使用**:
|
||||
```css
|
||||
/* 其他项目的 tailwind.css */
|
||||
@import 'tailwindcss';
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 覆盖主题色 */
|
||||
:root {
|
||||
--ds-primary: #your-brand-color;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、完整映射示例
|
||||
|
||||
### todocss.css → design-tokens.css
|
||||
|
||||
| todocss.css | design-tokens.css | 说明 |
|
||||
|-------------|-------------------|------|
|
||||
| `--Brand--Base_Colors--Primary` | `--ds-primary` | 简化命名 |
|
||||
| `--Spacing--md` + `--Sizing--md` | `--ds-size-md` | 合并重复 |
|
||||
| `--Opacity--Red--Red-80` | *(删除)* | 使用 `/80` 修饰符 |
|
||||
| `--Font_weight--Regular: 400px` | `--ds-font-weight-regular: 400` | 修正错误 |
|
||||
| `--Brand--UI_Element_Colors--Primary_Button--Background` | `--ds-btn-primary` | 简化语义 |
|
||||
|
||||
### design-tokens.css → globals.css → 工具类
|
||||
|
||||
| design-tokens.css | globals.css | 工具类 |
|
||||
|-------------------|-------------|--------|
|
||||
| `--ds-primary` | `--color-cs-primary` | `bg-cs-primary` |
|
||||
| `--ds-size-md` | `--spacing-cs-md` | `p-cs-md` |
|
||||
| `--ds-size-md` | `--size-cs-md` | `w-cs-md` |
|
||||
| `--ds-radius-lg` | `--radius-cs-lg` | `rounded-cs-lg` |
|
||||
|
||||
---
|
||||
|
||||
## 五、关键决策记录
|
||||
|
||||
1. **使用 `@theme inline`** - 支持运行时主题切换
|
||||
2. **`cs-` 前缀** - 命名空间隔离,避免冲突
|
||||
3. **合并 Spacing/Sizing** - 消除冗余
|
||||
4. **废弃 Opacity 变量** - 使用 Tailwind 的 `/modifier` 语法
|
||||
5. **双层变量系统** - `--ds-*` (定义) → `--color-cs-*` (映射)
|
||||
6. **共存策略** - Tailwind 默认类 + `cs-` 品牌类
|
||||
@@ -9,16 +9,16 @@ This document outlines the detailed plan for migrating Cherry Studio from antd +
|
||||
### Target Tech Stack
|
||||
|
||||
- **UI Component Library**: shadcn/ui (replacing antd and previously migrated HeroUI)
|
||||
- **Styling Solution**: Tailwind CSS (replacing styled-components)
|
||||
- **Design System**: Custom CSS variable system (see [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md))
|
||||
- **Theme System**: CSS variables + shadcn/ui theme
|
||||
- **Styling Solution**: Tailwind CSS v4 (replacing styled-components)
|
||||
- **Design System**: Custom CSS variable system (`--cs-*` namespace)
|
||||
- **Theme System**: CSS variables + Tailwind CSS theme
|
||||
|
||||
### Migration Principles
|
||||
|
||||
1. **Backward Compatibility**: Old components continue working until new components are fully available
|
||||
2. **Progressive Migration**: Migrate components one by one to avoid large-scale rewrites
|
||||
3. **Feature Parity**: Ensure new components have all the functionality of old components
|
||||
4. **Design Consistency**: Follow new design system specifications (see DESIGN_SYSTEM.md)
|
||||
4. **Design Consistency**: Follow new design system specifications (see [README.md](./README.md))
|
||||
5. **Performance Priority**: Optimize bundle size and rendering performance
|
||||
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
|
||||
|
||||
@@ -105,7 +105,7 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
|
||||
| Phase | Status | Main Tasks | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| **Phase 1** | 🚧 **In Progress** | **Design System Integration** | • Integrate design system CSS variables (todocss.css → design-tokens.css → globals.css)<br>• Configure Tailwind CSS to use custom design tokens<br>• Establish basic style guidelines and theme system |
|
||||
| **Phase 1** | ✅ **Completed** | **Design System Integration** | • Converted design tokens from todocss.css to tokens.css with `--cs-*` namespace<br>• Created theme.css mapping all design tokens to standard Tailwind classes<br>• Extended Tailwind with semantic spacing (5xs~8xl) and radius (4xs~3xl) systems<br>• Established two usage modes: full override and selective override<br>• Cleaned up main package's conflicting Shadcn theme definitions |
|
||||
| **Phase 2** | ⏳ **To Start** | **Component Migration and Optimization** | • Filter components for migration based on extraction criteria<br>• Remove antd dependencies, replace with shadcn/ui<br>• Remove HeroUI dependencies, replace with shadcn/ui<br>• Remove styled-components, replace with Tailwind CSS + design system variables<br>• Optimize component APIs and type definitions |
|
||||
| **Phase 3** | ⏳ **To Start** | **UI Refactoring and Optimization** | • Gradually implement UI refactoring with UI designers<br>• Ensure visual consistency and user experience<br>• Performance optimization and code quality improvement |
|
||||
|
||||
@@ -129,16 +129,22 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
## Design System Integration
|
||||
|
||||
### CSS Variable System
|
||||
- Refer to [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) for complete design system planning
|
||||
- Design variables will be managed through CSS variable system, naming conventions TBD
|
||||
- Support theme switching and responsive design
|
||||
|
||||
- All design tokens use `--cs-*` namespace (e.g., `--cs-primary`, `--cs-red-500`)
|
||||
- Complete color palette: 17 colors × 11 shades each
|
||||
- Semantic spacing system: `5xs` through `8xl` (16 levels)
|
||||
- Semantic radius system: `4xs` through `3xl` plus `round` (11 levels)
|
||||
- Full light/dark mode support
|
||||
- See [README.md](./README.md) for usage documentation
|
||||
|
||||
### Migration Priority Adjustment
|
||||
|
||||
1. **High Priority**: Basic components (buttons, inputs, tags, etc.)
|
||||
2. **Medium Priority**: Display components (cards, lists, tables, etc.)
|
||||
3. **Low Priority**: Composite components and business-coupled components
|
||||
|
||||
### UI Designer Collaboration
|
||||
|
||||
- All component designs need confirmation from UI designers
|
||||
- Gradually implement UI refactoring to maintain visual consistency
|
||||
- New components must comply with design system specifications
|
||||
@@ -2,54 +2,120 @@
|
||||
|
||||
Cherry Studio UI 组件库 - 为 Cherry Studio 设计的 React 组件集合
|
||||
|
||||
## 特性
|
||||
## ✨ 特性
|
||||
|
||||
- 🎨 基于 Tailwind CSS 的现代化设计
|
||||
- 📦 支持 ESM 和 CJS 格式
|
||||
- 🔷 完整的 TypeScript 支持
|
||||
- 🚀 可以作为 npm 包发布
|
||||
- 🔧 开箱即用的常用 hooks 和工具函数
|
||||
- 🎨 **设计系统**: 完整的 CherryStudio 设计令牌(17种颜色 × 11个色阶 + 语义化主题)
|
||||
- 🌓 **Dark Mode**: 开箱即用的深色模式支持
|
||||
- 🚀 **Tailwind v4**: 基于最新 Tailwind CSS v4 构建
|
||||
- 📦 **灵活导入**: 2种样式导入方式,满足不同使用场景
|
||||
- 🔷 **TypeScript**: 完整的类型定义和智能提示
|
||||
- 🎯 **零冲突**: CSS 变量隔离,不覆盖用户主题
|
||||
|
||||
## 安装
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 安装组件库
|
||||
npm install @cherrystudio/ui
|
||||
|
||||
# 安装必需的 peer dependencies
|
||||
# peer dependencies
|
||||
npm install @heroui/react framer-motion react react-dom tailwindcss
|
||||
```
|
||||
|
||||
## 配置
|
||||
### 两种使用方式
|
||||
|
||||
### 1. Tailwind CSS v4 配置
|
||||
#### 方式 1:完整覆盖 ✨
|
||||
|
||||
本组件库使用 Tailwind CSS v4,配置方式已改变。在你的主 CSS 文件(如 `src/styles/tailwind.css`)中:
|
||||
使用完整的 CherryStudio 设计系统,所有 Tailwind 类名映射到设计系统。
|
||||
|
||||
```css
|
||||
/* app.css */
|
||||
@import '@cherrystudio/ui/styles/theme.css';
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- ✅ 直接使用标准 Tailwind 类名(`bg-primary`、`bg-red-500`、`p-md`、`rounded-lg`)
|
||||
- ✅ 所有颜色使用设计师定义的值
|
||||
- ✅ 扩展的 Spacing 系统(`p-5xs` ~ `p-8xl`,共 16 个语义化尺寸)
|
||||
- ✅ 扩展的 Radius 系统(`rounded-4xs` ~ `rounded-3xl`,共 11 个圆角)
|
||||
- ⚠️ 会完全覆盖 Tailwind 默认主题
|
||||
|
||||
**示例**:
|
||||
|
||||
```tsx
|
||||
<Button className="bg-primary text-red-500 p-md rounded-lg">
|
||||
{/* bg-primary → 品牌色(lime-500) */}
|
||||
{/* text-red-500 → 设计师定义的红色 */}
|
||||
{/* p-md → 2.5rem(spacing-md) */}
|
||||
{/* rounded-lg → 2.5rem(radius-lg) */}
|
||||
</Button>
|
||||
|
||||
{/* 扩展的工具类 */}
|
||||
<div className="p-5xs">最小间距 (0.5rem)</div>
|
||||
<div className="p-xs">超小间距 (1rem)</div>
|
||||
<div className="p-sm">小间距 (1.5rem)</div>
|
||||
<div className="p-md">中等间距 (2.5rem)</div>
|
||||
<div className="p-lg">大间距 (3.5rem)</div>
|
||||
<div className="p-xl">超大间距 (5rem)</div>
|
||||
<div className="p-8xl">最大间距 (15rem)</div>
|
||||
|
||||
<div className="rounded-4xs">最小圆角 (0.25rem)</div>
|
||||
<div className="rounded-xs">小圆角 (1rem)</div>
|
||||
<div className="rounded-md">中等圆角 (2rem)</div>
|
||||
<div className="rounded-xl">大圆角 (3rem)</div>
|
||||
<div className="rounded-round">完全圆角 (999px)</div>
|
||||
```
|
||||
|
||||
#### 方式 2:选择性覆盖 🎯
|
||||
|
||||
只导入设计令牌(CSS 变量),手动选择要覆盖的部分。
|
||||
|
||||
```css
|
||||
/* app.css */
|
||||
@import 'tailwindcss';
|
||||
@import '@cherrystudio/ui/styles/tokens.css';
|
||||
|
||||
/* 必须扫描组件库文件以提取类名 */
|
||||
@source '../node_modules/@cherrystudio/ui/dist/**/*.{js,mjs}';
|
||||
|
||||
/* 你的应用源文件 */
|
||||
@source './src/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
/*
|
||||
* 如果你的应用直接使用 HeroUI 组件,需要添加:
|
||||
* @source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
* @plugin '@heroui/react/plugin';
|
||||
*/
|
||||
|
||||
/* 自定义主题配置(可选) */
|
||||
/* 只使用部分设计系统 */
|
||||
@theme {
|
||||
/* 你的主题扩展 */
|
||||
--color-primary: var(--cs-primary); /* 使用 CS 的主色 */
|
||||
--color-red-500: oklch(...); /* 使用自己的红色 */
|
||||
--spacing-md: var(--cs-size-md); /* 使用 CS 的间距 */
|
||||
--radius-lg: 1rem; /* 使用自己的圆角 */
|
||||
}
|
||||
```
|
||||
|
||||
注意:Tailwind CSS v4 不再使用 `tailwind.config.js` 文件,所有配置都在 CSS 中完成。
|
||||
**特点**:
|
||||
|
||||
### 2. Provider 配置
|
||||
- ✅ 不覆盖任何 Tailwind 默认主题
|
||||
- ✅ 通过 CSS 变量访问所有设计令牌(`var(--cs-primary)`、`var(--cs-red-500)`)
|
||||
- ✅ 精细控制哪些使用 CS、哪些保持原样
|
||||
- ✅ 适合有自己设计系统但想借用部分 CS 设计令牌的场景
|
||||
|
||||
**示例**:
|
||||
|
||||
```tsx
|
||||
{/* 通过 CSS 变量使用 CS 设计令牌 */}
|
||||
<button style={{ backgroundColor: 'var(--cs-primary)' }}>
|
||||
使用 CherryStudio 品牌色
|
||||
</button>
|
||||
|
||||
{/* 保持原有的 Tailwind 类名不受影响 */}
|
||||
<div className="bg-red-500">
|
||||
使用 Tailwind 默认的红色
|
||||
</div>
|
||||
|
||||
{/* 可用的 CSS 变量 */}
|
||||
<div style={{
|
||||
color: 'var(--cs-primary)', // 品牌色
|
||||
backgroundColor: 'var(--cs-red-500)', // 红色-500
|
||||
padding: 'var(--cs-size-md)', // 间距
|
||||
borderRadius: 'var(--cs-radius-lg)' // 圆角
|
||||
}} />
|
||||
```
|
||||
|
||||
### Provider 配置
|
||||
|
||||
在你的 App 根组件中添加 HeroUI Provider:
|
||||
|
||||
@@ -94,9 +160,6 @@ function App() {
|
||||
// 只导入组件
|
||||
import { Button } from '@cherrystudio/ui/components'
|
||||
|
||||
// 只导入 hooks
|
||||
import { useDebounce, useLocalStorage } from '@cherrystudio/ui/hooks'
|
||||
|
||||
// 只导入工具函数
|
||||
import { cn, formatFileSize } from '@cherrystudio/ui/utils'
|
||||
```
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"tailwind": {
|
||||
"baseColor": "zinc",
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"css": "src/styles/theme.css",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
|
||||
214
packages/ui/design-reference/CONVERSION_LOG.md
Normal file
214
packages/ui/design-reference/CONVERSION_LOG.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# todocss.css → design-tokens.css 转换日志
|
||||
|
||||
## ✅ 已转换的变量
|
||||
|
||||
### 基础颜色 (Primitive Colors)
|
||||
- ✅ Neutral (50-950)
|
||||
- ✅ Zinc (50-950)
|
||||
- ✅ Red (50-950)
|
||||
- ✅ Orange (50-950)
|
||||
- ✅ Amber (50-950)
|
||||
- ✅ Yellow (50-950)
|
||||
- ✅ Lime (50-950) - 品牌主色
|
||||
- ✅ Green (50-950)
|
||||
- ✅ Emerald (50-950)
|
||||
- ✅ Purple (50-950)
|
||||
- ✅ Blue (50-950)
|
||||
- ✅ Black & White
|
||||
|
||||
### 语义化颜色 (Semantic Colors)
|
||||
- ✅ `--cs-primary` (Lime 500)
|
||||
- ✅ `--cs-destructive` (Red 500)
|
||||
- ✅ `--cs-success` (Green 500)
|
||||
- ✅ `--cs-warning` (Amber 500)
|
||||
- ✅ `--cs-background` (Zinc 50/900)
|
||||
- ✅ `--cs-foreground` 系列 (main, secondary, muted)
|
||||
- ✅ `--cs-border` 系列 (default, hover, active)
|
||||
- ✅ `--cs-ring` (Focus)
|
||||
|
||||
### 容器颜色
|
||||
- ✅ `--cs-card` (White/Black)
|
||||
- ✅ `--cs-popover` (White/Black)
|
||||
- ✅ `--cs-sidebar` (White/Black)
|
||||
|
||||
### UI 元素细分颜色 (新增补充)
|
||||
- ✅ **Modal / Overlay**
|
||||
- `--cs-modal-backdrop`
|
||||
- `--cs-modal-thumb`
|
||||
- `--cs-modal-thumb-hover`
|
||||
|
||||
- ✅ **Icon**
|
||||
- `--cs-icon-default`
|
||||
- `--cs-icon-hover`
|
||||
|
||||
- ✅ **Input / Select**
|
||||
- `--cs-input-background`
|
||||
- `--cs-input-border`
|
||||
- `--cs-input-border-hover`
|
||||
- `--cs-input-border-focus`
|
||||
|
||||
- ✅ **Primary Button**
|
||||
- `--cs-primary-button-background`
|
||||
- `--cs-primary-button-text`
|
||||
- `--cs-primary-button-background-hover`
|
||||
- `--cs-primary-button-background-active`
|
||||
- `--cs-primary-button-background-2nd`
|
||||
- `--cs-primary-button-background-3rd`
|
||||
|
||||
- ✅ **Secondary Button**
|
||||
- `--cs-secondary-button-background`
|
||||
- `--cs-secondary-button-text`
|
||||
- `--cs-secondary-button-background-hover`
|
||||
- `--cs-secondary-button-background-active`
|
||||
- `--cs-secondary-button-border`
|
||||
|
||||
- ✅ **Ghost Button**
|
||||
- `--cs-ghost-button-background`
|
||||
- `--cs-ghost-button-text`
|
||||
- `--cs-ghost-button-background-hover`
|
||||
- `--cs-ghost-button-background-active`
|
||||
|
||||
### 尺寸系统
|
||||
- ✅ Spacing/Sizing 合并为 `--cs-size-*` (5xs ~ 8xl)
|
||||
- ✅ Border Radius (4xs ~ 3xl, round)
|
||||
- ✅ Border Width (sm, md, lg)
|
||||
|
||||
### 字体排版
|
||||
- ✅ Font Families (Heading, Body)
|
||||
- ✅ Font Weights (修正单位错误: 400px → 400)
|
||||
- ✅ Font Sizes (Body & Heading)
|
||||
- ✅ Line Heights (Body & Heading)
|
||||
- ✅ Paragraph Spacing
|
||||
|
||||
---
|
||||
|
||||
## ❌ 已废弃的变量
|
||||
|
||||
### Opacity 变量 (全部废弃)
|
||||
使用 Tailwind 的 `/modifier` 语法替代:
|
||||
|
||||
| todocss.css | 替代方案 |
|
||||
|-------------|---------|
|
||||
| `--Opacity--Red--Red-80` | `bg-cs-destructive/80` |
|
||||
| `--Opacity--Green--Green-60` | `bg-cs-success/60` |
|
||||
| `--Opacity--White--White-10` | `bg-white/10` |
|
||||
|
||||
**原因**: Tailwind v4 原生支持透明度修饰符,无需单独定义变量。
|
||||
|
||||
---
|
||||
|
||||
## 🔧 关键修正
|
||||
|
||||
### 1. 单位错误
|
||||
```css
|
||||
/* ❌ todocss.css */
|
||||
--Font_weight--Regular: 400px;
|
||||
|
||||
/* ✅ design-tokens.css */
|
||||
--cs-font-weight-regular: 400;
|
||||
```
|
||||
|
||||
### 2. px → rem 转换
|
||||
```css
|
||||
/* ❌ todocss.css */
|
||||
--Spacing--md: 40px;
|
||||
|
||||
/* ✅ design-tokens.css */
|
||||
--cs-size-md: 2.5rem; /* 40px / 16 = 2.5rem */
|
||||
```
|
||||
|
||||
### 3. 变量合并
|
||||
```css
|
||||
/* ❌ todocss.css (冗余) */
|
||||
--Spacing--md: 40px;
|
||||
--Sizing--md: 40px;
|
||||
|
||||
/* ✅ design-tokens.css (合并) */
|
||||
--cs-size-md: 2.5rem;
|
||||
```
|
||||
|
||||
### 4. Dark Mode 分离
|
||||
```css
|
||||
/* ❌ todocss.css (Light 和 Dark 都在 :root) */
|
||||
:root {
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50);
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /* 后面覆盖 */
|
||||
}
|
||||
|
||||
/* ✅ design-tokens.css (正确分离) */
|
||||
:root {
|
||||
--cs-background: var(--cs-zinc-50);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--cs-background: var(--cs-zinc-900);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 变量统计
|
||||
|
||||
| 分类 | todocss.css | design-tokens.css | 说明 |
|
||||
|------|-------------|-------------------|------|
|
||||
| Primitive Colors | ~250 | ~250 | 完整保留 |
|
||||
| Semantic Colors | ~20 | ~20 | 完整转换 |
|
||||
| UI Element Colors | ~30 | ~30 | ✅ 已补充完整 |
|
||||
| Opacity Variables | ~50 | 0 | 废弃,用 `/modifier` |
|
||||
| Spacing/Sizing | 32 | 16 | 合并去重 |
|
||||
| Typography | ~50 | ~50 | 修正单位 |
|
||||
| **总计** | ~430 | ~390 | 优化 40 个变量 |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Dark Mode 变量对比
|
||||
|
||||
| Light Mode | Dark Mode | 变量名 |
|
||||
|-----------|-----------|-------|
|
||||
| Zinc 50 | Zinc 900 | `--cs-background` |
|
||||
| Black 90% | White 90% | `--cs-foreground` |
|
||||
| Black 60% | White 60% | `--cs-foreground-secondary` |
|
||||
| Black 10% | White 10% | `--cs-border` |
|
||||
| White | Black | `--cs-card` |
|
||||
| White | Black | `--cs-popover` |
|
||||
| White | Black | `--cs-sidebar` |
|
||||
| White | Black | `--cs-input-background` |
|
||||
| Black 40% | Black 6% | `--cs-modal-backdrop` |
|
||||
| Black 20% | White 20% | `--cs-modal-thumb` |
|
||||
| Black 5% | White 10% | `--cs-secondary` |
|
||||
| Black 0% | White 0% | `--cs-ghost-button-background` |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验证清单
|
||||
|
||||
- [x] 所有 Primitive 颜色已转换
|
||||
- [x] 所有语义化颜色已转换
|
||||
- [x] 所有 UI 元素颜色已转换
|
||||
- [x] Dark Mode 变量完整
|
||||
- [x] 尺寸单位统一为 rem
|
||||
- [x] Font Weight 单位已修正
|
||||
- [x] Opacity 变量已废弃
|
||||
- [x] Spacing/Sizing 已合并
|
||||
|
||||
---
|
||||
|
||||
## 📝 使用指南
|
||||
|
||||
### 如果设计师更新 todocss.css
|
||||
|
||||
1. 对比此文档,找出新增/修改的变量
|
||||
2. 按照转换规则更新 `design-tokens.css`
|
||||
3. 验证 Light/Dark Mode 是否完整
|
||||
4. 更新此日志
|
||||
|
||||
### 验证转换正确性
|
||||
|
||||
```bash
|
||||
# 检查 Light Mode 变量数量
|
||||
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css
|
||||
|
||||
# 检查 Dark Mode 覆盖数量
|
||||
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css | grep -A 100 ".dark"
|
||||
```
|
||||
|
||||
26
packages/ui/design-reference/README.md
Normal file
26
packages/ui/design-reference/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Design Reference
|
||||
|
||||
本文件夹包含设计师提供的原始设计令牌文件,仅作为参考使用。
|
||||
|
||||
## 文件说明
|
||||
|
||||
### todocss.css
|
||||
- **来源**:设计师提供的原始设计令牌
|
||||
- **状态**:已转换为 `src/styles/design-tokens.css`
|
||||
- **用途**:
|
||||
- 追溯设计决策
|
||||
- 验证转换正确性
|
||||
- 设计师更新时作为对比基准
|
||||
|
||||
## 转换规则
|
||||
|
||||
原始文件 → 生产文件的转换规则参见:
|
||||
- [DESIGN_SYSTEM.md](../DESIGN_SYSTEM.md)
|
||||
- [USAGE_GUIDE.md](../USAGE_GUIDE.md)
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **请勿直接使用此文件夹中的文件**
|
||||
- 这些文件仅供参考
|
||||
- 实际使用请导入 `src/styles/` 中的文件
|
||||
|
||||
870
packages/ui/design-reference/todocss.css
Normal file
870
packages/ui/design-reference/todocss.css
Normal file
@@ -0,0 +1,870 @@
|
||||
:root {
|
||||
/* Typography: Desktop mode */
|
||||
--Font_family--Heading: Inter;
|
||||
--Font_weight--Regular: 400px;
|
||||
--Font_size--Heading--2xl: 60px;
|
||||
--Font_size--Heading--xl: 48px;
|
||||
--Font_size--Heading--lg: 40px;
|
||||
--Font_size--Heading--md: 32px;
|
||||
--Font_size--Heading--sm: 24px;
|
||||
--Font_size--Heading--xs: 20px;
|
||||
--Line_height--Heading--xl: 80px;
|
||||
--Line_height--Body--lg: 28px;
|
||||
--Line_height--Body--md: 24px;
|
||||
--Line_height--Body--sm: 24px;
|
||||
--Line_height--Body--xs: 20px;
|
||||
--Paragraph_spacing--Body--lg: 18px;
|
||||
--Paragraph_spacing--Body--md: 16px;
|
||||
--Paragraph_spacing--Body--sm: 14px;
|
||||
--Paragraph_spacing--Body--xs: 12px;
|
||||
--Line_height--Heading--lg: 60px;
|
||||
--Line_height--Heading--md: 48px;
|
||||
--Line_height--Heading--sm: 40px;
|
||||
--Line_height--Heading--xs: 32px;
|
||||
--Font_size--Body--lg: 18px;
|
||||
--Font_size--Body--md: 16px;
|
||||
--Font_size--Body--sm: 14px;
|
||||
--Font_size--Body--xs: 12px;
|
||||
--Font_weight--Italic: 400px;
|
||||
--Font_weight--Medium: 500px;
|
||||
--Font_weight--Bold: 700px;
|
||||
--Font_family--Body: Inter;
|
||||
--Paragraph_spacing--Heading--2xl: 60px;
|
||||
--Paragraph_spacing--Heading--xl: 48px;
|
||||
--Paragraph_spacing--Heading--lg: 40px;
|
||||
--Paragraph_spacing--Heading--md: 32px;
|
||||
--Paragraph_spacing--Heading--sm: 24px;
|
||||
--Paragraph_spacing--Heading--xs: 20px;
|
||||
--typography_components--h1--font-family: Inter;
|
||||
--typography_components--h2--font-family: Inter;
|
||||
--typography_components--h2--font-size: 30px;
|
||||
--typography_components--h2--line-height: 36px;
|
||||
--typography_components--h2--font-weight: 600;
|
||||
--typography_components--h2--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--h1--font-size: 36px;
|
||||
--typography_components--h1--font-size-lg: 48px;
|
||||
--typography_components--h1--line-height: 40px;
|
||||
--typography_components--h1--font-weight: 800;
|
||||
--typography_components--h1--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--h3--font-family: Inter;
|
||||
--typography_components--h3--font-size: 24px;
|
||||
--typography_components--h3--line-height: 32px;
|
||||
--typography_components--h3--font-weight: 600;
|
||||
--typography_components--h3--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--h4--font-family: Inter;
|
||||
--typography_components--h4--font-size: 20px;
|
||||
--typography_components--h4--line-height: 28px;
|
||||
--typography_components--h4--font-weight: 600;
|
||||
--typography_components--h4--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--p--font-family: Inter;
|
||||
--typography_components--p--font-size: 16px;
|
||||
--typography_components--p--line-height: 28px;
|
||||
--typography_components--p--font-weight: 400;
|
||||
--typography_components--p--letter-spacing: 0px;
|
||||
--typography_components--blockquote--font-family: Inter;
|
||||
--typography_components--blockquote--font-size: 16px;
|
||||
--typography_components--blockquote--line-height: 24px;
|
||||
--typography_components--blockquote--letter-spacing: 0px;
|
||||
--typography_components--blockquote--font-style: italic;
|
||||
--typography_components--list--font-family: Inter;
|
||||
--typography_components--list--font-size: 16px;
|
||||
--typography_components--list--line-height: 28px;
|
||||
--typography_components--list--letter-spacing: 0px;
|
||||
--typography_components--inline_code--font-family: Menlo;
|
||||
--typography_components--inline_code--font-size: 14px;
|
||||
--typography_components--inline_code--line-height: 20px;
|
||||
--typography_components--inline_code--font-weight: 600;
|
||||
--typography_components--inline_code--letter-spacing: 0px;
|
||||
--typography_components--lead--font-family: Inter;
|
||||
--typography_components--lead--font-size: 20px;
|
||||
--typography_components--lead--line-height: 28px;
|
||||
--typography_components--lead--font-weight: 400;
|
||||
--typography_components--lead--letter-spacing: 0px;
|
||||
--typography_components--large--font-family: Inter;
|
||||
--typography_components--large--font-size: 18px;
|
||||
--typography_components--large--line-height: 28px;
|
||||
--typography_components--large--font-weight: 600;
|
||||
--typography_components--large--letter-spacing: 0px;
|
||||
--typography_components--small--font-family: Inter;
|
||||
--typography_components--small--font-size: 14px;
|
||||
--typography_components--small--line-height: 14px;
|
||||
--typography_components--small--font-weight: 500;
|
||||
--typography_components--table--font-family: Inter;
|
||||
--typography_components--table--font-size: 16px;
|
||||
--typography_components--table--font-weight: 400;
|
||||
--typography_components--table--font-weight-bold: 700;
|
||||
--typography_components--table--letter-spacing: 0px;
|
||||
|
||||
/* Spacing and sizing: Desktop */
|
||||
--Border_width--sm: 1px;
|
||||
--Border_width--md: 2px;
|
||||
--Border_width--lg: 3px;
|
||||
--Radius--4xs: 4px;
|
||||
--Radius--3xs: 8px;
|
||||
--Radius--2xs: 12px;
|
||||
--Radius--xs: 16px;
|
||||
--Radius--sm: 24px;
|
||||
--Radius--md: 32px;
|
||||
--Radius--lg: 40px;
|
||||
--Radius--xl: 48px;
|
||||
--Radius--2xl: 56px;
|
||||
--Radius--3xl: 64px;
|
||||
--Radius--round: 999px;
|
||||
--Spacing--5xs: 4px;
|
||||
--Spacing--4xs: 8px;
|
||||
--Spacing--3xs: 12px;
|
||||
--Spacing--2xs: 16px;
|
||||
--Spacing--xs: 24px;
|
||||
--Spacing--sm: 32px;
|
||||
--Spacing--md: 40px;
|
||||
--Spacing--lg: 48px;
|
||||
--Spacing--xl: 56px;
|
||||
--Spacing--2xl: 64px;
|
||||
--Spacing--3xl: 72px;
|
||||
--Spacing--4xl: 80px;
|
||||
--Spacing--5xl: 88px;
|
||||
--Spacing--6xl: 96px;
|
||||
--Spacing--7xl: 104px;
|
||||
--Spacing--8xl: 112px;
|
||||
--Sizing--5xs: 4px;
|
||||
--Sizing--4xs: 8px;
|
||||
--Sizing--3xs: 12px;
|
||||
--Sizing--2xs: 16px;
|
||||
--Sizing--xs: 24px;
|
||||
--Sizing--sm: 32px;
|
||||
--Sizing--md: 40px;
|
||||
--Sizing--lg: 48px;
|
||||
--Sizing--xl: 56px;
|
||||
--Sizing--2xl: 64px;
|
||||
--Sizing--3xl: 72px;
|
||||
--Sizing--4xl: 80px;
|
||||
--Sizing--5xl: 88px;
|
||||
|
||||
/* Color: Light mode */
|
||||
--Opacity--Red--Red-100: var(--Primitive--Red--600);
|
||||
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
|
||||
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
|
||||
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
|
||||
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
|
||||
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
|
||||
--Opacity--Green--Green-100: var(--Primitive--Green--600);
|
||||
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
|
||||
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
|
||||
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
|
||||
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
|
||||
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
|
||||
--Opacity--Yellow--Yellow-100: var(--Primitive--Amber--400);
|
||||
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
|
||||
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
|
||||
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
|
||||
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
|
||||
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
|
||||
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
|
||||
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
|
||||
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
|
||||
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
|
||||
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
|
||||
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
|
||||
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
|
||||
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
|
||||
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
|
||||
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
|
||||
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
|
||||
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
|
||||
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
|
||||
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
|
||||
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
|
||||
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
|
||||
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
|
||||
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
|
||||
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
|
||||
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
|
||||
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
|
||||
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
|
||||
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
|
||||
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
|
||||
--Opacity--White--White-100: var(--Primitive--White);
|
||||
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
|
||||
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
|
||||
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
|
||||
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
|
||||
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
|
||||
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
|
||||
--Status--Error--colorErrorBg: var(--color--Red--50);
|
||||
--Status--Error--colorErrorBgHover: var(--color--Red--100);
|
||||
--Status--Error--colorErrorBorder: var(--color--Red--200);
|
||||
--Status--Error--colorErrorBorderHover: var(--color--Red--300);
|
||||
--Status--Error--colorErrorBase: var(--color--Red--500);
|
||||
--Status--Error--colorErrorActive: var(--color--Red--600);
|
||||
--Status--Error--colorErrorTextHover: var(--color--Red--700);
|
||||
--Status--Error--colorErrorText: var(--color--Red--800);
|
||||
--Status--Success--colorSuccessBg: var(--color--Green--50);
|
||||
--Status--Success--colorSuccessBgHover: var(--color--Green--100);
|
||||
--Status--Success--colorSuccessBase: var(--color--Green--500);
|
||||
--Status--Success--colorSuccessTextHover: var(--color--Green--700);
|
||||
--Status--Warning--colorWarningBg: var(--color--Yellow--50);
|
||||
--Status--Warning--colorWarningBgHover: var(--color--Yellow--100);
|
||||
--Status--Warning--colorWarningBase: var(--color--Yellow--500);
|
||||
--Status--Warning--colorWarningActive: var(--color--Yellow--600);
|
||||
--Status--Warning--colorWarningTextHover: var(--color--Yellow--700);
|
||||
--Primitive--Black: hsla(0, 0%, 0%, 1);
|
||||
--Primitive--White: hsla(0, 0%, 100%, 1);
|
||||
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
|
||||
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
|
||||
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
|
||||
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
|
||||
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
|
||||
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
|
||||
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
|
||||
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
|
||||
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
|
||||
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
|
||||
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
|
||||
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
|
||||
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
|
||||
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
|
||||
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
|
||||
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
|
||||
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
|
||||
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
|
||||
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
|
||||
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
|
||||
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
|
||||
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
|
||||
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
|
||||
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
|
||||
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
|
||||
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
|
||||
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
|
||||
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
|
||||
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
|
||||
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
|
||||
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
|
||||
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
|
||||
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
|
||||
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
|
||||
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
|
||||
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
|
||||
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
|
||||
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
|
||||
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
|
||||
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
|
||||
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
|
||||
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
|
||||
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
|
||||
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
|
||||
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
|
||||
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
|
||||
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
|
||||
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
|
||||
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
|
||||
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
|
||||
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
|
||||
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
|
||||
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
|
||||
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
|
||||
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
|
||||
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
|
||||
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
|
||||
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
|
||||
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
|
||||
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
|
||||
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
|
||||
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
|
||||
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
|
||||
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
|
||||
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
|
||||
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
|
||||
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
|
||||
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
|
||||
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
|
||||
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
|
||||
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
|
||||
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
|
||||
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
|
||||
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
|
||||
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
|
||||
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
|
||||
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
|
||||
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
|
||||
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
|
||||
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
|
||||
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
|
||||
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
|
||||
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
|
||||
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
|
||||
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
|
||||
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
|
||||
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
|
||||
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
|
||||
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
|
||||
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
|
||||
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
|
||||
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
|
||||
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
|
||||
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
|
||||
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
|
||||
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
|
||||
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
|
||||
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
|
||||
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
|
||||
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
|
||||
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
|
||||
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
|
||||
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
|
||||
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
|
||||
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
|
||||
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
|
||||
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
|
||||
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
|
||||
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
|
||||
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
|
||||
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
|
||||
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
|
||||
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
|
||||
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
|
||||
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
|
||||
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
|
||||
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
|
||||
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
|
||||
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
|
||||
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
|
||||
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
|
||||
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
|
||||
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
|
||||
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
|
||||
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
|
||||
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
|
||||
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
|
||||
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
|
||||
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
|
||||
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
|
||||
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
|
||||
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
|
||||
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
|
||||
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
|
||||
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
|
||||
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
|
||||
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
|
||||
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
|
||||
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
|
||||
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
|
||||
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
|
||||
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
|
||||
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
|
||||
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
|
||||
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
|
||||
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
|
||||
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
|
||||
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
|
||||
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
|
||||
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
|
||||
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
|
||||
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
|
||||
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
|
||||
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
|
||||
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
|
||||
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
|
||||
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
|
||||
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
|
||||
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
|
||||
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
|
||||
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
|
||||
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
|
||||
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
|
||||
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
|
||||
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
|
||||
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
|
||||
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
|
||||
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
|
||||
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
|
||||
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
|
||||
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
|
||||
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
|
||||
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
|
||||
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
|
||||
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
|
||||
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
|
||||
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
|
||||
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
|
||||
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
|
||||
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
|
||||
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
|
||||
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
|
||||
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
|
||||
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
|
||||
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
|
||||
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
|
||||
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
|
||||
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
|
||||
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
|
||||
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
|
||||
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
|
||||
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
|
||||
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
|
||||
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
|
||||
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
|
||||
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
|
||||
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
|
||||
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
|
||||
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
|
||||
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
|
||||
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
|
||||
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
|
||||
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
|
||||
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
|
||||
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
|
||||
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
|
||||
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
|
||||
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
|
||||
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
|
||||
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
|
||||
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
|
||||
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
|
||||
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
|
||||
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
|
||||
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
|
||||
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
|
||||
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
|
||||
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
|
||||
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
|
||||
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
|
||||
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
|
||||
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
|
||||
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
|
||||
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
|
||||
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
|
||||
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
|
||||
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
|
||||
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
|
||||
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
|
||||
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
|
||||
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
|
||||
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
|
||||
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
|
||||
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
|
||||
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
|
||||
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
|
||||
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
|
||||
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
|
||||
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
|
||||
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
|
||||
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
|
||||
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
|
||||
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
|
||||
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
|
||||
--Brand--Base_Colors--White: var(--Primitive--White);
|
||||
--Brand--Base_Colors--Black: var(--Primitive--Black);
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50); /*页面背景色:应用在整个页面的最底层。*/
|
||||
--Brand--Semantic_Colors--Background-subtle: hsla(
|
||||
0,
|
||||
0%,
|
||||
0%,
|
||||
0.02
|
||||
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
|
||||
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 0%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
|
||||
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 0%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
|
||||
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 0%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
|
||||
--Brand--Semantic_Colors--Border: hsla(0, 0%, 0%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
|
||||
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 0%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 0%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Ring: hsla(
|
||||
84,
|
||||
81%,
|
||||
44%,
|
||||
0.4
|
||||
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
|
||||
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.4);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 0%, 0.2);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 0%, 0.3);
|
||||
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
|
||||
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 0%, 0);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: hsla(0, 0%, 0%, 0.05);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 0%, 0.1);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background: hsla(0, 0%, 0%, 0.05);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: hsla(0, 0%, 0%, 0.85);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 0%, 0.7);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
|
||||
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
|
||||
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
|
||||
--Boolean: false;
|
||||
|
||||
/* Color: Dark mode */
|
||||
--Opacity--Red--Red-100: var(--Primitive--Red--600);
|
||||
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
|
||||
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
|
||||
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
|
||||
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
|
||||
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
|
||||
--Opacity--Green--Green-100: var(--Primitive--Green--600);
|
||||
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
|
||||
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
|
||||
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
|
||||
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
|
||||
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
|
||||
--Opacity--Yellow--Yellow-100: var(--Primitive--Yellow--400);
|
||||
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
|
||||
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
|
||||
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
|
||||
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
|
||||
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
|
||||
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
|
||||
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
|
||||
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
|
||||
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
|
||||
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
|
||||
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
|
||||
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
|
||||
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
|
||||
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
|
||||
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
|
||||
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
|
||||
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
|
||||
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
|
||||
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
|
||||
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
|
||||
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
|
||||
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
|
||||
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
|
||||
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
|
||||
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
|
||||
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
|
||||
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
|
||||
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
|
||||
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
|
||||
--Opacity--White--White-100: var(--Primitive--White);
|
||||
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
|
||||
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
|
||||
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
|
||||
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
|
||||
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
|
||||
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
|
||||
--Status--Error--colorErrorBg: var(--color--Red--900);
|
||||
--Status--Error--colorErrorBgHover: var(--color--Red--800);
|
||||
--Status--Error--colorErrorBorder: var(--color--Red--700);
|
||||
--Status--Error--colorErrorBorderHover: var(--color--Red--600);
|
||||
--Status--Error--colorErrorBase: var(--color--Red--400);
|
||||
--Status--Error--colorErrorActive: var(--color--Red--300);
|
||||
--Status--Error--colorErrorTextHover: var(--color--Red--200);
|
||||
--Status--Error--colorErrorText: var(--color--Red--100);
|
||||
--Status--Success--colorSuccessBg: var(--color--Green--900);
|
||||
--Status--Success--colorSuccessBgHover: var(--color--Green--800);
|
||||
--Status--Success--colorSuccessBase: var(--color--Green--400);
|
||||
--Status--Success--colorSuccessTextHover: var(--color--Green--200);
|
||||
--Status--Warning--colorWarningBg: var(--color--Yellow--900);
|
||||
--Status--Warning--colorWarningBgHover: var(--color--Yellow--800);
|
||||
--Status--Warning--colorWarningBase: var(--color--Yellow--400);
|
||||
--Status--Warning--colorWarningActive: var(--color--Yellow--300);
|
||||
--Status--Warning--colorWarningTextHover: var(--color--Yellow--200);
|
||||
--Primitive--Black: hsla(0, 0%, 0%, 1);
|
||||
--Primitive--White: hsla(0, 0%, 100%, 1);
|
||||
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
|
||||
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
|
||||
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
|
||||
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
|
||||
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
|
||||
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
|
||||
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
|
||||
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
|
||||
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
|
||||
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
|
||||
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
|
||||
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
|
||||
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
|
||||
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
|
||||
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
|
||||
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
|
||||
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
|
||||
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
|
||||
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
|
||||
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
|
||||
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
|
||||
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
|
||||
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
|
||||
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
|
||||
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
|
||||
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
|
||||
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
|
||||
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
|
||||
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
|
||||
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
|
||||
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
|
||||
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
|
||||
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
|
||||
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
|
||||
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
|
||||
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
|
||||
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
|
||||
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
|
||||
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
|
||||
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
|
||||
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
|
||||
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
|
||||
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
|
||||
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
|
||||
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
|
||||
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
|
||||
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
|
||||
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
|
||||
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
|
||||
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
|
||||
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
|
||||
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
|
||||
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
|
||||
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
|
||||
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
|
||||
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
|
||||
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
|
||||
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
|
||||
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
|
||||
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
|
||||
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
|
||||
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
|
||||
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
|
||||
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
|
||||
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
|
||||
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
|
||||
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
|
||||
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
|
||||
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
|
||||
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
|
||||
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
|
||||
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
|
||||
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
|
||||
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
|
||||
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
|
||||
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
|
||||
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
|
||||
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
|
||||
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
|
||||
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
|
||||
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
|
||||
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
|
||||
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
|
||||
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
|
||||
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
|
||||
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
|
||||
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
|
||||
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
|
||||
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
|
||||
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
|
||||
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
|
||||
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
|
||||
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
|
||||
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
|
||||
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
|
||||
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
|
||||
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
|
||||
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
|
||||
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
|
||||
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
|
||||
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
|
||||
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
|
||||
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
|
||||
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
|
||||
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
|
||||
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
|
||||
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
|
||||
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
|
||||
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
|
||||
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
|
||||
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
|
||||
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
|
||||
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
|
||||
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
|
||||
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
|
||||
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
|
||||
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
|
||||
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
|
||||
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
|
||||
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
|
||||
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
|
||||
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
|
||||
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
|
||||
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
|
||||
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
|
||||
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
|
||||
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
|
||||
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
|
||||
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
|
||||
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
|
||||
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
|
||||
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
|
||||
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
|
||||
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
|
||||
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
|
||||
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
|
||||
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
|
||||
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
|
||||
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
|
||||
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
|
||||
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
|
||||
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
|
||||
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
|
||||
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
|
||||
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
|
||||
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
|
||||
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
|
||||
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
|
||||
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
|
||||
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
|
||||
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
|
||||
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
|
||||
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
|
||||
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
|
||||
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
|
||||
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
|
||||
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
|
||||
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
|
||||
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
|
||||
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
|
||||
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
|
||||
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
|
||||
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
|
||||
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
|
||||
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
|
||||
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
|
||||
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
|
||||
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
|
||||
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
|
||||
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
|
||||
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
|
||||
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
|
||||
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
|
||||
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
|
||||
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
|
||||
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
|
||||
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
|
||||
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
|
||||
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
|
||||
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
|
||||
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
|
||||
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
|
||||
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
|
||||
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
|
||||
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
|
||||
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
|
||||
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
|
||||
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
|
||||
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
|
||||
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
|
||||
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
|
||||
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
|
||||
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
|
||||
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
|
||||
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
|
||||
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
|
||||
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
|
||||
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
|
||||
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
|
||||
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
|
||||
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
|
||||
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
|
||||
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
|
||||
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
|
||||
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
|
||||
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
|
||||
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
|
||||
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
|
||||
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
|
||||
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
|
||||
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
|
||||
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
|
||||
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
|
||||
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
|
||||
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
|
||||
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
|
||||
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
|
||||
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
|
||||
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
|
||||
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
|
||||
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
|
||||
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
|
||||
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
|
||||
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
|
||||
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
|
||||
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
|
||||
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
|
||||
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
|
||||
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
|
||||
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
|
||||
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
|
||||
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
|
||||
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
|
||||
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
|
||||
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
|
||||
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
|
||||
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
|
||||
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
|
||||
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
|
||||
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
|
||||
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
|
||||
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
|
||||
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
|
||||
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
|
||||
--Brand--Base_Colors--White: var(--Primitive--White);
|
||||
--Brand--Base_Colors--Black: var(--Primitive--Black);
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /*页面背景色:应用在整个页面的最底层。*/
|
||||
--Brand--Semantic_Colors--Background-subtle: hsla(
|
||||
0,
|
||||
0%,
|
||||
100%,
|
||||
0.02
|
||||
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
|
||||
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 100%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
|
||||
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 100%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
|
||||
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 100%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
|
||||
--Brand--Semantic_Colors--Border: hsla(0, 0%, 100%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
|
||||
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 100%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 100%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Ring: hsla(
|
||||
84,
|
||||
81%,
|
||||
44%,
|
||||
0.4
|
||||
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
|
||||
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.06);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 100%, 0.2);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 100%, 0.3);
|
||||
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
|
||||
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--Black);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--Black);
|
||||
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 100%, 0);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: var(--Opacity--White--White-10);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 100%, 0.15);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background: var(--Opacity--White--White-10);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: var(--Opacity--White--White-20);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 100%, 0.25);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
|
||||
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
|
||||
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
|
||||
--Boolean: false;
|
||||
}
|
||||
@@ -124,7 +124,11 @@
|
||||
"import": "./dist/utils/index.mjs",
|
||||
"require": "./dist/utils/index.js",
|
||||
"default": "./dist/utils/index.js"
|
||||
}
|
||||
},
|
||||
"./styles": "./src/styles/index.css",
|
||||
"./styles/tokens.css": "./src/styles/tokens.css",
|
||||
"./styles/theme.css": "./src/styles/theme.css",
|
||||
"./styles/index.css": "./src/styles/index.css"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
24
packages/ui/src/styles/index.css
Normal file
24
packages/ui/src/styles/index.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* CherryStudio Styles - npm 包入口
|
||||
*
|
||||
* 用途:仅导出 CSS 变量,不生成工具类,不覆盖用户主题
|
||||
* 使用场景:
|
||||
* - npm 用户想使用设计系统的变量,但不想覆盖 Tailwind 默认主题
|
||||
* - 可以通过 var(--cs-primary) 等方式使用
|
||||
* - 不会生成 bg-primary 等工具类(用户自己的 bg-primary 不受影响)
|
||||
*
|
||||
* 示例:
|
||||
* ```css
|
||||
* @import '@cherrystudio/ui/styles/index.css';
|
||||
*
|
||||
* .my-button {
|
||||
* background: var(--cs-primary); // 使用 CS 的品牌色
|
||||
* padding: var(--cs-size-md); // 使用 CS 的间距
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 如果想要完整的主题覆盖(生成 bg-primary 等类),请导入:
|
||||
* @import '@cherrystudio/ui/styles/theme.css';
|
||||
*/
|
||||
|
||||
@import './tokens.css';
|
||||
433
packages/ui/src/styles/theme.css
Normal file
433
packages/ui/src/styles/theme.css
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* CherryStudio Theme - Tailwind CSS 主题配置
|
||||
*
|
||||
* 用途:映射设计师的设计系统(--cs-*)到 Tailwind 标准命名
|
||||
* 使用场景:
|
||||
* - 主包:完全使用设计系统
|
||||
* - npm 用户:可选择导入,会覆盖 Tailwind 默认主题
|
||||
*
|
||||
* 生成的工具类:bg-primary, bg-red-500, p-md 等(无前缀)
|
||||
*/
|
||||
|
||||
@import 'tailwindcss';
|
||||
@import './tokens.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
/* ==================== */
|
||||
/* 语义化颜色(覆盖 Tailwind 标准命名) */
|
||||
/* ==================== */
|
||||
--color-primary: var(--cs-primary);
|
||||
--color-primary-foreground: var(--cs-white);
|
||||
|
||||
--color-destructive: var(--cs-destructive);
|
||||
--color-destructive-foreground: var(--cs-white);
|
||||
|
||||
--color-success: var(--cs-success);
|
||||
--color-success-foreground: var(--cs-white);
|
||||
|
||||
--color-warning: var(--cs-warning);
|
||||
--color-warning-foreground: var(--cs-black);
|
||||
|
||||
--color-background: var(--cs-background);
|
||||
--color-foreground: var(--cs-foreground);
|
||||
|
||||
--color-card: var(--cs-card);
|
||||
--color-card-foreground: var(--cs-foreground);
|
||||
|
||||
--color-popover: var(--cs-popover);
|
||||
--color-popover-foreground: var(--cs-foreground);
|
||||
|
||||
--color-secondary: var(--cs-secondary);
|
||||
--color-secondary-foreground: var(--cs-foreground);
|
||||
|
||||
--color-muted: var(--cs-muted);
|
||||
--color-muted-foreground: var(--cs-foreground-muted);
|
||||
|
||||
--color-accent: var(--cs-accent);
|
||||
--color-accent-foreground: var(--cs-foreground);
|
||||
|
||||
--color-border: var(--cs-border);
|
||||
--color-input: var(--cs-border);
|
||||
--color-ring: var(--cs-ring);
|
||||
|
||||
/* 侧边栏 */
|
||||
--color-sidebar: var(--cs-sidebar);
|
||||
--color-sidebar-foreground: var(--cs-foreground);
|
||||
--color-sidebar-primary: var(--cs-primary);
|
||||
--color-sidebar-primary-foreground: var(--cs-white);
|
||||
--color-sidebar-accent: var(--cs-sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--cs-foreground);
|
||||
--color-sidebar-border: var(--cs-border);
|
||||
--color-sidebar-ring: var(--cs-ring);
|
||||
|
||||
/* 图表颜色 */
|
||||
--color-chart-1: var(--cs-blue-500);
|
||||
--color-chart-2: var(--cs-emerald-500);
|
||||
--color-chart-3: var(--cs-purple-500);
|
||||
--color-chart-4: var(--cs-amber-500);
|
||||
--color-chart-5: var(--cs-orange-500);
|
||||
|
||||
/* ==================== */
|
||||
/* Primitive 颜色(覆盖 Tailwind 默认色板) */
|
||||
/* ==================== */
|
||||
|
||||
/* Neutral */
|
||||
--color-neutral-50: var(--cs-neutral-50);
|
||||
--color-neutral-100: var(--cs-neutral-100);
|
||||
--color-neutral-200: var(--cs-neutral-200);
|
||||
--color-neutral-300: var(--cs-neutral-300);
|
||||
--color-neutral-400: var(--cs-neutral-400);
|
||||
--color-neutral-500: var(--cs-neutral-500);
|
||||
--color-neutral-600: var(--cs-neutral-600);
|
||||
--color-neutral-700: var(--cs-neutral-700);
|
||||
--color-neutral-800: var(--cs-neutral-800);
|
||||
--color-neutral-900: var(--cs-neutral-900);
|
||||
--color-neutral-950: var(--cs-neutral-950);
|
||||
|
||||
/* Stone */
|
||||
--color-stone-50: var(--cs-stone-50);
|
||||
--color-stone-100: var(--cs-stone-100);
|
||||
--color-stone-200: var(--cs-stone-200);
|
||||
--color-stone-300: var(--cs-stone-300);
|
||||
--color-stone-400: var(--cs-stone-400);
|
||||
--color-stone-500: var(--cs-stone-500);
|
||||
--color-stone-600: var(--cs-stone-600);
|
||||
--color-stone-700: var(--cs-stone-700);
|
||||
--color-stone-800: var(--cs-stone-800);
|
||||
--color-stone-900: var(--cs-stone-900);
|
||||
--color-stone-950: var(--cs-stone-950);
|
||||
|
||||
/* Zinc */
|
||||
--color-zinc-50: var(--cs-zinc-50);
|
||||
--color-zinc-100: var(--cs-zinc-100);
|
||||
--color-zinc-200: var(--cs-zinc-200);
|
||||
--color-zinc-300: var(--cs-zinc-300);
|
||||
--color-zinc-400: var(--cs-zinc-400);
|
||||
--color-zinc-500: var(--cs-zinc-500);
|
||||
--color-zinc-600: var(--cs-zinc-600);
|
||||
--color-zinc-700: var(--cs-zinc-700);
|
||||
--color-zinc-800: var(--cs-zinc-800);
|
||||
--color-zinc-900: var(--cs-zinc-900);
|
||||
--color-zinc-950: var(--cs-zinc-950);
|
||||
|
||||
/* Slate */
|
||||
--color-slate-50: var(--cs-slate-50);
|
||||
--color-slate-100: var(--cs-slate-100);
|
||||
--color-slate-200: var(--cs-slate-200);
|
||||
--color-slate-300: var(--cs-slate-300);
|
||||
--color-slate-400: var(--cs-slate-400);
|
||||
--color-slate-500: var(--cs-slate-500);
|
||||
--color-slate-600: var(--cs-slate-600);
|
||||
--color-slate-700: var(--cs-slate-700);
|
||||
--color-slate-800: var(--cs-slate-800);
|
||||
--color-slate-900: var(--cs-slate-900);
|
||||
--color-slate-950: var(--cs-slate-950);
|
||||
|
||||
/* Gray */
|
||||
--color-gray-50: var(--cs-gray-50);
|
||||
--color-gray-100: var(--cs-gray-100);
|
||||
--color-gray-200: var(--cs-gray-200);
|
||||
--color-gray-300: var(--cs-gray-300);
|
||||
--color-gray-400: var(--cs-gray-400);
|
||||
--color-gray-500: var(--cs-gray-500);
|
||||
--color-gray-600: var(--cs-gray-600);
|
||||
--color-gray-700: var(--cs-gray-700);
|
||||
--color-gray-800: var(--cs-gray-800);
|
||||
--color-gray-900: var(--cs-gray-900);
|
||||
--color-gray-950: var(--cs-gray-950);
|
||||
|
||||
/* Red */
|
||||
--color-red-50: var(--cs-red-50);
|
||||
--color-red-100: var(--cs-red-100);
|
||||
--color-red-200: var(--cs-red-200);
|
||||
--color-red-300: var(--cs-red-300);
|
||||
--color-red-400: var(--cs-red-400);
|
||||
--color-red-500: var(--cs-red-500);
|
||||
--color-red-600: var(--cs-red-600);
|
||||
--color-red-700: var(--cs-red-700);
|
||||
--color-red-800: var(--cs-red-800);
|
||||
--color-red-900: var(--cs-red-900);
|
||||
--color-red-950: var(--cs-red-950);
|
||||
|
||||
/* Orange */
|
||||
--color-orange-50: var(--cs-orange-50);
|
||||
--color-orange-100: var(--cs-orange-100);
|
||||
--color-orange-200: var(--cs-orange-200);
|
||||
--color-orange-300: var(--cs-orange-300);
|
||||
--color-orange-400: var(--cs-orange-400);
|
||||
--color-orange-500: var(--cs-orange-500);
|
||||
--color-orange-600: var(--cs-orange-600);
|
||||
--color-orange-700: var(--cs-orange-700);
|
||||
--color-orange-800: var(--cs-orange-800);
|
||||
--color-orange-900: var(--cs-orange-900);
|
||||
--color-orange-950: var(--cs-orange-950);
|
||||
|
||||
/* Amber */
|
||||
--color-amber-50: var(--cs-amber-50);
|
||||
--color-amber-100: var(--cs-amber-100);
|
||||
--color-amber-200: var(--cs-amber-200);
|
||||
--color-amber-300: var(--cs-amber-300);
|
||||
--color-amber-400: var(--cs-amber-400);
|
||||
--color-amber-500: var(--cs-amber-500);
|
||||
--color-amber-600: var(--cs-amber-600);
|
||||
--color-amber-700: var(--cs-amber-700);
|
||||
--color-amber-800: var(--cs-amber-800);
|
||||
--color-amber-900: var(--cs-amber-900);
|
||||
--color-amber-950: var(--cs-amber-950);
|
||||
|
||||
/* Yellow */
|
||||
--color-yellow-50: var(--cs-yellow-50);
|
||||
--color-yellow-100: var(--cs-yellow-100);
|
||||
--color-yellow-200: var(--cs-yellow-200);
|
||||
--color-yellow-300: var(--cs-yellow-300);
|
||||
--color-yellow-400: var(--cs-yellow-400);
|
||||
--color-yellow-500: var(--cs-yellow-500);
|
||||
--color-yellow-600: var(--cs-yellow-600);
|
||||
--color-yellow-700: var(--cs-yellow-700);
|
||||
--color-yellow-800: var(--cs-yellow-800);
|
||||
--color-yellow-900: var(--cs-yellow-900);
|
||||
--color-yellow-950: var(--cs-yellow-950);
|
||||
|
||||
/* Lime (品牌主色) */
|
||||
--color-lime-50: var(--cs-lime-50);
|
||||
--color-lime-100: var(--cs-lime-100);
|
||||
--color-lime-200: var(--cs-lime-200);
|
||||
--color-lime-300: var(--cs-lime-300);
|
||||
--color-lime-400: var(--cs-lime-400);
|
||||
--color-lime-500: var(--cs-lime-500);
|
||||
--color-lime-600: var(--cs-lime-600);
|
||||
--color-lime-700: var(--cs-lime-700);
|
||||
--color-lime-800: var(--cs-lime-800);
|
||||
--color-lime-900: var(--cs-lime-900);
|
||||
--color-lime-950: var(--cs-lime-950);
|
||||
|
||||
/* Green */
|
||||
--color-green-50: var(--cs-green-50);
|
||||
--color-green-100: var(--cs-green-100);
|
||||
--color-green-200: var(--cs-green-200);
|
||||
--color-green-300: var(--cs-green-300);
|
||||
--color-green-400: var(--cs-green-400);
|
||||
--color-green-500: var(--cs-green-500);
|
||||
--color-green-600: var(--cs-green-600);
|
||||
--color-green-700: var(--cs-green-700);
|
||||
--color-green-800: var(--cs-green-800);
|
||||
--color-green-900: var(--cs-green-900);
|
||||
--color-green-950: var(--cs-green-950);
|
||||
|
||||
/* Emerald */
|
||||
--color-emerald-50: var(--cs-emerald-50);
|
||||
--color-emerald-100: var(--cs-emerald-100);
|
||||
--color-emerald-200: var(--cs-emerald-200);
|
||||
--color-emerald-300: var(--cs-emerald-300);
|
||||
--color-emerald-400: var(--cs-emerald-400);
|
||||
--color-emerald-500: var(--cs-emerald-500);
|
||||
--color-emerald-600: var(--cs-emerald-600);
|
||||
--color-emerald-700: var(--cs-emerald-700);
|
||||
--color-emerald-800: var(--cs-emerald-800);
|
||||
--color-emerald-900: var(--cs-emerald-900);
|
||||
--color-emerald-950: var(--cs-emerald-950);
|
||||
|
||||
/* Teal */
|
||||
--color-teal-50: var(--cs-teal-50);
|
||||
--color-teal-100: var(--cs-teal-100);
|
||||
--color-teal-200: var(--cs-teal-200);
|
||||
--color-teal-300: var(--cs-teal-300);
|
||||
--color-teal-400: var(--cs-teal-400);
|
||||
--color-teal-500: var(--cs-teal-500);
|
||||
--color-teal-600: var(--cs-teal-600);
|
||||
--color-teal-700: var(--cs-teal-700);
|
||||
--color-teal-800: var(--cs-teal-800);
|
||||
--color-teal-900: var(--cs-teal-900);
|
||||
--color-teal-950: var(--cs-teal-950);
|
||||
|
||||
/* Cyan */
|
||||
--color-cyan-50: var(--cs-cyan-50);
|
||||
--color-cyan-100: var(--cs-cyan-100);
|
||||
--color-cyan-200: var(--cs-cyan-200);
|
||||
--color-cyan-300: var(--cs-cyan-300);
|
||||
--color-cyan-400: var(--cs-cyan-400);
|
||||
--color-cyan-500: var(--cs-cyan-500);
|
||||
--color-cyan-600: var(--cs-cyan-600);
|
||||
--color-cyan-700: var(--cs-cyan-700);
|
||||
--color-cyan-800: var(--cs-cyan-800);
|
||||
--color-cyan-900: var(--cs-cyan-900);
|
||||
--color-cyan-950: var(--cs-cyan-950);
|
||||
|
||||
/* Sky */
|
||||
--color-sky-50: var(--cs-sky-50);
|
||||
--color-sky-100: var(--cs-sky-100);
|
||||
--color-sky-200: var(--cs-sky-200);
|
||||
--color-sky-300: var(--cs-sky-300);
|
||||
--color-sky-400: var(--cs-sky-400);
|
||||
--color-sky-500: var(--cs-sky-500);
|
||||
--color-sky-600: var(--cs-sky-600);
|
||||
--color-sky-700: var(--cs-sky-700);
|
||||
--color-sky-800: var(--cs-sky-800);
|
||||
--color-sky-900: var(--cs-sky-900);
|
||||
--color-sky-950: var(--cs-sky-950);
|
||||
|
||||
/* Blue */
|
||||
--color-blue-50: var(--cs-blue-50);
|
||||
--color-blue-100: var(--cs-blue-100);
|
||||
--color-blue-200: var(--cs-blue-200);
|
||||
--color-blue-300: var(--cs-blue-300);
|
||||
--color-blue-400: var(--cs-blue-400);
|
||||
--color-blue-500: var(--cs-blue-500);
|
||||
--color-blue-600: var(--cs-blue-600);
|
||||
--color-blue-700: var(--cs-blue-700);
|
||||
--color-blue-800: var(--cs-blue-800);
|
||||
--color-blue-900: var(--cs-blue-900);
|
||||
--color-blue-950: var(--cs-blue-950);
|
||||
|
||||
/* Indigo */
|
||||
--color-indigo-50: var(--cs-indigo-50);
|
||||
--color-indigo-100: var(--cs-indigo-100);
|
||||
--color-indigo-200: var(--cs-indigo-200);
|
||||
--color-indigo-300: var(--cs-indigo-300);
|
||||
--color-indigo-400: var(--cs-indigo-400);
|
||||
--color-indigo-500: var(--cs-indigo-500);
|
||||
--color-indigo-600: var(--cs-indigo-600);
|
||||
--color-indigo-700: var(--cs-indigo-700);
|
||||
--color-indigo-800: var(--cs-indigo-800);
|
||||
--color-indigo-900: var(--cs-indigo-900);
|
||||
--color-indigo-950: var(--cs-indigo-950);
|
||||
|
||||
/* Violet */
|
||||
--color-violet-50: var(--cs-violet-50);
|
||||
--color-violet-100: var(--cs-violet-100);
|
||||
--color-violet-200: var(--cs-violet-200);
|
||||
--color-violet-300: var(--cs-violet-300);
|
||||
--color-violet-400: var(--cs-violet-400);
|
||||
--color-violet-500: var(--cs-violet-500);
|
||||
--color-violet-600: var(--cs-violet-600);
|
||||
--color-violet-700: var(--cs-violet-700);
|
||||
--color-violet-800: var(--cs-violet-800);
|
||||
--color-violet-900: var(--cs-violet-900);
|
||||
--color-violet-950: var(--cs-violet-950);
|
||||
|
||||
/* Purple */
|
||||
--color-purple-50: var(--cs-purple-50);
|
||||
--color-purple-100: var(--cs-purple-100);
|
||||
--color-purple-200: var(--cs-purple-200);
|
||||
--color-purple-300: var(--cs-purple-300);
|
||||
--color-purple-400: var(--cs-purple-400);
|
||||
--color-purple-500: var(--cs-purple-500);
|
||||
--color-purple-600: var(--cs-purple-600);
|
||||
--color-purple-700: var(--cs-purple-700);
|
||||
--color-purple-800: var(--cs-purple-800);
|
||||
--color-purple-900: var(--cs-purple-900);
|
||||
--color-purple-950: var(--cs-purple-950);
|
||||
|
||||
/* Fuchsia */
|
||||
--color-fuchsia-50: var(--cs-fuchsia-50);
|
||||
--color-fuchsia-100: var(--cs-fuchsia-100);
|
||||
--color-fuchsia-200: var(--cs-fuchsia-200);
|
||||
--color-fuchsia-300: var(--cs-fuchsia-300);
|
||||
--color-fuchsia-400: var(--cs-fuchsia-400);
|
||||
--color-fuchsia-500: var(--cs-fuchsia-500);
|
||||
--color-fuchsia-600: var(--cs-fuchsia-600);
|
||||
--color-fuchsia-700: var(--cs-fuchsia-700);
|
||||
--color-fuchsia-800: var(--cs-fuchsia-800);
|
||||
--color-fuchsia-900: var(--cs-fuchsia-900);
|
||||
--color-fuchsia-950: var(--cs-fuchsia-950);
|
||||
|
||||
/* Pink */
|
||||
--color-pink-50: var(--cs-pink-50);
|
||||
--color-pink-100: var(--cs-pink-100);
|
||||
--color-pink-200: var(--cs-pink-200);
|
||||
--color-pink-300: var(--cs-pink-300);
|
||||
--color-pink-400: var(--cs-pink-400);
|
||||
--color-pink-500: var(--cs-pink-500);
|
||||
--color-pink-600: var(--cs-pink-600);
|
||||
--color-pink-700: var(--cs-pink-700);
|
||||
--color-pink-800: var(--cs-pink-800);
|
||||
--color-pink-900: var(--cs-pink-900);
|
||||
--color-pink-950: var(--cs-pink-950);
|
||||
|
||||
/* Rose */
|
||||
--color-rose-50: var(--cs-rose-50);
|
||||
--color-rose-100: var(--cs-rose-100);
|
||||
--color-rose-200: var(--cs-rose-200);
|
||||
--color-rose-300: var(--cs-rose-300);
|
||||
--color-rose-400: var(--cs-rose-400);
|
||||
--color-rose-500: var(--cs-rose-500);
|
||||
--color-rose-600: var(--cs-rose-600);
|
||||
--color-rose-700: var(--cs-rose-700);
|
||||
--color-rose-800: var(--cs-rose-800);
|
||||
--color-rose-900: var(--cs-rose-900);
|
||||
--color-rose-950: var(--cs-rose-950);
|
||||
|
||||
/* Black & White */
|
||||
--color-black: var(--cs-black);
|
||||
--color-white: var(--cs-white);
|
||||
|
||||
/* ==================== */
|
||||
/* Spacing & Sizing(自定义尺寸,额外扩展) */
|
||||
/* ==================== */
|
||||
|
||||
--spacing-5xs: var(--cs-size-5xs);
|
||||
--spacing-4xs: var(--cs-size-4xs);
|
||||
--spacing-3xs: var(--cs-size-3xs);
|
||||
--spacing-2xs: var(--cs-size-2xs);
|
||||
--spacing-xs: var(--cs-size-xs);
|
||||
--spacing-sm: var(--cs-size-sm);
|
||||
--spacing-md: var(--cs-size-md);
|
||||
--spacing-lg: var(--cs-size-lg);
|
||||
--spacing-xl: var(--cs-size-xl);
|
||||
--spacing-2xl: var(--cs-size-2xl);
|
||||
--spacing-3xl: var(--cs-size-3xl);
|
||||
--spacing-4xl: var(--cs-size-4xl);
|
||||
--spacing-5xl: var(--cs-size-5xl);
|
||||
--spacing-6xl: var(--cs-size-6xl);
|
||||
--spacing-7xl: var(--cs-size-7xl);
|
||||
--spacing-8xl: var(--cs-size-8xl);
|
||||
|
||||
/* ==================== */
|
||||
/* Border Radius(自定义圆角,额外扩展) */
|
||||
/* ==================== */
|
||||
|
||||
--radius-4xs: var(--cs-radius-4xs);
|
||||
--radius-3xs: var(--cs-radius-3xs);
|
||||
--radius-2xs: var(--cs-radius-2xs);
|
||||
--radius-xs: var(--cs-radius-xs);
|
||||
--radius-sm: var(--cs-radius-sm);
|
||||
--radius-md: var(--cs-radius-md);
|
||||
--radius-lg: var(--cs-radius-lg);
|
||||
--radius-xl: var(--cs-radius-xl);
|
||||
--radius-2xl: var(--cs-radius-2xl);
|
||||
--radius-3xl: var(--cs-radius-3xl);
|
||||
--radius-round: var(--cs-radius-round);
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Dark Mode 覆盖 */
|
||||
/* ==================== */
|
||||
|
||||
@theme dark {
|
||||
--color-background: var(--cs-background);
|
||||
--color-foreground: var(--cs-foreground);
|
||||
--color-card: var(--cs-card);
|
||||
--color-popover: var(--cs-popover);
|
||||
--color-border: var(--cs-border);
|
||||
--color-input: var(--cs-border);
|
||||
--color-secondary: var(--cs-secondary);
|
||||
--color-muted: var(--cs-muted);
|
||||
--color-accent: var(--cs-accent);
|
||||
--color-sidebar: var(--cs-sidebar);
|
||||
--color-sidebar-accent: var(--cs-sidebar-accent);
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Base Styles(可选) */
|
||||
/* ==================== */
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
560
packages/ui/src/styles/tokens.css
Normal file
560
packages/ui/src/styles/tokens.css
Normal file
@@ -0,0 +1,560 @@
|
||||
/**
|
||||
* CherryStudio Design Tokens
|
||||
*
|
||||
* 基于 todocss.css 转换的设计令牌
|
||||
* - 所有变量使用 --cs-* 前缀(CherryStudio)
|
||||
* - 合并重复的 Spacing/Sizing
|
||||
* - 修正错误(如 font-weight 单位)
|
||||
* - 删除 Opacity 变量(使用 Tailwind 修饰符)
|
||||
* - 正确分离 Light/Dark Mode
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ==================== */
|
||||
/* Primitive Colors - Light Mode */
|
||||
/* ==================== */
|
||||
|
||||
/* Neutral */
|
||||
--cs-neutral-50: hsla(0, 0%, 98%, 1);
|
||||
--cs-neutral-100: hsla(0, 0%, 96%, 1);
|
||||
--cs-neutral-200: hsla(0, 0%, 90%, 1);
|
||||
--cs-neutral-300: hsla(0, 0%, 83%, 1);
|
||||
--cs-neutral-400: hsla(0, 0%, 64%, 1);
|
||||
--cs-neutral-500: hsla(0, 0%, 45%, 1);
|
||||
--cs-neutral-600: hsla(215, 14%, 34%, 1);
|
||||
--cs-neutral-700: hsla(0, 0%, 25%, 1);
|
||||
--cs-neutral-800: hsla(0, 0%, 15%, 1);
|
||||
--cs-neutral-900: hsla(0, 0%, 9%, 1);
|
||||
--cs-neutral-950: hsla(0, 0%, 4%, 1);
|
||||
|
||||
/* Stone */
|
||||
--cs-stone-50: hsla(60, 9%, 98%, 1);
|
||||
--cs-stone-100: hsla(60, 5%, 96%, 1);
|
||||
--cs-stone-200: hsla(20, 6%, 90%, 1);
|
||||
--cs-stone-300: hsla(24, 6%, 83%, 1);
|
||||
--cs-stone-400: hsla(24, 5%, 64%, 1);
|
||||
--cs-stone-500: hsla(25, 5%, 45%, 1);
|
||||
--cs-stone-600: hsla(33, 5%, 32%, 1);
|
||||
--cs-stone-700: hsla(30, 6%, 25%, 1);
|
||||
--cs-stone-800: hsla(12, 6%, 15%, 1);
|
||||
--cs-stone-900: hsla(24, 10%, 10%, 1);
|
||||
--cs-stone-950: hsla(20, 14%, 4%, 1);
|
||||
|
||||
/* Zinc */
|
||||
--cs-zinc-50: hsla(0, 0%, 98%, 1);
|
||||
--cs-zinc-100: hsla(240, 5%, 96%, 1);
|
||||
--cs-zinc-200: hsla(240, 6%, 90%, 1);
|
||||
--cs-zinc-300: hsla(240, 5%, 84%, 1);
|
||||
--cs-zinc-400: hsla(240, 5%, 65%, 1);
|
||||
--cs-zinc-500: hsla(240, 4%, 46%, 1);
|
||||
--cs-zinc-600: hsla(240, 5%, 34%, 1);
|
||||
--cs-zinc-700: hsla(240, 5%, 26%, 1);
|
||||
--cs-zinc-800: hsla(240, 4%, 16%, 1);
|
||||
--cs-zinc-900: hsla(240, 6%, 10%, 1);
|
||||
--cs-zinc-950: hsla(240, 10%, 4%, 1);
|
||||
|
||||
/* Slate */
|
||||
--cs-slate-50: hsla(210, 40%, 98%, 1);
|
||||
--cs-slate-100: hsla(210, 40%, 96%, 1);
|
||||
--cs-slate-200: hsla(214, 32%, 91%, 1);
|
||||
--cs-slate-300: hsla(213, 27%, 84%, 1);
|
||||
--cs-slate-400: hsla(215, 20%, 65%, 1);
|
||||
--cs-slate-500: hsla(215, 16%, 47%, 1);
|
||||
--cs-slate-600: hsla(215, 19%, 35%, 1);
|
||||
--cs-slate-700: hsla(215, 25%, 27%, 1);
|
||||
--cs-slate-800: hsla(217, 33%, 17%, 1);
|
||||
--cs-slate-900: hsla(222, 47%, 11%, 1);
|
||||
--cs-slate-950: hsla(229, 84%, 5%, 1);
|
||||
|
||||
/* Gray */
|
||||
--cs-gray-50: hsla(210, 20%, 98%, 1);
|
||||
--cs-gray-100: hsla(220, 14%, 96%, 1);
|
||||
--cs-gray-200: hsla(220, 13%, 91%, 1);
|
||||
--cs-gray-300: hsla(216, 12%, 84%, 1);
|
||||
--cs-gray-400: hsla(218, 11%, 65%, 1);
|
||||
--cs-gray-500: hsla(220, 9%, 46%, 1);
|
||||
--cs-gray-600: hsla(0, 0%, 32%, 1);
|
||||
--cs-gray-700: hsla(217, 19%, 27%, 1);
|
||||
--cs-gray-800: hsla(215, 28%, 17%, 1);
|
||||
--cs-gray-900: hsla(221, 39%, 11%, 1);
|
||||
--cs-gray-950: hsla(224, 71%, 4%, 1);
|
||||
|
||||
/* Red */
|
||||
--cs-red-50: hsla(0, 86%, 97%, 1);
|
||||
--cs-red-100: hsla(0, 93%, 94%, 1);
|
||||
--cs-red-200: hsla(0, 96%, 89%, 1);
|
||||
--cs-red-300: hsla(0, 94%, 82%, 1);
|
||||
--cs-red-400: hsla(0, 91%, 71%, 1);
|
||||
--cs-red-500: hsla(0, 84%, 60%, 1);
|
||||
--cs-red-600: hsla(0, 72%, 51%, 1);
|
||||
--cs-red-700: hsla(0, 74%, 42%, 1);
|
||||
--cs-red-800: hsla(0, 70%, 35%, 1);
|
||||
--cs-red-900: hsla(0, 63%, 31%, 1);
|
||||
--cs-red-950: hsla(0, 75%, 15%, 1);
|
||||
|
||||
/* Orange */
|
||||
--cs-orange-50: hsla(33, 100%, 96%, 1);
|
||||
--cs-orange-100: hsla(34, 100%, 92%, 1);
|
||||
--cs-orange-200: hsla(32, 98%, 83%, 1);
|
||||
--cs-orange-300: hsla(31, 97%, 72%, 1);
|
||||
--cs-orange-400: hsla(27, 96%, 61%, 1);
|
||||
--cs-orange-500: hsla(25, 95%, 53%, 1);
|
||||
--cs-orange-600: hsla(21, 90%, 48%, 1);
|
||||
--cs-orange-700: hsla(17, 88%, 40%, 1);
|
||||
--cs-orange-800: hsla(15, 79%, 34%, 1);
|
||||
--cs-orange-900: hsla(15, 75%, 28%, 1);
|
||||
--cs-orange-950: hsla(13, 81%, 15%, 1);
|
||||
|
||||
/* Amber */
|
||||
--cs-amber-50: hsla(48, 100%, 96%, 1);
|
||||
--cs-amber-100: hsla(48, 96%, 89%, 1);
|
||||
--cs-amber-200: hsla(48, 97%, 77%, 1);
|
||||
--cs-amber-300: hsla(46, 97%, 65%, 1);
|
||||
--cs-amber-400: hsla(43, 96%, 56%, 1);
|
||||
--cs-amber-500: hsla(38, 92%, 50%, 1);
|
||||
--cs-amber-600: hsla(32, 95%, 44%, 1);
|
||||
--cs-amber-700: hsla(26, 90%, 37%, 1);
|
||||
--cs-amber-800: hsla(23, 83%, 31%, 1);
|
||||
--cs-amber-900: hsla(22, 78%, 26%, 1);
|
||||
--cs-amber-950: hsla(21, 92%, 14%, 1);
|
||||
|
||||
/* Yellow */
|
||||
--cs-yellow-50: hsla(55, 92%, 95%, 1);
|
||||
--cs-yellow-100: hsla(55, 97%, 88%, 1);
|
||||
--cs-yellow-200: hsla(53, 98%, 77%, 1);
|
||||
--cs-yellow-300: hsla(50, 98%, 64%, 1);
|
||||
--cs-yellow-400: hsla(48, 96%, 53%, 1);
|
||||
--cs-yellow-500: hsla(45, 93%, 47%, 1);
|
||||
--cs-yellow-600: hsla(41, 96%, 40%, 1);
|
||||
--cs-yellow-700: hsla(35, 92%, 33%, 1);
|
||||
--cs-yellow-800: hsla(32, 81%, 29%, 1);
|
||||
--cs-yellow-900: hsla(28, 73%, 26%, 1);
|
||||
--cs-yellow-950: hsla(26, 83%, 14%, 1);
|
||||
|
||||
/* Lime (品牌主色) */
|
||||
--cs-lime-50: hsla(78, 92%, 95%, 1);
|
||||
--cs-lime-100: hsla(80, 89%, 89%, 1);
|
||||
--cs-lime-200: hsla(81, 88%, 80%, 1);
|
||||
--cs-lime-300: hsla(82, 85%, 67%, 1);
|
||||
--cs-lime-400: hsla(83, 78%, 55%, 1);
|
||||
--cs-lime-500: hsla(84, 81%, 44%, 1);
|
||||
--cs-lime-600: hsla(85, 85%, 35%, 1);
|
||||
--cs-lime-700: hsla(86, 78%, 27%, 1);
|
||||
--cs-lime-800: hsla(86, 69%, 23%, 1);
|
||||
--cs-lime-900: hsla(88, 61%, 20%, 1);
|
||||
--cs-lime-950: hsla(89, 80%, 10%, 1);
|
||||
|
||||
/* Green */
|
||||
--cs-green-50: hsla(138, 76%, 97%, 1);
|
||||
--cs-green-100: hsla(141, 84%, 93%, 1);
|
||||
--cs-green-200: hsla(141, 79%, 85%, 1);
|
||||
--cs-green-300: hsla(142, 77%, 73%, 1);
|
||||
--cs-green-400: hsla(142, 69%, 58%, 1);
|
||||
--cs-green-500: hsla(142, 71%, 45%, 1);
|
||||
--cs-green-600: hsla(142, 76%, 36%, 1);
|
||||
--cs-green-700: hsla(142, 72%, 29%, 1);
|
||||
--cs-green-800: hsla(143, 64%, 24%, 1);
|
||||
--cs-green-900: hsla(144, 61%, 20%, 1);
|
||||
--cs-green-950: hsla(145, 80%, 10%, 1);
|
||||
|
||||
/* Emerald */
|
||||
--cs-emerald-50: hsla(152, 81%, 96%, 1);
|
||||
--cs-emerald-100: hsla(149, 80%, 90%, 1);
|
||||
--cs-emerald-200: hsla(152, 76%, 80%, 1);
|
||||
--cs-emerald-300: hsla(156, 72%, 67%, 1);
|
||||
--cs-emerald-400: hsla(158, 64%, 52%, 1);
|
||||
--cs-emerald-500: hsla(160, 84%, 39%, 1);
|
||||
--cs-emerald-600: hsla(161, 94%, 30%, 1);
|
||||
--cs-emerald-700: hsla(163, 94%, 24%, 1);
|
||||
--cs-emerald-800: hsla(163, 88%, 20%, 1);
|
||||
--cs-emerald-900: hsla(164, 86%, 16%, 1);
|
||||
--cs-emerald-950: hsla(166, 91%, 9%, 1);
|
||||
|
||||
/* Teal */
|
||||
--cs-teal-50: hsla(166, 76%, 97%, 1);
|
||||
--cs-teal-100: hsla(167, 85%, 89%, 1);
|
||||
--cs-teal-200: hsla(168, 84%, 78%, 1);
|
||||
--cs-teal-300: hsla(171, 77%, 64%, 1);
|
||||
--cs-teal-400: hsla(172, 66%, 50%, 1);
|
||||
--cs-teal-500: hsla(173, 80%, 40%, 1);
|
||||
--cs-teal-600: hsla(175, 84%, 32%, 1);
|
||||
--cs-teal-700: hsla(175, 77%, 26%, 1);
|
||||
--cs-teal-800: hsla(176, 69%, 22%, 1);
|
||||
--cs-teal-900: hsla(176, 61%, 19%, 1);
|
||||
--cs-teal-950: hsla(179, 84%, 10%, 1);
|
||||
|
||||
/* Cyan */
|
||||
--cs-cyan-50: hsla(183, 100%, 96%, 1);
|
||||
--cs-cyan-100: hsla(185, 96%, 90%, 1);
|
||||
--cs-cyan-200: hsla(186, 94%, 82%, 1);
|
||||
--cs-cyan-300: hsla(187, 92%, 69%, 1);
|
||||
--cs-cyan-400: hsla(188, 86%, 53%, 1);
|
||||
--cs-cyan-500: hsla(189, 94%, 43%, 1);
|
||||
--cs-cyan-600: hsla(192, 91%, 36%, 1);
|
||||
--cs-cyan-700: hsla(193, 82%, 31%, 1);
|
||||
--cs-cyan-800: hsla(194, 70%, 27%, 1);
|
||||
--cs-cyan-900: hsla(196, 64%, 24%, 1);
|
||||
--cs-cyan-950: hsla(197, 79%, 15%, 1);
|
||||
|
||||
/* Sky */
|
||||
--cs-sky-50: hsla(204, 100%, 97%, 1);
|
||||
--cs-sky-100: hsla(204, 94%, 94%, 1);
|
||||
--cs-sky-200: hsla(201, 94%, 86%, 1);
|
||||
--cs-sky-300: hsla(199, 95%, 74%, 1);
|
||||
--cs-sky-400: hsla(198, 93%, 60%, 1);
|
||||
--cs-sky-500: hsla(199, 89%, 48%, 1);
|
||||
--cs-sky-600: hsla(200, 98%, 39%, 1);
|
||||
--cs-sky-700: hsla(201, 96%, 32%, 1);
|
||||
--cs-sky-800: hsla(201, 90%, 27%, 1);
|
||||
--cs-sky-900: hsla(202, 80%, 24%, 1);
|
||||
--cs-sky-950: hsla(204, 80%, 16%, 1);
|
||||
|
||||
/* Blue */
|
||||
--cs-blue-50: hsla(214, 100%, 97%, 1);
|
||||
--cs-blue-100: hsla(214, 95%, 93%, 1);
|
||||
--cs-blue-200: hsla(213, 97%, 87%, 1);
|
||||
--cs-blue-300: hsla(212, 96%, 78%, 1);
|
||||
--cs-blue-400: hsla(213, 94%, 68%, 1);
|
||||
--cs-blue-500: hsla(217, 91%, 60%, 1);
|
||||
--cs-blue-600: hsla(221, 83%, 53%, 1);
|
||||
--cs-blue-700: hsla(224, 76%, 48%, 1);
|
||||
--cs-blue-800: hsla(226, 71%, 40%, 1);
|
||||
--cs-blue-900: hsla(224, 64%, 33%, 1);
|
||||
--cs-blue-950: hsla(226, 57%, 21%, 1);
|
||||
|
||||
/* Indigo */
|
||||
--cs-indigo-50: hsla(226, 100%, 97%, 1);
|
||||
--cs-indigo-100: hsla(226, 100%, 94%, 1);
|
||||
--cs-indigo-200: hsla(228, 96%, 89%, 1);
|
||||
--cs-indigo-300: hsla(230, 94%, 82%, 1);
|
||||
--cs-indigo-400: hsla(234, 89%, 74%, 1);
|
||||
--cs-indigo-500: hsla(239, 84%, 67%, 1);
|
||||
--cs-indigo-600: hsla(243, 75%, 59%, 1);
|
||||
--cs-indigo-700: hsla(245, 58%, 51%, 1);
|
||||
--cs-indigo-800: hsla(244, 55%, 41%, 1);
|
||||
--cs-indigo-900: hsla(242, 47%, 34%, 1);
|
||||
--cs-indigo-950: hsla(244, 47%, 20%, 1);
|
||||
|
||||
/* Violet */
|
||||
--cs-violet-50: hsla(250, 100%, 98%, 1);
|
||||
--cs-violet-100: hsla(251, 91%, 95%, 1);
|
||||
--cs-violet-200: hsla(251, 95%, 92%, 1);
|
||||
--cs-violet-300: hsla(253, 95%, 85%, 1);
|
||||
--cs-violet-400: hsla(255, 92%, 76%, 1);
|
||||
--cs-violet-500: hsla(258, 90%, 66%, 1);
|
||||
--cs-violet-600: hsla(262, 83%, 58%, 1);
|
||||
--cs-violet-700: hsla(263, 70%, 50%, 1);
|
||||
--cs-violet-800: hsla(263, 69%, 42%, 1);
|
||||
--cs-violet-900: hsla(264, 67%, 35%, 1);
|
||||
--cs-violet-950: hsla(262, 78%, 23%, 1);
|
||||
|
||||
/* Purple */
|
||||
--cs-purple-50: hsla(270, 100%, 98%, 1);
|
||||
--cs-purple-100: hsla(269, 100%, 95%, 1);
|
||||
--cs-purple-200: hsla(269, 100%, 92%, 1);
|
||||
--cs-purple-300: hsla(269, 97%, 85%, 1);
|
||||
--cs-purple-400: hsla(270, 95%, 75%, 1);
|
||||
--cs-purple-500: hsla(271, 91%, 65%, 1);
|
||||
--cs-purple-600: hsla(271, 81%, 56%, 1);
|
||||
--cs-purple-700: hsla(272, 72%, 47%, 1);
|
||||
--cs-purple-800: hsla(273, 67%, 39%, 1);
|
||||
--cs-purple-900: hsla(274, 66%, 32%, 1);
|
||||
--cs-purple-950: hsla(274, 87%, 21%, 1);
|
||||
|
||||
/* Fuchsia */
|
||||
--cs-fuchsia-50: hsla(289, 100%, 98%, 1);
|
||||
--cs-fuchsia-100: hsla(287, 100%, 95%, 1);
|
||||
--cs-fuchsia-200: hsla(288, 96%, 91%, 1);
|
||||
--cs-fuchsia-300: hsla(291, 93%, 83%, 1);
|
||||
--cs-fuchsia-400: hsla(292, 91%, 73%, 1);
|
||||
--cs-fuchsia-500: hsla(292, 84%, 61%, 1);
|
||||
--cs-fuchsia-600: hsla(293, 69%, 49%, 1);
|
||||
--cs-fuchsia-700: hsla(295, 72%, 40%, 1);
|
||||
--cs-fuchsia-800: hsla(295, 70%, 33%, 1);
|
||||
--cs-fuchsia-900: hsla(297, 64%, 28%, 1);
|
||||
--cs-fuchsia-950: hsla(297, 90%, 16%, 1);
|
||||
|
||||
/* Pink */
|
||||
--cs-pink-50: hsla(327, 73%, 97%, 1);
|
||||
--cs-pink-100: hsla(326, 78%, 95%, 1);
|
||||
--cs-pink-200: hsla(326, 85%, 90%, 1);
|
||||
--cs-pink-300: hsla(327, 87%, 82%, 1);
|
||||
--cs-pink-400: hsla(329, 86%, 70%, 1);
|
||||
--cs-pink-500: hsla(330, 81%, 60%, 1);
|
||||
--cs-pink-600: hsla(333, 71%, 51%, 1);
|
||||
--cs-pink-700: hsla(335, 78%, 42%, 1);
|
||||
--cs-pink-800: hsla(336, 74%, 35%, 1);
|
||||
--cs-pink-900: hsla(336, 69%, 30%, 1);
|
||||
--cs-pink-950: hsla(336, 84%, 17%, 1);
|
||||
|
||||
/* Rose */
|
||||
--cs-rose-50: hsla(356, 100%, 97%, 1);
|
||||
--cs-rose-100: hsla(356, 100%, 95%, 1);
|
||||
--cs-rose-200: hsla(353, 96%, 90%, 1);
|
||||
--cs-rose-300: hsla(353, 96%, 82%, 1);
|
||||
--cs-rose-400: hsla(351, 95%, 71%, 1);
|
||||
--cs-rose-500: hsla(350, 89%, 60%, 1);
|
||||
--cs-rose-600: hsla(347, 77%, 50%, 1);
|
||||
--cs-rose-700: hsla(345, 83%, 41%, 1);
|
||||
--cs-rose-800: hsla(343, 80%, 35%, 1);
|
||||
--cs-rose-900: hsla(342, 75%, 30%, 1);
|
||||
--cs-rose-950: hsla(343, 88%, 16%, 1);
|
||||
|
||||
/* Black & White */
|
||||
--cs-black: hsla(0, 0%, 0%, 1);
|
||||
--cs-white: hsla(0, 0%, 100%, 1);
|
||||
|
||||
/* ==================== */
|
||||
/* Semantic Tokens - Light Mode */
|
||||
/* ==================== */
|
||||
|
||||
/* Brand Colors */
|
||||
--cs-primary: var(--cs-lime-500);
|
||||
--cs-destructive: var(--cs-red-500);
|
||||
--cs-success: var(--cs-green-500);
|
||||
--cs-warning: var(--cs-amber-500);
|
||||
|
||||
/* Background & Foreground */
|
||||
--cs-background: var(--cs-zinc-50);
|
||||
--cs-background-subtle: hsla(0, 0%, 0%, 0.02);
|
||||
--cs-foreground: hsla(0, 0%, 0%, 0.9);
|
||||
--cs-foreground-secondary: hsla(0, 0%, 0%, 0.6);
|
||||
--cs-foreground-muted: hsla(0, 0%, 0%, 0.4);
|
||||
|
||||
/* Card & Popover */
|
||||
--cs-card: var(--cs-white);
|
||||
--cs-popover: var(--cs-white);
|
||||
|
||||
/* Border */
|
||||
--cs-border: hsla(0, 0%, 0%, 0.1);
|
||||
--cs-border-hover: hsla(0, 0%, 0%, 0.2);
|
||||
--cs-border-active: hsla(0, 0%, 0%, 0.3);
|
||||
|
||||
/* Ring (Focus) */
|
||||
--cs-ring: hsla(84, 81%, 44%, 0.4);
|
||||
|
||||
/* UI Element Colors */
|
||||
--cs-secondary: hsla(0, 0%, 0%, 0.05); /* Secondary Button Background */
|
||||
--cs-secondary-hover: hsla(0, 0%, 0%, 0.85);
|
||||
--cs-secondary-active: hsla(0, 0%, 0%, 0.7);
|
||||
--cs-muted: hsla(0, 0%, 0%, 0.05); /* Muted/Subtle Background */
|
||||
--cs-accent: hsla(0, 0%, 0%, 0.05); /* Accent Background */
|
||||
--cs-ghost-hover: hsla(0, 0%, 0%, 0.05); /* Ghost Button Hover */
|
||||
--cs-ghost-active: hsla(0, 0%, 0%, 0.1); /* Ghost Button Active */
|
||||
|
||||
/* Sidebar */
|
||||
--cs-sidebar: var(--cs-white);
|
||||
--cs-sidebar-accent: hsla(0, 0%, 0%, 0.05);
|
||||
|
||||
/* ==================== */
|
||||
/* UI 元素细分颜色 */
|
||||
/* ==================== */
|
||||
|
||||
/* Modal / Overlay */
|
||||
--cs-modal-backdrop: hsla(0, 0%, 0%, 0.4);
|
||||
--cs-modal-thumb: hsla(0, 0%, 0%, 0.2);
|
||||
--cs-modal-thumb-hover: hsla(0, 0%, 0%, 0.3);
|
||||
|
||||
/* Icon */
|
||||
--cs-icon-default: var(--cs-foreground-secondary);
|
||||
--cs-icon-hover: var(--cs-foreground);
|
||||
|
||||
/* Input / Select */
|
||||
--cs-input-background: var(--cs-white);
|
||||
--cs-input-border: var(--cs-border);
|
||||
--cs-input-border-hover: var(--cs-border-hover);
|
||||
--cs-input-border-focus: var(--cs-primary);
|
||||
|
||||
/* Primary Button */
|
||||
--cs-primary-button-background: var(--cs-primary);
|
||||
--cs-primary-button-text: var(--cs-white);
|
||||
--cs-primary-button-background-hover: hsla(84, 81%, 44%, 0.85);
|
||||
--cs-primary-button-background-active: hsla(84, 81%, 44%, 0.7);
|
||||
--cs-primary-button-background-2nd: hsla(84, 81%, 44%, 0.1);
|
||||
--cs-primary-button-background-3rd: hsla(84, 81%, 44%, 0.05);
|
||||
|
||||
/* Secondary Button */
|
||||
--cs-secondary-button-background: var(--cs-secondary);
|
||||
--cs-secondary-button-text: var(--cs-foreground);
|
||||
--cs-secondary-button-background-hover: hsla(0, 0%, 0%, 0.85);
|
||||
--cs-secondary-button-background-active: hsla(0, 0%, 0%, 0.7);
|
||||
--cs-secondary-button-border: var(--cs-border);
|
||||
|
||||
/* Ghost Button */
|
||||
--cs-ghost-button-background: hsla(0, 0%, 0%, 0);
|
||||
--cs-ghost-button-text: var(--cs-foreground);
|
||||
--cs-ghost-button-background-hover: var(--cs-ghost-hover);
|
||||
--cs-ghost-button-background-active: var(--cs-ghost-active);
|
||||
|
||||
/* ==================== */
|
||||
/* Spacing & Sizing (合并) */
|
||||
/* ==================== */
|
||||
|
||||
--cs-size-5xs: 0.25rem; /* 4px */
|
||||
--cs-size-4xs: 0.5rem; /* 8px */
|
||||
--cs-size-3xs: 0.75rem; /* 12px */
|
||||
--cs-size-2xs: 1rem; /* 16px */
|
||||
--cs-size-xs: 1.5rem; /* 24px */
|
||||
--cs-size-sm: 2rem; /* 32px */
|
||||
--cs-size-md: 2.5rem; /* 40px */
|
||||
--cs-size-lg: 3rem; /* 48px */
|
||||
--cs-size-xl: 3.5rem; /* 56px */
|
||||
--cs-size-2xl: 4rem; /* 64px */
|
||||
--cs-size-3xl: 4.5rem; /* 72px */
|
||||
--cs-size-4xl: 5rem; /* 80px */
|
||||
--cs-size-5xl: 5.5rem; /* 88px */
|
||||
--cs-size-6xl: 6rem; /* 96px */
|
||||
--cs-size-7xl: 6.5rem; /* 104px */
|
||||
--cs-size-8xl: 7rem; /* 112px */
|
||||
|
||||
/* ==================== */
|
||||
/* Border Radius */
|
||||
/* ==================== */
|
||||
|
||||
--cs-radius-4xs: 4px; /* 4px */
|
||||
--cs-radius-3xs: 8px; /* 8px */
|
||||
--cs-radius-2xs: 12px; /* 12px */
|
||||
--cs-radius-xs: 16px; /* 16px */
|
||||
--cs-radius-sm: 24px; /* 24px */
|
||||
--cs-radius-md: 32px; /* 32px */
|
||||
--cs-radius-lg: 2.5rem; /* 40px */
|
||||
--cs-radius-xl: 48px; /* 48px */
|
||||
--cs-radius-2xl: 56px; /* 56px */
|
||||
--cs-radius-3xl: 64px; /* 64px */
|
||||
--cs-radius-round: 999px; /* 保持 px,因为是特殊值 */
|
||||
|
||||
/* Border Width */
|
||||
--cs-border-width-sm: 1px; /* 1px */
|
||||
--cs-border-width-md: 2px; /* 2px */
|
||||
--cs-border-width-lg: 3px; /* 3px */
|
||||
|
||||
/* ==================== */
|
||||
/* Typography */
|
||||
/* ==================== */
|
||||
|
||||
/* Font Families */
|
||||
--cs-font-family-heading: Inter;
|
||||
--cs-font-family-body: Inter;
|
||||
|
||||
/* Font Weights (修正单位错误) */
|
||||
--cs-font-weight-regular: 400;
|
||||
--cs-font-weight-medium: 500;
|
||||
--cs-font-weight-bold: 700;
|
||||
|
||||
/* Font Sizes - Body */
|
||||
--cs-font-size-body-xs: 0.75rem; /* 12px */
|
||||
--cs-font-size-body-sm: 0.875rem; /* 14px */
|
||||
--cs-font-size-body-md: 1rem; /* 16px */
|
||||
--cs-font-size-body-lg: 1.125rem; /* 18px */
|
||||
|
||||
/* Font Sizes - Heading */
|
||||
--cs-font-size-heading-xs: 1.25rem; /* 20px */
|
||||
--cs-font-size-heading-sm: 1.5rem; /* 24px */
|
||||
--cs-font-size-heading-md: 2rem; /* 32px */
|
||||
--cs-font-size-heading-lg: 2.5rem; /* 40px */
|
||||
--cs-font-size-heading-xl: 3rem; /* 48px */
|
||||
--cs-font-size-heading-2xl: 3.75rem; /* 60px */
|
||||
|
||||
/* Line Heights - Body */
|
||||
--cs-line-height-body-xs: 1.25rem; /* 20px */
|
||||
--cs-line-height-body-sm: 1.5rem; /* 24px */
|
||||
--cs-line-height-body-md: 1.5rem; /* 24px */
|
||||
--cs-line-height-body-lg: 1.75rem; /* 28px */
|
||||
|
||||
/* Line Heights - Heading */
|
||||
--cs-line-height-heading-xs: 2rem; /* 32px */
|
||||
--cs-line-height-heading-sm: 2.5rem; /* 40px */
|
||||
--cs-line-height-heading-md: 3rem; /* 48px */
|
||||
--cs-line-height-heading-lg: 3.75rem; /* 60px */
|
||||
--cs-line-height-heading-xl: 5rem; /* 80px */
|
||||
|
||||
/* Paragraph Spacing */
|
||||
--cs-paragraph-spacing-body-xs: 0.75rem; /* 12px */
|
||||
--cs-paragraph-spacing-body-sm: 0.875rem; /* 14px */
|
||||
--cs-paragraph-spacing-body-md: 1rem; /* 16px */
|
||||
--cs-paragraph-spacing-body-lg: 1.125rem; /* 18px */
|
||||
--cs-paragraph-spacing-heading-xs: 1.25rem; /* 20px */
|
||||
--cs-paragraph-spacing-heading-sm: 1.5rem; /* 24px */
|
||||
--cs-paragraph-spacing-heading-md: 2rem; /* 32px */
|
||||
--cs-paragraph-spacing-heading-lg: 2.5rem; /* 40px */
|
||||
--cs-paragraph-spacing-heading-xl: 3rem; /* 48px */
|
||||
--cs-paragraph-spacing-heading-2xl: 3.75rem; /* 60px */
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Dark Mode */
|
||||
/* ==================== */
|
||||
|
||||
.dark {
|
||||
/* Background & Foreground */
|
||||
--cs-background: var(--cs-zinc-900);
|
||||
--cs-background-subtle: hsla(0, 0%, 100%, 0.02);
|
||||
--cs-foreground: hsla(0, 0%, 100%, 0.9);
|
||||
--cs-foreground-secondary: hsla(0, 0%, 100%, 0.6);
|
||||
--cs-foreground-muted: hsla(0, 0%, 100%, 0.4);
|
||||
|
||||
/* Card & Popover */
|
||||
--cs-card: var(--cs-black);
|
||||
--cs-popover: var(--cs-black);
|
||||
|
||||
/* Border */
|
||||
--cs-border: hsla(0, 0%, 100%, 0.1);
|
||||
--cs-border-hover: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-border-active: hsla(0, 0%, 100%, 0.3);
|
||||
|
||||
/* Ring (Focus) - 保持不变 */
|
||||
--cs-ring: hsla(84, 81%, 44%, 0.4);
|
||||
|
||||
/* UI Element Colors - Dark Mode */
|
||||
--cs-secondary: hsla(0, 0%, 100%, 0.1); /* Secondary Button Background */
|
||||
--cs-secondary-hover: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-secondary-active: hsla(0, 0%, 100%, 0.25);
|
||||
--cs-muted: hsla(0, 0%, 100%, 0.1); /* Muted/Subtle Background */
|
||||
--cs-accent: hsla(0, 0%, 100%, 0.1); /* Accent Background */
|
||||
--cs-ghost-hover: hsla(0, 0%, 100%, 0.1); /* Ghost Button Hover */
|
||||
--cs-ghost-active: hsla(0, 0%, 100%, 0.15); /* Ghost Button Active */
|
||||
|
||||
/* Sidebar */
|
||||
--cs-sidebar: var(--cs-black);
|
||||
--cs-sidebar-accent: hsla(0, 0%, 100%, 0.1);
|
||||
|
||||
/* ==================== */
|
||||
/* UI 元素细分颜色 (Dark Mode) */
|
||||
/* ==================== */
|
||||
|
||||
/* Modal / Overlay */
|
||||
--cs-modal-backdrop: hsla(0, 0%, 0%, 0.06);
|
||||
--cs-modal-thumb: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-modal-thumb-hover: hsla(0, 0%, 100%, 0.3);
|
||||
|
||||
/* Icon */
|
||||
--cs-icon-default: var(--cs-foreground-secondary);
|
||||
--cs-icon-hover: var(--cs-foreground);
|
||||
|
||||
/* Input / Select */
|
||||
--cs-input-background: var(--cs-black);
|
||||
--cs-input-border: var(--cs-border);
|
||||
--cs-input-border-hover: var(--cs-border-hover);
|
||||
--cs-input-border-focus: var(--cs-primary);
|
||||
|
||||
/* Primary Button - 保持不变 */
|
||||
--cs-primary-button-background: var(--cs-primary);
|
||||
--cs-primary-button-text: var(--cs-white);
|
||||
--cs-primary-button-background-hover: hsla(84, 81%, 44%, 0.85);
|
||||
--cs-primary-button-background-active: hsla(84, 81%, 44%, 0.7);
|
||||
--cs-primary-button-background-2nd: hsla(84, 81%, 44%, 0.1);
|
||||
--cs-primary-button-background-3rd: hsla(84, 81%, 44%, 0.05);
|
||||
|
||||
/* Secondary Button */
|
||||
--cs-secondary-button-background: var(--cs-secondary);
|
||||
--cs-secondary-button-text: var(--cs-foreground);
|
||||
--cs-secondary-button-background-hover: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-secondary-button-background-active: hsla(0, 0%, 100%, 0.25);
|
||||
--cs-secondary-button-border: var(--cs-border);
|
||||
|
||||
/* Ghost Button */
|
||||
--cs-ghost-button-background: hsla(0, 0%, 100%, 0);
|
||||
--cs-ghost-button-text: var(--cs-foreground);
|
||||
--cs-ghost-button-background-hover: var(--cs-ghost-hover);
|
||||
--cs-ghost-button-background-active: var(--cs-ghost-active);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
|
||||
import { agentService } from '../services/agents'
|
||||
import { windowService } from '../services/WindowService'
|
||||
import { app } from './app'
|
||||
import { config } from './config'
|
||||
|
||||
@@ -43,6 +45,13 @@ export class ApiServer {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server!.listen(port, host, () => {
|
||||
logger.info('API server started', { host, port })
|
||||
|
||||
// Notify renderer that API server is ready
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IpcChannel.ApiServer_Ready)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webC
|
||||
import fontList from 'font-list'
|
||||
|
||||
import { agentMessageRepository } from './services/agents/database'
|
||||
import { PluginService } from './services/agents/plugins/PluginService'
|
||||
import { apiServerService } from './services/ApiServerService'
|
||||
import appService from './services/AppService'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
@@ -50,7 +51,6 @@ import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ocrService } from './services/ocr/OcrService'
|
||||
import OvmsManager from './services/OvmsManager'
|
||||
import { PluginService } from './services/PluginService'
|
||||
import { proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
} from './services/SpanCacheService'
|
||||
import storeSyncService from './services/StoreSyncService'
|
||||
import VertexAIService from './services/VertexAIService'
|
||||
import WebSocketService from './services/WebSocketService'
|
||||
import { setOpenLinkExternal } from './services/WebviewService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { calculateDirectorySize, getResourcePath } from './utils'
|
||||
@@ -1021,6 +1022,13 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
}
|
||||
})
|
||||
|
||||
// WebSocket
|
||||
ipcMain.handle(IpcChannel.WebSocket_Start, WebSocketService.start)
|
||||
ipcMain.handle(IpcChannel.WebSocket_Stop, WebSocketService.stop)
|
||||
ipcMain.handle(IpcChannel.WebSocket_Status, WebSocketService.getStatus)
|
||||
ipcMain.handle(IpcChannel.WebSocket_SendFile, WebSocketService.sendFile)
|
||||
ipcMain.handle(IpcChannel.WebSocket_GetAllCandidates, WebSocketService.getAllCandidates)
|
||||
|
||||
// Preference handlers
|
||||
PreferenceService.registerIpcHandler()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import type { ApiClient } from '@types'
|
||||
import { net } from 'electron'
|
||||
|
||||
import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
|
||||
@@ -43,7 +44,7 @@ export default class EmbeddingsFactory {
|
||||
apiKey,
|
||||
dimensions,
|
||||
batchSize,
|
||||
configuration: { baseURL }
|
||||
configuration: { baseURL, fetch: net.fetch as typeof fetch }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
369
src/main/services/WebSocketService.ts
Normal file
369
src/main/services/WebSocketService.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { loggerService } from '@logger'
|
||||
import * as fs from 'fs'
|
||||
import { networkInterfaces } from 'os'
|
||||
import * as path from 'path'
|
||||
import type { Socket } from 'socket.io'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
const logger = loggerService.withContext('WebSocketService')
|
||||
|
||||
class WebSocketService {
|
||||
private io: Server | null = null
|
||||
private isStarted = false
|
||||
private port = 7017
|
||||
private connectedClients = new Set<string>()
|
||||
|
||||
private getLocalIpAddress(): string | undefined {
|
||||
const interfaces = networkInterfaces()
|
||||
|
||||
// 按优先级排序的网络接口名称模式
|
||||
const interfacePriority = [
|
||||
// macOS: 以太网/Wi-Fi 优先
|
||||
/^en[0-9]+$/, // en0, en1 (以太网/Wi-Fi)
|
||||
/^(en|eth)[0-9]+$/, // 以太网接口
|
||||
/^wlan[0-9]+$/, // 无线接口
|
||||
// Windows: 以太网/Wi-Fi 优先
|
||||
/^(Ethernet|Wi-Fi|Local Area Connection)/,
|
||||
/^(Wi-Fi|无线网络连接)/,
|
||||
// Linux: 以太网/Wi-Fi 优先
|
||||
/^(eth|enp|wlp|wlan)[0-9]+/,
|
||||
// 虚拟化接口(低优先级)
|
||||
/^bridge[0-9]+$/, // Docker bridge
|
||||
/^veth[0-9]+$/, // Docker veth
|
||||
/^docker[0-9]+/, // Docker interfaces
|
||||
/^br-[0-9a-f]+/, // Docker bridge
|
||||
/^vmnet[0-9]+$/, // VMware
|
||||
/^vboxnet[0-9]+$/, // VirtualBox
|
||||
// VPN 隧道接口(低优先级)
|
||||
/^utun[0-9]+$/, // macOS VPN
|
||||
/^tun[0-9]+$/, // Linux/Unix VPN
|
||||
/^tap[0-9]+$/, // TAP interfaces
|
||||
/^tailscale[0-9]*$/, // Tailscale VPN
|
||||
/^wg[0-9]+$/ // WireGuard VPN
|
||||
]
|
||||
|
||||
const candidates: Array<{ interface: string; address: string; priority: number }> = []
|
||||
|
||||
for (const [name, ifaces] of Object.entries(interfaces)) {
|
||||
for (const iface of ifaces || []) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
// 计算接口优先级
|
||||
let priority = 999 // 默认最低优先级
|
||||
for (let i = 0; i < interfacePriority.length; i++) {
|
||||
if (interfacePriority[i].test(name)) {
|
||||
priority = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
interface: name,
|
||||
address: iface.address,
|
||||
priority
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
logger.warn('无法获取局域网 IP,使用默认 IP: 127.0.0.1')
|
||||
return '127.0.0.1'
|
||||
}
|
||||
|
||||
// 按优先级排序,选择优先级最高的
|
||||
candidates.sort((a, b) => a.priority - b.priority)
|
||||
const best = candidates[0]
|
||||
|
||||
logger.info(`获取局域网 IP: ${best.address} (interface: ${best.interface})`)
|
||||
return best.address
|
||||
}
|
||||
|
||||
public start = async (): Promise<{ success: boolean; port?: number; error?: string }> => {
|
||||
if (this.isStarted && this.io) {
|
||||
return { success: true, port: this.port }
|
||||
}
|
||||
|
||||
try {
|
||||
this.io = new Server(this.port, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
},
|
||||
transports: ['websocket', 'polling'],
|
||||
allowEIO3: true,
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000
|
||||
})
|
||||
|
||||
this.io.on('connection', (socket: Socket) => {
|
||||
this.connectedClients.add(socket.id)
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (!mainWindow) {
|
||||
logger.error('Main window is null, cannot send connection event')
|
||||
} else {
|
||||
mainWindow.webContents.send('websocket-client-connected', {
|
||||
connected: true,
|
||||
clientId: socket.id
|
||||
})
|
||||
logger.info(`Connection event sent to renderer, total clients: ${this.connectedClients.size}`)
|
||||
}
|
||||
|
||||
socket.on('message', (data) => {
|
||||
logger.info('Received message from mobile:', data)
|
||||
mainWindow?.webContents.send('websocket-message-received', data)
|
||||
socket.emit('message_received', { success: true })
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logger.info(`Client disconnected: ${socket.id}`)
|
||||
this.connectedClients.delete(socket.id)
|
||||
|
||||
if (this.connectedClients.size === 0) {
|
||||
mainWindow?.webContents.send('websocket-client-connected', {
|
||||
connected: false,
|
||||
clientId: socket.id
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Engine 层面的事件监听
|
||||
this.io.engine.on('connection_error', (err) => {
|
||||
logger.error('Engine connection error:', err)
|
||||
})
|
||||
|
||||
this.io.engine.on('connection', (rawSocket) => {
|
||||
const remoteAddr = rawSocket.request.connection.remoteAddress
|
||||
logger.info(`[Engine] Raw connection from: ${remoteAddr}`)
|
||||
logger.info(`[Engine] Transport: ${rawSocket.transport.name}`)
|
||||
|
||||
rawSocket.on('packet', (packet: { type: string; data?: any }) => {
|
||||
logger.info(
|
||||
`[Engine] ← Packet from ${remoteAddr}: type="${packet.type}"`,
|
||||
packet.data ? { data: packet.data } : {}
|
||||
)
|
||||
})
|
||||
|
||||
rawSocket.on('packetCreate', (packet: { type: string; data?: any }) => {
|
||||
logger.info(`[Engine] → Packet to ${remoteAddr}: type="${packet.type}"`)
|
||||
})
|
||||
|
||||
rawSocket.on('close', (reason: string) => {
|
||||
logger.warn(`[Engine] Connection closed from ${remoteAddr}, reason: ${reason}`)
|
||||
})
|
||||
|
||||
rawSocket.on('error', (error: Error) => {
|
||||
logger.error(`[Engine] Connection error from ${remoteAddr}:`, error)
|
||||
})
|
||||
})
|
||||
|
||||
// Socket.IO 握手失败监听
|
||||
this.io.on('connection_error', (err) => {
|
||||
logger.error('[Socket.IO] Connection error during handshake:', err)
|
||||
})
|
||||
|
||||
this.isStarted = true
|
||||
logger.info(`WebSocket server started on port ${this.port}`)
|
||||
|
||||
return { success: true, port: this.port }
|
||||
} catch (error) {
|
||||
logger.error('Failed to start WebSocket server:', error as Error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async (): Promise<{ success: boolean }> => {
|
||||
if (!this.isStarted || !this.io) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.io!.close(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
this.io = null
|
||||
this.isStarted = false
|
||||
this.connectedClients.clear()
|
||||
logger.info('WebSocket server stopped')
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop WebSocket server:', error as Error)
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
public getStatus = async (): Promise<{
|
||||
isRunning: boolean
|
||||
port?: number
|
||||
ip?: string
|
||||
clientConnected: boolean
|
||||
}> => {
|
||||
return {
|
||||
isRunning: this.isStarted,
|
||||
port: this.isStarted ? this.port : undefined,
|
||||
ip: this.isStarted ? this.getLocalIpAddress() : undefined,
|
||||
clientConnected: this.connectedClients.size > 0
|
||||
}
|
||||
}
|
||||
|
||||
public getAllCandidates = async (): Promise<
|
||||
Array<{
|
||||
host: string
|
||||
interface: string
|
||||
priority: number
|
||||
}>
|
||||
> => {
|
||||
const interfaces = networkInterfaces()
|
||||
|
||||
// 按优先级排序的网络接口名称模式
|
||||
const interfacePriority = [
|
||||
// macOS: 以太网/Wi-Fi 优先
|
||||
/^en[0-9]+$/, // en0, en1 (以太网/Wi-Fi)
|
||||
/^(en|eth)[0-9]+$/, // 以太网接口
|
||||
/^wlan[0-9]+$/, // 无线接口
|
||||
// Windows: 以太网/Wi-Fi 优先
|
||||
/^(Ethernet|Wi-Fi|Local Area Connection)/,
|
||||
/^(Wi-Fi|无线网络连接)/,
|
||||
// Linux: 以太网/Wi-Fi 优先
|
||||
/^(eth|enp|wlp|wlan)[0-9]+/,
|
||||
// 虚拟化接口(低优先级)
|
||||
/^bridge[0-9]+$/, // Docker bridge
|
||||
/^veth[0-9]+$/, // Docker veth
|
||||
/^docker[0-9]+/, // Docker interfaces
|
||||
/^br-[0-9a-f]+/, // Docker bridge
|
||||
/^vmnet[0-9]+$/, // VMware
|
||||
/^vboxnet[0-9]+$/, // VirtualBox
|
||||
// VPN 隧道接口(低优先级)
|
||||
/^utun[0-9]+$/, // macOS VPN
|
||||
/^tun[0-9]+$/, // Linux/Unix VPN
|
||||
/^tap[0-9]+$/, // TAP interfaces
|
||||
/^tailscale[0-9]*$/, // Tailscale VPN
|
||||
/^wg[0-9]+$/ // WireGuard VPN
|
||||
]
|
||||
|
||||
const candidates: Array<{ host: string; interface: string; priority: number }> = []
|
||||
|
||||
for (const [name, ifaces] of Object.entries(interfaces)) {
|
||||
for (const iface of ifaces || []) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
// 计算接口优先级
|
||||
let priority = 999 // 默认最低优先级
|
||||
for (let i = 0; i < interfacePriority.length; i++) {
|
||||
if (interfacePriority[i].test(name)) {
|
||||
priority = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
host: iface.address,
|
||||
interface: name,
|
||||
priority
|
||||
})
|
||||
|
||||
logger.debug(`Found interface: ${name} -> ${iface.address} (priority: ${priority})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级排序返回
|
||||
candidates.sort((a, b) => a.priority - b.priority)
|
||||
logger.info(
|
||||
`Found ${candidates.length} IP candidates: ${candidates.map((c) => `${c.host}(${c.interface})`).join(', ')}`
|
||||
)
|
||||
return candidates
|
||||
}
|
||||
|
||||
public sendFile = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
filePath: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
if (!this.isStarted || !this.io) {
|
||||
const errorMsg = 'WebSocket server is not running.'
|
||||
logger.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
|
||||
if (this.connectedClients.size === 0) {
|
||||
const errorMsg = 'No client connected.'
|
||||
logger.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stats = fs.statSync(filePath)
|
||||
const totalSize = stats.size
|
||||
const filename = path.basename(filePath)
|
||||
const stream = fs.createReadStream(filePath)
|
||||
let bytesSent = 0
|
||||
const startTime = Date.now()
|
||||
|
||||
logger.info(`Starting file transfer: ${filename} (${this.formatFileSize(totalSize)})`)
|
||||
|
||||
// 向客户端发送文件开始的信号,包含文件名和总大小
|
||||
this.io!.emit('zip-file-start', { filename, totalSize })
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
bytesSent += chunk.length
|
||||
const progress = (bytesSent / totalSize) * 100
|
||||
|
||||
// 向客户端发送文件块
|
||||
this.io!.emit('zip-file-chunk', chunk)
|
||||
|
||||
// 向渲染进程发送进度更新
|
||||
mainWindow?.webContents.send('file-send-progress', { progress })
|
||||
|
||||
// 每10%记录一次进度
|
||||
if (Math.floor(progress) % 10 === 0) {
|
||||
const elapsed = (Date.now() - startTime) / 1000
|
||||
const speed = elapsed > 0 ? bytesSent / elapsed : 0
|
||||
logger.info(`Transfer progress: ${Math.floor(progress)}% (${this.formatFileSize(speed)}/s)`)
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('end', () => {
|
||||
const totalTime = (Date.now() - startTime) / 1000
|
||||
const avgSpeed = totalTime > 0 ? totalSize / totalTime : 0
|
||||
logger.info(
|
||||
`File transfer completed: ${filename} in ${totalTime.toFixed(1)}s (${this.formatFileSize(avgSpeed)}/s)`
|
||||
)
|
||||
|
||||
// 确保发送100%的进度
|
||||
mainWindow?.webContents.send('file-send-progress', { progress: 100 })
|
||||
// 向客户端发送文件结束的信号
|
||||
this.io!.emit('zip-file-end')
|
||||
resolve({ success: true })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
logger.error(`File transfer failed: ${filename}`, error)
|
||||
reject({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketService()
|
||||
426
src/main/services/agents/plugins/PluginCacheStore.ts
Normal file
426
src/main/services/agents/plugins/PluginCacheStore.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { findAllSkillDirectories, parsePluginMetadata, parseSkillMetadata } from '@main/utils/markdownParser'
|
||||
import type { CachedPluginsData, InstalledPlugin, PluginError, PluginMetadata, PluginType } from '@types'
|
||||
import { CachedPluginsDataSchema } from '@types'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const logger = loggerService.withContext('PluginCacheStore')
|
||||
|
||||
interface PluginCacheStoreDeps {
|
||||
allowedExtensions: string[]
|
||||
getPluginDirectoryName: (type: PluginType) => 'agents' | 'commands' | 'skills'
|
||||
getClaudeBasePath: (workdir: string) => string
|
||||
getClaudePluginDirectory: (workdir: string, type: PluginType) => string
|
||||
getPluginsBasePath: () => string
|
||||
}
|
||||
|
||||
export class PluginCacheStore {
|
||||
constructor(private readonly deps: PluginCacheStoreDeps) {}
|
||||
|
||||
async listAvailableFilePlugins(type: 'agent' | 'command'): Promise<PluginMetadata[]> {
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const directory = path.join(basePath, this.deps.getPluginDirectoryName(type))
|
||||
|
||||
try {
|
||||
await fs.promises.access(directory, fs.constants.R_OK)
|
||||
} catch (error) {
|
||||
logger.warn(`Plugin directory not accessible: ${directory}`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
const plugins: PluginMetadata[] = []
|
||||
const categories = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||
|
||||
for (const categoryEntry of categories) {
|
||||
if (!categoryEntry.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const category = categoryEntry.name
|
||||
const categoryPath = path.join(directory, category)
|
||||
const files = await fs.promises.readdir(categoryPath, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.isFile()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name).toLowerCase()
|
||||
if (!this.deps.allowedExtensions.includes(ext)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(categoryPath, file.name)
|
||||
const sourcePath = path.join(this.deps.getPluginDirectoryName(type), category, file.name)
|
||||
const metadata = await parsePluginMetadata(filePath, sourcePath, category, type)
|
||||
plugins.push(metadata)
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse plugin: ${file.name}`, {
|
||||
category,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
async listAvailableSkills(): Promise<PluginMetadata[]> {
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const skillsPath = path.join(basePath, this.deps.getPluginDirectoryName('skill'))
|
||||
const skills: PluginMetadata[] = []
|
||||
|
||||
try {
|
||||
await fs.promises.access(skillsPath)
|
||||
} catch {
|
||||
logger.warn('Skills directory not found', { skillsPath })
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const skillDirectories = await findAllSkillDirectories(skillsPath, basePath)
|
||||
logger.info(`Found ${skillDirectories.length} skill directories`, { skillsPath })
|
||||
|
||||
for (const { folderPath, sourcePath } of skillDirectories) {
|
||||
try {
|
||||
const metadata = await parseSkillMetadata(folderPath, sourcePath, 'skills')
|
||||
skills.push(metadata)
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse skill folder: ${sourcePath}`, {
|
||||
folderPath,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to scan skill directory', {
|
||||
skillsPath,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
async readSourceContent(sourcePath: string): Promise<string> {
|
||||
const absolutePath = this.resolveSourcePath(sourcePath)
|
||||
|
||||
try {
|
||||
await fs.promises.access(absolutePath, fs.constants.R_OK)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
try {
|
||||
return await fs.promises.readFile(absolutePath, 'utf-8')
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'READ_FAILED',
|
||||
path: sourcePath,
|
||||
reason: error instanceof Error ? error.message : String(error)
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
resolveSourcePath(sourcePath: string): string {
|
||||
const normalized = path.normalize(sourcePath)
|
||||
|
||||
if (normalized.includes('..')) {
|
||||
throw {
|
||||
type: 'PATH_TRAVERSAL',
|
||||
message: 'Path traversal detected',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const absolutePath = path.join(basePath, normalized)
|
||||
const resolvedPath = path.resolve(absolutePath)
|
||||
|
||||
if (!resolvedPath.startsWith(path.resolve(basePath))) {
|
||||
throw {
|
||||
type: 'PATH_TRAVERSAL',
|
||||
message: 'Path outside plugins directory',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
async ensureSkillSourceDirectory(sourceAbsolutePath: string, sourcePath: string): Promise<void> {
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = await fs.promises.stat(sourceAbsolutePath)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: sourceAbsolutePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: 'Skill source is not a directory',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
async validatePluginFile(filePath: string, maxFileSize: number): Promise<void> {
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = await fs.promises.stat(filePath)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: filePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
if (stats.size > maxFileSize) {
|
||||
throw {
|
||||
type: 'FILE_TOO_LARGE',
|
||||
size: stats.size,
|
||||
max: maxFileSize
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (!this.deps.allowedExtensions.includes(ext)) {
|
||||
throw {
|
||||
type: 'INVALID_FILE_TYPE',
|
||||
extension: ext
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
try {
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const relativeSourcePath = path.relative(basePath, filePath)
|
||||
const segments = relativeSourcePath.split(path.sep)
|
||||
const rootDir = segments[0]
|
||||
const agentDir = this.deps.getPluginDirectoryName('agent')
|
||||
const type: 'agent' | 'command' = rootDir === agentDir ? 'agent' : 'command'
|
||||
const category = path.basename(path.dirname(filePath))
|
||||
|
||||
await parsePluginMetadata(filePath, relativeSourcePath, category, type)
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: 'Failed to parse frontmatter',
|
||||
path: filePath
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
async listInstalled(workdir: string): Promise<InstalledPlugin[]> {
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
const cacheData = await this.readCacheFile(claudePath)
|
||||
|
||||
if (cacheData) {
|
||||
logger.debug(`Loaded ${cacheData.plugins.length} plugins from cache`, { workdir })
|
||||
return cacheData.plugins
|
||||
}
|
||||
|
||||
logger.info('Cache read failed, rebuilding from filesystem', { workdir })
|
||||
return await this.rebuild(workdir)
|
||||
}
|
||||
|
||||
async upsert(workdir: string, plugin: InstalledPlugin): Promise<void> {
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
let cacheData = await this.readCacheFile(claudePath)
|
||||
let plugins = cacheData?.plugins
|
||||
|
||||
if (!plugins) {
|
||||
plugins = await this.rebuild(workdir)
|
||||
cacheData = {
|
||||
version: 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPlugin: InstalledPlugin = {
|
||||
...plugin,
|
||||
metadata: {
|
||||
...plugin.metadata,
|
||||
installedAt: plugin.metadata.installedAt ?? Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const index = plugins.findIndex((p) => p.filename === updatedPlugin.filename && p.type === updatedPlugin.type)
|
||||
if (index >= 0) {
|
||||
plugins[index] = updatedPlugin
|
||||
} else {
|
||||
plugins.push(updatedPlugin)
|
||||
}
|
||||
|
||||
const data: CachedPluginsData = {
|
||||
version: cacheData?.version ?? 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(claudePath, { recursive: true })
|
||||
await this.writeCacheFile(claudePath, data)
|
||||
}
|
||||
|
||||
async remove(workdir: string, filename: string, type: PluginType): Promise<void> {
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
let cacheData = await this.readCacheFile(claudePath)
|
||||
let plugins = cacheData?.plugins
|
||||
|
||||
if (!plugins) {
|
||||
plugins = await this.rebuild(workdir)
|
||||
cacheData = {
|
||||
version: 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = plugins.filter((p) => !(p.filename === filename && p.type === type))
|
||||
|
||||
const data: CachedPluginsData = {
|
||||
version: cacheData?.version ?? 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins: filtered
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(claudePath, { recursive: true })
|
||||
await this.writeCacheFile(claudePath, data)
|
||||
}
|
||||
|
||||
async rebuild(workdir: string): Promise<InstalledPlugin[]> {
|
||||
logger.info('Rebuilding plugin cache from filesystem', { workdir })
|
||||
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
|
||||
try {
|
||||
await fs.promises.access(claudePath, fs.constants.R_OK)
|
||||
} catch {
|
||||
logger.warn('.claude directory not found, returning empty plugin list', { claudePath })
|
||||
return []
|
||||
}
|
||||
|
||||
const plugins: InstalledPlugin[] = []
|
||||
|
||||
await Promise.all([
|
||||
this.collectFilePlugins(workdir, 'agent', plugins),
|
||||
this.collectFilePlugins(workdir, 'command', plugins),
|
||||
this.collectSkillPlugins(workdir, plugins)
|
||||
])
|
||||
|
||||
try {
|
||||
const cacheData: CachedPluginsData = {
|
||||
version: 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
await this.writeCacheFile(claudePath, cacheData)
|
||||
logger.info(`Rebuilt cache with ${plugins.length} plugins`, { workdir })
|
||||
} catch (error) {
|
||||
logger.error('Failed to write cache file after rebuild', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
private async collectFilePlugins(
|
||||
workdir: string,
|
||||
type: Exclude<PluginType, 'skill'>,
|
||||
plugins: InstalledPlugin[]
|
||||
): Promise<void> {
|
||||
const directory = this.deps.getClaudePluginDirectory(workdir, type)
|
||||
|
||||
try {
|
||||
await fs.promises.access(directory, fs.constants.R_OK)
|
||||
} catch {
|
||||
logger.debug(`${type} directory not found or not accessible`, { directory })
|
||||
return
|
||||
}
|
||||
|
||||
const files = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.isFile()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name).toLowerCase()
|
||||
if (!this.deps.allowedExtensions.includes(ext)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(directory, file.name)
|
||||
const sourcePath = path.join(this.deps.getPluginDirectoryName(type), file.name)
|
||||
const metadata = await parsePluginMetadata(filePath, sourcePath, this.deps.getPluginDirectoryName(type), type)
|
||||
plugins.push({ filename: file.name, type, metadata })
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse ${type} plugin: ${file.name}`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async collectSkillPlugins(workdir: string, plugins: InstalledPlugin[]): Promise<void> {
|
||||
const skillsPath = this.deps.getClaudePluginDirectory(workdir, 'skill')
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
|
||||
try {
|
||||
await fs.promises.access(skillsPath, fs.constants.R_OK)
|
||||
} catch {
|
||||
logger.debug('Skills directory not found or not accessible', { skillsPath })
|
||||
return
|
||||
}
|
||||
|
||||
const skillDirectories = await findAllSkillDirectories(skillsPath, claudePath)
|
||||
|
||||
for (const { folderPath, sourcePath } of skillDirectories) {
|
||||
try {
|
||||
const metadata = await parseSkillMetadata(folderPath, sourcePath, 'skills')
|
||||
plugins.push({ filename: metadata.filename, type: 'skill', metadata })
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse skill plugin: ${sourcePath}`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async readCacheFile(claudePath: string): Promise<CachedPluginsData | null> {
|
||||
const cachePath = path.join(claudePath, 'plugins.json')
|
||||
try {
|
||||
const content = await fs.promises.readFile(cachePath, 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
return CachedPluginsDataSchema.parse(data)
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to read cache file at ${cachePath}`, {
|
||||
error: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async writeCacheFile(claudePath: string, data: CachedPluginsData): Promise<void> {
|
||||
const cachePath = path.join(claudePath, 'plugins.json')
|
||||
const tempPath = `${cachePath}.tmp`
|
||||
|
||||
const content = JSON.stringify(data, null, 2)
|
||||
await fs.promises.writeFile(tempPath, content, 'utf-8')
|
||||
await fs.promises.rename(tempPath, cachePath)
|
||||
}
|
||||
}
|
||||
149
src/main/services/agents/plugins/PluginInstaller.ts
Normal file
149
src/main/services/agents/plugins/PluginInstaller.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { copyDirectoryRecursive, deleteDirectoryRecursive } from '@main/utils/fileOperations'
|
||||
import type { PluginError } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
|
||||
const logger = loggerService.withContext('PluginInstaller')
|
||||
|
||||
export class PluginInstaller {
|
||||
async installFilePlugin(agentId: string, sourceAbsolutePath: string, destPath: string): Promise<void> {
|
||||
const tempPath = `${destPath}.tmp`
|
||||
let fileCopied = false
|
||||
|
||||
try {
|
||||
await fs.promises.copyFile(sourceAbsolutePath, tempPath)
|
||||
fileCopied = true
|
||||
logger.debug('File copied to temp location', { agentId, tempPath })
|
||||
|
||||
await fs.promises.rename(tempPath, destPath)
|
||||
logger.debug('File moved to final location', { agentId, destPath })
|
||||
} catch (error) {
|
||||
if (fileCopied) {
|
||||
await this.safeUnlink(tempPath, 'temp file')
|
||||
}
|
||||
throw this.toPluginError('install', error)
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallFilePlugin(
|
||||
agentId: string,
|
||||
filename: string,
|
||||
type: 'agent' | 'command',
|
||||
filePath: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fs.promises.unlink(filePath)
|
||||
logger.debug('Plugin file deleted', { agentId, filename, type, filePath })
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw this.toPluginError('uninstall', error)
|
||||
}
|
||||
logger.warn('Plugin file already deleted', { agentId, filename, type, filePath })
|
||||
}
|
||||
}
|
||||
|
||||
async updateFilePluginContent(agentId: string, filePath: string, content: string): Promise<string> {
|
||||
try {
|
||||
await fs.promises.access(filePath, fs.constants.W_OK)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: filePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(filePath, content, 'utf8')
|
||||
logger.debug('Plugin content written successfully', {
|
||||
agentId,
|
||||
filePath,
|
||||
size: Buffer.byteLength(content, 'utf8')
|
||||
})
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'WRITE_FAILED',
|
||||
path: filePath,
|
||||
reason: error instanceof Error ? error.message : String(error)
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
return crypto.createHash('sha256').update(content).digest('hex')
|
||||
}
|
||||
|
||||
async installSkill(agentId: string, sourceAbsolutePath: string, destPath: string): Promise<void> {
|
||||
const logContext = logger.withContext('installSkill')
|
||||
let folderCopied = false
|
||||
const tempPath = `${destPath}.tmp`
|
||||
|
||||
try {
|
||||
try {
|
||||
await fs.promises.access(destPath)
|
||||
await deleteDirectoryRecursive(destPath)
|
||||
logContext.info('Removed existing skill folder', { agentId, destPath })
|
||||
} catch {
|
||||
// No existing folder
|
||||
}
|
||||
|
||||
await copyDirectoryRecursive(sourceAbsolutePath, tempPath)
|
||||
folderCopied = true
|
||||
logContext.info('Skill folder copied to temp location', { agentId, tempPath })
|
||||
|
||||
await fs.promises.rename(tempPath, destPath)
|
||||
logContext.info('Skill folder moved to final location', { agentId, destPath })
|
||||
} catch (error) {
|
||||
if (folderCopied) {
|
||||
await this.safeRemoveDirectory(tempPath, 'temp folder')
|
||||
}
|
||||
throw this.toPluginError('install-skill', error)
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallSkill(agentId: string, folderName: string, skillPath: string): Promise<void> {
|
||||
const logContext = logger.withContext('uninstallSkill')
|
||||
|
||||
try {
|
||||
await deleteDirectoryRecursive(skillPath)
|
||||
logContext.info('Skill folder deleted', { agentId, folderName, skillPath })
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw this.toPluginError('uninstall-skill', error)
|
||||
}
|
||||
logContext.warn('Skill folder already deleted', { agentId, folderName, skillPath })
|
||||
}
|
||||
}
|
||||
|
||||
private toPluginError(operation: string, error: unknown): PluginError {
|
||||
return {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation,
|
||||
reason: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
|
||||
private async safeUnlink(targetPath: string, label: string): Promise<void> {
|
||||
try {
|
||||
await fs.promises.unlink(targetPath)
|
||||
logger.debug(`Rolled back ${label}`, { targetPath })
|
||||
} catch (unlinkError) {
|
||||
logger.error(`Failed to rollback ${label}`, {
|
||||
targetPath,
|
||||
error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async safeRemoveDirectory(targetPath: string, label: string): Promise<void> {
|
||||
try {
|
||||
await deleteDirectoryRecursive(targetPath)
|
||||
logger.info(`Rolled back ${label}`, { targetPath })
|
||||
} catch (unlinkError) {
|
||||
logger.error(`Failed to rollback ${label}`, {
|
||||
targetPath,
|
||||
error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
614
src/main/services/agents/plugins/PluginService.ts
Normal file
614
src/main/services/agents/plugins/PluginService.ts
Normal file
@@ -0,0 +1,614 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { parsePluginMetadata, parseSkillMetadata } from '@main/utils/markdownParser'
|
||||
import type {
|
||||
GetAgentResponse,
|
||||
InstalledPlugin,
|
||||
InstallPluginOptions,
|
||||
ListAvailablePluginsResult,
|
||||
PluginError,
|
||||
PluginMetadata,
|
||||
PluginType,
|
||||
UninstallPluginOptions
|
||||
} from '@types'
|
||||
import { app } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
import { AgentService } from '../services/AgentService'
|
||||
import { PluginCacheStore } from './PluginCacheStore'
|
||||
import { PluginInstaller } from './PluginInstaller'
|
||||
|
||||
const logger = loggerService.withContext('PluginService')
|
||||
|
||||
interface PluginServiceConfig {
|
||||
maxFileSize: number // bytes
|
||||
cacheTimeout: number // milliseconds
|
||||
}
|
||||
|
||||
/**
|
||||
* PluginService manages agent and command plugins from resources directory.
|
||||
*
|
||||
* Features:
|
||||
* - Singleton pattern for consistent state management
|
||||
* - Caching of available plugins for performance
|
||||
* - Security validation (path traversal, file size, extensions)
|
||||
* - Transactional install/uninstall operations
|
||||
* - Integration with AgentService for metadata persistence
|
||||
*/
|
||||
export class PluginService {
|
||||
private static instance: PluginService | null = null
|
||||
|
||||
private availablePluginsCache: ListAvailablePluginsResult | null = null
|
||||
private cacheTimestamp = 0
|
||||
private config: PluginServiceConfig
|
||||
private readonly cacheStore: PluginCacheStore
|
||||
private readonly installer: PluginInstaller
|
||||
private readonly agentService: AgentService
|
||||
|
||||
private readonly ALLOWED_EXTENSIONS = ['.md', '.markdown']
|
||||
|
||||
private constructor(config?: Partial<PluginServiceConfig>) {
|
||||
this.config = {
|
||||
maxFileSize: config?.maxFileSize ?? 1024 * 1024, // 1MB default
|
||||
cacheTimeout: config?.cacheTimeout ?? 5 * 60 * 1000 // 5 minutes default
|
||||
}
|
||||
this.agentService = AgentService.getInstance()
|
||||
this.cacheStore = new PluginCacheStore({
|
||||
allowedExtensions: this.ALLOWED_EXTENSIONS,
|
||||
getPluginDirectoryName: this.getPluginDirectoryName.bind(this),
|
||||
getClaudeBasePath: this.getClaudeBasePath.bind(this),
|
||||
getClaudePluginDirectory: this.getClaudePluginDirectory.bind(this),
|
||||
getPluginsBasePath: this.getPluginsBasePath.bind(this)
|
||||
})
|
||||
this.installer = new PluginInstaller()
|
||||
|
||||
logger.info('PluginService initialized', {
|
||||
maxFileSize: this.config.maxFileSize,
|
||||
cacheTimeout: this.config.cacheTimeout
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(config?: Partial<PluginServiceConfig>): PluginService {
|
||||
if (!PluginService.instance) {
|
||||
PluginService.instance = new PluginService(config)
|
||||
}
|
||||
return PluginService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available plugins from resources directory (with caching)
|
||||
*/
|
||||
async listAvailable(): Promise<ListAvailablePluginsResult> {
|
||||
const now = Date.now()
|
||||
|
||||
// Return cached data if still valid
|
||||
if (this.availablePluginsCache && now - this.cacheTimestamp < this.config.cacheTimeout) {
|
||||
logger.debug('Returning cached plugin list', {
|
||||
cacheAge: now - this.cacheTimestamp
|
||||
})
|
||||
return this.availablePluginsCache
|
||||
}
|
||||
|
||||
logger.info('Scanning available plugins')
|
||||
|
||||
// Scan all plugin types
|
||||
const [agents, commands, skills] = await Promise.all([
|
||||
this.cacheStore.listAvailableFilePlugins('agent'),
|
||||
this.cacheStore.listAvailableFilePlugins('command'),
|
||||
this.cacheStore.listAvailableSkills()
|
||||
])
|
||||
|
||||
const result: ListAvailablePluginsResult = {
|
||||
agents,
|
||||
commands,
|
||||
skills, // NEW: include skills
|
||||
total: agents.length + commands.length + skills.length
|
||||
}
|
||||
|
||||
// Update cache
|
||||
this.availablePluginsCache = result
|
||||
this.cacheTimestamp = now
|
||||
|
||||
logger.info('Available plugins scanned', {
|
||||
agentsCount: agents.length,
|
||||
commandsCount: commands.length,
|
||||
skillsCount: skills.length,
|
||||
total: result.total
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Install plugin with validation and transactional safety
|
||||
*/
|
||||
async install(options: InstallPluginOptions): Promise<PluginMetadata> {
|
||||
logger.info('Installing plugin', options)
|
||||
|
||||
const context = await this.prepareInstallContext(options)
|
||||
|
||||
if (options.type === 'skill') {
|
||||
return await this.installSkillPlugin(options, context)
|
||||
}
|
||||
|
||||
return await this.installFilePlugin(options, context)
|
||||
}
|
||||
|
||||
private async prepareInstallContext(options: InstallPluginOptions): Promise<{
|
||||
agent: GetAgentResponse
|
||||
workdir: string
|
||||
sourceAbsolutePath: string
|
||||
}> {
|
||||
const agent = await this.getAgentOrThrow(options.agentId)
|
||||
const workdir = this.getWorkdirOrThrow(agent, options.agentId)
|
||||
|
||||
await this.validateWorkdir(agent, workdir)
|
||||
|
||||
const sourceAbsolutePath = this.cacheStore.resolveSourcePath(options.sourcePath)
|
||||
|
||||
return { agent, workdir, sourceAbsolutePath }
|
||||
}
|
||||
|
||||
private async installSkillPlugin(
|
||||
options: InstallPluginOptions,
|
||||
context: {
|
||||
agent: GetAgentResponse
|
||||
workdir: string
|
||||
sourceAbsolutePath: string
|
||||
}
|
||||
): Promise<PluginMetadata> {
|
||||
const { agent, workdir, sourceAbsolutePath } = context
|
||||
|
||||
await this.cacheStore.ensureSkillSourceDirectory(sourceAbsolutePath, options.sourcePath)
|
||||
|
||||
const metadata = await parseSkillMetadata(sourceAbsolutePath, options.sourcePath, 'skills')
|
||||
const sanitizedFolderName = this.sanitizeFolderName(metadata.filename)
|
||||
|
||||
await this.ensureClaudeDirectory(workdir, 'skill')
|
||||
const destPath = this.getClaudePluginPath(workdir, 'skill', sanitizedFolderName)
|
||||
|
||||
metadata.filename = sanitizedFolderName
|
||||
|
||||
await this.installer.installSkill(agent.id, sourceAbsolutePath, destPath)
|
||||
|
||||
const installedAt = Date.now()
|
||||
const metadataWithInstall: PluginMetadata = {
|
||||
...metadata,
|
||||
filename: sanitizedFolderName,
|
||||
installedAt,
|
||||
updatedAt: metadata.updatedAt ?? installedAt,
|
||||
type: 'skill'
|
||||
}
|
||||
const installedPlugin: InstalledPlugin = {
|
||||
filename: sanitizedFolderName,
|
||||
type: 'skill',
|
||||
metadata: metadataWithInstall
|
||||
}
|
||||
|
||||
await this.cacheStore.upsert(workdir, installedPlugin)
|
||||
this.upsertAgentPlugin(agent, installedPlugin)
|
||||
|
||||
logger.info('Skill installed successfully', {
|
||||
agentId: options.agentId,
|
||||
sourcePath: options.sourcePath,
|
||||
folderName: sanitizedFolderName
|
||||
})
|
||||
|
||||
return metadataWithInstall
|
||||
}
|
||||
|
||||
private async installFilePlugin(
|
||||
options: InstallPluginOptions,
|
||||
context: {
|
||||
agent: GetAgentResponse
|
||||
workdir: string
|
||||
sourceAbsolutePath: string
|
||||
}
|
||||
): Promise<PluginMetadata> {
|
||||
const { agent, workdir, sourceAbsolutePath } = context
|
||||
|
||||
if (options.type === 'skill') {
|
||||
throw {
|
||||
type: 'INVALID_FILE_TYPE',
|
||||
extension: options.type
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const filePluginType: 'agent' | 'command' = options.type
|
||||
|
||||
await this.cacheStore.validatePluginFile(sourceAbsolutePath, this.config.maxFileSize)
|
||||
|
||||
const category = path.basename(path.dirname(options.sourcePath))
|
||||
const metadata = await parsePluginMetadata(sourceAbsolutePath, options.sourcePath, category, filePluginType)
|
||||
|
||||
const sanitizedFilename = this.sanitizeFilename(metadata.filename)
|
||||
metadata.filename = sanitizedFilename
|
||||
|
||||
await this.ensureClaudeDirectory(workdir, filePluginType)
|
||||
const destPath = this.getClaudePluginPath(workdir, filePluginType, sanitizedFilename)
|
||||
|
||||
await this.installer.installFilePlugin(agent.id, sourceAbsolutePath, destPath)
|
||||
|
||||
const installedAt = Date.now()
|
||||
const metadataWithInstall: PluginMetadata = {
|
||||
...metadata,
|
||||
filename: sanitizedFilename,
|
||||
installedAt,
|
||||
updatedAt: metadata.updatedAt ?? installedAt,
|
||||
type: filePluginType
|
||||
}
|
||||
const installedPlugin: InstalledPlugin = {
|
||||
filename: sanitizedFilename,
|
||||
type: filePluginType,
|
||||
metadata: metadataWithInstall
|
||||
}
|
||||
|
||||
await this.cacheStore.upsert(workdir, installedPlugin)
|
||||
this.upsertAgentPlugin(agent, installedPlugin)
|
||||
|
||||
logger.info('Plugin installed successfully', {
|
||||
agentId: options.agentId,
|
||||
filename: sanitizedFilename,
|
||||
type: filePluginType
|
||||
})
|
||||
|
||||
return metadataWithInstall
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall plugin with cleanup
|
||||
*/
|
||||
async uninstall(options: UninstallPluginOptions): Promise<void> {
|
||||
logger.info('Uninstalling plugin', options)
|
||||
|
||||
const agent = await this.getAgentOrThrow(options.agentId)
|
||||
const workdir = this.getWorkdirOrThrow(agent, options.agentId)
|
||||
|
||||
await this.validateWorkdir(agent, workdir)
|
||||
|
||||
if (options.type === 'skill') {
|
||||
const sanitizedFolderName = this.sanitizeFolderName(options.filename)
|
||||
const skillPath = this.getClaudePluginPath(workdir, 'skill', sanitizedFolderName)
|
||||
|
||||
await this.installer.uninstallSkill(agent.id, sanitizedFolderName, skillPath)
|
||||
await this.cacheStore.remove(workdir, sanitizedFolderName, 'skill')
|
||||
this.removeAgentPlugin(agent, sanitizedFolderName, 'skill')
|
||||
|
||||
logger.info('Skill uninstalled successfully', {
|
||||
agentId: options.agentId,
|
||||
folderName: sanitizedFolderName
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const sanitizedFilename = this.sanitizeFilename(options.filename)
|
||||
const filePath = this.getClaudePluginPath(workdir, options.type, sanitizedFilename)
|
||||
|
||||
await this.installer.uninstallFilePlugin(agent.id, sanitizedFilename, options.type, filePath)
|
||||
await this.cacheStore.remove(workdir, sanitizedFilename, options.type)
|
||||
this.removeAgentPlugin(agent, sanitizedFilename, options.type)
|
||||
|
||||
logger.info('Plugin uninstalled successfully', {
|
||||
agentId: options.agentId,
|
||||
filename: sanitizedFilename,
|
||||
type: options.type
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List installed plugins for an agent (from database + filesystem validation)
|
||||
*/
|
||||
async listInstalled(agentId: string): Promise<InstalledPlugin[]> {
|
||||
logger.debug('Listing installed plugins', { agentId })
|
||||
|
||||
const agent = await this.getAgentOrThrow(agentId)
|
||||
|
||||
const workdir = agent.accessible_paths?.[0]
|
||||
|
||||
if (!workdir) {
|
||||
logger.warn('Agent has no accessible paths', { agentId })
|
||||
return []
|
||||
}
|
||||
|
||||
const plugins = await this.listInstalledFromCache(workdir)
|
||||
|
||||
logger.debug('Listed installed plugins from cache', {
|
||||
agentId,
|
||||
count: plugins.length
|
||||
})
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate plugin cache (for development/testing)
|
||||
*/
|
||||
invalidateCache(): void {
|
||||
this.availablePluginsCache = null
|
||||
this.cacheTimestamp = 0
|
||||
logger.info('Plugin cache invalidated')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cache File Management (for installed plugins)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Read cache file from .claude/plugins.json
|
||||
* Returns null if cache doesn't exist or is invalid
|
||||
*/
|
||||
|
||||
/**
|
||||
* List installed plugins from cache file
|
||||
* Falls back to filesystem scan if cache is missing or corrupt
|
||||
*/
|
||||
async listInstalledFromCache(workdir: string): Promise<InstalledPlugin[]> {
|
||||
logger.debug('Listing installed plugins from cache', { workdir })
|
||||
return await this.cacheStore.listInstalled(workdir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read plugin content from source (resources directory)
|
||||
*/
|
||||
async readContent(sourcePath: string): Promise<string> {
|
||||
logger.info('Reading plugin content', { sourcePath })
|
||||
const content = await this.cacheStore.readSourceContent(sourcePath)
|
||||
logger.debug('Plugin content read successfully', {
|
||||
sourcePath,
|
||||
size: content.length
|
||||
})
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* Write plugin content to installed plugin (in agent's .claude directory)
|
||||
* Note: Only works for file-based plugins (agents/commands), not skills
|
||||
*/
|
||||
async writeContent(agentId: string, filename: string, type: PluginType, content: string): Promise<void> {
|
||||
logger.info('Writing plugin content', { agentId, filename, type })
|
||||
|
||||
const agent = await this.getAgentOrThrow(agentId)
|
||||
const workdir = this.getWorkdirOrThrow(agent, agentId)
|
||||
|
||||
await this.validateWorkdir(agent, workdir)
|
||||
|
||||
// Check if plugin is installed
|
||||
let installedPlugins = agent.installed_plugins ?? []
|
||||
if (installedPlugins.length === 0) {
|
||||
installedPlugins = await this.cacheStore.listInstalled(workdir)
|
||||
agent.installed_plugins = installedPlugins
|
||||
}
|
||||
const installedPlugin = installedPlugins.find((p) => p.filename === filename && p.type === type)
|
||||
|
||||
if (!installedPlugin) {
|
||||
throw {
|
||||
type: 'PLUGIN_NOT_INSTALLED',
|
||||
filename,
|
||||
agentId
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
if (type === 'skill') {
|
||||
throw {
|
||||
type: 'INVALID_FILE_TYPE',
|
||||
extension: type
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const filePluginType = type as 'agent' | 'command'
|
||||
const filePath = this.getClaudePluginPath(workdir, filePluginType, filename)
|
||||
const newContentHash = await this.installer.updateFilePluginContent(agent.id, filePath, content)
|
||||
|
||||
const updatedMetadata: PluginMetadata = {
|
||||
...installedPlugin.metadata,
|
||||
contentHash: newContentHash,
|
||||
size: Buffer.byteLength(content, 'utf8'),
|
||||
updatedAt: Date.now(),
|
||||
filename,
|
||||
type: filePluginType
|
||||
}
|
||||
const updatedPlugin: InstalledPlugin = {
|
||||
filename,
|
||||
type: filePluginType,
|
||||
metadata: updatedMetadata
|
||||
}
|
||||
|
||||
await this.cacheStore.upsert(workdir, updatedPlugin)
|
||||
this.upsertAgentPlugin(agent, updatedPlugin)
|
||||
|
||||
logger.info('Plugin content updated successfully', {
|
||||
agentId,
|
||||
filename,
|
||||
type: filePluginType,
|
||||
newContentHash
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Helper Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve plugin type to directory name under .claude
|
||||
*/
|
||||
private getPluginDirectoryName(type: PluginType): 'agents' | 'commands' | 'skills' {
|
||||
if (type === 'agent') {
|
||||
return 'agents'
|
||||
}
|
||||
if (type === 'command') {
|
||||
return 'commands'
|
||||
}
|
||||
return 'skills'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base .claude directory for a workdir
|
||||
*/
|
||||
private getClaudeBasePath(workdir: string): string {
|
||||
return path.join(workdir, '.claude')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for a specific plugin type inside .claude
|
||||
*/
|
||||
private getClaudePluginDirectory(workdir: string, type: PluginType): string {
|
||||
return path.join(this.getClaudeBasePath(workdir), this.getPluginDirectoryName(type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute path for a plugin file/folder inside .claude
|
||||
*/
|
||||
private getClaudePluginPath(workdir: string, type: PluginType, filename: string): string {
|
||||
return path.join(this.getClaudePluginDirectory(workdir, type), filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path to plugins directory (handles packaged vs dev)
|
||||
*/
|
||||
private getPluginsBasePath(): string {
|
||||
// Use the utility function which handles both dev and production correctly
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, 'claude-code-plugins')
|
||||
}
|
||||
return path.join(__dirname, '../../node_modules/claude-code-plugins/plugins')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate source path to prevent path traversal attacks
|
||||
*/
|
||||
private async getAgentOrThrow(agentId: string): Promise<GetAgentResponse> {
|
||||
const agent = await this.agentService.getAgent(agentId)
|
||||
if (!agent) {
|
||||
throw {
|
||||
type: 'INVALID_WORKDIR',
|
||||
agentId,
|
||||
workdir: '',
|
||||
message: 'Agent not found'
|
||||
} as PluginError
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
private getWorkdirOrThrow(agent: GetAgentResponse, agentId: string): string {
|
||||
const workdir = agent.accessible_paths?.[0]
|
||||
if (!workdir) {
|
||||
throw {
|
||||
type: 'INVALID_WORKDIR',
|
||||
agentId,
|
||||
workdir: '',
|
||||
message: 'Agent has no accessible paths'
|
||||
} as PluginError
|
||||
}
|
||||
return workdir
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate workdir against agent's accessible paths
|
||||
*/
|
||||
private async validateWorkdir(agent: GetAgentResponse, workdir: string): Promise<void> {
|
||||
// Verify workdir is in agent's accessible_paths
|
||||
if (!agent.accessible_paths?.includes(workdir)) {
|
||||
throw {
|
||||
type: 'INVALID_WORKDIR',
|
||||
workdir,
|
||||
agentId: agent.id,
|
||||
message: 'Workdir not in agent accessible paths'
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Verify workdir exists and is accessible
|
||||
try {
|
||||
await fs.promises.access(workdir, fs.constants.R_OK | fs.constants.W_OK)
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'WORKDIR_NOT_FOUND',
|
||||
workdir,
|
||||
message: 'Workdir does not exist or is not accessible'
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
private upsertAgentPlugin(agent: GetAgentResponse, plugin: InstalledPlugin): void {
|
||||
const existing = agent.installed_plugins ?? []
|
||||
const filtered = existing.filter((p) => !(p.filename === plugin.filename && p.type === plugin.type))
|
||||
agent.installed_plugins = [...filtered, plugin]
|
||||
}
|
||||
|
||||
private removeAgentPlugin(agent: GetAgentResponse, filename: string, type: PluginType): void {
|
||||
if (!agent.installed_plugins) {
|
||||
agent.installed_plugins = []
|
||||
return
|
||||
}
|
||||
agent.installed_plugins = agent.installed_plugins.filter((p) => !(p.filename === filename && p.type === type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize filename to remove unsafe characters (for agents/commands)
|
||||
*/
|
||||
private sanitizeFilename(filename: string): string {
|
||||
// Remove path separators
|
||||
let sanitized = filename.replace(/[/\\]/g, '_')
|
||||
// Remove null bytes using String method to avoid control-regex lint error
|
||||
sanitized = sanitized.replace(new RegExp(String.fromCharCode(0), 'g'), '')
|
||||
// Limit to safe characters (alphanumeric, dash, underscore, dot)
|
||||
sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_')
|
||||
|
||||
// Ensure .md extension
|
||||
if (!sanitized.endsWith('.md') && !sanitized.endsWith('.markdown')) {
|
||||
sanitized += '.md'
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize folder name for skills (different rules than file names)
|
||||
* NO dots allowed to avoid confusion with file extensions
|
||||
*/
|
||||
private sanitizeFolderName(folderName: string): string {
|
||||
// Remove path separators
|
||||
let sanitized = folderName.replace(/[/\\]/g, '_')
|
||||
// Remove null bytes using String method to avoid control-regex lint error
|
||||
sanitized = sanitized.replace(new RegExp(String.fromCharCode(0), 'g'), '')
|
||||
// Limit to safe characters (alphanumeric, dash, underscore)
|
||||
// NOTE: No dots allowed to avoid confusion with file extensions
|
||||
sanitized = sanitized.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
|
||||
// Validate no extension was provided
|
||||
if (folderName.includes('.')) {
|
||||
logger.warn('Skill folder name contained dots, sanitized', {
|
||||
original: folderName,
|
||||
sanitized
|
||||
})
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure .claude subdirectory exists for the given plugin type
|
||||
*/
|
||||
private async ensureClaudeDirectory(workdir: string, type: PluginType): Promise<void> {
|
||||
const typeDir = this.getClaudePluginDirectory(workdir, type)
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(typeDir, { recursive: true })
|
||||
logger.debug('Ensured directory exists', { typeDir })
|
||||
} catch (error) {
|
||||
logger.error('Failed to create directory', {
|
||||
typeDir,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
throw {
|
||||
type: 'PERMISSION_DENIED',
|
||||
path: typeDir
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginService = PluginService.getInstance()
|
||||
@@ -1,5 +1,7 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { pluginService } from '@main/services/agents/plugins/PluginService'
|
||||
import { getDataPath } from '@main/utils'
|
||||
import type {
|
||||
AgentEntity,
|
||||
@@ -17,6 +19,8 @@ import { BaseService } from '../BaseService'
|
||||
import { type AgentRow, agentsTable, type InsertAgentRow } from '../database/schema'
|
||||
import type { AgentModelField } from '../errors'
|
||||
|
||||
const logger = loggerService.withContext('AgentService')
|
||||
|
||||
export class AgentService extends BaseService {
|
||||
private static instance: AgentService | null = null
|
||||
private readonly modelFields: AgentModelField[] = ['model', 'plan_model', 'small_model']
|
||||
@@ -92,6 +96,24 @@ export class AgentService extends BaseService {
|
||||
|
||||
const agent = this.deserializeJsonFields(result[0]) as GetAgentResponse
|
||||
agent.tools = await this.listMcpTools(agent.type, agent.mcps)
|
||||
|
||||
// Load installed_plugins from cache file instead of database
|
||||
const workdir = agent.accessible_paths?.[0]
|
||||
if (workdir) {
|
||||
try {
|
||||
agent.installed_plugins = await pluginService.listInstalledFromCache(workdir)
|
||||
} catch (error) {
|
||||
// Log error but don't fail the request
|
||||
logger.warn(`Failed to load installed plugins for agent ${id}`, {
|
||||
workdir,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
agent.installed_plugins = []
|
||||
}
|
||||
} else {
|
||||
agent.installed_plugins = []
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
ANTHROPIC_AUTH_TOKEN: modelInfo.provider.apiKey,
|
||||
ANTHROPIC_BASE_URL: modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost,
|
||||
ANTHROPIC_MODEL: modelInfo.modelId,
|
||||
ANTHROPIC_SMALL_FAST_MODEL: modelInfo.modelId,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: modelInfo.modelId,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: modelInfo.modelId,
|
||||
// TODO: support set small model in UI
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelInfo.modelId,
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
ELECTRON_NO_ATTACH_CONSOLE: '1'
|
||||
}
|
||||
|
||||
@@ -73,6 +73,15 @@ const emptyUsage: LanguageModelUsage = {
|
||||
*/
|
||||
const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
|
||||
|
||||
/**
|
||||
* Filters out command-* tags from text content to prevent internal command
|
||||
* messages from appearing in the user-facing UI.
|
||||
* Removes tags like <command-message>...</command-message> and <command-name>...</command-name>
|
||||
*/
|
||||
const filterCommandTags = (text: string): string => {
|
||||
return text.replace(/<command-[^>]+>.*?<\/command-[^>]+>/gs, '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts provider metadata from the raw Claude message so we can surface it
|
||||
* on every emitted stream part for observability and debugging purposes.
|
||||
@@ -270,12 +279,17 @@ function handleUserMessage(
|
||||
const chunks: AgentStreamPart[] = []
|
||||
const providerMetadata = sdkMessageToProviderMetadata(message)
|
||||
const content = message.message.content
|
||||
|
||||
const isSynthetic = message.isSynthetic ?? false
|
||||
if (typeof content === 'string') {
|
||||
if (!content) {
|
||||
return chunks
|
||||
}
|
||||
|
||||
const filteredContent = filterCommandTags(content)
|
||||
if (!filteredContent) {
|
||||
return chunks
|
||||
}
|
||||
|
||||
const id = message.uuid?.toString() || generateMessageId()
|
||||
chunks.push({
|
||||
type: 'text-start',
|
||||
@@ -285,7 +299,7 @@ function handleUserMessage(
|
||||
chunks.push({
|
||||
type: 'text-delta',
|
||||
id,
|
||||
text: content,
|
||||
text: filteredContent,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
@@ -323,24 +337,30 @@ function handleUserMessage(
|
||||
providerExecuted: true
|
||||
})
|
||||
}
|
||||
} else if (block.type === 'text') {
|
||||
const id = message.uuid?.toString() || generateMessageId()
|
||||
chunks.push({
|
||||
type: 'text-start',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-delta',
|
||||
id,
|
||||
text: (block as { text: string }).text,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-end',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
} else if (block.type === 'text' && !isSynthetic) {
|
||||
const rawText = (block as { text: string }).text
|
||||
const filteredText = filterCommandTags(rawText)
|
||||
|
||||
// Only push text chunks if there's content after filtering
|
||||
if (filteredText) {
|
||||
const id = message.uuid?.toString() || generateMessageId()
|
||||
chunks.push({
|
||||
type: 'text-start',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-delta',
|
||||
id,
|
||||
text: filteredText,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-end',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.warn('Unhandled user content block', { type: (block as any).type })
|
||||
}
|
||||
|
||||
@@ -9,13 +9,20 @@ const logger = loggerService.withContext('URLSchema:handleMcpProtocolUrl')
|
||||
|
||||
function installMCPServer(server: MCPServer) {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
const now = Date.now()
|
||||
|
||||
if (!server.id) {
|
||||
server.id = nanoid()
|
||||
const payload: MCPServer = {
|
||||
...server,
|
||||
id: server.id ?? nanoid(),
|
||||
installSource: 'protocol',
|
||||
isTrusted: false,
|
||||
isActive: false,
|
||||
trustedAt: undefined,
|
||||
installedAt: server.installedAt ?? now
|
||||
}
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server)
|
||||
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -567,7 +567,16 @@ const api = {
|
||||
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop),
|
||||
onReady: (callback: () => void): (() => void) => {
|
||||
const listener = () => {
|
||||
callback()
|
||||
}
|
||||
ipcRenderer.on(IpcChannel.ApiServer_Ready, listener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener(IpcChannel.ApiServer_Ready, listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
claudeCodePlugin: {
|
||||
listAvailable: (): Promise<PluginResult<ListAvailablePluginsResult>> =>
|
||||
@@ -583,6 +592,13 @@ const api = {
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ReadContent, sourcePath),
|
||||
writeContent: (options: WritePluginContentOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_WriteContent, options)
|
||||
},
|
||||
webSocket: {
|
||||
start: () => ipcRenderer.invoke(IpcChannel.WebSocket_Start),
|
||||
stop: () => ipcRenderer.invoke(IpcChannel.WebSocket_Stop),
|
||||
status: () => ipcRenderer.invoke(IpcChannel.WebSocket_Status),
|
||||
sendFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.WebSocket_SendFile, filePath),
|
||||
getAllCandidates: () => ipcRenderer.invoke(IpcChannel.WebSocket_GetAllCandidates)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import type { EndpointType, Model, Provider } from '@renderer/types'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@renderer/config/models', () => ({
|
||||
DEFAULT_MODEL_MAP: {
|
||||
assistant: { id: 'gpt-4', name: 'GPT-4' },
|
||||
quick: { id: 'gpt-4', name: 'GPT-4' },
|
||||
translate: { id: 'gpt-4', name: 'GPT-4' }
|
||||
},
|
||||
SYSTEM_MODELS: {
|
||||
defaultModel: [
|
||||
{ id: 'gpt-4', name: 'GPT-4' },
|
||||
{ id: 'gpt-4', name: 'GPT-4' },
|
||||
{ id: 'gpt-4', name: 'GPT-4' }
|
||||
],
|
||||
zhipu: [],
|
||||
silicon: [],
|
||||
openai: [],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import './color.css';
|
||||
@import './font.css';
|
||||
@import './markdown.css';
|
||||
@import './ant.css';
|
||||
@import './scrollbar.css';
|
||||
@import './container.css';
|
||||
@import './animation.css';
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@import '../../../../../packages/ui/src/styles/theme.css';
|
||||
|
||||
/* TODO heroui 迁移完成后即可删除 */
|
||||
/* heroui */
|
||||
@plugin '../../hero.ts';
|
||||
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
@source '../../../../../packages/ui/src/**/*.{js,ts,jsx,tsx}';
|
||||
/* @plugin '../../hero.ts'; */
|
||||
@source '../../../../../packages/ui/src/components/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@@ -21,117 +23,24 @@
|
||||
4. Put the new custom utility class into the utilities layer.
|
||||
*/
|
||||
|
||||
/* 应用特定的原始变量 */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
--icon: #00000099;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
--icon: #ffffff99;
|
||||
}
|
||||
|
||||
/* 应用特定的变量和动画(不与 UI 库冲突) */
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
/* Icon 颜色 - 应用特定变量 */
|
||||
--color-icon: var(--icon);
|
||||
|
||||
/* 跑马灯动画 - 应用特定 */
|
||||
--animate-marquee: marquee var(--duration) infinite linear;
|
||||
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||
--color-icon: var(--icon);
|
||||
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface EditableNumberProps {
|
||||
suffix?: string
|
||||
prefix?: string
|
||||
align?: 'start' | 'center' | 'end'
|
||||
formatter?: (value: number | null) => string | number
|
||||
}
|
||||
|
||||
const EditableNumber: FC<EditableNumberProps> = ({
|
||||
@@ -36,7 +37,8 @@ const EditableNumber: FC<EditableNumberProps> = ({
|
||||
style,
|
||||
className,
|
||||
size = 'middle',
|
||||
align = 'end'
|
||||
align = 'end',
|
||||
formatter
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [inputValue, setInputValue] = useState(value)
|
||||
@@ -90,7 +92,7 @@ const EditableNumber: FC<EditableNumberProps> = ({
|
||||
changeOnBlur={changeOnBlur}
|
||||
/>
|
||||
<DisplayText style={style} className={className} $align={align} $isEditing={isEditing}>
|
||||
{value ?? placeholder}
|
||||
{formatter ? formatter(value ?? null) : (value ?? placeholder)}
|
||||
</DisplayText>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Alert, Space } from 'antd'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
|
||||
119
src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx
Normal file
119
src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Modal } from 'antd'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type OptionType = 'assistant' | 'agent'
|
||||
|
||||
interface ShowParams {
|
||||
onSelect: (type: OptionType) => void
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: { type?: OptionType }) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [hoveredOption, setHoveredOption] = useState<OptionType | null>(null)
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const handleSelect = (type: OptionType) => {
|
||||
setOpen(false)
|
||||
onSelect(type)
|
||||
resolve({ type })
|
||||
}
|
||||
|
||||
AddAssistantOrAgentPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('chat.add.option.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
footer={null}
|
||||
width={560}>
|
||||
<div className="grid grid-cols-2 gap-4 py-4">
|
||||
{/* Assistant Option */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect('assistant')}
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('assistant')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
<MessageSquare
|
||||
size={24}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
hoveredOption === 'assistant' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('chat.add.assistant.title')}</h3>
|
||||
<p className="text-[var(--color-text-2)] text-sm">{t('chat.add.assistant.description')}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Agent Option */}
|
||||
<button
|
||||
onClick={() => handleSelect('agent')}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('agent')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
<Bot
|
||||
size={24}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
hoveredOption === 'agent' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('agent.add.title')}</h3>
|
||||
<p className="text-[var(--color-text-2)] text-sm">{t('agent.add.description')}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AddAssistantOrAgentPopup'
|
||||
|
||||
export default class AddAssistantOrAgentPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<{ type?: OptionType }>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
591
src/renderer/src/components/Popups/ExportToPhoneLanPopup.tsx
Normal file
591
src/renderer/src/components/Popups/ExportToPhoneLanPopup.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
const logger = loggerService.withContext('ExportToPhoneLanPopup')
|
||||
|
||||
interface Props {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
type ConnectionPhase = 'initializing' | 'waiting_qr_scan' | 'connecting' | 'connected' | 'disconnected' | 'error'
|
||||
type TransferPhase = 'idle' | 'preparing' | 'sending' | 'completed' | 'error'
|
||||
|
||||
const LoadingQRCode: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<Spinner />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.generating_qr')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<QRCodeSVG
|
||||
marginSize={2}
|
||||
value={qrCodeValue}
|
||||
level="Q"
|
||||
size={160}
|
||||
imageSettings={{
|
||||
src: '/src/assets/images/logo.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.scan_qr')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectingAnimation: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px dashed var(--color-status-warning)',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-warning)'
|
||||
}}>
|
||||
<Spinner size="lg" color="warning" />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
|
||||
{t('settings.data.export_to_phone.lan.status.connecting')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectedDisplay: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px dashed var(--color-status-success)',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-success)'
|
||||
}}>
|
||||
<span style={{ fontSize: '48px' }}>📱</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '8px' }}>
|
||||
{t('settings.data.export_to_phone.lan.connected')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorQRCode: React.FC<{ error: string | null }> = ({ error }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '20px',
|
||||
border: `1px solid var(--color-error)`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-error)'
|
||||
}}>
|
||||
<span style={{ fontSize: '48px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>
|
||||
{t('settings.data.export_to_phone.lan.connection_failed')}
|
||||
</span>
|
||||
{error && <span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>{error}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
const [connectionPhase, setConnectionPhase] = useState<ConnectionPhase>('initializing')
|
||||
const [transferPhase, setTransferPhase] = useState<TransferPhase>('idle')
|
||||
const [qrCodeValue, setQrCodeValue] = useState('')
|
||||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||||
const [sendProgress, setSendProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
||||
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 派生状态
|
||||
const isConnected = connectionPhase === 'connected'
|
||||
const canSend = isConnected && selectedFolderPath && transferPhase === 'idle'
|
||||
const isSending = transferPhase === 'preparing' || transferPhase === 'sending'
|
||||
|
||||
// 状态文本映射
|
||||
const connectionStatusText = useMemo(() => {
|
||||
const statusMap = {
|
||||
initializing: t('settings.data.export_to_phone.lan.status.initializing'),
|
||||
waiting_qr_scan: t('settings.data.export_to_phone.lan.status.waiting_qr_scan'),
|
||||
connecting: t('settings.data.export_to_phone.lan.status.connecting'),
|
||||
connected: t('settings.data.export_to_phone.lan.status.connected'),
|
||||
disconnected: t('settings.data.export_to_phone.lan.status.disconnected'),
|
||||
error: t('settings.data.export_to_phone.lan.status.error')
|
||||
}
|
||||
return statusMap[connectionPhase]
|
||||
}, [connectionPhase, t])
|
||||
|
||||
const transferStatusText = useMemo(() => {
|
||||
const statusMap = {
|
||||
idle: '',
|
||||
preparing: t('settings.data.export_to_phone.lan.status.preparing'),
|
||||
sending: t('settings.data.export_to_phone.lan.status.sending'),
|
||||
completed: t('settings.data.export_to_phone.lan.status.completed'),
|
||||
error: t('settings.data.export_to_phone.lan.status.error')
|
||||
}
|
||||
return statusMap[transferPhase]
|
||||
}, [transferPhase, t])
|
||||
|
||||
// 状态样式映射
|
||||
const connectionStatusStyles = useMemo(() => {
|
||||
const styleMap = {
|
||||
initializing: {
|
||||
bg: 'var(--color-background-mute)',
|
||||
border: 'var(--color-border-mute)'
|
||||
},
|
||||
waiting_qr_scan: {
|
||||
bg: 'var(--color-primary-mute)',
|
||||
border: 'var(--color-primary-soft)'
|
||||
},
|
||||
connecting: { bg: 'var(--color-status-warning)', border: 'var(--color-status-warning)' },
|
||||
connected: {
|
||||
bg: 'var(--color-status-success)',
|
||||
border: 'var(--color-status-success)'
|
||||
},
|
||||
disconnected: { bg: 'var(--color-error)', border: 'var(--color-error)' },
|
||||
error: { bg: 'var(--color-error)', border: 'var(--color-error)' }
|
||||
}
|
||||
return styleMap[connectionPhase]
|
||||
}, [connectionPhase])
|
||||
|
||||
const initWebSocket = useCallback(async () => {
|
||||
try {
|
||||
setConnectionPhase('initializing')
|
||||
await window.api.webSocket.start()
|
||||
const { port, ip } = await window.api.webSocket.status()
|
||||
|
||||
if (ip && port) {
|
||||
const candidates = await window.api.webSocket.getAllCandidates()
|
||||
const connectionInfo = {
|
||||
type: 'cherry-studio-app',
|
||||
candidates,
|
||||
selectedHost: ip,
|
||||
port,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
setQrCodeValue(JSON.stringify(connectionInfo))
|
||||
setConnectionPhase('waiting_qr_scan')
|
||||
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
|
||||
} else {
|
||||
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
|
||||
setConnectionPhase('error')
|
||||
}
|
||||
} catch (error) {
|
||||
setError(
|
||||
`${t('settings.data.export_to_phone.lan.error.init_failed')}: ${error instanceof Error ? error.message : ''}`
|
||||
)
|
||||
setConnectionPhase('error')
|
||||
logger.error('Failed to initialize WebSocket:', error as Error)
|
||||
}
|
||||
}, [t])
|
||||
|
||||
const handleClientConnected = useCallback((_event: any, data: { connected: boolean }) => {
|
||||
logger.info(`Client connection status: ${data.connected ? 'connected' : 'disconnected'}`)
|
||||
if (data.connected) {
|
||||
setConnectionPhase('connected')
|
||||
setError(null)
|
||||
} else {
|
||||
setConnectionPhase('disconnected')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMessageReceived = useCallback((_event: any, data: any) => {
|
||||
logger.info(`Received message from mobile: ${JSON.stringify(data)}`)
|
||||
}, [])
|
||||
|
||||
const handleSendProgress = useCallback(
|
||||
(_event: any, data: { progress: number }) => {
|
||||
const progress = data.progress
|
||||
setSendProgress(progress)
|
||||
|
||||
if (transferPhase === 'preparing' && progress > 0) {
|
||||
setTransferPhase('sending')
|
||||
}
|
||||
|
||||
if (progress >= 100) {
|
||||
setTransferPhase('completed')
|
||||
// 启动 3 秒倒计时自动关闭
|
||||
setAutoCloseCountdown(3)
|
||||
}
|
||||
},
|
||||
[transferPhase]
|
||||
)
|
||||
|
||||
const handleSelectZip = useCallback(async () => {
|
||||
const result = await window.api.file.select()
|
||||
if (result) {
|
||||
setSelectedFolderPath(result[0].path)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSendZip = useCallback(async () => {
|
||||
if (!selectedFolderPath) {
|
||||
setError(t('settings.data.export_to_phone.lan.error.no_file'))
|
||||
return
|
||||
}
|
||||
|
||||
setTransferPhase('preparing')
|
||||
setError(null)
|
||||
setSendProgress(0)
|
||||
|
||||
try {
|
||||
logger.info(`Starting file transfer: ${selectedFolderPath}`)
|
||||
await window.api.webSocket.sendFile(selectedFolderPath)
|
||||
} catch (error) {
|
||||
setError(
|
||||
`${t('settings.data.export_to_phone.lan.error.send_failed')}: ${error instanceof Error ? error.message : ''}`
|
||||
)
|
||||
setTransferPhase('error')
|
||||
logger.error('Failed to send file:', error as Error)
|
||||
}
|
||||
}, [selectedFolderPath, t])
|
||||
|
||||
// 尝试关闭弹窗 - 如果正在传输则显示确认
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSending) {
|
||||
setShowCloseConfirm(true)
|
||||
} else {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [isSending])
|
||||
|
||||
// 确认强制关闭
|
||||
const handleForceClose = useCallback(() => {
|
||||
logger.info('Force closing popup during transfer')
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
// 取消关闭确认
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseConfirm(false)
|
||||
}, [])
|
||||
|
||||
// 清理并关闭
|
||||
const handleClose = useCallback(async () => {
|
||||
try {
|
||||
// 主动断开 WebSocket 连接
|
||||
if (isConnected || connectionPhase !== 'disconnected') {
|
||||
logger.info('Closing popup, stopping WebSocket')
|
||||
await window.api.webSocket.stop()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop WebSocket on close:', error as Error)
|
||||
}
|
||||
resolve({})
|
||||
}, [resolve, isConnected, connectionPhase])
|
||||
|
||||
useEffect(() => {
|
||||
initWebSocket()
|
||||
|
||||
const removeClientConnectedListener = window.electron.ipcRenderer.on(
|
||||
'websocket-client-connected',
|
||||
handleClientConnected
|
||||
)
|
||||
const removeMessageReceivedListener = window.electron.ipcRenderer.on(
|
||||
'websocket-message-received',
|
||||
handleMessageReceived
|
||||
)
|
||||
const removeSendProgressListener = window.electron.ipcRenderer.on('file-send-progress', handleSendProgress)
|
||||
|
||||
return () => {
|
||||
removeClientConnectedListener()
|
||||
removeMessageReceivedListener()
|
||||
removeSendProgressListener()
|
||||
window.api.webSocket.stop()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 自动关闭倒计时
|
||||
useEffect(() => {
|
||||
if (autoCloseCountdown === null) return
|
||||
|
||||
if (autoCloseCountdown <= 0) {
|
||||
logger.debug('Auto-closing popup after transfer completion')
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setAutoCloseCountdown(autoCloseCountdown - 1)
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [autoCloseCountdown])
|
||||
|
||||
// 状态指示器组件
|
||||
const StatusIndicator = useCallback(
|
||||
() => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: connectionStatusStyles.bg,
|
||||
border: `1px solid ${connectionStatusStyles.border}`
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
|
||||
</div>
|
||||
),
|
||||
[connectionStatusStyles, connectionStatusText]
|
||||
)
|
||||
|
||||
// 二维码显示组件 - 使用显式条件渲染以避免类型不匹配
|
||||
const QRCodeDisplay = useCallback(() => {
|
||||
switch (connectionPhase) {
|
||||
case 'waiting_qr_scan':
|
||||
case 'disconnected':
|
||||
return <ScanQRCode qrCodeValue={qrCodeValue} />
|
||||
case 'initializing':
|
||||
return <LoadingQRCode />
|
||||
case 'connecting':
|
||||
return <ConnectingAnimation />
|
||||
case 'connected':
|
||||
return <ConnectedDisplay />
|
||||
case 'error':
|
||||
return <ErrorQRCode error={error} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}, [connectionPhase, qrCodeValue, error])
|
||||
|
||||
// 传输进度组件
|
||||
const TransferProgress = useCallback(() => {
|
||||
if (!isSending && transferPhase !== 'completed') return null
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '8px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
padding: '12px',
|
||||
border: `1px solid var(--color-border)`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-background-mute)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
<span style={{ color: 'var(--color-text)' }}>
|
||||
{t('settings.data.export_to_phone.lan.transfer_progress')}
|
||||
</span>
|
||||
<span
|
||||
style={{ color: transferPhase === 'completed' ? 'var(--color-status-success)' : 'var(--color-primary)' }}>
|
||||
{transferPhase === 'completed' ? '✅ ' + t('common.completed') : `${Math.round(sendProgress)}%`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={Math.round(sendProgress)}
|
||||
size="md"
|
||||
color={transferPhase === 'completed' ? 'success' : 'primary'}
|
||||
showValueLabel={false}
|
||||
aria-label="Send progress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [isSending, transferPhase, sendProgress, t])
|
||||
|
||||
const AutoCloseCountdown = useCallback(() => {
|
||||
if (transferPhase !== 'completed' || autoCloseCountdown === null || autoCloseCountdown <= 0) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-2)',
|
||||
textAlign: 'center',
|
||||
paddingTop: '4px'
|
||||
}}>
|
||||
{t('settings.data.export_to_phone.lan.auto_close_tip', { seconds: autoCloseCountdown })}
|
||||
</div>
|
||||
)
|
||||
}, [transferPhase, autoCloseCountdown, t])
|
||||
|
||||
// 错误显示组件
|
||||
const ErrorDisplay = useCallback(() => {
|
||||
if (!error || transferPhase !== 'error') return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px',
|
||||
border: `1px solid var(--color-error)`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-error)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>❌ {error}</span>
|
||||
</div>
|
||||
)
|
||||
}, [error, transferPhase])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={false}
|
||||
placement="center"
|
||||
onClose={handleClose}>
|
||||
<ModalContent>
|
||||
{() => (
|
||||
<>
|
||||
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<div>{t('settings.data.export_to_phone.lan.content')}</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</ModalBody>
|
||||
|
||||
{showCloseConfirm && (
|
||||
<ModalFooter>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-status-warning)',
|
||||
border: '1px solid var(--color-status-warning)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '20px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_title')}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_message')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
|
||||
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="danger" onPress={handleForceClose}>
|
||||
{t('settings.data.export_to_phone.lan.force_close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'ExportToPhoneLanPopup'
|
||||
|
||||
export default class ExportToPhoneLanPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show() {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ type Props = {
|
||||
agent?: AgentWithTools
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
afterSubmit?: (a: AgentEntity) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +80,7 @@ type Props = {
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose }) => {
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
|
||||
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
@@ -302,8 +303,13 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
configuration: form.configuration ? { ...form.configuration } : undefined
|
||||
} satisfies UpdateAgentForm
|
||||
|
||||
updateAgent(updatePayload)
|
||||
logger.debug('Updated agent', updatePayload)
|
||||
const result = await updateAgent(updatePayload)
|
||||
if (result) {
|
||||
logger.debug('Updated agent', result)
|
||||
afterSubmit?.(result)
|
||||
} else {
|
||||
logger.error('Update failed.')
|
||||
}
|
||||
} else {
|
||||
const newAgent = {
|
||||
type: form.type,
|
||||
@@ -316,12 +322,13 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
configuration: form.configuration ? { ...form.configuration } : undefined
|
||||
} satisfies AddAgentForm
|
||||
const result = await addAgent(newAgent)
|
||||
|
||||
if (!result.success) {
|
||||
loadingRef.current = false
|
||||
throw result.error
|
||||
}
|
||||
afterSubmit?.(result.data)
|
||||
}
|
||||
|
||||
loadingRef.current = false
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
@@ -330,16 +337,17 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
[
|
||||
form.type,
|
||||
form.model,
|
||||
form.accessible_paths,
|
||||
form.name,
|
||||
form.description,
|
||||
form.instructions,
|
||||
form.accessible_paths,
|
||||
form.allowed_tools,
|
||||
form.configuration,
|
||||
agent,
|
||||
onClose,
|
||||
t,
|
||||
updateAgent,
|
||||
afterSubmit,
|
||||
addAgent
|
||||
]
|
||||
)
|
||||
|
||||
@@ -14,17 +14,13 @@ export const qwen38bModel: Model = {
|
||||
group: 'Qwen'
|
||||
}
|
||||
|
||||
export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> = {
|
||||
defaultModel: [
|
||||
// Default assistant model
|
||||
glm45FlashModel,
|
||||
// Default topic naming model
|
||||
qwen38bModel,
|
||||
// Default translation model
|
||||
glm45FlashModel,
|
||||
// Default quick assistant model
|
||||
glm45FlashModel
|
||||
],
|
||||
export const DEFAULT_MODEL_MAP = {
|
||||
assistant: glm45FlashModel,
|
||||
quick: qwen38bModel,
|
||||
translate: glm45FlashModel
|
||||
} as const
|
||||
|
||||
export const SYSTEM_MODELS: Record<SystemProviderId, Model[]> = {
|
||||
cherryin: [],
|
||||
vertexai: [],
|
||||
'302ai': [
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export type UpdateAgentBaseOptions = {
|
||||
/** Whether to show success toast after updating. Defaults to true. */
|
||||
showSuccessToast?: boolean
|
||||
}
|
||||
@@ -10,6 +10,10 @@ export const useAgent = (id: string | null) => {
|
||||
const client = useAgentClient()
|
||||
const key = id ? client.agentPaths.withId(id) : null
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
|
||||
// Disable SWR fetching when server is not running by setting key to null
|
||||
const swrKey = apiServerRunning && id ? key : null
|
||||
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!id) {
|
||||
throw new Error(t('agent.get.error.null_id'))
|
||||
@@ -17,13 +21,10 @@ export const useAgent = (id: string | null) => {
|
||||
if (!apiServerConfig.enabled) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
}
|
||||
if (!apiServerRunning) {
|
||||
throw new Error(t('agent.server.error.not_running'))
|
||||
}
|
||||
const result = await client.getAgent(id)
|
||||
return result
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, id, t])
|
||||
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
|
||||
}, [apiServerConfig.enabled, client, id, t])
|
||||
const { data, error, isLoading } = useSWR(swrKey, fetcher)
|
||||
|
||||
return {
|
||||
agent: data,
|
||||
|
||||
@@ -25,6 +25,10 @@ export const useAgents = () => {
|
||||
const client = useAgentClient()
|
||||
const key = client.agentPaths.base
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
|
||||
// Disable SWR fetching when server is not running by setting key to null
|
||||
const swrKey = apiServerRunning ? key : null
|
||||
|
||||
const fetcher = useCallback(async () => {
|
||||
// API server will start on startup if enabled OR there are agents
|
||||
if (!apiServerConfig.enabled && !apiServerRunning) {
|
||||
@@ -37,7 +41,7 @@ export const useAgents = () => {
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
||||
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ListAgentsResponse, UpdateAgentForm } from '@renderer/types'
|
||||
import type { AgentEntity, ListAgentsResponse, UpdateAgentForm } from '@renderer/types'
|
||||
import type { UpdateAgentBaseOptions, UpdateAgentFunction } from '@renderer/types/agent'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
import type { UpdateAgentBaseOptions } from './types'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useUpdateAgent = () => {
|
||||
@@ -12,8 +12,8 @@ export const useUpdateAgent = () => {
|
||||
const client = useAgentClient()
|
||||
const listKey = client.agentPaths.base
|
||||
|
||||
const updateAgent = useCallback(
|
||||
async (form: UpdateAgentForm, options?: UpdateAgentBaseOptions) => {
|
||||
const updateAgent: UpdateAgentFunction = useCallback(
|
||||
async (form: UpdateAgentForm, options?: UpdateAgentBaseOptions): Promise<AgentEntity | undefined> => {
|
||||
try {
|
||||
const itemKey = client.agentPaths.withId(form.id)
|
||||
// may change to optimistic update
|
||||
@@ -23,8 +23,10 @@ export const useUpdateAgent = () => {
|
||||
if (options?.showSuccessToast ?? true) {
|
||||
window.toast.success(t('common.update_success'))
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed')))
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
[client, listKey, t]
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
|
||||
import type { AgentSessionEntity, ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
|
||||
import type { UpdateAgentBaseOptions, UpdateAgentSessionFunction } from '@renderer/types/agent'
|
||||
import { getErrorMessage } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
import type { UpdateAgentBaseOptions } from './types'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useUpdateSession = (agentId: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
|
||||
const updateSession = useCallback(
|
||||
async (form: UpdateSessionForm, options?: UpdateAgentBaseOptions) => {
|
||||
const updateSession: UpdateAgentSessionFunction = useCallback(
|
||||
async (form: UpdateSessionForm, options?: UpdateAgentBaseOptions): Promise<AgentSessionEntity | undefined> => {
|
||||
if (!agentId) return
|
||||
const paths = client.getSessionPaths(agentId)
|
||||
const listKey = paths.base
|
||||
@@ -29,8 +29,10 @@ export const useUpdateSession = (agentId: string | null) => {
|
||||
if (options?.showSuccessToast ?? true) {
|
||||
window.toast.success(t('common.update_success'))
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
window.toast.error({ title: t('agent.session.update.error.failed'), description: getErrorMessage(error) })
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
[agentId, client, t]
|
||||
|
||||
@@ -14,8 +14,8 @@ export const useApiServer = () => {
|
||||
const apiServerConfig = useAppSelector((state) => state.settings.apiServer)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Optimistic initial state.
|
||||
const [apiServerRunning, setApiServerRunning] = useState(apiServerConfig.enabled)
|
||||
// Initial state - no longer optimistic, wait for actual status
|
||||
const [apiServerRunning, setApiServerRunning] = useState(false)
|
||||
const [apiServerLoading, setApiServerLoading] = useState(true)
|
||||
|
||||
const setApiServerEnabled = useCallback(
|
||||
@@ -99,6 +99,16 @@ export const useApiServer = () => {
|
||||
checkApiServerStatus()
|
||||
}, [checkApiServerStatus])
|
||||
|
||||
// Listen for API server ready event
|
||||
useEffect(() => {
|
||||
const cleanup = window.api.apiServer.onReady(() => {
|
||||
logger.info('API server ready event received, checking status')
|
||||
checkApiServerStatus()
|
||||
})
|
||||
|
||||
return cleanup
|
||||
}, [checkApiServerStatus])
|
||||
|
||||
return {
|
||||
apiServerConfig,
|
||||
apiServerRunning,
|
||||
|
||||
@@ -80,17 +80,34 @@ export function useAppInit() {
|
||||
|
||||
useEffect(() => {
|
||||
savedAvatar?.value && cacheService.set('avatar', savedAvatar.value)
|
||||
}, [savedAvatar, dispatch])
|
||||
}, [savedAvatar])
|
||||
|
||||
useEffect(() => {
|
||||
const checkForUpdates = async () => {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
|
||||
if (!isPackaged || !autoCheckUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
const { updateInfo } = await window.api.checkForUpdate()
|
||||
updateAppUpdateState({ info: updateInfo })
|
||||
}
|
||||
|
||||
// Initial check with delay
|
||||
runAsyncFunction(async () => {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
if (isPackaged && autoCheckUpdate) {
|
||||
await delay(2)
|
||||
const { updateInfo } = await window.api.checkForUpdate()
|
||||
updateAppUpdateState({ info: updateInfo })
|
||||
await checkForUpdates()
|
||||
}
|
||||
})
|
||||
|
||||
// Set up 4-hour interval check
|
||||
const FOUR_HOURS = 4 * 60 * 60 * 1000
|
||||
const intervalId = setInterval(checkForUpdates, FOUR_HOURS)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [autoCheckUpdate, updateAppUpdateState])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -135,7 +152,7 @@ export function useAppInit() {
|
||||
cacheService.set('filesPath', info.filesPath)
|
||||
cacheService.set('resourcesPath', info.resourcesPath)
|
||||
})
|
||||
}, [dispatch])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
KnowledgeQueue.checkAllBases()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import {
|
||||
DEFAULT_MODEL_MAP,
|
||||
getThinkModelType,
|
||||
isSupportedReasoningEffortModel,
|
||||
isSupportedThinkingTokenModel,
|
||||
@@ -24,7 +25,11 @@ import {
|
||||
updateTopic,
|
||||
updateTopics
|
||||
} from '@renderer/store/assistants'
|
||||
import { setDefaultModel, setQuickModel, setTranslateModel } from '@renderer/store/llm'
|
||||
import {
|
||||
setDefaultModel as setDefaultModelAction,
|
||||
setQuickModel as setQuickModelAction,
|
||||
setTranslateModel as setTranslateModelAction
|
||||
} from '@renderer/store/llm'
|
||||
import type { Assistant, AssistantSettings, Model, ThinkingOption, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
@@ -198,12 +203,31 @@ export function useDefaultModel() {
|
||||
const { defaultModel, quickModel, translateModel } = useAppSelector((state) => state.llm)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const setDefaultModel = useCallback((model: Model) => dispatch(setDefaultModelAction({ model })), [dispatch])
|
||||
const setQuickModel = useCallback((model: Model) => dispatch(setQuickModelAction({ model })), [dispatch])
|
||||
const setTranslateModel = useCallback((model: Model) => dispatch(setTranslateModelAction({ model })), [dispatch])
|
||||
|
||||
const resetDefaultAssistantModel = useCallback(() => {
|
||||
setDefaultModel(DEFAULT_MODEL_MAP.assistant)
|
||||
}, [setDefaultModel])
|
||||
|
||||
const resetTranslateModel = useCallback(() => {
|
||||
setTranslateModel(DEFAULT_MODEL_MAP.translate)
|
||||
}, [setTranslateModel])
|
||||
|
||||
const resetQuickModel = useCallback(() => {
|
||||
setQuickModel(DEFAULT_MODEL_MAP.quick)
|
||||
}, [setQuickModel])
|
||||
|
||||
return {
|
||||
defaultModel,
|
||||
setDefaultModel,
|
||||
resetDefaultAssistantModel,
|
||||
quickModel,
|
||||
resetQuickModel,
|
||||
setQuickModel,
|
||||
translateModel,
|
||||
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
|
||||
setQuickModel: (model: Model) => dispatch(setQuickModel({ model })),
|
||||
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
|
||||
setTranslateModel,
|
||||
resetTranslateModel
|
||||
}
|
||||
}
|
||||
|
||||
57
src/renderer/src/hooks/useMCPServerTrust.tsx
Normal file
57
src/renderer/src/hooks/useMCPServerTrust.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import ProtocolInstallWarningContent from '@renderer/pages/settings/MCPSettings/ProtocolInstallWarning'
|
||||
import {
|
||||
ensureServerTrusted as ensureServerTrustedCore,
|
||||
getCommandPreview
|
||||
} from '@renderer/pages/settings/MCPSettings/utils'
|
||||
import type { MCPServer } from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useMCPServers } from './useMCPServers'
|
||||
|
||||
/**
|
||||
* Hook for handling MCP server trust verification
|
||||
* Binds UI (modal dialog) to the core trust verification logic
|
||||
*/
|
||||
export const useMCPServerTrust = () => {
|
||||
const { updateMCPServer } = useMCPServers()
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* Request user confirmation to trust a server
|
||||
* Shows a warning modal with server command preview
|
||||
*/
|
||||
const requestConfirm = useCallback(
|
||||
async (server: MCPServer): Promise<boolean> => {
|
||||
const commandPreview = getCommandPreview(server)
|
||||
return modalConfirm({
|
||||
title: t('settings.mcp.protocolInstallWarning.title'),
|
||||
content: (
|
||||
<ProtocolInstallWarningContent
|
||||
message={t('settings.mcp.protocolInstallWarning.message')}
|
||||
commandLabel={t('settings.mcp.protocolInstallWarning.command')}
|
||||
commandPreview={commandPreview}
|
||||
/>
|
||||
),
|
||||
okText: t('settings.mcp.protocolInstallWarning.run'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: { danger: true }
|
||||
})
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
/**
|
||||
* Ensures a server is trusted before proceeding
|
||||
* Combines core logic with UI confirmation
|
||||
*/
|
||||
const ensureServerTrusted = useCallback(
|
||||
async (server: MCPServer): Promise<MCPServer | null> => {
|
||||
return ensureServerTrustedCore(server, requestConfirm, updateMCPServer)
|
||||
},
|
||||
[requestConfirm, updateMCPServer]
|
||||
)
|
||||
|
||||
return { ensureServerTrusted }
|
||||
}
|
||||
@@ -11,12 +11,12 @@ import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
||||
import { find, isEmpty } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react'
|
||||
|
||||
import { useAssistant } from './useAssistant'
|
||||
|
||||
let _activeTopic: Topic
|
||||
let _setActiveTopic: (topic: Topic) => void
|
||||
let _setActiveTopic: Dispatch<SetStateAction<Topic>>
|
||||
|
||||
// const logger = loggerService.withContext('useTopic')
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function useUserTheme() {
|
||||
const colorPrimary = Color(theme.colorPrimary)
|
||||
|
||||
document.body.style.setProperty('--color-primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--color-primary-foreground', getForegroundColor(colorPrimary.hex()))
|
||||
// overwrite hero UI primary color.
|
||||
document.body.style.setProperty('--primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--primary-foreground', getForegroundColor(colorPrimary.hex()))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Handle complex tasks with various tools",
|
||||
"error": {
|
||||
"failed": "Failed to add a agent",
|
||||
"invalid_agent": "Invalid Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Daily conversations and quick Q&A",
|
||||
"title": "Add Assistant"
|
||||
},
|
||||
"option": {
|
||||
"title": "Select Type"
|
||||
},
|
||||
"topic": {
|
||||
"title": "New Topic"
|
||||
}
|
||||
@@ -838,7 +843,7 @@
|
||||
"label": "Context",
|
||||
"tip": "The number of previous messages to keep in the context."
|
||||
},
|
||||
"max": "Max",
|
||||
"max": "Unlimited",
|
||||
"max_tokens": {
|
||||
"confirm": "Set max tokens",
|
||||
"confirm_content": "Set the maximum number of tokens the model can generate. Need to consider the context limit of the model, otherwise an error will be reported",
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"collapse": "Collapse",
|
||||
"completed": "Completed",
|
||||
"confirm": "Confirm",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
"copy_failed": "Copy failed",
|
||||
"current": "Current",
|
||||
"cut": "Cut",
|
||||
"default": "Default",
|
||||
"delete": "Delete",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "A powerful AI assistant for producer",
|
||||
"downloading": "Downloading...",
|
||||
"enterprise": {
|
||||
"title": "Enterprise"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Feedback"
|
||||
},
|
||||
"label": "About & Feedback",
|
||||
"license": {
|
||||
"button": "License",
|
||||
"title": "License"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Releases",
|
||||
"title": "Release Notes"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Export Menu Settings",
|
||||
"yuque": "Export to Yuque"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Select backup file"
|
||||
},
|
||||
"content": "Export some data, including chat logs and settings. Please note that the backup process may take some time. Thank you for your patience.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Auto-closing in {{seconds}} seconds...",
|
||||
"confirm_close_message": "File transfer is in progress. Closing will interrupt the transfer. Are you sure you want to force close?",
|
||||
"confirm_close_title": "Confirm Close",
|
||||
"connected": "Connected",
|
||||
"connection_failed": "Connection failed",
|
||||
"content": "Please ensure your computer and phone are on the same network for LAN transfer. Open the Cherry Studio App to scan this QR code.",
|
||||
"error": {
|
||||
"init_failed": "Initialization failed",
|
||||
"no_file": "No file selected",
|
||||
"no_ip": "Unable to get IP address",
|
||||
"send_failed": "Failed to send file"
|
||||
},
|
||||
"force_close": "Force Close",
|
||||
"generating_qr": "Generating QR code...",
|
||||
"noZipSelected": "No compressed file selected",
|
||||
"scan_qr": "Please scan QR code with your phone",
|
||||
"selectZip": "Select a compressed file",
|
||||
"sendZip": "Begin data recovery",
|
||||
"status": {
|
||||
"completed": "Transfer completed",
|
||||
"connected": "Connected",
|
||||
"connecting": "Connecting...",
|
||||
"disconnected": "Disconnected",
|
||||
"error": "Connection error",
|
||||
"initializing": "Initializing connection...",
|
||||
"preparing": "Preparing transfer...",
|
||||
"sending": "Transferring {{progress}}%",
|
||||
"waiting_qr_scan": "Please scan QR code to connect"
|
||||
},
|
||||
"title": "LAN transmission",
|
||||
"transfer_progress": "Transfer progress"
|
||||
},
|
||||
"title": "Export to phone"
|
||||
},
|
||||
"hour_interval_one": "{{count}} hour",
|
||||
"hour_interval_other": "{{count}} hours",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "No prompts available",
|
||||
"requiredField": "Required Field"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Startup command",
|
||||
"message": "This MCP was installed from an external source via protocol. Running unknown tools may harm your computer.",
|
||||
"run": "Run",
|
||||
"title": "Run external MCP?"
|
||||
},
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "Provider name",
|
||||
"providerUrl": "Provider URL",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "调用各种工具处理复杂任务",
|
||||
"error": {
|
||||
"failed": "添加 Agent 失败",
|
||||
"invalid_agent": "无效的 Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常对话和快速问答",
|
||||
"title": "添加助手"
|
||||
},
|
||||
"option": {
|
||||
"title": "选择添加类型"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新建话题"
|
||||
}
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "清除",
|
||||
"close": "关闭",
|
||||
"collapse": "折叠",
|
||||
"completed": "完成",
|
||||
"confirm": "确认",
|
||||
"copied": "已复制",
|
||||
"copy": "复制",
|
||||
"copy_failed": "复制失败",
|
||||
"current": "当前",
|
||||
"cut": "剪切",
|
||||
"default": "默认",
|
||||
"delete": "删除",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "一款为创造者而生的 AI 助手",
|
||||
"downloading": "正在下载更新...",
|
||||
"enterprise": {
|
||||
"title": "企业版"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "反馈",
|
||||
"title": "意见反馈"
|
||||
},
|
||||
"label": "关于我们",
|
||||
"license": {
|
||||
"button": "查看",
|
||||
"title": "许可证"
|
||||
},
|
||||
"releases": {
|
||||
"button": "查看",
|
||||
"title": "更新日志"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "导出菜单设置",
|
||||
"yuque": "导出到语雀"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "选择备份文件"
|
||||
},
|
||||
"content": "导出部分数据,包括聊天记录、设置。请注意,备份过程可能需要一些时间,感谢您的耐心等待。",
|
||||
"lan": {
|
||||
"auto_close_tip": "{{seconds}} 秒后自动关闭...",
|
||||
"confirm_close_message": "文件正在传输中,关闭将中断传输。确定要强制关闭吗?",
|
||||
"confirm_close_title": "确认关闭",
|
||||
"connected": "连接成功",
|
||||
"connection_failed": "连接失败",
|
||||
"content": "请确保电脑和手机处于同一网络以使用局域网传输。请打开 Cherry Studio App 扫描此二维码。",
|
||||
"error": {
|
||||
"init_failed": "初始化失败",
|
||||
"no_file": "未选择文件",
|
||||
"no_ip": "无法获取 IP 地址",
|
||||
"send_failed": "发送文件失败"
|
||||
},
|
||||
"force_close": "强制关闭",
|
||||
"generating_qr": "正在生成二维码...",
|
||||
"noZipSelected": "未选择压缩文件",
|
||||
"scan_qr": "请使用手机扫码连接",
|
||||
"selectZip": "选择压缩文件",
|
||||
"sendZip": "开始恢复数据",
|
||||
"status": {
|
||||
"completed": "传输完成",
|
||||
"connected": "连接成功",
|
||||
"connecting": "正在连接中...",
|
||||
"disconnected": "连接已断开",
|
||||
"error": "连接出错",
|
||||
"initializing": "正在初始化连接...",
|
||||
"preparing": "准备传输中...",
|
||||
"sending": "传输中 {{progress}}%",
|
||||
"waiting_qr_scan": "请扫描二维码连接"
|
||||
},
|
||||
"title": "局域网传输",
|
||||
"transfer_progress": "传输进度"
|
||||
},
|
||||
"title": "导出至手机"
|
||||
},
|
||||
"hour_interval_one": "{{count}} 小时",
|
||||
"hour_interval_other": "{{count}} 小时",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "无可用提示",
|
||||
"requiredField": "必填字段"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "启动命令",
|
||||
"message": "该 MCP 是通过协议从外部来源安装的,运行来历不明的工具可能对您的计算机造成危害。",
|
||||
"run": "运行",
|
||||
"title": "运行外部 MCP?"
|
||||
},
|
||||
"provider": "提供者",
|
||||
"providerPlaceholder": "提供者名称",
|
||||
"providerUrl": "提供者网址",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "調用各種工具處理複雜任務",
|
||||
"error": {
|
||||
"failed": "無法新增代理人",
|
||||
"invalid_agent": "無效的 Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常對話和快速問答",
|
||||
"title": "新增助手"
|
||||
},
|
||||
"option": {
|
||||
"title": "選擇新增類型"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新增話題"
|
||||
}
|
||||
@@ -838,7 +843,7 @@
|
||||
"label": "上下文",
|
||||
"tip": "在上下文中保留的前幾則訊息"
|
||||
},
|
||||
"max": "最大",
|
||||
"max": "不限",
|
||||
"max_tokens": {
|
||||
"confirm": "設置最大 Token 數",
|
||||
"confirm_content": "設置單次交互所用的最大 Token 數,會影響返回結果的長度。要根據模型上下文限制來設定,否則會發生錯誤",
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "清除",
|
||||
"close": "關閉",
|
||||
"collapse": "折疊",
|
||||
"completed": "已完成",
|
||||
"confirm": "確認",
|
||||
"copied": "已複製",
|
||||
"copy": "複製",
|
||||
"copy_failed": "複製失敗",
|
||||
"current": "当前",
|
||||
"cut": "剪下",
|
||||
"default": "預設",
|
||||
"delete": "刪除",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "一款為創作者而生的強大 AI 助手",
|
||||
"downloading": "正在下載...",
|
||||
"enterprise": {
|
||||
"title": "企業版"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "回饋",
|
||||
"title": "回饋"
|
||||
},
|
||||
"label": "關於與回饋",
|
||||
"license": {
|
||||
"button": "檢視",
|
||||
"title": "授權"
|
||||
},
|
||||
"releases": {
|
||||
"button": "檢視",
|
||||
"title": "更新日誌"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "匯出選單設定",
|
||||
"yuque": "匯出到語雀"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "選擇備份檔案"
|
||||
},
|
||||
"content": "匯出部分數據,包括聊天記錄、設定。請注意,備份過程可能需要一些時間,感謝您的耐心等候。",
|
||||
"lan": {
|
||||
"auto_close_tip": "將於 {{seconds}} 秒後自動關閉...",
|
||||
"confirm_close_message": "檔案傳輸正在進行中。關閉將會中斷傳輸。您確定要強制關閉嗎?",
|
||||
"confirm_close_title": "確認關閉",
|
||||
"connected": "已連線",
|
||||
"connection_failed": "連線失敗",
|
||||
"content": "請確保電腦和手機處於同一網路以使用區域網路傳輸。請打開 Cherry Studio App 掃描此 QR 碼。",
|
||||
"error": {
|
||||
"init_failed": "初始化失敗",
|
||||
"no_file": "未選擇檔案",
|
||||
"no_ip": "無法取得 IP 位址",
|
||||
"send_failed": "無法傳送檔案"
|
||||
},
|
||||
"force_close": "強制關閉",
|
||||
"generating_qr": "正在生成 QR 碼...",
|
||||
"noZipSelected": "未選取壓縮檔案",
|
||||
"scan_qr": "請使用手機掃描QR碼",
|
||||
"selectZip": "選擇壓縮檔案",
|
||||
"sendZip": "開始恢復資料",
|
||||
"status": {
|
||||
"completed": "轉帳完成",
|
||||
"connected": "已連線",
|
||||
"connecting": "連線中...",
|
||||
"disconnected": "已斷線",
|
||||
"error": "連線錯誤",
|
||||
"initializing": "正在初始化連線...",
|
||||
"preparing": "正在準備傳輸...",
|
||||
"sending": "傳輸中 {{progress}}%",
|
||||
"waiting_qr_scan": "請掃描QR碼以連接"
|
||||
},
|
||||
"title": "區域網路傳輸",
|
||||
"transfer_progress": "傳輸進度"
|
||||
},
|
||||
"title": "匯出手機"
|
||||
},
|
||||
"hour_interval_one": "{{count}} 小時",
|
||||
"hour_interval_other": "{{count}} 小時",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "無可用提示",
|
||||
"requiredField": "必填欄位"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "啟動命令",
|
||||
"message": "此 MCP 透過協議從外部來源安裝,執行來源不明的工具可能會對您的電腦造成危害。",
|
||||
"run": "執行",
|
||||
"title": "執行外部 MCP?"
|
||||
},
|
||||
"provider": "提供者",
|
||||
"providerPlaceholder": "提供者名稱",
|
||||
"providerUrl": "提供者網址",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Bearbeiten Sie komplexe Aufgaben mit verschiedenen Werkzeugen",
|
||||
"error": {
|
||||
"failed": "Agent hinzufügen fehlgeschlagen",
|
||||
"invalid_agent": "Ungültiger Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Tägliche Gespräche und kurze Fragen & Antworten",
|
||||
"title": "Assistent hinzufügen"
|
||||
},
|
||||
"option": {
|
||||
"title": "Typ auswählen"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Neues Thema erstellen"
|
||||
}
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Löschen",
|
||||
"close": "Schließen",
|
||||
"collapse": "Einklappen",
|
||||
"completed": "Abgeschlossen",
|
||||
"confirm": "Bestätigen",
|
||||
"copied": "Kopiert",
|
||||
"copy": "Kopieren",
|
||||
"copy_failed": "Kopieren fehlgeschlagen",
|
||||
"current": "Aktuell",
|
||||
"cut": "Ausschneiden",
|
||||
"default": "Standard",
|
||||
"delete": "Löschen",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "Ein KI-Assistent für Kreative",
|
||||
"downloading": "Update wird heruntergeladen...",
|
||||
"enterprise": {
|
||||
"title": "Unternehmen"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Feedback"
|
||||
},
|
||||
"label": "Über uns",
|
||||
"license": {
|
||||
"button": "Anzeigen",
|
||||
"title": "Lizenz"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Anzeigen",
|
||||
"title": "Changelog"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Export-Menü-Einstellungen",
|
||||
"yuque": "Nach Yuque exportieren"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Sicherungsdatei auswählen"
|
||||
},
|
||||
"content": "Exportieren Sie einige Daten, einschließlich Chat-Protokollen und Einstellungen. Bitte beachten Sie, dass der Sicherungsvorgang einige Zeit in Anspruch nehmen kann. Vielen Dank für Ihre Geduld.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Automatisches Schließen in {{seconds}} Sekunden...",
|
||||
"confirm_close_message": "Dateiübertragung läuft. Beim Schließen wird die Übertragung unterbrochen. Möchten Sie wirklich das Schließen erzwingen?",
|
||||
"confirm_close_title": "Schließen bestätigen",
|
||||
"connected": "Verbunden",
|
||||
"connection_failed": "Verbindung fehlgeschlagen",
|
||||
"content": "Bitte stelle sicher, dass sich dein Computer und dein Telefon im selben Netzwerk befinden, um eine LAN-Übertragung durchzuführen. Öffne die Cherry Studio App, um diesen QR-Code zu scannen.",
|
||||
"error": {
|
||||
"init_failed": "Initialisierung fehlgeschlagen",
|
||||
"no_file": "Keine Datei ausgewählt",
|
||||
"no_ip": "IP-Adresse kann nicht abgerufen werden",
|
||||
"send_failed": "Fehler beim Senden der Datei"
|
||||
},
|
||||
"force_close": "Erzwungenes Schließen",
|
||||
"generating_qr": "QR-Code wird generiert...",
|
||||
"noZipSelected": "Keine komprimierte Datei ausgewählt",
|
||||
"scan_qr": "Bitte scannen Sie den QR-Code mit Ihrem Telefon.",
|
||||
"selectZip": "Wählen Sie eine komprimierte Datei",
|
||||
"sendZip": "Datenwiederherstellung beginnen",
|
||||
"status": {
|
||||
"completed": "Übertragung abgeschlossen",
|
||||
"connected": "Verbunden",
|
||||
"connecting": "Verbindung wird hergestellt...",
|
||||
"disconnected": "Getrennt",
|
||||
"error": "Verbindungsfehler",
|
||||
"initializing": "Verbindung wird initialisiert...",
|
||||
"preparing": "Übertragung wird vorbereitet...",
|
||||
"sending": "Übertrage {{progress}}%",
|
||||
"waiting_qr_scan": "Bitte QR-Code scannen, um zu verbinden"
|
||||
},
|
||||
"title": "LAN-Übertragung",
|
||||
"transfer_progress": "Übertragungsfortschritt"
|
||||
},
|
||||
"title": "Auf Telefon exportieren"
|
||||
},
|
||||
"hour_interval_one": "{{count}} Stunde",
|
||||
"hour_interval_other": "{{count}} Stunden",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "Keine Prompts verfügbar",
|
||||
"requiredField": "Pflichtfeld"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Startbefehl",
|
||||
"message": "Dieses MCP wurde über ein Protokoll aus einer externen Quelle installiert. Das Ausführen unbekannter Tools kann Ihren Computer schädigen.",
|
||||
"run": "Laufen",
|
||||
"title": "Externes MCP ausführen?"
|
||||
},
|
||||
"provider": "Anbieter",
|
||||
"providerPlaceholder": "Anbietername",
|
||||
"providerUrl": "Anbieter-Website",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Χειριστείτε πολύπλοκες εργασίες με διάφορα εργαλεία",
|
||||
"error": {
|
||||
"failed": "Αποτυχία προσθήκης πράκτορα",
|
||||
"invalid_agent": "Μη έγκυρος Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Καθημερινές συνομιλίες και γρήγορες ερωταπαντήσεις",
|
||||
"title": "Προσθήκη βοηθού"
|
||||
},
|
||||
"option": {
|
||||
"title": "Επιλέξτε Τύπο"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Δημιουργία νέου θέματος"
|
||||
}
|
||||
@@ -838,7 +843,7 @@
|
||||
"label": "Πλήθος ενδιάμεσων",
|
||||
"tip": "Πλήθος των μηνυμάτων που θα παραμείνουν στα ενδιάμεσα, όσο μεγαλύτερο είναι το αριθμός, τόσο μεγαλύτερο είναι το μήκος του ενδιάμεσου και τόσο περισσότερα tokens χρησιμοποιούνται. Συνομιλία συνήθως συνιστάται μεταξύ 5-10"
|
||||
},
|
||||
"max": "Όχι ορισμένο",
|
||||
"max": "άπειρος",
|
||||
"max_tokens": {
|
||||
"confirm": "Ενεργοποίηση περιορισμού μήκους μηνύματος",
|
||||
"confirm_content": "Μετά την ενεργοποίηση του περιορισμού μήκους μηνύματος, ο μέγιστος αριθμός των tokens που χρησιμοποιούνται κάθε φορά, θα επηρεάζει το μήκος της απάντησης. Πρέπει να το ρυθμίζετε βάσει των περιορισμών του πλαισίου του μοντέλου, διαφορετικά θα σφάλλεται.",
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Καθαρισμός",
|
||||
"close": "Κλείσιμο",
|
||||
"collapse": "Σύμπτυξη",
|
||||
"completed": "Ολοκληρώθηκε",
|
||||
"confirm": "Επιβεβαίωση",
|
||||
"copied": "Αντιγράφηκε",
|
||||
"copy": "Αντιγραφή",
|
||||
"copy_failed": "Αποτυχία αντιγραφής",
|
||||
"current": "Τρέχων",
|
||||
"cut": "Κοπή",
|
||||
"default": "Προεπιλογή",
|
||||
"delete": "Διαγραφή",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",
|
||||
"downloading": "Λήψη ενημερώσεων...",
|
||||
"enterprise": {
|
||||
"title": "Επιχείρηση"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Σχόλια και Παρατηρήσεις",
|
||||
"title": "Αποστολή σχολίων"
|
||||
},
|
||||
"label": "Περί μας",
|
||||
"license": {
|
||||
"button": "Προβολή",
|
||||
"title": "Licenses"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Προβολή",
|
||||
"title": "Ημερολόγιο Ενημερώσεων"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Εξαγωγή ρυθμίσεων μενού",
|
||||
"yuque": "Εξαγωγή στο Yuque"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Επιλέξτε αρχείο αντιγράφων ασφαλείας"
|
||||
},
|
||||
"content": "Εξαγωγή μέρους των δεδομένων, συμπεριλαμβανομένων των ιστορικών συνομιλιών και των ρυθμίσεων. Σημειώστε ότι η διαδικασία δημιουργίας αντιγράφων ασφαλείας ενδέχεται να διαρκέσει κάποιο χρονικό διάστημα, ευχαριστούμε για την υπομονή σας.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Αυτόματο κλείσιμο σε {{seconds}} δευτερόλεπτα...",
|
||||
"confirm_close_message": "Η μεταφορά αρχείων είναι σε εξέλιξη. Το κλείσιμο θα διακόψει τη μεταφορά. Είστε σίγουροι ότι θέλετε να κλείσετε βίαια;",
|
||||
"confirm_close_title": "Επιβεβαίωση Κλεισίματος",
|
||||
"connected": "Συνδεδεμένος",
|
||||
"connection_failed": "Η σύνδεση απέτυχε",
|
||||
"content": "Βεβαιωθείτε ότι ο υπολογιστής και το κινητό βρίσκονται στο ίδιο δίκτυο για να χρησιμοποιήσετε τη μεταφορά LAN. Ανοίξτε την εφαρμογή Cherry Studio και σαρώστε αυτόν τον κωδικό QR.",
|
||||
"error": {
|
||||
"init_failed": "Η αρχικοποίηση απέτυχε",
|
||||
"no_file": "Κανένα αρχείο δεν επιλέχθηκε",
|
||||
"no_ip": "Αδυναμία λήψης διεύθυνσης IP",
|
||||
"send_failed": "Αποτυχία αποστολής αρχείου"
|
||||
},
|
||||
"force_close": "Κλείσιμο με βία",
|
||||
"generating_qr": "Δημιουργία κώδικα QR...",
|
||||
"noZipSelected": "Δεν επιλέχθηκε συμπιεσμένο αρχείο",
|
||||
"scan_qr": "Παρακαλώ σαρώστε τον κωδικό QR με το τηλέφωνό σας",
|
||||
"selectZip": "Επιλέξτε συμπιεσμένο αρχείο",
|
||||
"sendZip": "Έναρξη ανάκτησης δεδομένων",
|
||||
"status": {
|
||||
"completed": "Η μεταφορά ολοκληρώθηκε",
|
||||
"connected": "Συνδεδεμένος",
|
||||
"connecting": "Σύνδεση...",
|
||||
"disconnected": "Αποσυνδέθηκε",
|
||||
"error": "Σφάλμα σύνδεσης",
|
||||
"initializing": "Αρχικοποίηση σύνδεσης...",
|
||||
"preparing": "Προετοιμασία μεταφοράς...",
|
||||
"sending": "Μεταφορά {{progress}}%",
|
||||
"waiting_qr_scan": "Παρακαλώ σαρώστε τον κωδικό QR για σύνδεση"
|
||||
},
|
||||
"title": "Μεταφορά τοπικού δικτύου",
|
||||
"transfer_progress": "Πρόοδος μεταφοράς"
|
||||
},
|
||||
"title": "Εξαγωγή στο κινητό"
|
||||
},
|
||||
"hour_interval_one": "{{count}} ώρα",
|
||||
"hour_interval_other": "{{count}} ώρες",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "Δεν υπάρχουν διαθέσιμες υποδείξεις",
|
||||
"requiredField": "Υποχρεωτικό πεδίο"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Εντολή εκκίνησης",
|
||||
"message": "Αυτό το MCP εγκαταστάθηκε από εξωτερική πηγή μέσω πρωτοκόλλου. Η εκτέλεση άγνωστων εργαλείων ενδέχεται να βλάψει τον υπολογιστή σας.",
|
||||
"run": "Τρέξε",
|
||||
"title": "Εκτέλεση εξωτερικού MCP;"
|
||||
},
|
||||
"provider": "Πάροχος",
|
||||
"providerPlaceholder": "Όνομα παρόχου",
|
||||
"providerUrl": "URL Παρόχου",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Maneja tareas complejas con varias herramientas",
|
||||
"error": {
|
||||
"failed": "Error al añadir agente",
|
||||
"invalid_agent": "Agent inválido"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversaciones diarias y preguntas y respuestas rápidas",
|
||||
"title": "Agregar asistente"
|
||||
},
|
||||
"option": {
|
||||
"title": "Seleccionar Tipo"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Crear nuevo tema"
|
||||
}
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Limpiar",
|
||||
"close": "Cerrar",
|
||||
"collapse": "Colapsar",
|
||||
"completed": "Completado",
|
||||
"confirm": "Confirmar",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar",
|
||||
"copy_failed": "Error al copiar",
|
||||
"current": "Actual",
|
||||
"cut": "Cortar",
|
||||
"default": "Predeterminado",
|
||||
"delete": "Eliminar",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "Una asistente de IA creada para los creadores",
|
||||
"downloading": "Descargando actualización...",
|
||||
"enterprise": {
|
||||
"title": "Empresa"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Enviar feedback",
|
||||
"title": "Enviar comentarios"
|
||||
},
|
||||
"label": "Acerca de nosotros",
|
||||
"license": {
|
||||
"button": "Ver",
|
||||
"title": "Licencia"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Ver",
|
||||
"title": "Registro de cambios"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Exportar configuración del menú",
|
||||
"yuque": "Exportar a Yuque"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Seleccionar archivo de copia de seguridad"
|
||||
},
|
||||
"content": "Exportar parte de los datos, incluidos los registros de chat y la configuración. Tenga en cuenta que el proceso de copia de seguridad puede tardar un tiempo; gracias por su paciencia.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Cierre automático en {{seconds}} segundos...",
|
||||
"confirm_close_message": "La transferencia de archivos está en progreso. Cerrar interrumpirá la transferencia. ¿Estás seguro de que quieres forzar el cierre?",
|
||||
"confirm_close_title": "Confirmar Cierre",
|
||||
"connected": "Conectado",
|
||||
"connection_failed": "Conexión fallida",
|
||||
"content": "Asegúrate de que el ordenador y el móvil estén en la misma red para usar la transferencia por LAN. Abre la aplicación Cherry Studio y escanea este código QR.",
|
||||
"error": {
|
||||
"init_failed": "Falló la inicialización",
|
||||
"no_file": "Ningún archivo seleccionado",
|
||||
"no_ip": "No se puede obtener la dirección IP",
|
||||
"send_failed": "Error al enviar el archivo"
|
||||
},
|
||||
"force_close": "Cerrar forzosamente",
|
||||
"generating_qr": "Generando código QR...",
|
||||
"noZipSelected": "No se ha seleccionado ningún archivo comprimido",
|
||||
"scan_qr": "Por favor, escanea el código QR con tu teléfono",
|
||||
"selectZip": "Seleccionar archivo comprimido",
|
||||
"sendZip": "Comenzar la recuperación de datos",
|
||||
"status": {
|
||||
"completed": "Transferencia completada",
|
||||
"connected": "Conectado",
|
||||
"connecting": "Conectando...",
|
||||
"disconnected": "Desconectado",
|
||||
"error": "Error de conexión",
|
||||
"initializing": "Inicializando conexión...",
|
||||
"preparing": "Preparando transferencia...",
|
||||
"sending": "Transfiriendo {{progress}}%",
|
||||
"waiting_qr_scan": "Por favor, escanea el código QR para conectarte"
|
||||
},
|
||||
"title": "Transferencia de red local",
|
||||
"transfer_progress": "Progreso de transferencia"
|
||||
},
|
||||
"title": "Exportar al teléfono"
|
||||
},
|
||||
"hour_interval_one": "{{count}} hora",
|
||||
"hour_interval_other": "{{count}} horas",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "No hay indicaciones disponibles",
|
||||
"requiredField": "Campo obligatorio"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Comando de inicio",
|
||||
"message": "Este MCP fue instalado desde una fuente externa a través del protocolo. Ejecutar herramientas desconocidas puede dañar tu computadora.",
|
||||
"run": "Correr",
|
||||
"title": "¿Ejecutar MCP externo?"
|
||||
},
|
||||
"provider": "Proveedor",
|
||||
"providerPlaceholder": "Nombre del proveedor",
|
||||
"providerUrl": "URL del proveedor",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Gérez des tâches complexes avec divers outils",
|
||||
"error": {
|
||||
"failed": "Échec de l'ajout de l'agent",
|
||||
"invalid_agent": "Agent invalide"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversations quotidiennes et Q&R rapides",
|
||||
"title": "Ajouter un assistant"
|
||||
},
|
||||
"option": {
|
||||
"title": "Sélectionner le type"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Nouveau sujet"
|
||||
}
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Effacer",
|
||||
"close": "Fermer",
|
||||
"collapse": "Réduire",
|
||||
"completed": "Terminé",
|
||||
"confirm": "Confirmer",
|
||||
"copied": "Copié",
|
||||
"copy": "Copier",
|
||||
"copy_failed": "Échec de la copie",
|
||||
"current": "Actuel",
|
||||
"cut": "Couper",
|
||||
"default": "Défaut",
|
||||
"delete": "Supprimer",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "Un assistant IA conçu pour les créateurs",
|
||||
"downloading": "Téléchargement de la mise à jour en cours...",
|
||||
"enterprise": {
|
||||
"title": "Entreprise"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Faire un retour",
|
||||
"title": "Retour d'information"
|
||||
},
|
||||
"label": "À propos de nous",
|
||||
"license": {
|
||||
"button": "Afficher",
|
||||
"title": "Licence"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Afficher",
|
||||
"title": "Journal des mises à jour"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Exporter les paramètres du menu",
|
||||
"yuque": "Exporter vers Yuque"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Sélectionner le fichier de sauvegarde"
|
||||
},
|
||||
"content": "Exporter une partie des données, incluant les historiques de discussion et les paramètres. Veuillez noter que le processus de sauvegarde peut prendre un certain temps ; merci pour votre patience.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Fermeture automatique dans {{seconds}} secondes...",
|
||||
"confirm_close_message": "Le transfert de fichier est en cours. Fermer interrompra le transfert. Êtes-vous sûr de vouloir forcer la fermeture ?",
|
||||
"confirm_close_title": "Confirmer la fermeture",
|
||||
"connected": "Connecté",
|
||||
"connection_failed": "Échec de la connexion",
|
||||
"content": "Assurez-vous que l'ordinateur et le téléphone sont connectés au même réseau pour utiliser le transfert en réseau local. Ouvrez l'application Cherry Studio et scannez ce code QR.",
|
||||
"error": {
|
||||
"init_failed": "Échec de l'initialisation",
|
||||
"no_file": "Aucun fichier sélectionné",
|
||||
"no_ip": "Impossible d'obtenir l'adresse IP",
|
||||
"send_failed": "Échec de l'envoi du fichier"
|
||||
},
|
||||
"force_close": "Fermer de force",
|
||||
"generating_qr": "Génération du code QR...",
|
||||
"noZipSelected": "Aucun fichier compressé sélectionné",
|
||||
"scan_qr": "Veuillez scanner le code QR avec votre téléphone",
|
||||
"selectZip": "Sélectionner le fichier compressé",
|
||||
"sendZip": "Commencer la restauration des données",
|
||||
"status": {
|
||||
"completed": "Transfert terminé",
|
||||
"connected": "Connecté",
|
||||
"connecting": "Connexion...",
|
||||
"disconnected": "Déconnecté",
|
||||
"error": "Erreur de connexion",
|
||||
"initializing": "Initialisation de la connexion...",
|
||||
"preparing": "Préparation du transfert...",
|
||||
"sending": "Transfert {{progress}} %",
|
||||
"waiting_qr_scan": "Veuillez scanner le code QR pour vous connecter"
|
||||
},
|
||||
"title": "Transmission en réseau local",
|
||||
"transfer_progress": "Progression du transfert"
|
||||
},
|
||||
"title": "Exporter vers le téléphone"
|
||||
},
|
||||
"hour_interval_one": "{{count}} heure",
|
||||
"hour_interval_other": "{{count}} heures",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "Aucune invite disponible",
|
||||
"requiredField": "Champ obligatoire"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Commande de démarrage",
|
||||
"message": "Ce MCP a été installé depuis une source externe via le protocole. L'exécution d'outils inconnus peut endommager votre ordinateur.",
|
||||
"run": "Courir",
|
||||
"title": "Exécuter un MCP externe ?"
|
||||
},
|
||||
"provider": "Поставщик",
|
||||
"providerPlaceholder": "Название поставщика",
|
||||
"providerUrl": "Адрес поставщика",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "さまざまなツールを使って複雑なタスクを処理する",
|
||||
"error": {
|
||||
"failed": "エージェントの追加に失敗しました",
|
||||
"invalid_agent": "無効なエージェント"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常会話と簡単なQ&A",
|
||||
"title": "アシスタントを追加"
|
||||
},
|
||||
"option": {
|
||||
"title": "タイプを選択"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新しいトピック"
|
||||
}
|
||||
@@ -838,7 +843,7 @@
|
||||
"label": "コンテキスト",
|
||||
"tip": "コンテキストに保持する以前のメッセージの数"
|
||||
},
|
||||
"max": "最大",
|
||||
"max": "制限なし",
|
||||
"max_tokens": {
|
||||
"confirm": "最大トークン数",
|
||||
"confirm_content": "最大トークン数を設定すると、モデルが生成できる最大トークン数が制限されます。これにより、返される結果の長さに影響が出る可能性があります。モデルのコンテキスト制限に基づいて設定する必要があります。そうしないとエラーが発生します",
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "クリア",
|
||||
"close": "閉じる",
|
||||
"collapse": "折りたたむ",
|
||||
"completed": "完了",
|
||||
"confirm": "確認",
|
||||
"copied": "コピーされました",
|
||||
"copy": "コピー",
|
||||
"copy_failed": "コピーに失敗しました",
|
||||
"current": "現在",
|
||||
"cut": "切り取り",
|
||||
"default": "デフォルト",
|
||||
"delete": "削除",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "クリエイターのための強力なAIアシスタント",
|
||||
"downloading": "ダウンロード中...",
|
||||
"enterprise": {
|
||||
"title": "エンタープライズ"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "フィードバック",
|
||||
"title": "フィードバック"
|
||||
},
|
||||
"label": "について",
|
||||
"license": {
|
||||
"button": "ライセンス",
|
||||
"title": "ライセンス"
|
||||
},
|
||||
"releases": {
|
||||
"button": "リリース",
|
||||
"title": "リリースノート"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "エクスポートメニュー設定",
|
||||
"yuque": "語雀にエクスポート"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "バックアップファイルを選択"
|
||||
},
|
||||
"content": "一部のデータ、チャット履歴や設定をエクスポートします。バックアップには時間がかかる場合がありますので、しばらくお待ちください。",
|
||||
"lan": {
|
||||
"auto_close_tip": "{{seconds}}秒後に自動的に閉じます...",
|
||||
"confirm_close_message": "ファイル転送が進行中です。閉じると転送が中断されます。強制終了してもよろしいですか?",
|
||||
"confirm_close_title": "閉じることを確認",
|
||||
"connected": "接続済み",
|
||||
"connection_failed": "接続に失敗しました",
|
||||
"content": "コンピューターとスマートフォンが同じネットワークに接続されていることを確認し、ローカルエリアネットワーク転送を使用してください。Cherry Studioアプリを開き、このQRコードをスキャンしてください。",
|
||||
"error": {
|
||||
"init_failed": "初期化に失敗しました",
|
||||
"no_file": "ファイルが選択されていません",
|
||||
"no_ip": "IPアドレスを取得できません",
|
||||
"send_failed": "ファイルの送信に失敗しました"
|
||||
},
|
||||
"force_close": "強制終了",
|
||||
"generating_qr": "QRコードを生成中...",
|
||||
"noZipSelected": "圧縮ファイルが選択されていません",
|
||||
"scan_qr": "携帯電話でQRコードをスキャンしてください",
|
||||
"selectZip": "圧縮ファイルを選択",
|
||||
"sendZip": "データの復元を開始します",
|
||||
"status": {
|
||||
"completed": "転送完了",
|
||||
"connected": "接続済み",
|
||||
"connecting": "接続中...",
|
||||
"disconnected": "切断されました",
|
||||
"error": "接続エラー",
|
||||
"initializing": "接続を初期化中...",
|
||||
"preparing": "転送準備中...",
|
||||
"sending": "転送中 {{progress}}%",
|
||||
"waiting_qr_scan": "QRコードをスキャンして接続してください"
|
||||
},
|
||||
"title": "LAN転送",
|
||||
"transfer_progress": "転送進行"
|
||||
},
|
||||
"title": "スマートフォンにエクスポート"
|
||||
},
|
||||
"hour_interval_one": "{{count}} 時間",
|
||||
"hour_interval_other": "{{count}} 時間",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
||||
"requiredField": "必須フィールド"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "起動コマンド",
|
||||
"message": "このMCPは外部ソースからプロトコル経由でインストールされました。不明なツールを実行すると、コンピューターに危害を及ぼす可能性があります。",
|
||||
"run": "走る",
|
||||
"title": "外部のMCPを実行しますか?"
|
||||
},
|
||||
"provider": "プロバイダー",
|
||||
"providerPlaceholder": "プロバイダー名",
|
||||
"providerUrl": "プロバイダーURL",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Lide com tarefas complexas usando várias ferramentas",
|
||||
"error": {
|
||||
"failed": "Falha ao adicionar agente",
|
||||
"invalid_agent": "Agent inválido"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversas diárias e perguntas e respostas rápidas",
|
||||
"title": "Adicionar assistente"
|
||||
},
|
||||
"option": {
|
||||
"title": "Selecionar Tipo"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Novo Tópico"
|
||||
}
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Limpar",
|
||||
"close": "Fechar",
|
||||
"collapse": "Recolher",
|
||||
"completed": "Concluído",
|
||||
"confirm": "Confirmar",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar",
|
||||
"copy_failed": "Falha ao copiar",
|
||||
"current": "Atual",
|
||||
"cut": "Cortar",
|
||||
"default": "Padrão",
|
||||
"delete": "Excluir",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "Um assistente de IA criado para criadores",
|
||||
"downloading": "Baixando atualizações...",
|
||||
"enterprise": {
|
||||
"title": "Empresa"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Enviar feedback"
|
||||
},
|
||||
"label": "Sobre Nós",
|
||||
"license": {
|
||||
"button": "Ver",
|
||||
"title": "Licença"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Ver",
|
||||
"title": "Registro de alterações"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Exportar Configurações do Menu",
|
||||
"yuque": "Exportar para Yuque"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Selecionar arquivo de backup"
|
||||
},
|
||||
"content": "Exportar parte dos dados, incluindo registros de conversas e configurações. Observe que o processo de backup pode demorar um pouco; agradecemos sua paciência.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Fechando automaticamente em {{seconds}} segundos...",
|
||||
"confirm_close_message": "Transferência de arquivo em andamento. Fechar irá interromper a transferência. Tem certeza de que deseja forçar o fechamento?",
|
||||
"confirm_close_title": "Confirmar Fechamento",
|
||||
"connected": "Conectado",
|
||||
"connection_failed": "Falha na conexão",
|
||||
"content": "Certifique-se de que o computador e o telefone estejam na mesma rede para usar a transferência via LAN. Abra o aplicativo Cherry Studio e escaneie este código QR.",
|
||||
"error": {
|
||||
"init_failed": "Falha na inicialização",
|
||||
"no_file": "Nenhum arquivo selecionado",
|
||||
"no_ip": "Incapaz de obter endereço IP",
|
||||
"send_failed": "Falha ao enviar arquivo"
|
||||
},
|
||||
"force_close": "Forçar Fechamento",
|
||||
"generating_qr": "Gerando código QR...",
|
||||
"noZipSelected": "Nenhum arquivo de compressão selecionado",
|
||||
"scan_qr": "Por favor, escaneie o código QR com o seu telefone",
|
||||
"selectZip": "Selecionar arquivo compactado",
|
||||
"sendZip": "Iniciar recuperação de dados",
|
||||
"status": {
|
||||
"completed": "Transferência concluída",
|
||||
"connected": "Conectado",
|
||||
"connecting": "Conectando...",
|
||||
"disconnected": "Desconectado",
|
||||
"error": "Erro de conexão",
|
||||
"initializing": "Inicializando conexão...",
|
||||
"preparing": "Preparando transferência...",
|
||||
"sending": "Transferindo {{progress}}%",
|
||||
"waiting_qr_scan": "Por favor, escaneie o código QR para conectar"
|
||||
},
|
||||
"title": "transmissão de rede local",
|
||||
"transfer_progress": "Progresso da transferência"
|
||||
},
|
||||
"title": "Exportar para o telemóvel"
|
||||
},
|
||||
"hour_interval_one": "{{count}} hora",
|
||||
"hour_interval_other": "{{count}} horas",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "Nenhuma dica disponível",
|
||||
"requiredField": "Campo obrigatório"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Comando de inicialização",
|
||||
"message": "Este MCP foi instalado a partir de uma fonte externa via protocolo. Executar ferramentas desconhecidas pode prejudicar seu computador.",
|
||||
"run": "Correr",
|
||||
"title": "Executar MCP externo?"
|
||||
},
|
||||
"provider": "Fornecedor",
|
||||
"providerPlaceholder": "Nome do Fornecedor",
|
||||
"providerUrl": "URL do Fornecedor",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Справляйтесь со сложными задачами с помощью различных инструментов",
|
||||
"error": {
|
||||
"failed": "Не удалось добавить агента",
|
||||
"invalid_agent": "Недействительный агент"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Ежедневные разговоры и быстрые вопросы и ответы",
|
||||
"title": "Добавить ассистента"
|
||||
},
|
||||
"option": {
|
||||
"title": "Выберите тип"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Новый топик"
|
||||
}
|
||||
@@ -838,7 +843,7 @@
|
||||
"label": "Контекст",
|
||||
"tip": "Количество предыдущих сообщений, которые нужно сохранить в контексте."
|
||||
},
|
||||
"max": "Максимум",
|
||||
"max": "без ограничений",
|
||||
"max_tokens": {
|
||||
"confirm": "Максимальное количество токенов",
|
||||
"confirm_content": "Установить максимальное количество токенов, влияет на длину результата. Нужно учитывать контекст модели, иначе будет ошибка",
|
||||
@@ -1047,10 +1052,12 @@
|
||||
"clear": "Очистить",
|
||||
"close": "Закрыть",
|
||||
"collapse": "Свернуть",
|
||||
"completed": "Завершено",
|
||||
"confirm": "Подтверждение",
|
||||
"copied": "Скопировано",
|
||||
"copy": "Копировать",
|
||||
"copy_failed": "Не удалось скопировать",
|
||||
"current": "Текущий",
|
||||
"cut": "Вырезать",
|
||||
"default": "По умолчанию",
|
||||
"delete": "Удалить",
|
||||
@@ -2921,15 +2928,14 @@
|
||||
},
|
||||
"description": "Мощный AI-ассистент для созидания",
|
||||
"downloading": "Загрузка...",
|
||||
"enterprise": {
|
||||
"title": "Предприятие"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Обратная связь",
|
||||
"title": "Обратная связь"
|
||||
},
|
||||
"label": "О программе и обратная связь",
|
||||
"license": {
|
||||
"button": "Лицензия",
|
||||
"title": "Лицензия"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Релизы",
|
||||
"title": "Заметки о релизах"
|
||||
@@ -3037,6 +3043,46 @@
|
||||
"title": "Настройки меню экспорта",
|
||||
"yuque": "Экспорт в Yuque"
|
||||
},
|
||||
"export_to_phone": {
|
||||
"confirm": {
|
||||
"button": "Выберите файл резервной копии"
|
||||
},
|
||||
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
|
||||
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
|
||||
"confirm_close_title": "Подтвердить закрытие",
|
||||
"connected": "Подключено",
|
||||
"connection_failed": "Соединение не удалось",
|
||||
"content": "Убедитесь, что компьютер и телефон подключены к одной сети, чтобы использовать локальную передачу. Откройте приложение Cherry Studio и отсканируйте этот QR-код.",
|
||||
"error": {
|
||||
"init_failed": "Инициализация не удалась",
|
||||
"no_file": "Файл не выбран",
|
||||
"no_ip": "Не удалось получить IP-адрес",
|
||||
"send_failed": "Не удалось отправить файл"
|
||||
},
|
||||
"force_close": "Принудительное закрытие",
|
||||
"generating_qr": "Генерация QR-кода...",
|
||||
"noZipSelected": "Архив не выбран",
|
||||
"scan_qr": "Пожалуйста, отсканируйте QR-код с помощью вашего телефона",
|
||||
"selectZip": "Выберите архив",
|
||||
"sendZip": "Начать восстановление данных",
|
||||
"status": {
|
||||
"completed": "Перевод завершён",
|
||||
"connected": "Подключено",
|
||||
"connecting": "Подключение...",
|
||||
"disconnected": "Отключено",
|
||||
"error": "Ошибка подключения",
|
||||
"initializing": "Инициализация соединения...",
|
||||
"preparing": "Подготовка передачи...",
|
||||
"sending": "Передача {{progress}}%",
|
||||
"waiting_qr_scan": "Пожалуйста, отсканируйте QR-код для подключения"
|
||||
},
|
||||
"title": "Передача по локальной сети",
|
||||
"transfer_progress": "Прогресс передачи"
|
||||
},
|
||||
"title": "Экспорт на телефон"
|
||||
},
|
||||
"hour_interval_one": "{{count}} час",
|
||||
"hour_interval_other": "{{count}} часов",
|
||||
"joplin": {
|
||||
@@ -3783,6 +3829,12 @@
|
||||
"noPromptsAvailable": "Нет доступных подсказок",
|
||||
"requiredField": "Обязательное поле"
|
||||
},
|
||||
"protocolInstallWarning": {
|
||||
"command": "Команда запуска",
|
||||
"message": "Этот MCP был установлен из внешнего источника через протокол. Запуск неизвестных инструментов может повредить ваш компьютер.",
|
||||
"run": "Беги",
|
||||
"title": "Запускать внешний MCP?"
|
||||
},
|
||||
"provider": "Провайдер",
|
||||
"providerPlaceholder": "Имя провайдера",
|
||||
"providerUrl": "URL провайдера",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
@@ -35,8 +34,10 @@ const HomePage: FC = () => {
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
|
||||
const [activeAssistant, _setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
|
||||
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id, state?.topic)
|
||||
const [activeAssistant, _setActiveAssistant] = useState<Assistant>(
|
||||
state?.assistant || _activeAssistant || assistants[0]
|
||||
)
|
||||
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', state?.topic)
|
||||
const [showAssistants] = usePreference('assistant.tab.show')
|
||||
const [showTopics] = usePreference('topic.tab.show')
|
||||
const [topicPosition] = usePreference('topic.position')
|
||||
@@ -47,16 +48,20 @@ const HomePage: FC = () => {
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
const setActiveAssistant = useCallback(
|
||||
// TODO: allow to set it as null.
|
||||
(newAssistant: Assistant) => {
|
||||
if (newAssistant.id === activeAssistant.id) return
|
||||
if (newAssistant.id === activeAssistant?.id) return
|
||||
startTransition(() => {
|
||||
_setActiveAssistant(newAssistant)
|
||||
if (newAssistant.id !== 'fake') {
|
||||
dispatch(setActiveAgentId(null))
|
||||
}
|
||||
// 同步更新 active topic,避免不必要的重新渲染
|
||||
const newTopic = newAssistant.topics[0]
|
||||
_setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic))
|
||||
})
|
||||
},
|
||||
[_setActiveTopic, activeAssistant]
|
||||
[_setActiveTopic, activeAssistant?.id, dispatch]
|
||||
)
|
||||
|
||||
const setActiveTopic = useCallback(
|
||||
@@ -80,19 +85,6 @@ const HomePage: FC = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
|
||||
const newAssistant = assistants.find((a) => a.id === assistantId)
|
||||
if (newAssistant) {
|
||||
setActiveAssistant(newAssistant)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [assistants, setActiveAssistant])
|
||||
|
||||
useEffect(() => {
|
||||
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
|
||||
window.api.window.setMinimumSize(canMinimize ? SECOND_MIN_WINDOW_WIDTH : MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
@@ -102,29 +94,6 @@ const HomePage: FC = () => {
|
||||
}
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTopicOrSession === 'session') {
|
||||
setActiveAssistant({
|
||||
id: 'fake',
|
||||
name: '',
|
||||
prompt: '',
|
||||
topics: [
|
||||
{
|
||||
id: 'fake',
|
||||
assistantId: 'fake',
|
||||
name: 'fake',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
messages: []
|
||||
} as unknown as Topic
|
||||
],
|
||||
type: 'chat'
|
||||
})
|
||||
} else if (activeTopicOrSession === 'topic') {
|
||||
dispatch(setActiveAgentId(null))
|
||||
}
|
||||
}, [activeTopicOrSession, dispatch, setActiveAssistant])
|
||||
|
||||
return (
|
||||
<Container id="home-page">
|
||||
{isLeftNavbar && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import CodeViewer from '@renderer/components/CodeViewer'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutp
|
||||
|
||||
export function GlobTool({ input, output }: { input: GlobToolInputType; output?: GlobToolOutputType }) {
|
||||
// 如果有输出,计算文件数量
|
||||
const fileCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
const lineCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
@@ -17,7 +17,7 @@ export function GlobTool({ input, output }: { input: GlobToolInputType; output?:
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${fileCount} found` : undefined}
|
||||
stats={output ? `${lineCount} of output` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
|
||||
@@ -8,20 +8,31 @@ import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutp
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
|
||||
// 移除 system-reminder 标签及其内容的辅助函数
|
||||
const removeSystemReminderTags = (text: string): string => {
|
||||
// 使用正则表达式匹配 <system-reminder> 标签及其内容,包括换行符
|
||||
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, '')
|
||||
}
|
||||
|
||||
// 将 output 统一转换为字符串
|
||||
const outputString = useMemo(() => {
|
||||
if (!output) return null
|
||||
|
||||
let processedOutput: string
|
||||
|
||||
// 如果是 TextOutput[] 类型,提取所有 text 内容
|
||||
if (Array.isArray(output)) {
|
||||
return output
|
||||
processedOutput = output
|
||||
.filter((item): item is TextOutput => item.type === 'text')
|
||||
.map((item) => item.text)
|
||||
.map((item) => removeSystemReminderTags(item.text))
|
||||
.join('')
|
||||
} else {
|
||||
// 如果是字符串,直接使用
|
||||
processedOutput = output
|
||||
}
|
||||
|
||||
// 如果是字符串,直接返回
|
||||
return output
|
||||
// 移除 system-reminder 标签及其内容
|
||||
return removeSystemReminderTags(processedOutput)
|
||||
}, [output])
|
||||
|
||||
// 如果有输出,计算统计信息
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { PencilRuler } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { SkillToolInput, SkillToolOutput } from './types'
|
||||
|
||||
export function SkillTool({ input, output }: { input: SkillToolInput; output?: SkillToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Skill Tool"
|
||||
title={<ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,7 @@ import { AccordionItem, Card, CardBody, Chip } from '@heroui/react'
|
||||
import { CheckCircle, Circle, Clock, ListTodo } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type {
|
||||
TodoItem,
|
||||
TodoWriteToolInput as TodoWriteToolInputType,
|
||||
TodoWriteToolOutput as TodoWriteToolOutputType
|
||||
} from './types'
|
||||
import type { TodoItem, TodoWriteToolInput as TodoWriteToolInputType } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
const getStatusConfig = (status: TodoItem['status']) => {
|
||||
@@ -34,7 +30,7 @@ const getStatusConfig = (status: TodoItem['status']) => {
|
||||
}
|
||||
}
|
||||
|
||||
export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) {
|
||||
export function TodoWriteTool({ input }: { input: TodoWriteToolInputType }) {
|
||||
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length
|
||||
return (
|
||||
<AccordionItem
|
||||
@@ -72,7 +68,6 @@ export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { MultiEditTool } from './MultiEditTool'
|
||||
import { NotebookEditTool } from './NotebookEditTool'
|
||||
import { ReadTool } from './ReadTool'
|
||||
import { SearchTool } from './SearchTool'
|
||||
import { SkillTool } from './SkillTool'
|
||||
import { TaskTool } from './TaskTool'
|
||||
import { TodoWriteTool } from './TodoWriteTool'
|
||||
import type { ToolInput, ToolOutput } from './types'
|
||||
@@ -25,6 +26,7 @@ import { UnknownToolRenderer } from './UnknownToolRenderer'
|
||||
import { WebFetchTool } from './WebFetchTool'
|
||||
import { WebSearchTool } from './WebSearchTool'
|
||||
import { WriteTool } from './WriteTool'
|
||||
|
||||
const logger = loggerService.withContext('MessageAgentTools')
|
||||
|
||||
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
||||
@@ -43,7 +45,8 @@ export const toolRenderers = {
|
||||
[AgentToolsType.MultiEdit]: MultiEditTool,
|
||||
[AgentToolsType.BashOutput]: BashOutputTool,
|
||||
[AgentToolsType.NotebookEdit]: NotebookEditTool,
|
||||
[AgentToolsType.ExitPlanMode]: ExitPlanModeTool
|
||||
[AgentToolsType.ExitPlanMode]: ExitPlanModeTool,
|
||||
[AgentToolsType.Skill]: SkillTool
|
||||
} as const
|
||||
|
||||
// 类型守卫函数
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AgentToolsType {
|
||||
Skill = 'Skill',
|
||||
Read = 'Read',
|
||||
Task = 'Task',
|
||||
Bash = 'Bash',
|
||||
@@ -22,6 +23,15 @@ export type TextOutput = {
|
||||
}
|
||||
|
||||
// Read 工具的类型定义
|
||||
export interface SkillToolInput {
|
||||
/**
|
||||
* The skill to use
|
||||
*/
|
||||
command: string
|
||||
}
|
||||
|
||||
export type SkillToolOutput = string
|
||||
|
||||
export interface ReadToolInput {
|
||||
/**
|
||||
* The absolute path to the file to read
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NormalToolResponse } from '@renderer/types'
|
||||
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
||||
|
||||
import { MessageAgentTools } from './MessageAgentTools'
|
||||
import { AgentToolsType } from './MessageAgentTools/types'
|
||||
import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch'
|
||||
import { MessageMemorySearchToolTitle } from './MessageMemorySearch'
|
||||
import { MessageWebSearchToolTitle } from './MessageWebSearch'
|
||||
@@ -9,27 +10,12 @@ import { MessageWebSearchToolTitle } from './MessageWebSearch'
|
||||
interface Props {
|
||||
block: ToolMessageBlock
|
||||
}
|
||||
const prefix = 'builtin_'
|
||||
const agentPrefix = 'mcp__'
|
||||
const agentTools = [
|
||||
'Read',
|
||||
'Task',
|
||||
'Bash',
|
||||
'Search',
|
||||
'Glob',
|
||||
'TodoWrite',
|
||||
'WebSearch',
|
||||
'Grep',
|
||||
'Write',
|
||||
'WebFetch',
|
||||
'Edit',
|
||||
'MultiEdit',
|
||||
'BashOutput',
|
||||
'NotebookEdit',
|
||||
'ExitPlanMode'
|
||||
]
|
||||
const isAgentTool = (toolName: string) => {
|
||||
if (agentTools.includes(toolName) || toolName.startsWith(agentPrefix)) {
|
||||
const builtinToolsPrefix = 'builtin_'
|
||||
const agentMcpToolsPrefix = 'mcp__'
|
||||
const agentTools = Object.values(AgentToolsType)
|
||||
|
||||
const isAgentTool = (toolName: AgentToolsType) => {
|
||||
if (agentTools.includes(toolName) || toolName.startsWith(agentMcpToolsPrefix)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -38,8 +24,8 @@ const isAgentTool = (toolName: string) => {
|
||||
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
|
||||
let toolName = toolResponse.tool.name
|
||||
const toolType = toolResponse.tool.type
|
||||
if (toolName.startsWith(prefix)) {
|
||||
toolName = toolName.slice(prefix.length)
|
||||
if (toolName.startsWith(builtinToolsPrefix)) {
|
||||
toolName = toolName.slice(builtinToolsPrefix.length)
|
||||
switch (toolName) {
|
||||
case 'web_search':
|
||||
case 'web_search_preview':
|
||||
@@ -51,7 +37,7 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null =>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else if (isAgentTool(toolName)) {
|
||||
} else if (isAgentTool(toolName as AgentToolsType)) {
|
||||
return <MessageAgentTools toolResponse={toolResponse} />
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { Button, ButtonGroup, Chip, ScrollShadow } from '@heroui/react'
|
||||
import { Button, Chip, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { selectPendingPermissionByToolName, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||
@@ -54,7 +54,6 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
const isSubmittingAllow = request?.status === 'submitting-allow'
|
||||
const isSubmittingDeny = request?.status === 'submitting-deny'
|
||||
const isSubmitting = isSubmittingAllow || isSubmittingDeny
|
||||
const hasSuggestions = (request?.suggestions?.length ?? 0) > 0
|
||||
|
||||
const handleDecision = useCallback(
|
||||
async (
|
||||
@@ -147,37 +146,16 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
{t('agent.toolPermission.button.cancel')}
|
||||
</Button>
|
||||
|
||||
{hasSuggestions ? (
|
||||
<ButtonGroup className="h-8">
|
||||
<Button
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.runWithOptions')}
|
||||
className="h-8 rounded-l-none"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isIconOnly
|
||||
variant="solid"></Button>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.allowRequest')}
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.allowRequest')}
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
aria-label={
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { addIknowAction } from '@renderer/store/runtime'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import type { AssistantTabSortType } from '@shared/data/preference/preferenceTypes'
|
||||
import type { Assistant } from '@types'
|
||||
import type { Assistant, Topic } from '@types'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -38,7 +38,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const { apiServerConfig, apiServerRunning, apiServerLoading } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { iknow, chat } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -101,6 +101,30 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
[setAssistantsTabSortType]
|
||||
)
|
||||
|
||||
const handleAgentPress = useCallback(
|
||||
(agentId: string) => {
|
||||
setActiveAgentId(agentId)
|
||||
// TODO: should allow it to be null
|
||||
setActiveAssistant({
|
||||
id: 'fake',
|
||||
name: '',
|
||||
prompt: '',
|
||||
topics: [
|
||||
{
|
||||
id: 'fake',
|
||||
assistantId: 'fake',
|
||||
name: 'fake',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
messages: []
|
||||
} as unknown as Topic
|
||||
],
|
||||
type: 'chat'
|
||||
})
|
||||
},
|
||||
[setActiveAgentId, setActiveAssistant]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
{!apiServerConfig.enabled && !apiServerRunning && !iknow[ALERT_KEY] && (
|
||||
@@ -115,8 +139,8 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{agentsLoading && <Spinner />}
|
||||
{apiServerConfig.enabled && !apiServerRunning && (
|
||||
{(agentsLoading || apiServerLoading) && <Spinner />}
|
||||
{apiServerConfig.enabled && !apiServerLoading && !apiServerRunning && (
|
||||
<Alert color="danger" title={t('agent.server.error.not_running')} isClosable className="mb-2" />
|
||||
)}
|
||||
{apiServerRunning && agentsError && (
|
||||
@@ -128,7 +152,11 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<UnifiedAddButton onCreateAssistant={onCreateAssistant} />
|
||||
<UnifiedAddButton
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
setActiveAgentId={setActiveAgentId}
|
||||
/>
|
||||
|
||||
{assistantsTabSortType === 'tags' ? (
|
||||
<UnifiedTagGroups
|
||||
@@ -144,7 +172,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
onAssistantSwitch={setActiveAssistant}
|
||||
onAssistantDelete={onDeleteAssistant}
|
||||
onAgentDelete={deleteAgent}
|
||||
onAgentPress={setActiveAgentId}
|
||||
onAgentPress={handleAgentPress}
|
||||
addPreset={addAssistantPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
@@ -164,7 +192,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
onAssistantSwitch={setActiveAssistant}
|
||||
onAssistantDelete={onDeleteAssistant}
|
||||
onAgentDelete={deleteAgent}
|
||||
onAgentPress={setActiveAgentId}
|
||||
onAgentPress={handleAgentPress}
|
||||
addPreset={addAssistantPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Button, DescriptionSwitch, HelpTooltip, RowFlex, Selector, type Selecto
|
||||
import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
|
||||
import EditableNumber from '@renderer/components/EditableNumber'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import {
|
||||
DEFAULT_CONTEXTCOUNT,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
MAX_CONTEXT_COUNT
|
||||
} from '@renderer/config/constant'
|
||||
import { isOpenAIModel } from '@renderer/config/models'
|
||||
import { UNKNOWN } from '@renderer/config/translate'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
@@ -214,9 +219,6 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
setStreamOutput(assistant?.settings?.streamOutput ?? true)
|
||||
}, [assistant])
|
||||
|
||||
const assistantContextCount = assistant?.settings?.contextCount || 20
|
||||
const maxContextCount = assistantContextCount > 20 ? assistantContextCount : 20
|
||||
|
||||
const model = assistant.model || getDefaultModel()
|
||||
|
||||
const isOpenAI = isOpenAIModel(model)
|
||||
@@ -269,21 +271,44 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
) : (
|
||||
<SettingDivider />
|
||||
)}
|
||||
<Row align="middle">
|
||||
<Row align="middle" gutter={10} justify="space-between">
|
||||
<SettingRowTitleSmall>
|
||||
{t('chat.settings.context_count.label')}
|
||||
<HelpTooltip title={t('chat.settings.context_count.tip')} />
|
||||
</SettingRowTitleSmall>
|
||||
<Col span={8}>
|
||||
<EditableNumber
|
||||
min={0}
|
||||
max={20}
|
||||
step={1}
|
||||
value={contextCount}
|
||||
changeOnBlur
|
||||
onChange={(value) => {
|
||||
if (value !== null && value >= 0 && value <= 20) {
|
||||
setContextCount(value)
|
||||
onContextCountChange(value)
|
||||
}
|
||||
}}
|
||||
formatter={(value) => (value === MAX_CONTEXT_COUNT ? t('chat.settings.max') : (value ?? ''))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={23}>
|
||||
<Col span={24}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={maxContextCount}
|
||||
max={20}
|
||||
onChange={setContextCount}
|
||||
onChangeComplete={onContextCountChange}
|
||||
value={typeof contextCount === 'number' ? contextCount : 0}
|
||||
value={Math.min(contextCount, 20)}
|
||||
tooltip={{ open: false }}
|
||||
step={1}
|
||||
marks={{
|
||||
0: '0',
|
||||
10: '10',
|
||||
20: '20'
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -85,7 +85,8 @@ export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<H
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2',
|
||||
!isActive && 'hover:bg-[var(--color-list-item-hover)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -404,7 +404,8 @@ const Container = ({
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2',
|
||||
!isActive && 'hover:bg-[var(--color-list-item-hover)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}>
|
||||
|
||||
@@ -1,60 +1,69 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from '@heroui/react'
|
||||
import { useDisclosure } from '@heroui/react'
|
||||
import AddAssistantOrAgentPopup from '@renderer/components/Popups/AddAssistantOrAgentPopup'
|
||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { AgentEntity, Assistant, Topic } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
|
||||
interface UnifiedAddButtonProps {
|
||||
onCreateAssistant: () => void
|
||||
setActiveAssistant: (a: Assistant) => void
|
||||
setActiveAgentId: (id: string) => void
|
||||
}
|
||||
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant }) => {
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||
const { isOpen: isAgentModalOpen, onOpen: onAgentModalOpen, onClose: onAgentModalClose } = useDisclosure()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleAddAssistant = () => {
|
||||
setIsPopoverOpen(false)
|
||||
onCreateAssistant()
|
||||
const handleAddButtonClick = () => {
|
||||
AddAssistantOrAgentPopup.show({
|
||||
onSelect: (type) => {
|
||||
if (type === 'assistant') {
|
||||
onCreateAssistant()
|
||||
} else if (type === 'agent') {
|
||||
onAgentModalOpen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddAgent = () => {
|
||||
setIsPopoverOpen(false)
|
||||
onAgentModalOpen()
|
||||
}
|
||||
const afterCreate = useCallback(
|
||||
(a: AgentEntity) => {
|
||||
// TODO: should allow it to be null
|
||||
setActiveAssistant({
|
||||
id: 'fake',
|
||||
name: '',
|
||||
prompt: '',
|
||||
topics: [
|
||||
{
|
||||
id: 'fake',
|
||||
assistantId: 'fake',
|
||||
name: 'fake',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
messages: []
|
||||
} as unknown as Topic
|
||||
],
|
||||
type: 'chat'
|
||||
})
|
||||
setActiveAgentId(a.id)
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
},
|
||||
[dispatch, setActiveAgentId, setActiveAssistant]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mb-1">
|
||||
<Popover
|
||||
isOpen={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
placement="bottom"
|
||||
classNames={{ content: 'p-0 min-w-[200px]' }}>
|
||||
<PopoverTrigger>
|
||||
<AddButton>{t('chat.add.assistant.title')}</AddButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="flex w-full flex-col gap-1 p-1">
|
||||
<Button
|
||||
onClick={handleAddAssistant}
|
||||
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
|
||||
startContent={<MessageSquare size={16} className="shrink-0" />}>
|
||||
{t('chat.add.assistant.title')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddAgent}
|
||||
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
|
||||
startContent={<Bot size={16} className="shrink-0" />}>
|
||||
{t('agent.add.title')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} />
|
||||
<AddButton onClick={handleAddButtonClick} className="-mt-[1px] mb-[2px]">
|
||||
{t('chat.add.assistant.title')}
|
||||
</AddButton>
|
||||
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} afterSubmit={afterCreate} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||
import type { AgentEntity, Assistant } from '@renderer/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
@@ -21,6 +23,13 @@ export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Selector to get tagsOrder from Redux store
|
||||
const selectTagsOrder = useMemo(
|
||||
() => createSelector([(state: RootState) => state.assistants], (assistants) => assistants.tagsOrder ?? []),
|
||||
[]
|
||||
)
|
||||
const savedTagsOrder = useAppSelector(selectTagsOrder)
|
||||
|
||||
// Group unified items by tags
|
||||
const groupedUnifiedItems = useMemo(() => {
|
||||
const groups = new Map<string, UnifiedItem[]>()
|
||||
@@ -45,16 +54,30 @@ export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Sort groups: untagged first, then tagged groups
|
||||
// Sort groups: untagged first, then by savedTagsOrder
|
||||
const untaggedKey = t('assistants.tags.untagged')
|
||||
const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => {
|
||||
if (tagA === untaggedKey) return -1
|
||||
if (tagB === untaggedKey) return 1
|
||||
|
||||
if (savedTagsOrder.length > 0) {
|
||||
const indexA = savedTagsOrder.indexOf(tagA)
|
||||
const indexB = savedTagsOrder.indexOf(tagB)
|
||||
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
return indexA - indexB
|
||||
}
|
||||
|
||||
if (indexA !== -1) return -1
|
||||
|
||||
if (indexB !== -1) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return sortedGroups.map(([tag, items]) => ({ tag, items }))
|
||||
}, [unifiedItems, t])
|
||||
}, [unifiedItems, t, savedTagsOrder])
|
||||
|
||||
const handleUnifiedGroupReorder = useCallback(
|
||||
(tag: string, newGroupList: UnifiedItem[]) => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Tab } from '@renderer/types/chat'
|
||||
import { classNames, getErrorMessage, uuid } from '@renderer/utils'
|
||||
@@ -52,6 +54,7 @@ const HomeTabs: FC<Props> = ({
|
||||
const { activeTopicOrSession, activeAgentId } = chat
|
||||
const { session, isLoading: isSessionLoading, error: sessionError } = useActiveSession()
|
||||
const { updateSession } = useUpdateSession(activeAgentId)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isSessionView = activeTopicOrSession === 'session'
|
||||
const isTopicView = activeTopicOrSession === 'topic'
|
||||
@@ -71,13 +74,19 @@ const HomeTabs: FC<Props> = ({
|
||||
|
||||
const onCreateAssistant = async () => {
|
||||
const assistant = await AddAssistantPopup.show()
|
||||
assistant && setActiveAssistant(assistant)
|
||||
if (assistant) {
|
||||
setActiveAssistant(assistant)
|
||||
dispatch(setActiveAgentId(null))
|
||||
dispatch(setActiveTopicOrSessionAction('topic'))
|
||||
}
|
||||
}
|
||||
|
||||
const onCreateDefaultAssistant = () => {
|
||||
const assistant = { ...defaultAssistant, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
setActiveAssistant(assistant)
|
||||
dispatch(setActiveAgentId(null))
|
||||
dispatch(setActiveTopicOrSessionAction('topic'))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -63,7 +63,12 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
|
||||
)}
|
||||
{activeSession && (
|
||||
<BreadcrumbItem>
|
||||
<SelectAgentBaseModelButton agentBase={activeSession} onSelect={handleUpdateModel} />
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={activeSession}
|
||||
onSelect={async (model) => {
|
||||
await handleUpdateModel(model)
|
||||
}}
|
||||
/>
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{activeAgent && activeSession && (
|
||||
|
||||
@@ -717,10 +717,17 @@ const NotesPage: FC = () => {
|
||||
const normalizedActivePath = activeFilePath ? normalizePathValue(activeFilePath) : undefined
|
||||
if (normalizedActivePath) {
|
||||
if (normalizedActivePath === sourceNode.externalPath) {
|
||||
// Cancel debounced save to prevent saving to old path
|
||||
debouncedSaveRef.current?.cancel()
|
||||
lastFilePathRef.current = destinationPath
|
||||
dispatch(setActiveFilePath(destinationPath))
|
||||
} else if (sourceNode.type === 'folder' && normalizedActivePath.startsWith(`${sourceNode.externalPath}/`)) {
|
||||
const suffix = normalizedActivePath.slice(sourceNode.externalPath.length)
|
||||
dispatch(setActiveFilePath(`${destinationPath}${suffix}`))
|
||||
const newActivePath = `${destinationPath}${suffix}`
|
||||
// Cancel debounced save to prevent saving to old path
|
||||
debouncedSaveRef.current?.cancel()
|
||||
lastFilePathRef.current = newActivePath
|
||||
dispatch(setActiveFilePath(newActivePath))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Progress, Row, Tag } from 'antd'
|
||||
import type { UpdateInfo } from 'builder-util-runtime'
|
||||
import { debounce } from 'lodash'
|
||||
import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { BadgeQuestionMark } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -90,14 +90,8 @@ const AboutSettings: FC = () => {
|
||||
await window.api.devTools.toggle()
|
||||
}
|
||||
|
||||
const showLicense = async () => {
|
||||
const { appPath } = await window.api.getAppInfo()
|
||||
openSmartMinapp({
|
||||
id: 'cherrystudio-license',
|
||||
name: t('settings.about.license.title'),
|
||||
url: `file://${appPath}/resources/cherry-studio/license.html`,
|
||||
logo: AppLogo
|
||||
})
|
||||
const showEnterprise = async () => {
|
||||
onOpenWebsite('https://cherry-ai.com/enterprise')
|
||||
}
|
||||
|
||||
const showReleases = async () => {
|
||||
@@ -315,7 +309,7 @@ const AboutSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<GithubOutlined size={18} />
|
||||
<Github size={18} />
|
||||
{t('settings.about.feedback.title')}
|
||||
</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio/issues/new/choose')}>
|
||||
@@ -325,10 +319,10 @@ const AboutSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<FileCheck size={18} />
|
||||
{t('settings.about.license.title')}
|
||||
<Building2 size={18} />
|
||||
{t('settings.about.enterprise.title')}
|
||||
</SettingRowTitle>
|
||||
<Button onClick={showLicense}>{t('settings.about.license.button')}</Button>
|
||||
<Button onClick={showEnterprise}>{t('settings.about.website.button')}</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Button, Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm } from '@renderer/types'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { Plus } from 'lucide-react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
export interface AccessibleDirsSettingProps {
|
||||
base: AgentBaseWithId | undefined | null
|
||||
update: (form: UpdateAgentBaseForm) => Promise<void>
|
||||
update: UpdateAgentFunctionUnion
|
||||
}
|
||||
|
||||
const logger = loggerService.withContext('AccessibleDirsSetting')
|
||||
|
||||
export const AccessibleDirsSetting: React.FC<AccessibleDirsSettingProps> = ({ base, update }) => {
|
||||
export const AccessibleDirsSetting = ({ base, update }: AccessibleDirsSettingProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const updateAccessiblePaths = useCallback(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EmojiAvatarWithPicker } from '@renderer/components/Avatar/EmojiAvatarWithPicker'
|
||||
import type { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||
import type { AgentEntity, UpdateAgentForm, UpdateAgentFunction } from '@renderer/types'
|
||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -8,7 +8,7 @@ import { SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
export interface AvatarSettingsProps {
|
||||
agent: AgentEntity
|
||||
update: (form: UpdateAgentForm) => Promise<void>
|
||||
update: UpdateAgentFunction
|
||||
}
|
||||
|
||||
// const logger = loggerService.withContext('AvatarSetting')
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Textarea } from '@heroui/react'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm } from '@renderer/types'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
export interface DescriptionSettingProps {
|
||||
base: AgentBaseWithId | undefined | null
|
||||
update: (form: UpdateAgentBaseForm) => Promise<void>
|
||||
update: UpdateAgentFunctionUnion
|
||||
}
|
||||
|
||||
export const DescriptionSetting: React.FC<DescriptionSettingProps> = ({ base, update }) => {
|
||||
export const DescriptionSetting = ({ base, update }: DescriptionSettingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [description, setDescription] = useState<string | undefined>(base?.description?.trim())
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update, show
|
||||
</div>
|
||||
</SettingsItem>
|
||||
)}
|
||||
{isAgent && <AvatarSetting agent={agentBase} update={update} />}
|
||||
{isAgent && (
|
||||
<AvatarSetting agent={agentBase} update={update as ReturnType<typeof useUpdateAgent>['updateAgent']} />
|
||||
)}
|
||||
<NameSetting base={agentBase} update={update} />
|
||||
{showModelSetting && <ModelSetting base={agentBase} update={update} />}
|
||||
<AccessibleDirsSetting base={agentBase} update={update} />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
|
||||
import type { AgentBaseWithId, ApiModel, UpdateAgentBaseForm } from '@renderer/types'
|
||||
import type { AgentBaseWithId, ApiModel, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
export interface ModelSettingProps {
|
||||
base: AgentBaseWithId | undefined | null
|
||||
update: (form: UpdateAgentBaseForm) => Promise<void>
|
||||
update: UpdateAgentFunctionUnion
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export const ModelSetting: React.FC<ModelSettingProps> = ({ base, update, isDisabled }) => {
|
||||
export const ModelSetting = ({ base, update, isDisabled }: ModelSettingProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const updateModel = async (model: ApiModel) => {
|
||||
@@ -23,7 +23,13 @@ export const ModelSetting: React.FC<ModelSettingProps> = ({ base, update, isDisa
|
||||
return (
|
||||
<SettingsItem inline>
|
||||
<SettingsTitle id="model">{t('common.model')}</SettingsTitle>
|
||||
<SelectAgentBaseModelButton agentBase={base} onSelect={updateModel} isDisabled={isDisabled} />
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={base}
|
||||
onSelect={async (model) => {
|
||||
await updateModel(model)
|
||||
}}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</SettingsItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Input } from '@heroui/react'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm } from '@renderer/types'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -7,10 +7,10 @@ import { SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
export interface NameSettingsProps {
|
||||
base: AgentBaseWithId | undefined | null
|
||||
update: (form: UpdateAgentBaseForm) => Promise<void>
|
||||
update: UpdateAgentFunctionUnion
|
||||
}
|
||||
|
||||
export const NameSetting: React.FC<NameSettingsProps> = ({ base, update }) => {
|
||||
export const NameSetting = ({ base, update }: NameSettingsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [name, setName] = useState<string | undefined>(base?.name?.trim())
|
||||
const updateName = async (name: UpdateAgentBaseForm['name']) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, CardBody, Tab, Tabs } from '@heroui/react'
|
||||
import { useAvailablePlugins, useInstalledPlugins, usePluginActions } from '@renderer/hooks/usePlugins'
|
||||
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentBaseForm } from '@renderer/types/agent'
|
||||
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types/agent'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -11,7 +11,7 @@ import { SettingsContainer } from './shared'
|
||||
|
||||
interface PluginSettingsProps {
|
||||
agentBase: GetAgentResponse | GetAgentSessionResponse
|
||||
update: (partial: UpdateAgentBaseForm) => Promise<void>
|
||||
update: UpdateAgentFunctionUnion
|
||||
}
|
||||
|
||||
const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
@@ -55,7 +55,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsContainer className="pr-0">
|
||||
<Tabs
|
||||
aria-label="Plugin settings tabs"
|
||||
classNames={{
|
||||
@@ -64,7 +64,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
panel: 'w-full flex-1 overflow-hidden'
|
||||
}}>
|
||||
<Tab key="available" title={t('agent.settings.plugins.available.title')}>
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4">
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-1 pr-2">
|
||||
{errorAvailable ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
@@ -89,7 +89,7 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
</Tab>
|
||||
|
||||
<Tab key="installed" title={t('agent.settings.plugins.installed.title')}>
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4">
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
||||
{errorInstalled ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
PermissionMode,
|
||||
Tool,
|
||||
UpdateAgentBaseForm,
|
||||
UpdateAgentForm,
|
||||
UpdateSessionForm
|
||||
UpdateAgentFunction,
|
||||
UpdateAgentSessionFunction
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema } from '@renderer/types'
|
||||
import { Modal } from 'antd'
|
||||
@@ -24,11 +24,11 @@ import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||
type AgentToolingSettingsProps =
|
||||
| {
|
||||
agentBase: GetAgentResponse | undefined | null
|
||||
update: (form: UpdateAgentForm) => Promise<void> | void
|
||||
update: UpdateAgentFunction
|
||||
}
|
||||
| {
|
||||
agentBase: GetAgentSessionResponse | undefined | null
|
||||
update: (form: UpdateSessionForm) => Promise<void> | void
|
||||
update: UpdateAgentSessionFunction
|
||||
}
|
||||
|
||||
type AgentConfigurationState = AgentConfiguration & Record<string, unknown>
|
||||
@@ -169,6 +169,7 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
centered: true,
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: applyChange,
|
||||
@@ -275,9 +276,10 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
key={card.mode}
|
||||
isPressable={!disabled}
|
||||
isDisabled={disabled || isUpdatingMode}
|
||||
shadow="none"
|
||||
onPress={() => handleSelectPermissionMode(card.mode)}
|
||||
className={`border ${
|
||||
isSelected ? 'border-primary shadow-lg' : 'border-default-200'
|
||||
isSelected ? 'border-primary' : 'border-default-200'
|
||||
} ${disabled ? 'opacity-60' : ''}`}>
|
||||
<CardHeader className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Chip } from '@heroui/react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface CategoryFilterProps {
|
||||
categories: string[]
|
||||
selectedCategories: string[]
|
||||
onChange: (categories: string[]) => void
|
||||
}
|
||||
|
||||
export const CategoryFilter: FC<CategoryFilterProps> = ({ categories, selectedCategories, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isAllSelected = selectedCategories.length === 0
|
||||
|
||||
const handleCategoryClick = (category: string) => {
|
||||
if (selectedCategories.includes(category)) {
|
||||
onChange(selectedCategories.filter((c) => c !== category))
|
||||
} else {
|
||||
onChange([...selectedCategories, category])
|
||||
}
|
||||
}
|
||||
|
||||
const handleAllClick = () => {
|
||||
onChange([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex max-h-24 flex-wrap gap-2 overflow-y-auto">
|
||||
<Chip
|
||||
variant={isAllSelected ? 'solid' : 'bordered'}
|
||||
color={isAllSelected ? 'primary' : 'default'}
|
||||
onClick={handleAllClick}
|
||||
className="cursor-pointer">
|
||||
{t('plugins.all_categories')}
|
||||
</Chip>
|
||||
|
||||
{categories.map((category) => {
|
||||
const isSelected = selectedCategories.includes(category)
|
||||
return (
|
||||
<Chip
|
||||
key={category}
|
||||
variant={isSelected ? 'solid' : 'bordered'}
|
||||
color={isSelected ? 'primary' : 'default'}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
className="cursor-pointer">
|
||||
{category}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
||||
<TableColumn>{t('plugins.name')}</TableColumn>
|
||||
<TableColumn>{t('plugins.type')}</TableColumn>
|
||||
<TableColumn>{t('plugins.category')}</TableColumn>
|
||||
<TableColumn width={100}>{t('plugins.actions')}</TableColumn>
|
||||
<TableColumn align="end">{t('plugins.actions')}</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plugins.map((plugin) => (
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Input, Pagination, Tab, Tabs } from '@heroui/react'
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Tab, Tabs } from '@heroui/react'
|
||||
import type { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Search } from 'lucide-react'
|
||||
import { Filter, Search } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { CategoryFilter } from './CategoryFilter'
|
||||
import { PluginCard } from './PluginCard'
|
||||
import { PluginDetailModal } from './PluginDetailModal'
|
||||
|
||||
@@ -38,10 +37,11 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||
const [activeType, setActiveType] = useState<PluginType>('all')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [displayCount, setDisplayCount] = useState(ITEMS_PER_PAGE)
|
||||
const [actioningPlugin, setActioningPlugin] = useState<string | null>(null)
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginMetadata | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const observerTarget = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Combine all plugins based on active type
|
||||
const allPlugins = useMemo(() => {
|
||||
@@ -87,14 +87,35 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
})
|
||||
}, [allPlugins, searchQuery, selectedCategories])
|
||||
|
||||
// Paginate filtered plugins
|
||||
const paginatedPlugins = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE
|
||||
return filteredPlugins.slice(startIndex, endIndex)
|
||||
}, [filteredPlugins, currentPage])
|
||||
// Display plugins based on displayCount
|
||||
const displayedPlugins = useMemo(() => {
|
||||
return filteredPlugins.slice(0, displayCount)
|
||||
}, [filteredPlugins, displayCount])
|
||||
|
||||
const totalPages = Math.ceil(filteredPlugins.length / ITEMS_PER_PAGE)
|
||||
const hasMore = displayCount < filteredPlugins.length
|
||||
|
||||
// Reset display count when filters change
|
||||
useEffect(() => {
|
||||
setDisplayCount(ITEMS_PER_PAGE)
|
||||
}, [filteredPlugins])
|
||||
|
||||
// Infinite scroll observer
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore) {
|
||||
setDisplayCount((prev) => prev + ITEMS_PER_PAGE)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
if (observerTarget.current) {
|
||||
observer.observe(observerTarget.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [hasMore])
|
||||
|
||||
// Check if a plugin is installed
|
||||
const isPluginInstalled = (plugin: PluginMetadata): boolean => {
|
||||
@@ -117,20 +138,22 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
setActioningPlugin(null)
|
||||
}
|
||||
|
||||
// Reset to first page when filters change
|
||||
// Reset display count when filters change
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchQuery(value)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (categories: string[]) => {
|
||||
setSelectedCategories(categories)
|
||||
setCurrentPage(1)
|
||||
const handleCategoryChange = (keys: Set<string>) => {
|
||||
// Reset if "all" selected, otherwise filter categories
|
||||
if (keys.has('all') || keys.size === 0) {
|
||||
setSelectedCategories([])
|
||||
} else {
|
||||
setSelectedCategories(Array.from(keys).filter((key) => key !== 'all'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeChange = (type: string | number) => {
|
||||
setActiveType(type as PluginType)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handlePluginClick = (plugin: PluginMetadata) => {
|
||||
@@ -145,33 +168,76 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search Input */}
|
||||
<Input
|
||||
placeholder={t('plugins.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onValueChange={handleSearchChange}
|
||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
||||
isClearable
|
||||
classNames={{
|
||||
input: 'text-small',
|
||||
inputWrapper: 'h-10'
|
||||
}}
|
||||
/>
|
||||
{/* Search and Filter */}
|
||||
<div className="relative flex gap-0">
|
||||
<Input
|
||||
placeholder={t('plugins.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onValueChange={handleSearchChange}
|
||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
||||
isClearable
|
||||
size="md"
|
||||
className="flex-1"
|
||||
classNames={{
|
||||
inputWrapper: 'pr-12'
|
||||
}}
|
||||
/>
|
||||
<Dropdown placement="bottom-end" classNames={{ content: 'max-h-60 overflow-y-auto p-0' }}>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant={selectedCategories.length > 0 ? 'flat' : 'light'}
|
||||
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
||||
size="sm"
|
||||
className="-translate-y-1/2 absolute top-1/2 right-2 z-10">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="Category filter"
|
||||
closeOnSelect={false}
|
||||
className="max-h-60 overflow-y-auto"
|
||||
items={[
|
||||
{ key: 'all', label: t('plugins.all_categories') },
|
||||
...allCategories.map((category) => ({ key: category, label: category }))
|
||||
]}>
|
||||
{(item) => {
|
||||
const isSelected =
|
||||
item.key === 'all' ? selectedCategories.length === 0 : selectedCategories.includes(item.key)
|
||||
|
||||
{/* Category Filter */}
|
||||
<CategoryFilter
|
||||
categories={allCategories}
|
||||
selectedCategories={selectedCategories}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
return (
|
||||
<DropdownItem
|
||||
key={item.key}
|
||||
textValue={item.label}
|
||||
onPress={() => {
|
||||
if (item.key === 'all') {
|
||||
handleCategoryChange(new Set(['all']))
|
||||
} else {
|
||||
const newKeys = selectedCategories.includes(item.key)
|
||||
? new Set(selectedCategories.filter((c) => c !== item.key))
|
||||
: new Set([...selectedCategories, item.key])
|
||||
handleCategoryChange(newKeys)
|
||||
}
|
||||
}}
|
||||
className={isSelected ? 'bg-primary-50' : ''}>
|
||||
{item.label}
|
||||
{isSelected && <span className="ml-2 text-primary text-sm">✓</span>}
|
||||
</DropdownItem>
|
||||
)
|
||||
}}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* Type Tabs */}
|
||||
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
|
||||
<Tab key="all" title={t('plugins.all_types')} />
|
||||
<Tab key="agent" title={t('plugins.agents')} />
|
||||
<Tab key="command" title={t('plugins.commands')} />
|
||||
<Tab key="skill" title={t('plugins.skills')} />
|
||||
</Tabs>
|
||||
<div className="-mt-3 flex justify-center">
|
||||
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
|
||||
<Tab key="all" title={t('plugins.all_types')} />
|
||||
<Tab key="agent" title={t('plugins.agents')} />
|
||||
<Tab key="command" title={t('plugins.commands')} />
|
||||
<Tab key="skill" title={t('plugins.skills')} />
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Result Count */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -179,37 +245,35 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Plugin Grid */}
|
||||
{paginatedPlugins.length === 0 ? (
|
||||
{displayedPlugins.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-default-400">{t('plugins.no_results')}</p>
|
||||
<p className="text-default-300 text-small">{t('plugins.try_different_search')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{paginatedPlugins.map((plugin) => {
|
||||
const installed = isPluginInstalled(plugin)
|
||||
const isActioning = actioningPlugin === plugin.sourcePath
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{displayedPlugins.map((plugin) => {
|
||||
const installed = isPluginInstalled(plugin)
|
||||
const isActioning = actioningPlugin === plugin.sourcePath
|
||||
|
||||
return (
|
||||
<PluginCard
|
||||
key={`${plugin.type}-${plugin.sourcePath}`}
|
||||
plugin={plugin}
|
||||
installed={installed}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
loading={loading || isActioning}
|
||||
onClick={() => handlePluginClick(plugin)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination total={totalPages} page={currentPage} onChange={setCurrentPage} showControls />
|
||||
</div>
|
||||
return (
|
||||
<div key={`${plugin.type}-${plugin.sourcePath}`} className="h-full">
|
||||
<PluginCard
|
||||
plugin={plugin}
|
||||
installed={installed}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
loading={loading || isActioning}
|
||||
onClick={() => handlePluginClick(plugin)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* Infinite scroll trigger */}
|
||||
{hasMore && <div ref={observerTarget} className="h-10" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Plugin Detail Modal */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Card, CardBody, CardFooter, CardHeader, Chip, Spinner } from '@heroui/react'
|
||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { Download, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -17,15 +18,20 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Card className="w-full cursor-pointer transition-shadow hover:shadow-md" isPressable onPress={onClick}>
|
||||
<Card
|
||||
className="flex h-full w-full cursor-pointer flex-col border-[0.5px] border-default-200"
|
||||
isPressable
|
||||
shadow="none"
|
||||
onPress={onClick}>
|
||||
<CardHeader className="flex flex-col items-start gap-2 pb-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="font-semibold text-medium">{plugin.name}</h3>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<h3 className="truncate font-medium text-small">{plugin.name}</h3>
|
||||
<Chip
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color={plugin.type === 'agent' ? 'primary' : plugin.type === 'skill' ? 'success' : 'secondary'}>
|
||||
{plugin.type}
|
||||
color={plugin.type === 'agent' ? 'primary' : plugin.type === 'skill' ? 'success' : 'secondary'}
|
||||
className="h-4 min-w-0 flex-shrink-0 px-0.5 text-xs">
|
||||
{upperFirst(plugin.type)}
|
||||
</Chip>
|
||||
</div>
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
@@ -33,7 +39,7 @@ export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall,
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="py-2">
|
||||
<CardBody className="flex-1 py-2">
|
||||
<p className="line-clamp-3 text-default-500 text-small">{plugin.description || t('plugins.no_description')}</p>
|
||||
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user