mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
Merge branch 'main' of github.com:CherryHQ/cherry-studio into feat/mcp-runjs
This commit is contained in:
commit
d5ae3e6edc
@ -125,7 +125,21 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
Optimized note-taking feature, now able to quickly rename by modifying the title
|
||||
Fixed issue where CherryAI free model could not be used
|
||||
Fixed issue where VertexAI proxy address could not be called normally
|
||||
Fixed issue where built-in tools from service providers could not be called normally
|
||||
What's New in v1.6.3
|
||||
|
||||
Features:
|
||||
- Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming
|
||||
- UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow
|
||||
- Editor: Add read-only extension support, make TextFilePreview read-only but copyable
|
||||
- Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models
|
||||
- Code Tools: Add GitHub Copilot CLI integration
|
||||
|
||||
Bug Fixes:
|
||||
- Fix migration for missing providers
|
||||
- Fix forked topic retaining old name after rename
|
||||
- Restore first token latency reporting in metrics
|
||||
- Fix UI scrollbar and overflow issues
|
||||
|
||||
Technical Updates:
|
||||
- Upgrade to Electron 37.6.0
|
||||
- Update dependencies across packages
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.6.2",
|
||||
"version": "1.6.3",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
|
||||
@ -48,7 +48,8 @@ const RichEditor = ({
|
||||
enableContentSearch = false,
|
||||
isFullWidth = false,
|
||||
fontFamily = 'default',
|
||||
fontSize = 16
|
||||
fontSize = 16,
|
||||
enableSpellCheck = false
|
||||
// toolbarItems: _toolbarItems // TODO: Implement custom toolbar items
|
||||
}: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => {
|
||||
// Use the rich editor hook for complete editor management
|
||||
@ -71,6 +72,7 @@ const RichEditor = ({
|
||||
onBlur,
|
||||
placeholder,
|
||||
editable,
|
||||
enableSpellCheck,
|
||||
scrollParent: () => scrollContainerRef.current,
|
||||
onShowTableActionMenu: ({ position, actions }) => {
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
|
||||
@ -50,6 +50,8 @@ export interface RichEditorProps {
|
||||
fontFamily?: 'default' | 'serif'
|
||||
/** Font size in pixels */
|
||||
fontSize?: number
|
||||
/** Whether to enable spell check */
|
||||
enableSpellCheck?: boolean
|
||||
}
|
||||
|
||||
export interface ToolbarItem {
|
||||
|
||||
@ -57,6 +57,8 @@ export interface UseRichEditorOptions {
|
||||
editable?: boolean
|
||||
/** Whether to enable table of contents functionality */
|
||||
enableTableOfContents?: boolean
|
||||
/** Whether to enable spell check */
|
||||
enableSpellCheck?: boolean
|
||||
/** Show table action menu (row/column) with concrete actions and position */
|
||||
onShowTableActionMenu?: (payload: {
|
||||
type: 'row' | 'column'
|
||||
@ -126,6 +128,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
|
||||
previewLength = 50,
|
||||
placeholder = '',
|
||||
editable = true,
|
||||
enableSpellCheck = false,
|
||||
onShowTableActionMenu,
|
||||
scrollParent
|
||||
} = options
|
||||
@ -410,7 +413,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
|
||||
// Allow text selection even when not editable
|
||||
style: editable
|
||||
? ''
|
||||
: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;'
|
||||
: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;',
|
||||
// Set spellcheck attribute on the contenteditable element
|
||||
spellcheck: enableSpellCheck ? 'true' : 'false'
|
||||
}
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "Update time (oldest first)",
|
||||
"sort_updated_desc": "Update time (newest first)",
|
||||
"sort_z2a": "File name (Z-A)",
|
||||
"spell_check": "Spell Check",
|
||||
"spell_check_tooltip": "Enable/Disable spell check",
|
||||
"star": "Favorite note",
|
||||
"starred_notes": "Collected notes",
|
||||
"title": "Notes",
|
||||
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "更新时间(从旧到新)",
|
||||
"sort_updated_desc": "更新时间(从新到旧)",
|
||||
"sort_z2a": "文件名(Z-A)",
|
||||
"spell_check": "拼写检查",
|
||||
"spell_check_tooltip": "启用/禁用拼写检查",
|
||||
"star": "收藏笔记",
|
||||
"starred_notes": "收藏的笔记",
|
||||
"title": "笔记",
|
||||
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "更新時間(從舊到新)",
|
||||
"sort_updated_desc": "更新時間(從新到舊)",
|
||||
"sort_z2a": "文件名(Z-A)",
|
||||
"spell_check": "拼寫檢查",
|
||||
"spell_check_tooltip": "啟用/禁用拼寫檢查",
|
||||
"star": "收藏筆記",
|
||||
"starred_notes": "收藏的筆記",
|
||||
"title": "筆記",
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
"new_topic": "Νέο θέμα {{Command}}",
|
||||
"pause": "Παύση",
|
||||
"placeholder": "Εισάγετε μήνυμα εδώ...",
|
||||
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送",
|
||||
"placeholder_without_triggers": "Εδώ εισαγάγετε το μήνυμα, πατήστε {{key}} για αποστολή",
|
||||
"send": "Αποστολή",
|
||||
"settings": "Ρυθμίσεις",
|
||||
"thinking": {
|
||||
@ -1698,10 +1698,10 @@
|
||||
},
|
||||
"notes": {
|
||||
"auto_rename": {
|
||||
"empty_note": "[to be translated]:笔记为空,无法生成名称",
|
||||
"failed": "[to be translated]:生成笔记名称失败",
|
||||
"label": "[to be translated]:生成笔记名称",
|
||||
"success": "[to be translated]:笔记名称生成成功"
|
||||
"empty_note": "Το σημείωμα είναι κενό, δεν μπορεί να δημιουργηθεί όνομα",
|
||||
"failed": "Αποτυχία δημιουργίας ονόματος σημείωσης",
|
||||
"label": "Δημιουργία ονόματος σημείωσης",
|
||||
"success": "Η δημιουργία του ονόματος σημειώσεων ολοκληρώθηκε με επιτυχία"
|
||||
},
|
||||
"characters": "χαρακτήρας",
|
||||
"collapse": "σύμπτυξη",
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)",
|
||||
"sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)",
|
||||
"sort_z2a": "όνομα αρχείου (Z-A)",
|
||||
"spell_check": "Έλεγχος ορθογραφίας",
|
||||
"spell_check_tooltip": "Ενεργοποίηση/Απενεργοποίηση ελέγχου ορθογραφίας",
|
||||
"star": "Αγαπημένες σημειώσεις",
|
||||
"starred_notes": "Σημειώσεις συλλογής",
|
||||
"title": "σημειώσεις",
|
||||
@ -3346,7 +3348,6 @@
|
||||
"dify_knowledge": "Η υλοποίηση του Dify για τον διακομιστή MCP, παρέχει μια απλή API για να αλληλεπιδρά με το Dify. Απαιτείται η ρύθμιση του κλειδιού Dify",
|
||||
"fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL",
|
||||
"filesystem": "Εξυπηρετητής Node.js για το πρωτόκολλο περιβάλλοντος μοντέλου (MCP) που εφαρμόζει λειτουργίες συστήματος αρχείων. Απαιτείται διαμόρφωση για την επιτροπή πρόσβασης σε καταλόγους",
|
||||
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||
"mcp_auto_install": "Αυτόματη εγκατάσταση υπηρεσίας MCP (προβολή)",
|
||||
"memory": "Βασική υλοποίηση μόνιμης μνήμης με βάση τοπικό γράφημα γνώσης. Αυτό επιτρέπει στο μοντέλο να θυμάται πληροφορίες σχετικές με τον χρήστη ανάμεσα σε διαφορετικές συνομιλίες. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος MEMORY_FILE_PATH.",
|
||||
"no": "Χωρίς περιγραφή",
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
"new_topic": "Nuevo tema {{Command}}",
|
||||
"pause": "Pausar",
|
||||
"placeholder": "Escribe aquí tu mensaje...",
|
||||
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送",
|
||||
"placeholder_without_triggers": "Escriba un mensaje aquí y presione {{key}} para enviar",
|
||||
"send": "Enviar",
|
||||
"settings": "Configuración",
|
||||
"thinking": {
|
||||
@ -1698,10 +1698,10 @@
|
||||
},
|
||||
"notes": {
|
||||
"auto_rename": {
|
||||
"empty_note": "[to be translated]:笔记为空,无法生成名称",
|
||||
"failed": "[to be translated]:生成笔记名称失败",
|
||||
"label": "[to be translated]:生成笔记名称",
|
||||
"success": "[to be translated]:笔记名称生成成功"
|
||||
"empty_note": "La nota está vacía, no se puede generar un nombre",
|
||||
"failed": "Error al generar el nombre de la nota",
|
||||
"label": "Generar nombre de nota",
|
||||
"success": "Se ha generado correctamente el nombre de la nota"
|
||||
},
|
||||
"characters": "carácter",
|
||||
"collapse": "ocultar",
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)",
|
||||
"sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)",
|
||||
"sort_z2a": "Nombre de archivo (Z-A)",
|
||||
"spell_check": "comprobación ortográfica",
|
||||
"spell_check_tooltip": "Habilitar/deshabilitar revisión ortográfica",
|
||||
"star": "Notas guardadas",
|
||||
"starred_notes": "notas guardadas",
|
||||
"title": "notas",
|
||||
@ -3346,7 +3348,6 @@
|
||||
"dify_knowledge": "Implementación del servidor MCP de Dify, que proporciona una API sencilla para interactuar con Dify. Se requiere configurar la clave de Dify.",
|
||||
"fetch": "Servidor MCP para obtener el contenido de la página web de una URL",
|
||||
"filesystem": "Servidor Node.js que implementa el protocolo de contexto del modelo (MCP) para operaciones del sistema de archivos. Requiere configuración del directorio permitido para el acceso",
|
||||
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||
"mcp_auto_install": "Instalación automática del servicio MCP (versión beta)",
|
||||
"memory": "Implementación básica de memoria persistente basada en un grafo de conocimiento local. Esto permite que el modelo recuerde información relevante del usuario entre diferentes conversaciones. Es necesario configurar la variable de entorno MEMORY_FILE_PATH.",
|
||||
"no": "sin descripción",
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
"new_topic": "Nouveau sujet {{Command}}",
|
||||
"pause": "Pause",
|
||||
"placeholder": "Entrez votre message ici...",
|
||||
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送",
|
||||
"placeholder_without_triggers": "Entrez votre message ici, appuyez sur {{key}} pour envoyer",
|
||||
"send": "Envoyer",
|
||||
"settings": "Paramètres",
|
||||
"thinking": {
|
||||
@ -1698,10 +1698,10 @@
|
||||
},
|
||||
"notes": {
|
||||
"auto_rename": {
|
||||
"empty_note": "[to be translated]:笔记为空,无法生成名称",
|
||||
"failed": "[to be translated]:生成笔记名称失败",
|
||||
"label": "[to be translated]:生成笔记名称",
|
||||
"success": "[to be translated]:笔记名称生成成功"
|
||||
"empty_note": "La note est vide, impossible de générer un nom",
|
||||
"failed": "Échec de la génération du nom de note",
|
||||
"label": "Générer un nom de note",
|
||||
"success": "La génération du nom de note a réussi"
|
||||
},
|
||||
"characters": "caractère",
|
||||
"collapse": "réduire",
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)",
|
||||
"sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)",
|
||||
"sort_z2a": "Nom de fichier (Z-A)",
|
||||
"spell_check": "Vérification orthographique",
|
||||
"spell_check_tooltip": "Activer/Désactiver la vérification orthographique",
|
||||
"star": "Notes enregistrées",
|
||||
"starred_notes": "notes de collection",
|
||||
"title": "notes",
|
||||
@ -3346,7 +3348,6 @@
|
||||
"dify_knowledge": "Implémentation du serveur MCP de Dify, fournissant une API simple pour interagir avec Dify. Nécessite la configuration de la clé Dify",
|
||||
"fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL",
|
||||
"filesystem": "Serveur Node.js implémentant le protocole de contexte de modèle (MCP) pour les opérations de système de fichiers. Nécessite une configuration des répertoires autorisés à être accédés.",
|
||||
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||
"mcp_auto_install": "Installation automatique du service MCP (version bêta)",
|
||||
"memory": "Implémentation de base de mémoire persistante basée sur un graphe de connaissances local. Cela permet au modèle de se souvenir des informations relatives à l'utilisateur entre différentes conversations. Nécessite la configuration de la variable d'environnement MEMORY_FILE_PATH.",
|
||||
"no": "sans description",
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
"new_topic": "新しいトピック {{Command}}",
|
||||
"pause": "一時停止",
|
||||
"placeholder": "ここにメッセージを入力し、{{key}} を押して送信...",
|
||||
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送",
|
||||
"placeholder_without_triggers": "ここにメッセージを入力し、{{key}} を押して送信してください",
|
||||
"send": "送信",
|
||||
"settings": "設定",
|
||||
"thinking": {
|
||||
@ -1698,10 +1698,10 @@
|
||||
},
|
||||
"notes": {
|
||||
"auto_rename": {
|
||||
"empty_note": "[to be translated]:笔记为空,无法生成名称",
|
||||
"failed": "[to be translated]:生成笔记名称失败",
|
||||
"label": "[to be translated]:生成笔记名称",
|
||||
"success": "[to be translated]:笔记名称生成成功"
|
||||
"empty_note": "ノートが空です。名前を生成できません。",
|
||||
"failed": "ノート名の生成に失敗しました",
|
||||
"label": "ノート名の生成",
|
||||
"success": "ノート名の生成に成功しました"
|
||||
},
|
||||
"characters": "文字",
|
||||
"collapse": "閉じる",
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "更新日時(古い順)",
|
||||
"sort_updated_desc": "更新日時(新しい順)",
|
||||
"sort_z2a": "ファイル名(Z-A)",
|
||||
"spell_check": "スペルチェック",
|
||||
"spell_check_tooltip": "スペルチェックの有効/無効",
|
||||
"star": "お気に入りのノート",
|
||||
"starred_notes": "収集したノート",
|
||||
"title": "ノート",
|
||||
@ -3346,7 +3348,6 @@
|
||||
"dify_knowledge": "DifyのMCPサーバー実装は、Difyと対話するためのシンプルなAPIを提供します。Dify Keyの設定が必要です。",
|
||||
"fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー",
|
||||
"filesystem": "Node.jsサーバーによるファイルシステム操作を実現するモデルコンテキストプロトコル(MCP)。アクセスを許可するディレクトリの設定が必要です",
|
||||
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||
"mcp_auto_install": "MCPサービスの自動インストール(ベータ版)",
|
||||
"memory": "ローカルのナレッジグラフに基づく永続的なメモリの基本的な実装です。これにより、モデルは異なる会話間でユーザーの関連情報を記憶できるようになります。MEMORY_FILE_PATH 環境変数の設定が必要です。",
|
||||
"no": "説明なし",
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
"new_topic": "Novo tópico {{Command}}",
|
||||
"pause": "Pausar",
|
||||
"placeholder": "Digite sua mensagem aqui...",
|
||||
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送",
|
||||
"placeholder_without_triggers": "Digite a mensagem aqui, pressione {{key}} para enviar",
|
||||
"send": "Enviar",
|
||||
"settings": "Configurações",
|
||||
"thinking": {
|
||||
@ -1698,10 +1698,10 @@
|
||||
},
|
||||
"notes": {
|
||||
"auto_rename": {
|
||||
"empty_note": "[to be translated]:笔记为空,无法生成名称",
|
||||
"failed": "[to be translated]:生成笔记名称失败",
|
||||
"label": "[to be translated]:生成笔记名称",
|
||||
"success": "[to be translated]:笔记名称生成成功"
|
||||
"empty_note": "A nota está vazia, não é possível gerar um nome",
|
||||
"failed": "Falha ao gerar o nome da nota",
|
||||
"label": "Gerar nome da nota",
|
||||
"success": "Nome da nota gerado com sucesso"
|
||||
},
|
||||
"characters": "caractere",
|
||||
"collapse": "[minimizar]",
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)",
|
||||
"sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)",
|
||||
"sort_z2a": "Nome do arquivo (Z-A)",
|
||||
"spell_check": "verificação ortográfica",
|
||||
"spell_check_tooltip": "Ativar/Desativar verificação ortográfica",
|
||||
"star": "Notas favoritas",
|
||||
"starred_notes": "notas salvas",
|
||||
"title": "nota",
|
||||
@ -3346,7 +3348,6 @@
|
||||
"dify_knowledge": "Implementação do servidor MCP do Dify, que fornece uma API simples para interagir com o Dify. Requer a configuração da chave Dify",
|
||||
"fetch": "servidor MCP para obter o conteúdo da página web do URL",
|
||||
"filesystem": "Servidor Node.js do protocolo de contexto de modelo (MCP) para implementar operações de sistema de ficheiros. Requer configuração do diretório permitido para acesso",
|
||||
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||
"mcp_auto_install": "Instalação automática do serviço MCP (beta)",
|
||||
"memory": "Implementação base de memória persistente baseada em grafos de conhecimento locais. Isso permite que o modelo lembre informações relevantes do utilizador entre diferentes conversas. É necessário configurar a variável de ambiente MEMORY_FILE_PATH.",
|
||||
"no": "sem descrição",
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
"new_topic": "Новый топик {{Command}}",
|
||||
"pause": "Остановить",
|
||||
"placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...",
|
||||
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送",
|
||||
"placeholder_without_triggers": "Введите сообщение здесь, нажмите {{key}}, чтобы отправить",
|
||||
"send": "Отправить",
|
||||
"settings": "Настройки",
|
||||
"thinking": {
|
||||
@ -1698,10 +1698,10 @@
|
||||
},
|
||||
"notes": {
|
||||
"auto_rename": {
|
||||
"empty_note": "[to be translated]:笔记为空,无法生成名称",
|
||||
"failed": "[to be translated]:生成笔记名称失败",
|
||||
"label": "[to be translated]:生成笔记名称",
|
||||
"success": "[to be translated]:笔记名称生成成功"
|
||||
"empty_note": "Заметки пусты, имя невозможно сгенерировать",
|
||||
"failed": "Создание названия заметки не удалось",
|
||||
"label": "Создать название заметки",
|
||||
"success": "Имя заметки успешно создано"
|
||||
},
|
||||
"characters": "Символы",
|
||||
"collapse": "Свернуть",
|
||||
@ -1784,6 +1784,8 @@
|
||||
"sort_updated_asc": "Время обновления (от старого к новому)",
|
||||
"sort_updated_desc": "Время обновления (от нового к старому)",
|
||||
"sort_z2a": "Имя файла (Я-А)",
|
||||
"spell_check": "Проверка орфографии",
|
||||
"spell_check_tooltip": "Включить/отключить проверку орфографии",
|
||||
"star": "Избранные заметки",
|
||||
"starred_notes": "Сохраненные заметки",
|
||||
"title": "заметки",
|
||||
@ -3346,7 +3348,6 @@
|
||||
"dify_knowledge": "Реализация сервера MCP Dify, предоставляющая простой API для взаимодействия с Dify. Требуется настройка ключа Dify",
|
||||
"fetch": "MCP-сервер для получения содержимого веб-страниц по URL",
|
||||
"filesystem": "Node.js-сервер протокола контекста модели (MCP) для реализации операций файловой системы. Требуется настройка каталогов, к которым разрешён доступ",
|
||||
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||
"mcp_auto_install": "Автоматическая установка службы MCP (бета-версия)",
|
||||
"memory": "реализация постоянной памяти на основе локального графа знаний. Это позволяет модели запоминать информацию о пользователе между различными диалогами. Требуется настроить переменную среды MEMORY_FILE_PATH.",
|
||||
"no": "без описания",
|
||||
|
||||
@ -231,7 +231,7 @@ const MessageItem: FC<Props> = ({
|
||||
<HorizontalScrollContainer
|
||||
classNames={{
|
||||
content: cn(
|
||||
'items-center',
|
||||
'flex-1 items-center justify-between',
|
||||
isLastMessage && messageStyle === 'plain' ? 'flex-row-reverse' : 'flex-row'
|
||||
)
|
||||
}}>
|
||||
|
||||
@ -386,6 +386,7 @@ const Container = styled.div`
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border: 0.5px solid transparent;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import ActionIconButton from '@renderer/components/Buttons/ActionIconButton'
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { HSpaceBetweenStack } from '@renderer/components/Layout'
|
||||
import RichEditor from '@renderer/components/RichEditor'
|
||||
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setEnableSpellCheck } from '@renderer/store/settings'
|
||||
import { EditorView } from '@renderer/types'
|
||||
import { Empty } from 'antd'
|
||||
import { Empty, Tooltip } from 'antd'
|
||||
import { SpellCheck } from 'lucide-react'
|
||||
import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -21,7 +26,9 @@ interface NotesEditorProps {
|
||||
const NotesEditor: FC<NotesEditorProps> = memo(
|
||||
({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { settings } = useNotesSettings()
|
||||
const { enableSpellCheck } = useSettings()
|
||||
const currentViewMode = useMemo(() => {
|
||||
if (settings.defaultViewMode === 'edit') {
|
||||
return settings.defaultEditMode
|
||||
@ -78,6 +85,7 @@ const NotesEditor: FC<NotesEditorProps> = memo(
|
||||
isFullWidth={settings.isFullWidth}
|
||||
fontFamily={settings.fontFamily}
|
||||
fontSize={settings.fontSize}
|
||||
enableSpellCheck={enableSpellCheck}
|
||||
/>
|
||||
)}
|
||||
</RichEditorContainer>
|
||||
@ -92,8 +100,21 @@ const NotesEditor: FC<NotesEditorProps> = memo(
|
||||
color: 'var(--color-text-3)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8
|
||||
gap: 12
|
||||
}}>
|
||||
{tmpViewMode === 'preview' && (
|
||||
<Tooltip placement="top" title={t('notes.spell_check_tooltip')} mouseLeaveDelay={0} arrow>
|
||||
<ActionIconButton
|
||||
active={enableSpellCheck}
|
||||
onClick={() => {
|
||||
const newValue = !enableSpellCheck
|
||||
dispatch(setEnableSpellCheck(newValue))
|
||||
window.api.setEnableSpellCheck(newValue)
|
||||
}}>
|
||||
<SpellCheck size={18} />
|
||||
</ActionIconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Selector
|
||||
value={tmpViewMode as EditorView}
|
||||
onChange={(value: EditorView) => setTmpViewMode(value)}
|
||||
|
||||
@ -359,6 +359,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
[bases.length, t]
|
||||
)
|
||||
|
||||
const handleImageAction = useCallback(
|
||||
async (node: NotesTreeNode, platform: 'copyImage' | 'exportImage') => {
|
||||
try {
|
||||
if (activeNode?.id !== node.id) {
|
||||
onSelectNode(node)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
await exportNote({ node, platform })
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ${platform === 'copyImage' ? 'copy' : 'export'} as image:`, error as Error)
|
||||
window.toast.error(t('common.copy_failed'))
|
||||
}
|
||||
},
|
||||
[activeNode, onSelectNode, t]
|
||||
)
|
||||
|
||||
const handleAutoRename = useCallback(
|
||||
async (note: NotesTreeNode) => {
|
||||
if (note.type !== 'file') return
|
||||
@ -612,6 +629,16 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
key: 'export',
|
||||
icon: <UploadIcon size={14} />,
|
||||
children: [
|
||||
exportMenuOptions.image && {
|
||||
label: t('chat.topics.copy.image'),
|
||||
key: 'copy-image',
|
||||
onClick: () => handleImageAction(node, 'copyImage')
|
||||
},
|
||||
exportMenuOptions.image && {
|
||||
label: t('chat.topics.export.image'),
|
||||
key: 'export-image',
|
||||
onClick: () => handleImageAction(node, 'exportImage')
|
||||
},
|
||||
exportMenuOptions.markdown && {
|
||||
label: t('chat.topics.export.md.label'),
|
||||
key: 'markdown',
|
||||
@ -671,6 +698,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
handleStartEdit,
|
||||
onToggleStar,
|
||||
handleExportKnowledge,
|
||||
handleImageAction,
|
||||
handleDeleteNode,
|
||||
renamingNodeIds,
|
||||
handleAutoRename,
|
||||
@ -1129,7 +1157,7 @@ const DragOverIndicator = styled.div`
|
||||
`
|
||||
|
||||
const DropHintNode = styled.div`
|
||||
margin: 8px;
|
||||
margin: 6px 0;
|
||||
margin-bottom: 20px;
|
||||
|
||||
${TreeNodeContainer} {
|
||||
|
||||
@ -89,6 +89,7 @@ export async function restore() {
|
||||
}
|
||||
|
||||
await handleData(data)
|
||||
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'success',
|
||||
@ -850,6 +851,12 @@ export async function handleData(data: Record<string, any>) {
|
||||
|
||||
if (data.version >= 2) {
|
||||
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||
|
||||
// remove notes_tree from indexedDB
|
||||
if (data.indexedDB['notes_tree']) {
|
||||
delete data.indexedDB['notes_tree']
|
||||
}
|
||||
|
||||
await restoreDatabase(data.indexedDB)
|
||||
|
||||
if (data.version === 3) {
|
||||
|
||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 158,
|
||||
version: 159,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -85,6 +85,15 @@ function addProvider(state: RootState, id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fix missing provider
|
||||
function fixMissingProvider(state: RootState) {
|
||||
SYSTEM_PROVIDERS.forEach((p) => {
|
||||
if (!state.llm.providers.find((provider) => provider.id === p.id)) {
|
||||
state.llm.providers.push(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// add ocr provider
|
||||
function addOcrProvider(state: RootState, provider: BuiltinOcrProvider) {
|
||||
if (!state.ocr.providers.find((p) => p.id === provider.id)) {
|
||||
@ -2553,6 +2562,7 @@ const migrateConfig = {
|
||||
'159': (state: RootState) => {
|
||||
try {
|
||||
addProvider(state, 'ovms')
|
||||
fixMissingProvider(state)
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 159 error', error as Error)
|
||||
|
||||
@ -9,6 +9,7 @@ import { setExportState } from '@renderer/store/runtime'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { removeSpecialCharactersForFileName } from '@renderer/utils/file'
|
||||
import { captureScrollableAsBlob, captureScrollableAsDataURL } from '@renderer/utils/image'
|
||||
import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown'
|
||||
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
|
||||
import { markdownToBlocks } from '@tryfabric/martian'
|
||||
@ -1092,9 +1093,57 @@ const exportNoteAsMarkdown = async (noteName: string, content: string): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
const getScrollableElement = (): HTMLElement | null => {
|
||||
const notesPage = document.querySelector('#notes-page')
|
||||
if (!notesPage) return null
|
||||
|
||||
const allDivs = notesPage.querySelectorAll('div')
|
||||
for (const div of Array.from(allDivs)) {
|
||||
const style = window.getComputedStyle(div)
|
||||
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
|
||||
if (div.querySelector('.ProseMirror')) {
|
||||
return div as HTMLElement
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getScrollableRef = (): { current: HTMLElement } | null => {
|
||||
const element = getScrollableElement()
|
||||
if (!element) {
|
||||
window.toast.warning(i18n.t('notes.no_content_to_copy'))
|
||||
return null
|
||||
}
|
||||
return { current: element }
|
||||
}
|
||||
|
||||
const exportNoteAsImageToClipboard = async (): Promise<void> => {
|
||||
const scrollableRef = getScrollableRef()
|
||||
if (!scrollableRef) return
|
||||
|
||||
await captureScrollableAsBlob(scrollableRef, async (blob) => {
|
||||
if (blob) {
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
||||
window.toast.success(i18n.t('common.copied'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const exportNoteAsImageFile = async (noteName: string): Promise<void> => {
|
||||
const scrollableRef = getScrollableRef()
|
||||
if (!scrollableRef) return
|
||||
|
||||
const dataUrl = await captureScrollableAsDataURL(scrollableRef)
|
||||
if (dataUrl) {
|
||||
const fileName = removeSpecialCharactersForFileName(noteName)
|
||||
await window.api.file.saveImage(fileName, dataUrl)
|
||||
}
|
||||
}
|
||||
|
||||
interface NoteExportOptions {
|
||||
node: { name: string; externalPath: string }
|
||||
platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan'
|
||||
platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan' | 'copyImage' | 'exportImage'
|
||||
}
|
||||
|
||||
export const exportNote = async ({ node, platform }: NoteExportOptions): Promise<void> => {
|
||||
@ -1102,6 +1151,10 @@ export const exportNote = async ({ node, platform }: NoteExportOptions): Promise
|
||||
const content = await window.api.file.readExternal(node.externalPath)
|
||||
|
||||
switch (platform) {
|
||||
case 'copyImage':
|
||||
return await exportNoteAsImageToClipboard()
|
||||
case 'exportImage':
|
||||
return await exportNoteAsImageFile(node.name)
|
||||
case 'markdown':
|
||||
return await exportNoteAsMarkdown(node.name, content)
|
||||
case 'docx':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user