From cd881ceb34860e3a72f2cad86f3b80fe1326a462 Mon Sep 17 00:00:00 2001 From: Daniel Hofheinz Date: Tue, 7 Oct 2025 10:55:21 -0700 Subject: [PATCH 1/7] fix(ui): remove redundant scrollbar in side-by-side view & fix message menubar overflow (#10543) * fix(ui): remove redundant scrollbar in side-by-side view Changed GridContainer from styled(Scrollbar) to styled.div to eliminate redundant horizontal scrollbar in multi-model horizontal layout mode. The Scrollbar component is designed for vertical scrolling and conflicts with horizontal layouts. Fixes #10520 * fix(ui): restore vertical scrollbar for grid mode while preserving horizontal fix Optimal solution: Use Scrollbar component as base to preserve auto-hide behavior for vertical modes (grid, vertical, fold) while overriding its overflow-y behavior for horizontal mode only. This approach: - Preserves the June 2025 UX optimization (auto-hide scrollbars) - Fixes horizontal scrollbar issue from #10520 - Restores vertical scrolling for grid mode - Maintains auto-hide behavior for all vertical scrolling modes - Minimal change with no code duplication The Scrollbar component provides scrollbar thumb auto-hide after 1.5s, which enhances UX for vertical scrolling. By using CSS overrides only for horizontal mode, we get the best of both worlds. * chore: fix import sorting in MessageGroup.tsx Unrelated to PR scope - fixing to unblock CI. Auto-fixed via eslint --fix (moved Scrollbar import to correct position). Also updated yarn.lock to resolve dependency sync. * fix(ui): add explicit overflow declarations for all grid modes Previous fix relied on CSS inheritance from Scrollbar base component, but display: grid interferes with overflow property inheritance. This iteration adds explicit overflow-y: auto and overflow-x: hidden to grid, fold, vertical, and multi-select modes to ensure vertical scrolling works reliably across all layouts. - horizontal mode: overflow-y visible, overflow-x auto (unchanged) - grid/fold/vertical modes: explicit overflow-y auto, overflow-x hidden - multi-select mode: explicit overflow-y auto, overflow-x hidden Fixes vertical scrollbar missing in grid mode reported by @EurFelux * fix(Messages): adjust overflow behavior in message groups Fix scrollbar issues by hiding vertical overflow in horizontal layout and simplifying overflow handling in grid layout * feat(HorizontalScrollContainer): add classNames prop for container and content styling allow custom styling of container and content via classNames prop --------- Co-authored-by: icarus --- .../HorizontalScrollContainer/index.tsx | 15 ++++++- .../src/pages/home/Messages/Message.tsx | 42 +++++++++++-------- .../src/pages/home/Messages/MessageGroup.tsx | 24 +++++++++-- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/components/HorizontalScrollContainer/index.tsx b/src/renderer/src/components/HorizontalScrollContainer/index.tsx index ed5cdc52de..fdc890d2e2 100644 --- a/src/renderer/src/components/HorizontalScrollContainer/index.tsx +++ b/src/renderer/src/components/HorizontalScrollContainer/index.tsx @@ -1,3 +1,4 @@ +import { cn } from '@heroui/react' import Scrollbar from '@renderer/components/Scrollbar' import { ChevronRight } from 'lucide-react' import { useEffect, useRef, useState } from 'react' @@ -17,6 +18,10 @@ export interface HorizontalScrollContainerProps { dependencies?: readonly unknown[] scrollDistance?: number className?: string + classNames?: { + container?: string + content?: string + } gap?: string expandable?: boolean } @@ -26,6 +31,7 @@ const HorizontalScrollContainer: React.FC = ({ dependencies = [], scrollDistance = 200, className, + classNames, gap = '8px', expandable = false }) => { @@ -95,11 +101,16 @@ const HorizontalScrollContainer: React.FC = ({ return ( - + {children} {canScroll && !isExpanded && !isScrolledToEnd && ( diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 0e86e9b232..d4a7ceef71 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -1,4 +1,6 @@ +import { cn } from '@heroui/react' import { loggerService } from '@logger' +import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' import Scrollbar from '@renderer/components/Scrollbar' import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useAssistant } from '@renderer/hooks/useAssistant' @@ -225,20 +227,28 @@ const MessageItem: FC = ({ {showMenubar && ( - - } - setModel={setModel} - onUpdateUseful={onUpdateUseful} - /> + + + } + setModel={setModel} + onUpdateUseful={onUpdateUseful} + /> + )} @@ -282,10 +292,8 @@ const MessageContentContainer = styled(Scrollbar)` overflow-y: auto; ` -const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plain' | 'bubble' }>` +const MessageFooter = styled.div` display: flex; - flex-direction: ${({ $isLastMessage, $messageStyle }) => - $isLastMessage && $messageStyle === 'plain' ? 'row-reverse' : 'row'}; align-items: center; justify-content: space-between; gap: 10px; diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 4632c9ffb9..94c9672eda 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -337,17 +337,30 @@ const GroupContainer = styled.div` const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number }>` width: 100%; display: grid; - overflow-y: visible; gap: 16px; + &.horizontal { padding-bottom: 4px; grid-template-columns: repeat(${({ $count }) => $count}, minmax(420px, 1fr)); + overflow-y: hidden; overflow-x: auto; + &::-webkit-scrollbar { + height: 6px; + } + &::-webkit-scrollbar-thumb { + background: var(--color-scrollbar-thumb); + border-radius: var(--scrollbar-thumb-radius); + } + &::-webkit-scrollbar-thumb:hover { + background: var(--color-scrollbar-thumb-hover); + } } &.fold, &.vertical { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 8px; + overflow-y: auto; + overflow-x: hidden; } &.grid { grid-template-columns: repeat( @@ -355,11 +368,15 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number } minmax(0, 1fr) ); grid-template-rows: auto; + overflow-y: auto; + overflow-x: hidden; } &.multi-select-mode { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 10px; + overflow-y: auto; + overflow-x: hidden; .grid { height: auto; } @@ -385,7 +402,7 @@ interface MessageWrapperProps { const MessageWrapper = styled.div` &.horizontal { padding: 1px; - overflow-y: auto; + /* overflow-y: auto; */ .message { height: 100%; border: 0.5px solid var(--color-border); @@ -405,8 +422,9 @@ const MessageWrapper = styled.div` } } &.grid { + display: block; height: 300px; - overflow-y: hidden; + overflow: hidden; border: 0.5px solid var(--color-border); border-radius: 10px; cursor: pointer; From d4b34281604c62bd54acb27bc855993234c095c1 Mon Sep 17 00:00:00 2001 From: Tristan Zhang Date: Wed, 8 Oct 2025 01:57:00 +0800 Subject: [PATCH 2/7] feat: Support automatic line wrapping for tables in notes (#10503) * feat: add table auto-wrap feature for notes * chore: lint * feat: remove settings for auto wrap --- .../src/components/RichEditor/styles.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/renderer/src/components/RichEditor/styles.ts b/src/renderer/src/components/RichEditor/styles.ts index b184063d08..6acb943528 100644 --- a/src/renderer/src/components/RichEditor/styles.ts +++ b/src/renderer/src/components/RichEditor/styles.ts @@ -14,6 +14,31 @@ export const RichEditorWrapper = styled.div<{ border-radius: 6px; background: var(--color-background); overflow-y: hidden; + .ProseMirror table, + .tiptap table { + table-layout: auto !important; + } + + .ProseMirror table th, + .ProseMirror table td, + .tiptap th, + .tiptap td { + white-space: normal !important; + word-wrap: break-word !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + overflow: visible !important; + text-overflow: clip !important; + } + + .ProseMirror table th > *, + .ProseMirror table td > *, + .tiptap td > *, + .tiptap th > * { + white-space: normal !important; + overflow: visible !important; + text-overflow: clip !important; + } width: ${({ $isFullWidth }) => ($isFullWidth ? '100%' : '60%')}; margin: ${({ $isFullWidth }) => ($isFullWidth ? '0' : '0 auto')}; font-family: ${({ $fontFamily }) => ($fontFamily === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)')}; @@ -21,6 +46,7 @@ export const RichEditorWrapper = styled.div<{ ${({ $minHeight }) => $minHeight && `min-height: ${$minHeight}px;`} ${({ $maxHeight }) => $maxHeight && `max-height: ${$maxHeight}px;`} + ` export const ToolbarWrapper = styled.div` 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 3/7] 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 4/7] =?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 5/7] 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 6/7] 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 7/7] 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':