mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 15:10:59 +08:00
Merge branch 'CherryHQ:main' into New1207
This commit is contained in:
commit
b93394b369
@ -163,7 +163,7 @@ class FileStorage {
|
|||||||
fs.mkdirSync(this.storageDir, { recursive: true })
|
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(this.notesDir)) {
|
if (!fs.existsSync(this.notesDir)) {
|
||||||
fs.mkdirSync(this.storageDir, { recursive: true })
|
fs.mkdirSync(this.notesDir, { recursive: true })
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(this.tempDir)) {
|
if (!fs.existsSync(this.tempDir)) {
|
||||||
fs.mkdirSync(this.tempDir, { recursive: true })
|
fs.mkdirSync(this.tempDir, { recursive: true })
|
||||||
|
|||||||
@ -128,8 +128,8 @@ export class CallBackServer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return new Promise<http.Server>((resolve, reject) => {
|
return new Promise<http.Server>((resolve, reject) => {
|
||||||
server.listen(port, () => {
|
server.listen(port, '127.0.0.1', () => {
|
||||||
logger.info(`OAuth callback server listening on port ${port}`)
|
logger.info(`OAuth callback server listening on 127.0.0.1:${port}`)
|
||||||
resolve(server)
|
resolve(server)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import {
|
|||||||
isSupportStreamOptionsProvider,
|
isSupportStreamOptionsProvider,
|
||||||
isVertexProvider
|
isVertexProvider
|
||||||
} from '@renderer/utils/provider'
|
} from '@renderer/utils/provider'
|
||||||
|
import { defaultAppHeaders } from '@shared/utils'
|
||||||
import { cloneDeep, isEmpty } from 'lodash'
|
import { cloneDeep, isEmpty } from 'lodash'
|
||||||
|
|
||||||
import type { AiSdkConfig } from '../types'
|
import type { AiSdkConfig } from '../types'
|
||||||
@ -197,18 +198,13 @@ export function providerToAiSdkConfig(actualProvider: Provider, model: Model): A
|
|||||||
extraOptions.mode = 'chat'
|
extraOptions.mode = 'chat'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加额外headers
|
extraOptions.headers = {
|
||||||
if (actualProvider.extra_headers) {
|
...defaultAppHeaders(),
|
||||||
extraOptions.headers = actualProvider.extra_headers
|
...actualProvider.extra_headers
|
||||||
// copy from openaiBaseClient/openaiResponseApiClient
|
}
|
||||||
if (aiSdkProviderId === 'openai') {
|
|
||||||
extraOptions.headers = {
|
if (aiSdkProviderId === 'openai') {
|
||||||
...extraOptions.headers,
|
extraOptions.headers['X-Api-Key'] = baseConfig.apiKey
|
||||||
'HTTP-Referer': 'https://cherry-ai.com',
|
|
||||||
'X-Title': 'Cherry Studio',
|
|
||||||
'X-Api-Key': baseConfig.apiKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// azure
|
// azure
|
||||||
// https://learn.microsoft.com/en-us/azure/ai-foundry/openai/latest
|
// https://learn.microsoft.com/en-us/azure/ai-foundry/openai/latest
|
||||||
|
|||||||
34
src/renderer/src/components/Avatar/AssistantAvatar.tsx
Normal file
34
src/renderer/src/components/Avatar/AssistantAvatar.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||||
|
import type { Assistant } from '@renderer/types'
|
||||||
|
import { getLeadingEmoji } from '@renderer/utils'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import ModelAvatar from './ModelAvatar'
|
||||||
|
|
||||||
|
interface AssistantAvatarProps {
|
||||||
|
assistant: Assistant
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssistantAvatar: FC<AssistantAvatarProps> = ({ assistant, size = 24, className }) => {
|
||||||
|
const { assistantIconType } = useSettings()
|
||||||
|
const defaultModel = getDefaultModel()
|
||||||
|
|
||||||
|
const assistantName = useMemo(() => assistant.name || '', [assistant.name])
|
||||||
|
|
||||||
|
if (assistantIconType === 'model') {
|
||||||
|
return <ModelAvatar model={assistant.model || defaultModel} size={size} className={className} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assistantIconType === 'emoji') {
|
||||||
|
return <EmojiIcon emoji={assistant.emoji || getLeadingEmoji(assistantName)} size={size} className={className} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssistantAvatar
|
||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Import",
|
"button": "Import",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Failed to fetch from URL",
|
"fetch_failed": "Failed to fetch from URL",
|
||||||
|
"file_required": "Please select a file first",
|
||||||
"invalid_format": "Invalid assistant format: missing required fields",
|
"invalid_format": "Invalid assistant format: missing required fields",
|
||||||
"url_required": "Please enter a URL"
|
"url_required": "Please enter a URL"
|
||||||
},
|
},
|
||||||
@ -486,11 +487,14 @@
|
|||||||
},
|
},
|
||||||
"manage": {
|
"manage": {
|
||||||
"batch_delete": {
|
"batch_delete": {
|
||||||
"button": "Batch Delete",
|
"button": "Delete",
|
||||||
"confirm": "Are you sure you want to delete the selected {{count}} assistants?"
|
"confirm": "Are you sure you want to delete the selected {{count}} assistants?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Export"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Delete",
|
"manage": "Manage",
|
||||||
"sort": "Sort"
|
"sort": "Sort"
|
||||||
},
|
},
|
||||||
"title": "Manage Assistants"
|
"title": "Manage Assistants"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Export to Yuque"
|
"yuque": "Export to Yuque"
|
||||||
},
|
},
|
||||||
"list": "Topic List",
|
"list": "Topic List",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Clear Selection",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "Are you sure you want to delete {{count}} selected topic(s)? This action cannot be undone.",
|
||||||
|
"title": "Delete Topics"
|
||||||
|
},
|
||||||
|
"success": "Deleted {{count}} topic(s)"
|
||||||
|
},
|
||||||
|
"deselect_all": "Deselect All",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "At least one topic must be kept"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Move",
|
||||||
|
"placeholder": "Select target assistant",
|
||||||
|
"success": "Moved {{count}} topic(s)"
|
||||||
|
},
|
||||||
|
"pinned": "Pinned Topics",
|
||||||
|
"selected_count": "{{count}} selected",
|
||||||
|
"title": "Manage Topics",
|
||||||
|
"unpinned": "Unpinned Topics"
|
||||||
|
},
|
||||||
"move_to": "Move to",
|
"move_to": "Move to",
|
||||||
"new": "New Topic",
|
"new": "New Topic",
|
||||||
"pin": "Pin Topic",
|
"pin": "Pin Topic",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Topic Prompts",
|
"label": "Topic Prompts",
|
||||||
"tips": "Topic Prompts: Additional supplementary prompts provided for the current topic"
|
"tips": "Topic Prompts: Additional supplementary prompts provided for the current topic"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search topics...",
|
||||||
|
"title": "Search"
|
||||||
|
},
|
||||||
"title": "Topics",
|
"title": "Topics",
|
||||||
"unpin": "Unpin Topic"
|
"unpin": "Unpin Topic"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
|
"subscribe": "Subscribe",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"topics": "Topics",
|
"topics": "Topics",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unnamed": "Unnamed",
|
"unnamed": "Unnamed",
|
||||||
|
"unsubscribe": "Unsubscribe",
|
||||||
"update_success": "Update successfully",
|
"update_success": "Update successfully",
|
||||||
"upload_files": "Upload file",
|
"upload_files": "Upload file",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
@ -1720,7 +1753,7 @@
|
|||||||
"import": {
|
"import": {
|
||||||
"error": "Import failed"
|
"error": "Import failed"
|
||||||
},
|
},
|
||||||
"imported": "Imported successfully"
|
"imported": "Successfully imported {{count}} assistant(s)"
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"check": {
|
"check": {
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
"content_placeholder": "Please enter the note content...",
|
"content_placeholder": "Please enter the note content...",
|
||||||
"copyContent": "Copy Content",
|
"copyContent": "Copy Content",
|
||||||
|
"crossPlatformRestoreWarning": "Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "delete",
|
"delete": "delete",
|
||||||
"delete_confirm": "Are you sure you want to delete this {{type}}?",
|
"delete_confirm": "Are you sure you want to delete this {{type}}?",
|
||||||
"delete_folder_confirm": "Are you sure you want to delete the folder \"{{name}}\" and all of its contents?",
|
"delete_folder_confirm": "Are you sure you want to delete the folder \"{{name}}\" and all of its contents?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "导入",
|
"button": "导入",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "从 URL 获取数据失败",
|
"fetch_failed": "从 URL 获取数据失败",
|
||||||
|
"file_required": "请先选择文件",
|
||||||
"invalid_format": "无效的助手格式:缺少必填字段",
|
"invalid_format": "无效的助手格式:缺少必填字段",
|
||||||
"url_required": "请输入 URL"
|
"url_required": "请输入 URL"
|
||||||
},
|
},
|
||||||
@ -486,11 +487,14 @@
|
|||||||
},
|
},
|
||||||
"manage": {
|
"manage": {
|
||||||
"batch_delete": {
|
"batch_delete": {
|
||||||
"button": "批量删除",
|
"button": "删除",
|
||||||
"confirm": "确定要删除选中的 {{count}} 个助手吗?"
|
"confirm": "确定要删除选中的 {{count}} 个助手吗?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "导出"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "删除",
|
"manage": "管理",
|
||||||
"sort": "排序"
|
"sort": "排序"
|
||||||
},
|
},
|
||||||
"title": "管理助手"
|
"title": "管理助手"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "导出到语雀"
|
"yuque": "导出到语雀"
|
||||||
},
|
},
|
||||||
"list": "话题列表",
|
"list": "话题列表",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "取消选择",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "确定要删除选中的 {{count}} 个话题吗?此操作不可撤销。",
|
||||||
|
"title": "删除话题"
|
||||||
|
},
|
||||||
|
"success": "已删除 {{count}} 个话题"
|
||||||
|
},
|
||||||
|
"deselect_all": "取消全选",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "至少需要保留一个话题"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "移动",
|
||||||
|
"placeholder": "选择目标助手",
|
||||||
|
"success": "已移动 {{count}} 个话题"
|
||||||
|
},
|
||||||
|
"pinned": "已固定的话题",
|
||||||
|
"selected_count": "已选择 {{count}} 个",
|
||||||
|
"title": "管理话题",
|
||||||
|
"unpinned": "未固定的话题"
|
||||||
|
},
|
||||||
"move_to": "移动到",
|
"move_to": "移动到",
|
||||||
"new": "开始新对话",
|
"new": "开始新对话",
|
||||||
"pin": "固定话题",
|
"pin": "固定话题",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "话题提示词",
|
"label": "话题提示词",
|
||||||
"tips": "话题提示词:针对当前话题提供额外的补充提示词"
|
"tips": "话题提示词:针对当前话题提供额外的补充提示词"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "搜索话题...",
|
||||||
|
"title": "搜索"
|
||||||
|
},
|
||||||
"title": "话题",
|
"title": "话题",
|
||||||
"unpin": "取消固定"
|
"unpin": "取消固定"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "停止",
|
"stop": "停止",
|
||||||
|
"subscribe": "订阅",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"swap": "交换",
|
"swap": "交换",
|
||||||
"topics": "话题",
|
"topics": "话题",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"unnamed": "未命名",
|
"unnamed": "未命名",
|
||||||
|
"unsubscribe": "退订",
|
||||||
"update_success": "更新成功",
|
"update_success": "更新成功",
|
||||||
"upload_files": "上传文件",
|
"upload_files": "上传文件",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
@ -1720,7 +1753,7 @@
|
|||||||
"import": {
|
"import": {
|
||||||
"error": "导入失败"
|
"error": "导入失败"
|
||||||
},
|
},
|
||||||
"imported": "导入成功"
|
"imported": "成功导入 {{count}} 个助手"
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"check": {
|
"check": {
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "收起",
|
"collapse": "收起",
|
||||||
"content_placeholder": "请输入笔记内容...",
|
"content_placeholder": "请输入笔记内容...",
|
||||||
"copyContent": "复制内容",
|
"copyContent": "复制内容",
|
||||||
|
"crossPlatformRestoreWarning": "检测到从其他设备恢复配置,但笔记目录为空。请将笔记文件复制到: {{path}}",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete_confirm": "确定要删除这个{{type}}吗?",
|
"delete_confirm": "确定要删除这个{{type}}吗?",
|
||||||
"delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?",
|
"delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "匯入",
|
"button": "匯入",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "從 URL 取得資料失敗",
|
"fetch_failed": "從 URL 取得資料失敗",
|
||||||
|
"file_required": "請先選擇一個檔案",
|
||||||
"invalid_format": "無效的助手格式:缺少必填欄位",
|
"invalid_format": "無效的助手格式:缺少必填欄位",
|
||||||
"url_required": "請輸入 URL"
|
"url_required": "請輸入 URL"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "批次刪除",
|
"button": "批次刪除",
|
||||||
"confirm": "確定要刪除所選的 {{count}} 個助手嗎?"
|
"confirm": "確定要刪除所選的 {{count}} 個助手嗎?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "匯出"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "刪除",
|
"manage": "管理",
|
||||||
"sort": "排序"
|
"sort": "排序"
|
||||||
},
|
},
|
||||||
"title": "管理助手"
|
"title": "管理助手"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "匯出到語雀"
|
"yuque": "匯出到語雀"
|
||||||
},
|
},
|
||||||
"list": "話題列表",
|
"list": "話題列表",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "取消選擇",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "確定要刪除選中的 {{count}} 個話題嗎?此操作不可撤銷。",
|
||||||
|
"title": "刪除話題"
|
||||||
|
},
|
||||||
|
"success": "已刪除 {{count}} 個話題"
|
||||||
|
},
|
||||||
|
"deselect_all": "取消全選",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "至少需要保留一個話題"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "移動",
|
||||||
|
"placeholder": "選擇目標助手",
|
||||||
|
"success": "已移動 {{count}} 個話題"
|
||||||
|
},
|
||||||
|
"pinned": "已固定的話題",
|
||||||
|
"selected_count": "已選擇 {{count}} 個",
|
||||||
|
"title": "管理話題",
|
||||||
|
"unpinned": "未固定的話題"
|
||||||
|
},
|
||||||
"move_to": "移動到",
|
"move_to": "移動到",
|
||||||
"new": "開始新對話",
|
"new": "開始新對話",
|
||||||
"pin": "固定話題",
|
"pin": "固定話題",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "話題提示詞",
|
"label": "話題提示詞",
|
||||||
"tips": "話題提示詞:針對目前話題提供額外的補充提示詞"
|
"tips": "話題提示詞:針對目前話題提供額外的補充提示詞"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "搜尋話題...",
|
||||||
|
"title": "搜尋"
|
||||||
|
},
|
||||||
"title": "話題",
|
"title": "話題",
|
||||||
"unpin": "取消固定"
|
"unpin": "取消固定"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "停止",
|
"stop": "停止",
|
||||||
|
"subscribe": "訂閱",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"swap": "交換",
|
"swap": "交換",
|
||||||
"topics": "話題",
|
"topics": "話題",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"unnamed": "未命名",
|
"unnamed": "未命名",
|
||||||
|
"unsubscribe": "取消訂閱",
|
||||||
"update_success": "更新成功",
|
"update_success": "更新成功",
|
||||||
"upload_files": "上傳檔案",
|
"upload_files": "上傳檔案",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "收合",
|
"collapse": "收合",
|
||||||
"content_placeholder": "請輸入筆記內容...",
|
"content_placeholder": "請輸入筆記內容...",
|
||||||
"copyContent": "複製內容",
|
"copyContent": "複製內容",
|
||||||
|
"crossPlatformRestoreWarning": "偵測到從其他裝置恢復設定,但筆記目錄為空。請將筆記檔案複製到:{{path}}",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"delete_confirm": "確定要刪除此 {{type}} 嗎?",
|
"delete_confirm": "確定要刪除此 {{type}} 嗎?",
|
||||||
"delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?",
|
"delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Importieren",
|
"button": "Importieren",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Daten von URL abrufen fehlgeschlagen",
|
"fetch_failed": "Daten von URL abrufen fehlgeschlagen",
|
||||||
|
"file_required": "Bitte wählen Sie zuerst eine Datei aus",
|
||||||
"invalid_format": "Ungültiges Assistentenformat: Pflichtfelder fehlen",
|
"invalid_format": "Ungültiges Assistentenformat: Pflichtfelder fehlen",
|
||||||
"url_required": "Bitte geben Sie eine URL ein"
|
"url_required": "Bitte geben Sie eine URL ein"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "Stapel löschen",
|
"button": "Stapel löschen",
|
||||||
"confirm": "Sind Sie sicher, dass Sie die ausgewählten {{count}} Assistenten löschen möchten?"
|
"confirm": "Sind Sie sicher, dass Sie die ausgewählten {{count}} Assistenten löschen möchten?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Exportieren"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Löschen",
|
"manage": "Verwalten",
|
||||||
"sort": "Sortieren"
|
"sort": "Sortieren"
|
||||||
},
|
},
|
||||||
"title": "Assistenten verwalten"
|
"title": "Assistenten verwalten"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Nach Yuque exportieren"
|
"yuque": "Nach Yuque exportieren"
|
||||||
},
|
},
|
||||||
"list": "Themenliste",
|
"list": "Themenliste",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Auswahl aufheben",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "Sind Sie sicher, dass Sie {{count}} ausgewähltes Thema bzw. ausgewählte Themen löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"title": "Themen löschen"
|
||||||
|
},
|
||||||
|
"success": "{{count}} Thema/Themen gelöscht"
|
||||||
|
},
|
||||||
|
"deselect_all": "Alle abwählen",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "Mindestens ein Thema muss beibehalten werden"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Bewegen",
|
||||||
|
"placeholder": "Ziel auswählen",
|
||||||
|
"success": "{{count}} Thema(s) verschoben"
|
||||||
|
},
|
||||||
|
"pinned": "Angepinnte Themen",
|
||||||
|
"selected_count": "{{count}} ausgewählt",
|
||||||
|
"title": "Themen verwalten",
|
||||||
|
"unpinned": "Losgelöste Themen"
|
||||||
|
},
|
||||||
"move_to": "Verschieben nach",
|
"move_to": "Verschieben nach",
|
||||||
"new": "Neues Gespräch starten",
|
"new": "Neues Gespräch starten",
|
||||||
"pin": "Thema anheften",
|
"pin": "Thema anheften",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Themen-Prompt",
|
"label": "Themen-Prompt",
|
||||||
"tips": "Themen-Prompt: Bietet zusätzliche ergänzende Prompts für das aktuelle Thema"
|
"tips": "Themen-Prompt: Bietet zusätzliche ergänzende Prompts für das aktuelle Thema"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Themen durchsuchen...",
|
||||||
|
"title": "Suche"
|
||||||
|
},
|
||||||
"title": "Thema",
|
"title": "Thema",
|
||||||
"unpin": "Anheften aufheben"
|
"unpin": "Anheften aufheben"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "Stoppen",
|
"stop": "Stoppen",
|
||||||
|
"subscribe": "Abonnieren",
|
||||||
"success": "Erfolgreich",
|
"success": "Erfolgreich",
|
||||||
"swap": "Tauschen",
|
"swap": "Tauschen",
|
||||||
"topics": "Themen",
|
"topics": "Themen",
|
||||||
"unknown": "Unbekannt",
|
"unknown": "Unbekannt",
|
||||||
"unnamed": "Unbenannt",
|
"unnamed": "Unbenannt",
|
||||||
|
"unsubscribe": "Abmelden",
|
||||||
"update_success": "Erfolgreich aktualisiert",
|
"update_success": "Erfolgreich aktualisiert",
|
||||||
"upload_files": "Dateien hochladen",
|
"upload_files": "Dateien hochladen",
|
||||||
"warning": "Warnung",
|
"warning": "Warnung",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "Einklappen",
|
"collapse": "Einklappen",
|
||||||
"content_placeholder": "Bitte Notizinhalt eingeben...",
|
"content_placeholder": "Bitte Notizinhalt eingeben...",
|
||||||
"copyContent": "Inhalt kopieren",
|
"copyContent": "Inhalt kopieren",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete_confirm": "Möchten Sie diesen {{type}} wirklich löschen?",
|
"delete_confirm": "Möchten Sie diesen {{type}} wirklich löschen?",
|
||||||
"delete_folder_confirm": "Möchten Sie Ordner \"{{name}}\" und alle seine Inhalte wirklich löschen?",
|
"delete_folder_confirm": "Möchten Sie Ordner \"{{name}}\" und alle seine Inhalte wirklich löschen?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Εισαγωγή",
|
"button": "Εισαγωγή",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Αποτυχία λήψης δεδομένων από το URL",
|
"fetch_failed": "Αποτυχία λήψης δεδομένων από το URL",
|
||||||
|
"file_required": "Παρακαλώ επιλέξτε πρώτα ένα αρχείο",
|
||||||
"invalid_format": "Μη έγκυρη μορφή βοηθού: λείπουν υποχρεωτικά πεδία",
|
"invalid_format": "Μη έγκυρη μορφή βοηθού: λείπουν υποχρεωτικά πεδία",
|
||||||
"url_required": "Παρακαλώ εισάγετε ένα URL"
|
"url_required": "Παρακαλώ εισάγετε ένα URL"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "Μαζική Διαγραφή",
|
"button": "Μαζική Διαγραφή",
|
||||||
"confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους επιλεγμένους {{count}} βοηθούς;"
|
"confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους επιλεγμένους {{count}} βοηθούς;"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Εξαγωγή"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Διαγραφή",
|
"manage": "Διαχειριστείτε",
|
||||||
"sort": "Ταξινόμηση"
|
"sort": "Ταξινόμηση"
|
||||||
},
|
},
|
||||||
"title": "Διαχείριση βοηθών"
|
"title": "Διαχείριση βοηθών"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Εξαγωγή στο Yuque"
|
"yuque": "Εξαγωγή στο Yuque"
|
||||||
},
|
},
|
||||||
"list": "Λίστα θεμάτων",
|
"list": "Λίστα θεμάτων",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Καθαρισμός Επιλογής",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "Είσαι βέβαιος ότι θέλεις να διαγράψεις {{count}} επιλεγμένο(α) θέμα(τα); Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
|
||||||
|
"title": "Διαγραφή Θεμάτων"
|
||||||
|
},
|
||||||
|
"success": "Διαγράφηκαν {{count}} θέμα(τα)"
|
||||||
|
},
|
||||||
|
"deselect_all": "Αποεπιλογή όλων",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "Τουλάχιστον ένα θέμα πρέπει να διατηρηθεί"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Μετακίνηση",
|
||||||
|
"placeholder": "Επιλέξτε στόχο",
|
||||||
|
"success": "Μετακινήθηκαν {{count}} θέματα"
|
||||||
|
},
|
||||||
|
"pinned": "Καρφιτσωμένα Θέματα",
|
||||||
|
"selected_count": "{{count}} επιλεγμένα",
|
||||||
|
"title": "Διαχείριση Θεμάτων",
|
||||||
|
"unpinned": "Ξεκαρφωμένα Θέματα"
|
||||||
|
},
|
||||||
"move_to": "Μετακίνηση στο",
|
"move_to": "Μετακίνηση στο",
|
||||||
"new": "Ξεκινήστε νέα συζήτηση",
|
"new": "Ξεκινήστε νέα συζήτηση",
|
||||||
"pin": "Σταθερά θέματα",
|
"pin": "Σταθερά θέματα",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Προσδοκώμενα όρια",
|
"label": "Προσδοκώμενα όρια",
|
||||||
"tips": "Προσδοκώμενα όρια: προσθέτει επιπλέον επιστημονικές προσθήκες για το παρόν θέμα"
|
"tips": "Προσδοκώμενα όρια: προσθέτει επιπλέον επιστημονικές προσθήκες για το παρόν θέμα"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Αναζήτηση θεμάτων...",
|
||||||
|
"title": "Αναζήτηση"
|
||||||
|
},
|
||||||
"title": "Θέματα",
|
"title": "Θέματα",
|
||||||
"unpin": "Ξεκαρφίτσωμα"
|
"unpin": "Ξεκαρφίτσωμα"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "σταματήστε",
|
"stop": "σταματήστε",
|
||||||
|
"subscribe": "Εγγραφείτε",
|
||||||
"success": "Επιτυχία",
|
"success": "Επιτυχία",
|
||||||
"swap": "Εναλλαγή",
|
"swap": "Εναλλαγή",
|
||||||
"topics": "Θέματα",
|
"topics": "Θέματα",
|
||||||
"unknown": "Άγνωστο",
|
"unknown": "Άγνωστο",
|
||||||
"unnamed": "Χωρίς όνομα",
|
"unnamed": "Χωρίς όνομα",
|
||||||
|
"unsubscribe": "Απεγγραφή",
|
||||||
"update_success": "Επιτυχής ενημέρωση",
|
"update_success": "Επιτυχής ενημέρωση",
|
||||||
"upload_files": "Ανέβασμα αρχείου",
|
"upload_files": "Ανέβασμα αρχείου",
|
||||||
"warning": "Προσοχή",
|
"warning": "Προσοχή",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "σύμπτυξη",
|
"collapse": "σύμπτυξη",
|
||||||
"content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...",
|
"content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...",
|
||||||
"copyContent": "αντιγραφή περιεχομένου",
|
"copyContent": "αντιγραφή περιεχομένου",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "διαγραφή",
|
"delete": "διαγραφή",
|
||||||
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};",
|
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};",
|
||||||
"delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;",
|
"delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Importar",
|
"button": "Importar",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Error al obtener datos desde la URL",
|
"fetch_failed": "Error al obtener datos desde la URL",
|
||||||
|
"file_required": "Por favor, selecciona primero un archivo",
|
||||||
"invalid_format": "Formato de asistente inválido: faltan campos obligatorios",
|
"invalid_format": "Formato de asistente inválido: faltan campos obligatorios",
|
||||||
"url_required": "Por favor introduce una URL"
|
"url_required": "Por favor introduce una URL"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "Eliminación por lotes",
|
"button": "Eliminación por lotes",
|
||||||
"confirm": "¿Estás seguro de que quieres eliminar los {{count}} asistentes seleccionados?"
|
"confirm": "¿Estás seguro de que quieres eliminar los {{count}} asistentes seleccionados?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Exportar"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Eliminar",
|
"manage": "Gestionar",
|
||||||
"sort": "Ordenar"
|
"sort": "Ordenar"
|
||||||
},
|
},
|
||||||
"title": "Gestionar asistentes"
|
"title": "Gestionar asistentes"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Exportar a Yuque"
|
"yuque": "Exportar a Yuque"
|
||||||
},
|
},
|
||||||
"list": "Lista de temas",
|
"list": "Lista de temas",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Borrar selección",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "¿Estás seguro de que quieres eliminar {{count}} tema(s) seleccionado(s)? Esta acción no se puede deshacer.",
|
||||||
|
"title": "Eliminar Temas"
|
||||||
|
},
|
||||||
|
"success": "Eliminado(s) {{count}} tema(s)"
|
||||||
|
},
|
||||||
|
"deselect_all": "Deseleccionar todo",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "Al menos se debe mantener un tema."
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Mover",
|
||||||
|
"placeholder": "Seleccionar asistente de destino",
|
||||||
|
"success": "Movido(s) {{count}} tema(s)"
|
||||||
|
},
|
||||||
|
"pinned": "Temas Fijados",
|
||||||
|
"selected_count": "{{count}} seleccionado",
|
||||||
|
"title": "Administrar Temas",
|
||||||
|
"unpinned": "Temas no fijados"
|
||||||
|
},
|
||||||
"move_to": "Mover a",
|
"move_to": "Mover a",
|
||||||
"new": "Iniciar nueva conversación",
|
"new": "Iniciar nueva conversación",
|
||||||
"pin": "Fijar tema",
|
"pin": "Fijar tema",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Palabras clave del tema",
|
"label": "Palabras clave del tema",
|
||||||
"tips": "Palabras clave del tema: proporcionar indicaciones adicionales para el tema actual"
|
"tips": "Palabras clave del tema: proporcionar indicaciones adicionales para el tema actual"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Buscar temas...",
|
||||||
|
"title": "Buscar"
|
||||||
|
},
|
||||||
"title": "Tema",
|
"title": "Tema",
|
||||||
"unpin": "Quitar fijación"
|
"unpin": "Quitar fijación"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "Detener",
|
"stop": "Detener",
|
||||||
|
"subscribe": "Suscribirse",
|
||||||
"success": "Éxito",
|
"success": "Éxito",
|
||||||
"swap": "Intercambiar",
|
"swap": "Intercambiar",
|
||||||
"topics": "Temas",
|
"topics": "Temas",
|
||||||
"unknown": "Desconocido",
|
"unknown": "Desconocido",
|
||||||
"unnamed": "Sin nombre",
|
"unnamed": "Sin nombre",
|
||||||
|
"unsubscribe": "Cancelar suscripción",
|
||||||
"update_success": "Actualización exitosa",
|
"update_success": "Actualización exitosa",
|
||||||
"upload_files": "Subir archivo",
|
"upload_files": "Subir archivo",
|
||||||
"warning": "Advertencia",
|
"warning": "Advertencia",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "ocultar",
|
"collapse": "ocultar",
|
||||||
"content_placeholder": "Introduzca el contenido de la nota...",
|
"content_placeholder": "Introduzca el contenido de la nota...",
|
||||||
"copyContent": "copiar contenido",
|
"copyContent": "copiar contenido",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "eliminar",
|
"delete": "eliminar",
|
||||||
"delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?",
|
"delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?",
|
||||||
"delete_folder_confirm": "¿Está seguro de que desea eliminar la carpeta \"{{name}}\" y todo su contenido?",
|
"delete_folder_confirm": "¿Está seguro de que desea eliminar la carpeta \"{{name}}\" y todo su contenido?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Importer",
|
"button": "Importer",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Échec de la récupération des données depuis l'URL",
|
"fetch_failed": "Échec de la récupération des données depuis l'URL",
|
||||||
|
"file_required": "Veuillez d'abord sélectionner un fichier",
|
||||||
"invalid_format": "Format d'assistant invalide : champs obligatoires manquants",
|
"invalid_format": "Format d'assistant invalide : champs obligatoires manquants",
|
||||||
"url_required": "Veuillez saisir une URL"
|
"url_required": "Veuillez saisir une URL"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "Suppression par lot",
|
"button": "Suppression par lot",
|
||||||
"confirm": "Êtes-vous sûr de vouloir supprimer les {{count}} assistants sélectionnés ?"
|
"confirm": "Êtes-vous sûr de vouloir supprimer les {{count}} assistants sélectionnés ?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Exporter"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Supprimer",
|
"manage": "Gérer",
|
||||||
"sort": "Trier"
|
"sort": "Trier"
|
||||||
},
|
},
|
||||||
"title": "Gérer les assistants"
|
"title": "Gérer les assistants"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Exporter vers Yuque"
|
"yuque": "Exporter vers Yuque"
|
||||||
},
|
},
|
||||||
"list": "Liste des sujets",
|
"list": "Liste des sujets",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Effacer la sélection",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "Êtes-vous sûr de vouloir supprimer {{count}} sujet(s) sélectionné(s) ? Cette action est irréversible.",
|
||||||
|
"title": "Supprimer des sujets"
|
||||||
|
},
|
||||||
|
"success": "Supprimé {{count}} sujet(s)"
|
||||||
|
},
|
||||||
|
"deselect_all": "Tout désélectionner",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "Au moins un sujet doit être conservé"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Déplacer",
|
||||||
|
"placeholder": "Sélectionner la cible",
|
||||||
|
"success": "Déplacé {{count}} sujet(s)"
|
||||||
|
},
|
||||||
|
"pinned": "Sujets épinglés",
|
||||||
|
"selected_count": "{{count}} sélectionné",
|
||||||
|
"title": "Gérer les sujets",
|
||||||
|
"unpinned": "Sujets non épinglés"
|
||||||
|
},
|
||||||
"move_to": "Déplacer vers",
|
"move_to": "Déplacer vers",
|
||||||
"new": "Commencer une nouvelle conversation",
|
"new": "Commencer une nouvelle conversation",
|
||||||
"pin": "Fixer le sujet",
|
"pin": "Fixer le sujet",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Indicateurs de sujet",
|
"label": "Indicateurs de sujet",
|
||||||
"tips": "Indicateurs de sujet : fournir des indications supplémentaires pour le sujet actuel"
|
"tips": "Indicateurs de sujet : fournir des indications supplémentaires pour le sujet actuel"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Rechercher des sujets...",
|
||||||
|
"title": "Rechercher"
|
||||||
|
},
|
||||||
"title": "Sujet",
|
"title": "Sujet",
|
||||||
"unpin": "Annuler le fixage"
|
"unpin": "Annuler le fixage"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "Arrêter",
|
"stop": "Arrêter",
|
||||||
|
"subscribe": "S'abonner",
|
||||||
"success": "Succès",
|
"success": "Succès",
|
||||||
"swap": "Échanger",
|
"swap": "Échanger",
|
||||||
"topics": "Sujets",
|
"topics": "Sujets",
|
||||||
"unknown": "Inconnu",
|
"unknown": "Inconnu",
|
||||||
"unnamed": "Sans nom",
|
"unnamed": "Sans nom",
|
||||||
|
"unsubscribe": "Se désabonner",
|
||||||
"update_success": "Mise à jour réussie",
|
"update_success": "Mise à jour réussie",
|
||||||
"upload_files": "Uploader des fichiers",
|
"upload_files": "Uploader des fichiers",
|
||||||
"warning": "Avertissement",
|
"warning": "Avertissement",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "réduire",
|
"collapse": "réduire",
|
||||||
"content_placeholder": "Veuillez saisir le contenu de la note...",
|
"content_placeholder": "Veuillez saisir le contenu de la note...",
|
||||||
"copyContent": "contenu copié",
|
"copyContent": "contenu copié",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "supprimer",
|
"delete": "supprimer",
|
||||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?",
|
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?",
|
||||||
"delete_folder_confirm": "Êtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" et tout son contenu ?",
|
"delete_folder_confirm": "Êtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" et tout son contenu ?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "インポート",
|
"button": "インポート",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "URLからのデータ取得に失敗しました",
|
"fetch_failed": "URLからのデータ取得に失敗しました",
|
||||||
|
"file_required": "まずファイルを選択してください",
|
||||||
"invalid_format": "無効なアシスタント形式:必須フィールドが不足しています",
|
"invalid_format": "無効なアシスタント形式:必須フィールドが不足しています",
|
||||||
"url_required": "URLを入力してください"
|
"url_required": "URLを入力してください"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "バッチ削除",
|
"button": "バッチ削除",
|
||||||
"confirm": "選択した{{count}}件のアシスタントを削除してもよろしいですか?"
|
"confirm": "選択した{{count}}件のアシスタントを削除してもよろしいですか?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "エクスポート"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "削除",
|
"manage": "管理",
|
||||||
"sort": "並べ替え"
|
"sort": "並べ替え"
|
||||||
},
|
},
|
||||||
"title": "アシスタントを管理"
|
"title": "アシスタントを管理"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "語雀にエクスポート"
|
"yuque": "語雀にエクスポート"
|
||||||
},
|
},
|
||||||
"list": "トピックリスト",
|
"list": "トピックリスト",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "選択をクリア",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "{{count}}件の選択したトピックを削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
|
"title": "トピックを削除"
|
||||||
|
},
|
||||||
|
"success": "{{count}}件のトピックを削除しました"
|
||||||
|
},
|
||||||
|
"deselect_all": "すべての選択を解除",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "少なくとも1つのトピックは保持されなければなりません"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "移動",
|
||||||
|
"placeholder": "対象を選択",
|
||||||
|
"success": "{{count}}件のトピックを移動しました"
|
||||||
|
},
|
||||||
|
"pinned": "ピン留めされたトピック",
|
||||||
|
"selected_count": "{{count}} 選択済み",
|
||||||
|
"title": "トピックを管理",
|
||||||
|
"unpinned": "ピン留めされていないトピック"
|
||||||
|
},
|
||||||
"move_to": "移動先",
|
"move_to": "移動先",
|
||||||
"new": "新しいトピック",
|
"new": "新しいトピック",
|
||||||
"pin": "トピックを固定",
|
"pin": "トピックを固定",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "トピック提示語",
|
"label": "トピック提示語",
|
||||||
"tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供"
|
"tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "トピックを検索...",
|
||||||
|
"title": "検索"
|
||||||
|
},
|
||||||
"title": "トピック",
|
"title": "トピック",
|
||||||
"unpin": "固定解除"
|
"unpin": "固定解除"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "停止",
|
"stop": "停止",
|
||||||
|
"subscribe": "購読",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"swap": "交換",
|
"swap": "交換",
|
||||||
"topics": "トピック",
|
"topics": "トピック",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unnamed": "無題",
|
"unnamed": "無題",
|
||||||
|
"unsubscribe": "配信停止",
|
||||||
"update_success": "更新成功",
|
"update_success": "更新成功",
|
||||||
"upload_files": "ファイルをアップロードする",
|
"upload_files": "ファイルをアップロードする",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "閉じる",
|
"collapse": "閉じる",
|
||||||
"content_placeholder": "メモの内容を入力してください...",
|
"content_placeholder": "メモの内容を入力してください...",
|
||||||
"copyContent": "コンテンツをコピーします",
|
"copyContent": "コンテンツをコピーします",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"delete_confirm": "この{{type}}を本当に削除しますか?",
|
"delete_confirm": "この{{type}}を本当に削除しますか?",
|
||||||
"delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?",
|
"delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Importar",
|
"button": "Importar",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Falha ao obter dados do URL",
|
"fetch_failed": "Falha ao obter dados do URL",
|
||||||
|
"file_required": "Por favor, selecione um arquivo primeiro",
|
||||||
"invalid_format": "Formato de assistente inválido: campos obrigatórios em falta",
|
"invalid_format": "Formato de assistente inválido: campos obrigatórios em falta",
|
||||||
"url_required": "Por favor insere um URL"
|
"url_required": "Por favor insere um URL"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "Exclusão em Lote",
|
"button": "Exclusão em Lote",
|
||||||
"confirm": "Tem certeza de que deseja excluir os {{count}} assistentes selecionados?"
|
"confirm": "Tem certeza de que deseja excluir os {{count}} assistentes selecionados?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Exportar"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Excluir",
|
"manage": "Gerenciar",
|
||||||
"sort": "Ordenar"
|
"sort": "Ordenar"
|
||||||
},
|
},
|
||||||
"title": "Gerir assistentes"
|
"title": "Gerir assistentes"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Exportar para Yuque"
|
"yuque": "Exportar para Yuque"
|
||||||
},
|
},
|
||||||
"list": "Lista de tópicos",
|
"list": "Lista de tópicos",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Limpar Seleção",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "Tem certeza de que deseja excluir {{count}} tópico(s) selecionado(s)? Esta ação não pode ser desfeita.",
|
||||||
|
"title": "Excluir Tópicos"
|
||||||
|
},
|
||||||
|
"success": "Excluído(s) {{count}} tópico(s)"
|
||||||
|
},
|
||||||
|
"deselect_all": "Desmarcar Todos",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "Pelo menos um tópico deve ser mantido"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Mover",
|
||||||
|
"placeholder": "Selecionar assistente de destino",
|
||||||
|
"success": "Movido(s) {{count}} tópico(s)"
|
||||||
|
},
|
||||||
|
"pinned": "Tópicos Fixados",
|
||||||
|
"selected_count": "{{count}} selecionado",
|
||||||
|
"title": "Gerenciar Tópicos",
|
||||||
|
"unpinned": "Tópicos Desafixados"
|
||||||
|
},
|
||||||
"move_to": "Mover para",
|
"move_to": "Mover para",
|
||||||
"new": "Começar nova conversa",
|
"new": "Começar nova conversa",
|
||||||
"pin": "Fixar tópico",
|
"pin": "Fixar tópico",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Prompt do tópico",
|
"label": "Prompt do tópico",
|
||||||
"tips": "Prompt do tópico: fornecer prompts adicionais para o tópico atual"
|
"tips": "Prompt do tópico: fornecer prompts adicionais para o tópico atual"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Pesquisar tópicos...",
|
||||||
|
"title": "Pesquisar"
|
||||||
|
},
|
||||||
"title": "Tópicos",
|
"title": "Tópicos",
|
||||||
"unpin": "Desfixar"
|
"unpin": "Desfixar"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "Parar",
|
"stop": "Parar",
|
||||||
|
"subscribe": "Subscrever",
|
||||||
"success": "Sucesso",
|
"success": "Sucesso",
|
||||||
"swap": "Trocar",
|
"swap": "Trocar",
|
||||||
"topics": "Tópicos",
|
"topics": "Tópicos",
|
||||||
"unknown": "Desconhecido",
|
"unknown": "Desconhecido",
|
||||||
"unnamed": "Sem nome",
|
"unnamed": "Sem nome",
|
||||||
|
"unsubscribe": "Cancelar inscrição",
|
||||||
"update_success": "Atualização bem-sucedida",
|
"update_success": "Atualização bem-sucedida",
|
||||||
"upload_files": "Carregar arquivo",
|
"upload_files": "Carregar arquivo",
|
||||||
"warning": "Aviso",
|
"warning": "Aviso",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "[minimizar]",
|
"collapse": "[minimizar]",
|
||||||
"content_placeholder": "Introduza o conteúdo da nota...",
|
"content_placeholder": "Introduza o conteúdo da nota...",
|
||||||
"copyContent": "copiar conteúdo",
|
"copyContent": "copiar conteúdo",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "eliminar",
|
"delete": "eliminar",
|
||||||
"delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?",
|
"delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?",
|
||||||
"delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?",
|
"delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?",
|
||||||
|
|||||||
@ -472,6 +472,7 @@
|
|||||||
"button": "Импортировать",
|
"button": "Импортировать",
|
||||||
"error": {
|
"error": {
|
||||||
"fetch_failed": "Ошибка получения данных с URL",
|
"fetch_failed": "Ошибка получения данных с URL",
|
||||||
|
"file_required": "Сначала выберите файл",
|
||||||
"invalid_format": "Неверный формат помощника: отсутствуют обязательные поля",
|
"invalid_format": "Неверный формат помощника: отсутствуют обязательные поля",
|
||||||
"url_required": "Пожалуйста, введите URL"
|
"url_required": "Пожалуйста, введите URL"
|
||||||
},
|
},
|
||||||
@ -489,8 +490,11 @@
|
|||||||
"button": "Массовое удаление",
|
"button": "Массовое удаление",
|
||||||
"confirm": "Вы уверены, что хотите удалить выбранных {{count}} ассистентов?"
|
"confirm": "Вы уверены, что хотите удалить выбранных {{count}} ассистентов?"
|
||||||
},
|
},
|
||||||
|
"batch_export": {
|
||||||
|
"button": "Экспорт"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"delete": "Удалить",
|
"manage": "Управлять",
|
||||||
"sort": "Сортировать"
|
"sort": "Сортировать"
|
||||||
},
|
},
|
||||||
"title": "Управление помощниками"
|
"title": "Управление помощниками"
|
||||||
@ -1027,6 +1031,29 @@
|
|||||||
"yuque": "Экспорт в Yuque"
|
"yuque": "Экспорт в Yuque"
|
||||||
},
|
},
|
||||||
"list": "Список топиков",
|
"list": "Список топиков",
|
||||||
|
"manage": {
|
||||||
|
"clear_selection": "Очистить выбор",
|
||||||
|
"delete": {
|
||||||
|
"confirm": {
|
||||||
|
"content": "Вы уверены, что хотите удалить выбранные темы ({{count}})? Это действие нельзя отменить.",
|
||||||
|
"title": "Удалить темы"
|
||||||
|
},
|
||||||
|
"success": "Удалено {{count}} тем(ы)"
|
||||||
|
},
|
||||||
|
"deselect_all": "Снять выделение со всех",
|
||||||
|
"error": {
|
||||||
|
"at_least_one": "Должна быть сохранена хотя бы одна тема"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"button": "Переместить",
|
||||||
|
"placeholder": "Выберите цель",
|
||||||
|
"success": "Перемещено {{count}} тем(ы)"
|
||||||
|
},
|
||||||
|
"pinned": "Закреплённые темы",
|
||||||
|
"selected_count": "{{count}} выбрано",
|
||||||
|
"title": "Управление темами",
|
||||||
|
"unpinned": "Откреплённые темы"
|
||||||
|
},
|
||||||
"move_to": "Переместить в",
|
"move_to": "Переместить в",
|
||||||
"new": "Новый топик",
|
"new": "Новый топик",
|
||||||
"pin": "Закрепленные темы",
|
"pin": "Закрепленные темы",
|
||||||
@ -1037,6 +1064,10 @@
|
|||||||
"label": "Тематические подсказки",
|
"label": "Тематические подсказки",
|
||||||
"tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы"
|
"tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Искать темы...",
|
||||||
|
"title": "Поиск"
|
||||||
|
},
|
||||||
"title": "Топики",
|
"title": "Топики",
|
||||||
"unpin": "Открепленные темы"
|
"unpin": "Открепленные темы"
|
||||||
},
|
},
|
||||||
@ -1221,11 +1252,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stop": "остановить",
|
"stop": "остановить",
|
||||||
|
"subscribe": "Подписаться",
|
||||||
"success": "Успешно",
|
"success": "Успешно",
|
||||||
"swap": "Поменять местами",
|
"swap": "Поменять местами",
|
||||||
"topics": "Топики",
|
"topics": "Топики",
|
||||||
"unknown": "Неизвестно",
|
"unknown": "Неизвестно",
|
||||||
"unnamed": "Без имени",
|
"unnamed": "Без имени",
|
||||||
|
"unsubscribe": "Отписаться",
|
||||||
"update_success": "Обновление выполнено успешно",
|
"update_success": "Обновление выполнено успешно",
|
||||||
"upload_files": "Загрузить файл",
|
"upload_files": "Загрузить файл",
|
||||||
"warning": "Предупреждение",
|
"warning": "Предупреждение",
|
||||||
@ -2152,6 +2185,7 @@
|
|||||||
"collapse": "Свернуть",
|
"collapse": "Свернуть",
|
||||||
"content_placeholder": "Введите содержимое заметки...",
|
"content_placeholder": "Введите содержимое заметки...",
|
||||||
"copyContent": "Копировать контент",
|
"copyContent": "Копировать контент",
|
||||||
|
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
|
||||||
"delete": "удалить",
|
"delete": "удалить",
|
||||||
"delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?",
|
"delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?",
|
||||||
"delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?",
|
"delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?",
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar'
|
||||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
|
||||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useTags } from '@renderer/hooks/useTags'
|
import { useTags } from '@renderer/hooks/useTags'
|
||||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||||
import { cn, getLeadingEmoji, uuid } from '@renderer/utils'
|
import { cn, uuid } from '@renderer/utils'
|
||||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Dropdown } from 'antd'
|
import { Dropdown } from 'antd'
|
||||||
@ -67,8 +65,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { allTags } = useTags()
|
const { allTags } = useTags()
|
||||||
const { removeAllTopics } = useAssistant(assistant.id)
|
const { removeAllTopics } = useAssistant(assistant.id)
|
||||||
const { clickAssistantToShowTopic, topicPosition, assistantIconType, setAssistantIconType } = useSettings()
|
const { clickAssistantToShowTopic, topicPosition, setAssistantIconType } = useSettings()
|
||||||
const defaultModel = getDefaultModel()
|
|
||||||
const { assistants, updateAssistants } = useAssistants()
|
const { assistants, updateAssistants } = useAssistants()
|
||||||
|
|
||||||
const [isPending, setIsPending] = useState(false)
|
const [isPending, setIsPending] = useState(false)
|
||||||
@ -166,20 +163,11 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
|||||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||||
<Container onClick={handleSwitch} isActive={isActive}>
|
<Container onClick={handleSwitch} isActive={isActive}>
|
||||||
<AssistantNameRow className="name" title={fullAssistantName}>
|
<AssistantNameRow className="name" title={fullAssistantName}>
|
||||||
{assistantIconType === 'model' ? (
|
<AssistantAvatar
|
||||||
<ModelAvatar
|
assistant={assistant}
|
||||||
model={assistant.model || defaultModel}
|
size={24}
|
||||||
size={24}
|
className={isPending && !isActive ? 'animation-pulse' : ''}
|
||||||
className={isPending && !isActive ? 'animation-pulse' : ''}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
assistantIconType === 'emoji' && (
|
|
||||||
<EmojiIcon
|
|
||||||
emoji={assistant.emoji || getLeadingEmoji(assistantName)}
|
|
||||||
className={isPending && !isActive ? 'animation-pulse' : ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
|
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
|
||||||
</AssistantNameRow>
|
</AssistantNameRow>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
|
|||||||
411
src/renderer/src/pages/home/Tabs/components/TopicManageMode.tsx
Normal file
411
src/renderer/src/pages/home/Tabs/components/TopicManageMode.tsx
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar'
|
||||||
|
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||||
|
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||||
|
import type { Assistant, Topic } from '@renderer/types'
|
||||||
|
import { cn } from '@renderer/utils'
|
||||||
|
import { Dropdown, Tooltip } from 'antd'
|
||||||
|
import { CheckSquare, FolderOpen, Search, Square, Trash2, XIcon } from 'lucide-react'
|
||||||
|
import type { FC, PropsWithChildren, Ref } from 'react'
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export interface TopicManageModeState {
|
||||||
|
isManageMode: boolean
|
||||||
|
selectedIds: Set<string>
|
||||||
|
searchText: string
|
||||||
|
enterManageMode: () => void
|
||||||
|
exitManageMode: () => void
|
||||||
|
toggleSelectTopic: (topicId: string) => void
|
||||||
|
setSelectedIds: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||||
|
setSearchText: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing topic selection state
|
||||||
|
*/
|
||||||
|
export function useTopicManageMode(): TopicManageModeState {
|
||||||
|
const [isManageMode, setIsManageMode] = useState(false)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
|
const [searchText, setSearchText] = useState('')
|
||||||
|
|
||||||
|
const enterManageMode = useCallback(() => {
|
||||||
|
setIsManageMode(true)
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
setSearchText('')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const exitManageMode = useCallback(() => {
|
||||||
|
setIsManageMode(false)
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
setSearchText('')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleSelectTopic = useCallback((topicId: string) => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(topicId)) {
|
||||||
|
next.delete(topicId)
|
||||||
|
} else {
|
||||||
|
next.add(topicId)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
isManageMode,
|
||||||
|
selectedIds,
|
||||||
|
searchText,
|
||||||
|
enterManageMode,
|
||||||
|
exitManageMode,
|
||||||
|
toggleSelectTopic,
|
||||||
|
setSelectedIds,
|
||||||
|
setSearchText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopicManagePanelProps {
|
||||||
|
assistant: Assistant
|
||||||
|
assistants: Assistant[]
|
||||||
|
activeTopic: Topic
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
|
removeTopic: (topic: Topic) => void
|
||||||
|
moveTopic: (topic: Topic, toAssistant: Assistant) => void
|
||||||
|
manageState: TopicManageModeState
|
||||||
|
filteredTopics: Topic[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom panel component for topic management mode
|
||||||
|
*/
|
||||||
|
export const TopicManagePanel: React.FC<TopicManagePanelProps> = ({
|
||||||
|
assistant,
|
||||||
|
assistants,
|
||||||
|
activeTopic,
|
||||||
|
setActiveTopic,
|
||||||
|
removeTopic,
|
||||||
|
moveTopic,
|
||||||
|
manageState,
|
||||||
|
filteredTopics
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isManageMode, selectedIds, searchText, exitManageMode, setSelectedIds, setSearchText } = manageState
|
||||||
|
const [isSearchMode, setIsSearchMode] = useState(false)
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Topics that can be selected (non-pinned, and filtered when in search mode)
|
||||||
|
const selectableTopics = useMemo(() => {
|
||||||
|
const baseTopics = isSearchMode ? filteredTopics : assistant.topics
|
||||||
|
return baseTopics.filter((topic) => !topic.pinned)
|
||||||
|
}, [assistant.topics, filteredTopics, isSearchMode])
|
||||||
|
|
||||||
|
// Check if all selectable topics are selected
|
||||||
|
const isAllSelected = useMemo(() => {
|
||||||
|
return selectableTopics.length > 0 && selectableTopics.every((topic) => selectedIds.has(topic.id))
|
||||||
|
}, [selectableTopics, selectedIds])
|
||||||
|
|
||||||
|
// Other assistants for move operation
|
||||||
|
const otherAssistants = useMemo(() => assistants.filter((a) => a.id !== assistant.id), [assistants, assistant.id])
|
||||||
|
|
||||||
|
// Handle select all / deselect all
|
||||||
|
const handleSelectAll = useCallback(() => {
|
||||||
|
if (isAllSelected) {
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
} else {
|
||||||
|
setSelectedIds(new Set(selectableTopics.map((topic) => topic.id)))
|
||||||
|
}
|
||||||
|
}, [isAllSelected, selectableTopics, setSelectedIds])
|
||||||
|
|
||||||
|
// Handle clear selection
|
||||||
|
const handleClearSelection = useCallback(() => {
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
}, [setSelectedIds])
|
||||||
|
|
||||||
|
// Handle delete selected topics
|
||||||
|
const handleDeleteSelected = useCallback(async () => {
|
||||||
|
if (selectedIds.size === 0) return
|
||||||
|
|
||||||
|
const remainingTopics = assistant.topics.filter((topic) => !selectedIds.has(topic.id))
|
||||||
|
if (remainingTopics.length === 0) {
|
||||||
|
window.toast.error(t('chat.topics.manage.error.at_least_one'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await window.modal.confirm({
|
||||||
|
title: t('chat.topics.manage.delete.confirm.title'),
|
||||||
|
content: t('chat.topics.manage.delete.confirm.content', { count: selectedIds.size }),
|
||||||
|
centered: true,
|
||||||
|
okButtonProps: { danger: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
await modelGenerating()
|
||||||
|
|
||||||
|
const deletedCount = selectedIds.size
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
const topic = assistant.topics.find((t) => t.id === id)
|
||||||
|
if (topic) {
|
||||||
|
await TopicManager.removeTopic(id)
|
||||||
|
removeTopic(topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to first remaining topic if current topic was deleted
|
||||||
|
if (selectedIds.has(activeTopic.id)) {
|
||||||
|
setActiveTopic(remainingTopics[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toast.success(t('chat.topics.manage.delete.success', { count: deletedCount }))
|
||||||
|
exitManageMode()
|
||||||
|
}, [selectedIds, assistant.topics, removeTopic, activeTopic.id, setActiveTopic, t, exitManageMode])
|
||||||
|
|
||||||
|
// Handle move selected topics to another assistant
|
||||||
|
const handleMoveSelected = useCallback(
|
||||||
|
async (targetAssistantId: string) => {
|
||||||
|
if (selectedIds.size === 0) return
|
||||||
|
|
||||||
|
const targetAssistant = assistants.find((a) => a.id === targetAssistantId)
|
||||||
|
if (!targetAssistant) return
|
||||||
|
|
||||||
|
const remainingTopics = assistant.topics.filter((topic) => !selectedIds.has(topic.id))
|
||||||
|
if (remainingTopics.length === 0) {
|
||||||
|
window.toast.error(t('chat.topics.manage.error.at_least_one'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await modelGenerating()
|
||||||
|
|
||||||
|
const movedCount = selectedIds.size
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
const topic = assistant.topics.find((t) => t.id === id)
|
||||||
|
if (topic) {
|
||||||
|
moveTopic(topic, targetAssistant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to first remaining topic if current topic was moved
|
||||||
|
if (selectedIds.has(activeTopic.id)) {
|
||||||
|
setActiveTopic(remainingTopics[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toast.success(t('chat.topics.manage.move.success', { count: movedCount }))
|
||||||
|
exitManageMode()
|
||||||
|
},
|
||||||
|
[selectedIds, assistant.topics, assistants, moveTopic, activeTopic.id, setActiveTopic, t, exitManageMode]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
const enterSearchMode = useCallback(() => {
|
||||||
|
setIsSearchMode(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Exit search mode
|
||||||
|
const exitSearchMode = useCallback(() => {
|
||||||
|
setIsSearchMode(false)
|
||||||
|
setSearchText('')
|
||||||
|
}, [setSearchText])
|
||||||
|
|
||||||
|
// Focus input when entering search mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSearchMode && searchInputRef.current) {
|
||||||
|
searchInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [isSearchMode])
|
||||||
|
|
||||||
|
// Reset search mode when exiting manage mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManageMode) {
|
||||||
|
setIsSearchMode(false)
|
||||||
|
}
|
||||||
|
}, [isManageMode])
|
||||||
|
|
||||||
|
// Handle search input keydown
|
||||||
|
const handleSearchKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
exitSearchMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[exitSearchMode]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isManageMode) return null
|
||||||
|
|
||||||
|
// Search mode UI
|
||||||
|
if (isSearchMode) {
|
||||||
|
return (
|
||||||
|
<ManagePanel>
|
||||||
|
<ManagePanelContent>
|
||||||
|
<LeftGroup>
|
||||||
|
<Tooltip title={isAllSelected ? t('chat.topics.manage.deselect_all') : t('common.select_all')}>
|
||||||
|
<ManageIconButton onClick={handleSelectAll}>
|
||||||
|
{isAllSelected ? <CheckSquare size={16} /> : <Square size={16} />}
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{selectedIds.size > 0 && (
|
||||||
|
<Tooltip title={t('chat.topics.manage.clear_selection')}>
|
||||||
|
<SelectedBadge onClick={handleClearSelection}>{selectedIds.size}</SelectedBadge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</LeftGroup>
|
||||||
|
<SearchInputWrapper>
|
||||||
|
<SearchInput
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('chat.topics.search.placeholder')}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
|
/>
|
||||||
|
</SearchInputWrapper>
|
||||||
|
<Tooltip title={t('common.close')}>
|
||||||
|
<ManageIconButton onClick={exitSearchMode}>
|
||||||
|
<XIcon size={16} />
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ManagePanelContent>
|
||||||
|
</ManagePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal manage mode UI
|
||||||
|
return (
|
||||||
|
<ManagePanel>
|
||||||
|
<ManagePanelContent>
|
||||||
|
<LeftGroup>
|
||||||
|
<Tooltip title={isAllSelected ? t('chat.topics.manage.deselect_all') : t('common.select_all')}>
|
||||||
|
<ManageIconButton onClick={handleSelectAll}>
|
||||||
|
{isAllSelected ? <CheckSquare size={16} /> : <Square size={16} />}
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{selectedIds.size > 0 && (
|
||||||
|
<Tooltip title={t('chat.topics.manage.clear_selection')}>
|
||||||
|
<SelectedBadge onClick={handleClearSelection}>{selectedIds.size}</SelectedBadge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</LeftGroup>
|
||||||
|
<RightGroup>
|
||||||
|
<Tooltip title={t('chat.topics.search.title')}>
|
||||||
|
<ManageIconButton onClick={enterSearchMode}>
|
||||||
|
<Search size={16} />
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{otherAssistants.length > 0 && (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: otherAssistants.map((a) => ({
|
||||||
|
key: a.id,
|
||||||
|
label: a.name,
|
||||||
|
icon: <AssistantAvatar assistant={a} size={18} />,
|
||||||
|
onClick: () => handleMoveSelected(a.id),
|
||||||
|
disabled: selectedIds.size === 0
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
trigger={['click']}
|
||||||
|
disabled={selectedIds.size === 0}>
|
||||||
|
<Tooltip title={t('chat.topics.move_to')}>
|
||||||
|
<ManageIconButton disabled={selectedIds.size === 0}>
|
||||||
|
<FolderOpen size={16} />
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
<Tooltip title={t('common.delete')}>
|
||||||
|
<ManageIconButton danger onClick={handleDeleteSelected} disabled={selectedIds.size === 0}>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<ManageDivider />
|
||||||
|
<Tooltip title={t('common.cancel')}>
|
||||||
|
<ManageIconButton onClick={exitManageMode}>
|
||||||
|
<XIcon size={16} />
|
||||||
|
</ManageIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</RightGroup>
|
||||||
|
</ManagePanelContent>
|
||||||
|
</ManagePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tailwind components
|
||||||
|
const ManagePanel: FC<PropsWithChildren> = ({ children }) => (
|
||||||
|
<div className="absolute bottom-[15px] left-[12px] z-[100] flex w-[calc(var(--assistants-width)-24px)] flex-row items-center rounded-xl bg-[var(--color-background)] px-3 py-2 shadow-[0_4px_12px_rgba(0,0,0,0.15),0_0_0_1px_var(--color-border)]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ManagePanelContent: FC<PropsWithChildren> = ({ children }) => (
|
||||||
|
<div className="flex w-full min-w-0 flex-row items-center gap-1 overflow-hidden">{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ManageIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
danger?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ManageIconButton: FC<PropsWithChildren<ManageIconButtonProps>> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
danger,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-none bg-transparent text-[var(--color-text-2)] transition-all duration-200',
|
||||||
|
disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',
|
||||||
|
!disabled && !danger && 'hover:bg-[var(--color-background-mute)] hover:text-[var(--color-text-1)]',
|
||||||
|
danger && 'text-[var(--color-error)]',
|
||||||
|
danger && !disabled && 'hover:bg-[var(--color-error)] hover:text-white [&:hover>svg]:text-white',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ManageDivider: FC = () => <div className="mx-1 h-5 w-px bg-[var(--color-border)]" />
|
||||||
|
|
||||||
|
const LeftGroup: FC<PropsWithChildren> = ({ children }) => <div className="flex items-center gap-1">{children}</div>
|
||||||
|
|
||||||
|
const RightGroup: FC<PropsWithChildren> = ({ children }) => (
|
||||||
|
<div className="ml-auto flex items-center gap-1">{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SelectedBadge: FC<PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<span
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex h-[18px] min-w-[18px] cursor-pointer items-center justify-center rounded-[9px] bg-[var(--color-primary)] px-[5px] font-medium text-[11px] text-white transition-opacity duration-200 hover:opacity-[0.85]',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SearchInputWrapper: FC<PropsWithChildren> = ({ children }) => (
|
||||||
|
<div className="mx-1 flex min-w-0 flex-1 items-center gap-1">{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
ref?: Ref<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchInput: FC<SearchInputProps> = ({ className, ref, ...props }) => (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'h-7 min-w-0 flex-1 border-none bg-transparent p-0 text-[13px] text-[var(--color-text-1)] outline-none placeholder:text-[var(--color-text-3)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TopicManagePanel
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar'
|
||||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||||
@ -37,8 +38,10 @@ import dayjs from 'dayjs'
|
|||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import {
|
import {
|
||||||
BrushCleaning,
|
BrushCleaning,
|
||||||
|
CheckSquare,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
ListChecks,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
NotebookPen,
|
NotebookPen,
|
||||||
PackagePlus,
|
PackagePlus,
|
||||||
@ -46,6 +49,7 @@ import {
|
|||||||
PinOffIcon,
|
PinOffIcon,
|
||||||
Save,
|
Save,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Square,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon
|
XIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@ -55,6 +59,7 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import AddButton from './AddButton'
|
import AddButton from './AddButton'
|
||||||
|
import { TopicManagePanel, useTopicManageMode } from './TopicManageMode'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@ -81,6 +86,10 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// 管理模式状态
|
||||||
|
const manageState = useTopicManageMode()
|
||||||
|
const { isManageMode, selectedIds, searchText, enterManageMode, exitManageMode, toggleSelectTopic } = manageState
|
||||||
|
|
||||||
const { startEdit, isEditing, inputProps } = useInPlaceEdit({
|
const { startEdit, isEditing, inputProps } = useInPlaceEdit({
|
||||||
onSave: (name: string) => {
|
onSave: (name: string) => {
|
||||||
const topic = assistant.topics.find((t) => t.id === editingTopicId)
|
const topic = assistant.topics.find((t) => t.id === editingTopicId)
|
||||||
@ -437,6 +446,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
.map((a) => ({
|
.map((a) => ({
|
||||||
label: a.name,
|
label: a.name,
|
||||||
key: a.id,
|
key: a.id,
|
||||||
|
icon: <AssistantAvatar assistant={a} size={18} />,
|
||||||
onClick: () => onMoveTopic(topic, a)
|
onClick: () => onMoveTopic(topic, a)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@ -492,107 +502,187 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
return assistant.topics
|
return assistant.topics
|
||||||
}, [assistant.topics, pinTopicsToTop])
|
}, [assistant.topics, pinTopicsToTop])
|
||||||
|
|
||||||
|
// Filter topics based on search text (only in manage mode)
|
||||||
|
// Supports: case-insensitive, space-separated keywords (all must match)
|
||||||
|
const deferredSearchText = useDeferredValue(searchText)
|
||||||
|
const filteredTopics = useMemo(() => {
|
||||||
|
if (!isManageMode || !deferredSearchText.trim()) {
|
||||||
|
return sortedTopics
|
||||||
|
}
|
||||||
|
// Split by spaces and filter out empty strings
|
||||||
|
const keywords = deferredSearchText
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((k) => k.length > 0)
|
||||||
|
if (keywords.length === 0) {
|
||||||
|
return sortedTopics
|
||||||
|
}
|
||||||
|
// All keywords must match (AND logic)
|
||||||
|
return sortedTopics.filter((topic) => {
|
||||||
|
const lowerName = topic.name.toLowerCase()
|
||||||
|
return keywords.every((keyword) => lowerName.includes(keyword))
|
||||||
|
})
|
||||||
|
}, [sortedTopics, deferredSearchText, isManageMode])
|
||||||
|
|
||||||
const singlealone = topicPosition === 'right' && position === 'right'
|
const singlealone = topicPosition === 'right' && position === 'right'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DraggableVirtualList
|
<>
|
||||||
className="topics-tab"
|
<DraggableVirtualList
|
||||||
list={sortedTopics}
|
className="topics-tab"
|
||||||
onUpdate={updateTopics}
|
list={filteredTopics}
|
||||||
style={{ height: '100%', padding: '9px 0 10px 10px' }}
|
onUpdate={updateTopics}
|
||||||
itemContainerStyle={{ paddingBottom: '8px' }}
|
style={{ height: '100%', padding: '8px 0 10px 10px', paddingBottom: isManageMode ? 70 : 10 }}
|
||||||
header={
|
itemContainerStyle={{ paddingBottom: '8px' }}
|
||||||
<>
|
header={
|
||||||
<AddButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
<HeaderRow>
|
||||||
{t('chat.add.topic.title')}
|
<AddButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||||
</AddButton>
|
{t('chat.add.topic.title')}
|
||||||
<div className="my-1"></div>
|
</AddButton>
|
||||||
</>
|
<Tooltip title={t('chat.topics.manage.title')} mouseEnterDelay={0.5}>
|
||||||
}>
|
<HeaderIconButton
|
||||||
{(topic) => {
|
onClick={isManageMode ? exitManageMode : enterManageMode}
|
||||||
const isActive = topic.id === activeTopic?.id
|
className={isManageMode ? 'active' : ''}>
|
||||||
const topicName = topic.name.replace('`', '')
|
<ListChecks size={14} />
|
||||||
const topicPrompt = topic.prompt
|
</HeaderIconButton>
|
||||||
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
</Tooltip>
|
||||||
|
</HeaderRow>
|
||||||
const getTopicNameClassName = () => {
|
|
||||||
if (isRenaming(topic.id)) return 'shimmer'
|
|
||||||
if (isNewlyRenamed(topic.id)) return 'typing'
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
disabled={isManageMode}>
|
||||||
|
{(topic) => {
|
||||||
|
const isActive = topic.id === activeTopic?.id
|
||||||
|
const topicName = topic.name.replace('`', '')
|
||||||
|
const topicPrompt = topic.prompt
|
||||||
|
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
||||||
|
const isSelected = selectedIds.has(topic.id)
|
||||||
|
const canSelect = !topic.pinned
|
||||||
|
|
||||||
return (
|
const getTopicNameClassName = () => {
|
||||||
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
|
if (isRenaming(topic.id)) return 'shimmer'
|
||||||
<TopicListItem
|
if (isNewlyRenamed(topic.id)) return 'typing'
|
||||||
onContextMenu={() => setTargetTopic(topic)}
|
return ''
|
||||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
}
|
||||||
onClick={editingTopicId === topic.id && isEditing ? undefined : () => onSwitchTopic(topic)}
|
|
||||||
style={{
|
const handleItemClick = () => {
|
||||||
borderRadius,
|
if (isManageMode) {
|
||||||
cursor: editingTopicId === topic.id && isEditing ? 'default' : 'pointer'
|
if (canSelect) {
|
||||||
}}>
|
toggleSelectTopic(topic.id)
|
||||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
}
|
||||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
} else {
|
||||||
<TopicNameContainer>
|
onSwitchTopic(topic)
|
||||||
{editingTopicId === topic.id && isEditing ? (
|
}
|
||||||
<TopicEditInput {...inputProps} onClick={(e) => e.stopPropagation()} />
|
}
|
||||||
) : (
|
|
||||||
<TopicName
|
return (
|
||||||
className={getTopicNameClassName()}
|
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']} disabled={isManageMode}>
|
||||||
title={topicName}
|
<TopicListItem
|
||||||
onDoubleClick={() => {
|
onContextMenu={() => setTargetTopic(topic)}
|
||||||
setEditingTopicId(topic.id)
|
className={classNames(
|
||||||
startEdit(topic.name)
|
isActive && !isManageMode ? 'active' : '',
|
||||||
}}>
|
singlealone ? 'singlealone' : '',
|
||||||
{topicName}
|
isManageMode && isSelected ? 'selected' : '',
|
||||||
</TopicName>
|
isManageMode && !canSelect ? 'disabled' : ''
|
||||||
)}
|
)}
|
||||||
{!topic.pinned && (
|
onClick={editingTopicId === topic.id && isEditing ? undefined : handleItemClick}
|
||||||
<Tooltip
|
style={{
|
||||||
placement="bottom"
|
borderRadius,
|
||||||
mouseEnterDelay={0.7}
|
cursor:
|
||||||
mouseLeaveDelay={0}
|
editingTopicId === topic.id && isEditing
|
||||||
title={
|
? 'default'
|
||||||
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
: isManageMode && !canSelect
|
||||||
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
? 'not-allowed'
|
||||||
</div>
|
: 'pointer'
|
||||||
}>
|
}}>
|
||||||
<MenuButton
|
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||||
className="menu"
|
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||||
onClick={(e) => {
|
<TopicNameContainer>
|
||||||
if (e.ctrlKey || e.metaKey) {
|
{isManageMode && (
|
||||||
handleConfirmDelete(topic, e)
|
<SelectIcon className={!canSelect ? 'disabled' : ''}>
|
||||||
} else if (deletingTopicId === topic.id) {
|
{isSelected ? (
|
||||||
handleConfirmDelete(topic, e)
|
<CheckSquare size={16} color="var(--color-primary)" />
|
||||||
} else {
|
|
||||||
handleDeleteClick(topic.id, e)
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{deletingTopicId === topic.id ? (
|
|
||||||
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
|
||||||
) : (
|
) : (
|
||||||
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
<Square size={16} color="var(--color-text-3)" />
|
||||||
)}
|
)}
|
||||||
|
</SelectIcon>
|
||||||
|
)}
|
||||||
|
{editingTopicId === topic.id && isEditing ? (
|
||||||
|
<TopicEditInput {...inputProps} onClick={(e) => e.stopPropagation()} />
|
||||||
|
) : (
|
||||||
|
<TopicName
|
||||||
|
className={getTopicNameClassName()}
|
||||||
|
title={topicName}
|
||||||
|
onDoubleClick={
|
||||||
|
isManageMode
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
setEditingTopicId(topic.id)
|
||||||
|
startEdit(topic.name)
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
{topicName}
|
||||||
|
</TopicName>
|
||||||
|
)}
|
||||||
|
{!topic.pinned && (
|
||||||
|
<Tooltip
|
||||||
|
placement="bottom"
|
||||||
|
mouseEnterDelay={0.7}
|
||||||
|
mouseLeaveDelay={0}
|
||||||
|
title={
|
||||||
|
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
||||||
|
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<MenuButton
|
||||||
|
className="menu"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
handleConfirmDelete(topic, e)
|
||||||
|
} else if (deletingTopicId === topic.id) {
|
||||||
|
handleConfirmDelete(topic, e)
|
||||||
|
} else {
|
||||||
|
handleDeleteClick(topic.id, e)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{deletingTopicId === topic.id ? (
|
||||||
|
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
||||||
|
) : (
|
||||||
|
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
||||||
|
)}
|
||||||
|
</MenuButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{topic.pinned && (
|
||||||
|
<MenuButton className="pin">
|
||||||
|
<PinIcon size={14} color="var(--color-text-3)" />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</Tooltip>
|
)}
|
||||||
|
</TopicNameContainer>
|
||||||
|
{topicPrompt && (
|
||||||
|
<TopicPromptText className="prompt" title={fullTopicPrompt}>
|
||||||
|
{fullTopicPrompt}
|
||||||
|
</TopicPromptText>
|
||||||
)}
|
)}
|
||||||
{topic.pinned && (
|
{showTopicTime && (
|
||||||
<MenuButton className="pin">
|
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
|
||||||
<PinIcon size={14} color="var(--color-text-3)" />
|
|
||||||
</MenuButton>
|
|
||||||
)}
|
)}
|
||||||
</TopicNameContainer>
|
</TopicListItem>
|
||||||
{topicPrompt && (
|
</Dropdown>
|
||||||
<TopicPromptText className="prompt" title={fullTopicPrompt}>
|
)
|
||||||
{fullTopicPrompt}
|
}}
|
||||||
</TopicPromptText>
|
</DraggableVirtualList>
|
||||||
)}
|
|
||||||
{showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
|
{/* 管理模式底部面板 */}
|
||||||
</TopicListItem>
|
<TopicManagePanel
|
||||||
</Dropdown>
|
assistant={assistant}
|
||||||
)
|
assistants={assistants}
|
||||||
}}
|
activeTopic={activeTopic}
|
||||||
</DraggableVirtualList>
|
setActiveTopic={setActiveTopic}
|
||||||
|
removeTopic={removeTopic}
|
||||||
|
moveTopic={moveTopic}
|
||||||
|
manageState={manageState}
|
||||||
|
filteredTopics={filteredTopics}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,6 +730,15 @@ const TopicListItem = styled.div`
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--color-primary-bg);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopicNameContainer = styled.div`
|
const TopicNameContainer = styled.div`
|
||||||
@ -648,7 +747,6 @@ const TopicNameContainer = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
justify-content: space-between;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopicName = styled.div`
|
const TopicName = styled.div`
|
||||||
@ -659,6 +757,8 @@ const TopicName = styled.div`
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
position: relative;
|
position: relative;
|
||||||
will-change: background-position, width;
|
will-change: background-position, width;
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
--color-shimmer-mid: var(--color-text-1);
|
--color-shimmer-mid: var(--color-text-1);
|
||||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||||
@ -765,3 +865,49 @@ const MenuButton = styled.div`
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const HeaderRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const HeaderIconButton = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
border-radius: var(--list-item-border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SelectIcon = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -145,6 +145,7 @@ const Container = styled.div`
|
|||||||
width: var(--assistants-width);
|
width: var(--assistants-width);
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
} from '@renderer/store/note'
|
} from '@renderer/store/note'
|
||||||
import type { NotesSortType, NotesTreeNode } from '@renderer/types/note'
|
import type { NotesSortType, NotesTreeNode } from '@renderer/types/note'
|
||||||
import type { FileChangeEvent } from '@shared/config/types'
|
import type { FileChangeEvent } from '@shared/config/types'
|
||||||
|
import { message } from 'antd'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { AnimatePresence, motion } from 'motion/react'
|
import { AnimatePresence, motion } from 'motion/react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@ -246,6 +247,43 @@ const NotesPage: FC = () => {
|
|||||||
updateNotesPath(defaultPath)
|
updateNotesPath(defaultPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证路径是否有效(处理跨平台恢复场景)
|
||||||
|
try {
|
||||||
|
// 获取当前平台的默认路径
|
||||||
|
const info = await window.api.getAppInfo()
|
||||||
|
const defaultPath = info.notesPath
|
||||||
|
|
||||||
|
// 如果当前路径就是默认路径,跳过验证(默认路径始终有效)
|
||||||
|
if (notesPath === defaultPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await window.api.file.validateNotesDirectory(notesPath)
|
||||||
|
if (!isValid) {
|
||||||
|
logger.warn('Invalid notes path detected, resetting to default', { path: notesPath })
|
||||||
|
|
||||||
|
// 重置为默认路径
|
||||||
|
updateNotesPath(defaultPath)
|
||||||
|
|
||||||
|
// 检查默认路径下是否有笔记文件
|
||||||
|
try {
|
||||||
|
const tree = await window.api.file.getDirectoryStructure(defaultPath)
|
||||||
|
if (!tree || tree.length === 0) {
|
||||||
|
// 默认目录为空,提示用户需要迁移文件
|
||||||
|
message.warning({
|
||||||
|
content: t('notes.crossPlatformRestoreWarning', { path: defaultPath }),
|
||||||
|
duration: 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 目录不存在或读取失败,会由 FileStorage 自动创建
|
||||||
|
logger.debug('Default notes directory will be created', { error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to validate notes path:', error as Error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { CloseCircleFilled, QuestionCircleOutlined } from '@ant-design/icons'
|
|||||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||||
import { ResetIcon } from '@renderer/components/Icons'
|
import { ResetIcon } from '@renderer/components/Icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import Selector from '@renderer/components/Selector'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import type { AssistantSettings as AssistantSettingsType } from '@renderer/types'
|
import type { AssistantSettings as AssistantSettingsType } from '@renderer/types'
|
||||||
import { getLeadingEmoji, modalConfirm } from '@renderer/utils'
|
import { getLeadingEmoji, modalConfirm } from '@renderer/utils'
|
||||||
import { Button, Col, Flex, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd'
|
import { Button, Col, Divider, Flex, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
@ -26,6 +27,9 @@ const AssistantSettings: FC = () => {
|
|||||||
const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0)
|
const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0)
|
||||||
const [topP, setTopP] = useState(defaultAssistant.settings?.topP ?? 1)
|
const [topP, setTopP] = useState(defaultAssistant.settings?.topP ?? 1)
|
||||||
const [enableTopP, setEnableTopP] = useState(defaultAssistant.settings?.enableTopP ?? false)
|
const [enableTopP, setEnableTopP] = useState(defaultAssistant.settings?.enableTopP ?? false)
|
||||||
|
const [toolUseMode, setToolUseMode] = useState<AssistantSettingsType['toolUseMode']>(
|
||||||
|
defaultAssistant.settings?.toolUseMode ?? 'function'
|
||||||
|
)
|
||||||
const [emoji, setEmoji] = useState(defaultAssistant.emoji || getLeadingEmoji(defaultAssistant.name) || '')
|
const [emoji, setEmoji] = useState(defaultAssistant.emoji || getLeadingEmoji(defaultAssistant.name) || '')
|
||||||
const [name, setName] = useState(
|
const [name, setName] = useState(
|
||||||
defaultAssistant.name.replace(getLeadingEmoji(defaultAssistant.name) || '', '').trim()
|
defaultAssistant.name.replace(getLeadingEmoji(defaultAssistant.name) || '', '').trim()
|
||||||
@ -46,7 +50,8 @@ const AssistantSettings: FC = () => {
|
|||||||
maxTokens: settings.maxTokens ?? maxTokens,
|
maxTokens: settings.maxTokens ?? maxTokens,
|
||||||
streamOutput: settings.streamOutput ?? true,
|
streamOutput: settings.streamOutput ?? true,
|
||||||
topP: settings.topP ?? topP,
|
topP: settings.topP ?? topP,
|
||||||
enableTopP: settings.enableTopP ?? enableTopP
|
enableTopP: settings.enableTopP ?? enableTopP,
|
||||||
|
toolUseMode: settings.toolUseMode ?? toolUseMode
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -73,6 +78,7 @@ const AssistantSettings: FC = () => {
|
|||||||
setMaxTokens(0)
|
setMaxTokens(0)
|
||||||
setTopP(1)
|
setTopP(1)
|
||||||
setEnableTopP(false)
|
setEnableTopP(false)
|
||||||
|
setToolUseMode('function')
|
||||||
updateDefaultAssistant({
|
updateDefaultAssistant({
|
||||||
...defaultAssistant,
|
...defaultAssistant,
|
||||||
settings: {
|
settings: {
|
||||||
@ -84,7 +90,8 @@ const AssistantSettings: FC = () => {
|
|||||||
maxTokens: DEFAULT_MAX_TOKENS,
|
maxTokens: DEFAULT_MAX_TOKENS,
|
||||||
streamOutput: true,
|
streamOutput: true,
|
||||||
topP: 1,
|
topP: 1,
|
||||||
enableTopP: false
|
enableTopP: false,
|
||||||
|
toolUseMode: 'function'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -107,10 +114,9 @@ const AssistantSettings: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer
|
<SettingContainer
|
||||||
style={{ height: 'auto', background: 'transparent', padding: `0 0 12px 0`, gap: 12 }}
|
style={{ height: 'auto', background: 'transparent', padding: `0 0 12px 0`, gap: 10 }}
|
||||||
theme={theme}>
|
theme={theme}>
|
||||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle>
|
<HStack gap={8} alignItems="center" mt={10}>
|
||||||
<HStack gap={8} alignItems="center">
|
|
||||||
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow trigger="click">
|
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow trigger="click">
|
||||||
<EmojiButtonWrapper>
|
<EmojiButtonWrapper>
|
||||||
<Button style={{ fontSize: 20, padding: '4px', minWidth: '30px', height: '30px' }}>{emoji}</Button>
|
<Button style={{ fontSize: 20, padding: '4px', minWidth: '30px', height: '30px' }}>{emoji}</Button>
|
||||||
@ -161,6 +167,7 @@ const AssistantSettings: FC = () => {
|
|||||||
<Button type="text" onClick={onReset} icon={<ResetIcon size={16} />} />
|
<Button type="text" onClick={onReset} icon={<ResetIcon size={16} />} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</SettingSubtitle>
|
</SettingSubtitle>
|
||||||
|
<Divider style={{ margin: '2px 0' }} />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center">
|
||||||
<Label>{t('chat.settings.temperature.label')}</Label>
|
<Label>{t('chat.settings.temperature.label')}</Label>
|
||||||
@ -178,7 +185,7 @@ const AssistantSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{enableTemperature && (
|
{enableTemperature && (
|
||||||
<Row align="middle" gutter={12}>
|
<Row align="middle" gutter={12} style={{ marginTop: -5, marginBottom: -10 }}>
|
||||||
<Col span={20}>
|
<Col span={20}>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
@ -202,6 +209,7 @@ const AssistantSettings: FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
<Divider style={{ margin: '2px 0' }} />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center">
|
||||||
<Label>{t('chat.settings.top_p.label')}</Label>
|
<Label>{t('chat.settings.top_p.label')}</Label>
|
||||||
@ -219,7 +227,7 @@ const AssistantSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{enableTopP && (
|
{enableTopP && (
|
||||||
<Row align="middle" gutter={12}>
|
<Row align="middle" gutter={12} style={{ marginTop: -5, marginBottom: -10 }}>
|
||||||
<Col span={20}>
|
<Col span={20}>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
@ -236,13 +244,14 @@ const AssistantSettings: FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
<Divider style={{ margin: '2px 0' }} />
|
||||||
<Row align="middle">
|
<Row align="middle">
|
||||||
<Label>{t('chat.settings.context_count.label')}</Label>
|
<Label>{t('chat.settings.context_count.label')}</Label>
|
||||||
<Tooltip title={t('chat.settings.context_count.tip')}>
|
<Tooltip title={t('chat.settings.context_count.tip')}>
|
||||||
<QuestionIcon />
|
<QuestionIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Row>
|
</Row>
|
||||||
<Row align="middle" gutter={20}>
|
<Row align="middle" gutter={20} style={{ marginTop: -5, marginBottom: -10 }}>
|
||||||
<Col span={19}>
|
<Col span={19}>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
@ -265,6 +274,7 @@ const AssistantSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Divider style={{ margin: '2px 0' }} />
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center">
|
||||||
<Label>{t('chat.settings.max_tokens.label')}</Label>
|
<Label>{t('chat.settings.max_tokens.label')}</Label>
|
||||||
@ -308,6 +318,22 @@ const AssistantSettings: FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
<Divider style={{ margin: '2px 0' }} />
|
||||||
|
<SettingRow>
|
||||||
|
<Label>{t('assistants.settings.tool_use_mode.label')}</Label>
|
||||||
|
<Selector
|
||||||
|
value={toolUseMode}
|
||||||
|
options={[
|
||||||
|
{ label: t('assistants.settings.tool_use_mode.prompt'), value: 'prompt' },
|
||||||
|
{ label: t('assistants.settings.tool_use_mode.function'), value: 'function' }
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setToolUseMode(value)
|
||||||
|
onUpdateAssistantSettings({ toolUseMode: value })
|
||||||
|
}}
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import ListItem from '@renderer/components/ListItem'
|
import ListItem from '@renderer/components/ListItem'
|
||||||
import GeneralPopup from '@renderer/components/Popups/GeneralPopup'
|
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||||
@ -11,7 +10,7 @@ import type { AssistantPreset } from '@renderer/types'
|
|||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Button, Empty, Flex, Input } from 'antd'
|
import { Button, Empty, Flex, Input } from 'antd'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { Import, Plus, Rss, Search, Settings2 } from 'lucide-react'
|
import { Import, Plus, Search, Settings2 } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -23,7 +22,6 @@ import { groupTranslations } from './assistantPresetGroupTranslations'
|
|||||||
import AddAssistantPresetPopup from './components/AddAssistantPresetPopup'
|
import AddAssistantPresetPopup from './components/AddAssistantPresetPopup'
|
||||||
import AssistantPresetCard from './components/AssistantPresetCard'
|
import AssistantPresetCard from './components/AssistantPresetCard'
|
||||||
import { AssistantPresetGroupIcon } from './components/AssistantPresetGroupIcon'
|
import { AssistantPresetGroupIcon } from './components/AssistantPresetGroupIcon'
|
||||||
import AssistantsSubscribeUrlSettings from './components/AssistantsSubscribeUrlSettings'
|
|
||||||
import ImportAssistantPresetPopup from './components/ImportAssistantPresetPopup'
|
import ImportAssistantPresetPopup from './components/ImportAssistantPresetPopup'
|
||||||
import ManageAssistantPresetsPopup from './components/ManageAssistantPresetsPopup'
|
import ManageAssistantPresetsPopup from './components/ManageAssistantPresetsPopup'
|
||||||
|
|
||||||
@ -177,15 +175,6 @@ const AssistantPresetsPage: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubscribeSettings = () => {
|
|
||||||
GeneralPopup.show({
|
|
||||||
title: t('assistants.presets.settings.title'),
|
|
||||||
content: <AssistantsSubscribeUrlSettings />,
|
|
||||||
footer: null,
|
|
||||||
width: 600
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleManageAgents = () => {
|
const handleManageAgents = () => {
|
||||||
ManageAssistantPresetsPopup.show()
|
ManageAssistantPresetsPopup.show()
|
||||||
}
|
}
|
||||||
@ -292,9 +281,6 @@ const AssistantPresetsPage: FC = () => {
|
|||||||
<Button type="text" onClick={handleImportAgent} icon={<Import size={18} color="var(--color-icon)" />}>
|
<Button type="text" onClick={handleImportAgent} icon={<Import size={18} color="var(--color-icon)" />}>
|
||||||
{t('assistants.presets.import.title')}
|
{t('assistants.presets.import.title')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="text" onClick={handleSubscribeSettings} icon={<Rss size={18} color="var(--color-icon)" />}>
|
|
||||||
{t('assistants.presets.settings.title')}
|
|
||||||
</Button>
|
|
||||||
<Button type="text" onClick={handleManageAgents} icon={<Settings2 size={18} color="var(--color-icon)" />}>
|
<Button type="text" onClick={handleManageAgents} icon={<Settings2 size={18} color="var(--color-icon)" />}>
|
||||||
{t('assistants.presets.manage.title')}
|
{t('assistants.presets.manage.title')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
|
||||||
import { setAgentssubscribeUrl } from '@renderer/store/settings'
|
|
||||||
import Input from 'antd/es/input/Input'
|
|
||||||
import { HelpCircle } from 'lucide-react'
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const AssistantsSubscribeUrlSettings: FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
const { agentssubscribeUrl } = useSettings()
|
|
||||||
|
|
||||||
const handleAgentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
dispatch(setAgentssubscribeUrl(e.target.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHelpClick = () => {
|
|
||||||
window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingGroup theme={theme}>
|
|
||||||
<HStack alignItems="center" gap="8px">
|
|
||||||
<SettingTitle>
|
|
||||||
{t('assistants.presets.tag.agent')}
|
|
||||||
{t('settings.tool.websearch.subscribe_add')}
|
|
||||||
</SettingTitle>
|
|
||||||
<HelpCircle
|
|
||||||
size={16}
|
|
||||||
color="var(--color-icon)"
|
|
||||||
onClick={handleHelpClick}
|
|
||||||
className="hover:!text-[var(--color-primary)] cursor-pointer transition-colors"
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.tool.websearch.subscribe_url')}</SettingRowTitle>
|
|
||||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={agentssubscribeUrl || ''}
|
|
||||||
onChange={handleAgentChange}
|
|
||||||
style={{ width: 315 }}
|
|
||||||
placeholder={t('settings.tool.websearch.subscribe_url')}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</SettingRow>
|
|
||||||
</SettingGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AssistantsSubscribeUrlSettings
|
|
||||||
@ -1,11 +1,15 @@
|
|||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useTimer } from '@renderer/hooks/useTimer'
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setAgentssubscribeUrl } from '@renderer/store/settings'
|
||||||
import type { AssistantPreset } from '@renderer/types'
|
import type { AssistantPreset } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Button, Flex, Form, Input, Modal, Radio } from 'antd'
|
import { Button, Divider, Flex, Form, Input, Modal, Radio, Typography } from 'antd'
|
||||||
|
import { HelpCircle } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -20,35 +24,53 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const { addAssistantPreset } = useAssistantPresets()
|
const { addAssistantPreset } = useAssistantPresets()
|
||||||
const [importType, setImportType] = useState<'url' | 'file'>('url')
|
const [importType, setImportType] = useState<'url' | 'file'>('url')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [subscribeLoading, setSubscribeLoading] = useState(false)
|
||||||
const { setTimeoutTimer } = useTimer()
|
const { setTimeoutTimer } = useTimer()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { agentssubscribeUrl } = useSettings()
|
||||||
|
const [subscribeUrl, setSubscribeUrl] = useState(agentssubscribeUrl || '')
|
||||||
|
const [selectedFile, setSelectedFile] = useState<{ name: string; content: Uint8Array } | null>(null)
|
||||||
|
const [urlValue, setUrlValue] = useState('')
|
||||||
|
|
||||||
|
const isImportDisabled = importType === 'url' ? !urlValue.trim() : !selectedFile
|
||||||
|
const isSubscribed = !!agentssubscribeUrl
|
||||||
|
|
||||||
|
const handleSelectFile = async () => {
|
||||||
|
const result = await window.api.file.open({
|
||||||
|
filters: [{ name: t('assistants.presets.import.file_filter'), extensions: ['json'] }]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
setSelectedFile({ name: result.fileName, content: result.content })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFinish = async () => {
|
||||||
|
// Validate before setting loading
|
||||||
|
if (importType === 'url' && !urlValue.trim()) {
|
||||||
|
window.toast.error(t('assistants.presets.import.error.url_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (importType === 'file' && !selectedFile) {
|
||||||
|
window.toast.error(t('assistants.presets.import.error.file_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const onFinish = async (values: { url?: string }) => {
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
let presets: AssistantPreset[] = []
|
let presets: AssistantPreset[] = []
|
||||||
|
|
||||||
if (importType === 'url') {
|
if (importType === 'url') {
|
||||||
if (!values.url) {
|
const response = await fetch(urlValue.trim())
|
||||||
throw new Error(t('assistants.presets.import.error.url_required'))
|
|
||||||
}
|
|
||||||
const response = await fetch(values.url)
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(t('assistants.presets.import.error.fetch_failed'))
|
throw new Error(t('assistants.presets.import.error.fetch_failed'))
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
presets = Array.isArray(data) ? data : [data]
|
presets = Array.isArray(data) ? data : [data]
|
||||||
} else {
|
} else {
|
||||||
const result = await window.api.file.open({
|
presets = JSON.parse(new TextDecoder('utf-8').decode(selectedFile!.content))
|
||||||
filters: [{ name: t('assistants.presets.import.file_filter'), extensions: ['json'] }]
|
if (!Array.isArray(presets)) {
|
||||||
})
|
presets = [presets]
|
||||||
|
|
||||||
if (result) {
|
|
||||||
presets = JSON.parse(new TextDecoder('utf-8').decode(result.content))
|
|
||||||
if (!Array.isArray(presets)) {
|
|
||||||
presets = [presets]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +96,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
addAssistantPreset(newPreset)
|
addAssistantPreset(newPreset)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.toast.success(t('message.agents.imported'))
|
window.toast.success(t('message.agents.imported', { count: presets.length }))
|
||||||
|
|
||||||
setTimeoutTimer('onFinish', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
setTimeoutTimer('onFinish', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@ -88,7 +110,42 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
resolve(null)
|
}
|
||||||
|
|
||||||
|
const handleSubscribeUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSubscribeUrl(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubscribe = async () => {
|
||||||
|
// If already subscribed, unsubscribe
|
||||||
|
if (isSubscribed) {
|
||||||
|
dispatch(setAgentssubscribeUrl(''))
|
||||||
|
setSubscribeUrl('')
|
||||||
|
window.location.reload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscribeUrl.trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubscribeLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(subscribeUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(t('assistants.presets.import.error.fetch_failed'))
|
||||||
|
}
|
||||||
|
dispatch(setAgentssubscribeUrl(subscribeUrl))
|
||||||
|
window.location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
window.toast.error(error instanceof Error ? error.message : t('message.agents.import.error'))
|
||||||
|
} finally {
|
||||||
|
setSubscribeLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHelpClick = () => {
|
||||||
|
window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -96,39 +153,79 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
title={t('assistants.presets.import.title')}
|
title={t('assistants.presets.import.title')}
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
maskClosable={false}
|
afterClose={() => resolve(null)}
|
||||||
footer={
|
footer={null}
|
||||||
<Flex justify="end" gap={8}>
|
|
||||||
<Button onClick={onCancel}>{t('common.cancel')}</Button>
|
|
||||||
<Button type="primary" onClick={() => form.submit()} loading={loading}>
|
|
||||||
{t('assistants.presets.import.button')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
transitionName="animation-move-down"
|
transitionName="animation-move-down"
|
||||||
|
styles={{ body: { padding: '16px' } }}
|
||||||
centered>
|
centered>
|
||||||
<Form form={form} onFinish={onFinish} layout="vertical">
|
<Form form={form} onFinish={onFinish} layout="vertical">
|
||||||
<Form.Item>
|
<Form.Item style={{ marginBottom: 0 }}>
|
||||||
<Radio.Group value={importType} onChange={(e) => setImportType(e.target.value)}>
|
<Flex align="center" gap={12} style={{ width: '100%' }}>
|
||||||
<Radio.Button value="url">{t('assistants.presets.import.type.url')}</Radio.Button>
|
<Radio.Group value={importType} onChange={(e) => setImportType(e.target.value)}>
|
||||||
<Radio.Button value="file">{t('assistants.presets.import.type.file')}</Radio.Button>
|
<Radio.Button value="url">{t('assistants.presets.import.type.url')}</Radio.Button>
|
||||||
</Radio.Group>
|
<Radio.Button value="file">{t('assistants.presets.import.type.file')}</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
{importType === 'url' && (
|
||||||
|
<Form.Item
|
||||||
|
name="url"
|
||||||
|
rules={[{ required: true, message: t('assistants.presets.import.error.url_required') }]}
|
||||||
|
style={{ flex: 1, marginBottom: 0 }}>
|
||||||
|
<Input
|
||||||
|
placeholder={t('assistants.presets.import.url_placeholder')}
|
||||||
|
value={urlValue}
|
||||||
|
onChange={(e) => setUrlValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{importType === 'file' && (
|
||||||
|
<>
|
||||||
|
<Button onClick={handleSelectFile}>{t('assistants.presets.import.select_file')}</Button>
|
||||||
|
{selectedFile && (
|
||||||
|
<Typography.Text type="secondary" ellipsis style={{ maxWidth: 200 }}>
|
||||||
|
{selectedFile.name}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="primary" onClick={onFinish} loading={loading} disabled={isImportDisabled}>
|
||||||
|
{t('assistants.presets.import.button')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{importType === 'url' && (
|
|
||||||
<Form.Item
|
|
||||||
name="url"
|
|
||||||
rules={[{ required: true, message: t('assistants.presets.import.error.url_required') }]}>
|
|
||||||
<Input placeholder={t('assistants.presets.import.url_placeholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{importType === 'file' && (
|
|
||||||
<Form.Item>
|
|
||||||
<Button onClick={() => form.submit()}>{t('assistants.presets.import.select_file')}</Button>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
|
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
<Typography.Text strong style={{ flexShrink: 0, fontSize: 16 }}>
|
||||||
|
{t('assistants.presets.tag.agent')}
|
||||||
|
{t('settings.tool.websearch.subscribe_add')}
|
||||||
|
</Typography.Text>
|
||||||
|
<HelpCircle
|
||||||
|
size={16}
|
||||||
|
color="var(--color-icon)"
|
||||||
|
onClick={handleHelpClick}
|
||||||
|
className="hover:!text-[var(--color-primary)] cursor-pointer transition-colors"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex align="center" gap={12} style={{ marginTop: 10 }}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={subscribeUrl}
|
||||||
|
onChange={handleSubscribeUrlChange}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
placeholder={t('settings.tool.websearch.subscribe_url')}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={handleSubscribe} loading={subscribeLoading} disabled={!subscribeUrl.trim()}>
|
||||||
|
{isSubscribed ? t('common.unsubscribe') : t('common.subscribe')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MenuOutlined } from '@ant-design/icons'
|
import { ExportOutlined, MenuOutlined } from '@ant-design/icons'
|
||||||
import { DraggableList } from '@renderer/components/DraggableList'
|
import { DraggableList } from '@renderer/components/DraggableList'
|
||||||
import { DeleteIcon } from '@renderer/components/Icons'
|
import { DeleteIcon } from '@renderer/components/Icons'
|
||||||
import { Box, HStack } from '@renderer/components/Layout'
|
import { Box, HStack } from '@renderer/components/Layout'
|
||||||
@ -10,13 +10,13 @@ import { useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
type Mode = 'sort' | 'delete'
|
type Mode = 'sort' | 'manage'
|
||||||
|
|
||||||
const PopupContainer: React.FC = () => {
|
const PopupContainer: React.FC = () => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { presets, setAssistantPresets } = useAssistantPresets()
|
const { presets, setAssistantPresets } = useAssistantPresets()
|
||||||
const [mode, setMode] = useState<Mode>(() => (presets.length > 50 ? 'delete' : 'sort'))
|
const [mode, setMode] = useState<Mode>('manage')
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
@ -88,6 +88,23 @@ const PopupContainer: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBatchExport = async () => {
|
||||||
|
if (selectedIds.size === 0) return
|
||||||
|
|
||||||
|
const selectedPresets = presets.filter((p) => selectedIds.has(p.id))
|
||||||
|
const exportData = selectedPresets.map((p) => ({
|
||||||
|
name: p.name,
|
||||||
|
emoji: p.emoji,
|
||||||
|
prompt: p.prompt,
|
||||||
|
description: p.description,
|
||||||
|
group: p.group
|
||||||
|
}))
|
||||||
|
|
||||||
|
const fileName = selectedIds.size === 1 ? `${selectedPresets[0].name}.json` : `assistants_${selectedIds.size}.json`
|
||||||
|
|
||||||
|
await window.api.file.save(fileName, JSON.stringify(exportData, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
const isAllSelected = presets.length > 0 && selectedIds.size === presets.length
|
const isAllSelected = presets.length > 0 && selectedIds.size === presets.length
|
||||||
const isIndeterminate = selectedIds.size > 0 && selectedIds.size < presets.length
|
const isIndeterminate = selectedIds.size > 0 && selectedIds.size < presets.length
|
||||||
|
|
||||||
@ -98,13 +115,14 @@ const PopupContainer: React.FC = () => {
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
|
width={600}
|
||||||
transitionName="animation-move-down"
|
transitionName="animation-move-down"
|
||||||
centered>
|
centered>
|
||||||
<Container>
|
<Container>
|
||||||
{presets.length > 0 && (
|
{presets.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<ActionBar>
|
<ActionBar>
|
||||||
{mode === 'delete' ? (
|
{mode === 'manage' ? (
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center">
|
||||||
<Checkbox checked={isAllSelected} indeterminate={isIndeterminate} onChange={handleSelectAll}>
|
<Checkbox checked={isAllSelected} indeterminate={isIndeterminate} onChange={handleSelectAll}>
|
||||||
{t('common.select_all')}
|
{t('common.select_all')}
|
||||||
@ -119,15 +137,24 @@ const PopupContainer: React.FC = () => {
|
|||||||
<div />
|
<div />
|
||||||
)}
|
)}
|
||||||
<HStack gap="8px" alignItems="center">
|
<HStack gap="8px" alignItems="center">
|
||||||
{mode === 'delete' && (
|
{mode === 'manage' && (
|
||||||
<Button
|
<>
|
||||||
danger
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteIcon size={14} />}
|
icon={<ExportOutlined />}
|
||||||
disabled={selectedIds.size === 0}
|
disabled={selectedIds.size === 0}
|
||||||
onClick={handleBatchDelete}>
|
onClick={handleBatchExport}>
|
||||||
{t('assistants.presets.manage.batch_delete.button')} ({selectedIds.size})
|
{t('assistants.presets.manage.batch_export.button')} ({selectedIds.size})
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="text"
|
||||||
|
icon={<DeleteIcon size={14} />}
|
||||||
|
disabled={selectedIds.size === 0}
|
||||||
|
onClick={handleBatchDelete}>
|
||||||
|
{t('assistants.presets.manage.batch_delete.button')} ({selectedIds.size})
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Segmented
|
<Segmented
|
||||||
size="small"
|
size="small"
|
||||||
@ -135,7 +162,7 @@ const PopupContainer: React.FC = () => {
|
|||||||
onChange={(value) => handleModeChange(value as Mode)}
|
onChange={(value) => handleModeChange(value as Mode)}
|
||||||
options={[
|
options={[
|
||||||
{ label: t('assistants.presets.manage.mode.sort'), value: 'sort' },
|
{ label: t('assistants.presets.manage.mode.sort'), value: 'sort' },
|
||||||
{ label: t('assistants.presets.manage.mode.delete'), value: 'delete' }
|
{ label: t('assistants.presets.manage.mode.manage'), value: 'manage' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 184,
|
version: 186,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2913,31 +2913,6 @@ const migrateConfig = {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'180': (state: RootState) => {
|
|
||||||
try {
|
|
||||||
if (state.settings.apiServer) {
|
|
||||||
state.settings.apiServer.host = API_SERVER_DEFAULTS.HOST
|
|
||||||
}
|
|
||||||
// @ts-expect-error
|
|
||||||
if (state.settings.openAI.summaryText === 'undefined') {
|
|
||||||
state.settings.openAI.summaryText = undefined
|
|
||||||
}
|
|
||||||
// @ts-expect-error
|
|
||||||
if (state.settings.openAI.verbosity === 'undefined') {
|
|
||||||
state.settings.openAI.verbosity = undefined
|
|
||||||
}
|
|
||||||
state.llm.providers.forEach((provider) => {
|
|
||||||
if (provider.id === SystemProviderIds.ollama) {
|
|
||||||
provider.type = 'ollama'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
logger.info('migrate 180 success')
|
|
||||||
return state
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('migrate 180 error', error as Error)
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'181': (state: RootState) => {
|
'181': (state: RootState) => {
|
||||||
try {
|
try {
|
||||||
state.llm.providers.forEach((provider) => {
|
state.llm.providers.forEach((provider) => {
|
||||||
@ -3017,6 +2992,52 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 184 error', error as Error)
|
logger.error('migrate 184 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'185': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
// Reset toolUseMode to function for default assistant
|
||||||
|
if (state.assistants.defaultAssistant.settings?.toolUseMode) {
|
||||||
|
state.assistants.defaultAssistant.settings.toolUseMode = 'function'
|
||||||
|
}
|
||||||
|
// Reset toolUseMode to function for assistants
|
||||||
|
state.assistants.assistants.forEach((assistant) => {
|
||||||
|
if (assistant.settings?.toolUseMode === 'prompt') {
|
||||||
|
if (assistant.model && isFunctionCallingModel(assistant.model)) {
|
||||||
|
assistant.settings.toolUseMode = 'function'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
logger.info('migrate 185 success')
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 185 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'186': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
if (state.settings.apiServer) {
|
||||||
|
state.settings.apiServer.host = API_SERVER_DEFAULTS.HOST
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
if (state.settings.openAI.summaryText === 'undefined') {
|
||||||
|
state.settings.openAI.summaryText = undefined
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
if (state.settings.openAI.verbosity === 'undefined') {
|
||||||
|
state.settings.openAI.verbosity = undefined
|
||||||
|
}
|
||||||
|
state.llm.providers.forEach((provider) => {
|
||||||
|
if (provider.id === SystemProviderIds.ollama) {
|
||||||
|
provider.type = 'ollama'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
logger.info('migrate 186 success')
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 186 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user