Compare commits

...

30 Commits

Author SHA1 Message Date
lizhixuan c518c9090b docs: add example implementation of Redux Slice for message management
- Introduced a new section in the technical documentation detailing the implementation of a `messages` slice using `createEntityAdapter`.
- Provided TypeScript code for the slice, including actions for adding, updating, and removing messages.
- Summarized the core principles of the slice's design, emphasizing single responsibility, logical separation of concerns, and performance optimization.
- Included a migration strategy for transitioning from the previous state structure to the new message pool approach.
2025-06-12 18:34:36 +08:00
suyao 91045ecc2b docs: finalize technical report for message history version management system with multi-model support
- Updated the design document to reflect the final version, incorporating multi-model support and enhanced version management features.
- Expanded the data structure section to include new entities and relationships, such as `askId`, `parentMessageId`, and `siblingIds`.
- Improved the core operation processes, including sending new messages and managing message versions.
- Added detailed diagrams and performance analysis to illustrate the new architecture and its advantages over the previous system.
- Ensured backward compatibility while introducing new functionalities for branching conversations and version control.
2025-06-12 15:59:54 +08:00
suyao 748ca008b4 docs: add technical report for message history version management system
- Introduced a comprehensive design document outlining the architecture and requirements for a message history version management system.
- Added new entities `UserMessage` and `AssistantMessageGroup` to support a directed multi-branch conversation structure and version management.
- Updated existing entities to accommodate the new architecture while maintaining backward compatibility.
- Included performance analysis and migration strategies for transitioning to the new system.
2025-06-12 14:25:42 +08:00
kangfenmao 6ad9044cd1 refactor: replace 302ai PNG with WEBP format and update provider configurations
- Deleted the old PNG logo for 302ai and added a new WEBP version.
- Updated the provider configuration to use the new WEBP logo.
- Added translations for the new Cephalon provider in Japanese and Russian.
- Disabled the 302ai and Cephalon providers in the initial state of the store.
- Adjusted migration logic to accommodate the new provider setup.
2025-06-12 12:16:43 +08:00
JI4JUN 9e9a1ec024 feat: support 302ai provider (#7044)
* feat(porvider): add provider 302ai

* style(provider): change provider name 302AI to 302.AI

* style(provider): system models replacement of 302.AI provider

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-06-12 12:04:21 +08:00
HzTTT a214dca6fa feat:add cephalon provider (#7050)
* feat: add Cephalon provider and related assets

* add Cephalon logo image
* update models to include Cephalon's DeepSeek-R1
* add Cephalon provider configuration and API details
* include Cephalon translations in multiple languages
* update store to initialize Cephalon as a provider
* increment version for migration

* feat: update Cephalon provider configuration and assets

* add Cephalon logo image
* enable Cephalon provider in the store
* remove previous disabled configuration for Cephalon

* fix: update Cephalon provider model URL

* fix: update official website URL for Cephalon provider
2025-06-12 12:02:03 +08:00
one b142e5647e fix(Markdown): inline math overflow (#7095) 2025-06-12 11:05:52 +08:00
kangfenmao a33a8da5c1 chore(version): 1.4.2 2025-06-12 09:36:27 +08:00
kangfenmao e029159067 Revert "fix: qwen3 cannot name a topic (#6722)"
This reverts commit 389f750d7b.
2025-06-11 20:31:24 +08:00
fullex 8582ad2529 fix(SelectionAssistant): shortcut in mac and running handling (#7084)
fix(SelectionService): enhance selection and clipboard handling

- Updated processSelectTextByShortcut to include a check for the 'started' state before processing.
- Modified writeToClipboard to ensure it only attempts to write if the selectionHook is available and 'started'.
- Adjusted ShortcutSettings to filter out additional shortcuts when not on Windows, improving platform compatibility.
2025-06-11 18:30:48 +08:00
fullex e7f1127aee feat(SelectionAssistant): add shortcut for selecting text (#7073)
* feat(SelectionAssistant): add shortcut for selecting text and update trigger modes

- Introduced a new trigger mode 'Shortcut' in SelectionService to handle text selection via shortcuts.
- Implemented processSelectTextByShortcut method to process selected text when the shortcut is activated.
- Updated ShortcutService to register the new selection_assistant_select_text shortcut.
- Enhanced localization for the new shortcut and updated descriptions for trigger modes in multiple languages.
- Adjusted SelectionAssistantSettings to include tooltip information for the new shortcut option.

* fix: should destroy window when disable
2025-06-11 17:39:12 +08:00
Guscccc 7e54c465b1 feat: add plain text copy functionality for messages and topics. 添加了复制纯文本的功能(去除Markdown格式符号) (#5965)
* feat: add plain text copy functionality for messages and topics.

* refactor: move minapp settings to minapp page

* fix: add success message after copying topic and message as text

* fix: refactor test imports and add mocks for translation and window.message

---------

Co-authored-by: Guscccc <Augustus.Li@outlook.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-06-11 17:23:35 +08:00
自由的世界人 5c76d398c5 fix: readme twitter link error (#7075) 2025-06-11 15:30:25 +08:00
fullex f6a935f14f feat(SelectionAssistant): shortcut key to toggle on/off (#6983)
* feat: add toggle selection assistant functionality and corresponding shortcuts

- Implemented toggleEnabled method in SelectionService to manage the selection assistant state.
- Registered new shortcut for toggling the selection assistant in ShortcutService.
- Updated StoreSyncService to sync the selection assistant state across renderer windows.
- Added localization for the toggle selection assistant feature in multiple languages.
- Adjusted ShortcutSettings to conditionally display the toggle selection assistant shortcut based on the platform.
- Included toggle selection assistant in the initial state of shortcuts in the store.

* fix: shortcut key

* fix: accelerator name
2025-06-11 13:32:49 +08:00
fullex 26d018b1b7 fix(SelectionAssistant): improve auto-scroll behavior in action window (#6999)
fix(SelectionActionApp): improve auto-scroll behavior and manage scroll height tracking
2025-06-11 13:03:52 +08:00
Wang Jiyuan cd8c5115df Feat: Allows setting the vector dimension of the knowledge base embedding model (#7025) 2025-06-11 11:52:15 +08:00
beyondkmp 0020e9f3c9 feat(i18n): add tooltips for model name in multiple languages (#7064)
Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-11 11:44:04 +08:00
fullex 8df4cd7e76 fix(SelectionAssistant): reduce Copy conflict (#7060)
fix: reduce Copy conflict
2025-06-10 23:56:38 +08:00
Wang Jiyuan ee7e6c0f87 fix: bubble overflow patch (#7055)
* fix: bubble overflow

* fix: bubble content doesn't fill context width
2025-06-10 21:42:18 +08:00
Wang Jiyuan e65091f83c feat: add citation index to show (#7052) 2025-06-10 19:42:30 +08:00
Wang Jiyuan 3ee8186f96 Fix: bubble-style unnecessary menu background (Plan D) (#7026)
* fix: bubble-style  unnecessary menu background

* fix: show divider in message only in plain mode

* fix: bubble user message style in dark mode

* fix: action button hover style

* refactor: The rendering position of the message menbar is determined by the settings

* fix: bubble style assistant message token usage left align

* fix: bubble style

* fix: bubble style

* fix: text color and bubble edit

* fix: bubble editing

* fix: bubble editing

* fix: bubble editor

* fix: editor width

* fix: remove redundant tokens usage

* fix: not unified token font size and color

* fix: unexpected display behavior in plain mode

* fix: info style

* fix: bubble style

* fix: Style fixes for better compatibility

* fix: bubble style

* fix: Move the menu of the last message to the outside

* fix: bubble style

* fix: why this happened?

* feat: add description for messages divider in settings

* fix: 谁想出来的上下margin不一样还是神秘数字

* fix: new context style
2025-06-10 18:13:11 +08:00
FischLu 49f1b62848 翻译功能增加手动选择源语言的选项 (#6916)
* feat(TranslatePage): add user-selectable source language with auto-detection

* fix: update detected language label for consistency across translations

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>
2025-06-10 16:25:22 +08:00
Wang Jiyuan 90a84bb55a fix: shouldn't edit embedding dimension on existing knowledge base (#7022)
* fix: shouldn't edit embedding dimension on existing knowledge base

* remove dim settings
2025-06-10 15:34:27 +08:00
neko engineer d2147aed3b fix: fix waring in usetags (#7039)
fix: 修复usetags中的警告

Co-authored-by: linshuhao <nmnm1996>
2025-06-10 15:07:29 +08:00
fullex 4f28086a64 feat(SelectionAssistant): support thinking block in action window (#6998)
feat(ActionUtils): enhance message processing to include thinking block handling
2025-06-09 20:08:17 +08:00
one d9c20c8815 refactor: use CodeEditor for customizing css (#6877)
* refactor: use CodeEditor for customizing css

* fix: editor height
2025-06-09 19:56:57 +08:00
beyondkmp b951d89c6a feat: enhance unresponsive renderer handling and crash reporting (#6995)
* feat: enhance unresponsive renderer handling and crash reporting

* Added support for collecting JavaScript call stacks from unresponsive renderers.
* Updated the Document Policy in the HTML to include JS call stacks in crash reports.
* Removed legacy unresponsive logging from WindowService.

* feat: improve unresponsive renderer handling and update crash reporting

* Added session web request handling to include Document-Policy for JS call stacks in crash reports.
* Removed legacy Document-Policy meta tag from HTML.
* Enhanced logging for unresponsive renderer call stacks.

* fix: remove unused session import in index.ts

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-09 19:50:05 +08:00
Suzu ac7d4cb4fa fix: check if embedding is base64 encoded before convert it to float … (#7014)
fix: check if embedding is base64 encoded before convert it to float array
2025-06-09 19:46:17 +08:00
自由的世界人 d2ea0592ce fix: add Youdao and Nomic logos to model logo mapping (#7017) 2025-06-09 19:44:49 +08:00
Wang Jiyuan 66ddeb94bf fix: ollama embedding knowledge query score always 100% (#7001)
* fix: ollama embedding knowledge query score always 100%

* fix: force ollama to use api without v1
2025-06-09 16:52:01 +08:00
76 changed files with 2190 additions and 356 deletions
+2 -2
View File
@@ -151,7 +151,7 @@ index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b
+ "embeddings/decoding base64 embeddings from base64" + "embeddings/decoding base64 embeddings from base64"
+ ); + );
+ return response._thenUnwrap((response) => { + return response._thenUnwrap((response) => {
+ if (response && response.data) { + if (response && response.data && typeof response.data[0]?.embedding === 'string') {
+ response.data.forEach((embeddingBase64Obj) => { + response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding; + const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)( + embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
@@ -266,7 +266,7 @@ index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a
+ "embeddings/decoding base64 embeddings from base64" + "embeddings/decoding base64 embeddings from base64"
+ ); + );
+ return response._thenUnwrap((response) => { + return response._thenUnwrap((response) => {
+ if (response && response.data) { + if (response && response.data && typeof response.data[0]?.embedding === 'string') {
+ response.data.forEach((embeddingBase64Obj) => { + response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding; + const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str); + embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
+1 -1
View File
@@ -188,7 +188,7 @@ Thank you for your support and contributions!
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic [deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio [deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x [twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp [twitter-link]: https://twitter.com/CherryStudioHQ
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord [discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ [discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram [telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
+1 -1
View File
@@ -190,7 +190,7 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic [deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio [deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x [twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp [twitter-link]: https://twitter.com/CherryStudioHQ
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord [discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ [discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram [telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
+1 -1
View File
@@ -202,7 +202,7 @@ https://docs.cherry-ai.com
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic [deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio [deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x [twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp [twitter-link]: https://twitter.com/CherryStudioHQ
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord [discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ [discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram [telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
+635
View File
@@ -0,0 +1,635 @@
# 消息历史版本管理系统设计技术报告(最终版 - 含多模型支持)
## 1. 系统概述
基于现有扁平化架构的最小化扩展,通过 **Topic快照 + Message字段扩展(含siblingIds** 实现版本管理、分支对话和多模型并行回复功能。
### 1.1 核心设计理念
- **最小破坏性**:只扩展现有实体,不新增表
- **快照渲染**:通过Topic简单快照管理主线渲染顺序
- **关系扩展**:通过Message字段实现树状分支、双向链表版本、多模型兄弟关系
## 2. 数据结构设计
### 2.1 实体定义
```typescript
interface Topic {
// === 现有字段保持不变 ===
id: string
name: string
createdAt: string
updatedAt: string
// === 保持简单快照 ===
activeMessageIds: string[] // 当前活跃对话主线的消息ID顺序
}
interface Message {
// === 现有字段保持不变 ===
id: string
role: 'user' | 'assistant' | 'system'
topicId: string
blocks: MessageBlock['id'][]
// === 新增:关系字段 ===
askId?: string // 问答关系:assistant指向对应的user消息
parentMessageId?: string // 分支关系:指向回复的目标消息
version?: number // 版本号(assistant消息专用)
prevVersionId?: string // 版本链表:前一版本
nextVersionId?: string // 版本链表:后一版本
groupRequestId?: string // 请求分组:同次API请求的标识
siblingIds?: string[] // 兄弟关系:同级多模型回复的ID列表
}
interface MessageBlock {
// === 完全不变 ===
id: string
messageId: string
type: MessageBlockType
content: string
// ...其他现有字段
}
```
### 2.2 数据关系图
```mermaid
graph TB
subgraph "Topic快照层 (主线)"
T[Topic.activeMessageIds: user1→asst1-gpt→user2]
end
subgraph "消息实体层"
U1[User Message 1<br/>id: user1]
A1G["GPT-4 回复<br/>id: asst1-gpt, askId: user1<br/>siblingIds: [asst1-claude]"]
A1C["Claude 回复<br/>id: asst1-claude, askId: user1<br/>siblingIds: [asst1-gpt]"]
U2["User Message 2<br/>id: user2, parentMessageId: asst1-gpt"]
end
subgraph "版本链表层 (隐藏)"
A1GV0[GPT-4 v0<br/>askId: user1, version: 0]
A1GV1[GPT-4 v1<br/>askId: user1, version: 1]
A1GV0 -.->|nextVersionId| A1GV1
A1GV1 -.->|prevVersionId| A1GV0
end
subgraph "分支树层 (隐藏)"
U1B[User Branch 1<br/>parentMessageId: asst1-gpt]
A1B[Assistant Branch 1<br/>askId: user1b]
end
T --> U1
T --> A1G
T --> U2
A1G -.->|askId| U1
A1C -.->|askId| U1
A1G -.->|siblingIds| A1C
A1C -.->|siblingIds| A1G
U2 -.->|parentMessageId| A1G
U1B -.->|parentMessageId| A1G
A1B -.->|askId| U1B
```
## 3. 核心操作流程
### 3.1 发送新消息(多模型)
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
participant API
UI->>Redux: sendMessage(userContent, models[])
Note over Redux: 1. 创建用户消息
Redux->>Redux: userMessage = { id: uuid(), role: 'user', ... }
Note over Redux: 2. 创建助手消息(多模型)
Redux->>Redux: groupRequestId = uuid()
Redux->>Redux: assistantMessages = models.map(m => createAssistant(userMessage.id, m))
Note over Redux: 3. 设置兄弟关系
Redux->>Redux: assistantIds = assistantMessages.map(m => m.id)
loop 每个助手消息
Redux->>Redux: msg.siblingIds = assistantIds.filter(id => id !== msg.id)
end
Note over Redux: 4. 更新Topic快照
Redux->>Redux: newActiveMessageIds = [<br/>...oldIds,<br/>userMessage.id,<br/>assistantMessages[0].id<br/>]
Note over Redux: 5. 原子保存
Redux->>DB: transaction([messages, topics])
DB->>DB: messages.bulkPut([userMessage, ...assistantMessages])
DB->>DB: topics.update(topicId, { activeMessageIds })
Note over Redux: 6. 发送API请求
loop 每个模型
Redux->>API: generateResponse(model, userContent)
end
Redux->>UI: 更新状态
```
**复杂度**O(M) where M = 模型数量
### 3.2 重发消息(版本管理)
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
UI->>Redux: resendMessage(userMessageId)
Note over Redux: 1. 查找现有版本
Redux->>DB: messages.where('askId').equals(userMessageId)
DB-->>Redux: existingVersions[]
Note over Redux: 2. 计算新版本号
Redux->>Redux: latestVersion = max(versions.map(v => v.version))
Redux->>Redux: newVersion = latestVersion + 1
Note over Redux: 3. 创建新版本消息(可能多模型)
Redux->>Redux: newGroupRequestId = uuid()
Redux->>Redux: newVersionMessages = models.map(m => createNewVersion(prevMsg, newVersion, newGroupRequestId))
Note over Redux: 4. 设置新版本的兄弟关系
Redux->>Redux: newVersionIds = newVersionMessages.map(m => m.id)
loop 每个新版本消息
Redux->>Redux: msg.siblingIds = newVersionIds.filter(id => id !== msg.id)
end
Note over Redux: 5. 更新版本链表
Redux->>DB: transaction(messages)
DB->>DB: messages.update(prevMessage.id, { nextVersionId })
DB->>DB: messages.bulkPut(newVersionMessages)
Redux->>UI: 更新状态
```
**复杂度**O(V) 查找 + O(M) 创建
### 3.3 切换活跃模型(UI交互)
```mermaid
flowchart TD
A[用户在UI上选择其他模型] --> B[获取当前快照]
B --> C[找到当前助手消息在快照中的位置]
C --> D[用新选择的模型消息ID替换快照中的ID]
D --> E[保存到数据库]
E --> F[Redux自动重新渲染]
style A fill:#e1f5fe
style F fill:#c8e6c9
```
```typescript
const switchActiveModel = async (topicId: string, messageIndex: number, newModelMessageId: string) => {
const topic = await db.topics.get(topicId)
const newActiveMessageIds = [...topic.activeMessageIds]
newActiveMessageIds[messageIndex] = newModelMessageId
await db.topics.update(topicId, { activeMessageIds: newActiveMessageIds })
}
```
**复杂度**O(1)
## 4. 字段作用详解
### 4.1 关键字段关系图
```mermaid
graph LR
subgraph "问答关系"
askId[askId<br/>assistant → user<br/>逻辑关系,永久不变]
end
subgraph "分支关系"
parentId[parentMessageId<br/>message → message<br/>分支对话,树状结构]
end
subgraph "版本关系"
version[version + prevVersionId + nextVersionId<br/>同askId下的版本链表]
end
subgraph "请求分组"
groupId[groupRequestId<br/>同次API请求标识<br/>一次性,每次重发都变]
end
subgraph "兄弟关系"
siblingId[siblingIds<br/>同级多模型回复<br/>双向引用]
end
askId -.-> version
askId -.-> siblingId
parentId -.-> askId
groupId -.-> askId
```
### 4.2 字段使用场景
| 字段 | 用途 | 查询场景 | 生命周期 |
| -------------------------------- | ---------- | -------------------------- | -------- |
| **askId** | 问答映射 | 查找用户问题的所有回复版本 | 永久不变 |
| **parentMessageId** | 分支对话 | 查找某消息的回复分支 | 永久不变 |
| **version + prev/nextVersionId** | 版本管理 | 版本历史导航 | 永久不变 |
| **groupRequestId** | 请求追踪 | 批量状态更新、请求监控 | 一次性 |
| **siblingIds** | 多模型并行 | 渲染同级多模型回复 | 永久不变 |
### 4.3 多模型并行渲染示例
```mermaid
graph TD
U1[User: 帮我写个函数<br/>id: user1]
subgraph "第一次请求 (groupRequestId: req1)"
A1["GPT-4 回复<br/>id: asst1-gpt, askId: user1<br/>siblingIds: [asst1-claude]"]
A2["Claude 回复<br/>id: asst1-claude, askId: user1<br/>siblingIds: [asst1-gpt]"]
end
subgraph "Topic快照 (主线)"
T["activeMessageIds: [user1, asst1-gpt]"]
end
subgraph "UI渲染 (通过siblingIds扩展)"
UI_U1[User: 帮我写个函数]
UI_A1["GPT-4 回复 (活跃)"]
UI_A2["Claude 回复 (可选)"]
end
U1 --> A1
U1 --> A2
T --> U1
T --> A1
A1 -.->|siblingIds| A2
A2 -.->|siblingIds| A1
UI_U1 -.-> UI_A1
UI_U1 -.-> UI_A2
```
## 5. 数据查询与状态管理
### 5.1 话题加载流程
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
participant Selector
UI->>Redux: loadTopic(topicId)
Redux->>DB: 并行查询
par 查询消息
DB->>DB: messages.where('topicId').equals(topicId)
and 查询块
DB->>DB: messageBlocks.where('topicId').equals(topicId)
end
DB-->>Redux: { messages[], blocks[] }
Redux->>Redux: 更新实体状态
UI->>Selector: selectActiveConversationWithSiblings(topicId)
Selector->>Redux: 获取Topic.activeMessageIds
Selector->>Redux: 获取messages实体
Selector-->>UI: 按快照顺序的消息列表 (含兄弟节点)
Note over UI: 渲染对话界面 (支持多模型)
```
### 5.2 渲染选择器(含兄弟节点)
```typescript
export const selectActiveConversationWithSiblings = createSelector(
[
(state: RootState, topicId: string) => state.topics.entities[topicId]?.activeMessageIds || [],
(state: RootState) => state.messages.entities,
(state: RootState) => state.messageBlocks.entities
],
(activeMessageIds, messagesEntities, blocksEntities) => {
return activeMessageIds
.map((messageId) => {
const message = messagesEntities[messageId]
if (!message) return null
if (message.role === 'user') {
return { type: 'user', message, blocks: getMessageBlocks(message, blocksEntities) }
} else if (message.role === 'assistant') {
const siblingMessages = (message.siblingIds || []).map((id) => messagesEntities[id]).filter(Boolean)
const allAssistantMessages = [message, ...siblingMessages]
return {
type: 'assistant_group',
messages: allAssistantMessages.map((msg) => ({
message: msg,
blocks: getMessageBlocks(msg, blocksEntities),
isActive: msg.id === messageId
})),
activeMessageId: messageId
}
}
})
.filter(Boolean)
}
)
```
**复杂度**O(N + S) where N = 快照长度, S = 兄弟节点总数
## 6. 时空复杂度分析
### 6.1 核心操作复杂度对比
```mermaid
graph LR
subgraph "现有架构"
A1[加载话题: O(M+B)]
A2[渲染对话: O(M) 需要过滤排序]
A3[发送消息: O(1)]
end
subgraph "新架构 (含多模型)"
B1[加载话题: O(M+B) ✅相同]
B2[渲染对话: O(N+S) ✅更优]
B3[发送消息: O(M_models) ✅相同]
B4[版本切换: O(1) ➕新功能]
B5[重发消息: O(V)+O(M_models) ➕新功能]
B6[模型切换: O(1) ➕新功能]
end
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9
style B4 fill:#fff3e0
style B5 fill:#fff3e0
style B6 fill:#fff3e0
```
### 6.2 性能优势分析
| 操作 | 现有架构 | 新架构 | 优势说明 |
| ------------ | -------------- | ---------------------------- | -------------------- |
| **话题加载** | O(M + B) | O(M + B) | 性能保持不变 |
| **对话渲染** | O(M) 过滤+排序 | **O(N+S)** 直接索引+兄弟扩展 | N << MS通常较小 |
| **发送消息** | O(1) | O(M_models) | 支持多模型,合理增长 |
| **版本切换** | 不支持 | **O(1)** | 新功能,极佳性能 |
| **模型切换** | 不支持 | **O(1)** | 新功能,极佳性能 |
**关键优势**
- **渲染性能提升**:从 O(M) 优化到 O(N+S),长对话场景收益显著
- **多模型支持**:通过 siblingIds 优雅实现
- **版本管理**:O(1) 的版本/模型切换,用户体验极佳
- **向后兼容**:现有核心操作性能保持不变
## 7. 数据库Schema演进
### 7.1 Migration策略
```mermaid
flowchart TD
A[现有Schema] --> B[添加字段]
B --> C[创建索引]
C --> D[数据迁移]
D --> E[验证完整性]
B1[Topic: +activeMessageIds]
B2[Message: +askId, +parentMessageId<br/>+version, +prevVersionId<br/>+nextVersionId, +groupRequestId<br/>+siblingIds]
C1[idx_messages_askid_version]
C2[idx_messages_parent]
C3[idx_messages_group_request]
D1[生成activeMessageIds快照]
D2[设置现有assistant消息version=0]
B --> B1
B --> B2
C --> C1
C --> C2
C --> C3
D --> D1
D --> D2
```
### 7.2 SQL Migration
```sql
-- 1. 添加字段
ALTER TABLE topics ADD COLUMN activeMessageIds TEXT; -- JSON数组
ALTER TABLE messages ADD COLUMN askId TEXT;
ALTER TABLE messages ADD COLUMN parentMessageId TEXT;
ALTER TABLE messages ADD COLUMN version INTEGER;
ALTER TABLE messages ADD COLUMN prevVersionId TEXT;
ALTER TABLE messages ADD COLUMN nextVersionId TEXT;
ALTER TABLE messages ADD COLUMN groupRequestId TEXT;
ALTER TABLE messages ADD COLUMN siblingIds TEXT; -- JSON数组
-- 2. 创建索引
CREATE INDEX idx_messages_askid_version ON messages(askId, version);
CREATE INDEX idx_messages_parent ON messages(parentMessageId);
CREATE INDEX idx_messages_group_request ON messages(groupRequestId);
-- 3. 数据迁移
UPDATE messages SET version = 0 WHERE role = 'assistant';
```
## 8. 流式更新兼容性
### 8.1 MessageBlock更新流程
```mermaid
sequenceDiagram
participant Stream
participant Redux
participant DB
participant UI
Note over Stream: 流式内容到达
Stream->>Redux: updateBlock(blockId, content)
Redux->>Redux: updateOneBlock({ id, changes })
Redux->>UI: 立即更新显示
Note over Redux: 节流数据库写入
Redux->>DB: throttledDbUpdate(blockId, content)
Note over Stream,UI: 版本/兄弟关系不影响块更新
```
**关键点**
- MessageBlock 仍然直接关联到 Message
- 版本/兄弟关系在 Message 层面,不影响 Block 的流式更新
- 现有的节流机制和更新逻辑完全保持不变
## 9. 系统架构总览
### 9.1 整体架构图
```mermaid
graph TB
subgraph "UI层"
UI1[对话界面]
UI2[版本选择器]
UI3[分支导航]
UI4[模型切换器]
end
subgraph "Redux状态层"
R1[topics: EntityAdapter]
R2[messages: EntityAdapter]
R3[messageBlocks: EntityAdapter]
S1[selectActiveConversationWithSiblings]
S2[selectVersionHistory]
end
subgraph "数据库层"
DB1[(topics表)]
DB2[(messages表)]
DB3[(messageBlocks表)]
end
subgraph "API层"
API1[多模型并行请求]
API2[流式响应处理]
end
UI1 --> S1
UI2 --> S2
UI4 --> S1
S1 --> R1
S1 --> R2
S2 --> R2
R1 <--> DB1
R2 <--> DB2
R3 <--> DB3
R2 --> API1
API2 --> R3
style UI1 fill:#e3f2fd
style R1 fill:#f3e5f5
style R2 fill:#f3e5f5
style R3 fill:#f3e5f5
style DB1 fill:#e8f5e8
style DB2 fill:#e8f5e8
style DB3 fill:#e8f5e8
```
### 9.2 数据流向
```mermaid
flowchart LR
A[用户输入] --> B[创建User Message]
B --> C["创建Assistant Messages (多模型)"]
C --> C1[设置Sibling关系]
C1 --> D["更新Topic快照 (主线)"]
D --> E[API并行请求]
E --> F[流式更新Blocks]
F --> G["UI实时渲染 (含多模型)"]
H[版本切换] --> I[更新快照指针]
I --> G
J[分支对话] --> K[创建分支消息]
K --> D
L[模型切换] --> I
style A fill:#ffebee
style G fill:#e8f5e8
style H fill:#fff3e0
style J fill:#f3e5f5
style L fill:#e1f5fe
```
## 10. Redux Slice 实现范例
根据上述架构设计,`messages` slice 将演变为一个纯粹的、由 `createEntityAdapter` 管理的"消息池"。它只负责高效地存储和访问单个消息实体,而不再关心对话的顺序。
### `store/messagesSlice.ts`
```typescript
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from './store' // 你的store类型定义
import type { Message } from '@renderer/types/newMessage' // 假设 Message 类型定义在外部
// 1. 创建 Entity Adapter
// 它会自动生成管理实体的reducer逻辑,实现一个高效的消息池。
const messagesAdapter = createEntityAdapter<Message>()
// 2. 定义 Slice 的初始状态
// adapter.getInitialState() 会自动创建 { ids: [], entities: {} } 结构
const initialState = messagesAdapter.getInitialState()
// 3. 创建 Slice
const messagesSlice = createSlice({
name: 'messages',
initialState,
// Reducers被极大简化,多数直接引用adapter提供的方法
reducers: {
// Action: 添加一条消息
messageAdded: messagesAdapter.addOne,
// Action: 一次性添加或更新多个消息 (高性能)
// 用途: 加载话题历史、发送新一轮问答(user+assistants)
messagesUpserted: messagesAdapter.upsertMany,
// Action: 更新单个消息
// 用途: 流式更新结束、状态变更等
messageUpdated: messagesAdapter.updateOne,
// Action: 删除单个消息
messageRemoved: messagesAdapter.removeOne,
// Action: 删除多个消息
messagesRemoved: messagesAdapter.removeMany,
// Action: 用新数据完全替换消息池
// 用途: 首次加载或强制刷新
messagesSet: messagesAdapter.setAll
}
})
// 4. 导出 Actions
export const { messageAdded, messagesUpserted, messageUpdated, messageRemoved, messagesRemoved, messagesSet } =
messagesSlice.actions
// 5. 导出 Selectors
// Adapter 会自动创建高效的查询函数 (e.g., O(1) by ID)
export const messagesSelectors = messagesAdapter.getSelectors((state: RootState) => state.messages)
// 6. 导出 Reducer
export default messagesSlice.reducer
```
### 核心思想总结
1. **职责单一**: 此 Slice 只做一件事——管理 `Message` 实体。它像一个数据库表,高效地处理增删改查,但对业务逻辑(如对话顺序)一无所知。
2. **逻辑上移**: 所有涉及多个 Slice 的复杂业务逻辑(如发送消息、切换版本)都应封装在 **Thunks** 或其他中间件中。Thunk 作为流程协调者,会 `dispatch` 多个原子化的 Action 给 `messagesSlice``topicsSlice`,以完成一次完整的业务操作并保证数据一致性。
3. **性能保证**: `createEntityAdapter` 内部使用哈希表(对象)来存储实体,确保通过 ID 查询消息的操作为 O(1) 复杂度,性能极佳。
### 旧状态属性迁移
为了完成 `messagesSlice` 向纯粹"消息池"的演进,原有的混合状态属性需要被迁移或废弃,以实现彻底的职责分离。
| 原属性 (`newMessage.ts`) | 处理方式 | 新的归宿 / 说明 |
| :----------------------- | :------------ | :-------------------------------------------------------------------------------------------- |
| `messageIdsByTopic` | **废弃** | 核心职责转移。由 `topicsSlice` 中的 `activeMessageIds` 字段接管,作为渲染快照。 |
| `currentTopicId` | **迁移** | 属于UI当前上下文状态,应迁移至 `topicsSlice`。 |
| `loadingByTopic` | **迁移** | 话题的加载状态与话题本身更相关,应迁移至 `topicsSlice`。 |
| `displayCount` | **废弃/迁移** | UI相关的显示逻辑,不属于消息数据层。建议迁移至专门的 `Slice` 或在相关组件中作为本地状态管理。 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+8 -5
View File
@@ -107,8 +107,11 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
新增划词助手 划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能
助手支持分组 复制功能:新增纯文本复制(去除Markdown格式符号)
支持主题颜色切换 知识库:支持设置向量维度,修复Ollama分数错误和维度编辑问题
划词助手支持应用过滤 多语言:增加模型名称多语言提示和翻译源语言手动选择
翻译模块功能改进 文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程
模型:修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
图像功能:统一图片查看器,支持Base64图片渲染,修复图片预览相关问题
UI:实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示
+5 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "1.4.1", "version": "1.4.2",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -68,9 +68,11 @@
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31", "@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
"@cherrystudio/embedjs-loader-web": "^0.1.31", "@cherrystudio/embedjs-loader-web": "^0.1.31",
"@cherrystudio/embedjs-loader-xml": "^0.1.31", "@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31", "@cherrystudio/embedjs-openai": "^0.1.31",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@langchain/community": "^0.3.36", "@langchain/community": "^0.3.36",
"@langchain/ollama": "^0.2.1",
"@strongtz/win32-arm64-msvc": "^0.4.7", "@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0", "@tanstack/react-query": "^5.27.0",
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
@@ -92,7 +94,8 @@
"officeparser": "^4.1.1", "officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2", "os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"selection-hook": "^0.9.22", "remove-markdown": "^0.6.2",
"selection-hook": "^0.9.23",
"tar": "^7.4.3", "tar": "^7.4.3",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"webdav": "^5.8.0", "webdav": "^5.8.0",
+9 -2
View File
@@ -5,8 +5,15 @@ import EmbeddingsFactory from './EmbeddingsFactory'
export default class Embeddings { export default class Embeddings {
private sdk: BaseEmbeddings private sdk: BaseEmbeddings
constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) { constructor({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams) this.sdk = EmbeddingsFactory.create({
model,
provider,
apiKey,
apiVersion,
baseURL,
dimensions
} as KnowledgeBaseParams)
} }
public async init(): Promise<void> { public async init(): Promise<void> {
return this.sdk.init() return this.sdk.init()
+37 -8
View File
@@ -1,20 +1,49 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces' import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai' import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings' import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import { getInstanceName } from '@main/utils' import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types' import { KnowledgeBaseParams } from '@types'
import VoyageEmbeddings from './VoyageEmbeddings' import { SUPPORTED_DIM_MODELS as VOYAGE_SUPPORTED_DIM_MODELS, VoyageEmbeddings } from './VoyageEmbeddings'
export default class EmbeddingsFactory { export default class EmbeddingsFactory {
static create({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings { static create({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
const batchSize = 10 const batchSize = 10
if (model.includes('voyage')) { if (provider === 'voyageai') {
return new VoyageEmbeddings({ if (VOYAGE_SUPPORTED_DIM_MODELS.includes(model)) {
modelName: model, return new VoyageEmbeddings({
apiKey, modelName: model,
outputDimension: dimensions, apiKey,
batchSize: 8 outputDimension: dimensions,
batchSize: 8
})
} else {
return new VoyageEmbeddings({
modelName: model,
apiKey,
batchSize: 8
})
}
}
if (provider === 'ollama') {
if (baseURL.includes('v1/')) {
return new OllamaEmbeddings({
model: model,
baseUrl: baseURL.replace('v1/', ''),
requestOptions: {
// @ts-ignore expected
'encoding-format': 'float'
}
})
}
return new OllamaEmbeddings({
model: model,
baseUrl: baseURL,
requestOptions: {
// @ts-ignore expected
'encoding-format': 'float'
}
}) })
} }
if (apiVersion !== undefined) { if (apiVersion !== undefined) {
+8 -4
View File
@@ -1,16 +1,20 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces' import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage' import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
export default class VoyageEmbeddings extends BaseEmbeddings { /**
* 支持设置嵌入维度的模型
*/
export const SUPPORTED_DIM_MODELS = ['voyage-3-large', 'voyage-3.5', 'voyage-3.5-lite', 'voyage-code-3']
export class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) { constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
super() super()
if (!this.configuration) this.configuration = {} if (!this.configuration) this.configuration = {}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3' if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
if (!SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
if (!this.configuration.outputDimension) { throw new Error(`VoyageEmbeddings only supports ${SUPPORTED_DIM_MODELS.join(', ')}`)
throw new Error('You need to pass in the optional dimensions parameter for this model')
} }
this.model = new _VoyageEmbeddings(this.configuration) this.model = new _VoyageEmbeddings(this.configuration)
} }
override async getDimensions(): Promise<number> { override async getDimensions(): Promise<number> {
+20
View File
@@ -34,6 +34,26 @@ if (isWin) {
app.commandLine.appendSwitch('wm-window-animations-disabled') app.commandLine.appendSwitch('wm-window-animations-disabled')
} }
// Enable features for unresponsive renderer js call stacks
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
app.on('web-contents-created', (_, webContents) => {
webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Document-Policy': ['include-js-call-stacks-in-crash-reports']
}
})
})
webContents.on('unresponsive', async () => {
// Interrupt execution and collect call stack from unresponsive renderer
Logger.error('Renderer unresponsive start')
const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
Logger.error('Renderer unresponsive js call stack\n', callStack)
})
})
// in production mode, handle uncaught exception and unhandled rejection globally // in production mode, handle uncaught exception and unhandled rejection globally
if (!isDev) { if (!isDev) {
// handle uncaught exception // handle uncaught exception
+9 -1
View File
@@ -110,13 +110,21 @@ class KnowledgeService {
private getRagApplication = async ({ private getRagApplication = async ({
id, id,
model, model,
provider,
apiKey, apiKey,
apiVersion, apiVersion,
baseURL, baseURL,
dimensions dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => { }: KnowledgeBaseParams): Promise<RAGApplication> => {
let ragApplication: RAGApplication let ragApplication: RAGApplication
const embeddings = new Embeddings({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams) const embeddings = new Embeddings({
model,
provider,
apiKey,
apiVersion,
baseURL,
dimensions
} as KnowledgeBaseParams)
try { try {
ragApplication = await new RAGApplicationBuilder() ragApplication = await new RAGApplicationBuilder()
.setModel('NO_MODEL') .setModel('NO_MODEL')
+79 -17
View File
@@ -14,6 +14,7 @@ import type {
import type { ActionItem } from '../../renderer/src/types/selectionTypes' import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager' import { ConfigKeys, configManager } from './ConfigManager'
import storeSyncService from './StoreSyncService'
let SelectionHook: SelectionHookConstructor | null = null let SelectionHook: SelectionHookConstructor | null = null
try { try {
@@ -39,7 +40,8 @@ type RelativeOrientation =
enum TriggerMode { enum TriggerMode {
Selected = 'selected', Selected = 'selected',
Ctrlkey = 'ctrlkey' Ctrlkey = 'ctrlkey',
Shortcut = 'shortcut'
} }
/** SelectionService is a singleton class that manages the selection hook and the toolbar window /** SelectionService is a singleton class that manages the selection hook and the toolbar window
@@ -314,6 +316,8 @@ export class SelectionService {
this.toolbarWindow.close() this.toolbarWindow.close()
this.toolbarWindow = null this.toolbarWindow = null
} }
this.closePreloadedActionWindows()
this.started = false this.started = false
this.logInfo('SelectionService Stopped') this.logInfo('SelectionService Stopped')
return true return true
@@ -334,6 +338,21 @@ export class SelectionService {
this.logInfo('SelectionService Quitted') this.logInfo('SelectionService Quitted')
} }
/**
* Toggle the enabled state of the selection service
* Will sync the new enabled store to all renderer windows
*/
public toggleEnabled(enabled: boolean | undefined = undefined) {
if (!this.selectionHook) return
const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
configManager.setSelectionAssistantEnabled(newEnabled)
//sync the new enabled state to all renderer windows
storeSyncService.syncToRenderer('selectionStore/setSelectionEnabled', newEnabled)
}
/** /**
* Create and configure the toolbar window * Create and configure the toolbar window
* Sets up window properties, event handlers, and loads the toolbar UI * Sets up window properties, event handlers, and loads the toolbar UI
@@ -378,6 +397,9 @@ export class SelectionService {
// Clean up when closed // Clean up when closed
this.toolbarWindow.on('closed', () => { this.toolbarWindow.on('closed', () => {
if (!this.toolbarWindow?.isDestroyed()) {
this.toolbarWindow?.destroy()
}
this.toolbarWindow = null this.toolbarWindow = null
}) })
@@ -563,6 +585,21 @@ export class SelectionService {
return startTop.y === endTop.y && startBottom.y === endBottom.y return startTop.y === endTop.y && startBottom.y === endBottom.y
} }
/**
* Get the user selected text and process it (trigger by shortcut)
*
* it's a public method used by shortcut service
*/
public processSelectTextByShortcut(): void {
if (!this.selectionHook || !this.started || this.triggerMode !== TriggerMode.Shortcut) return
const selectionData = this.selectionHook.getCurrentSelection()
if (selectionData) {
this.processTextSelection(selectionData)
}
}
/** /**
* Determine if the text selection should be processed by filter mode&list * Determine if the text selection should be processed by filter mode&list
* @param selectionData Text selection information and coordinates * @param selectionData Text selection information and coordinates
@@ -854,7 +891,6 @@ export class SelectionService {
this.lastCtrlkeyDownTime = -1 this.lastCtrlkeyDownTime = -1
const selectionData = this.selectionHook!.getCurrentSelection() const selectionData = this.selectionHook!.getCurrentSelection()
if (selectionData) { if (selectionData) {
this.processTextSelection(selectionData) this.processTextSelection(selectionData)
} }
@@ -958,6 +994,17 @@ export class SelectionService {
} }
} }
/**
* Close all preloaded action windows
*/
private closePreloadedActionWindows() {
for (const actionWindow of this.preloadedActionWindows) {
if (!actionWindow.isDestroyed()) {
actionWindow.destroy()
}
}
}
/** /**
* Preload a new action window asynchronously * Preload a new action window asynchronously
* This method is called after popping a window to ensure we always have windows ready * This method is called after popping a window to ensure we always have windows ready
@@ -1106,29 +1153,44 @@ export class SelectionService {
* Manages appropriate event listeners for each mode * Manages appropriate event listeners for each mode
*/ */
private processTriggerMode() { private processTriggerMode() {
if (this.triggerMode === TriggerMode.Selected) { switch (this.triggerMode) {
if (this.isCtrlkeyListenerActive) { case TriggerMode.Selected:
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode) if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode) this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false this.isCtrlkeyListenerActive = false
} }
this.selectionHook!.setSelectionPassiveMode(false) this.selectionHook!.setSelectionPassiveMode(false)
} else if (this.triggerMode === TriggerMode.Ctrlkey) { break
if (!this.isCtrlkeyListenerActive) { case TriggerMode.Ctrlkey:
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode) if (!this.isCtrlkeyListenerActive) {
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode) this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = true this.isCtrlkeyListenerActive = true
} }
this.selectionHook!.setSelectionPassiveMode(true) this.selectionHook!.setSelectionPassiveMode(true)
break
case TriggerMode.Shortcut:
//remove the ctrlkey listener, don't need any key listener for shortcut mode
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(true)
break
} }
} }
public writeToClipboard(text: string): boolean { public writeToClipboard(text: string): boolean {
return this.selectionHook?.writeToClipboard(text) ?? false if (!this.selectionHook || !this.started) return false
return this.selectionHook.writeToClipboard(text)
} }
/** /**
+59 -16
View File
@@ -4,10 +4,16 @@ import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { windowService } from './WindowService' import { windowService } from './WindowService'
let showAppAccelerator: string | null = null let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null let showMiniWindowAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null
//indicate if the shortcuts are registered on app boot time
let isRegisterOnBoot = true
// store the focus and blur handlers for each window to unregister them later // store the focus and blur handlers for each window to unregister them later
const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>() const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>()
@@ -28,6 +34,18 @@ function getShortcutHandler(shortcut: Shortcut) {
return () => { return () => {
windowService.toggleMiniWindow() windowService.toggleMiniWindow()
} }
case 'selection_assistant_toggle':
return () => {
if (selectionService) {
selectionService.toggleEnabled()
}
}
case 'selection_assistant_select_text':
return () => {
if (selectionService) {
selectionService.processSelectTextByShortcut()
}
}
default: default:
return null return null
} }
@@ -37,9 +55,8 @@ function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+') return shortcut.join('+')
} }
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = ( // convert the shortcut recorded by keyboard event key value to electron global shortcut format
shortcut: string | string[] const convertShortcutFormat = (shortcut: string | string[]): string => {
): string => {
const accelerator = (() => { const accelerator = (() => {
if (Array.isArray(shortcut)) { if (Array.isArray(shortcut)) {
return shortcut return shortcut
@@ -93,11 +110,14 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
} }
export function registerShortcuts(window: BrowserWindow) { export function registerShortcuts(window: BrowserWindow) {
window.once('ready-to-show', () => { if (isRegisterOnBoot) {
if (configManager.getLaunchToTray()) { window.once('ready-to-show', () => {
registerOnlyUniversalShortcuts() if (configManager.getLaunchToTray()) {
} registerOnlyUniversalShortcuts()
}) }
})
isRegisterOnBoot = false
}
//only for clearer code //only for clearer code
const registerOnlyUniversalShortcuts = () => { const registerOnlyUniversalShortcuts = () => {
@@ -124,7 +144,12 @@ export function registerShortcuts(window: BrowserWindow) {
} }
// only register universal shortcuts when needed // only register universal shortcuts when needed
if (onlyUniversalShortcuts && !['show_app', 'mini_window'].includes(shortcut.key)) { if (
onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
shortcut.key
)
) {
return return
} }
@@ -146,6 +171,14 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut) showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break break
case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_select_text':
selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut)
break
//the following ZOOMs will register shortcuts seperately, so will return //the following ZOOMs will register shortcuts seperately, so will return
case 'zoom_in': case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window)) globalShortcut.register('CommandOrControl+=', () => handler(window))
@@ -162,9 +195,7 @@ export function registerShortcuts(window: BrowserWindow) {
return return
} }
const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat( const accelerator = convertShortcutFormat(shortcut.shortcut)
shortcut.shortcut
)
globalShortcut.register(accelerator, () => handler(window)) globalShortcut.register(accelerator, () => handler(window))
} catch (error) { } catch (error) {
@@ -181,15 +212,25 @@ export function registerShortcuts(window: BrowserWindow) {
if (showAppAccelerator) { if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut) const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
const accelerator = const accelerator = convertShortcutFormat(showAppAccelerator)
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window)) handler && globalShortcut.register(accelerator, () => handler(window))
} }
if (showMiniWindowAccelerator) { if (showMiniWindowAccelerator) {
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut) const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
const accelerator = const accelerator = convertShortcutFormat(showMiniWindowAccelerator)
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator) handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantSelectTextAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_select_text' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantSelectTextAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window)) handler && globalShortcut.register(accelerator, () => handler(window))
} }
} catch (error) { } catch (error) {
@@ -217,6 +258,8 @@ export function unregisterAllShortcuts() {
try { try {
showAppAccelerator = null showAppAccelerator = null
showMiniWindowAccelerator = null showMiniWindowAccelerator = null
selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => { windowOnHandlers.forEach((handlers, window) => {
window.off('focus', handlers.onFocusHandler) window.off('focus', handlers.onFocusHandler)
window.off('blur', handlers.onBlurHandler) window.off('blur', handlers.onBlurHandler)
+17
View File
@@ -49,6 +49,23 @@ export class StoreSyncService {
this.windowIds = this.windowIds.filter((id) => id !== windowId) this.windowIds = this.windowIds.filter((id) => id !== windowId)
} }
/**
* Sync an action to all renderer windows
* @param type Action type, like 'settings/setTray'
* @param payload Action payload
*
* NOTICE: DO NOT use directly in ConfigManager, may cause infinite sync loop
*/
public syncToRenderer(type: string, payload: any): void {
const action: StoreSyncAction = {
type,
payload
}
//-1 means the action is from the main process, will be broadcast to all windows
this.broadcastToOtherWindows(-1, action)
}
/** /**
* Register IPC handlers for store sync communication * Register IPC handlers for store sync communication
* Handles window subscription, unsubscription and action broadcasting * Handles window subscription, unsubscription and action broadcasting
-6
View File
@@ -116,12 +116,6 @@ export class WindowService {
app.exit(1) app.exit(1)
} }
}) })
mainWindow.webContents.on('unresponsive', () => {
// 在升级到electron 34后,可以获取具体js stack trace,目前只打个日志监控下
// https://www.electronjs.org/blog/electron-34-0#unresponsive-renderer-javascript-call-stacks
Logger.error('Renderer process unresponsive')
})
} }
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) { private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -4,13 +4,3 @@
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border); border-left: 0.5px solid var(--color-border);
} }
.group-container {
.context-menu-container {
width: 100%;
}
}
.context-menu-container {
max-width: 100%;
}
+21 -8
View File
@@ -129,22 +129,29 @@ ul {
.message-content-container { .message-content-container {
margin: 5px 0; margin: 5px 0;
border-radius: 8px; border-radius: 8px;
padding: 10px 15px 0 15px; padding: 0.5rem 1rem;
} }
.block-wrapper {
display: flow-root;
}
.message-content-container > *:last-child {
margin-bottom: 0;
}
.message-thought-container { .message-thought-container {
margin-top: 8px; margin-top: 8px;
} }
.message-user { .message-user {
color: var(--chat-text-user); color: var(--chat-text-user);
.markdown, .message-content-container-user .anticon {
.anticon,
.iconfont,
.lucide,
.message-tokens {
color: var(--chat-text-user) !important; color: var(--chat-text-user) !important;
} }
.message-action-button:hover {
background-color: var(--color-white-soft); .markdown {
color: var(--chat-text-user);
} }
} }
.group-grid-container.horizontal, .group-grid-container.horizontal,
@@ -165,6 +172,12 @@ ul {
code { code {
color: var(--color-text); color: var(--color-text);
} }
.markdown {
display: flow-root;
*:last-child {
margin-bottom: 0;
}
}
} }
.lucide { .lucide {
+8 -5
View File
@@ -295,13 +295,16 @@ emoji-picker {
--border-size: 0; --border-size: 0;
} }
.katex-display { .katex,
mjx-container {
display: inline-block;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
} overflow-wrap: break-word;
vertical-align: middle;
mjx-container { max-width: 100%;
overflow-x: auto; padding: 1px 2px;
margin-top: -2px;
} }
/* CodeMirror 相关样式 */ /* CodeMirror 相关样式 */
@@ -26,6 +26,7 @@ interface Props {
onSave?: (newContent: string) => void onSave?: (newContent: string) => void
onChange?: (newContent: string) => void onChange?: (newContent: string) => void
setTools?: (value: React.SetStateAction<CodeTool[]>) => void setTools?: (value: React.SetStateAction<CodeTool[]>) => void
height?: string
minHeight?: string minHeight?: string
maxHeight?: string maxHeight?: string
/** 用于覆写编辑器的某些设置 */ /** 用于覆写编辑器的某些设置 */
@@ -54,6 +55,7 @@ const CodeEditor = ({
onSave, onSave,
onChange, onChange,
setTools, setTools,
height,
minHeight, minHeight,
maxHeight, maxHeight,
options, options,
@@ -193,6 +195,7 @@ const CodeEditor = ({
value={initialContent.current} value={initialContent.current}
placeholder={placeholder} placeholder={placeholder}
width="100%" width="100%"
height={height}
minHeight={minHeight} minHeight={minHeight}
maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'} maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'}
editable={true} editable={true}
@@ -6,9 +6,10 @@ import styled from 'styled-components'
interface ContextMenuProps { interface ContextMenuProps {
children: React.ReactNode children: React.ReactNode
onContextMenu?: (e: React.MouseEvent) => void onContextMenu?: (e: React.MouseEvent) => void
style?: React.CSSProperties
} }
const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) => { const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu, style }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null) const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedText, setSelectedText] = useState<string>('') const [selectedText, setSelectedText] = useState<string>('')
@@ -66,7 +67,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
] ]
return ( return (
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container"> <ContextContainer onContextMenu={handleContextMenu} className="context-menu-container" style={style}>
{contextMenuPosition && ( {contextMenuPosition && (
<Dropdown <Dropdown
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }} overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
+94 -3
View File
@@ -140,6 +140,8 @@ import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png' import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png' import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png' import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
import YoudaoLogo from '@renderer/assets/images/providers/netease-youdao.svg'
import NomicLogo from '@renderer/assets/images/providers/nomic.png'
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import { Assistant, Model } from '@renderer/types' import { Assistant, Model } from '@renderer/types'
import OpenAI from 'openai' import OpenAI from 'openai'
@@ -297,7 +299,7 @@ export function getModelLogo(modelId: string) {
'davinci-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr, 'davinci-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark, glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark, deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
'(qwen|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark, '(qwen|qwq|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark, gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,
'yi-': isLight ? YiModelLogo : YiModelLogoDark, 'yi-': isLight ? YiModelLogo : YiModelLogoDark,
llama: isLight ? LlamaModelLogo : LlamaModelLogoDark, llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,
@@ -376,12 +378,14 @@ export function getModelLogo(modelId: string) {
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark, 'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
xirang: isLight ? XirangModelLogo : XirangModelLogoDark, xirang: isLight ? XirangModelLogo : XirangModelLogoDark,
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark, hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
youdao: YoudaoLogo,
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark, embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark, perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark, sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
'bge-': BgeModelLogo, 'bge-': BgeModelLogo,
'voyage-': VoyageModelLogo, 'voyage-': VoyageModelLogo,
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark,
'nomic-': NomicLogo
} }
for (const key in logoMap) { for (const key in logoMap) {
@@ -425,7 +429,86 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'deepseek-ai' group: 'deepseek-ai'
} }
], ],
'302ai': [
{
id: 'deepseek-chat',
name: 'deepseek-chat',
provider: '302ai',
group: 'DeepSeek'
},
{
id: 'deepseek-reasoner',
name: 'deepseek-reasoner',
provider: '302ai',
group: 'DeepSeek'
},
{
id: 'chatgpt-4o-latest',
name: 'chatgpt-4o-latest',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'gpt-4.1',
name: 'gpt-4.1',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'o3',
name: 'o3',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'o4-mini',
name: 'o4-mini',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'qwen3-235b-a22b',
name: 'qwen3-235b-a22b',
provider: '302ai',
group: 'Qwen'
},
{
id: 'gemini-2.5-flash-preview-05-20',
name: 'gemini-2.5-flash-preview-05-20',
provider: '302ai',
group: 'Gemini'
},
{
id: 'gemini-2.5-pro-preview-06-05',
name: 'gemini-2.5-pro-preview-06-05',
provider: '302ai',
group: 'Gemini'
},
{
id: 'claude-sonnet-4-20250514',
provider: '302ai',
name: 'claude-sonnet-4-20250514',
group: 'Anthropic'
},
{
id: 'claude-opus-4-20250514',
provider: '302ai',
name: 'claude-opus-4-20250514',
group: 'Anthropic'
},
{
id: 'jina-clip-v2',
name: 'jina-clip-v2',
provider: '302ai',
group: 'Jina AI'
},
{
id: 'jina-reranker-m0',
name: 'jina-reranker-m0',
provider: '302ai',
group: 'Jina AI'
}
],
aihubmix: [ aihubmix: [
{ {
id: 'gpt-4o', id: 'gpt-4o',
@@ -2078,6 +2161,14 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'Qwen Plus', name: 'Qwen Plus',
group: 'Qwen' group: 'Qwen'
} }
],
cephalon: [
{
id: 'DeepSeek-R1',
provider: 'cephalon',
name: 'DeepSeek-R1满血版',
group: 'DeepSeek'
}
] ]
} }
+27 -1
View File
@@ -1,6 +1,7 @@
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png' import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png' import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png'
import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png' import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png'
import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp' import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp' import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png' import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png'
@@ -8,6 +9,7 @@ import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png
import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg' import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg'
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png' import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png' import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png' import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png' import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
@@ -48,6 +50,7 @@ import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { TOKENFLUX_HOST } from './constant' import { TOKENFLUX_HOST } from './constant'
const PROVIDER_LOGO_MAP = { const PROVIDER_LOGO_MAP = {
'302ai': Ai302ProviderLogo,
openai: OpenAiProviderLogo, openai: OpenAiProviderLogo,
silicon: SiliconFlowProviderLogo, silicon: SiliconFlowProviderLogo,
deepseek: DeepSeekProviderLogo, deepseek: DeepSeekProviderLogo,
@@ -94,7 +97,8 @@ const PROVIDER_LOGO_MAP = {
alayanew: AlayaNewProviderLogo, alayanew: AlayaNewProviderLogo,
voyageai: VoyageAIProviderLogo, voyageai: VoyageAIProviderLogo,
qiniu: QiniuProviderLogo, qiniu: QiniuProviderLogo,
tokenflux: TokenFluxProviderLogo tokenflux: TokenFluxProviderLogo,
cephalon: CephalonProviderLogo
} as const } as const
export function getProviderLogo(providerId: string) { export function getProviderLogo(providerId: string) {
@@ -106,6 +110,17 @@ export const NOT_SUPPORTED_REANK_PROVIDERS = ['ollama']
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini'] export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini']
export const PROVIDER_CONFIG = { export const PROVIDER_CONFIG = {
'302ai': {
api: {
url: 'https://api.302.ai'
},
websites: {
official: 'https://302.ai',
apiKey: 'https://dash.302.ai/apis/list',
docs: 'https://302ai.apifox.cn/api-147522039',
models: 'https://302.ai/pricing/'
}
},
openai: { openai: {
api: { api: {
url: 'https://api.openai.com' url: 'https://api.openai.com'
@@ -612,5 +627,16 @@ export const PROVIDER_CONFIG = {
docs: `${TOKENFLUX_HOST}/docs`, docs: `${TOKENFLUX_HOST}/docs`,
models: `${TOKENFLUX_HOST}/models` models: `${TOKENFLUX_HOST}/models`
} }
},
cephalon: {
api: {
url: 'https://cephalon.cloud/user-center/v1/model'
},
websites: {
official: 'https://cephalon.cloud/share/register-landing?invite_id=jSdOYA',
apiKey: 'https://cephalon.cloud/api',
docs: 'https://cephalon.cloud/apitoken/1864244127731589124',
models: 'https://cephalon.cloud/model'
}
} }
} }
+8 -3
View File
@@ -1,4 +1,5 @@
import { useAppDispatch, useAppSelector } from '@renderer/store' import { createSelector } from '@reduxjs/toolkit'
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { setTagsOrder, updateAssistants } from '@renderer/store/assistants' import { setTagsOrder, updateAssistants } from '@renderer/store/assistants'
import { flatMap, groupBy, uniq } from 'lodash' import { flatMap, groupBy, uniq } from 'lodash'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
@@ -6,15 +7,19 @@ import { useTranslation } from 'react-i18next'
import { useAssistants } from './useAssistant' import { useAssistants } from './useAssistant'
// 基础选择器
const selectAssistantsState = (state: RootState) => state.assistants
// 记忆化 tagsOrder 选择器(自动处理默认值)--- 这是一个选择器,用于从 store 中获取 tagsOrder 的值。因为之前的tagsOrder是后面新加的,不这样做会报错,所以这里需要处理一下默认值
const selectTagsOrder = createSelector([selectAssistantsState], (assistants) => assistants.tagsOrder ?? [])
// 定义useTags的返回类型,包含所有标签和获取特定标签的助手函数 // 定义useTags的返回类型,包含所有标签和获取特定标签的助手函数
// 为了不增加新的概念,标签直接作为助手的属性,所以这里的标签是指助手的标签属性 // 为了不增加新的概念,标签直接作为助手的属性,所以这里的标签是指助手的标签属性
// 但是为了方便管理,增加了一个获取特定标签的助手函数 // 但是为了方便管理,增加了一个获取特定标签的助手函数
export const useTags = () => { export const useTags = () => {
const { assistants } = useAssistants() const { assistants } = useAssistants()
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || []) const savedTagsOrder = useAppSelector(selectTagsOrder)
// 计算所有标签 // 计算所有标签
const allTags = useMemo(() => { const allTags = useMemo(() => {
-2
View File
@@ -99,8 +99,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const data = { ...topic, name: summaryText } const data = { ...topic, name: summaryText }
_setActiveTopic(data) _setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} else {
window.message?.error(i18n.t('message.error.fetchTopicName'))
} }
} }
} finally { } finally {
+23 -7
View File
@@ -261,6 +261,7 @@
"topics.clear.title": "Clear Messages", "topics.clear.title": "Clear Messages",
"topics.copy.image": "Copy as image", "topics.copy.image": "Copy as image",
"topics.copy.md": "Copy as markdown", "topics.copy.md": "Copy as markdown",
"topics.copy.plain_text": "Copy as plain text (remove Markdown)",
"topics.copy.title": "Copy", "topics.copy.title": "Copy",
"topics.delete.shortcut": "Hold {{key}} to delete directly", "topics.delete.shortcut": "Hold {{key}} to delete directly",
"topics.edit.placeholder": "Enter new name", "topics.edit.placeholder": "Enter new name",
@@ -565,8 +566,12 @@
"urls": "URLs", "urls": "URLs",
"dimensions": "Embedding dimension", "dimensions": "Embedding dimension",
"dimensions_size_tooltip": "The size of the embedding dimension; the larger the value, the larger the embedding dimension, but it also consumes more tokens.", "dimensions_size_tooltip": "The size of the embedding dimension; the larger the value, the larger the embedding dimension, but it also consumes more tokens.",
"dimensions_size_placeholder": "Default value (modification not recommended)", "dimensions_size_placeholder": " Embedding dimension size, e.g. 1024",
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})." "dimensions_auto_set": "Auto-set embedding dimensions",
"dimensions_error_invalid": "Please enter embedding dimension size",
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}}).",
"dimensions_set_right": "⚠️ Please ensure the model supports the set embedding dimension size",
"dimensions_default": "The model will use default embedding dimensions"
}, },
"languages": { "languages": {
"arabic": "Arabic", "arabic": "Arabic",
@@ -973,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "Baichuan", "baichuan": "Baichuan",
"baidu-cloud": "Baidu Cloud", "baidu-cloud": "Baidu Cloud",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud", "dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
@@ -1013,7 +1019,8 @@
"zhipu": "ZHIPU AI", "zhipu": "ZHIPU AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI"
}, },
"restore": { "restore": {
"confirm": "Are you sure you want to restore data?", "confirm": "Are you sure you want to restore data?",
@@ -1512,6 +1519,7 @@
"messages.prompt": "Show prompt", "messages.prompt": "Show prompt",
"messages.tokens": "Show token usage", "messages.tokens": "Show token usage",
"messages.divider": "Show divider between messages", "messages.divider": "Show divider between messages",
"messages.divider.tooltip": "Not applicable to bubble-style message",
"messages.grid_columns": "Message grid display columns", "messages.grid_columns": "Message grid display columns",
"messages.grid_popover_trigger": "Grid detail trigger", "messages.grid_popover_trigger": "Grid detail trigger",
"messages.grid_popover_trigger.click": "Click to display", "messages.grid_popover_trigger.click": "Click to display",
@@ -1544,6 +1552,7 @@
"models.add.model_id.select.placeholder": "Select Model", "models.add.model_id.select.placeholder": "Select Model",
"models.add.model_id.tooltip": "Example: gpt-3.5-turbo", "models.add.model_id.tooltip": "Example: gpt-3.5-turbo",
"models.add.model_name": "Model Name", "models.add.model_name": "Model Name",
"models.add.model_name.tooltip": "Optional e.g. GPT-4",
"models.add.model_name.placeholder": "Optional e.g. GPT-4", "models.add.model_name.placeholder": "Optional e.g. GPT-4",
"models.check.all": "All", "models.check.all": "All",
"models.check.all_models_passed": "All models check passed", "models.check.all_models_passed": "All models check passed",
@@ -1695,6 +1704,8 @@
"exit_fullscreen": "Exit Fullscreen", "exit_fullscreen": "Exit Fullscreen",
"key": "Key", "key": "Key",
"mini_window": "Quick Assistant", "mini_window": "Quick Assistant",
"selection_assistant_toggle": "Toggle Selection Assistant",
"selection_assistant_select_text": "Selection Assistant: Select Text",
"new_topic": "New Topic", "new_topic": "New Topic",
"press_shortcut": "Press Shortcut", "press_shortcut": "Press Shortcut",
"reset_defaults": "Reset Defaults", "reset_defaults": "Reset Defaults",
@@ -1820,7 +1831,7 @@
"close": "Close", "close": "Close",
"closed": "Translation closed", "closed": "Translation closed",
"copied": "Translation content copied", "copied": "Translation content copied",
"detected.language": "Detected Language", "detected.language": "Auto Detect",
"empty": "Translation content is empty", "empty": "Translation content is empty",
"not.found": "Translation content not found", "not.found": "Translation content not found",
"confirm": { "confirm": {
@@ -1911,10 +1922,15 @@
"title": "Toolbar", "title": "Toolbar",
"trigger_mode": { "trigger_mode": {
"title": "Trigger Mode", "title": "Trigger Mode",
"description": "Show toolbar immediately when text is selected, or show only when Ctrl key is held after selection.", "description": "The way to trigger the selection assistant and show the toolbar",
"description_note": "The Ctrl key may not work in some apps. If you use AHK or other tools to remap the Ctrl key, it may not work.", "description_note": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.",
"selected": "Selection", "selected": "Selection",
"ctrlkey": "Ctrl Key" "selected_note": "Show toolbar immediately when text is selected",
"ctrlkey": "Ctrl Key",
"ctrlkey_note": "After selection, hold down the Ctrl key to show the toolbar",
"shortcut": "Shortcut",
"shortcut_note": "After selection, use shortcut to show the toolbar. Please set the shortcut in the shortcut settings page and enable it. ",
"shortcut_link": "Go to Shortcut Settings"
}, },
"compact_mode": { "compact_mode": {
"title": "Compact Mode", "title": "Compact Mode",
+27 -11
View File
@@ -261,6 +261,7 @@
"topics.clear.title": "メッセージをクリア", "topics.clear.title": "メッセージをクリア",
"topics.copy.image": "画像としてコピー", "topics.copy.image": "画像としてコピー",
"topics.copy.md": "Markdownとしてコピー", "topics.copy.md": "Markdownとしてコピー",
"topics.copy.plain_text": "プレーンテキストとしてコピー(Markdownを除去)",
"topics.copy.title": "コピー", "topics.copy.title": "コピー",
"topics.delete.shortcut": "{{key}}キーを押しながらで直接削除", "topics.delete.shortcut": "{{key}}キーを押しながらで直接削除",
"topics.edit.placeholder": "新しい名前を入力", "topics.edit.placeholder": "新しい名前を入力",
@@ -565,8 +566,12 @@
"urls": "URL", "urls": "URL",
"dimensions": "埋め込み次元", "dimensions": "埋め込み次元",
"dimensions_size_tooltip": "埋め込み次元のサイズは、数値が大きいほど埋め込み次元も大きくなりますが、消費するトークンも増えます。", "dimensions_size_tooltip": "埋め込み次元のサイズは、数値が大きいほど埋め込み次元も大きくなりますが、消費するトークンも増えます。",
"dimensions_size_placeholder": "デフォルト値(変更はお勧めしません", "dimensions_size_placeholder": " 埋め込み次元のサイズ(例:1024",
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。" "dimensions_auto_set": "埋め込み次元を自動設定",
"dimensions_error_invalid": "埋め込み次元のサイズを入力してください",
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。",
"dimensions_set_right": "⚠️ モデルが設定した埋め込み次元のサイズをサポートしていることを確認してください",
"dimensions_default": "モデルはデフォルトの埋め込み次元を使用します"
}, },
"languages": { "languages": {
"arabic": "アラビア語", "arabic": "アラビア語",
@@ -628,7 +633,6 @@
"error.enter.api.key": "APIキーを入力してください", "error.enter.api.key": "APIキーを入力してください",
"error.enter.model": "モデルを選択してください", "error.enter.model": "モデルを選択してください",
"error.enter.name": "ナレッジベース名を入力してください", "error.enter.name": "ナレッジベース名を入力してください",
"error.fetchTopicName": "トピックの命名に失敗しました",
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした", "error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
"error.invalid.api.host": "無効なAPIアドレスです", "error.invalid.api.host": "無効なAPIアドレスです",
"error.invalid.api.key": "無効なAPIキーです", "error.invalid.api.key": "無効なAPIキーです",
@@ -699,7 +703,8 @@
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!", "warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません", "error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
"download.success": "ダウンロードに成功しました", "download.success": "ダウンロードに成功しました",
"download.failed": "ダウンロードに失敗しました" "download.failed": "ダウンロードに失敗しました",
"error.fetchTopicName": "トピック名の取得に失敗しました"
}, },
"minapp": { "minapp": {
"popup": { "popup": {
@@ -1013,7 +1018,9 @@
"zhipu": "智譜AI", "zhipu": "智譜AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理", "qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI",
"cephalon": "Cephalon"
}, },
"restore": { "restore": {
"confirm": "データを復元しますか?", "confirm": "データを復元しますか?",
@@ -1506,6 +1513,7 @@
"messages.prompt": "プロンプト表示", "messages.prompt": "プロンプト表示",
"messages.tokens": "トークン使用量を表示", "messages.tokens": "トークン使用量を表示",
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.divider.tooltip": "バブルスタイルのメッセージには適用されません",
"messages.grid_columns": "メッセージグリッドの表示列数", "messages.grid_columns": "メッセージグリッドの表示列数",
"messages.grid_popover_trigger": "グリッド詳細トリガー", "messages.grid_popover_trigger": "グリッド詳細トリガー",
"messages.grid_popover_trigger.click": "クリックで表示", "messages.grid_popover_trigger.click": "クリックで表示",
@@ -1538,7 +1546,8 @@
"models.add.model_id.select.placeholder": "モデルを選択", "models.add.model_id.select.placeholder": "モデルを選択",
"models.add.model_id.tooltip": "例:gpt-3.5-turbo", "models.add.model_id.tooltip": "例:gpt-3.5-turbo",
"models.add.model_name": "モデル名", "models.add.model_name": "モデル名",
"models.add.model_name.placeholder": "例:GPT-3.5", "models.add.model_name.tooltip": "例:GPT-4",
"models.add.model_name.placeholder": "例:GPT-4",
"models.check.all": "すべて", "models.check.all": "すべて",
"models.check.all_models_passed": "すべてのモデルチェックが成功しました", "models.check.all_models_passed": "すべてのモデルチェックが成功しました",
"models.check.button_caption": "健康チェック", "models.check.button_caption": "健康チェック",
@@ -1683,6 +1692,8 @@
"exit_fullscreen": "フルスクリーンを終了", "exit_fullscreen": "フルスクリーンを終了",
"key": "キー", "key": "キー",
"mini_window": "クイックアシスタント", "mini_window": "クイックアシスタント",
"selection_assistant_toggle": "選択アシスタントを切り替え",
"selection_assistant_select_text": "選択アシスタント:テキストを選択",
"new_topic": "新しいトピック", "new_topic": "新しいトピック",
"press_shortcut": "ショートカットを押す", "press_shortcut": "ショートカットを押す",
"reset_defaults": "デフォルトのショートカットをリセット", "reset_defaults": "デフォルトのショートカットをリセット",
@@ -1853,7 +1864,7 @@
"menu": { "menu": {
"description": "對當前輸入框內容進行翻譯" "description": "對當前輸入框內容進行翻譯"
}, },
"detected.language": "検出された言語" "detected.language": "自動検出"
}, },
"tray": { "tray": {
"quit": "終了", "quit": "終了",
@@ -1910,11 +1921,16 @@
"toolbar": { "toolbar": {
"title": "ツールバー", "title": "ツールバー",
"trigger_mode": { "trigger_mode": {
"title": "表示方法", "title": "単語の取り出し方",
"description": "テキスト選択時に即時表示、またはCtrlキー押下時のみ表示", "description": "テキスト選択後、取詞ツールバーを表示する方法",
"description_note": "一部のアプリCtrlキーでテキスト選択に対応していません。AHKなどCtrlキーをマップすると、選択できなくなる場合があります。", "description_note": "一部のアプリケーションでは、Ctrl キーでテキスト選択できません。AHK などのツールを使用して Ctrl キーをマップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
"selected": "選択時", "selected": "選択時",
"ctrlkey": "Ctrlキー" "selected_note": "テキスト選択時に即時表示",
"ctrlkey": "Ctrlキー",
"ctrlkey_note": "テキスト選択後、Ctrlキーを押下して表示",
"shortcut": "ショートカットキー",
"shortcut_note": "テキスト選択後、ショートカットキーを押下して表示。ショートカットキーを設定するには、ショートカット設定ページで有効にしてください。",
"shortcut_link": "ショートカット設定ページに移動"
}, },
"compact_mode": { "compact_mode": {
"title": "コンパクトモード", "title": "コンパクトモード",
+24 -8
View File
@@ -261,6 +261,7 @@
"topics.clear.title": "Очистить сообщения", "topics.clear.title": "Очистить сообщения",
"topics.copy.image": "Скопировать как изображение", "topics.copy.image": "Скопировать как изображение",
"topics.copy.md": "Скопировать как Markdown", "topics.copy.md": "Скопировать как Markdown",
"topics.copy.plain_text": "Копировать как обычный текст (удалить Markdown)",
"topics.copy.title": "Скопировать", "topics.copy.title": "Скопировать",
"topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления", "topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления",
"topics.edit.placeholder": "Введите новый заголовок", "topics.edit.placeholder": "Введите новый заголовок",
@@ -565,8 +566,12 @@
"urls": "URL-адреса", "urls": "URL-адреса",
"dimensions": "векторное пространство", "dimensions": "векторное пространство",
"dimensions_size_tooltip": "Размерность вложения, чем больше значение, тем больше размерность вложения, но и потребляемых токенов также становится больше.", "dimensions_size_tooltip": "Размерность вложения, чем больше значение, тем больше размерность вложения, но и потребляемых токенов также становится больше.",
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)", "dimensions_size_placeholder": " Размерность эмбеддинга, например 1024",
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})" "dimensions_auto_set": "Автоматическая установка размерности эмбеддинга",
"dimensions_error_invalid": "Пожалуйста, введите размерность эмбеддинга",
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})",
"dimensions_set_right": "⚠️ Убедитесь, что модель поддерживает заданный размер эмбеддинга",
"dimensions_default": "Модель будет использовать размер эмбеддинга по умолчанию"
}, },
"languages": { "languages": {
"arabic": "Арабский", "arabic": "Арабский",
@@ -628,7 +633,6 @@
"error.enter.api.key": "Пожалуйста, введите ваш API ключ", "error.enter.api.key": "Пожалуйста, введите ваш API ключ",
"error.enter.model": "Пожалуйста, выберите модель", "error.enter.model": "Пожалуйста, выберите модель",
"error.enter.name": "Пожалуйста, введите название базы знаний", "error.enter.name": "Пожалуйста, введите название базы знаний",
"error.fetchTopicName": "Не удалось назвать тему",
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания", "error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
"error.invalid.api.host": "Неверный API адрес", "error.invalid.api.host": "Неверный API адрес",
"error.invalid.api.key": "Неверный API ключ", "error.invalid.api.key": "Неверный API ключ",
@@ -699,7 +703,8 @@
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!", "warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!", "warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
"download.success": "Скачано успешно", "download.success": "Скачано успешно",
"download.failed": "Скачивание не удалось" "download.failed": "Скачивание не удалось",
"error.fetchTopicName": "Не удалось назвать топик"
}, },
"minapp": { "minapp": {
"popup": { "popup": {
@@ -973,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "Baichuan", "baichuan": "Baichuan",
"baidu-cloud": "Baidu Cloud", "baidu-cloud": "Baidu Cloud",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud", "dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
@@ -1013,7 +1019,8 @@
"zhipu": "ZHIPU AI", "zhipu": "ZHIPU AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI"
}, },
"restore": { "restore": {
"confirm": "Вы уверены, что хотите восстановить данные?", "confirm": "Вы уверены, что хотите восстановить данные?",
@@ -1506,6 +1513,7 @@
"messages.prompt": "Показывать подсказки", "messages.prompt": "Показывать подсказки",
"messages.tokens": "Показать использование токенов", "messages.tokens": "Показать использование токенов",
"messages.divider": "Показывать разделитель между сообщениями", "messages.divider": "Показывать разделитель между сообщениями",
"messages.divider.tooltip": "Не применимо к сообщениям в стиле пузырей",
"messages.grid_columns": "Количество столбцов сетки сообщений", "messages.grid_columns": "Количество столбцов сетки сообщений",
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке", "messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
"messages.grid_popover_trigger.click": "Нажатие для отображения", "messages.grid_popover_trigger.click": "Нажатие для отображения",
@@ -1538,6 +1546,7 @@
"models.add.model_id.select.placeholder": "Выберите модель", "models.add.model_id.select.placeholder": "Выберите модель",
"models.add.model_id.tooltip": "Пример: gpt-3.5-turbo", "models.add.model_id.tooltip": "Пример: gpt-3.5-turbo",
"models.add.model_name": "Имя модели", "models.add.model_name": "Имя модели",
"models.add.model_name.tooltip": "Необязательно, например, GPT-4",
"models.add.model_name.placeholder": "Необязательно, например, GPT-4", "models.add.model_name.placeholder": "Необязательно, например, GPT-4",
"models.check.all": "Все", "models.check.all": "Все",
"models.check.all_models_passed": "Все модели прошли проверку", "models.check.all_models_passed": "Все модели прошли проверку",
@@ -1683,6 +1692,8 @@
"exit_fullscreen": "Выйти из полноэкранного режима", "exit_fullscreen": "Выйти из полноэкранного режима",
"key": "Клавиша", "key": "Клавиша",
"mini_window": "Быстрый помощник", "mini_window": "Быстрый помощник",
"selection_assistant_toggle": "Переключить помощник выделения",
"selection_assistant_select_text": "Помощник выделения: выделить текст",
"new_topic": "Новый топик", "new_topic": "Новый топик",
"press_shortcut": "Нажмите сочетание клавиш", "press_shortcut": "Нажмите сочетание клавиш",
"reset_defaults": "Сбросить настройки по умолчанию", "reset_defaults": "Сбросить настройки по умолчанию",
@@ -1853,7 +1864,7 @@
"menu": { "menu": {
"description": "Перевести содержимое текущего ввода" "description": "Перевести содержимое текущего ввода"
}, },
"detected.language": "Обнаруженный язык" "detected.language": "Автоматическое обнаружение"
}, },
"tray": { "tray": {
"quit": "Выйти", "quit": "Выйти",
@@ -1911,10 +1922,15 @@
"title": "Панель инструментов", "title": "Панель инструментов",
"trigger_mode": { "trigger_mode": {
"title": "Режим активации", "title": "Режим активации",
"description": "Показывать панель сразу при выделении или только при удержании Ctrl.", "description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш",
"description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.", "description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
"selected": "При выделении", "selected": "При выделении",
"ctrlkey": "По Ctrl" "selected_note": "После выделения",
"ctrlkey": "По Ctrl",
"ctrlkey_note": "После выделения, удерживайте Ctrl для показа панели. Пожалуйста, установите Ctrl в настройках клавиатуры и активируйте его.",
"shortcut": "По сочетанию клавиш",
"shortcut_note": "После выделения, используйте сочетание клавиш для показа панели. Пожалуйста, установите сочетание клавиш в настройках клавиатуры и активируйте его.",
"shortcut_link": "Перейти к настройкам клавиатуры"
}, },
"compact_mode": { "compact_mode": {
"title": "Компактный режим", "title": "Компактный режим",
+23 -7
View File
@@ -279,6 +279,7 @@
"topics.clear.title": "清空消息", "topics.clear.title": "清空消息",
"topics.copy.image": "复制为图片", "topics.copy.image": "复制为图片",
"topics.copy.md": "复制为 Markdown", "topics.copy.md": "复制为 Markdown",
"topics.copy.plain_text": "复制为纯文本(去除 Markdown",
"topics.copy.title": "复制", "topics.copy.title": "复制",
"topics.delete.shortcut": "按住 {{key}} 可直接删除", "topics.delete.shortcut": "按住 {{key}} 可直接删除",
"topics.edit.placeholder": "输入新名称", "topics.edit.placeholder": "输入新名称",
@@ -518,7 +519,11 @@
"delete_confirm": "确定要删除此知识库吗?", "delete_confirm": "确定要删除此知识库吗?",
"dimensions": "嵌入维度", "dimensions": "嵌入维度",
"dimensions_size_tooltip": "嵌入维度大小,数值越大,嵌入维度越大,但消耗的 Token 也越多", "dimensions_size_tooltip": "嵌入维度大小,数值越大,嵌入维度越大,但消耗的 Token 也越多",
"dimensions_size_placeholder": " 默认值(不建议修改)", "dimensions_set_right": "⚠️ 请确保模型支持所设置的嵌入维度大小",
"dimensions_default": "模型将使用默认嵌入维度",
"dimensions_size_placeholder": " 嵌入维度大小,如 1024",
"dimensions_auto_set": "自动设置嵌入维度",
"dimensions_error_invalid": "请输入嵌入维度大小",
"dimensions_size_too_large": "嵌入维度不能超过模型上下文限制({{max_context}}", "dimensions_size_too_large": "嵌入维度不能超过模型上下文限制({{max_context}}",
"directories": "目录", "directories": "目录",
"directory_placeholder": "请输入目录路径", "directory_placeholder": "请输入目录路径",
@@ -973,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "百川", "baichuan": "百川",
"baidu-cloud": "百度云千帆", "baidu-cloud": "百度云千帆",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "阿里云百炼", "dashscope": "阿里云百炼",
"deepseek": "深度求索", "deepseek": "深度求索",
@@ -1013,7 +1019,8 @@
"zhipu": "智谱AI", "zhipu": "智谱AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理", "qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI"
}, },
"restore": { "restore": {
"confirm": "确定要恢复数据吗?", "confirm": "确定要恢复数据吗?",
@@ -1512,6 +1519,7 @@
"messages.prompt": "显示提示词", "messages.prompt": "显示提示词",
"messages.tokens": "显示Token用量", "messages.tokens": "显示Token用量",
"messages.divider": "消息分割线", "messages.divider": "消息分割线",
"messages.divider.tooltip": "不适用于气泡样式消息",
"messages.grid_columns": "消息网格展示列数", "messages.grid_columns": "消息网格展示列数",
"messages.grid_popover_trigger": "网格详情触发", "messages.grid_popover_trigger": "网格详情触发",
"messages.grid_popover_trigger.click": "点击显示", "messages.grid_popover_trigger.click": "点击显示",
@@ -1544,7 +1552,8 @@
"models.add.model_id.select.placeholder": "选择模型", "models.add.model_id.select.placeholder": "选择模型",
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo", "models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
"models.add.model_name": "模型名称", "models.add.model_name": "模型名称",
"models.add.model_name.placeholder": "例如 GPT-3.5", "models.add.model_name.placeholder": "例如 GPT-4",
"models.add.model_name.tooltip": "例如 GPT-4",
"models.check.all": "所有", "models.check.all": "所有",
"models.check.all_models_passed": "所有模型检测通过", "models.check.all_models_passed": "所有模型检测通过",
"models.check.button_caption": "健康检测", "models.check.button_caption": "健康检测",
@@ -1695,6 +1704,8 @@
"exit_fullscreen": "退出全屏", "exit_fullscreen": "退出全屏",
"key": "按键", "key": "按键",
"mini_window": "快捷助手", "mini_window": "快捷助手",
"selection_assistant_toggle": "开关划词助手",
"selection_assistant_select_text": "划词助手:取词",
"new_topic": "新建话题", "new_topic": "新建话题",
"press_shortcut": "按下快捷键", "press_shortcut": "按下快捷键",
"reset_defaults": "重置默认快捷键", "reset_defaults": "重置默认快捷键",
@@ -1853,7 +1864,7 @@
}, },
"title": "翻译", "title": "翻译",
"tooltip.newline": "换行", "tooltip.newline": "换行",
"detected.language": "检测到的语言" "detected.language": "自动检测"
}, },
"tray": { "tray": {
"quit": "退出", "quit": "退出",
@@ -1910,11 +1921,16 @@
"toolbar": { "toolbar": {
"title": "工具栏", "title": "工具栏",
"trigger_mode": { "trigger_mode": {
"title": "触发方式", "title": "取词方式",
"description": "划词立即显示工具栏,或者划词后按住 Ctrl 键才显示工具栏", "description": "划词后,触发取词并显示工具栏的方式",
"description_note": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。", "description_note": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
"selected": "划词", "selected": "划词",
"ctrlkey": "Ctrl 键" "selected_note": "划词后立即显示工具栏",
"ctrlkey": "Ctrl 键",
"ctrlkey_note": "划词后,再 按住 Ctrl键,才显示工具栏",
"shortcut": "快捷键",
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
"shortcut_link": "前往快捷键设置"
}, },
"compact_mode": { "compact_mode": {
"title": "紧凑模式", "title": "紧凑模式",
+23 -7
View File
@@ -261,6 +261,7 @@
"topics.clear.title": "清空訊息", "topics.clear.title": "清空訊息",
"topics.copy.image": "複製為圖片", "topics.copy.image": "複製為圖片",
"topics.copy.md": "複製為 Markdown", "topics.copy.md": "複製為 Markdown",
"topics.copy.plain_text": "複製為純文字(移除 Markdown",
"topics.copy.title": "複製", "topics.copy.title": "複製",
"topics.delete.shortcut": "按住 {{key}} 可直接刪除", "topics.delete.shortcut": "按住 {{key}} 可直接刪除",
"topics.edit.placeholder": "輸入新名稱", "topics.edit.placeholder": "輸入新名稱",
@@ -565,8 +566,12 @@
"urls": "網址", "urls": "網址",
"dimensions": "嵌入維度", "dimensions": "嵌入維度",
"dimensions_size_tooltip": "嵌入維度大小,數值越大,嵌入維度越大,但消耗的 Token 也越多", "dimensions_size_tooltip": "嵌入維度大小,數值越大,嵌入維度越大,但消耗的 Token 也越多",
"dimensions_size_placeholder": "預設值(不建議修改)", "dimensions_size_placeholder": " 嵌入維度大小,例如 1024",
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}}" "dimensions_auto_set": "自動設定嵌入維度",
"dimensions_error_invalid": "請輸入嵌入維度大小",
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}}",
"dimensions_set_right": "⚠️ 請確保模型支援所設置的嵌入維度大小",
"dimensions_default": "模型將使用預設嵌入維度"
}, },
"languages": { "languages": {
"arabic": "阿拉伯文", "arabic": "阿拉伯文",
@@ -973,6 +978,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "百川", "baichuan": "百川",
"baidu-cloud": "百度雲千帆", "baidu-cloud": "百度雲千帆",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "阿里雲百鍊", "dashscope": "阿里雲百鍊",
"deepseek": "深度求索", "deepseek": "深度求索",
@@ -1013,7 +1019,8 @@
"zhipu": "智譜 AI", "zhipu": "智譜 AI",
"voyageai": "Voyage AI", "voyageai": "Voyage AI",
"qiniu": "七牛雲 AI 推理", "qiniu": "七牛雲 AI 推理",
"tokenflux": "TokenFlux" "tokenflux": "TokenFlux",
"302ai": "302.AI"
}, },
"restore": { "restore": {
"confirm": "確定要復原資料嗎?", "confirm": "確定要復原資料嗎?",
@@ -1509,6 +1516,7 @@
"messages.prompt": "提示詞顯示", "messages.prompt": "提示詞顯示",
"messages.tokens": "Token用量顯示", "messages.tokens": "Token用量顯示",
"messages.divider": "訊息間顯示分隔線", "messages.divider": "訊息間顯示分隔線",
"messages.divider.tooltip": "不適用於氣泡樣式消息",
"messages.grid_columns": "訊息網格展示列數", "messages.grid_columns": "訊息網格展示列數",
"messages.grid_popover_trigger": "網格詳細資訊觸發", "messages.grid_popover_trigger": "網格詳細資訊觸發",
"messages.grid_popover_trigger.click": "點選顯示", "messages.grid_popover_trigger.click": "點選顯示",
@@ -1542,6 +1550,7 @@
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo", "models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
"models.add.model_name": "模型名稱", "models.add.model_name": "模型名稱",
"models.add.model_name.placeholder": "選填,例如 GPT-4", "models.add.model_name.placeholder": "選填,例如 GPT-4",
"models.add.model_name.tooltip": "例如 GPT-4",
"models.check.all": "所有", "models.check.all": "所有",
"models.check.all_models_passed": "所有模型檢查通過", "models.check.all_models_passed": "所有模型檢查通過",
"models.check.button_caption": "健康檢查", "models.check.button_caption": "健康檢查",
@@ -1685,6 +1694,8 @@
"copy_last_message": "複製上一則訊息", "copy_last_message": "複製上一則訊息",
"key": "按鍵", "key": "按鍵",
"mini_window": "快捷助手", "mini_window": "快捷助手",
"selection_assistant_toggle": "開關劃詞助手",
"selection_assistant_select_text": "劃詞助手:取词",
"new_topic": "新增話題", "new_topic": "新增話題",
"press_shortcut": "按下快捷鍵", "press_shortcut": "按下快捷鍵",
"reset_defaults": "重設預設快捷鍵", "reset_defaults": "重設預設快捷鍵",
@@ -1853,7 +1864,7 @@
"menu": { "menu": {
"description": "對當前輸入框內容進行翻譯" "description": "對當前輸入框內容進行翻譯"
}, },
"detected.language": "檢測到的語言" "detected.language": "自動檢測"
}, },
"tray": { "tray": {
"quit": "結束", "quit": "結束",
@@ -1910,11 +1921,16 @@
"toolbar": { "toolbar": {
"title": "工具列", "title": "工具列",
"trigger_mode": { "trigger_mode": {
"title": "觸發方式", "title": "取詞方式",
"description": "劃詞立即顯示工具列,或者劃詞後按住 Ctrl 鍵才顯示工具列", "description": "劃詞後,觸發取詞並顯示工具列的方式",
"description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了AHK等工具對Ctrl鍵進行了重新對應,可能導致部分應用程式無法劃詞。", "description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了AHK等工具對Ctrl鍵進行了重新對應,可能導致部分應用程式無法劃詞。",
"selected": "劃詞", "selected": "劃詞",
"ctrlkey": "Ctrl 鍵" "selected_note": "劃詞後,立即顯示工具列",
"ctrlkey": "Ctrl 鍵",
"ctrlkey_note": "劃詞後,再 按住 Ctrl鍵,才顯示工具列",
"shortcut": "快捷鍵",
"shortcut_note": "劃詞後,使用快捷鍵顯示工具列。請在快捷鍵設定頁面中設置取詞快捷鍵並啟用。",
"shortcut_link": "前往快捷鍵設定"
}, },
"compact_mode": { "compact_mode": {
"title": "緊湊模式", "title": "緊湊模式",
+9 -3
View File
@@ -198,6 +198,7 @@
"topics.clear.title": "Καθαρισμός μηνυμάτων", "topics.clear.title": "Καθαρισμός μηνυμάτων",
"topics.copy.image": "Αντιγραφή ως εικόνα", "topics.copy.image": "Αντιγραφή ως εικόνα",
"topics.copy.md": "Αντιγραφή ως Markdown", "topics.copy.md": "Αντιγραφή ως Markdown",
"topics.copy.plain_text": "Αντιγραφή ως απλό κείμενο (αφαίρεση Markdown)",
"topics.copy.title": "Αντιγραφή", "topics.copy.title": "Αντιγραφή",
"topics.delete.shortcut": "Πατήστε {{key}} για να διαγράψετε αμέσως", "topics.delete.shortcut": "Πατήστε {{key}} για να διαγράψετε αμέσως",
"topics.edit.placeholder": "Εισαγάγετε το νέο όνομα", "topics.edit.placeholder": "Εισαγάγετε το νέο όνομα",
@@ -490,8 +491,12 @@
"urls": "Διευθύνσεις", "urls": "Διευθύνσεις",
"dimensions": "Διαστάσεις ενσωμάτωσης", "dimensions": "Διαστάσεις ενσωμάτωσης",
"dimensions_size_tooltip": "Το μέγεθος των διαστάσεων ενσωμάτωσης. Όσο μεγαλύτερη η τιμή, τόσο περισσότερες οι διαστάσεις ενσωμάτωσης, αλλά και οι απαιτούμενες μονάδες (Tokens).", "dimensions_size_tooltip": "Το μέγεθος των διαστάσεων ενσωμάτωσης. Όσο μεγαλύτερη η τιμή, τόσο περισσότερες οι διαστάσεις ενσωμάτωσης, αλλά και οι απαιτούμενες μονάδες (Tokens).",
"dimensions_size_placeholder": "Προεπιλεγμένη τιμή (δεν συνιστάται να τροποποιηθεί)", "dimensions_size_placeholder": " Μέγεθος διαστάσεων ενσωμάτωσης, π.χ. 1024",
"dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})" "dimensions_auto_set": "Αυτόματη ρύθμιση διαστάσεων ενσωμάτωσης",
"dimensions_error_invalid": "Παρακαλώ εισάγετε μέγεθος διαστάσεων ενσωμάτωσης",
"dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})",
"dimensions_set_right": "⚠️ Βεβαιωθείτε ότι το μοντέλο υποστηρίζει το καθορισμένο μέγεθος διαστάσεων ενσωμάτωσης",
"dimensions_default": "Το μοντέλο θα χρησιμοποιήσει τις προεπιλεγμένες διαστάσεις ενσωμάτωσης"
}, },
"languages": { "languages": {
"arabic": "Αραβικά", "arabic": "Αραβικά",
@@ -551,7 +556,6 @@
"error.enter.api.key": "Παρακαλώ εισάγετε το κλειδί API σας", "error.enter.api.key": "Παρακαλώ εισάγετε το κλειδί API σας",
"error.enter.model": "Παρακαλώ επιλέξτε ένα μοντέλο", "error.enter.model": "Παρακαλώ επιλέξτε ένα μοντέλο",
"error.enter.name": "Παρακαλώ εισάγετε ένα όνομα για τη βάση γνώσεων", "error.enter.name": "Παρακαλώ εισάγετε ένα όνομα για τη βάση γνώσεων",
"error.fetchTopicName": "Αποτυχία ονοματοδοσίας θέματος",
"error.get_embedding_dimensions": "Απέτυχε η πρόσληψη διαστάσεων ενσωμάτωσης", "error.get_embedding_dimensions": "Απέτυχε η πρόσληψη διαστάσεων ενσωμάτωσης",
"error.invalid.api.host": "Μη έγκυρη διεύθυνση API", "error.invalid.api.host": "Μη έγκυρη διεύθυνση API",
"error.invalid.api.key": "Μη έγκυρο κλειδί API", "error.invalid.api.key": "Μη έγκυρο κλειδί API",
@@ -836,6 +840,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "Παράκειμαι", "baichuan": "Παράκειμαι",
"baidu-cloud": "Baidu Cloud Qianfan", "baidu-cloud": "Baidu Cloud Qianfan",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot", "copilot": "GitHub Copilot",
"dashscope": "AliCloud Bailian", "dashscope": "AliCloud Bailian",
"deepseek": "Βαθιά Αναζήτηση", "deepseek": "Βαθιά Αναζήτηση",
@@ -1300,6 +1305,7 @@
"advancedSettings": "Προχωρημένες Ρυθμίσεις" "advancedSettings": "Προχωρημένες Ρυθμίσεις"
}, },
"messages.divider": "Διαχωριστική γραμμή μηνυμάτων", "messages.divider": "Διαχωριστική γραμμή μηνυμάτων",
"messages.divider.tooltip": "Δεν ισχύει για μηνύματα με στυλ φυσαλίδας",
"messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων", "messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων",
"messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid", "messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid",
"messages.grid_popover_trigger.click": "Εμφάνιση κλικ", "messages.grid_popover_trigger.click": "Εμφάνιση κλικ",
+9 -3
View File
@@ -199,6 +199,7 @@
"topics.clear.title": "Limpiar mensajes", "topics.clear.title": "Limpiar mensajes",
"topics.copy.image": "Copiar como imagen", "topics.copy.image": "Copiar como imagen",
"topics.copy.md": "Copiar como Markdown", "topics.copy.md": "Copiar como Markdown",
"topics.copy.plain_text": "Copiar como texto sin formato (eliminar Markdown)",
"topics.copy.title": "Copiar", "topics.copy.title": "Copiar",
"topics.delete.shortcut": "Mantén presionada {{key}} para eliminar directamente", "topics.delete.shortcut": "Mantén presionada {{key}} para eliminar directamente",
"topics.edit.placeholder": "Introduce nuevo nombre", "topics.edit.placeholder": "Introduce nuevo nombre",
@@ -491,8 +492,12 @@
"urls": "URLs", "urls": "URLs",
"dimensions": "Dimensión de incrustación", "dimensions": "Dimensión de incrustación",
"dimensions_size_tooltip": "Tamaño de la dimensión de incrustación, cuanto mayor sea el valor, mayor será la dimensión de incrustación, pero también consumirá más Tokens", "dimensions_size_tooltip": "Tamaño de la dimensión de incrustación, cuanto mayor sea el valor, mayor será la dimensión de incrustación, pero también consumirá más Tokens",
"dimensions_size_placeholder": "Valor predeterminado (no recomendado modificar)", "dimensions_size_placeholder": " Tamaño de dimensión de incrustación, ej. 1024",
"dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})" "dimensions_auto_set": "Configuración automática de dimensiones de incrustación",
"dimensions_error_invalid": "Por favor ingrese el tamaño de dimensión de incrustación",
"dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})",
"dimensions_set_right": "⚠️ Asegúrese de que el modelo admita el tamaño de dimensión de incrustación establecido",
"dimensions_default": "El modelo utilizará las dimensiones de incrustación predeterminadas"
}, },
"languages": { "languages": {
"arabic": "Árabe", "arabic": "Árabe",
@@ -552,7 +557,6 @@
"error.enter.api.key": "Ingrese su clave API", "error.enter.api.key": "Ingrese su clave API",
"error.enter.model": "Seleccione un modelo", "error.enter.model": "Seleccione un modelo",
"error.enter.name": "Ingrese el nombre de la base de conocimiento", "error.enter.name": "Ingrese el nombre de la base de conocimiento",
"error.fetchTopicName": "Error al nombrar el tema",
"error.get_embedding_dimensions": "Fallo al obtener las dimensiones de incrustación", "error.get_embedding_dimensions": "Fallo al obtener las dimensiones de incrustación",
"error.invalid.api.host": "Dirección API inválida", "error.invalid.api.host": "Dirección API inválida",
"error.invalid.api.key": "Clave API inválida", "error.invalid.api.key": "Clave API inválida",
@@ -837,6 +841,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "BaiChuan", "baichuan": "BaiChuan",
"baidu-cloud": "Baidu Nube Qiánfān", "baidu-cloud": "Baidu Nube Qiánfān",
"cephalon": "Cephalon",
"copilot": "GitHub Copiloto", "copilot": "GitHub Copiloto",
"dashscope": "Álibaba Nube BaiLiàn", "dashscope": "Álibaba Nube BaiLiàn",
"deepseek": "Profundo Buscar", "deepseek": "Profundo Buscar",
@@ -1299,6 +1304,7 @@
"advancedSettings": "Configuración avanzada" "advancedSettings": "Configuración avanzada"
}, },
"messages.divider": "Separador de mensajes", "messages.divider": "Separador de mensajes",
"messages.divider.tooltip": "No aplicable para mensajes de estilo burbuja",
"messages.grid_columns": "Número de columnas en la cuadrícula de mensajes", "messages.grid_columns": "Número de columnas en la cuadrícula de mensajes",
"messages.grid_popover_trigger": "Desencadenante de detalles de cuadrícula", "messages.grid_popover_trigger": "Desencadenante de detalles de cuadrícula",
"messages.grid_popover_trigger.click": "Mostrar al hacer clic", "messages.grid_popover_trigger.click": "Mostrar al hacer clic",
+9 -3
View File
@@ -198,6 +198,7 @@
"topics.clear.title": "Effacer le message", "topics.clear.title": "Effacer le message",
"topics.copy.image": "Copier sous forme d'image", "topics.copy.image": "Copier sous forme d'image",
"topics.copy.md": "Copier sous forme de Markdown", "topics.copy.md": "Copier sous forme de Markdown",
"topics.copy.plain_text": "Copier en tant que texte brut (supprimer Markdown)",
"topics.copy.title": "Copier", "topics.copy.title": "Copier",
"topics.delete.shortcut": "Maintenez {{key}} pour supprimer directement", "topics.delete.shortcut": "Maintenez {{key}} pour supprimer directement",
"topics.edit.placeholder": "Entrez un nouveau nom", "topics.edit.placeholder": "Entrez un nouveau nom",
@@ -490,8 +491,12 @@
"urls": "URLs", "urls": "URLs",
"dimensions": "Размерность встраивания", "dimensions": "Размерность встраивания",
"dimensions_size_tooltip": "Размерность встраивания. Чем больше значение, тем выше размерность, но тем больше токенов требуется", "dimensions_size_tooltip": "Размерность встраивания. Чем больше значение, тем выше размерность, но тем больше токенов требуется",
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)", "dimensions_size_placeholder": " Taille de dimension d'incorporation, ex. 1024",
"dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})" "dimensions_auto_set": "Réglage automatique des dimensions d'incorporation",
"dimensions_error_invalid": "Veuillez saisir la taille de dimension d'incorporation",
"dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})",
"dimensions_set_right": "⚠️ Assurez-vous que le modèle prend en charge la taille de dimension d'incorporation définie",
"dimensions_default": "Le modèle utilisera les dimensions d'incorporation par défaut"
}, },
"languages": { "languages": {
"arabic": "Arabe", "arabic": "Arabe",
@@ -551,7 +556,6 @@
"error.enter.api.key": "Veuillez entrer votre clé API", "error.enter.api.key": "Veuillez entrer votre clé API",
"error.enter.model": "Veuillez sélectionner un modèle", "error.enter.model": "Veuillez sélectionner un modèle",
"error.enter.name": "Veuillez entrer le nom de la base de connaissances", "error.enter.name": "Veuillez entrer le nom de la base de connaissances",
"error.fetchTopicName": "Échec de la dénomination du sujet",
"error.get_embedding_dimensions": "Impossible d'obtenir les dimensions d'encodage", "error.get_embedding_dimensions": "Impossible d'obtenir les dimensions d'encodage",
"error.invalid.api.host": "Adresse API invalide", "error.invalid.api.host": "Adresse API invalide",
"error.invalid.api.key": "Clé API invalide", "error.invalid.api.key": "Clé API invalide",
@@ -836,6 +840,7 @@
"azure-openai": "Azure OpenAI", "azure-openai": "Azure OpenAI",
"baichuan": "BaiChuan", "baichuan": "BaiChuan",
"baidu-cloud": "Baidu Cloud Qianfan", "baidu-cloud": "Baidu Cloud Qianfan",
"cephalon": "Cephalon",
"copilot": "GitHub Copilote", "copilot": "GitHub Copilote",
"dashscope": "AliCloud BaiLian", "dashscope": "AliCloud BaiLian",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
@@ -1300,6 +1305,7 @@
"advancedSettings": "Расширенные настройки" "advancedSettings": "Расширенные настройки"
}, },
"messages.divider": "Séparateur de messages", "messages.divider": "Séparateur de messages",
"messages.divider.tooltip": "Non applicable aux messages de style bulle",
"messages.grid_columns": "Nombre de colonnes de la grille de messages", "messages.grid_columns": "Nombre de colonnes de la grille de messages",
"messages.grid_popover_trigger": "Déclencheur de popover de la grille", "messages.grid_popover_trigger": "Déclencheur de popover de la grille",
"messages.grid_popover_trigger.click": "Afficher au clic", "messages.grid_popover_trigger.click": "Afficher au clic",
+8 -3
View File
@@ -199,6 +199,7 @@
"topics.clear.title": "Limpar mensagens", "topics.clear.title": "Limpar mensagens",
"topics.copy.image": "Copiar como imagem", "topics.copy.image": "Copiar como imagem",
"topics.copy.md": "Copiar como Markdown", "topics.copy.md": "Copiar como Markdown",
"topics.copy.plain_text": "Copiar como texto simples (remover Markdown)",
"topics.copy.title": "Copiar", "topics.copy.title": "Copiar",
"topics.delete.shortcut": "Pressione {{key}} para deletar diretamente", "topics.delete.shortcut": "Pressione {{key}} para deletar diretamente",
"topics.edit.placeholder": "Digite novo nome", "topics.edit.placeholder": "Digite novo nome",
@@ -492,8 +493,12 @@
"urls": "URLs", "urls": "URLs",
"dimensions": "Dimensão de incorporação", "dimensions": "Dimensão de incorporação",
"dimensions_size_tooltip": "Tamanho da dimensão de incorporação, quanto maior o valor, maior a dimensão de incorporação, mas também maior o consumo de tokens", "dimensions_size_tooltip": "Tamanho da dimensão de incorporação, quanto maior o valor, maior a dimensão de incorporação, mas também maior o consumo de tokens",
"dimensions_size_placeholder": "Valor padrão (não recomendado alterar)", "dimensions_size_placeholder": " Tamanho da dimensão de incorporação, ex. 1024",
"dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})" "dimensions_auto_set": "Definição automática de dimensões de incorporação",
"dimensions_error_invalid": "Por favor insira o tamanho da dimensão de incorporação",
"dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})",
"dimensions_set_right": "⚠️ Certifique-se de que o modelo suporta o tamanho da dimensão de incorporação definido",
"dimensions_default": "O modelo utilizará as dimensões de incorporação padrão"
}, },
"languages": { "languages": {
"arabic": "Árabe", "arabic": "Árabe",
@@ -553,7 +558,6 @@
"error.enter.api.key": "Insira sua chave API", "error.enter.api.key": "Insira sua chave API",
"error.enter.model": "Selecione um modelo", "error.enter.model": "Selecione um modelo",
"error.enter.name": "Insira o nome da base de conhecimento", "error.enter.name": "Insira o nome da base de conhecimento",
"error.fetchTopicName": "Falha ao nomear o tópico",
"error.get_embedding_dimensions": "Falha ao obter dimensões de incorporação", "error.get_embedding_dimensions": "Falha ao obter dimensões de incorporação",
"error.invalid.api.host": "Endereço API inválido", "error.invalid.api.host": "Endereço API inválido",
"error.invalid.api.key": "Chave API inválida", "error.invalid.api.key": "Chave API inválida",
@@ -1302,6 +1306,7 @@
"advancedSettings": "Configurações Avançadas" "advancedSettings": "Configurações Avançadas"
}, },
"messages.divider": "Divisor de mensagens", "messages.divider": "Divisor de mensagens",
"messages.divider.tooltip": "Não aplicável a mensagens de estilo bolha",
"messages.grid_columns": "Número de colunas da grade de mensagens", "messages.grid_columns": "Número de colunas da grade de mensagens",
"messages.grid_popover_trigger": "Disparador de detalhes da grade", "messages.grid_popover_trigger": "Disparador de detalhes da grade",
"messages.grid_popover_trigger.click": "Clique para mostrar", "messages.grid_popover_trigger.click": "Clique para mostrar",
@@ -93,19 +93,15 @@ const Markdown: FC<Props> = ({ block }) => {
} as Partial<Components> } as Partial<Components>
}, [onSaveCodeBlock]) }, [onSaveCodeBlock])
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
const urlTransform = useCallback((value: string) => { const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
return defaultUrlTransform(value) return defaultUrlTransform(value)
}, []) }, [])
// if (role === 'user' && !renderInputMessageAsMarkdown) {
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
// }
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
return ( return (
<ReactMarkdown <ReactMarkdown
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
@@ -31,7 +31,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock }> = ({ block }) =>
} }
const Alert = styled(AntdAlert)` const Alert = styled(AntdAlert)`
margin: 15px 0 8px; margin: 0.5rem 0;
padding: 10px; padding: 10px;
font-size: 12px; font-size: 12px;
` `
@@ -151,7 +151,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
</Flex> </Flex>
)} )}
{role === 'user' && !renderInputMessageAsMarkdown ? ( {role === 'user' && !renderInputMessageAsMarkdown ? (
<p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}> <p className="markdown" style={{ whiteSpace: 'pre-wrap' }}>
{block.content} {block.content}
</p> </p>
) : ( ) : (
@@ -42,6 +42,7 @@ const blockWrapperVariants = {
const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => { const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => {
return ( return (
<motion.div <motion.div
className="block-wrapper"
variants={blockWrapperVariants} variants={blockWrapperVariants}
initial={enableAnimation ? 'hidden' : 'static'} initial={enableAnimation ? 'hidden' : 'static'}
animate={enableAnimation ? 'visible' : 'static'}> animate={enableAnimation ? 'visible' : 'static'}>
@@ -139,6 +139,7 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
<WebSearchCard> <WebSearchCard>
<ContextMenu> <ContextMenu>
<WebSearchCardHeader> <WebSearchCardHeader>
<CitationIndex>{citation.number}</CitationIndex>
{citation.showFavicon && citation.url && ( {citation.showFavicon && citation.url && (
<Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} /> <Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} />
)} )}
@@ -162,6 +163,7 @@ const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
<WebSearchCard> <WebSearchCard>
<ContextMenu> <ContextMenu>
<WebSearchCardHeader> <WebSearchCardHeader>
<CitationIndex>{citation.number}</CitationIndex>
{citation.showFavicon && <FileSearch width={16} />} {citation.showFavicon && <FileSearch width={16} />}
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}> <CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
{citation.title} {citation.title}
@@ -210,6 +212,13 @@ const PreviewIcon = styled.div`
} }
` `
const CitationIndex = styled.div`
font-size: 14px;
line-height: 1.6;
color: var(--color-text-2);
margin-right: 8px;
`
const CitationLink = styled.a` const CitationLink = styled.a`
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
@@ -21,7 +21,6 @@ import MessageEditor from './MessageEditor'
import MessageErrorBoundary from './MessageErrorBoundary' import MessageErrorBoundary from './MessageErrorBoundary'
import MessageHeader from './MessageHeader' import MessageHeader from './MessageHeader'
import MessageMenubar from './MessageMenubar' import MessageMenubar from './MessageMenubar'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
@@ -99,7 +98,7 @@ const MessageItem: FC<Props> = ({
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
const messageBorder = showMessageDivider ? undefined : 'none' const messageBorder = !isBubbleStyle && showMessageDivider ? '1px dotted var(--color-border)' : 'none'
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage) const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const messageHighlightHandler = useCallback((highlight: boolean = true) => { const messageHighlightHandler = useCallback((highlight: boolean = true) => {
@@ -130,22 +129,6 @@ const MessageItem: FC<Props> = ({
) )
} }
if (isEditing) {
return (
<MessageContainer style={{ paddingTop: 15 }}>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</MessageContainer>
)
}
return ( return (
<MessageContainer <MessageContainer
key={message.id} key={message.id}
@@ -155,35 +138,100 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage 'message-user': !isAssistantMessage
})} })}
ref={messageContainerRef} ref={messageContainerRef}
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}> style={{
<ContextMenu> ...style,
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} /> justifyContent: isBubbleStyle ? (isAssistantMessage ? 'flex-start' : 'flex-end') : undefined,
<MessageContentContainer flex: isBubbleStyle ? undefined : 1
className={ }}>
message.role === 'user' {isEditing && (
? 'message-content-container message-content-container-user' <ContextMenu
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{ style={{
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)', display: 'flex',
fontSize, flexDirection: 'column',
background: messageBackground, alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
overflowY: 'visible', width: isBubbleStyle ? '70%' : '100%'
maxWidth: narrowMode ? 760 : undefined
}}> }}>
<MessageErrorBoundary> <MessageHeader
<MessageContent message={message} /> message={message}
</MessageErrorBoundary> assistant={assistant}
{showMenubar && ( model={model}
key={getModelUniqId(model)}
index={index}
/>
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</ContextMenu>
)}
{!isEditing && (
<ContextMenu
style={{
display: 'flex',
flexDirection: 'column',
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
flex: 1,
maxWidth: '100%'
}}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
key={getModelUniqId(model)}
index={index}
/>
<MessageContentContainer
className={
message.role === 'user'
? 'message-content-container message-content-container-user'
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
overflowY: 'visible',
maxWidth: narrowMode ? 760 : undefined,
alignSelf: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined
}}>
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && !isBubbleStyle && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isLastMessage ? 'row-reverse' : undefined
}}>
<MessageMenubar
message={message}
assistant={assistant}
model={model}
index={index}
topic={topic}
isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel}
/>
</MessageFooter>
)}
</MessageContentContainer>
{showMenubar && isBubbleStyle && (
<MessageFooter <MessageFooter
className="MessageFooter" className="MessageFooter"
style={{ style={{
border: messageBorder, borderTop: messageBorder,
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined flexDirection: !isAssistantMessage ? 'row-reverse' : undefined
}}> }}>
<MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar <MessageMenubar
message={message} message={message}
assistant={assistant} assistant={assistant}
@@ -198,8 +246,8 @@ const MessageItem: FC<Props> = ({
/> />
</MessageFooter> </MessageFooter>
)} )}
</MessageContentContainer> </ContextMenu>
</ContextMenu> )}
</MessageContainer> </MessageContainer>
) )
} }
@@ -214,7 +262,7 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
flex-direction: column; width: 100%;
position: relative; position: relative;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
padding: 0 20px; padding: 0 20px;
@@ -257,12 +305,12 @@ const MessageFooter = styled.div`
align-items: center; align-items: center;
padding: 2px 0; padding: 2px 0;
margin-top: 2px; margin-top: 2px;
border-top: 1px dotted var(--color-border);
gap: 20px; gap: 20px;
` `
const NewContextMessage = styled.div` const NewContextMessage = styled.div`
cursor: pointer; cursor: pointer;
flex: 1;
` `
export default memo(MessageItem) export default memo(MessageItem)
@@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return ( return (
<> <>
{!isEmpty(message.mentions) && ( {!isEmpty(message.mentions) && (
<Flex gap="8px" wrap style={{ marginBottom: 10 }}> <Flex gap="8px" wrap>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)} {message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex> </Flex>
)} )}
@@ -180,7 +180,8 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
$isGrouped={isGrouped} $isGrouped={isGrouped}
key={message.id} key={message.id}
className={classNames({ className={classNames({
'group-message-wrapper': message.role === 'assistant' && isHorizontal && isGrouped, // 加个卡片布局
'group-message-wrapper': message.role === 'assistant' && (isHorizontal || isGrid) && isGrouped,
[multiModelMessageStyle]: isGrouped, [multiModelMessageStyle]: isGrouped,
selected: message.id === selectedMessageId selected: message.id === selectedMessageId
})}> })}>
@@ -315,6 +316,7 @@ interface MessageWrapperProps {
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>` const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
width: 100%; width: 100%;
display: flex;
&.horizontal { &.horizontal {
display: inline-block; display: inline-block;
@@ -17,10 +17,13 @@ import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
assistant: Assistant assistant: Assistant
model?: Model model?: Model
index: number | undefined
} }
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => { const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
@@ -28,7 +31,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
return modelId ? getModelLogo(modelId) : undefined return modelId ? getModelLogo(modelId) : undefined
} }
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => { const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { theme } = useTheme() const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings() const { userName, sidebarIcons } = useSettings()
@@ -52,9 +55,11 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp') const showMinappIcon = sidebarIcons.visible.includes('minapp')
const { showTokens } = useSettings()
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const isLastMessage = index === 0
const showMiniApp = useCallback(() => { const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && openMinappById(model.provider) showMinappIcon && model?.provider && openMinappById(model.provider)
@@ -111,7 +116,14 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
<UserName isBubbleStyle={isBubbleStyle} theme={theme}> <UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username} {username}
</UserName> </UserName>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime> <InfoWrap
style={{
flexDirection: !isAssistantMessage && isBubbleStyle ? 'row-reverse' : undefined
}}>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
{showTokens && <DividerContainer style={{ color: 'var(--color-text-3)' }}> | </DividerContainer>}
<MessageTokens message={message} isLastMessage={isLastMessage} />
</InfoWrap>
</UserWrap> </UserWrap>
</AvatarWrapper> </AvatarWrapper>
</Container> </Container>
@@ -140,6 +152,19 @@ const UserWrap = styled.div`
justify-content: space-between; justify-content: space-between;
` `
const InfoWrap = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
`
const DividerContainer = styled.div`
font-size: 10px;
color: var(--color-text-3);
margin: 0 2px;
`
const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>` const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>`
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
@@ -5,6 +5,7 @@ import { TranslateLanguageOptions } from '@renderer/config/translate'
import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { useMessageStyle } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle } from '@renderer/services/MessagesService' import { getMessageTitle } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
@@ -22,6 +23,7 @@ import {
exportMessageToNotion, exportMessageToNotion,
messageToMarkdown messageToMarkdown
} from '@renderer/utils/export' } from '@renderer/utils/export'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
// import { withMessageThought } from '@renderer/utils/formats' // import { withMessageThought } from '@renderer/utils/formats'
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown' import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
@@ -66,6 +68,9 @@ const MessageMenubar: FC<Props> = (props) => {
appendAssistantResponse, appendAssistantResponse,
removeMessageBlock removeMessageBlock
} = useMessageOperations(topic) } = useMessageOperations(topic)
const { isBubbleStyle } = useMessageStyle()
const loading = useTopicLoading(topic) const loading = useTopicLoading(topic)
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
@@ -197,6 +202,11 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'export', key: 'export',
icon: <Share size={16} color="var(--color-icon)" style={{ marginTop: 3 }} />, icon: <Share size={16} color="var(--color-icon)" style={{ marginTop: 3 }} />,
children: [ children: [
{
label: t('chat.topics.copy.plain_text'),
key: 'copy_message_plain_text',
onClick: () => copyMessageAsPlainText(message)
},
exportMenuOptions.image && { exportMenuOptions.image && {
label: t('chat.topics.copy.image'), label: t('chat.topics.copy.image'),
key: 'img', key: 'img',
@@ -332,24 +342,29 @@ const MessageMenubar: FC<Props> = (props) => {
return translationBlocks.length > 0 return translationBlocks.length > 0
}, [message]) }, [message])
const softHoverBg = isBubbleStyle && !isLastMessage
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}> <Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={() => handleResendUserMessage()}> <ActionButton
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<SyncOutlined /> <SyncOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}> <Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit}> <ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
<EditOutlined /> <EditOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}> <Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy}> <ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
{!copied && <Copy size={16} />} {!copied && <Copy size={16} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />} {copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton> </ActionButton>
@@ -366,7 +381,7 @@ const MessageMenubar: FC<Props> = (props) => {
mouseEnterDelay={0.8} mouseEnterDelay={0.8}
open={showRegenerateTooltip} open={showRegenerateTooltip}
onOpenChange={setShowRegenerateTooltip}> onOpenChange={setShowRegenerateTooltip}>
<ActionButton className="message-action-button"> <ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
<RefreshCw size={16} /> <RefreshCw size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@@ -374,7 +389,7 @@ const MessageMenubar: FC<Props> = (props) => {
)} )}
{isAssistantMessage && ( {isAssistantMessage && (
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}> <Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onMentionModel}> <ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
<AtSign size={16} /> <AtSign size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@@ -444,7 +459,10 @@ const MessageMenubar: FC<Props> = (props) => {
placement="top" placement="top"
arrow> arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}> <Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}> <ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Languages size={16} /> <Languages size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@@ -452,7 +470,7 @@ const MessageMenubar: FC<Props> = (props) => {
)} )}
{isAssistantMessage && isGrouped && ( {isAssistantMessage && isGrouped && (
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}> <Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful}> <ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
{message.useful ? ( {message.useful ? (
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} /> <ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
) : ( ) : (
@@ -467,7 +485,7 @@ const MessageMenubar: FC<Props> = (props) => {
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onOpenChange={(open) => open && setShowDeleteTooltip(false)} onOpenChange={(open) => open && setShowDeleteTooltip(false)}
onConfirm={() => deleteMessage(message.id)}> onConfirm={() => deleteMessage(message.id)}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}> <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()} $softHoverBg={softHoverBg}>
<Tooltip <Tooltip
title={t('common.delete')} title={t('common.delete')}
mouseEnterDelay={1} mouseEnterDelay={1}
@@ -483,7 +501,10 @@ const MessageMenubar: FC<Props> = (props) => {
trigger={['click']} trigger={['click']}
placement="topRight" placement="topRight"
arrow> arrow>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}> <ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Menu size={19} /> <Menu size={19} />
</ActionButton> </ActionButton>
</Dropdown> </Dropdown>
@@ -500,7 +521,7 @@ const MenusBar = styled.div`
gap: 6px; gap: 6px;
` `
const ActionButton = styled.div` const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
@@ -511,8 +532,11 @@ const ActionButton = styled.div`
height: 30px; height: 30px;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: var(--color-background-mute); background-color: ${(props) =>
.anticon { props.$softHoverBg ? 'var(--color-background-soft)' : 'var(--color-background-mute)'};
color: var(--color-text-1);
.anticon,
.lucide {
color: var(--color-text-1); color: var(--color-text-1);
} }
} }
@@ -522,9 +546,6 @@ const ActionButton = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-icon); color: var(--color-icon);
} }
&:hover {
color: var(--color-text-1);
}
.icon-at { .icon-at {
font-size: 16px; font-size: 16px;
} }
@@ -69,19 +69,14 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
} }
const MessageMetadata = styled.div` const MessageMetadata = styled.div`
font-size: 11px; font-size: 10px;
color: var(--color-text-2); color: var(--color-text-3);
user-select: text; user-select: text;
margin: 2px 0;
cursor: pointer; cursor: pointer;
text-align: right; text-align: right;
.tokens { .tokens span {
display: block; padding: 0 2px;
span {
padding: 0 2px;
}
} }
` `
@@ -318,7 +318,12 @@ const SettingsTab: FC<Props> = (props) => {
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall> <SettingRowTitleSmall>
{t('settings.messages.divider')}
<Tooltip title={t('settings.messages.divider.tooltip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch <Switch
size="small" size="small"
checked={showMessageDivider} checked={showMessageDivider}
@@ -26,7 +26,7 @@ import { RootState } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { removeSpecialCharactersForFileName } from '@renderer/utils' import { removeSpecialCharactersForFileName } from '@renderer/utils'
import { copyTopicAsMarkdown } from '@renderer/utils/copy' import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
import { import {
exportMarkdownToJoplin, exportMarkdownToJoplin,
exportMarkdownToSiyuan, exportMarkdownToSiyuan,
@@ -280,6 +280,11 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.copy.md'), label: t('chat.topics.copy.md'),
key: 'md', key: 'md',
onClick: () => copyTopicAsMarkdown(topic) onClick: () => copyTopicAsMarkdown(topic)
},
{
label: t('chat.topics.copy.plain_text'),
key: 'plain_text',
onClick: () => copyTopicAsPlainText(topic)
} }
] ]
}, },
@@ -9,9 +9,9 @@ import { SettingHelpText } from '@renderer/pages/settings'
import AiProvider from '@renderer/providers/AiProvider' import AiProvider from '@renderer/providers/AiProvider'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService' import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { KnowledgeBase, Model } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils/error' import { getErrorMessage } from '@renderer/utils/error'
import { Form, Input, Modal, Select, Slider } from 'antd' import { Flex, Form, Input, InputNumber, Modal, Select, Slider, Switch } from 'antd'
import { find, sortBy } from 'lodash' import { find, sortBy } from 'lodash'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { useMemo, useRef, useState } from 'react' import { useMemo, useRef, useState } from 'react'
@@ -24,6 +24,8 @@ interface ShowParams {
interface FormData { interface FormData {
name: string name: string
model: string model: string
autoDims: boolean | undefined
dimensions: number | undefined
rerankModel: string | undefined rerankModel: string | undefined
documentCount: number | undefined documentCount: number | undefined
} }
@@ -35,6 +37,7 @@ interface Props extends ShowParams {
const PopupContainer: React.FC<Props> = ({ title, resolve }) => { const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [autoDims, setAutoDims] = useState(true)
const [form] = Form.useForm<FormData>() const [form] = Form.useForm<FormData>()
const { t } = useTranslation() const { t } = useTranslation()
const { providers } = useProviders() const { providers } = useProviders()
@@ -67,7 +70,8 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
.map((m) => ({ .map((m) => ({
label: m.name, label: m.name,
value: getModelUniqId(m), value: getModelUniqId(m),
key: `${p.id}-${m.id}` providerId: p.id,
modelId: m.id
})) }))
})) }))
.filter((group) => group.options.length > 0) .filter((group) => group.options.length > 0)
@@ -107,24 +111,27 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
return return
} }
const aiProvider = new AiProvider(provider) if (autoDims || typeof values.dimensions === 'undefined') {
let dimensions = 0 try {
const aiProvider = new AiProvider(provider)
try { values.dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel)
dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel) } catch (error) {
} catch (error) { console.error('Error getting embedding dimensions:', error)
console.error('Error getting embedding dimensions:', error) window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error))
window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error)) setLoading(false)
setLoading(false) return
return }
} else if (typeof values.dimensions === 'string') {
// 按理来说不应该是string的,但是确实是string
values.dimensions = parseInt(values.dimensions)
} }
const newBase = { const newBase: KnowledgeBase = {
id: nanoid(), id: nanoid(),
name: values.name, name: values.name,
model: selectedEmbeddingModel, model: selectedEmbeddingModel,
rerankModel: selectedRerankModel, rerankModel: selectedRerankModel,
dimensions, dimensions: values.dimensions,
documentCount: values.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, documentCount: values.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT,
items: [], items: [],
created_at: Date.now(), created_at: Date.now(),
@@ -134,7 +141,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
await window.api.knowledgeBase.create(getKnowledgeBaseParams(newBase)) await window.api.knowledgeBase.create(getKnowledgeBaseParams(newBase))
addKnowledgeBase(newBase as any) addKnowledgeBase(newBase)
setOpen(false) setOpen(false)
resolve(newBase) resolve(newBase)
} }
@@ -203,11 +210,59 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }} marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }}
/> />
</Form.Item> </Form.Item>
<Form.Item
name="autoDims"
colon={false}
initialValue={true}
layout="horizontal"
label={t('knowledge.dimensions_auto_set')}
tooltip={t('knowledge.dimensions_default')}
style={{ marginBottom: 0, justifyContent: 'space-between' }}>
<Flex justify="flex-end" style={{ marginBottom: '1rem' }}>
<Switch
checked={autoDims}
onClick={() => {
form.setFieldValue('autoDims', !autoDims)
if (!autoDims) {
form.validateFields(['dimensions'])
}
setAutoDims(!autoDims)
}}></Switch>
</Flex>
</Form.Item>
<Form.Item
name="dimensions"
colon={false}
layout="horizontal"
initialValue={undefined}
label={t('knowledge.dimensions')}
tooltip={{ title: t('knowledge.dimensions_size_tooltip') }}
dependencies={['model']}
style={{ display: autoDims ? 'none' : 'block' }}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
if (getFieldValue('autoDims') || value > 0) {
return Promise.resolve()
} else {
return Promise.reject(t('knowledge.dimensions_error_invalid'))
}
}
})
]}>
<InputNumber min={1} style={{ width: '100%' }} placeholder={t('knowledge.dimensions_size_placeholder')} />
</Form.Item>
{!autoDims && (
<SettingHelpText style={{ marginTop: -15, marginBottom: 20 }}>
{t('knowledge.dimensions_set_right')}
</SettingHelpText>
)}
</Form> </Form>
</Modal> </Modal>
) )
} }
export default class AddKnowledgePopup { export default class AddKnowledgePopup {
static hide() { static hide() {
TopView.hide('AddKnowledgePopup') TopView.hide('AddKnowledgePopup')
@@ -187,32 +187,6 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
</AdvancedSettingsButton> </AdvancedSettingsButton>
<div style={{ display: showAdvanced ? 'block' : 'none' }}> <div style={{ display: showAdvanced ? 'block' : 'none' }}>
<Form.Item
name="dimensions"
label={t('knowledge.dimensions')}
layout="horizontal"
initialValue={base.dimensions}
tooltip={{ title: t('knowledge.dimensions_size_tooltip') }}
rules={[
{
validator(_, value) {
const maxContext = getEmbeddingMaxContext(base.model.id)
if (value && maxContext && value > maxContext) {
return Promise.reject(
new Error(t('knowledge.dimensions_size_too_large', { max_context: maxContext }))
)
}
return Promise.resolve()
}
}
]}>
<InputNumber
style={{ width: '100%' }}
defaultValue={base.dimensions}
placeholder={t('knowledge.dimensions_size_placeholder')}
disabled={base.model.id !== 'voyage-3-large'}
/>
</Form.Item>
<Form.Item <Form.Item
name="chunkSize" name="chunkSize"
label={t('knowledge.chunk_size')} label={t('knowledge.chunk_size')}
@@ -1,4 +1,5 @@
import { SyncOutlined } from '@ant-design/icons' import { SyncOutlined } from '@ant-design/icons'
import CodeEditor from '@renderer/components/CodeEditor'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant' import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
@@ -16,7 +17,7 @@ import {
setSidebarIcons setSidebarIcons
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { Button, ColorPicker, Input, Segmented, Switch } from 'antd' import { Button, ColorPicker, Segmented, Switch } from 'antd'
import { Minus, Plus, RotateCcw } from 'lucide-react' import { Minus, Plus, RotateCcw } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -307,17 +308,24 @@ const DisplaySettings: FC = () => {
</TitleExtra> </TitleExtra>
</SettingTitle> </SettingTitle>
<SettingDivider /> <SettingDivider />
<Input.TextArea <CodeEditor
value={customCss} value={customCss}
onChange={(e) => { language="css"
dispatch(setCustomCss(e.target.value))
}}
placeholder={t('settings.display.custom.css.placeholder')} placeholder={t('settings.display.custom.css.placeholder')}
style={{ onChange={(value) => dispatch(setCustomCss(value))}
minHeight: 200, height="350px"
fontFamily: 'monospace' options={{
collapsible: true,
wrappable: true,
autocompletion: true,
lineNumbers: true,
foldGutter: true,
keymap: true
}}
style={{
outline: '0.5px solid var(--color-border)',
borderRadius: '5px'
}} }}
spellCheck={false}
/> />
</SettingGroup> </SettingGroup>
</SettingContainer> </SettingContainer>
@@ -7,6 +7,7 @@ import { Button, Radio, Row, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, Edit2 } from 'lucide-react' import { CircleHelp, Edit2 } from 'lucide-react'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import { import {
@@ -111,8 +112,25 @@ const SelectionAssistantSettings: FC = () => {
value={triggerMode} value={triggerMode}
onChange={(e) => setTriggerMode(e.target.value as TriggerMode)} onChange={(e) => setTriggerMode(e.target.value as TriggerMode)}
buttonStyle="solid"> buttonStyle="solid">
<Radio.Button value="selected">{t('selection.settings.toolbar.trigger_mode.selected')}</Radio.Button> <Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.selected_note')} arrow>
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button> <Radio.Button value="selected">{t('selection.settings.toolbar.trigger_mode.selected')}</Radio.Button>
</Tooltip>
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow>
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button>
</Tooltip>
<Tooltip
placement="topRight"
title={
<div>
{t('selection.settings.toolbar.trigger_mode.shortcut_note')}
<Link to="/settings/shortcut" style={{ color: 'var(--color-primary)' }}>
{t('selection.settings.toolbar.trigger_mode.shortcut_link')}
</Link>
</div>
}
arrow>
<Radio.Button value="shortcut">{t('selection.settings.toolbar.trigger_mode.shortcut')}</Radio.Button>
</Tooltip>
</Radio.Group> </Radio.Group>
</SettingRow> </SettingRow>
@@ -18,10 +18,18 @@ const ShortcutSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { shortcuts } = useShortcuts() const { shortcuts: originalShortcuts } = useShortcuts()
const inputRefs = useRef<Record<string, InputRef>>({}) const inputRefs = useRef<Record<string, InputRef>>({})
const [editingKey, setEditingKey] = useState<string | null>(null) const [editingKey, setEditingKey] = useState<string | null>(null)
//if shortcut is not available on all the platforms, block the shortcut here
let shortcuts = originalShortcuts
if (!isWindows) {
//Selection Assistant only available on Windows now
const excludedShortcuts = ['selection_assistant_toggle', 'selection_assistant_select_text']
shortcuts = shortcuts.filter((s) => !excludedShortcuts.includes(s.key))
}
const handleClear = (record: Shortcut) => { const handleClear = (record: Shortcut) => {
dispatch( dispatch(
updateShortcut({ updateShortcut({
@@ -215,6 +215,7 @@ const TranslatePage: FC = () => {
const [bidirectionalPair, setBidirectionalPair] = useState<[string, string]>(['english', 'chinese']) const [bidirectionalPair, setBidirectionalPair] = useState<[string, string]>(['english', 'chinese'])
const [settingsVisible, setSettingsVisible] = useState(false) const [settingsVisible, setSettingsVisible] = useState(false)
const [detectedLanguage, setDetectedLanguage] = useState<string | null>(null) const [detectedLanguage, setDetectedLanguage] = useState<string | null>(null)
const [sourceLanguage, setSourceLanguage] = useState<string>('auto') // 添加用户选择的源语言状态
const contentContainerRef = useRef<HTMLDivElement>(null) const contentContainerRef = useRef<HTMLDivElement>(null)
const textAreaRef = useRef<TextAreaRef>(null) const textAreaRef = useRef<TextAreaRef>(null)
const outputTextRef = useRef<HTMLDivElement>(null) const outputTextRef = useRef<HTMLDivElement>(null)
@@ -288,10 +289,17 @@ const TranslatePage: FC = () => {
setLoading(true) setLoading(true)
try { try {
const sourceLanguage = await detectLanguage(text) // 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
console.log('检测到的语言:', sourceLanguage) let actualSourceLanguage: string
setDetectedLanguage(sourceLanguage) if (sourceLanguage === 'auto') {
const result = determineTargetLanguage(sourceLanguage, targetLanguage, isBidirectional, bidirectionalPair) actualSourceLanguage = await detectLanguage(text)
console.log('检测到的语言:', actualSourceLanguage)
setDetectedLanguage(actualSourceLanguage) // 更新检测到的语言
} else {
actualSourceLanguage = sourceLanguage
}
const result = determineTargetLanguage(actualSourceLanguage, targetLanguage, isBidirectional, bidirectionalPair)
if (!result.success) { if (!result.success) {
let errorMessage = '' let errorMessage = ''
if (result.errorType === 'same_language') { if (result.errorType === 'same_language') {
@@ -324,7 +332,7 @@ const TranslatePage: FC = () => {
} }
}) })
await saveTranslateHistory(text, translatedText, sourceLanguage, actualTargetLanguage) await saveTranslateHistory(text, translatedText, actualSourceLanguage, actualTargetLanguage)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
console.error('Translation error:', error) console.error('Translation error:', error)
@@ -498,15 +506,28 @@ const TranslatePage: FC = () => {
<Flex align="center" gap={20}> <Flex align="center" gap={20}>
<Select <Select
showSearch showSearch
value="auto" value={sourceLanguage}
style={{ width: 180 }} style={{ width: 180 }}
optionFilterProp="label" optionFilterProp="label"
disabled onChange={(value) => setSourceLanguage(value)}
options={[ options={[
{ {
label: detectedLanguage ? t(`languages.${detectedLanguage}`) : t('translate.detected.language'), value: 'auto',
value: 'auto' label: detectedLanguage
} ? `${t('translate.detected.language')}(${t(`languages.${detectedLanguage.toLowerCase()}`)})`
: t('translate.detected.language')
},
...translateLanguageOptions().map((lang) => ({
value: lang.value,
label: (
<Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji}
</span>
<Space.Compact block>{lang.label}</Space.Compact>
</Space.Compact>
)
}))
]} ]}
/> />
<Button <Button
@@ -56,6 +56,7 @@ export default abstract class BaseProvider {
abstract models(): Promise<OpenAI.Models.Model[]> abstract models(): Promise<OpenAI.Models.Model[]>
abstract generateImage(params: GenerateImageParams): Promise<string[]> abstract generateImage(params: GenerateImageParams): Promise<string[]>
abstract generateImageByChat({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> abstract generateImageByChat({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
// 由于现在出现了一些能够选择嵌入维度的嵌入模型,这个不考虑dimensions参数的方法将只能应用于那些不支持dimensions的模型
abstract getEmbeddingDimensions(model: Model): Promise<number> abstract getEmbeddingDimensions(model: Model): Promise<number>
public abstract convertMcpTools<T>(mcpTools: MCPTool[]): T[] public abstract convertMcpTools<T>(mcpTools: MCPTool[]): T[]
public abstract mcpToolCallResponseToMessage( public abstract mcpToolCallResponseToMessage(
@@ -1021,20 +1021,14 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
await this.checkIsCopilot() await this.checkIsCopilot()
const params = { // @ts-ignore key is not typed
const response = await this.sdk.chat.completions.create({
model: model.id, model: model.id,
messages: [systemMessage, userMessage] as ChatCompletionMessageParam[], messages: [systemMessage, userMessage] as ChatCompletionMessageParam[],
stream: false, stream: false,
keep_alive: this.keepAliveTime, keep_alive: this.keepAliveTime,
max_tokens: 1000 max_tokens: 1000
} })
if (isSupportedThinkingTokenQwenModel(model)) {
params['enable_thinking'] = false
}
// @ts-ignore key is not typed
const response = await this.sdk.chat.completions.create(params as ChatCompletionCreateParamsNonStreaming)
// 针对思考类模型的返回,总结仅截取</think>之后的内容 // 针对思考类模型的返回,总结仅截取</think>之后的内容
let content = response.choices[0].message?.content || '' let content = response.choices[0].message?.content || ''
@@ -2,7 +2,6 @@ import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant' import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant'
import { getEmbeddingMaxContext } from '@renderer/config/embedings' import { getEmbeddingMaxContext } from '@renderer/config/embedings'
import Logger from '@renderer/config/logger' import Logger from '@renderer/config/logger'
import { ONLY_SUPPORTED_DIMENSION_PROVIDERS } from '@renderer/config/providers'
import AiProvider from '@renderer/providers/AiProvider' import AiProvider from '@renderer/providers/AiProvider'
import store from '@renderer/store' import store from '@renderer/store'
import { FileType, KnowledgeBase, KnowledgeBaseParams, KnowledgeReference } from '@renderer/types' import { FileType, KnowledgeBase, KnowledgeBaseParams, KnowledgeReference } from '@renderer/types'
@@ -39,7 +38,8 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
return { return {
id: base.id, id: base.id,
model: base.model.id, model: base.model.id,
dimensions: ONLY_SUPPORTED_DIMENSION_PROVIDERS.includes(base.model.provider) ? base.dimensions : undefined, provider: base.model.provider,
dimensions: base.dimensions,
apiKey: aiProvider.getApiKey() || 'secret', apiKey: aiProvider.getApiKey() || 'secret',
apiVersion: provider.apiVersion, apiVersion: provider.apiVersion,
baseURL: host, baseURL: host,
@@ -230,8 +230,6 @@ export async function getMessageTitle(message: Message, length = 30): Promise<st
if (title) { if (title) {
window.message.success({ content: t('chat.topics.export.title_naming_success'), key: 'message-title-naming' }) window.message.success({ content: t('chat.topics.export.title_naming_success'), key: 'message-title-naming' })
return title return title
} else {
window.message?.error(t('message.error.fetchTopicName'))
} }
} catch (e) { } catch (e) {
window.message.error({ content: t('chat.topics.export.title_naming_failed'), key: 'message-title-naming' }) window.message.error({ content: t('chat.topics.export.title_naming_failed'), key: 'message-title-naming' })
+1 -1
View File
@@ -50,7 +50,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 111, version: 112,
blacklist: ['runtime', 'messages', 'messageBlocks'], blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate migrate
}, },
+14 -4
View File
@@ -127,12 +127,22 @@ export const INITIAL_PROVIDERS: Provider[] = [
enabled: false enabled: false
}, },
{ {
id: 'o3', id: '302ai',
name: 'O3', name: '302.AI',
type: 'openai', type: 'openai',
apiKey: '', apiKey: '',
apiHost: 'https://api.o3.fan', apiHost: 'https://api.302.ai',
models: SYSTEM_MODELS.o3, models: SYSTEM_MODELS['302ai'],
isSystem: true,
enabled: false
},
{
id: 'cephalon',
name: 'Cephalon',
type: 'openai',
apiKey: '',
apiHost: 'https://cephalon.cloud/user-center/v1/model',
models: SYSTEM_MODELS.cephalon,
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
+57
View File
@@ -16,6 +16,7 @@ import { INITIAL_PROVIDERS, moveProvider } from './llm'
import { mcpSlice } from './mcp' import { mcpSlice } from './mcp'
import { defaultActionItems } from './selectionStore' import { defaultActionItems } from './selectionStore'
import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings' import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings'
import { initialState as shortcutsInitialState } from './shortcuts'
import { defaultWebSearchProviders } from './websearch' import { defaultWebSearchProviders } from './websearch'
// remove logo base64 data to reduce the size of the state // remove logo base64 data to reduce the size of the state
@@ -89,6 +90,48 @@ function addSelectionAction(state: RootState, id: string) {
} }
} }
/**
* Add shortcuts(ids from shortcutsInitialState) after the shortcut(afterId)
* if afterId is 'first', add to the first
* if afterId is 'last', add to the last
*/
function addShortcuts(state: RootState, ids: string[], afterId: string) {
const defaultShortcuts = shortcutsInitialState.shortcuts
// 确保 state.shortcuts 存在
if (!state.shortcuts) {
return
}
// 从 defaultShortcuts 中找到要添加的快捷键
const shortcutsToAdd = defaultShortcuts.filter((shortcut) => ids.includes(shortcut.key))
// 过滤掉已经存在的快捷键
const existingKeys = state.shortcuts.shortcuts.map((s) => s.key)
const newShortcuts = shortcutsToAdd.filter((shortcut) => !existingKeys.includes(shortcut.key))
if (newShortcuts.length === 0) {
return
}
if (afterId === 'first') {
// 添加到最前面
state.shortcuts.shortcuts.unshift(...newShortcuts)
} else if (afterId === 'last') {
// 添加到最后面
state.shortcuts.shortcuts.push(...newShortcuts)
} else {
// 添加到指定快捷键后面
const afterIndex = state.shortcuts.shortcuts.findIndex((shortcut) => shortcut.key === afterId)
if (afterIndex !== -1) {
state.shortcuts.shortcuts.splice(afterIndex + 1, 0, ...newShortcuts)
} else {
// 如果找不到指定的快捷键,则添加到最后
state.shortcuts.shortcuts.push(...newShortcuts)
}
}
}
const migrateConfig = { const migrateConfig = {
'2': (state: RootState) => { '2': (state: RootState) => {
try { try {
@@ -1508,6 +1551,20 @@ const migrateConfig = {
state.llm.translateModel = SYSTEM_MODELS.defaultModel[2] state.llm.translateModel = SYSTEM_MODELS.defaultModel[2]
} }
// add selection_assistant_toggle and selection_assistant_select_text shortcuts after mini_window
addShortcuts(state, ['selection_assistant_toggle', 'selection_assistant_select_text'], 'mini_window')
return state
} catch (error) {
return state
}
},
'112': (state: RootState) => {
try {
addProvider(state, 'cephalon')
addProvider(state, '302ai')
state.llm.providers = moveProvider(state.llm.providers, 'cephalon', 13)
state.llm.providers = moveProvider(state.llm.providers, '302ai', 14)
return state return state
} catch (error) { } catch (error) {
return state return state
+17
View File
@@ -31,6 +31,22 @@ const initialState: ShortcutsState = {
enabled: false, enabled: false,
system: true system: true
}, },
{
//enable/disable selection assistant
key: 'selection_assistant_toggle',
shortcut: [],
editable: true,
enabled: false,
system: true
},
{
//to select text with selection assistant
key: 'selection_assistant_select_text',
shortcut: [],
editable: true,
enabled: false,
system: true
},
{ {
key: 'new_topic', key: 'new_topic',
shortcut: [isMac ? 'Command' : 'Ctrl', 'N'], shortcut: [isMac ? 'Command' : 'Ctrl', 'N'],
@@ -45,6 +61,7 @@ const initialState: ShortcutsState = {
enabled: true, enabled: true,
system: false system: false
}, },
{ {
key: 'toggle_show_topics', key: 'toggle_show_topics',
shortcut: [isMac ? 'Command' : 'Ctrl', ']'], shortcut: [isMac ? 'Command' : 'Ctrl', ']'],
+2
View File
@@ -423,6 +423,7 @@ export interface KnowledgeBase {
export type KnowledgeBaseParams = { export type KnowledgeBaseParams = {
id: string id: string
model: string model: string
provider: string
dimensions?: number dimensions?: number
apiKey: string apiKey: string
apiVersion?: string apiVersion?: string
@@ -704,3 +705,4 @@ export interface StoreSyncAction {
export type OpenAISummaryText = 'auto' | 'concise' | 'detailed' | 'off' export type OpenAISummaryText = 'auto' | 'concise' | 'detailed' | 'off'
export type OpenAIServiceTier = 'auto' | 'default' | 'flex' export type OpenAIServiceTier = 'auto' | 'default' | 'flex'
export type { Message } from './newMessage'
+1 -1
View File
@@ -1,4 +1,4 @@
export type TriggerMode = 'selected' | 'ctrlkey' export type TriggerMode = 'selected' | 'ctrlkey' | 'shortcut'
export type FilterMode = 'default' | 'whitelist' | 'blacklist' export type FilterMode = 'default' | 'whitelist' | 'blacklist'
export interface ActionItem { export interface ActionItem {
id: string id: string
+243 -2
View File
@@ -1,7 +1,7 @@
// Import Message, MessageBlock, and necessary enums // Import Message, MessageBlock, and necessary enums
import type { Message, MessageBlock } from '@renderer/types/newMessage' import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// --- Mocks Setup --- // --- Mocks Setup ---
@@ -36,8 +36,36 @@ vi.mock('@renderer/utils/messageUtils/find', () => ({
}) })
})) }))
vi.mock('@renderer/databases', () => ({
default: {
topics: {
get: vi.fn()
}
}
}))
vi.mock('@renderer/utils/markdown', async (importOriginal) => {
const actual = await importOriginal()
return {
...(actual as any),
markdownToPlainText: vi.fn((str) => str) // Simple pass-through for testing export logic
}
})
// Import the functions to test AFTER setting up mocks // Import the functions to test AFTER setting up mocks
import { getTitleFromString, messagesToMarkdown, messageToMarkdown, messageToMarkdownWithReasoning } from '../export' import db from '@renderer/databases'
import { Topic } from '@renderer/types'
import { markdownToPlainText } from '@renderer/utils/markdown'
import { copyMessageAsPlainText } from '../copy'
import {
getTitleFromString,
messagesToMarkdown,
messageToMarkdown,
messageToMarkdownWithReasoning,
messageToPlainText,
topicToPlainText
} from '../export'
// --- Helper Functions for Test Data --- // --- Helper Functions for Test Data ---
@@ -135,6 +163,13 @@ beforeEach(() => {
vi.resetModules() vi.resetModules()
vi.clearAllMocks() vi.clearAllMocks()
// Mock i18next translation function
vi.mock('i18next', () => ({
default: {
t: vi.fn((key) => key)
}
}))
// Mock store - primarily for settings // Mock store - primarily for settings
vi.doMock('@renderer/store', () => ({ vi.doMock('@renderer/store', () => ({
default: { default: {
@@ -344,4 +379,210 @@ describe('export', () => {
expect(markdown.split('\n\n---\n\n').length).toBe(1) expect(markdown.split('\n\n---\n\n').length).toBe(1)
}) })
}) })
describe('formatMessageAsPlainText (via topicToPlainText)', () => {
it('should format user and assistant messages correctly to plain text with roles', async () => {
const userMsg = createMessage({ role: 'user', id: 'u_plain_formatted' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '# User Content Formatted' }
])
const assistantMsg = createMessage({ role: 'assistant', id: 'a_plain_formatted' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '*Assistant Content Formatted*' }
])
const testTopic: Topic = {
id: 't_plain_formatted',
name: 'Formatted Plain Topic',
assistantId: 'asst_test_formatted',
messages: [userMsg, assistantMsg] as any,
createdAt: '',
updatedAt: ''
}
;(db.topics.get as any).mockResolvedValue({ messages: [userMsg, assistantMsg] })
// Specific mock for this test to check formatting
;(markdownToPlainText as any).mockImplementation((str) => str.replace(/[#*]/g, ''))
const plainText = await topicToPlainText(testTopic)
expect(plainText).toContain('User:\nUser Content Formatted')
expect(plainText).toContain('Assistant:\nAssistant Content Formatted')
expect(markdownToPlainText).toHaveBeenCalledWith('# User Content Formatted')
expect(markdownToPlainText).toHaveBeenCalledWith('*Assistant Content Formatted*')
expect(markdownToPlainText).toHaveBeenCalledWith('Formatted Plain Topic')
})
})
describe('messageToPlainText', () => {
it('should convert a single message content to plain text without role prefix', () => {
const testMessage = createMessage({ role: 'user', id: 'single_msg_plain' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '### Single Message Content' }
])
;(markdownToPlainText as any).mockImplementation((str) => str.replace(/[#*_]/g, ''))
const result = messageToPlainText(testMessage)
expect(result).toBe('Single Message Content')
expect(markdownToPlainText).toHaveBeenCalledWith('### Single Message Content')
})
it('should return empty string for message with no main text', () => {
const testMessage = createMessage({ role: 'user', id: 'empty_msg_plain' }, [])
;(markdownToPlainText as any).mockReturnValue('') // Mock to return empty for empty input
const result = messageToPlainText(testMessage)
expect(result).toBe('')
expect(markdownToPlainText).toHaveBeenCalledWith('')
})
})
describe('messagesToPlainText (via topicToPlainText)', () => {
it('should join multiple formatted plain text messages with double newlines', async () => {
const msg1 = createMessage({ role: 'user', id: 'm_plain1_formatted' }, [
{ type: MessageBlockType.MAIN_TEXT, content: 'Msg1 Formatted' }
])
const msg2 = createMessage({ role: 'assistant', id: 'm_plain2_formatted' }, [
{ type: MessageBlockType.MAIN_TEXT, content: 'Msg2 Formatted' }
])
const testTopic: Topic = {
id: 't_multi_plain_formatted',
name: 'Multi Plain Formatted',
assistantId: 'asst_test_multi_formatted',
messages: [msg1, msg2] as any,
createdAt: '',
updatedAt: ''
}
;(db.topics.get as any).mockResolvedValue({ messages: [msg1, msg2] })
;(markdownToPlainText as any).mockImplementation((str) => str) // Pass-through
const plainText = await topicToPlainText(testTopic)
expect(plainText).toBe('Multi Plain Formatted\n\nUser:\nMsg1 Formatted\n\nAssistant:\nMsg2 Formatted')
})
})
describe('topicToPlainText', () => {
beforeEach(() => {
vi.clearAllMocks() // Clear mocks before each test in this suite
// Mock store for settings if not already done globally or if specific settings are needed
vi.doMock('@renderer/store', () => ({
default: {
getState: () => ({
settings: { forceDollarMathInMarkdown: false } // Default or specific settings
})
}
}))
})
it('should return plain text for a topic with messages', async () => {
const msg1 = createMessage({ role: 'user', id: 'tp_u1' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '**Hello**' }
])
const msg2 = createMessage({ role: 'assistant', id: 'tp_a1' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '_World_' }
])
const testTopic: Topic = {
id: 'topic1_plain',
name: '# Topic One',
assistantId: 'asst_test',
messages: [msg1, msg2] as any,
createdAt: '',
updatedAt: ''
}
;(db.topics.get as any).mockResolvedValue({ messages: [msg1, msg2] })
;(markdownToPlainText as any).mockImplementation((str) => str.replace(/[#*_]/g, ''))
const result = await topicToPlainText(testTopic)
expect(db.topics.get).toHaveBeenCalledWith('topic1_plain')
expect(markdownToPlainText).toHaveBeenCalledWith('# Topic One')
expect(markdownToPlainText).toHaveBeenCalledWith('**Hello**')
expect(markdownToPlainText).toHaveBeenCalledWith('_World_')
expect(result).toBe('Topic One\n\nUser:\nHello\n\nAssistant:\nWorld')
})
it('should return only topic name if topic has no messages', async () => {
const testTopic: Topic = {
id: 'topic_empty_plain',
name: '## Empty Topic',
assistantId: 'asst_test',
messages: [] as any,
createdAt: '',
updatedAt: ''
}
;(db.topics.get as any).mockResolvedValue({ messages: [] })
;(markdownToPlainText as any).mockImplementation((str) => str.replace(/[#*_]/g, ''))
const result = await topicToPlainText(testTopic)
expect(result).toBe('Empty Topic')
expect(markdownToPlainText).toHaveBeenCalledWith('## Empty Topic')
})
it('should return empty string if topicMessages is null', async () => {
const testTopic: Topic = {
id: 'topic_null_msgs_plain',
name: 'Null Messages Topic',
assistantId: 'asst_test',
messages: null as any,
createdAt: '',
updatedAt: ''
}
;(db.topics.get as any).mockResolvedValue(null)
const result = await topicToPlainText(testTopic)
expect(result).toBe('')
})
})
describe('copyMessageAsPlainText', () => {
// Mock navigator.clipboard.writeText
const writeTextMock = vi.fn()
beforeEach(() => {
vi.stubGlobal('navigator', {
clipboard: {
writeText: writeTextMock
}
})
// Mock window.message methods
vi.stubGlobal('window', {
message: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn()
}
})
// Mock i18next translation function
vi.mock('i18next', () => ({
default: {
t: vi.fn((key) => key)
}
}))
writeTextMock.mockReset()
// Ensure markdownToPlainText mock is set
;(markdownToPlainText as any).mockImplementation((str) => str.replace(/[#*_]/g, ''))
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('should call messageToPlainText and copy its result to clipboard', async () => {
const testMessage = createMessage({ role: 'user', id: 'copy_msg_plain' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '**Copy This Plain**' }
])
await copyMessageAsPlainText(testMessage)
expect(markdownToPlainText).toHaveBeenCalledWith('**Copy This Plain**')
expect(writeTextMock).toHaveBeenCalledWith('Copy This Plain')
})
it('should handle empty message content', async () => {
const testMessage = createMessage({ role: 'user', id: 'copy_empty_msg_plain' }, [])
;(markdownToPlainText as any).mockReturnValue('')
await copyMessageAsPlainText(testMessage)
expect(markdownToPlainText).toHaveBeenCalledWith('')
expect(writeTextMock).toHaveBeenCalledWith('')
})
})
}) })
@@ -8,7 +8,8 @@ import {
findCitationInChildren, findCitationInChildren,
getCodeBlockId, getCodeBlockId,
removeTrailingDoubleSpaces, removeTrailingDoubleSpaces,
updateCodeBlock updateCodeBlock,
markdownToPlainText
} from '../markdown' } from '../markdown'
describe('markdown', () => { describe('markdown', () => {
@@ -184,7 +185,7 @@ describe('markdown', () => {
// function getAllCodeBlockIds(markdown: string): { [content: string]: string } { // function getAllCodeBlockIds(markdown: string): { [content: string]: string } {
// const result: { [content: string]: string } = {} // const result: { [content: string]: string } = {}
// const tree = unified().use(remarkParse).parse(markdown) // const tree = unified().use(remarkParse).parse(markdown)
//
// visit(tree, 'code', (node) => { // visit(tree, 'code', (node) => {
// const id = getCodeBlockId(node.position?.start) // const id = getCodeBlockId(node.position?.start)
// if (id) { // if (id) {
@@ -192,7 +193,7 @@ describe('markdown', () => {
// console.log(`Code Block ID: "${id}" for content: "${node.value}" lang: "${node.lang}"`) // console.log(`Code Block ID: "${id}" for content: "${node.value}" lang: "${node.lang}"`)
// } // }
// }) // })
//
// return result // return result
// } // }
@@ -323,4 +324,79 @@ describe('markdown', () => {
expect(result).toBe(expectedResult) expect(result).toBe(expectedResult)
}) })
}) })
describe('markdownToPlainText', () => {
it('should return an empty string if input is null or empty', () => {
expect(markdownToPlainText(null as any)).toBe('')
expect(markdownToPlainText('')).toBe('')
})
it('should remove headers', () => {
expect(markdownToPlainText('# Header 1')).toBe('Header 1')
expect(markdownToPlainText('## Header 2')).toBe('Header 2')
expect(markdownToPlainText('### Header 3')).toBe('Header 3')
})
it('should remove bold and italic', () => {
expect(markdownToPlainText('**bold**')).toBe('bold')
expect(markdownToPlainText('*italic*')).toBe('italic')
expect(markdownToPlainText('***bolditalic***')).toBe('bolditalic')
expect(markdownToPlainText('__bold__')).toBe('bold')
expect(markdownToPlainText('_italic_')).toBe('italic')
expect(markdownToPlainText('___bolditalic___')).toBe('bolditalic')
})
it('should remove strikethrough', () => {
expect(markdownToPlainText('~~strikethrough~~')).toBe('strikethrough')
})
it('should remove links, keeping the text', () => {
expect(markdownToPlainText('[link text](http://example.com)')).toBe('link text')
expect(markdownToPlainText('[link text with title](http://example.com "title")')).toBe('link text with title')
})
it('should remove images, keeping the alt text', () => {
expect(markdownToPlainText('![alt text](http://example.com/image.png)')).toBe('alt text')
})
it('should remove inline code', () => {
expect(markdownToPlainText('`inline code`')).toBe('inline code')
})
it('should remove code blocks', () => {
const codeBlock = '```javascript\nconst x = 1;\n```'
expect(markdownToPlainText(codeBlock)).toBe('const x = 1;') // remove-markdown keeps code content
})
it('should remove blockquotes', () => {
expect(markdownToPlainText('> blockquote')).toBe('blockquote')
})
it('should remove unordered lists', () => {
const list = '* item 1\n* item 2'
expect(markdownToPlainText(list).replace(/\n+/g, ' ')).toBe('item 1 item 2')
})
it('should remove ordered lists', () => {
const list = '1. item 1\n2. item 2'
expect(markdownToPlainText(list).replace(/\n+/g, ' ')).toBe('item 1 item 2')
})
it('should remove horizontal rules', () => {
expect(markdownToPlainText('---')).toBe('')
expect(markdownToPlainText('***')).toBe('')
expect(markdownToPlainText('___')).toBe('')
})
it('should handle a mix of markdown elements', () => {
const mixed = '# Title\nSome **bold** and *italic* text.\n[link](url)\n`code`\n> quote\n* list item'
const expected = 'Title\nSome bold and italic text.\nlink\ncode\nquote\nlist item'
const normalize = (str: string) => str.replace(/\s+/g, ' ').trim()
expect(normalize(markdownToPlainText(mixed))).toBe(normalize(expected))
})
it('should keep plain text unchanged', () => {
expect(markdownToPlainText('This is plain text.')).toBe('This is plain text.')
})
})
}) })
+16 -2
View File
@@ -1,8 +1,22 @@
import { Topic } from '@renderer/types' import { Message, Topic } from '@renderer/types'
import i18next from 'i18next'
import { topicToMarkdown } from './export' import { messageToPlainText, topicToMarkdown, topicToPlainText } from './export'
export const copyTopicAsMarkdown = async (topic: Topic) => { export const copyTopicAsMarkdown = async (topic: Topic) => {
const markdown = await topicToMarkdown(topic) const markdown = await topicToMarkdown(topic)
await navigator.clipboard.writeText(markdown) await navigator.clipboard.writeText(markdown)
window.message.success(i18next.t('message.copy.success'))
}
export const copyTopicAsPlainText = async (topic: Topic) => {
const plainText = await topicToPlainText(topic)
await navigator.clipboard.writeText(plainText)
window.message.success(i18next.t('message.copy.success'))
}
export const copyMessageAsPlainText = async (message: Message) => {
const plainText = messageToPlainText(message)
await navigator.clipboard.writeText(plainText)
window.message.success(i18next.t('message.copy.success'))
} }
+32 -1
View File
@@ -7,7 +7,7 @@ import { setExportState } from '@renderer/store/runtime'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { removeSpecialCharactersForFileName } from '@renderer/utils/file' import { removeSpecialCharactersForFileName } from '@renderer/utils/file'
import { convertMathFormula } from '@renderer/utils/markdown' import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown'
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find' import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
import { markdownToBlocks } from '@tryfabric/martian' import { markdownToBlocks } from '@tryfabric/martian'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -124,6 +124,22 @@ export const messagesToMarkdown = (messages: Message[], exportReasoning?: boolea
.join('\n\n---\n\n') .join('\n\n---\n\n')
} }
const formatMessageAsPlainText = (message: Message): string => {
const roleText = message.role === 'user' ? 'User:' : 'Assistant:'
const content = getMainTextContent(message)
const plainTextContent = markdownToPlainText(content).trim()
return `${roleText}\n${plainTextContent}`
}
export const messageToPlainText = (message: Message): string => {
const content = getMainTextContent(message)
return markdownToPlainText(content).trim()
}
const messagesToPlainText = (messages: Message[]): string => {
return messages.map(formatMessageAsPlainText).join('\n\n')
}
export const topicToMarkdown = async (topic: Topic, exportReasoning?: boolean) => { export const topicToMarkdown = async (topic: Topic, exportReasoning?: boolean) => {
const topicName = `# ${topic.name}` const topicName = `# ${topic.name}`
const topicMessages = await db.topics.get(topic.id) const topicMessages = await db.topics.get(topic.id)
@@ -135,6 +151,21 @@ export const topicToMarkdown = async (topic: Topic, exportReasoning?: boolean) =
return '' return ''
} }
export const topicToPlainText = async (topic: Topic): Promise<string> => {
const topicName = markdownToPlainText(topic.name).trim()
const topicMessages = await db.topics.get(topic.id)
if (topicMessages && topicMessages.messages.length > 0) {
return topicName + '\n\n' + messagesToPlainText(topicMessages.messages)
}
if (topicMessages && topicMessages.messages.length === 0) {
return topicName
}
return ''
}
export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: boolean) => { export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: boolean) => {
const { markdownExportPath } = store.getState().settings const { markdownExportPath } = store.getState().settings
if (!markdownExportPath) { if (!markdownExportPath) {
+14
View File
@@ -2,6 +2,7 @@ import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify' import remarkStringify from 'remark-stringify'
import { unified } from 'unified' import { unified } from 'unified'
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit'
import removeMarkdown from 'remove-markdown'
/** /**
* *
@@ -100,3 +101,16 @@ export function isValidPlantUML(code: string | null): boolean {
return diagramType !== undefined && code.search(`@end${diagramType}`) !== -1 return diagramType !== undefined && code.search(`@end${diagramType}`) !== -1
} }
/**
* Markdown
* @param markdown Markdown
* @returns
*/
export const markdownToPlainText = (markdown: string): string => {
if (!markdown) {
return ''
}
// 直接用 remove-markdown 库,使用默认的 removeMarkdown 参数
return removeMarkdown(markdown)
}
@@ -29,9 +29,10 @@ const SelectionActionApp: FC = () => {
const [showOpacitySlider, setShowOpacitySlider] = useState(false) const [showOpacitySlider, setShowOpacitySlider] = useState(false)
const [opacity, setOpacity] = useState(actionWindowOpacity) const [opacity, setOpacity] = useState(actionWindowOpacity)
const shouldCloseWhenBlur = useRef(false)
const contentElementRef = useRef<HTMLDivElement>(null) const contentElementRef = useRef<HTMLDivElement>(null)
const isAutoScrollEnabled = useRef(true) const isAutoScrollEnabled = useRef(true)
const shouldCloseWhenBlur = useRef(false) const lastScrollHeight = useRef(0)
useEffect(() => { useEffect(() => {
if (isAutoPin) { if (isAutoPin) {
@@ -80,6 +81,8 @@ const SelectionActionApp: FC = () => {
const contentEl = contentElementRef.current const contentEl = contentElementRef.current
if (contentEl) { if (contentEl) {
contentEl.addEventListener('scroll', handleUserScroll) contentEl.addEventListener('scroll', handleUserScroll)
// Initialize the scroll height
lastScrollHeight.current = contentEl.scrollHeight
} }
return () => { return () => {
if (contentEl) { if (contentEl) {
@@ -140,6 +143,7 @@ const SelectionActionApp: FC = () => {
setOpacity(value) setOpacity(value)
} }
//must useCallback to avoid re-rendering the component
const handleScrollToBottom = useCallback(() => { const handleScrollToBottom = useCallback(() => {
if (contentElementRef.current && isAutoScrollEnabled.current) { if (contentElementRef.current && isAutoScrollEnabled.current) {
contentElementRef.current.scrollTo({ contentElementRef.current.scrollTo({
@@ -153,9 +157,19 @@ const SelectionActionApp: FC = () => {
if (!contentElementRef.current) return if (!contentElementRef.current) return
const { scrollTop, scrollHeight, clientHeight } = contentElementRef.current const { scrollTop, scrollHeight, clientHeight } = contentElementRef.current
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 24
// Only update isAutoScrollEnabled if user is at bottom // Check if content height has increased (new content added)
const contentIncreased = scrollHeight > lastScrollHeight.current
lastScrollHeight.current = scrollHeight
// If content increased and we're in auto-scroll mode, don't change the auto-scroll state
if (contentIncreased && isAutoScrollEnabled.current) {
return
}
// Only check user position if content didn't increase
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 32
if (isAtBottom) { if (isAtBottom) {
isAutoScrollEnabled.current = true isAutoScrollEnabled.current = true
} else { } else {
@@ -7,7 +7,7 @@ import { Assistant, Topic } from '@renderer/types'
import { Chunk, ChunkType } from '@renderer/types/chunk' import { Chunk, ChunkType } from '@renderer/types/chunk'
import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage' import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage'
import { isAbortError } from '@renderer/utils/error' import { isAbortError } from '@renderer/utils/error'
import { createMainTextBlock } from '@renderer/utils/messageUtils/create' import { createMainTextBlock, createThinkingBlock } from '@renderer/utils/messageUtils/create'
export const processMessages = async ( export const processMessages = async (
assistant: Assistant, assistant: Assistant,
@@ -32,8 +32,11 @@ export const processMessages = async (
store.dispatch(newMessagesActions.addMessage({ topicId: topic.id, message: userMessage })) store.dispatch(newMessagesActions.addMessage({ topicId: topic.id, message: userMessage }))
store.dispatch(upsertManyBlocks(userBlocks)) store.dispatch(upsertManyBlocks(userBlocks))
let blockId: string | null = null let textBlockId: string | null = null
let blockContent: string = '' let textBlockContent: string = ''
let thinkingBlockId: string | null = null
let thinkingBlockContent: string = ''
const assistantMessage = getAssistantMessage({ const assistantMessage = getAssistantMessage({
assistant, assistant,
@@ -52,17 +55,14 @@ export const processMessages = async (
onChunkReceived: (chunk: Chunk) => { onChunkReceived: (chunk: Chunk) => {
switch (chunk.type) { switch (chunk.type) {
case ChunkType.THINKING_DELTA: case ChunkType.THINKING_DELTA:
case ChunkType.THINKING_COMPLETE:
//TODO
break
case ChunkType.TEXT_DELTA:
{ {
blockContent += chunk.text thinkingBlockContent += chunk.text
if (!blockId) { if (!thinkingBlockId) {
const block = createMainTextBlock(assistantMessage.id, chunk.text, { const block = createThinkingBlock(assistantMessage.id, chunk.text, {
status: MessageBlockStatus.STREAMING status: MessageBlockStatus.STREAMING,
thinking_millsec: chunk.thinking_millsec
}) })
blockId = block.id thinkingBlockId = block.id
store.dispatch( store.dispatch(
newMessagesActions.updateMessage({ newMessagesActions.updateMessage({
topicId: topic.id, topicId: topic.id,
@@ -72,7 +72,46 @@ export const processMessages = async (
) )
store.dispatch(upsertOneBlock(block)) store.dispatch(upsertOneBlock(block))
} else { } else {
store.dispatch(updateOneBlock({ id: blockId, changes: { content: blockContent } })) store.dispatch(
updateOneBlock({
id: thinkingBlockId,
changes: { content: thinkingBlockContent, thinking_millsec: chunk.thinking_millsec }
})
)
}
onStream()
}
break
case ChunkType.THINKING_COMPLETE:
{
if (thinkingBlockId) {
store.dispatch(
updateOneBlock({
id: thinkingBlockId,
changes: { status: MessageBlockStatus.SUCCESS, thinking_millsec: chunk.thinking_millsec }
})
)
}
}
break
case ChunkType.TEXT_DELTA:
{
textBlockContent += chunk.text
if (!textBlockId) {
const block = createMainTextBlock(assistantMessage.id, chunk.text, {
status: MessageBlockStatus.STREAMING
})
textBlockId = block.id
store.dispatch(
newMessagesActions.updateMessage({
topicId: topic.id,
messageId: assistantMessage.id,
updates: { blockInstruction: { id: block.id } }
})
)
store.dispatch(upsertOneBlock(block))
} else {
store.dispatch(updateOneBlock({ id: textBlockId, changes: { content: textBlockContent } }))
} }
onStream() onStream()
@@ -80,10 +119,10 @@ export const processMessages = async (
break break
case ChunkType.TEXT_COMPLETE: case ChunkType.TEXT_COMPLETE:
{ {
blockId && textBlockId &&
store.dispatch( store.dispatch(
updateOneBlock({ updateOneBlock({
id: blockId, id: textBlockId,
changes: { status: MessageBlockStatus.SUCCESS } changes: { status: MessageBlockStatus.SUCCESS }
}) })
) )
@@ -94,12 +133,12 @@ export const processMessages = async (
updates: { status: AssistantMessageStatus.SUCCESS } updates: { status: AssistantMessageStatus.SUCCESS }
}) })
) )
blockContent = chunk.text textBlockContent = chunk.text
} }
break break
case ChunkType.BLOCK_COMPLETE: case ChunkType.BLOCK_COMPLETE:
case ChunkType.ERROR: case ChunkType.ERROR:
onFinish(blockContent) onFinish(textBlockContent)
break break
} }
} }
+80 -7
View File
@@ -654,6 +654,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@cherrystudio/embedjs-ollama@npm:^0.1.31":
version: 0.1.31
resolution: "@cherrystudio/embedjs-ollama@npm:0.1.31"
dependencies:
"@cherrystudio/embedjs-interfaces": "npm:0.1.31"
"@langchain/core": "npm:^0.3.26"
"@langchain/ollama": "npm:^0.1.4"
debug: "npm:^4.4.0"
checksum: 10c0/7ac807aca5a99dccc2d2bfc8245c9b9c5184c7b0b48f1ea4d3367a2175a8978c5cbc425614e1851074cffd93ffbb31350e4e567de308aa5100a6a2cbd795813f
languageName: node
linkType: hard
"@cherrystudio/embedjs-openai@npm:^0.1.31": "@cherrystudio/embedjs-openai@npm:^0.1.31":
version: 0.1.31 version: 0.1.31
resolution: "@cherrystudio/embedjs-openai@npm:0.1.31" resolution: "@cherrystudio/embedjs-openai@npm:0.1.31"
@@ -2617,6 +2629,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@langchain/ollama@npm:^0.1.4":
version: 0.1.6
resolution: "@langchain/ollama@npm:0.1.6"
dependencies:
ollama: "npm:^0.5.12"
uuid: "npm:^10.0.0"
zod: "npm:^3.24.1"
zod-to-json-schema: "npm:^3.24.1"
peerDependencies:
"@langchain/core": ">=0.2.21 <0.4.0"
checksum: 10c0/535f2f4304edf125d0133a80fcb7657c5ffcab3c180e3ea69ce171c7a5fadb220b0d00c3bb7523bbe4f2f56663e0dd59cd19e19adadf0091c800b9ddca17ed3b
languageName: node
linkType: hard
"@langchain/ollama@npm:^0.2.1":
version: 0.2.1
resolution: "@langchain/ollama@npm:0.2.1"
dependencies:
ollama: "npm:^0.5.12"
uuid: "npm:^10.0.0"
zod: "npm:^3.24.1"
zod-to-json-schema: "npm:^3.24.1"
peerDependencies:
"@langchain/core": ">=0.2.21 <0.4.0"
checksum: 10c0/be3083a15e879f2c19d0a51aafb88a3ba4ea69f9fdd90e6069b84edd92649c00a29a34cb885746e099f5cab0791b55df3776cd582cba7de299b8bd574d32b8c1
languageName: node
linkType: hard
"@langchain/openai@npm:0.3.16": "@langchain/openai@npm:0.3.16":
version: 0.3.16 version: 0.3.16
resolution: "@langchain/openai@npm:0.3.16" resolution: "@langchain/openai@npm:0.3.16"
@@ -5544,6 +5584,7 @@ __metadata:
"@cherrystudio/embedjs-loader-sitemap": "npm:^0.1.31" "@cherrystudio/embedjs-loader-sitemap": "npm:^0.1.31"
"@cherrystudio/embedjs-loader-web": "npm:^0.1.31" "@cherrystudio/embedjs-loader-web": "npm:^0.1.31"
"@cherrystudio/embedjs-loader-xml": "npm:^0.1.31" "@cherrystudio/embedjs-loader-xml": "npm:^0.1.31"
"@cherrystudio/embedjs-ollama": "npm:^0.1.31"
"@cherrystudio/embedjs-openai": "npm:^0.1.31" "@cherrystudio/embedjs-openai": "npm:^0.1.31"
"@electron-toolkit/eslint-config-prettier": "npm:^3.0.0" "@electron-toolkit/eslint-config-prettier": "npm:^3.0.0"
"@electron-toolkit/eslint-config-ts": "npm:^3.0.0" "@electron-toolkit/eslint-config-ts": "npm:^3.0.0"
@@ -5558,6 +5599,7 @@ __metadata:
"@hello-pangea/dnd": "npm:^16.6.0" "@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0" "@kangfenmao/keyv-storage": "npm:^0.1.0"
"@langchain/community": "npm:^0.3.36" "@langchain/community": "npm:^0.3.36"
"@langchain/ollama": "npm:^0.2.1"
"@modelcontextprotocol/sdk": "npm:^1.11.4" "@modelcontextprotocol/sdk": "npm:^1.11.4"
"@mozilla/readability": "npm:^0.6.0" "@mozilla/readability": "npm:^0.6.0"
"@notionhq/client": "npm:^2.2.15" "@notionhq/client": "npm:^2.2.15"
@@ -5665,9 +5707,10 @@ __metadata:
remark-cjk-friendly: "npm:^1.1.0" remark-cjk-friendly: "npm:^1.1.0"
remark-gfm: "npm:^4.0.0" remark-gfm: "npm:^4.0.0"
remark-math: "npm:^6.0.0" remark-math: "npm:^6.0.0"
remove-markdown: "npm:^0.6.2"
rollup-plugin-visualizer: "npm:^5.12.0" rollup-plugin-visualizer: "npm:^5.12.0"
sass: "npm:^1.88.0" sass: "npm:^1.88.0"
selection-hook: "npm:^0.9.22" selection-hook: "npm:^0.9.23"
shiki: "npm:^3.4.2" shiki: "npm:^3.4.2"
string-width: "npm:^7.2.0" string-width: "npm:^7.2.0"
styled-components: "npm:^6.1.11" styled-components: "npm:^6.1.11"
@@ -13910,6 +13953,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ollama@npm:^0.5.12":
version: 0.5.16
resolution: "ollama@npm:0.5.16"
dependencies:
whatwg-fetch: "npm:^3.6.20"
checksum: 10c0/901f9dac5692447219b3a48dcb9ad4f7da292a8ddbbbf429482cdde56e787a8a37123f3df18957733b8990f70b67fd05eb13b9bb17bd029eb6597d5b36345d7a
languageName: node
linkType: hard
"on-finished@npm:^2.4.1": "on-finished@npm:^2.4.1":
version: 2.4.1 version: 2.4.1
resolution: "on-finished@npm:2.4.1" resolution: "on-finished@npm:2.4.1"
@@ -14012,7 +14064,7 @@ __metadata:
"openai@patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch": "openai@patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch":
version: 5.1.0 version: 5.1.0
resolution: "openai@patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch::version=5.1.0&hash=cf4b11" resolution: "openai@patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch::version=5.1.0&hash=7d7491"
peerDependencies: peerDependencies:
ws: ^8.18.0 ws: ^8.18.0
zod: ^3.23.8 zod: ^3.23.8
@@ -14023,7 +14075,7 @@ __metadata:
optional: true optional: true
bin: bin:
openai: bin/cli openai: bin/cli
checksum: 10c0/26abab8311c5e130759d8b2a939ac408872d808c8e8b8f6a7bb5c85f2df0a66d61aece3af783dbf04a2aa401481cf20a6eddcb777b545b387f66220a6a6d25d7 checksum: 10c0/e7d2429887d0060cf9d8cd2c04640f759b55bffab696b3e926e510357af1b5f5b3bcf55d0e0dbe2282da8438a61fd75259847899db289d1e18ff0798b2450344
languageName: node languageName: node
linkType: hard linkType: hard
@@ -15965,6 +16017,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"remove-markdown@npm:^0.6.2":
version: 0.6.2
resolution: "remove-markdown@npm:0.6.2"
checksum: 10c0/d26fb020b202f227877a29701fcbc96980af3c17001ae46f3253ba21307babcfa424006a207161f83ed04efe19fc9be6b084bd1308ef0b2217c59139e6ef6eb4
languageName: node
linkType: hard
"repeat-string@npm:^1.0.0": "repeat-string@npm:^1.0.0":
version: 1.6.1 version: 1.6.1
resolution: "repeat-string@npm:1.6.1" resolution: "repeat-string@npm:1.6.1"
@@ -16384,13 +16443,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"selection-hook@npm:^0.9.22": "selection-hook@npm:^0.9.23":
version: 0.9.22 version: 0.9.23
resolution: "selection-hook@npm:0.9.22" resolution: "selection-hook@npm:0.9.23"
dependencies: dependencies:
node-gyp: "npm:latest" node-gyp: "npm:latest"
node-gyp-build: "npm:^4.8.4" node-gyp-build: "npm:^4.8.4"
checksum: 10c0/4a202485fc4aed250f4ab863fdca9235240d49e5629549f5cf72dd3087af81ee54d65c28fa7d215d7ca609e25132b125e10540acbab1f6f4a7d8a508e1468ea3 checksum: 10c0/3b91193814c063e14dd788cff3b27020821bbeae24eab106d2ce5bf600c034c1b3db96ce573c456b74d0553346dfcf4c7cc8d49386a22797b42667f7ed3eee01
languageName: node languageName: node
linkType: hard linkType: hard
@@ -18442,6 +18501,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"whatwg-fetch@npm:^3.6.20":
version: 3.6.20
resolution: "whatwg-fetch@npm:3.6.20"
checksum: 10c0/fa972dd14091321d38f36a4d062298df58c2248393ef9e8b154493c347c62e2756e25be29c16277396046d6eaa4b11bd174f34e6403fff6aaca9fb30fa1ff46d
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0": "whatwg-mimetype@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0" resolution: "whatwg-mimetype@npm:4.0.0"
@@ -18812,6 +18878,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"zod@npm:^3.24.1":
version: 3.25.56
resolution: "zod@npm:3.25.56"
checksum: 10c0/3800f01d4b1df932b91354eb1e648f69cc7e5561549e6d2bf83827d930a5f33bbf92926099445f6fc1ebb64ca9c6513ef9ae5e5409cfef6325f354bcf6fc9a24
languageName: node
linkType: hard
"zustand@npm:^4.4.0": "zustand@npm:^4.4.0":
version: 4.5.6 version: 4.5.6
resolution: "zustand@npm:4.5.6" resolution: "zustand@npm:4.5.6"