From 504531d4d5f3399ab5f7b762fcafdec817e45c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:48:26 +0800 Subject: [PATCH 1/5] feat(notes): add spell-check control (#10507) * feat(notes): add spell-check control * feat(notes): add spell-check toggle to preview mode toolbar * feat(settings): move spellcheck to global and use hook --- .../src/components/RichEditor/index.tsx | 4 ++- .../src/components/RichEditor/types.ts | 2 ++ .../components/RichEditor/useRichEditor.ts | 7 +++++- src/renderer/src/i18n/locales/en-us.json | 2 ++ src/renderer/src/i18n/locales/zh-cn.json | 2 ++ src/renderer/src/i18n/locales/zh-tw.json | 2 ++ src/renderer/src/i18n/translate/el-gr.json | 9 +++++++ src/renderer/src/i18n/translate/es-es.json | 9 +++++++ src/renderer/src/i18n/translate/fr-fr.json | 9 +++++++ src/renderer/src/i18n/translate/ja-jp.json | 9 +++++++ src/renderer/src/i18n/translate/pt-pt.json | 9 +++++++ src/renderer/src/i18n/translate/ru-ru.json | 9 +++++++ src/renderer/src/pages/notes/NotesEditor.tsx | 25 +++++++++++++++++-- 13 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/RichEditor/index.tsx b/src/renderer/src/components/RichEditor/index.tsx index 0b9e3876ac..83023dab9a 100644 --- a/src/renderer/src/components/RichEditor/index.tsx +++ b/src/renderer/src/components/RichEditor/index.tsx @@ -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 }) => { // 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 = { diff --git a/src/renderer/src/components/RichEditor/types.ts b/src/renderer/src/components/RichEditor/types.ts index 8804210aef..48ae5bb112 100644 --- a/src/renderer/src/components/RichEditor/types.ts +++ b/src/renderer/src/components/RichEditor/types.ts @@ -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 { diff --git a/src/renderer/src/components/RichEditor/useRichEditor.ts b/src/renderer/src/components/RichEditor/useRichEditor.ts index 7dae176068..1ece36fb00 100644 --- a/src/renderer/src/components/RichEditor/useRichEditor.ts +++ b/src/renderer/src/components/RichEditor/useRichEditor.ts @@ -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 }) => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0f3d2a3f24..00ab42128b 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ce2dc5c222..af2389dbe2 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1784,6 +1784,8 @@ "sort_updated_asc": "更新时间(从旧到新)", "sort_updated_desc": "更新时间(从新到旧)", "sort_z2a": "文件名(Z-A)", + "spell_check": "拼写检查", + "spell_check_tooltip": "启用/禁用拼写检查", "star": "收藏笔记", "starred_notes": "收藏的笔记", "title": "笔记", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e4f45288ee..d0c08124ea 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1784,6 +1784,8 @@ "sort_updated_asc": "更新時間(從舊到新)", "sort_updated_desc": "更新時間(從新到舊)", "sort_z2a": "文件名(Z-A)", + "spell_check": "拼寫檢查", + "spell_check_tooltip": "啟用/禁用拼寫檢查", "star": "收藏筆記", "starred_notes": "收藏的筆記", "title": "筆記", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index a071f27783..57edf11f96 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -334,6 +334,7 @@ "new_topic": "Νέο θέμα {{Command}}", "pause": "Παύση", "placeholder": "Εισάγετε μήνυμα εδώ...", + "placeholder_without_triggers": "Εδώ εισαγάγετε το μήνυμα, πατήστε {{key}} για αποστολή", "send": "Αποστολή", "settings": "Ρυθμίσεις", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου" }, "notes": { + "auto_rename": { + "empty_note": "Το σημείωμα είναι κενό, δεν μπορεί να δημιουργηθεί όνομα", + "failed": "Αποτυχία δημιουργίας ονόματος σημείωσης", + "label": "Δημιουργία ονόματος σημείωσης", + "success": "Η δημιουργία του ονόματος σημειώσεων ολοκληρώθηκε με επιτυχία" + }, "characters": "χαρακτήρας", "collapse": "σύμπτυξη", "content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)", "sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)", "sort_z2a": "όνομα αρχείου (Z-A)", + "spell_check": "Έλεγχος ορθογραφίας", + "spell_check_tooltip": "Ενεργοποίηση/Απενεργοποίηση ελέγχου ορθογραφίας", "star": "Αγαπημένες σημειώσεις", "starred_notes": "Σημειώσεις συλλογής", "title": "σημειώσεις", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 7ae0a1bd3c..739943ffc7 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -334,6 +334,7 @@ "new_topic": "Nuevo tema {{Command}}", "pause": "Pausar", "placeholder": "Escribe aquí tu mensaje...", + "placeholder_without_triggers": "Escriba un mensaje aquí y presione {{key}} para enviar", "send": "Enviar", "settings": "Configuración", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Ir a la configuración del proveedor" }, "notes": { + "auto_rename": { + "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", "content_placeholder": "Introduzca el contenido de la nota...", @@ -1777,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", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 64a788f266..d5eda2e61e 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -334,6 +334,7 @@ "new_topic": "Nouveau sujet {{Command}}", "pause": "Pause", "placeholder": "Entrez votre message ici...", + "placeholder_without_triggers": "Entrez votre message ici, appuyez sur {{key}} pour envoyer", "send": "Envoyer", "settings": "Paramètres", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Aller aux paramètres du fournisseur" }, "notes": { + "auto_rename": { + "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", "content_placeholder": "Veuillez saisir le contenu de la note...", @@ -1777,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", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 23eea05fe6..f5cde82e28 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -334,6 +334,7 @@ "new_topic": "新しいトピック {{Command}}", "pause": "一時停止", "placeholder": "ここにメッセージを入力し、{{key}} を押して送信...", + "placeholder_without_triggers": "ここにメッセージを入力し、{{key}} を押して送信してください", "send": "送信", "settings": "設定", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "プロバイダー設定に移動" }, "notes": { + "auto_rename": { + "empty_note": "ノートが空です。名前を生成できません。", + "failed": "ノート名の生成に失敗しました", + "label": "ノート名の生成", + "success": "ノート名の生成に成功しました" + }, "characters": "文字", "collapse": "閉じる", "content_placeholder": "メモの内容を入力してください...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "更新日時(古い順)", "sort_updated_desc": "更新日時(新しい順)", "sort_z2a": "ファイル名(Z-A)", + "spell_check": "スペルチェック", + "spell_check_tooltip": "スペルチェックの有効/無効", "star": "お気に入りのノート", "starred_notes": "収集したノート", "title": "ノート", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index ab9bec0e66..befcedf381 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -334,6 +334,7 @@ "new_topic": "Novo tópico {{Command}}", "pause": "Pausar", "placeholder": "Digite sua mensagem aqui...", + "placeholder_without_triggers": "Digite a mensagem aqui, pressione {{key}} para enviar", "send": "Enviar", "settings": "Configurações", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Ir para as configurações do provedor" }, "notes": { + "auto_rename": { + "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]", "content_placeholder": "Introduza o conteúdo da nota...", @@ -1777,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", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index ccc1f49344..f74529300d 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -334,6 +334,7 @@ "new_topic": "Новый топик {{Command}}", "pause": "Остановить", "placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...", + "placeholder_without_triggers": "Введите сообщение здесь, нажмите {{key}}, чтобы отправить", "send": "Отправить", "settings": "Настройки", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Перейти к настройкам поставщика" }, "notes": { + "auto_rename": { + "empty_note": "Заметки пусты, имя невозможно сгенерировать", + "failed": "Создание названия заметки не удалось", + "label": "Создать название заметки", + "success": "Имя заметки успешно создано" + }, "characters": "Символы", "collapse": "Свернуть", "content_placeholder": "Введите содержимое заметки...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "Время обновления (от старого к новому)", "sort_updated_desc": "Время обновления (от нового к старому)", "sort_z2a": "Имя файла (Я-А)", + "spell_check": "Проверка орфографии", + "spell_check_tooltip": "Включить/отключить проверку орфографии", "star": "Избранные заметки", "starred_notes": "Сохраненные заметки", "title": "заметки", diff --git a/src/renderer/src/pages/notes/NotesEditor.tsx b/src/renderer/src/pages/notes/NotesEditor.tsx index 8bdd44d12c..18c2cfe9d5 100644 --- a/src/renderer/src/pages/notes/NotesEditor.tsx +++ b/src/renderer/src/pages/notes/NotesEditor.tsx @@ -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 = 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 = memo( isFullWidth={settings.isFullWidth} fontFamily={settings.fontFamily} fontSize={settings.fontSize} + enableSpellCheck={enableSpellCheck} /> )} @@ -92,8 +100,21 @@ const NotesEditor: FC = memo( color: 'var(--color-text-3)', display: 'flex', alignItems: 'center', - gap: 8 + gap: 12 }}> + {tmpViewMode === 'preview' && ( + + { + const newValue = !enableSpellCheck + dispatch(setEnableSpellCheck(newValue)) + window.api.setEnableSpellCheck(newValue) + }}> + + + + )} setTmpViewMode(value)} From 65d066cbefbc881281b909600165b6b2444d5a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Wed, 8 Oct 2025 19:28:08 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20migration=20for=20missing=20provider?= =?UTF-8?q?s=20=E2=80=A6=20(#10438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: bump version to 1.6.3 and add migration for missing providers #10425 fix: #10425 - Updated the version from 158 to 159 in the persisted reducer configuration. - Implemented a migration function to ensure missing system providers are added to the state during the migration to version 159, enhancing state consistency. --- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 4b74ba91a2..0c6383cc1d 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 158, + version: 159, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index f1e76ed956..8de9781bf2 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -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) From 37f7042f0f2b716789a925c11f0e35b6d4d6f846 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 8 Oct 2025 21:42:36 +0800 Subject: [PATCH 3/5] refactor: update styling and layout in Message component and NotesSidebar - Adjusted class names in Message component for better layout management. - Modified margin in DropHintNode of NotesSidebar for improved spacing. - Enhanced BackupService to remove 'notes_tree' from indexedDB during data restoration. --- src/renderer/src/pages/home/Messages/Message.tsx | 2 +- .../src/pages/home/Tabs/components/AssistantItem.tsx | 1 + src/renderer/src/pages/notes/NotesSidebar.tsx | 2 +- src/renderer/src/services/BackupService.ts | 7 +++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index d4a7ceef71..926aa66414 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -231,7 +231,7 @@ const MessageItem: FC = ({ diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index d20c69b052..c7d6c7a870 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -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); diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index a53fc5b5f7..e7f3db9494 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -1129,7 +1129,7 @@ const DragOverIndicator = styled.div` ` const DropHintNode = styled.div` - margin: 8px; + margin: 6px 0; margin-bottom: 20px; ${TreeNodeContainer} { diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index 985247e43f..c168bd489b 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -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) { 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) { From 6a8544fb0e303828f5de4d096f0c14cfc1cbaf00 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 8 Oct 2025 22:08:08 +0800 Subject: [PATCH 4/5] chore: bump version to 1.6.3 --- electron-builder.yml | 22 ++++++++++++++++++---- package.json | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 56dba2795c..e1e29eb111 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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 diff --git a/package.json b/package.json index ce586b5afe..3955e885ce 100644 --- a/package.json +++ b/package.json @@ -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", From 42849e4586bb2332dc3d27ab18f430e85276fb15 Mon Sep 17 00:00:00 2001 From: Tristan Zhang Date: Wed, 8 Oct 2025 23:32:32 +0800 Subject: [PATCH 5/5] feat: support export image for notes (#10559) * feat: support export image for notes * feat: extract functions --- src/renderer/src/pages/notes/NotesSidebar.tsx | 28 ++++++++++ src/renderer/src/utils/export.ts | 55 ++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index e7f3db9494..a2917f6316 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -359,6 +359,23 @@ const NotesSidebar: FC = ({ [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 = ({ key: 'export', icon: , 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 = ({ handleStartEdit, onToggleStar, handleExportKnowledge, + handleImageAction, handleDeleteNode, renamingNodeIds, handleAutoRename, diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index d50b5b0e5d..728fb2d89b 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -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 => { + 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 => { + 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 => { @@ -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':