Compare commits
30 Commits
feat/cherr
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c518c9090b | ||
|
|
91045ecc2b | ||
|
|
748ca008b4 | ||
|
|
6ad9044cd1 | ||
|
|
9e9a1ec024 | ||
|
|
a214dca6fa | ||
|
|
b142e5647e | ||
|
|
a33a8da5c1 | ||
|
|
e029159067 | ||
|
|
8582ad2529 | ||
|
|
e7f1127aee | ||
|
|
7e54c465b1 | ||
|
|
5c76d398c5 | ||
|
|
f6a935f14f | ||
|
|
26d018b1b7 | ||
|
|
cd8c5115df | ||
|
|
0020e9f3c9 | ||
|
|
8df4cd7e76 | ||
|
|
ee7e6c0f87 | ||
|
|
e65091f83c | ||
|
|
3ee8186f96 | ||
|
|
49f1b62848 | ||
|
|
90a84bb55a | ||
|
|
d2147aed3b | ||
|
|
4f28086a64 | ||
|
|
d9c20c8815 | ||
|
|
b951d89c6a | ||
|
|
ac7d4cb4fa | ||
|
|
d2ea0592ce | ||
|
|
66ddeb94bf |
@@ -4,8 +4,5 @@
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none",
|
||||
"endOfLine": "lf",
|
||||
"bracketSameLine": true,
|
||||
"tailwindStylesheet": "./src/renderer/src/assets/styles/tailwind.css",
|
||||
"tailwindFunctions": ["clsx"],
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"bracketSameLine": true
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b
|
||||
+ "embeddings/decoding base64 embeddings from base64"
|
||||
+ );
|
||||
+ return response._thenUnwrap((response) => {
|
||||
+ if (response && response.data) {
|
||||
+ if (response && response.data && typeof response.data[0]?.embedding === 'string') {
|
||||
+ response.data.forEach((embeddingBase64Obj) => {
|
||||
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
|
||||
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
|
||||
@@ -266,7 +266,7 @@ index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a
|
||||
+ "embeddings/decoding base64 embeddings from base64"
|
||||
+ );
|
||||
+ return response._thenUnwrap((response) => {
|
||||
+ if (response && response.data) {
|
||||
+ if (response && response.data && typeof response.data[0]?.embedding === 'string') {
|
||||
+ response.data.forEach((embeddingBase64Obj) => {
|
||||
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
|
||||
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
|
||||
|
||||
@@ -188,7 +188,7 @@ Thank you for your support and contributions!
|
||||
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
|
||||
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
|
||||
[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-link]: https://discord.gg/wez8HtpxqQ
|
||||
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/renderer/src/assets/styles/tailwind.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@renderer/ui/third-party",
|
||||
"utils": "@renderer/utils",
|
||||
"ui": "@renderer/ui",
|
||||
"lib": "@renderer/lib",
|
||||
"hooks": "@renderer/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -190,7 +190,7 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
|
||||
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
|
||||
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
|
||||
[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-link]: https://discord.gg/wez8HtpxqQ
|
||||
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
|
||||
|
||||
@@ -202,7 +202,7 @@ https://docs.cherry-ai.com
|
||||
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
|
||||
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
|
||||
[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-link]: https://discord.gg/wez8HtpxqQ
|
||||
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
|
||||
|
||||
635
docs/technical/topic-message-tree.md
Normal file
635
docs/technical/topic-message-tree.md
Normal 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 << M,S通常较小 |
|
||||
| **发送消息** | 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` 或在相关组件中作为本地状态管理。 |
|
||||
BIN
docs/technical/topic-message-tree.png
Normal file
BIN
docs/technical/topic-message-tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -107,8 +107,11 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
新增划词助手
|
||||
助手支持分组
|
||||
支持主题颜色切换
|
||||
划词助手支持应用过滤
|
||||
翻译模块功能改进
|
||||
划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能
|
||||
复制功能:新增纯文本复制(去除Markdown格式符号)
|
||||
知识库:支持设置向量维度,修复Ollama分数错误和维度编辑问题
|
||||
多语言:增加模型名称多语言提示和翻译源语言手动选择
|
||||
文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程
|
||||
模型:修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
|
||||
图像功能:统一图片查看器,支持Base64图片渲染,修复图片预览相关问题
|
||||
UI:实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示
|
||||
|
||||
@@ -40,7 +40,6 @@ export default defineConfig({
|
||||
},
|
||||
renderer: {
|
||||
plugins: [
|
||||
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||
react({
|
||||
plugins: [
|
||||
[
|
||||
|
||||
@@ -62,8 +62,7 @@ export default defineConfig([
|
||||
'.yarn/**',
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/renderer/src/ui/**'
|
||||
'src/main/integration/nutstore/sso/lib/**'
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
29
package.json
29
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -68,9 +68,11 @@
|
||||
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-web": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
|
||||
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@langchain/ollama": "^0.2.1",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@tanstack/react-query": "^5.27.0",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
@@ -92,7 +94,8 @@
|
||||
"officeparser": "^4.1.1",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"selection-hook": "^0.9.22",
|
||||
"remove-markdown": "^0.6.2",
|
||||
"selection-hook": "^0.9.23",
|
||||
"tar": "^7.4.3",
|
||||
"turndown": "^7.2.0",
|
||||
"webdav": "^5.8.0",
|
||||
@@ -119,18 +122,9 @@
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.4.2",
|
||||
"@swc/plugin-styled-components": "^7.1.5",
|
||||
"@tailwindcss/vite": "^4.1.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -159,8 +153,6 @@
|
||||
"antd": "^5.22.5",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "^5.0.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
@@ -184,17 +176,15 @@
|
||||
"lint-staged": "^15.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"lucide-react": "^0.487.0",
|
||||
"mermaid": "^11.6.0",
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.12.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"motion": "^12.10.5",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"playwright": "^1.52.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"rc-virtual-list": "^3.18.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -218,16 +208,11 @@
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.88.0",
|
||||
"shiki": "^3.4.2",
|
||||
"sonner": "^2.0.3",
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
"tokenx": "^0.4.1",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.6.2",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "6.2.6",
|
||||
"vitest": "^3.1.4"
|
||||
|
||||
@@ -11,13 +11,13 @@ if (isDev) {
|
||||
export const DATA_PATH = getDataPath()
|
||||
|
||||
export const titleBarOverlayDark = {
|
||||
height: 42,
|
||||
height: 40,
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#fff'
|
||||
}
|
||||
|
||||
export const titleBarOverlayLight = {
|
||||
height: 42,
|
||||
height: 40,
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#000'
|
||||
}
|
||||
|
||||
@@ -5,8 +5,15 @@ import EmbeddingsFactory from './EmbeddingsFactory'
|
||||
|
||||
export default class Embeddings {
|
||||
private sdk: BaseEmbeddings
|
||||
constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
|
||||
this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
|
||||
constructor({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
|
||||
this.sdk = EmbeddingsFactory.create({
|
||||
model,
|
||||
provider,
|
||||
apiKey,
|
||||
apiVersion,
|
||||
baseURL,
|
||||
dimensions
|
||||
} as KnowledgeBaseParams)
|
||||
}
|
||||
public async init(): Promise<void> {
|
||||
return this.sdk.init()
|
||||
|
||||
@@ -1,20 +1,49 @@
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
|
||||
import { getInstanceName } from '@main/utils'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
import VoyageEmbeddings from './VoyageEmbeddings'
|
||||
import { SUPPORTED_DIM_MODELS as VOYAGE_SUPPORTED_DIM_MODELS, VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
|
||||
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
|
||||
if (model.includes('voyage')) {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
apiKey,
|
||||
outputDimension: dimensions,
|
||||
batchSize: 8
|
||||
if (provider === 'voyageai') {
|
||||
if (VOYAGE_SUPPORTED_DIM_MODELS.includes(model)) {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
apiKey,
|
||||
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) {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
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
|
||||
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
|
||||
super()
|
||||
if (!this.configuration) this.configuration = {}
|
||||
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
|
||||
|
||||
if (!this.configuration.outputDimension) {
|
||||
throw new Error('You need to pass in the optional dimensions parameter for this model')
|
||||
if (!SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
|
||||
throw new Error(`VoyageEmbeddings only supports ${SUPPORTED_DIM_MODELS.join(', ')}`)
|
||||
}
|
||||
|
||||
this.model = new _VoyageEmbeddings(this.configuration)
|
||||
}
|
||||
override async getDimensions(): Promise<number> {
|
||||
|
||||
@@ -34,6 +34,26 @@ if (isWin) {
|
||||
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
|
||||
if (!isDev) {
|
||||
// handle uncaught exception
|
||||
|
||||
@@ -110,13 +110,21 @@ class KnowledgeService {
|
||||
private getRagApplication = async ({
|
||||
id,
|
||||
model,
|
||||
provider,
|
||||
apiKey,
|
||||
apiVersion,
|
||||
baseURL,
|
||||
dimensions
|
||||
}: KnowledgeBaseParams): Promise<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 {
|
||||
ragApplication = await new RAGApplicationBuilder()
|
||||
.setModel('NO_MODEL')
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
|
||||
import type { ActionItem } from '../../renderer/src/types/selectionTypes'
|
||||
import { ConfigKeys, configManager } from './ConfigManager'
|
||||
import storeSyncService from './StoreSyncService'
|
||||
|
||||
let SelectionHook: SelectionHookConstructor | null = null
|
||||
try {
|
||||
@@ -39,7 +40,8 @@ type RelativeOrientation =
|
||||
|
||||
enum TriggerMode {
|
||||
Selected = 'selected',
|
||||
Ctrlkey = 'ctrlkey'
|
||||
Ctrlkey = 'ctrlkey',
|
||||
Shortcut = 'shortcut'
|
||||
}
|
||||
|
||||
/** 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 = null
|
||||
}
|
||||
this.closePreloadedActionWindows()
|
||||
|
||||
this.started = false
|
||||
this.logInfo('SelectionService Stopped')
|
||||
return true
|
||||
@@ -334,6 +338,21 @@ export class SelectionService {
|
||||
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
|
||||
* Sets up window properties, event handlers, and loads the toolbar UI
|
||||
@@ -378,6 +397,9 @@ export class SelectionService {
|
||||
|
||||
// Clean up when closed
|
||||
this.toolbarWindow.on('closed', () => {
|
||||
if (!this.toolbarWindow?.isDestroyed()) {
|
||||
this.toolbarWindow?.destroy()
|
||||
}
|
||||
this.toolbarWindow = null
|
||||
})
|
||||
|
||||
@@ -563,6 +585,21 @@ export class SelectionService {
|
||||
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
|
||||
* @param selectionData Text selection information and coordinates
|
||||
@@ -854,7 +891,6 @@ export class SelectionService {
|
||||
this.lastCtrlkeyDownTime = -1
|
||||
|
||||
const selectionData = this.selectionHook!.getCurrentSelection()
|
||||
|
||||
if (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
|
||||
* 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
|
||||
*/
|
||||
private processTriggerMode() {
|
||||
if (this.triggerMode === TriggerMode.Selected) {
|
||||
if (this.isCtrlkeyListenerActive) {
|
||||
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
|
||||
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
|
||||
switch (this.triggerMode) {
|
||||
case TriggerMode.Selected:
|
||||
if (this.isCtrlkeyListenerActive) {
|
||||
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
|
||||
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
|
||||
|
||||
this.isCtrlkeyListenerActive = false
|
||||
}
|
||||
this.isCtrlkeyListenerActive = false
|
||||
}
|
||||
|
||||
this.selectionHook!.setSelectionPassiveMode(false)
|
||||
} else if (this.triggerMode === TriggerMode.Ctrlkey) {
|
||||
if (!this.isCtrlkeyListenerActive) {
|
||||
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
|
||||
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
|
||||
this.selectionHook!.setSelectionPassiveMode(false)
|
||||
break
|
||||
case TriggerMode.Ctrlkey:
|
||||
if (!this.isCtrlkeyListenerActive) {
|
||||
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 {
|
||||
return this.selectionHook?.writeToClipboard(text) ?? false
|
||||
if (!this.selectionHook || !this.started) return false
|
||||
return this.selectionHook.writeToClipboard(text)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,10 +4,16 @@ import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
import selectionService from './SelectionService'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
let showAppAccelerator: 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
|
||||
const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>()
|
||||
@@ -28,6 +34,18 @@ function getShortcutHandler(shortcut: Shortcut) {
|
||||
return () => {
|
||||
windowService.toggleMiniWindow()
|
||||
}
|
||||
case 'selection_assistant_toggle':
|
||||
return () => {
|
||||
if (selectionService) {
|
||||
selectionService.toggleEnabled()
|
||||
}
|
||||
}
|
||||
case 'selection_assistant_select_text':
|
||||
return () => {
|
||||
if (selectionService) {
|
||||
selectionService.processSelectTextByShortcut()
|
||||
}
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -37,9 +55,8 @@ function formatShortcutKey(shortcut: string[]): string {
|
||||
return shortcut.join('+')
|
||||
}
|
||||
|
||||
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
|
||||
shortcut: string | string[]
|
||||
): string => {
|
||||
// convert the shortcut recorded by keyboard event key value to electron global shortcut format
|
||||
const convertShortcutFormat = (shortcut: string | string[]): string => {
|
||||
const accelerator = (() => {
|
||||
if (Array.isArray(shortcut)) {
|
||||
return shortcut
|
||||
@@ -93,11 +110,14 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
|
||||
}
|
||||
|
||||
export function registerShortcuts(window: BrowserWindow) {
|
||||
window.once('ready-to-show', () => {
|
||||
if (configManager.getLaunchToTray()) {
|
||||
registerOnlyUniversalShortcuts()
|
||||
}
|
||||
})
|
||||
if (isRegisterOnBoot) {
|
||||
window.once('ready-to-show', () => {
|
||||
if (configManager.getLaunchToTray()) {
|
||||
registerOnlyUniversalShortcuts()
|
||||
}
|
||||
})
|
||||
isRegisterOnBoot = false
|
||||
}
|
||||
|
||||
//only for clearer code
|
||||
const registerOnlyUniversalShortcuts = () => {
|
||||
@@ -124,7 +144,12 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -146,6 +171,14 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||
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
|
||||
case 'zoom_in':
|
||||
globalShortcut.register('CommandOrControl+=', () => handler(window))
|
||||
@@ -162,9 +195,7 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
|
||||
shortcut.shortcut
|
||||
)
|
||||
const accelerator = convertShortcutFormat(shortcut.shortcut)
|
||||
|
||||
globalShortcut.register(accelerator, () => handler(window))
|
||||
} catch (error) {
|
||||
@@ -181,15 +212,25 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
|
||||
if (showAppAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
const accelerator =
|
||||
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
|
||||
const accelerator = convertShortcutFormat(showAppAccelerator)
|
||||
handler && globalShortcut.register(accelerator, () => handler(window))
|
||||
}
|
||||
|
||||
if (showMiniWindowAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||
const accelerator =
|
||||
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
|
||||
const accelerator = convertShortcutFormat(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))
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -217,6 +258,8 @@ export function unregisterAllShortcuts() {
|
||||
try {
|
||||
showAppAccelerator = null
|
||||
showMiniWindowAccelerator = null
|
||||
selectionAssistantToggleAccelerator = null
|
||||
selectionAssistantSelectTextAccelerator = null
|
||||
windowOnHandlers.forEach((handlers, window) => {
|
||||
window.off('focus', handlers.onFocusHandler)
|
||||
window.off('blur', handlers.onBlurHandler)
|
||||
|
||||
@@ -49,6 +49,23 @@ export class StoreSyncService {
|
||||
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
|
||||
* Handles window subscription, unsubscription and action broadcasting
|
||||
|
||||
@@ -56,14 +56,14 @@ export class WindowService {
|
||||
minHeight: 600,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
transparent: false,
|
||||
transparent: isMac,
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
|
||||
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||
trafficLightPosition: { x: 12, y: 12 },
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(isLinux ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
@@ -116,12 +116,6 @@ export class WindowService {
|
||||
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) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { getConfigDir } from '@main/utils/file'
|
||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||
import {
|
||||
OAuthClientInformation,
|
||||
OAuthClientInformationFull,
|
||||
OAuthTokens
|
||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth'
|
||||
import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth'
|
||||
import Logger from 'electron-log'
|
||||
import open from 'open'
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) {
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')")
|
||||
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import MainSidebar from './components/app/MainSidebar'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
@@ -13,9 +13,14 @@ import { NotificationProvider } from './context/NotificationProvider'
|
||||
import StyleSheetManager from './context/StyleSheetManager'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import NavigationHandler from './handler/NavigationHandler'
|
||||
import DiscoverPage from './pages/discover'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
function App(): React.ReactElement {
|
||||
return (
|
||||
@@ -29,18 +34,16 @@ function App(): React.ReactElement {
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<NavigationHandler />
|
||||
<MainSidebar />
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
{/* <Route path="/agents" element={<AgentsPage />} /> */}
|
||||
{/* <Route path="/paintings/*" element={<PaintingsRoutePage />} /> */}
|
||||
{/* <Route path="/translate" element={<TranslatePage />} /> */}
|
||||
{/* <Route path="/files" element={<FilesPage />} /> */}
|
||||
{/* <Route path="/knowledge" element={<KnowledgePage />} /> */}
|
||||
{/* <Route path="/apps" element={<AppsPage />} /> */}
|
||||
{/* <Route path="/mcp-servers/*" element={<McpServersPage />} /> */}
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
<Route path="/discover/*" element={<DiscoverPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
|
||||
BIN
src/renderer/src/assets/images/providers/302ai.webp
Normal file
BIN
src/renderer/src/assets/images/providers/302ai.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src/renderer/src/assets/images/providers/cephalon.jpeg
Normal file
BIN
src/renderer/src/assets/images/providers/cephalon.jpeg
Normal file
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 |
BIN
src/renderer/src/assets/images/providers/nomic.png
Normal file
BIN
src/renderer/src/assets/images/providers/nomic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -25,6 +25,7 @@
|
||||
}
|
||||
|
||||
.minapp-drawer {
|
||||
max-width: calc(100vw - var(--sidebar-width));
|
||||
.ant-drawer-content-wrapper {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -32,7 +33,7 @@
|
||||
position: absolute;
|
||||
-webkit-app-region: drag;
|
||||
min-height: calc(var(--navbar-height) + 0.5px);
|
||||
width: 100%;
|
||||
width: calc(100vw - var(--sidebar-width));
|
||||
margin-top: -0.5px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
--color-text-secondary: rgba(235, 235, 245, 0.7);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #383838;
|
||||
--color-border: #ffffff19;
|
||||
--color-border-soft: #ffffff10;
|
||||
--color-border-mute: #ffffff05;
|
||||
--color-error: #f44336;
|
||||
@@ -44,8 +44,8 @@
|
||||
--color-reference-text: #ffffff;
|
||||
--color-reference-background: #0b0e12;
|
||||
|
||||
--color-list-item: rgba(255, 255, 255, 0.1);
|
||||
--color-list-item-hover: rgba(255, 255, 255, 0.05);
|
||||
--color-list-item: #222;
|
||||
--color-list-item-hover: #1e1e1e;
|
||||
|
||||
--modal-background: #1f1f1f;
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
--navbar-background-mac: rgba(20, 20, 20, 0.55);
|
||||
--navbar-background: #1f1f1f;
|
||||
|
||||
--navbar-height: 42px;
|
||||
--navbar-height: 40px;
|
||||
--sidebar-width: 50px;
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 100px;
|
||||
@@ -71,8 +71,7 @@
|
||||
--chat-background-assistant: #2c2c2c;
|
||||
--chat-text-user: var(--color-black);
|
||||
|
||||
--list-item-border-radius: 8px;
|
||||
--border-width: 0.5px;
|
||||
--list-item-border-radius: 20px;
|
||||
}
|
||||
|
||||
[theme-mode='light'] {
|
||||
@@ -121,8 +120,8 @@
|
||||
--color-reference-text: #000000;
|
||||
--color-reference-background: #f1f7ff;
|
||||
|
||||
--color-list-item: rgba(255, 255, 255, 0.9);
|
||||
--color-list-item-hover: rgba(255, 255, 255, 0.5);
|
||||
--color-list-item: #eee;
|
||||
--color-list-item-hover: #f5f5f5;
|
||||
|
||||
--modal-background: var(--color-white);
|
||||
|
||||
@@ -137,6 +136,4 @@
|
||||
--chat-background-user: #95ec69;
|
||||
--chat-background-assistant: #ffffff;
|
||||
--chat-text-user: var(--color-text);
|
||||
|
||||
--border-width: 0.5px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
#content-container {
|
||||
background-color: var(--color-background);
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
}
|
||||
|
||||
.group-container {
|
||||
.context-menu-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
max-width: 100%;
|
||||
border-top-left-radius: 10px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
// margin: 0;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -129,22 +129,29 @@ ul {
|
||||
.message-content-container {
|
||||
margin: 5px 0;
|
||||
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 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
color: var(--chat-text-user);
|
||||
.markdown,
|
||||
.anticon,
|
||||
.iconfont,
|
||||
.lucide,
|
||||
.message-tokens {
|
||||
.message-content-container-user .anticon {
|
||||
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,
|
||||
@@ -165,6 +172,12 @@ ul {
|
||||
code {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.markdown {
|
||||
display: flow-root;
|
||||
*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lucide {
|
||||
|
||||
@@ -295,13 +295,16 @@ emoji-picker {
|
||||
--border-size: 0;
|
||||
}
|
||||
|
||||
.katex-display {
|
||||
.katex,
|
||||
mjx-container {
|
||||
display: inline-block;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
mjx-container {
|
||||
overflow-x: auto;
|
||||
overflow-wrap: break-word;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
padding: 1px 2px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
/* CodeMirror 相关样式 */
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
@import 'tailwindcss' source('../../../src');
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 如需自定义:
|
||||
1. 清晰地组织自定义 CSS 到相应的层中。
|
||||
2. 基础样式(如全局重置、链接样式)放入 base 层;
|
||||
3. 可复用的组件样式(如果仍使用 @apply 或原生 CSS 嵌套创建)放入 components 层;
|
||||
4. 新的自定义工具类放入 utilities 层。
|
||||
*/
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-marquee: marquee var(--duration) infinite linear;
|
||||
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ interface Props {
|
||||
onSave?: (newContent: string) => void
|
||||
onChange?: (newContent: string) => void
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
height?: string
|
||||
minHeight?: string
|
||||
maxHeight?: string
|
||||
/** 用于覆写编辑器的某些设置 */
|
||||
@@ -54,6 +55,7 @@ const CodeEditor = ({
|
||||
onSave,
|
||||
onChange,
|
||||
setTools,
|
||||
height,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
options,
|
||||
@@ -193,6 +195,7 @@ const CodeEditor = ({
|
||||
value={initialContent.current}
|
||||
placeholder={placeholder}
|
||||
width="100%"
|
||||
height={height}
|
||||
minHeight={minHeight}
|
||||
maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'}
|
||||
editable={true}
|
||||
|
||||
@@ -6,9 +6,10 @@ import styled from 'styled-components'
|
||||
interface ContextMenuProps {
|
||||
children: React.ReactNode
|
||||
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 [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
|
||||
const [selectedText, setSelectedText] = useState<string>('')
|
||||
@@ -66,7 +67,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
]
|
||||
|
||||
return (
|
||||
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container">
|
||||
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container" style={style}>
|
||||
{contextMenuPosition && (
|
||||
<Dropdown
|
||||
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
|
||||
|
||||
@@ -395,7 +395,10 @@ const MinappPopupContainer: React.FC = () => {
|
||||
height={'100%'}
|
||||
maskClosable={false}
|
||||
closeIcon={null}
|
||||
style={{ backgroundColor: window.root.style.background }}>
|
||||
style={{
|
||||
marginLeft: 'var(--sidebar-width)',
|
||||
backgroundColor: window.root.style.background
|
||||
}}>
|
||||
{!isReady && (
|
||||
<EmptyView>
|
||||
<Avatar
|
||||
@@ -415,7 +418,7 @@ const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${isMac ? '80px' : '10px'};
|
||||
padding-left: ${isMac ? '20px' : '10px'};
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -78,7 +78,7 @@ const WebviewContainer = memo(
|
||||
)
|
||||
|
||||
const WebviewStyle: React.CSSProperties = {
|
||||
width: '100vw',
|
||||
width: 'calc(100vw - var(--sidebar-width))',
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
display: 'inline-flex'
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { MessageSquareDiff, Search } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SearchPopup from '../Popups/SearchPopup'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const HeaderNavbar: FC<Props> = () => {
|
||||
return (
|
||||
<Container>
|
||||
<div>
|
||||
{!isMac && (
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
|
||||
<MessageSquareDiff size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: var(--assistant-width);
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
padding-left: var(--sidebar-width);
|
||||
height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
background-color: transparent;
|
||||
-webkit-app-region: drag;
|
||||
padding-left: ${isMac ? '75px' : '0'};
|
||||
`
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-appstore {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
@@ -1,379 +0,0 @@
|
||||
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
|
||||
import { UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useChat } from '@renderer/hooks/useChat'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { isEmoji } from '@renderer/utils'
|
||||
import { Avatar, Tooltip } from 'antd'
|
||||
import {
|
||||
Blocks,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Compass,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Languages,
|
||||
MessageSquare,
|
||||
Moon,
|
||||
Palette,
|
||||
SquareTerminal,
|
||||
Sun,
|
||||
SunMoon
|
||||
} from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Tabs from '../../pages/home/Tabs'
|
||||
import MainNavbar from './MainNavbar'
|
||||
|
||||
type Tab = 'assistants' | 'topic' | 'settings'
|
||||
|
||||
const MainSidebar: FC = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const navigate = useNavigate()
|
||||
const [tab, setTab] = useState<Tab>('assistants')
|
||||
const avatar = useAvatar()
|
||||
const { userName, defaultPaintingProvider } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const { theme, settedTheme, toggleTheme } = useTheme()
|
||||
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
|
||||
|
||||
const location = useLocation()
|
||||
const { pathname } = location
|
||||
|
||||
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
|
||||
useEffect(() => {
|
||||
NavigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, () => setTab('topic'))
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
|
||||
const newAssistant = assistants.find((a) => a.id === assistantId)
|
||||
if (newAssistant) {
|
||||
setActiveAssistant(newAssistant)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [assistants, setActiveAssistant])
|
||||
|
||||
useEffect(() => {
|
||||
const canMinimize = !showAssistants && !showTopics
|
||||
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
|
||||
|
||||
return () => {
|
||||
window.api.window.resetMinimumSize()
|
||||
}
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
useEffect(() => {
|
||||
setIsAppMenuExpanded(false)
|
||||
}, [activeAssistant.id, activeTopic.id])
|
||||
|
||||
const onAvatarClick = () => {
|
||||
navigate('/settings/provider')
|
||||
}
|
||||
|
||||
const onChageTheme = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
toggleTheme()
|
||||
}
|
||||
|
||||
const appMenuItems = [
|
||||
// { icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
|
||||
{ icon: <Compass size={18} className="icon" />, text: t('discover.title'), path: '/discover' },
|
||||
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
|
||||
{
|
||||
icon: <Palette size={18} className="icon" />,
|
||||
text: t('paintings.title'),
|
||||
path: `/paintings/${defaultPaintingProvider}`
|
||||
},
|
||||
// { icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
|
||||
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
|
||||
{ icon: <SquareTerminal size={18} className="icon" />, text: t('common.mcp'), path: '/mcp-servers' },
|
||||
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
|
||||
]
|
||||
|
||||
const isRoutes = (path: string): boolean => pathname.startsWith(path)
|
||||
|
||||
const onChageTab = (tab: Tab) => {
|
||||
setTab(tab)
|
||||
setIsAppMenuExpanded(false)
|
||||
}
|
||||
|
||||
if (!showAssistants) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (location.pathname !== '/') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container id="main-sidebar">
|
||||
<MainNavbar />
|
||||
<MainMenu>
|
||||
<MainMenuItem
|
||||
active={tab === 'assistants' && location.pathname === '/'}
|
||||
onClick={() => onChageTab('assistants')}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>
|
||||
<Bot size={18} />
|
||||
</MainMenuItemIcon>
|
||||
<MainMenuItemText>{t('assistants.title')}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
{tab === 'topic' && (
|
||||
<MainMenuItemRight>
|
||||
<MainMenuItemRightText>{activeAssistant.name}</MainMenuItemRightText>
|
||||
</MainMenuItemRight>
|
||||
)}
|
||||
</MainMenuItem>
|
||||
<MainMenuItem active={tab === 'topic' && location.pathname === '/'} onClick={() => onChageTab('topic')}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>
|
||||
<MessageSquare size={18} />
|
||||
</MainMenuItemIcon>
|
||||
<MainMenuItemText>{t('common.topics')}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
</MainMenuItem>
|
||||
<MainMenuItem
|
||||
style={{ opacity: isAppMenuExpanded ? 0.5 : 1 }}
|
||||
onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>
|
||||
<Blocks size={19} className="icon" />
|
||||
</MainMenuItemIcon>
|
||||
<MainMenuItemText>{t('common.apps')}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
<MainMenuItemRight>
|
||||
{isAppMenuExpanded ? (
|
||||
<ChevronDown size={18} color="var(--color-text-3)" />
|
||||
) : (
|
||||
<ChevronRight size={18} color="var(--color-text-3)" />
|
||||
)}
|
||||
</MainMenuItemRight>
|
||||
</MainMenuItem>
|
||||
{isAppMenuExpanded && (
|
||||
<SubMenu>
|
||||
{appMenuItems.map((item) => (
|
||||
<MainMenuItem key={item.path} active={isRoutes(item.path)} onClick={() => navigate(item.path)}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
|
||||
<MainMenuItemText>{item.text}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
</MainMenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
)}
|
||||
</MainMenu>
|
||||
<Tabs
|
||||
tab={tab}
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
setActiveTopic={setActiveTopic}
|
||||
/>
|
||||
<UserMenu onClick={onAvatarClick}>
|
||||
<UserMenuLeft>
|
||||
{isEmoji(avatar) ? (
|
||||
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
|
||||
{avatar}
|
||||
</EmojiAvatar>
|
||||
) : (
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
|
||||
)}
|
||||
<UserMenuText>{userName}</UserMenuText>
|
||||
</UserMenuLeft>
|
||||
<Tooltip
|
||||
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settedTheme}`)}
|
||||
mouseEnterDelay={0.8}
|
||||
placement="right">
|
||||
<Icon theme={theme} onClick={onChageTheme}>
|
||||
{settedTheme === ThemeMode.dark ? (
|
||||
<Moon size={18} className="icon" />
|
||||
) : settedTheme === ThemeMode.light ? (
|
||||
<Sun size={18} className="icon" />
|
||||
) : (
|
||||
<SunMoon size={18} className="icon" />
|
||||
)}
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
</UserMenu>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--assistant-width);
|
||||
max-width: var(--assistant-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const MainMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const MainMenuItem = styled.div<{ active?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')};
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
|
||||
}
|
||||
`
|
||||
|
||||
const MainMenuItemLeft = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const MainMenuItemRight = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const MainMenuItemRightText = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const MainMenuItemIcon = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
const MainMenuItemText = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const UserMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 8px;
|
||||
gap: 5px;
|
||||
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background-color: var(--color-list-item);
|
||||
}
|
||||
`
|
||||
|
||||
const UserMenuLeft = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const AvatarImg = styled(Avatar)`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const UserMenuText = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const Icon = styled.div<{ theme: string }>`
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
-webkit-app-region: none;
|
||||
border: 0.5px solid transparent;
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
.icon {
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
|
||||
border: 0.5px solid var(--color-border);
|
||||
.icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderBreath {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&.opened-minapp {
|
||||
position: relative;
|
||||
}
|
||||
&.opened-minapp::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: inherit;
|
||||
opacity: 0.3;
|
||||
border: 0.5px solid var(--color-primary);
|
||||
}
|
||||
`
|
||||
|
||||
const SubMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
export default MainSidebar
|
||||
@@ -1,16 +1,24 @@
|
||||
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronDown, X } from 'lucide-react'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
return <NavbarContainer {...props}>{children}</NavbarContainer>
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
|
||||
return (
|
||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
||||
{children}
|
||||
</NavbarContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
|
||||
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
|
||||
}
|
||||
|
||||
export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
|
||||
@@ -28,54 +36,41 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
|
||||
|
||||
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
||||
const isFullscreen = useFullscreen()
|
||||
|
||||
return (
|
||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
||||
<CloseIconWindows />
|
||||
{children}
|
||||
<CloseIconMac />
|
||||
</NavbarMainContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseIconMac = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!isMac) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Button type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag" />
|
||||
}
|
||||
|
||||
const CloseIconWindows = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (isMac) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
shape="circle"
|
||||
icon={<ChevronDown size={16} />}
|
||||
onClick={() => navigate('/')}
|
||||
className="nodrag"
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const NavbarContainer = styled.div`
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
||||
-webkit-app-region: drag;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const NavbarLeftContainer = styled.div`
|
||||
min-width: var(--assistants-width);
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
const NavbarCenterContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 ${isMac ? '20px' : 0};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||
@@ -92,26 +87,9 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
justify-content: space-between;
|
||||
padding-left: ${isMac ? '70px' : '10px'};
|
||||
padding: 0 ${isMac ? '20px' : 0};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
const NavbarCenterContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
padding: 0 8px;
|
||||
font-weight: bold;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
@@ -16,7 +16,6 @@ import type { MenuProps } from 'antd'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import {
|
||||
CircleHelp,
|
||||
Compass,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Languages,
|
||||
@@ -156,8 +155,7 @@ const MainMenus: FC = () => {
|
||||
translate: <Languages size={18} className="icon" />,
|
||||
minapp: <LayoutGrid size={18} className="icon" />,
|
||||
knowledge: <FileSearch size={18} className="icon" />,
|
||||
files: <Folder size={17} className="icon" />,
|
||||
discover: <Compass size={18} className="icon" />
|
||||
files: <Folder size={17} className="icon" />
|
||||
}
|
||||
|
||||
const pathMap = {
|
||||
@@ -167,8 +165,7 @@ const MainMenus: FC = () => {
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files',
|
||||
discover: '/discover'
|
||||
files: '/files'
|
||||
}
|
||||
|
||||
return sidebarIcons.visible.map((icon) => {
|
||||
|
||||
@@ -140,6 +140,8 @@ import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
|
||||
import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.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 { Assistant, Model } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
@@ -297,7 +299,7 @@ export function getModelLogo(modelId: string) {
|
||||
'davinci-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
|
||||
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
|
||||
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
|
||||
'(qwen|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
|
||||
'(qwen|qwq|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
|
||||
gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,
|
||||
'yi-': isLight ? YiModelLogo : YiModelLogoDark,
|
||||
llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,
|
||||
@@ -376,12 +378,14 @@ export function getModelLogo(modelId: string) {
|
||||
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
|
||||
xirang: isLight ? XirangModelLogo : XirangModelLogoDark,
|
||||
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
|
||||
youdao: YoudaoLogo,
|
||||
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
|
||||
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
|
||||
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
|
||||
'bge-': BgeModelLogo,
|
||||
'voyage-': VoyageModelLogo,
|
||||
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark
|
||||
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark,
|
||||
'nomic-': NomicLogo
|
||||
}
|
||||
|
||||
for (const key in logoMap) {
|
||||
@@ -425,7 +429,86 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
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: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
@@ -2078,6 +2161,14 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'Qwen Plus',
|
||||
group: 'Qwen'
|
||||
}
|
||||
],
|
||||
cephalon: [
|
||||
{
|
||||
id: 'DeepSeek-R1',
|
||||
provider: 'cephalon',
|
||||
name: 'DeepSeek-R1满血版',
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
|
||||
`
|
||||
|
||||
export const SUMMARIZE_PROMPT =
|
||||
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks, markdown language markers, or other special symbols"
|
||||
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks or other special symbols"
|
||||
|
||||
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
|
||||
export const SEARCH_SUMMARY_PROMPT = `
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
|
||||
import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.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 AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp'
|
||||
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 BailianProviderLogo from '@renderer/assets/images/providers/bailian.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 DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.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'
|
||||
|
||||
const PROVIDER_LOGO_MAP = {
|
||||
'302ai': Ai302ProviderLogo,
|
||||
openai: OpenAiProviderLogo,
|
||||
silicon: SiliconFlowProviderLogo,
|
||||
deepseek: DeepSeekProviderLogo,
|
||||
@@ -94,7 +97,8 @@ const PROVIDER_LOGO_MAP = {
|
||||
alayanew: AlayaNewProviderLogo,
|
||||
voyageai: VoyageAIProviderLogo,
|
||||
qiniu: QiniuProviderLogo,
|
||||
tokenflux: TokenFluxProviderLogo
|
||||
tokenflux: TokenFluxProviderLogo,
|
||||
cephalon: CephalonProviderLogo
|
||||
} as const
|
||||
|
||||
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 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: {
|
||||
api: {
|
||||
url: 'https://api.openai.com'
|
||||
@@ -612,5 +627,16 @@ export const PROVIDER_CONFIG = {
|
||||
docs: `${TOKENFLUX_HOST}/docs`,
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,22 +38,8 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
setSettedTheme(nextTheme || ThemeMode.system)
|
||||
}
|
||||
|
||||
const tailwindThemeChange = (theme) => {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(theme)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.api?.setTheme(settedTheme || actualTheme)
|
||||
}, [settedTheme, actualTheme])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('theme-mode', settedTheme)
|
||||
tailwindThemeChange(settedTheme)
|
||||
}, [settedTheme])
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial theme and OS attributes on body
|
||||
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
|
||||
document.body.setAttribute('theme-mode', actualTheme)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import './assets/styles/tailwind.css'
|
||||
import './assets/styles/index.scss'
|
||||
import '@ant-design/v5-patch-for-react-19'
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useAssistants } from './useAssistant'
|
||||
import { useSettings } from './useSettings'
|
||||
|
||||
export const useChat = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
|
||||
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
|
||||
const { clickAssistantToShowTopic } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTopic) {
|
||||
dispatch(loadTopicMessagesThunk(activeTopic.id))
|
||||
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
|
||||
}
|
||||
}, [activeTopic, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
const firstTopic = activeAssistant.topics[0]
|
||||
firstTopic && dispatch(setActiveTopic(firstTopic))
|
||||
}, [activeAssistant, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (clickAssistantToShowTopic) {
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
}
|
||||
}, [clickAssistantToShowTopic, activeAssistant])
|
||||
|
||||
return {
|
||||
activeAssistant,
|
||||
activeTopic,
|
||||
setActiveAssistant: (assistant: Assistant) => {
|
||||
dispatch(setActiveAssistant(assistant))
|
||||
},
|
||||
setActiveTopic: (topic: Topic) => {
|
||||
dispatch(setActiveTopic(topic))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { flatMap, groupBy, uniq } from 'lodash'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
@@ -6,15 +7,19 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useAssistants } from './useAssistant'
|
||||
|
||||
// 基础选择器
|
||||
const selectAssistantsState = (state: RootState) => state.assistants
|
||||
// 记忆化 tagsOrder 选择器(自动处理默认值)--- 这是一个选择器,用于从 store 中获取 tagsOrder 的值。因为之前的tagsOrder是后面新加的,不这样做会报错,所以这里需要处理一下默认值
|
||||
const selectTagsOrder = createSelector([selectAssistantsState], (assistants) => assistants.tagsOrder ?? [])
|
||||
|
||||
// 定义useTags的返回类型,包含所有标签和获取特定标签的助手函数
|
||||
// 为了不增加新的概念,标签直接作为助手的属性,所以这里的标签是指助手的标签属性
|
||||
// 但是为了方便管理,增加了一个获取特定标签的助手函数
|
||||
|
||||
export const useTags = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || [])
|
||||
const savedTagsOrder = useAppSelector(selectTagsOrder)
|
||||
|
||||
// 计算所有标签
|
||||
const allTags = useMemo(() => {
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { updateTopic } from '@renderer/store/assistants'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { find, isEmpty } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useAssistant } from './useAssistant'
|
||||
import { getStoreSetting } from './useSettings'
|
||||
|
||||
const renamingTopics = new Set<string>()
|
||||
|
||||
let _activeTopic: Topic
|
||||
let _setActiveTopic: (topic: Topic) => void
|
||||
|
||||
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
|
||||
const { assistant } = useAssistant(_assistant.id)
|
||||
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
|
||||
|
||||
_activeTopic = activeTopic
|
||||
_setActiveTopic = setActiveTopic
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTopic) {
|
||||
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
|
||||
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
|
||||
}
|
||||
}, [activeTopic])
|
||||
|
||||
useEffect(() => {
|
||||
// activeTopic not in assistant.topics
|
||||
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
}, [activeTopic?.id, assistant])
|
||||
|
||||
return { activeTopic, setActiveTopic }
|
||||
}
|
||||
|
||||
export function useTopic(assistant: Assistant, topicId?: string) {
|
||||
return assistant?.topics.find((topic) => topic.id === topicId)
|
||||
}
|
||||
@@ -55,6 +86,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
||||
.substring(0, 50)
|
||||
if (topicName) {
|
||||
const data = { ...topic, name: topicName } as Topic
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
}
|
||||
return
|
||||
@@ -65,9 +97,8 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
||||
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
||||
if (summaryText) {
|
||||
const data = { ...topic, name: summaryText }
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
} else {
|
||||
window.message?.error(i18n.t('message.error.fetchTopicName'))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"topics.clear.title": "Clear Messages",
|
||||
"topics.copy.image": "Copy as image",
|
||||
"topics.copy.md": "Copy as markdown",
|
||||
"topics.copy.plain_text": "Copy as plain text (remove Markdown)",
|
||||
"topics.copy.title": "Copy",
|
||||
"topics.delete.shortcut": "Hold {{key}} to delete directly",
|
||||
"topics.edit.placeholder": "Enter new name",
|
||||
@@ -421,9 +422,7 @@
|
||||
"pinyin.asc": "Sort by Pinyin (A-Z)",
|
||||
"pinyin.desc": "Sort by Pinyin (Z-A)"
|
||||
},
|
||||
"no_results": "No results",
|
||||
"apps": "Apps",
|
||||
"mcp": "Tools"
|
||||
"no_results": "No results"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Docs"
|
||||
@@ -567,8 +566,12 @@
|
||||
"urls": "URLs",
|
||||
"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_placeholder": "Default value (modification not recommended)",
|
||||
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})."
|
||||
"dimensions_size_placeholder": " Embedding dimension size, e.g. 1024",
|
||||
"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": {
|
||||
"arabic": "Arabic",
|
||||
@@ -975,6 +978,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Baichuan",
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
@@ -1015,7 +1019,8 @@
|
||||
"zhipu": "ZHIPU AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "Qiniu AI",
|
||||
"tokenflux": "TokenFlux"
|
||||
"tokenflux": "TokenFlux",
|
||||
"302ai": "302.AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "Are you sure you want to restore data?",
|
||||
@@ -1514,6 +1519,7 @@
|
||||
"messages.prompt": "Show prompt",
|
||||
"messages.tokens": "Show token usage",
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.divider.tooltip": "Not applicable to bubble-style message",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||
"messages.grid_popover_trigger.click": "Click to display",
|
||||
@@ -1546,6 +1552,7 @@
|
||||
"models.add.model_id.select.placeholder": "Select Model",
|
||||
"models.add.model_id.tooltip": "Example: gpt-3.5-turbo",
|
||||
"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.check.all": "All",
|
||||
"models.check.all_models_passed": "All models check passed",
|
||||
@@ -1697,6 +1704,8 @@
|
||||
"exit_fullscreen": "Exit Fullscreen",
|
||||
"key": "Key",
|
||||
"mini_window": "Quick Assistant",
|
||||
"selection_assistant_toggle": "Toggle Selection Assistant",
|
||||
"selection_assistant_select_text": "Selection Assistant: Select Text",
|
||||
"new_topic": "New Topic",
|
||||
"press_shortcut": "Press Shortcut",
|
||||
"reset_defaults": "Reset Defaults",
|
||||
@@ -1822,7 +1831,7 @@
|
||||
"close": "Close",
|
||||
"closed": "Translation closed",
|
||||
"copied": "Translation content copied",
|
||||
"detected.language": "Detected Language",
|
||||
"detected.language": "Auto Detect",
|
||||
"empty": "Translation content is empty",
|
||||
"not.found": "Translation content not found",
|
||||
"confirm": {
|
||||
@@ -1913,10 +1922,15 @@
|
||||
"title": "Toolbar",
|
||||
"trigger_mode": {
|
||||
"title": "Trigger Mode",
|
||||
"description": "Show toolbar immediately when text is selected, or show only when Ctrl key is held after selection.",
|
||||
"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": "The way to trigger the selection assistant and show the toolbar",
|
||||
"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",
|
||||
"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": {
|
||||
"title": "Compact Mode",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"topics.clear.title": "メッセージをクリア",
|
||||
"topics.copy.image": "画像としてコピー",
|
||||
"topics.copy.md": "Markdownとしてコピー",
|
||||
"topics.copy.plain_text": "プレーンテキストとしてコピー(Markdownを除去)",
|
||||
"topics.copy.title": "コピー",
|
||||
"topics.delete.shortcut": "{{key}}キーを押しながらで直接削除",
|
||||
"topics.edit.placeholder": "新しい名前を入力",
|
||||
@@ -421,9 +422,7 @@
|
||||
"pinyin.asc": "ピンインで昇順ソート",
|
||||
"pinyin.desc": "ピンインで降順ソート"
|
||||
},
|
||||
"no_results": "検索結果なし",
|
||||
"apps": "アプリ",
|
||||
"mcp": "ツール"
|
||||
"no_results": "検索結果なし"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ドキュメント"
|
||||
@@ -567,8 +566,12 @@
|
||||
"urls": "URL",
|
||||
"dimensions": "埋め込み次元",
|
||||
"dimensions_size_tooltip": "埋め込み次元のサイズは、数値が大きいほど埋め込み次元も大きくなりますが、消費するトークンも増えます。",
|
||||
"dimensions_size_placeholder": "デフォルト値(変更はお勧めしません)",
|
||||
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。"
|
||||
"dimensions_size_placeholder": " 埋め込み次元のサイズ(例:1024)",
|
||||
"dimensions_auto_set": "埋め込み次元を自動設定",
|
||||
"dimensions_error_invalid": "埋め込み次元のサイズを入力してください",
|
||||
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。",
|
||||
"dimensions_set_right": "⚠️ モデルが設定した埋め込み次元のサイズをサポートしていることを確認してください",
|
||||
"dimensions_default": "モデルはデフォルトの埋め込み次元を使用します"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "アラビア語",
|
||||
@@ -630,7 +633,6 @@
|
||||
"error.enter.api.key": "APIキーを入力してください",
|
||||
"error.enter.model": "モデルを選択してください",
|
||||
"error.enter.name": "ナレッジベース名を入力してください",
|
||||
"error.fetchTopicName": "トピックの命名に失敗しました",
|
||||
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
|
||||
"error.invalid.api.host": "無効なAPIアドレスです",
|
||||
"error.invalid.api.key": "無効なAPIキーです",
|
||||
@@ -701,7 +703,8 @@
|
||||
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
|
||||
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
|
||||
"download.success": "ダウンロードに成功しました",
|
||||
"download.failed": "ダウンロードに失敗しました"
|
||||
"download.failed": "ダウンロードに失敗しました",
|
||||
"error.fetchTopicName": "トピック名の取得に失敗しました"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
@@ -1015,7 +1018,9 @@
|
||||
"zhipu": "智譜AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "七牛云 AI 推理",
|
||||
"tokenflux": "TokenFlux"
|
||||
"tokenflux": "TokenFlux",
|
||||
"302ai": "302.AI",
|
||||
"cephalon": "Cephalon"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "データを復元しますか?",
|
||||
@@ -1508,6 +1513,7 @@
|
||||
"messages.prompt": "プロンプト表示",
|
||||
"messages.tokens": "トークン使用量を表示",
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.divider.tooltip": "バブルスタイルのメッセージには適用されません",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||
"messages.grid_popover_trigger.click": "クリックで表示",
|
||||
@@ -1540,7 +1546,8 @@
|
||||
"models.add.model_id.select.placeholder": "モデルを選択",
|
||||
"models.add.model_id.tooltip": "例:gpt-3.5-turbo",
|
||||
"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_passed": "すべてのモデルチェックが成功しました",
|
||||
"models.check.button_caption": "健康チェック",
|
||||
@@ -1685,6 +1692,8 @@
|
||||
"exit_fullscreen": "フルスクリーンを終了",
|
||||
"key": "キー",
|
||||
"mini_window": "クイックアシスタント",
|
||||
"selection_assistant_toggle": "選択アシスタントを切り替え",
|
||||
"selection_assistant_select_text": "選択アシスタント:テキストを選択",
|
||||
"new_topic": "新しいトピック",
|
||||
"press_shortcut": "ショートカットを押す",
|
||||
"reset_defaults": "デフォルトのショートカットをリセット",
|
||||
@@ -1855,7 +1864,7 @@
|
||||
"menu": {
|
||||
"description": "對當前輸入框內容進行翻譯"
|
||||
},
|
||||
"detected.language": "検出された言語"
|
||||
"detected.language": "自動検出"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "終了",
|
||||
@@ -1912,11 +1921,16 @@
|
||||
"toolbar": {
|
||||
"title": "ツールバー",
|
||||
"trigger_mode": {
|
||||
"title": "表示方法",
|
||||
"description": "テキスト選択時に即時表示、またはCtrlキー押下時のみ表示",
|
||||
"description_note": "一部のアプリはCtrlキーでのテキスト選択に対応していません。AHKなどでCtrlキーをリマップすると、選択できなくなる場合があります。",
|
||||
"title": "単語の取り出し方",
|
||||
"description": "テキスト選択後、取詞ツールバーを表示する方法",
|
||||
"description_note": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
|
||||
"selected": "選択時",
|
||||
"ctrlkey": "Ctrlキー"
|
||||
"selected_note": "テキスト選択時に即時表示",
|
||||
"ctrlkey": "Ctrlキー",
|
||||
"ctrlkey_note": "テキスト選択後、Ctrlキーを押下して表示",
|
||||
"shortcut": "ショートカットキー",
|
||||
"shortcut_note": "テキスト選択後、ショートカットキーを押下して表示。ショートカットキーを設定するには、ショートカット設定ページで有効にしてください。",
|
||||
"shortcut_link": "ショートカット設定ページに移動"
|
||||
},
|
||||
"compact_mode": {
|
||||
"title": "コンパクトモード",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"topics.clear.title": "Очистить сообщения",
|
||||
"topics.copy.image": "Скопировать как изображение",
|
||||
"topics.copy.md": "Скопировать как Markdown",
|
||||
"topics.copy.plain_text": "Копировать как обычный текст (удалить Markdown)",
|
||||
"topics.copy.title": "Скопировать",
|
||||
"topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления",
|
||||
"topics.edit.placeholder": "Введите новый заголовок",
|
||||
@@ -421,9 +422,7 @@
|
||||
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
|
||||
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
|
||||
},
|
||||
"no_results": "Результатов не найдено",
|
||||
"apps": "Приложения",
|
||||
"mcp": "Инструменты"
|
||||
"no_results": "Результатов не найдено"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация"
|
||||
@@ -567,8 +566,12 @@
|
||||
"urls": "URL-адреса",
|
||||
"dimensions": "векторное пространство",
|
||||
"dimensions_size_tooltip": "Размерность вложения, чем больше значение, тем больше размерность вложения, но и потребляемых токенов также становится больше.",
|
||||
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
|
||||
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})"
|
||||
"dimensions_size_placeholder": " Размерность эмбеддинга, например 1024",
|
||||
"dimensions_auto_set": "Автоматическая установка размерности эмбеддинга",
|
||||
"dimensions_error_invalid": "Пожалуйста, введите размерность эмбеддинга",
|
||||
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})",
|
||||
"dimensions_set_right": "⚠️ Убедитесь, что модель поддерживает заданный размер эмбеддинга",
|
||||
"dimensions_default": "Модель будет использовать размер эмбеддинга по умолчанию"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "Арабский",
|
||||
@@ -630,7 +633,6 @@
|
||||
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
|
||||
"error.enter.model": "Пожалуйста, выберите модель",
|
||||
"error.enter.name": "Пожалуйста, введите название базы знаний",
|
||||
"error.fetchTopicName": "Не удалось назвать тему",
|
||||
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
|
||||
"error.invalid.api.host": "Неверный API адрес",
|
||||
"error.invalid.api.key": "Неверный API ключ",
|
||||
@@ -701,7 +703,8 @@
|
||||
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
|
||||
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
|
||||
"download.success": "Скачано успешно",
|
||||
"download.failed": "Скачивание не удалось"
|
||||
"download.failed": "Скачивание не удалось",
|
||||
"error.fetchTopicName": "Не удалось назвать топик"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
@@ -975,6 +978,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Baichuan",
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
@@ -1015,7 +1019,8 @@
|
||||
"zhipu": "ZHIPU AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "Qiniu AI",
|
||||
"tokenflux": "TokenFlux"
|
||||
"tokenflux": "TokenFlux",
|
||||
"302ai": "302.AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "Вы уверены, что хотите восстановить данные?",
|
||||
@@ -1508,6 +1513,7 @@
|
||||
"messages.prompt": "Показывать подсказки",
|
||||
"messages.tokens": "Показать использование токенов",
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.divider.tooltip": "Не применимо к сообщениям в стиле пузырей",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||
"messages.grid_popover_trigger.click": "Нажатие для отображения",
|
||||
@@ -1540,6 +1546,7 @@
|
||||
"models.add.model_id.select.placeholder": "Выберите модель",
|
||||
"models.add.model_id.tooltip": "Пример: gpt-3.5-turbo",
|
||||
"models.add.model_name": "Имя модели",
|
||||
"models.add.model_name.tooltip": "Необязательно, например, GPT-4",
|
||||
"models.add.model_name.placeholder": "Необязательно, например, GPT-4",
|
||||
"models.check.all": "Все",
|
||||
"models.check.all_models_passed": "Все модели прошли проверку",
|
||||
@@ -1685,6 +1692,8 @@
|
||||
"exit_fullscreen": "Выйти из полноэкранного режима",
|
||||
"key": "Клавиша",
|
||||
"mini_window": "Быстрый помощник",
|
||||
"selection_assistant_toggle": "Переключить помощник выделения",
|
||||
"selection_assistant_select_text": "Помощник выделения: выделить текст",
|
||||
"new_topic": "Новый топик",
|
||||
"press_shortcut": "Нажмите сочетание клавиш",
|
||||
"reset_defaults": "Сбросить настройки по умолчанию",
|
||||
@@ -1855,7 +1864,7 @@
|
||||
"menu": {
|
||||
"description": "Перевести содержимое текущего ввода"
|
||||
},
|
||||
"detected.language": "Обнаруженный язык"
|
||||
"detected.language": "Автоматическое обнаружение"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Выйти",
|
||||
@@ -1913,10 +1922,15 @@
|
||||
"title": "Панель инструментов",
|
||||
"trigger_mode": {
|
||||
"title": "Режим активации",
|
||||
"description": "Показывать панель сразу при выделении или только при удержании Ctrl.",
|
||||
"description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш",
|
||||
"description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
|
||||
"selected": "При выделении",
|
||||
"ctrlkey": "По Ctrl"
|
||||
"selected_note": "После выделения",
|
||||
"ctrlkey": "По Ctrl",
|
||||
"ctrlkey_note": "После выделения, удерживайте Ctrl для показа панели. Пожалуйста, установите Ctrl в настройках клавиатуры и активируйте его.",
|
||||
"shortcut": "По сочетанию клавиш",
|
||||
"shortcut_note": "После выделения, используйте сочетание клавиш для показа панели. Пожалуйста, установите сочетание клавиш в настройках клавиатуры и активируйте его.",
|
||||
"shortcut_link": "Перейти к настройкам клавиатуры"
|
||||
},
|
||||
"compact_mode": {
|
||||
"title": "Компактный режим",
|
||||
|
||||
@@ -279,6 +279,7 @@
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.copy.image": "复制为图片",
|
||||
"topics.copy.md": "复制为 Markdown",
|
||||
"topics.copy.plain_text": "复制为纯文本(去除 Markdown)",
|
||||
"topics.copy.title": "复制",
|
||||
"topics.delete.shortcut": "按住 {{key}} 可直接删除",
|
||||
"topics.edit.placeholder": "输入新名称",
|
||||
@@ -421,9 +422,7 @@
|
||||
"pinyin.asc": "按拼音升序",
|
||||
"pinyin.desc": "按拼音降序"
|
||||
},
|
||||
"no_results": "无结果",
|
||||
"apps": "应用",
|
||||
"mcp": "工具"
|
||||
"no_results": "无结果"
|
||||
},
|
||||
"docs": {
|
||||
"title": "帮助文档"
|
||||
@@ -520,7 +519,11 @@
|
||||
"delete_confirm": "确定要删除此知识库吗?",
|
||||
"dimensions": "嵌入维度",
|
||||
"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}})",
|
||||
"directories": "目录",
|
||||
"directory_placeholder": "请输入目录路径",
|
||||
@@ -975,6 +978,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"baidu-cloud": "百度云千帆",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "阿里云百炼",
|
||||
"deepseek": "深度求索",
|
||||
@@ -1015,7 +1019,8 @@
|
||||
"zhipu": "智谱AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "七牛云 AI 推理",
|
||||
"tokenflux": "TokenFlux"
|
||||
"tokenflux": "TokenFlux",
|
||||
"302ai": "302.AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "确定要恢复数据吗?",
|
||||
@@ -1514,6 +1519,7 @@
|
||||
"messages.prompt": "显示提示词",
|
||||
"messages.tokens": "显示Token用量",
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.divider.tooltip": "不适用于气泡样式消息",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
"messages.grid_popover_trigger": "网格详情触发",
|
||||
"messages.grid_popover_trigger.click": "点击显示",
|
||||
@@ -1546,7 +1552,8 @@
|
||||
"models.add.model_id.select.placeholder": "选择模型",
|
||||
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
|
||||
"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_passed": "所有模型检测通过",
|
||||
"models.check.button_caption": "健康检测",
|
||||
@@ -1697,6 +1704,8 @@
|
||||
"exit_fullscreen": "退出全屏",
|
||||
"key": "按键",
|
||||
"mini_window": "快捷助手",
|
||||
"selection_assistant_toggle": "开关划词助手",
|
||||
"selection_assistant_select_text": "划词助手:取词",
|
||||
"new_topic": "新建话题",
|
||||
"press_shortcut": "按下快捷键",
|
||||
"reset_defaults": "重置默认快捷键",
|
||||
@@ -1814,13 +1823,6 @@
|
||||
"service_tier.flex": "灵活"
|
||||
}
|
||||
},
|
||||
"discover": {
|
||||
"title": "发现",
|
||||
"install": "安装",
|
||||
"uninstall": "卸载",
|
||||
"update": "更新",
|
||||
"update_all": "全部更新"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意语言",
|
||||
"target_language": "目标语言",
|
||||
@@ -1862,7 +1864,7 @@
|
||||
},
|
||||
"title": "翻译",
|
||||
"tooltip.newline": "换行",
|
||||
"detected.language": "检测到的语言"
|
||||
"detected.language": "自动检测"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
@@ -1919,11 +1921,16 @@
|
||||
"toolbar": {
|
||||
"title": "工具栏",
|
||||
"trigger_mode": {
|
||||
"title": "触发方式",
|
||||
"description": "划词立即显示工具栏,或者划词后按住 Ctrl 键才显示工具栏。",
|
||||
"title": "取词方式",
|
||||
"description": "划词后,触发取词并显示工具栏的方式",
|
||||
"description_note": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
|
||||
"selected": "划词",
|
||||
"ctrlkey": "Ctrl 键"
|
||||
"selected_note": "划词后立即显示工具栏",
|
||||
"ctrlkey": "Ctrl 键",
|
||||
"ctrlkey_note": "划词后,再 按住 Ctrl键,才显示工具栏",
|
||||
"shortcut": "快捷键",
|
||||
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
|
||||
"shortcut_link": "前往快捷键设置"
|
||||
},
|
||||
"compact_mode": {
|
||||
"title": "紧凑模式",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"topics.clear.title": "清空訊息",
|
||||
"topics.copy.image": "複製為圖片",
|
||||
"topics.copy.md": "複製為 Markdown",
|
||||
"topics.copy.plain_text": "複製為純文字(移除 Markdown)",
|
||||
"topics.copy.title": "複製",
|
||||
"topics.delete.shortcut": "按住 {{key}} 可直接刪除",
|
||||
"topics.edit.placeholder": "輸入新名稱",
|
||||
@@ -421,9 +422,7 @@
|
||||
"pinyin.asc": "按拼音升序",
|
||||
"pinyin.desc": "按拼音降序"
|
||||
},
|
||||
"no_results": "沒有結果",
|
||||
"apps": "應用",
|
||||
"mcp": "工具"
|
||||
"no_results": "沒有結果"
|
||||
},
|
||||
"docs": {
|
||||
"title": "說明文件"
|
||||
@@ -567,8 +566,12 @@
|
||||
"urls": "網址",
|
||||
"dimensions": "嵌入維度",
|
||||
"dimensions_size_tooltip": "嵌入維度大小,數值越大,嵌入維度越大,但消耗的 Token 也越多",
|
||||
"dimensions_size_placeholder": "預設值(不建議修改)",
|
||||
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})"
|
||||
"dimensions_size_placeholder": " 嵌入維度大小,例如 1024",
|
||||
"dimensions_auto_set": "自動設定嵌入維度",
|
||||
"dimensions_error_invalid": "請輸入嵌入維度大小",
|
||||
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})",
|
||||
"dimensions_set_right": "⚠️ 請確保模型支援所設置的嵌入維度大小",
|
||||
"dimensions_default": "模型將使用預設嵌入維度"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "阿拉伯文",
|
||||
@@ -975,6 +978,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"baidu-cloud": "百度雲千帆",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "阿里雲百鍊",
|
||||
"deepseek": "深度求索",
|
||||
@@ -1015,7 +1019,8 @@
|
||||
"zhipu": "智譜 AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "七牛雲 AI 推理",
|
||||
"tokenflux": "TokenFlux"
|
||||
"tokenflux": "TokenFlux",
|
||||
"302ai": "302.AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "確定要復原資料嗎?",
|
||||
@@ -1511,6 +1516,7 @@
|
||||
"messages.prompt": "提示詞顯示",
|
||||
"messages.tokens": "Token用量顯示",
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.divider.tooltip": "不適用於氣泡樣式消息",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
"messages.grid_popover_trigger": "網格詳細資訊觸發",
|
||||
"messages.grid_popover_trigger.click": "點選顯示",
|
||||
@@ -1544,6 +1550,7 @@
|
||||
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
|
||||
"models.add.model_name": "模型名稱",
|
||||
"models.add.model_name.placeholder": "選填,例如 GPT-4",
|
||||
"models.add.model_name.tooltip": "例如 GPT-4",
|
||||
"models.check.all": "所有",
|
||||
"models.check.all_models_passed": "所有模型檢查通過",
|
||||
"models.check.button_caption": "健康檢查",
|
||||
@@ -1687,6 +1694,8 @@
|
||||
"copy_last_message": "複製上一則訊息",
|
||||
"key": "按鍵",
|
||||
"mini_window": "快捷助手",
|
||||
"selection_assistant_toggle": "開關劃詞助手",
|
||||
"selection_assistant_select_text": "劃詞助手:取词",
|
||||
"new_topic": "新增話題",
|
||||
"press_shortcut": "按下快捷鍵",
|
||||
"reset_defaults": "重設預設快捷鍵",
|
||||
@@ -1855,7 +1864,7 @@
|
||||
"menu": {
|
||||
"description": "對當前輸入框內容進行翻譯"
|
||||
},
|
||||
"detected.language": "檢測到的語言"
|
||||
"detected.language": "自動檢測"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "結束",
|
||||
@@ -1912,11 +1921,16 @@
|
||||
"toolbar": {
|
||||
"title": "工具列",
|
||||
"trigger_mode": {
|
||||
"title": "觸發方式",
|
||||
"description": "劃詞立即顯示工具列,或者劃詞後按住 Ctrl 鍵才顯示工具列。",
|
||||
"title": "取詞方式",
|
||||
"description": "劃詞後,觸發取詞並顯示工具列的方式",
|
||||
"description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了AHK等工具對Ctrl鍵進行了重新對應,可能導致部分應用程式無法劃詞。",
|
||||
"selected": "劃詞",
|
||||
"ctrlkey": "Ctrl 鍵"
|
||||
"selected_note": "劃詞後,立即顯示工具列",
|
||||
"ctrlkey": "Ctrl 鍵",
|
||||
"ctrlkey_note": "劃詞後,再 按住 Ctrl鍵,才顯示工具列",
|
||||
"shortcut": "快捷鍵",
|
||||
"shortcut_note": "劃詞後,使用快捷鍵顯示工具列。請在快捷鍵設定頁面中設置取詞快捷鍵並啟用。",
|
||||
"shortcut_link": "前往快捷鍵設定"
|
||||
},
|
||||
"compact_mode": {
|
||||
"title": "緊湊模式",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"topics.clear.title": "Καθαρισμός μηνυμάτων",
|
||||
"topics.copy.image": "Αντιγραφή ως εικόνα",
|
||||
"topics.copy.md": "Αντιγραφή ως Markdown",
|
||||
"topics.copy.plain_text": "Αντιγραφή ως απλό κείμενο (αφαίρεση Markdown)",
|
||||
"topics.copy.title": "Αντιγραφή",
|
||||
"topics.delete.shortcut": "Πατήστε {{key}} για να διαγράψετε αμέσως",
|
||||
"topics.edit.placeholder": "Εισαγάγετε το νέο όνομα",
|
||||
@@ -490,8 +491,12 @@
|
||||
"urls": "Διευθύνσεις",
|
||||
"dimensions": "Διαστάσεις ενσωμάτωσης",
|
||||
"dimensions_size_tooltip": "Το μέγεθος των διαστάσεων ενσωμάτωσης. Όσο μεγαλύτερη η τιμή, τόσο περισσότερες οι διαστάσεις ενσωμάτωσης, αλλά και οι απαιτούμενες μονάδες (Tokens).",
|
||||
"dimensions_size_placeholder": "Προεπιλεγμένη τιμή (δεν συνιστάται να τροποποιηθεί)",
|
||||
"dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})"
|
||||
"dimensions_size_placeholder": " Μέγεθος διαστάσεων ενσωμάτωσης, π.χ. 1024",
|
||||
"dimensions_auto_set": "Αυτόματη ρύθμιση διαστάσεων ενσωμάτωσης",
|
||||
"dimensions_error_invalid": "Παρακαλώ εισάγετε μέγεθος διαστάσεων ενσωμάτωσης",
|
||||
"dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})",
|
||||
"dimensions_set_right": "⚠️ Βεβαιωθείτε ότι το μοντέλο υποστηρίζει το καθορισμένο μέγεθος διαστάσεων ενσωμάτωσης",
|
||||
"dimensions_default": "Το μοντέλο θα χρησιμοποιήσει τις προεπιλεγμένες διαστάσεις ενσωμάτωσης"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "Αραβικά",
|
||||
@@ -551,7 +556,6 @@
|
||||
"error.enter.api.key": "Παρακαλώ εισάγετε το κλειδί API σας",
|
||||
"error.enter.model": "Παρακαλώ επιλέξτε ένα μοντέλο",
|
||||
"error.enter.name": "Παρακαλώ εισάγετε ένα όνομα για τη βάση γνώσεων",
|
||||
"error.fetchTopicName": "Αποτυχία ονοματοδοσίας θέματος",
|
||||
"error.get_embedding_dimensions": "Απέτυχε η πρόσληψη διαστάσεων ενσωμάτωσης",
|
||||
"error.invalid.api.host": "Μη έγκυρη διεύθυνση API",
|
||||
"error.invalid.api.key": "Μη έγκυρο κλειδί API",
|
||||
@@ -836,6 +840,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Παράκειμαι",
|
||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "AliCloud Bailian",
|
||||
"deepseek": "Βαθιά Αναζήτηση",
|
||||
@@ -1300,6 +1305,7 @@
|
||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις"
|
||||
},
|
||||
"messages.divider": "Διαχωριστική γραμμή μηνυμάτων",
|
||||
"messages.divider.tooltip": "Δεν ισχύει για μηνύματα με στυλ φυσαλίδας",
|
||||
"messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων",
|
||||
"messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid",
|
||||
"messages.grid_popover_trigger.click": "Εμφάνιση κλικ",
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"topics.clear.title": "Limpiar mensajes",
|
||||
"topics.copy.image": "Copiar como imagen",
|
||||
"topics.copy.md": "Copiar como Markdown",
|
||||
"topics.copy.plain_text": "Copiar como texto sin formato (eliminar Markdown)",
|
||||
"topics.copy.title": "Copiar",
|
||||
"topics.delete.shortcut": "Mantén presionada {{key}} para eliminar directamente",
|
||||
"topics.edit.placeholder": "Introduce nuevo nombre",
|
||||
@@ -491,8 +492,12 @@
|
||||
"urls": "URLs",
|
||||
"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_placeholder": "Valor predeterminado (no recomendado modificar)",
|
||||
"dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})"
|
||||
"dimensions_size_placeholder": " Tamaño de dimensión de incrustación, ej. 1024",
|
||||
"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": {
|
||||
"arabic": "Árabe",
|
||||
@@ -552,7 +557,6 @@
|
||||
"error.enter.api.key": "Ingrese su clave API",
|
||||
"error.enter.model": "Seleccione un modelo",
|
||||
"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.invalid.api.host": "Dirección API inválida",
|
||||
"error.invalid.api.key": "Clave API inválida",
|
||||
@@ -837,6 +841,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "BaiChuan",
|
||||
"baidu-cloud": "Baidu Nube Qiánfān",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copiloto",
|
||||
"dashscope": "Álibaba Nube BaiLiàn",
|
||||
"deepseek": "Profundo Buscar",
|
||||
@@ -1299,6 +1304,7 @@
|
||||
"advancedSettings": "Configuración avanzada"
|
||||
},
|
||||
"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_popover_trigger": "Desencadenante de detalles de cuadrícula",
|
||||
"messages.grid_popover_trigger.click": "Mostrar al hacer clic",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"topics.clear.title": "Effacer le message",
|
||||
"topics.copy.image": "Copier sous forme d'image",
|
||||
"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.delete.shortcut": "Maintenez {{key}} pour supprimer directement",
|
||||
"topics.edit.placeholder": "Entrez un nouveau nom",
|
||||
@@ -490,8 +491,12 @@
|
||||
"urls": "URLs",
|
||||
"dimensions": "Размерность встраивания",
|
||||
"dimensions_size_tooltip": "Размерность встраивания. Чем больше значение, тем выше размерность, но тем больше токенов требуется",
|
||||
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
|
||||
"dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})"
|
||||
"dimensions_size_placeholder": " Taille de dimension d'incorporation, ex. 1024",
|
||||
"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": {
|
||||
"arabic": "Arabe",
|
||||
@@ -551,7 +556,6 @@
|
||||
"error.enter.api.key": "Veuillez entrer votre clé API",
|
||||
"error.enter.model": "Veuillez sélectionner un modèle",
|
||||
"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.invalid.api.host": "Adresse API invalide",
|
||||
"error.invalid.api.key": "Clé API invalide",
|
||||
@@ -836,6 +840,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "BaiChuan",
|
||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilote",
|
||||
"dashscope": "AliCloud BaiLian",
|
||||
"deepseek": "DeepSeek",
|
||||
@@ -1300,6 +1305,7 @@
|
||||
"advancedSettings": "Расширенные настройки"
|
||||
},
|
||||
"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_popover_trigger": "Déclencheur de popover de la grille",
|
||||
"messages.grid_popover_trigger.click": "Afficher au clic",
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"topics.clear.title": "Limpar mensagens",
|
||||
"topics.copy.image": "Copiar como imagem",
|
||||
"topics.copy.md": "Copiar como Markdown",
|
||||
"topics.copy.plain_text": "Copiar como texto simples (remover Markdown)",
|
||||
"topics.copy.title": "Copiar",
|
||||
"topics.delete.shortcut": "Pressione {{key}} para deletar diretamente",
|
||||
"topics.edit.placeholder": "Digite novo nome",
|
||||
@@ -492,8 +493,12 @@
|
||||
"urls": "URLs",
|
||||
"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_placeholder": "Valor padrão (não recomendado alterar)",
|
||||
"dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})"
|
||||
"dimensions_size_placeholder": " Tamanho da dimensão de incorporação, ex. 1024",
|
||||
"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": {
|
||||
"arabic": "Árabe",
|
||||
@@ -553,7 +558,6 @@
|
||||
"error.enter.api.key": "Insira sua chave API",
|
||||
"error.enter.model": "Selecione um modelo",
|
||||
"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.invalid.api.host": "Endereço API inválido",
|
||||
"error.invalid.api.key": "Chave API inválida",
|
||||
@@ -1302,6 +1306,7 @@
|
||||
"advancedSettings": "Configurações Avançadas"
|
||||
},
|
||||
"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_popover_trigger": "Disparador de detalhes da grade",
|
||||
"messages.grid_popover_trigger.click": "Clique para mostrar",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@@ -151,23 +152,27 @@ const AgentsPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="p-4">
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
allowClear
|
||||
onClear={handleSearchClear}
|
||||
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
|
||||
value={searchInput}
|
||||
maxLength={50}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<div style={{ width: 80 }} />
|
||||
</div>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('agents.title')}
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
allowClear
|
||||
onClear={handleSearchClear}
|
||||
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
|
||||
value={searchInput}
|
||||
maxLength={50}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<div style={{ width: 80 }} />
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
|
||||
<Main id="content-container">
|
||||
<AgentsGroupList>
|
||||
{Object.entries(agentGroups).map(([group]) => (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { Button, Input } from 'antd'
|
||||
import { Search, SettingsIcon, X } from 'lucide-react'
|
||||
@@ -40,37 +41,35 @@ const AppsPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container onContextMenu={handleContextMenu}>
|
||||
{/* <Navbar> */}
|
||||
{/* <NavbarMain> */}
|
||||
{/* {t('minapp.title')} */}
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{
|
||||
width: '30%',
|
||||
height: 28,
|
||||
borderRadius: 15,
|
||||
position: 'absolute',
|
||||
left: '50vw',
|
||||
transform: 'translateX(-50%)'
|
||||
}}
|
||||
size="small"
|
||||
variant="filled"
|
||||
suffix={<Search size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
disabled={isSettingsOpen}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
className="nodrag"
|
||||
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
/>
|
||||
</div>
|
||||
{/* </NavbarMain> */}
|
||||
{/* </Navbar> */}
|
||||
<Navbar>
|
||||
<NavbarMain>
|
||||
{t('minapp.title')}
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{
|
||||
width: '30%',
|
||||
height: 28,
|
||||
borderRadius: 15,
|
||||
position: 'absolute',
|
||||
left: '50vw',
|
||||
transform: 'translateX(-50%)'
|
||||
}}
|
||||
size="small"
|
||||
variant="filled"
|
||||
suffix={<Search size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
disabled={isSettingsOpen}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
className="nodrag"
|
||||
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
/>
|
||||
</NavbarMain>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
{isSettingsOpen && <MiniAppSettings />}
|
||||
{!isSettingsOpen && (
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Category } from '@renderer/types/cherryStore'
|
||||
import React, { Suspense } from 'react'
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
||||
|
||||
// 实际的 AgentsPage 组件 - 请确保路径正确
|
||||
import { discoverRouters } from '../routers'
|
||||
// import AssistantDetailsPage from '../../agents/AssistantDetailsPage'; // 示例详情页
|
||||
|
||||
// 其他分类的页面组件 (如果需要)
|
||||
// const MiniAppPagePlaceholder = ({ categoryId, subcategoryId }: { categoryId?: string; subcategoryId?: string }) => (
|
||||
// <div className="p-4">
|
||||
// MiniApp Placeholder for Category: {categoryId || 'N/A'}, Subcategory: {subcategoryId || 'N/A'}
|
||||
// </div>
|
||||
// )
|
||||
|
||||
export interface DiscoverContentProps {
|
||||
activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant"
|
||||
// selectedSubcategoryId: string
|
||||
currentCategory: Category | undefined
|
||||
}
|
||||
|
||||
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
|
||||
const location = useLocation() // To see the current path for debugging or more complex logic
|
||||
|
||||
if (!currentCategory || !activeTabId) {
|
||||
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
|
||||
}
|
||||
|
||||
if (!activeTabId && !location.pathname.startsWith('/discover/')) {
|
||||
return <Navigate to="/discover/assistant?subcategory=all" replace /> // Fallback redirect, adjust as needed
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Routes>
|
||||
{discoverRouters.map((_Route) => {
|
||||
if (!_Route.component) return null
|
||||
return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} />
|
||||
})}
|
||||
|
||||
<Route path="*" element={<div>Discover Feature Not Found at {location.pathname}</div>} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiscoverContent
|
||||
@@ -1,64 +0,0 @@
|
||||
import { SubCategoryItem } from '@renderer/types/cherryStore'
|
||||
import { Badge } from '@renderer/ui/badge'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider
|
||||
} from '@renderer/ui/sidebar'
|
||||
|
||||
import { InternalCategory } from '../hooks/useDiscoverCategories'
|
||||
|
||||
interface DiscoverSidebarProps {
|
||||
activeCategory: InternalCategory | undefined
|
||||
selectedSubcategory: string
|
||||
onSelectSubcategory: (subcategoryId: string, row?: SubCategoryItem) => void
|
||||
}
|
||||
|
||||
export default function DiscoverSidebar({
|
||||
activeCategory,
|
||||
selectedSubcategory,
|
||||
onSelectSubcategory
|
||||
}: DiscoverSidebarProps) {
|
||||
if (!activeCategory) {
|
||||
return (
|
||||
<Sidebar className="absolute top-0 left-0 h-full border-r">
|
||||
<SidebarContent>
|
||||
<p className="p-4 text-sm text-gray-500">No active category selected.</p>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider className="relative h-full min-h-full w-full">
|
||||
<Sidebar className="absolute top-0 left-0 h-full border-r">
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
{activeCategory.items &&
|
||||
activeCategory.items.length > 0 &&
|
||||
activeCategory.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.id}>
|
||||
<SidebarMenuButton
|
||||
isActive={subItem.id === selectedSubcategory}
|
||||
onClick={() => {
|
||||
onSelectSubcategory(subItem.id, subItem)
|
||||
}}
|
||||
size="sm">
|
||||
<span className="truncate">{subItem.name}</span>
|
||||
{typeof subItem.count === 'number' && (
|
||||
<Badge variant="secondary" className="ml-auto shrink-0">
|
||||
{subItem.count}
|
||||
</Badge>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { Category } from '@renderer/types/cherryStore'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { discoverRouters } from '../routers'
|
||||
|
||||
// Extended Category type for internal use in hook, including path and sidebar flag
|
||||
// Export this interface so other files can import it
|
||||
export interface InternalCategory extends Category {
|
||||
path: string
|
||||
hasSidebar?: boolean // Optional: defaults to true if not specified, or handle explicitly
|
||||
}
|
||||
|
||||
// Initial category data with path and hasSidebar
|
||||
const initialCategories: InternalCategory[] = discoverRouters.map((router) => ({
|
||||
id: router.id,
|
||||
title: router.title,
|
||||
path: router.path,
|
||||
hasSidebar: !router.component,
|
||||
// 目前没有需要二级分类的分类
|
||||
items: []
|
||||
}))
|
||||
|
||||
// Helper to find category by path
|
||||
const findCategoryByPath = (path: string | undefined): InternalCategory | undefined =>
|
||||
initialCategories.find((cat) => cat.path === path)
|
||||
|
||||
// Helper to find category by id (activeTab)
|
||||
const findCategoryById = (id: string | undefined): InternalCategory | undefined =>
|
||||
initialCategories.find((cat) => cat.id === id)
|
||||
|
||||
export function useDiscoverCategories() {
|
||||
const [categories, setCategories] = useState<InternalCategory[]>(initialCategories)
|
||||
const [activeTab, setActiveTab] = useState<string>('')
|
||||
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('all')
|
||||
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// Effect to initialize activeTab from URL path segment or navigate to default
|
||||
useEffect(() => {
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean) // e.g., ["discover", "assistant"]
|
||||
// Expects URL like /discover/:categoryPathSegment/...
|
||||
const currentCategoryPath = pathSegments.length >= 2 && pathSegments[0] === 'discover' ? pathSegments[1] : undefined
|
||||
|
||||
const categoryFromPath = findCategoryByPath(currentCategoryPath)
|
||||
|
||||
// Synchronize active tab with the category determined from the URL path.
|
||||
// If a category is found from the path, update the active tab to match its ID.
|
||||
if (categoryFromPath) {
|
||||
if (activeTab !== categoryFromPath.id) {
|
||||
setActiveTab(categoryFromPath.id)
|
||||
}
|
||||
} else if (location.pathname === '/discover' || location.pathname === '/discover/') {
|
||||
// Handle the case where the URL is the base /discover path.
|
||||
// Redirect to the first category's path to ensure a category is always selected.
|
||||
if (categories.length > 0) {
|
||||
const firstCategory = categories[0]
|
||||
if (firstCategory?.path) {
|
||||
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
|
||||
}
|
||||
}
|
||||
} else if (!currentCategoryPath && categories.length > 0 && !activeTab) {
|
||||
// Fallback for invalid or unmatched /discover/xxx URLs.
|
||||
// If the URL contains a path segment that doesn't correspond to a known category,
|
||||
// and no tab is active, redirect to the first valid category.
|
||||
const firstCategory = categories[0]
|
||||
if (firstCategory?.path) {
|
||||
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
|
||||
}
|
||||
}
|
||||
// If categoryFromPath is undefined, and it's not /discover, it means it's an invalid path like /discover/unknown
|
||||
// In this case, we don't navigate from here; ideally App.tsx has a NotFound route, or DiscoverContent shows a message.
|
||||
}, [location.pathname, categories, activeTab, navigate])
|
||||
|
||||
// Effect to initialize selectedSubcategory from URL query param or default to 'all'
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const subcategoryIdFromQuery = searchParams.get('subcategory')
|
||||
const currentCatDetails = findCategoryById(activeTab) // Use the helper here
|
||||
|
||||
if (subcategoryIdFromQuery && currentCatDetails) {
|
||||
// Check if the subcategory from query is valid for the current active category
|
||||
if (currentCatDetails.items.some((item) => item.id === subcategoryIdFromQuery)) {
|
||||
if (selectedSubcategory !== subcategoryIdFromQuery) {
|
||||
setSelectedSubcategory(subcategoryIdFromQuery)
|
||||
}
|
||||
return // Valid subcategory from URL is set, no further action needed in this effect iteration
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid subcategory in query, or if activeTab has changed and subcategory needs reset/defaulting
|
||||
if (activeTab && currentCatDetails) {
|
||||
const defaultSub = currentCatDetails.items.find((item) => item.id === 'all') || currentCatDetails.items[0]
|
||||
if (defaultSub) {
|
||||
// Ensure defaultSub exists
|
||||
// Set selectedSubcategory state first
|
||||
if (selectedSubcategory !== defaultSub.id) {
|
||||
setSelectedSubcategory(defaultSub.id)
|
||||
}
|
||||
// Then, if URL doesn't match this default, update URL to reflect the default subcategory
|
||||
// This ensures the URL is the source of truth / always consistent.
|
||||
if (!subcategoryIdFromQuery || subcategoryIdFromQuery !== defaultSub.id) {
|
||||
const newSearchParams = new URLSearchParams() // Start with clean params for this path
|
||||
newSearchParams.set('subcategory', defaultSub.id)
|
||||
// Ensure we use the current actual path from currentCatDetails if available for navigation
|
||||
// This avoids issues if location.pathname is briefly out of sync during transitions.
|
||||
const basePath = currentCatDetails.path
|
||||
? `/discover/${currentCatDetails.path}`
|
||||
: location.pathname.split('?')[0]
|
||||
navigate(`${basePath}?${newSearchParams.toString()}`, { replace: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [activeTab, location.search, categories, navigate, selectedSubcategory]) // location.pathname removed as basePath logic handles path part
|
||||
|
||||
const currentCategory = useMemo(() => {
|
||||
return findCategoryById(activeTab) // Use the helper here
|
||||
}, [activeTab]) // categories removed from deps as findCategoryById uses stable initialCategories
|
||||
|
||||
const handleSelectTab = (tabId: string) => {
|
||||
const categoryToSelect = findCategoryById(tabId)
|
||||
if (categoryToSelect && categoryToSelect.path && activeTab !== tabId) {
|
||||
navigate(`/discover/${categoryToSelect.path}?subcategory=all`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectSubcategory = (subcategoryId: string) => {
|
||||
const currentCatDetails = findCategoryById(activeTab)
|
||||
if (selectedSubcategory !== subcategoryId && currentCatDetails?.path) {
|
||||
const newSearchParams = new URLSearchParams()
|
||||
newSearchParams.set('subcategory', subcategoryId)
|
||||
navigate(`/discover/${currentCatDetails.path}?${newSearchParams.toString()}`, { replace: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure each category has an "All" subcategory (runs once on mount)
|
||||
useEffect(() => {
|
||||
setCategories((prev) =>
|
||||
prev.map((cat) => {
|
||||
if (!cat.items.some((item) => item.id === 'all')) {
|
||||
return { ...cat, items: [{ id: 'all', name: `All ${cat.title}` }, ...cat.items] }
|
||||
}
|
||||
return cat
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
categories,
|
||||
activeTab,
|
||||
selectedSubcategory,
|
||||
currentCategory,
|
||||
handleSelectTab,
|
||||
handleSelectSubcategory,
|
||||
setActiveTab
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||
// import { useRuntime } from '@renderer/hooks/useRuntime' // No longer needed if resourcesPath is not used
|
||||
import { Tabs as VercelTabs } from '@renderer/ui/vercel-tabs'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
// Import Context and the main Dialog Manager component
|
||||
import DiscoverContent from './components/DiscoverContent' // Removed DiscoverContent import
|
||||
import DiscoverSidebar from './components/DiscoverSidebar'
|
||||
import { InternalCategory, useDiscoverCategories } from './hooks/useDiscoverCategories'
|
||||
|
||||
// Function to adapt categories for VercelTabs
|
||||
const adaptCategoriesForVercelTabs = (categories: InternalCategory[]) => {
|
||||
return categories.map((category) => ({
|
||||
id: category.id, // VercelTabs expects `id`
|
||||
label: category.title // VercelTabs expects `label`
|
||||
}))
|
||||
}
|
||||
|
||||
export default function DiscoverPage() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
categories,
|
||||
activeTab,
|
||||
selectedSubcategory,
|
||||
currentCategory,
|
||||
handleSelectTab,
|
||||
handleSelectSubcategory,
|
||||
setActiveTab
|
||||
} = useDiscoverCategories()
|
||||
|
||||
// Path like /discover/:categoryIdFromUrl. categoryIdFromUrl is lowercase from URL.
|
||||
const { categoryIdFromUrl } = useParams<{ categoryIdFromUrl: string }>()
|
||||
|
||||
useEffect(() => {
|
||||
const matchedCategory = categories.find((cat) => cat.id.toLowerCase() === categoryIdFromUrl?.toLowerCase())
|
||||
if (matchedCategory && activeTab !== matchedCategory.id) {
|
||||
setActiveTab(matchedCategory.id)
|
||||
}
|
||||
}, [categoryIdFromUrl, categories, activeTab, setActiveTab])
|
||||
|
||||
const vercelTabsData = adaptCategoriesForVercelTabs(categories)
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<NavbarMain className="h-auto flex-shrink-0">
|
||||
<NavbarCenter>{t('discover.title')}</NavbarCenter>
|
||||
</NavbarMain>
|
||||
|
||||
{categories.length > 0 && (
|
||||
<div className="px-4 py-2">
|
||||
<VercelTabs tabs={vercelTabsData} onTabChange={handleSelectTab} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-grow flex-row overflow-auto">
|
||||
{currentCategory?.hasSidebar && (
|
||||
<div className="w-64 flex-shrink-0 border-r">
|
||||
<DiscoverSidebar
|
||||
activeCategory={currentCategory}
|
||||
selectedSubcategory={selectedSubcategory}
|
||||
onSelectSubcategory={handleSelectSubcategory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* {!currentCategory && categories.length > 0 && (
|
||||
<div className="w-64 flex-shrink-0 border-r p-4 text-muted-foreground">Select a category...</div>
|
||||
)} */}
|
||||
|
||||
<main className="flex-grow overflow-hidden">
|
||||
<DiscoverContent
|
||||
activeTabId={activeTab}
|
||||
// selectedSubcategoryId={selectedSubcategory}
|
||||
currentCategory={currentCategory}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import { CherryStoreType } from '@renderer/types/cherryStore'
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const discoverRouters = [
|
||||
{
|
||||
id: CherryStoreType.ASSISTANT,
|
||||
title: i18n.t('assistants.title'),
|
||||
path: 'assistant',
|
||||
component: lazy(() => import('../agents/AgentsPage'))
|
||||
},
|
||||
{
|
||||
id: CherryStoreType.MINI_APP,
|
||||
title: i18n.t('minapp.title'),
|
||||
path: 'mini-app',
|
||||
component: lazy(() => import('../apps/AppsPage'))
|
||||
},
|
||||
{
|
||||
id: CherryStoreType.TRANSLATE,
|
||||
title: i18n.t('translate.title'),
|
||||
path: 'translate',
|
||||
component: lazy(() => import('../translate/TranslatePage'))
|
||||
},
|
||||
{
|
||||
id: CherryStoreType.FILES,
|
||||
title: i18n.t('files.title'),
|
||||
path: 'files',
|
||||
component: lazy(() => import('../files/FilesPage'))
|
||||
},
|
||||
{
|
||||
id: CherryStoreType.PAINTINGS,
|
||||
title: i18n.t('paintings.title'),
|
||||
path: 'paintings/*',
|
||||
isPrefix: true,
|
||||
component: lazy(() => import('../paintings/PaintingsRoutePage'))
|
||||
},
|
||||
{
|
||||
id: CherryStoreType.MCP_SERVER,
|
||||
title: i18n.t('common.mcp'),
|
||||
path: 'mcp-servers/*',
|
||||
isPrefix: true,
|
||||
component: lazy(() => import('../mcp-servers'))
|
||||
}
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Category } from '@renderer/types/cherryStore'
|
||||
|
||||
export interface DiscoverContextType {
|
||||
selectedSubcategory: string
|
||||
activeTabId: string
|
||||
currentCategory?: Category // currentCategory might be undefined initially
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Logger from '@renderer/config/logger'
|
||||
@@ -206,9 +207,9 @@ const FilesPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{/* <NavbarMain>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
||||
</NavbarMain> */}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SideNav>
|
||||
{menuItems.map((item) => (
|
||||
|
||||
@@ -15,6 +15,7 @@ import styled from 'styled-components'
|
||||
|
||||
import Inputbar from './Inputbar/Inputbar'
|
||||
import Messages from './Messages/Messages'
|
||||
import Tabs from './Tabs'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -37,7 +38,7 @@ const Chat: FC<Props> = (props) => {
|
||||
const showRightTopics = showTopics && topicPosition === 'right'
|
||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
|
||||
return `calc(100vw - ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
@@ -127,6 +128,15 @@ const Chat: FC<Props> = (props) => {
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</QuickPanelProvider>
|
||||
</Main>
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<Tabs
|
||||
activeAssistant={assistant}
|
||||
activeTopic={props.activeTopic}
|
||||
setActiveAssistant={props.setActiveAssistant}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
position="right"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { Navbar } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
import UpdateAppButton from './components/UpdateAppButton'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
position: 'left' | 'right'
|
||||
}
|
||||
|
||||
const ChatNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const isFullscreen = useFullscreen()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
const { toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Function to toggle assistants with cooldown
|
||||
const handleToggleShowAssistants = useCallback(() => {
|
||||
if (showAssistants) {
|
||||
// When hiding sidebar, set cooldown
|
||||
toggleShowAssistants()
|
||||
// setTimeout(() => {
|
||||
// setSidebarHideCooldown(false)
|
||||
// }, 10000) // 10 seconds cooldown
|
||||
} else {
|
||||
// When showing sidebar, no cooldown needed
|
||||
toggleShowAssistants()
|
||||
}
|
||||
}, [showAssistants, toggleShowAssistants])
|
||||
|
||||
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
|
||||
|
||||
useShortcut('toggle_show_topics', () => {
|
||||
if (topicPosition === 'right') {
|
||||
toggleShowTopics()
|
||||
} else {
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
const handleNarrowModeToggle = async () => {
|
||||
await modelGenerating()
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
|
||||
<HStack alignItems="center">
|
||||
<NavbarIcon
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
|
||||
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
|
||||
</NavbarIcon>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
{isMac && (
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
{sidebarIcons.visible.includes('minapp') && (
|
||||
<MinAppsPopover>
|
||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon>
|
||||
<LayoutGrid size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
</MinAppsPopover>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarContainer>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
const NavbarContainer = styled.div<{ $isFullscreen: boolean; $showSidebar: boolean }>`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ $showSidebar }) => (isMac ? ($showSidebar ? '10px' : '75px') : '15px')};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-appstore {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default ChatNavbar
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useChat } from '@renderer/hooks/useChat'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Chat from './Chat'
|
||||
import ChatNavbar from './ChatNavbar'
|
||||
import Navbar from './Navbar'
|
||||
import HomeTabs from './Tabs'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { assistants } = useAssistants()
|
||||
@@ -17,9 +21,12 @@ const HomePage: FC = () => {
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
|
||||
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
|
||||
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
useEffect(() => {
|
||||
NavigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
@@ -54,8 +61,23 @@ const HomePage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container id="home-page">
|
||||
<ChatNavbar activeAssistant={activeAssistant} position="left" />
|
||||
<Navbar
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
position="left"
|
||||
/>
|
||||
<ContentContainer id="content-container">
|
||||
{showAssistants && (
|
||||
<HomeTabs
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
setActiveTopic={setActiveTopic}
|
||||
position="left"
|
||||
/>
|
||||
)}
|
||||
<Chat
|
||||
assistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
@@ -71,6 +93,7 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: calc(100vw - var(--sidebar-width));
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
@@ -52,7 +52,6 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
|
||||
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
||||
import MentionModelsInput from './MentionModelsInput'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import SettingButton from './SettingButton'
|
||||
import TokenCount from './TokenCount'
|
||||
|
||||
interface Props {
|
||||
@@ -406,6 +405,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
|
||||
const topic = getDefaultTopic(assistant.id)
|
||||
|
||||
await db.topics.add({ id: topic.id, messages: [] })
|
||||
@@ -857,7 +858,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
ToolbarButton={ToolbarButton}
|
||||
onClick={onNewContext}
|
||||
/>
|
||||
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
{loading && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||
|
||||
@@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
newList.push({
|
||||
label: t('settings.mcp.addServer') + '...',
|
||||
icon: <Plus />,
|
||||
action: () => navigate('/mcp-servers')
|
||||
action: () => navigate('/settings/mcp')
|
||||
})
|
||||
|
||||
newList.unshift({
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Popover } from 'antd'
|
||||
import { Settings } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
|
||||
import SettingsTab from '../Tabs/SettingsTab'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const SettingButton: FC<Props> = ({ assistant, ToolbarButton }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="topLeft"
|
||||
content={<SettingsTab assistant={assistant} onClose={handleClose} />}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
styles={{
|
||||
body: {
|
||||
padding: '4px 2px 4px 2px'
|
||||
}
|
||||
}}>
|
||||
<ToolbarButton type="text">
|
||||
<Settings size={18} />
|
||||
</ToolbarButton>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingButton
|
||||
@@ -93,19 +93,15 @@ const Markdown: FC<Props> = ({ block }) => {
|
||||
} as Partial<Components>
|
||||
}, [onSaveCodeBlock])
|
||||
|
||||
if (messageContent.includes('<style>')) {
|
||||
components.style = MarkdownShadowDOMRenderer as any
|
||||
}
|
||||
|
||||
const urlTransform = useCallback((value: string) => {
|
||||
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return 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 (
|
||||
<ReactMarkdown
|
||||
rehypePlugins={rehypePlugins}
|
||||
|
||||
@@ -31,7 +31,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock }> = ({ block }) =>
|
||||
}
|
||||
|
||||
const Alert = styled(AntdAlert)`
|
||||
margin: 15px 0 8px;
|
||||
margin: 0.5rem 0;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
@@ -151,7 +151,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
|
||||
</Flex>
|
||||
)}
|
||||
{role === 'user' && !renderInputMessageAsMarkdown ? (
|
||||
<p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>
|
||||
<p className="markdown" style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{block.content}
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -42,6 +42,7 @@ const blockWrapperVariants = {
|
||||
const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="block-wrapper"
|
||||
variants={blockWrapperVariants}
|
||||
initial={enableAnimation ? 'hidden' : 'static'}
|
||||
animate={enableAnimation ? 'visible' : 'static'}>
|
||||
|
||||
@@ -139,6 +139,7 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
||||
<WebSearchCard>
|
||||
<ContextMenu>
|
||||
<WebSearchCardHeader>
|
||||
<CitationIndex>{citation.number}</CitationIndex>
|
||||
{citation.showFavicon && citation.url && (
|
||||
<Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} />
|
||||
)}
|
||||
@@ -162,6 +163,7 @@ const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
||||
<WebSearchCard>
|
||||
<ContextMenu>
|
||||
<WebSearchCardHeader>
|
||||
<CitationIndex>{citation.number}</CitationIndex>
|
||||
{citation.showFavicon && <FileSearch width={16} />}
|
||||
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
||||
{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`
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
@@ -21,7 +21,6 @@ import MessageEditor from './MessageEditor'
|
||||
import MessageErrorBoundary from './MessageErrorBoundary'
|
||||
import MessageHeader from './MessageHeader'
|
||||
import MessageMenubar from './MessageMenubar'
|
||||
import MessageTokens from './MessageTokens'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -99,7 +98,7 @@ const MessageItem: FC<Props> = ({
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
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 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 (
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
@@ -155,35 +138,100 @@ const MessageItem: FC<Props> = ({
|
||||
'message-user': !isAssistantMessage
|
||||
})}
|
||||
ref={messageContainerRef}
|
||||
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
|
||||
<ContextMenu>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
|
||||
<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={{
|
||||
...style,
|
||||
justifyContent: isBubbleStyle ? (isAssistantMessage ? 'flex-start' : 'flex-end') : undefined,
|
||||
flex: isBubbleStyle ? undefined : 1
|
||||
}}>
|
||||
{isEditing && (
|
||||
<ContextMenu
|
||||
style={{
|
||||
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
||||
fontSize,
|
||||
background: messageBackground,
|
||||
overflowY: 'visible',
|
||||
maxWidth: narrowMode ? 760 : undefined
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
|
||||
width: isBubbleStyle ? '70%' : '100%'
|
||||
}}>
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} />
|
||||
</MessageErrorBoundary>
|
||||
{showMenubar && (
|
||||
<MessageHeader
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
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
|
||||
className="MessageFooter"
|
||||
style={{
|
||||
border: messageBorder,
|
||||
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined
|
||||
borderTop: messageBorder,
|
||||
flexDirection: !isAssistantMessage ? 'row-reverse' : undefined
|
||||
}}>
|
||||
<MessageTokens message={message} isLastMessage={isLastMessage} />
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
@@ -198,8 +246,8 @@ const MessageItem: FC<Props> = ({
|
||||
/>
|
||||
</MessageFooter>
|
||||
)}
|
||||
</MessageContentContainer>
|
||||
</ContextMenu>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
@@ -214,7 +262,7 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
padding: 0 20px;
|
||||
@@ -257,12 +305,12 @@ const MessageFooter = styled.div`
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
margin-top: 2px;
|
||||
border-top: 0.5px dotted var(--color-border);
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const NewContextMessage = styled.div`
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
export default memo(MessageItem)
|
||||
|
||||
@@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
|
||||
return (
|
||||
<>
|
||||
{!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>)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -180,7 +180,8 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}
|
||||
className={classNames({
|
||||
'group-message-wrapper': message.role === 'assistant' && isHorizontal && isGrouped,
|
||||
// 加个卡片布局
|
||||
'group-message-wrapper': message.role === 'assistant' && (isHorizontal || isGrid) && isGrouped,
|
||||
[multiModelMessageStyle]: isGrouped,
|
||||
selected: message.id === selectedMessageId
|
||||
})}>
|
||||
@@ -315,6 +316,7 @@ interface MessageWrapperProps {
|
||||
|
||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
&.horizontal {
|
||||
display: inline-block;
|
||||
|
||||
@@ -17,10 +17,13 @@ import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageTokens from './MessageTokens'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
model?: Model
|
||||
index: number | undefined
|
||||
}
|
||||
|
||||
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
@@ -28,7 +31,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | 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 { theme } = useTheme()
|
||||
const { userName, sidebarIcons } = useSettings()
|
||||
@@ -52,9 +55,11 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMinappIcon = sidebarIcons.visible.includes('minapp')
|
||||
const { showTokens } = useSettings()
|
||||
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
const isLastMessage = index === 0
|
||||
|
||||
const showMiniApp = useCallback(() => {
|
||||
showMinappIcon && model?.provider && openMinappById(model.provider)
|
||||
@@ -111,7 +116,14 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
|
||||
{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>
|
||||
</AvatarWrapper>
|
||||
</Container>
|
||||
@@ -140,6 +152,19 @@ const UserWrap = styled.div`
|
||||
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 }>`
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { useMessageStyle } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageTitle } from '@renderer/services/MessagesService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
exportMessageToNotion,
|
||||
messageToMarkdown
|
||||
} from '@renderer/utils/export'
|
||||
import { copyMessageAsPlainText } from '@renderer/utils/copy'
|
||||
// import { withMessageThought } from '@renderer/utils/formats'
|
||||
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
|
||||
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
@@ -66,6 +68,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
appendAssistantResponse,
|
||||
removeMessageBlock
|
||||
} = useMessageOperations(topic)
|
||||
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
|
||||
const loading = useTopicLoading(topic)
|
||||
|
||||
const isUserMessage = message.role === 'user'
|
||||
@@ -197,6 +202,11 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
key: 'export',
|
||||
icon: <Share size={16} color="var(--color-icon)" style={{ marginTop: 3 }} />,
|
||||
children: [
|
||||
{
|
||||
label: t('chat.topics.copy.plain_text'),
|
||||
key: 'copy_message_plain_text',
|
||||
onClick: () => copyMessageAsPlainText(message)
|
||||
},
|
||||
exportMenuOptions.image && {
|
||||
label: t('chat.topics.copy.image'),
|
||||
key: 'img',
|
||||
@@ -332,24 +342,29 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
return translationBlocks.length > 0
|
||||
}, [message])
|
||||
|
||||
const softHoverBg = isBubbleStyle && !isLastMessage
|
||||
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<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 />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{message.role === 'user' && (
|
||||
<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 />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<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 && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
@@ -366,7 +381,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
mouseEnterDelay={0.8}
|
||||
open={showRegenerateTooltip}
|
||||
onOpenChange={setShowRegenerateTooltip}>
|
||||
<ActionButton className="message-action-button">
|
||||
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
|
||||
<RefreshCw size={16} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@@ -374,7 +389,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<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} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@@ -444,7 +459,10 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
placement="top"
|
||||
arrow>
|
||||
<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} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@@ -452,7 +470,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
)}
|
||||
{isAssistantMessage && isGrouped && (
|
||||
<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 ? (
|
||||
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
|
||||
) : (
|
||||
@@ -467,7 +485,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
|
||||
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
|
||||
title={t('common.delete')}
|
||||
mouseEnterDelay={1}
|
||||
@@ -483,7 +501,10 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
trigger={['click']}
|
||||
placement="topRight"
|
||||
arrow>
|
||||
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
$softHoverBg={softHoverBg}>
|
||||
<Menu size={19} />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
@@ -500,7 +521,7 @@ const MenusBar = styled.div`
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const ActionButton = styled.div`
|
||||
const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
@@ -511,8 +532,11 @@ const ActionButton = styled.div`
|
||||
height: 30px;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
.anticon {
|
||||
background-color: ${(props) =>
|
||||
props.$softHoverBg ? 'var(--color-background-soft)' : 'var(--color-background-mute)'};
|
||||
color: var(--color-text-1);
|
||||
.anticon,
|
||||
.lucide {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
@@ -522,9 +546,6 @@ const ActionButton = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.icon-at {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -69,19 +69,14 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
||||
}
|
||||
|
||||
const MessageMetadata = styled.div`
|
||||
font-size: 11px;
|
||||
color: var(--color-text-2);
|
||||
font-size: 10px;
|
||||
color: var(--color-text-3);
|
||||
user-select: text;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
|
||||
.tokens {
|
||||
display: block;
|
||||
|
||||
span {
|
||||
padding: 0 2px;
|
||||
}
|
||||
.tokens span {
|
||||
padding: 0 2px;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
231
src/renderer/src/pages/home/Navbar.tsx
Normal file
231
src/renderer/src/pages/home/Navbar.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
import UpdateAppButton from './components/UpdateAppButton'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
position: 'left' | 'right'
|
||||
}
|
||||
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const isFullscreen = useFullscreen()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
const [sidebarHideCooldown, setSidebarHideCooldown] = useState(false)
|
||||
|
||||
// Function to toggle assistants with cooldown
|
||||
const handleToggleShowAssistants = useCallback(() => {
|
||||
if (showAssistants) {
|
||||
// When hiding sidebar, set cooldown
|
||||
toggleShowAssistants()
|
||||
setSidebarHideCooldown(true)
|
||||
// setTimeout(() => {
|
||||
// setSidebarHideCooldown(false)
|
||||
// }, 10000) // 10 seconds cooldown
|
||||
} else {
|
||||
// When showing sidebar, no cooldown needed
|
||||
toggleShowAssistants()
|
||||
}
|
||||
}, [showAssistants, toggleShowAssistants])
|
||||
const handleToggleShowTopics = useCallback(() => {
|
||||
if (showTopics) {
|
||||
// When hiding sidebar, set cooldown
|
||||
toggleShowTopics()
|
||||
setSidebarHideCooldown(true)
|
||||
// setTimeout(() => {
|
||||
// setSidebarHideCooldown(false)
|
||||
// }, 10000) // 10 seconds cooldown
|
||||
} else {
|
||||
// When showing sidebar, no cooldown needed
|
||||
toggleShowTopics()
|
||||
}
|
||||
}, [showTopics, toggleShowTopics])
|
||||
|
||||
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
|
||||
|
||||
useShortcut('toggle_show_topics', () => {
|
||||
if (topicPosition === 'right') {
|
||||
toggleShowTopics()
|
||||
} else {
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
const handleNarrowModeToggle = async () => {
|
||||
await modelGenerating()
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
{showAssistants && (
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={handleToggleShowAssistants} style={{ marginLeft: isMac && !isFullscreen ? 16 : 0 }}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
|
||||
<MessageSquareDiff size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
|
||||
<HStack alignItems="center">
|
||||
{!showAssistants && !sidebarHideCooldown && (
|
||||
<FloatingSidebar
|
||||
activeAssistant={assistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveTopic={setActiveTopic}
|
||||
position={'left'}>
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</FloatingSidebar>
|
||||
)}
|
||||
{!showAssistants && sidebarHideCooldown && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
|
||||
onMouseOut={() => setSidebarHideCooldown(false)}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
{sidebarIcons.visible.includes('minapp') && (
|
||||
<MinAppsPopover>
|
||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon>
|
||||
<LayoutGrid size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
</MinAppsPopover>
|
||||
)}
|
||||
{topicPosition === 'right' && !showTopics && !sidebarHideCooldown && (
|
||||
<FloatingSidebar
|
||||
activeAssistant={assistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveTopic={setActiveTopic}
|
||||
position={'right'}>
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={() => toggleShowTopics()}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</FloatingSidebar>
|
||||
)}
|
||||
{topicPosition === 'right' && !showTopics && sidebarHideCooldown && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={() => toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={() => handleToggleShowTopics()}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-appstore {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
@@ -6,7 +6,7 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Divider, Tooltip } from 'antd'
|
||||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -80,7 +80,7 @@ const Assistants: FC<AssistantsTabProps> = ({
|
||||
if (assistantsTabSortType === 'tags') {
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: 4, gap: 10 }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
{getGroupedAssistants.map((group) => (
|
||||
<TagsContainer key={group.tag}>
|
||||
{group.tag !== t('assistants.tags.untagged') && (
|
||||
@@ -95,7 +95,7 @@ const Assistants: FC<AssistantsTabProps> = ({
|
||||
{group.tag}
|
||||
</GroupTitleName>
|
||||
</Tooltip>
|
||||
<GroupTitleDivider />
|
||||
<Divider style={{ margin: '12px 0' }}></Divider>
|
||||
</GroupTitle>
|
||||
)}
|
||||
{!collapsedTags[group.tag] && (
|
||||
@@ -197,20 +197,23 @@ const AssistantAddItem = styled.div`
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
}
|
||||
`
|
||||
|
||||
const GroupTitle = styled.div`
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: -8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const GroupTitleName = styled.div`
|
||||
@@ -218,18 +221,13 @@ const GroupTitleName = styled.div`
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-background);
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
color: var(--color-text);
|
||||
position: absolute;
|
||||
transform: translateY(2px);
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
margin-right: 5px;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const GroupTitleDivider = styled.div`
|
||||
flex: 1;
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const AssistantName = styled.div`
|
||||
|
||||
@@ -69,7 +69,6 @@ import OpenAISettingsGroup from './components/OpenAISettingsGroup'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const SettingsTab: FC<Props> = (props) => {
|
||||
@@ -198,10 +197,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Settings2 size={16} />}
|
||||
onClick={() => {
|
||||
AssistantSettingsPopup.show({ assistant, tab: 'model' })
|
||||
props.onClose()
|
||||
}}
|
||||
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
|
||||
/>
|
||||
</HStack>
|
||||
}>
|
||||
@@ -322,7 +318,12 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<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
|
||||
size="small"
|
||||
checked={showMessageDivider}
|
||||
@@ -685,10 +686,8 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const Container = styled(Scrollbar)`
|
||||
min-width: 300px;
|
||||
max-width: 40vw;
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 0 8px;
|
||||
padding-right: 0;
|
||||
|
||||
@@ -26,7 +26,7 @@ import { RootState } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
||||
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
|
||||
import {
|
||||
exportMarkdownToJoplin,
|
||||
exportMarkdownToSiyuan,
|
||||
@@ -280,6 +280,11 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
label: t('chat.topics.copy.md'),
|
||||
key: 'md',
|
||||
onClick: () => copyTopicAsMarkdown(topic)
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.copy.plain_text'),
|
||||
key: 'plain_text',
|
||||
onClick: () => copyTopicAsPlainText(topic)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -20,11 +20,11 @@ import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { classNames, getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||
import { Button, Dropdown, MenuProps } from 'antd'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { omit } from 'lodash'
|
||||
import { AlignJustify, EllipsisVertical, Plus, Settings2, Tag, Tags } from 'lucide-react'
|
||||
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react'
|
||||
import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -63,7 +63,6 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
const { assistants, updateAssistants } = useAssistants()
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
@@ -142,7 +141,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Container onClick={handleSwitch} className={classNames({ active: isActive, 'is-menu-open': isMenuOpen })}>
|
||||
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
|
||||
<AssistantNameRow className="name" title={fullAssistantName}>
|
||||
{assistantIconType === 'model' ? (
|
||||
<ModelAvatar
|
||||
@@ -160,15 +159,11 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
)}
|
||||
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
|
||||
</AssistantNameRow>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click']} onOpenChange={setIsMenuOpen}>
|
||||
<Button
|
||||
className="item-menu-button"
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EllipsisVertical size={16} color="var(--color-text-3)" />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Dropdown>
|
||||
{isActive && (
|
||||
<MenuButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
|
||||
<TopicCount className="topics-count">{assistant.topics.length}</TopicCount>
|
||||
</MenuButton>
|
||||
)}
|
||||
</Container>
|
||||
</Dropdown>
|
||||
)
|
||||
@@ -387,7 +382,6 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
height: 37px;
|
||||
position: relative;
|
||||
@@ -395,23 +389,12 @@ const Container = styled.div`
|
||||
border: 0.5px solid transparent;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
cursor: pointer;
|
||||
&.is-menu-open {
|
||||
.item-menu-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
.item-menu-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-list-item);
|
||||
}
|
||||
.item-menu-button {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const AssistantNameRow = styled.div`
|
||||
@@ -427,4 +410,31 @@ const AssistantName = styled.div`
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
const MenuButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
border-radius: 11px;
|
||||
position: absolute;
|
||||
background-color: var(--color-background);
|
||||
right: 9px;
|
||||
top: 6px;
|
||||
padding: 0 5px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const TopicCount = styled.div`
|
||||
color: var(--color-text);
|
||||
font-size: 10px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default memo(AssistantItem)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
import { Segmented as AntSegmented, SegmentedProps } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Assistants from './AssistantsTab'
|
||||
@@ -10,19 +15,51 @@ import Settings from './SettingsTab'
|
||||
import Topics from './TopicsTab'
|
||||
|
||||
interface Props {
|
||||
tab: Tab
|
||||
activeAssistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
position: 'left' | 'right'
|
||||
forceToSeeAllTab?: boolean
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
type Tab = 'assistants' | 'topic' | 'settings'
|
||||
|
||||
const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, style }) => {
|
||||
let _tab: any = ''
|
||||
|
||||
const HomeTabs: FC<Props> = ({
|
||||
activeAssistant,
|
||||
activeTopic,
|
||||
setActiveAssistant,
|
||||
setActiveTopic,
|
||||
position,
|
||||
forceToSeeAllTab,
|
||||
style
|
||||
}) => {
|
||||
const { addAssistant } = useAssistants()
|
||||
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
|
||||
const { topicPosition } = useSettings()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const borderStyle = '0.5px solid var(--color-border)'
|
||||
const border =
|
||||
position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 }
|
||||
|
||||
if (position === 'left' && topicPosition === 'left') {
|
||||
_tab = tab
|
||||
}
|
||||
|
||||
const showTab = !(position === 'left' && topicPosition === 'right')
|
||||
|
||||
const assistantTab = {
|
||||
label: t('assistants.abbr'),
|
||||
value: 'assistants'
|
||||
// icon: <BotIcon size={16} />
|
||||
}
|
||||
|
||||
const onCreateAssistant = async () => {
|
||||
const assistant = await AddAssistantPopup.show()
|
||||
@@ -35,8 +72,68 @@ const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssis
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
|
||||
showTab && setTab('assistants')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
|
||||
showTab && setTab('topic')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
|
||||
showTab && setTab('settings')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
|
||||
showTab && setTab('topic')
|
||||
if (position === 'left' && topicPosition === 'right') {
|
||||
toggleShowTopics()
|
||||
}
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [position, showTab, tab, toggleShowTopics, topicPosition])
|
||||
|
||||
useEffect(() => {
|
||||
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
|
||||
setTab('topic')
|
||||
}
|
||||
if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') {
|
||||
setTab('assistants')
|
||||
}
|
||||
}, [position, tab, topicPosition, forceToSeeAllTab])
|
||||
|
||||
return (
|
||||
<Container style={{ ...style }} className="home-tabs">
|
||||
<Container style={{ ...border, ...style }} className="home-tabs">
|
||||
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
|
||||
<>
|
||||
<Segmented
|
||||
value={tab}
|
||||
style={{ borderRadius: 50 }}
|
||||
shape="round"
|
||||
options={
|
||||
[
|
||||
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
|
||||
? assistantTab
|
||||
: undefined,
|
||||
{
|
||||
label: t('common.topics'),
|
||||
value: 'topic'
|
||||
// icon: <MessageSquareQuote size={16} />
|
||||
},
|
||||
{
|
||||
label: t('settings.title'),
|
||||
value: 'settings'
|
||||
// icon: <SettingsIcon size={16} />
|
||||
}
|
||||
].filter(Boolean) as SegmentedProps['options']
|
||||
}
|
||||
onChange={(value) => setTab(value as 'topic' | 'settings')}
|
||||
block
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<TabContent className="home-tabs-content">
|
||||
{tab === 'assistants' && (
|
||||
<Assistants
|
||||
@@ -57,7 +154,6 @@ const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssis
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: var(--assistants-width);
|
||||
min-width: var(--assistants-width);
|
||||
@@ -77,4 +173,68 @@ const TabContent = styled.div`
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const Divider = styled.div`
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
const Segmented = styled(AntSegmented)`
|
||||
font-family: var(--font-family);
|
||||
|
||||
&.ant-segmented {
|
||||
background-color: transparent;
|
||||
margin: 0 10px;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
.ant-segmented-item {
|
||||
overflow: hidden;
|
||||
transition: none !important;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
background-color: transparent;
|
||||
user-select: none;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ant-segmented-item-selected,
|
||||
.ant-segmented-item-selected:active {
|
||||
transition: none !important;
|
||||
background-color: var(--color-list-item);
|
||||
}
|
||||
.ant-segmented-item-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
}
|
||||
.ant-segmented-item-label[aria-selected='true'] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.icon-business-smart-assistant {
|
||||
margin-right: -2px;
|
||||
}
|
||||
.ant-segmented-thumb {
|
||||
transition: none !important;
|
||||
background-color: var(--color-list-item);
|
||||
border-radius: var(--list-item-border-radius);
|
||||
box-shadow: none;
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.ant-segmented-item-label,
|
||||
.ant-segmented-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* These styles ensure the same appearance as before */
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
`
|
||||
|
||||
export default HomeTabs
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import { NavbarIcon } from '@renderer/components/app/MainNavbar'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
@@ -23,6 +22,7 @@ import styled from 'styled-components'
|
||||
|
||||
import CustomCollapse from '../../components/CustomCollapse'
|
||||
import FileItem from '../files/FileItem'
|
||||
import { NavbarIcon } from '../home/Navbar'
|
||||
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
||||
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
|
||||
import StatusIcon from './components/StatusIcon'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
@@ -92,9 +92,9 @@ const KnowledgePage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<NavbarMain>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
|
||||
</NavbarMain>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SideNav>
|
||||
<ScrollContainer>
|
||||
|
||||
@@ -9,9 +9,9 @@ import { SettingHelpText } from '@renderer/pages/settings'
|
||||
import AiProvider from '@renderer/providers/AiProvider'
|
||||
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { KnowledgeBase, Model } from '@renderer/types'
|
||||
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 { nanoid } from 'nanoid'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
@@ -24,6 +24,8 @@ interface ShowParams {
|
||||
interface FormData {
|
||||
name: string
|
||||
model: string
|
||||
autoDims: boolean | undefined
|
||||
dimensions: number | undefined
|
||||
rerankModel: string | undefined
|
||||
documentCount: number | undefined
|
||||
}
|
||||
@@ -35,6 +37,7 @@ interface Props extends ShowParams {
|
||||
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [autoDims, setAutoDims] = useState(true)
|
||||
const [form] = Form.useForm<FormData>()
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useProviders()
|
||||
@@ -67,7 +70,8 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
.map((m) => ({
|
||||
label: m.name,
|
||||
value: getModelUniqId(m),
|
||||
key: `${p.id}-${m.id}`
|
||||
providerId: p.id,
|
||||
modelId: m.id
|
||||
}))
|
||||
}))
|
||||
.filter((group) => group.options.length > 0)
|
||||
@@ -107,24 +111,27 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
return
|
||||
}
|
||||
|
||||
const aiProvider = new AiProvider(provider)
|
||||
let dimensions = 0
|
||||
|
||||
try {
|
||||
dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel)
|
||||
} catch (error) {
|
||||
console.error('Error getting embedding dimensions:', error)
|
||||
window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error))
|
||||
setLoading(false)
|
||||
return
|
||||
if (autoDims || typeof values.dimensions === 'undefined') {
|
||||
try {
|
||||
const aiProvider = new AiProvider(provider)
|
||||
values.dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel)
|
||||
} catch (error) {
|
||||
console.error('Error getting embedding dimensions:', error)
|
||||
window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
} else if (typeof values.dimensions === 'string') {
|
||||
// 按理来说不应该是string的,但是确实是string
|
||||
values.dimensions = parseInt(values.dimensions)
|
||||
}
|
||||
|
||||
const newBase = {
|
||||
const newBase: KnowledgeBase = {
|
||||
id: nanoid(),
|
||||
name: values.name,
|
||||
model: selectedEmbeddingModel,
|
||||
rerankModel: selectedRerankModel,
|
||||
dimensions,
|
||||
dimensions: values.dimensions,
|
||||
documentCount: values.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT,
|
||||
items: [],
|
||||
created_at: Date.now(),
|
||||
@@ -134,7 +141,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
|
||||
await window.api.knowledgeBase.create(getKnowledgeBaseParams(newBase))
|
||||
|
||||
addKnowledgeBase(newBase as any)
|
||||
addKnowledgeBase(newBase)
|
||||
setOpen(false)
|
||||
resolve(newBase)
|
||||
}
|
||||
@@ -203,11 +210,59 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }}
|
||||
/>
|
||||
</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>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class AddKnowledgePopup {
|
||||
static hide() {
|
||||
TopView.hide('AddKnowledgePopup')
|
||||
|
||||
@@ -187,32 +187,6 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
||||
</AdvancedSettingsButton>
|
||||
|
||||
<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
|
||||
name="chunkSize"
|
||||
label={t('knowledge.chunk_size')}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { RedoOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
@@ -17,7 +19,7 @@ import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileType } from '@renderer/types'
|
||||
import type { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Avatar, Input, InputNumber, Radio, Segmented, Select, Slider, Switch, Tooltip, Upload } from 'antd'
|
||||
import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, Switch, Tooltip, Upload } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { Info } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
@@ -783,7 +785,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{/* <NavbarMain>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
@@ -792,7 +794,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</NavbarMain> */}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
|
||||
import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@@ -645,7 +645,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<NavbarMain>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
@@ -658,7 +658,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</NavbarMain>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
|
||||
@@ -5,7 +5,7 @@ import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
|
||||
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
|
||||
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
|
||||
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
|
||||
import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
@@ -354,7 +354,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<NavbarMain>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
@@ -367,7 +367,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</NavbarMain>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user