From 089477eb1e4cd247ad041661d571723415fa044c Mon Sep 17 00:00:00 2001 From: one Date: Tue, 2 Sep 2025 21:52:14 +0800 Subject: [PATCH 01/12] refactor(CodeViewer): improve props, aligned to CodeEditor (#9786) * refactor(CodeViewer): improve props, aligned to CodeEditor * refactor: simplify internal variables * refactor: remove default lineNumbers * fix: shiki theme container style * revert: use ReactMarkdown for prompt editing --- .../src/components/CodeBlockView/view.tsx | 7 +- .../src/components/CodeEditor/index.tsx | 14 +-- src/renderer/src/components/CodeViewer.tsx | 89 ++++++++++++++----- .../AssistantPromptSettings.tsx | 12 ++- 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/renderer/src/components/CodeBlockView/view.tsx b/src/renderer/src/components/CodeBlockView/view.tsx index 174327ae68..46f6cbce86 100644 --- a/src/renderer/src/components/CodeBlockView/view.tsx +++ b/src/renderer/src/components/CodeBlockView/view.tsx @@ -257,12 +257,13 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave ) : ( - {children} - + maxHeight={`${MAX_COLLAPSED_CODE_HEIGHT}px`} + /> ), [children, codeEditor.enabled, handleHeightChange, language, onSave, shouldExpand, shouldWrap] ) diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx index 7c541cbdb8..4304ec324e 100644 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ b/src/renderer/src/components/CodeEditor/index.tsx @@ -48,8 +48,6 @@ export interface CodeEditorProps { maxHeight?: string /** Minimum editor height. */ minHeight?: string - /** Font size that overrides the app setting. */ - fontSize?: string /** Editor options that extend BasicSetupOptions. */ options?: { /** @@ -70,6 +68,8 @@ export interface CodeEditorProps { } & BasicSetupOptions /** Additional extensions for CodeMirror. */ extensions?: Extension[] + /** Font size that overrides the app setting. */ + fontSize?: number /** Style overrides for the editor, passed directly to CodeMirror's style property. */ style?: React.CSSProperties /** CSS class name appended to the default `code-editor` class. */ @@ -108,9 +108,9 @@ const CodeEditor = ({ height, maxHeight, minHeight, - fontSize, options, extensions, + fontSize: customFontSize, style, className, editable = true, @@ -121,7 +121,7 @@ const CodeEditor = ({ const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap]) // 合并 codeEditor 和 options 的 basicSetup,options 优先 - const customBasicSetup = useMemo(() => { + const basicSetup = useMemo(() => { return { lineNumbers: _lineNumbers, ...(codeEditor as BasicSetupOptions), @@ -129,7 +129,7 @@ const CodeEditor = ({ } }, [codeEditor, _lineNumbers, options]) - const customFontSize = useMemo(() => fontSize ?? `${_fontSize - 1}px`, [fontSize, _fontSize]) + const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize]) const { activeCmTheme } = useCodeStyle() const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? '')) @@ -214,10 +214,10 @@ const CodeEditor = ({ foldKeymap: enableKeymap, completionKeymap: enableKeymap, lintKeymap: enableKeymap, - ...customBasicSetup // override basicSetup + ...basicSetup // override basicSetup }} style={{ - fontSize: customFontSize, + fontSize, marginTop: 0, borderRadius: 'inherit', ...style diff --git a/src/renderer/src/components/CodeViewer.tsx b/src/renderer/src/components/CodeViewer.tsx index 440aed9c7c..e8080d9518 100644 --- a/src/renderer/src/components/CodeViewer.tsx +++ b/src/renderer/src/components/CodeViewer.tsx @@ -1,4 +1,3 @@ -import { MAX_COLLAPSED_CODE_HEIGHT } from '@renderer/config/constant' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight' import { useSettings } from '@renderer/hooks/useSettings' @@ -11,13 +10,49 @@ import { ThemedToken } from 'shiki/core' import styled from 'styled-components' interface CodeViewerProps { + /** Code string value. */ + value: string + /** + * Code language string. + * - Case-insensitive. + * - Supports common names: javascript, json, python, etc. + * - Supports shiki aliases: c#/csharp, objective-c++/obj-c++/objc++, etc. + */ language: string - children: string - expanded?: boolean - wrapped?: boolean + /** Fired when the editor height changes. */ onHeightChange?: (scrollHeight: number) => void - className?: string + /** + * Height of the scroll container. + * Only works when expanded is false. + */ height?: string | number + /** + * Maximum height of the scroll container. + * Only works when expanded is false. + */ + maxHeight?: string | number + /** Viewer options. */ + options?: { + /** + * Whether to show line numbers. + */ + lineNumbers?: boolean + } + /** Font size that overrides the app setting. */ + fontSize?: number + /** CSS class name appended to the default `code-viewer` class. */ + className?: string + /** + * Whether the editor is expanded. + * If true, the height and maxHeight props are ignored. + * @default true + */ + expanded?: boolean + /** + * Whether the code lines are wrapped. + * @default true + */ + wrapped?: boolean } /** @@ -26,19 +61,33 @@ interface CodeViewerProps { * - 使用虚拟滚动和按需高亮,改善页面内有大量长代码块时的响应 * - 并发安全 */ -const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, className, height }: CodeViewerProps) => { - const { codeShowLineNumbers, fontSize } = useSettings() +const CodeViewer = ({ + value, + language, + height, + maxHeight, + onHeightChange, + options, + fontSize: customFontSize, + className, + expanded = true, + wrapped = true +}: CodeViewerProps) => { + const { codeShowLineNumbers: _lineNumbers, fontSize: _fontSize } = useSettings() const { getShikiPreProperties, isShikiThemeDark } = useCodeStyle() const shikiThemeRef = useRef(null) const scrollerRef = useRef(null) const callerId = useRef(`${Date.now()}-${uuid()}`).current - const rawLines = useMemo(() => (typeof children === 'string' ? children.trimEnd().split('\n') : []), [children]) + const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize]) + const lineNumbers = useMemo(() => options?.lineNumbers ?? _lineNumbers, [options?.lineNumbers, _lineNumbers]) + + const rawLines = useMemo(() => (typeof value === 'string' ? value.trimEnd().split('\n') : []), [value]) // 计算行号数字位数 const gutterDigits = useMemo( - () => (codeShowLineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0), - [codeShowLineNumbers, rawLines.length] + () => (lineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0), + [lineNumbers, rawLines.length] ) // 设置 pre 标签属性 @@ -68,7 +117,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla const getScrollElement = useCallback(() => scrollerRef.current, []) const getItemKey = useCallback((index: number) => `${callerId}-${index}`, [callerId]) // `line-height: 1.6` 为全局样式,但是为了避免测量误差在这里取整 - const estimateSize = useCallback(() => Math.round((fontSize - 1) * 1.6), [fontSize]) + const estimateSize = useCallback(() => Math.round(fontSize * 1.6), [fontSize]) // 创建 virtualizer 实例 const virtualizer = useVirtualizer({ @@ -105,20 +154,19 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla }, [rawLines.length, onHeightChange]) return ( -
+
@@ -142,7 +190,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
@@ -226,9 +274,8 @@ VirtualizedRow.displayName = 'VirtualizedRow' const ScrollContainer = styled.div<{ $wrap?: boolean - $expanded?: boolean + $expand?: boolean $lineHeight?: number - $height?: string | number }>` display: block; overflow-x: auto; @@ -244,7 +291,7 @@ const ScrollContainer = styled.div<{ line-height: ${(props) => props.$lineHeight}px; /* contain 优化 wrap 时滚动性能,will-change 优化 unwrap 时滚动性能 */ contain: ${(props) => (props.$wrap ? 'content' : 'none')}; - will-change: ${(props) => (!props.$wrap && !props.$expanded ? 'transform' : 'auto')}; + will-change: ${(props) => (!props.$wrap && !props.$expand ? 'transform' : 'auto')}; .line-number { width: var(--gutter-width, 1.2ch); diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx index 9f27db6afb..a2ce2657b9 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx @@ -2,7 +2,6 @@ import 'emoji-picker-element' import { CloseCircleFilled } from '@ant-design/icons' import CodeEditor from '@renderer/components/CodeEditor' -import CodeViewer from '@renderer/components/CodeViewer' import EmojiPicker from '@renderer/components/EmojiPicker' import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout' import { RichEditorRef } from '@renderer/components/RichEditor/types' @@ -14,6 +13,7 @@ import { Button, Input, Popover } from 'antd' import { Edit, HelpCircle, Save } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import ReactMarkdown from 'react-markdown' import styled from 'styled-components' import { SettingDivider } from '..' @@ -122,7 +122,9 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } {showPreview ? ( - + + {processedPrompt || prompt} + ) : ( Date: Tue, 2 Sep 2025 23:28:29 +0800 Subject: [PATCH 02/12] fix: draggable list id type (#9809) * refactor(dnd): rename idKey to itemKey for clarity * refactor: key and id type for draggable lists * chore: update yarn lock * fix: type error * refactor: improve getId fallbacks --- .../__tests__/DraggableList.test.tsx | 3 ++- .../__tests__/useDraggableReorder.test.ts | 10 ++++---- .../src/components/DraggableList/list.tsx | 23 +++++++++++++++---- .../DraggableList/useDraggableReorder.ts | 14 ++++++++--- .../components/DraggableList/virtual-list.tsx | 2 +- .../src/components/dnd/useDndReorder.ts | 9 +++++--- .../settings/MCPSettings/McpServersList.tsx | 2 +- .../ProviderSettings/ProviderList.tsx | 2 +- 8 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/renderer/src/components/DraggableList/__tests__/DraggableList.test.tsx b/src/renderer/src/components/DraggableList/__tests__/DraggableList.test.tsx index 2d61583787..87765a504b 100644 --- a/src/renderer/src/components/DraggableList/__tests__/DraggableList.test.tsx +++ b/src/renderer/src/components/DraggableList/__tests__/DraggableList.test.tsx @@ -71,8 +71,9 @@ describe('DraggableList', () => { }) it('should render nothing when list is empty', () => { + const emptyList: Array<{ id: string; name: string }> = [] render( - {}}> + {}}> {(item) =>
{item.name}
}
) diff --git a/src/renderer/src/components/DraggableList/__tests__/useDraggableReorder.test.ts b/src/renderer/src/components/DraggableList/__tests__/useDraggableReorder.test.ts index f2d2fe837f..a9ffd3d889 100644 --- a/src/renderer/src/components/DraggableList/__tests__/useDraggableReorder.test.ts +++ b/src/renderer/src/components/DraggableList/__tests__/useDraggableReorder.test.ts @@ -33,7 +33,7 @@ describe('useDraggableReorder', () => { originalList: mockOriginalList, filteredList: mockOriginalList, // 列表未过滤 onUpdate, - idKey: 'id' + itemKey: 'id' }) ) @@ -61,7 +61,7 @@ describe('useDraggableReorder', () => { originalList: mockOriginalList, filteredList, onUpdate, - idKey: 'id' + itemKey: 'id' }) ) @@ -89,7 +89,7 @@ describe('useDraggableReorder', () => { originalList: mockOriginalList, filteredList: mockOriginalList, onUpdate, - idKey: 'id' + itemKey: 'id' }) ) @@ -110,7 +110,7 @@ describe('useDraggableReorder', () => { originalList: mockOriginalList, filteredList: mockOriginalList, onUpdate, - idKey: 'id' + itemKey: 'id' }) ) @@ -136,7 +136,7 @@ describe('useDraggableReorder', () => { originalList: mockOriginalList, filteredList, onUpdate, - idKey: 'id' + itemKey: 'id' }) ) diff --git a/src/renderer/src/components/DraggableList/list.tsx b/src/renderer/src/components/DraggableList/list.tsx index b0e87bd2d2..fbb5f29762 100644 --- a/src/renderer/src/components/DraggableList/list.tsx +++ b/src/renderer/src/components/DraggableList/list.tsx @@ -9,7 +9,7 @@ import { ResponderProvided } from '@hello-pangea/dnd' import { droppableReorder } from '@renderer/utils' -import { FC, HTMLAttributes } from 'react' +import { HTMLAttributes, Key, useCallback } from 'react' interface Props { list: T[] @@ -17,23 +17,25 @@ interface Props { listStyle?: React.CSSProperties listProps?: HTMLAttributes children: (item: T, index: number) => React.ReactNode + itemKey?: keyof T | ((item: T) => Key) onUpdate: (list: T[]) => void onDragStart?: OnDragStartResponder onDragEnd?: OnDragEndResponder droppableProps?: Partial } -const DraggableList: FC> = ({ +function DraggableList({ children, list, style, listStyle, listProps, + itemKey, droppableProps, onDragStart, onUpdate, onDragEnd -}) => { +}: Props) { const _onDragEnd = (result: DropResult, provided: ResponderProvided) => { onDragEnd?.(result, provided) if (result.destination) { @@ -46,6 +48,17 @@ const DraggableList: FC> = ({ } } + const getId = useCallback( + (item: T) => { + if (typeof itemKey === 'function') return itemKey(item) + if (itemKey) return item[itemKey] as Key + if (typeof item === 'string') return item as Key + if (item && typeof item === 'object' && 'id' in item) return item.id as Key + return undefined + }, + [itemKey] + ) + return ( @@ -53,9 +66,9 @@ const DraggableList: FC> = ({
{list.map((item, index) => { - const id = item.id || item + const draggableId = String(getId(item) ?? index) return ( - + {(provided) => (
{ /** 用于更新原始列表状态的函数 */ onUpdate: (newList: T[]) => void /** 用于从列表项中获取唯一ID的属性名或函数 */ - idKey: keyof T | ((item: T) => Key) + itemKey: keyof T | ((item: T) => Key) } /** @@ -19,8 +19,16 @@ interface UseDraggableReorderParams { * @param params - { originalList, filteredList, onUpdate, idKey } * @returns 返回可以直接传递给 DraggableVirtualList 的 props: { onDragEnd, itemKey } */ -export function useDraggableReorder({ originalList, filteredList, onUpdate, idKey }: UseDraggableReorderParams) { - const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey]) +export function useDraggableReorder({ + originalList, + filteredList, + onUpdate, + itemKey +}: UseDraggableReorderParams) { + const getId = useCallback( + (item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)), + [itemKey] + ) // 创建从 item ID 到其在 *原始列表* 中索引的映射 const itemIndexMap = useMemo(() => { diff --git a/src/renderer/src/components/DraggableList/virtual-list.tsx b/src/renderer/src/components/DraggableList/virtual-list.tsx index e915eec1fe..b2efe7c247 100644 --- a/src/renderer/src/components/DraggableList/virtual-list.tsx +++ b/src/renderer/src/components/DraggableList/virtual-list.tsx @@ -208,7 +208,7 @@ const VirtualRow = memo( const draggableId = String(virtualItem.key) return ( diff --git a/src/renderer/src/components/dnd/useDndReorder.ts b/src/renderer/src/components/dnd/useDndReorder.ts index 1b91507cb5..60beaf925a 100644 --- a/src/renderer/src/components/dnd/useDndReorder.ts +++ b/src/renderer/src/components/dnd/useDndReorder.ts @@ -8,7 +8,7 @@ interface UseDndReorderParams { /** 用于更新原始列表状态的函数 */ onUpdate: (newList: T[]) => void /** 用于从列表项中获取唯一ID的属性名或函数 */ - idKey: keyof T | ((item: T) => Key) + itemKey: keyof T | ((item: T) => Key) } /** @@ -18,8 +18,11 @@ interface UseDndReorderParams { * @param params - { originalList, filteredList, onUpdate, idKey } * @returns 返回可以直接传递给 Sortable 的 onSortEnd 回调 */ -export function useDndReorder({ originalList, filteredList, onUpdate, idKey }: UseDndReorderParams) { - const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey]) +export function useDndReorder({ originalList, filteredList, onUpdate, itemKey }: UseDndReorderParams) { + const getId = useCallback( + (item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)), + [itemKey] + ) // 创建从 item ID 到其在 *原始列表* 中索引的映射 const itemIndexMap = useMemo(() => { diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index f9f98476d9..bbd108a26b 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -55,7 +55,7 @@ const McpServersList: FC = () => { originalList: mcpServers, filteredList: filteredMcpServers, onUpdate: updateMcpServers, - idKey: 'id' + itemKey: 'id' }) const scrollRef = useRef(null) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index a5f2b0139e..f04f62def6 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -321,7 +321,7 @@ const ProviderList: FC = () => { originalList: providers, filteredList: filteredProviders, onUpdate: updateProviders, - idKey: 'id' + itemKey: 'id' }) const handleDragStart = useCallback(() => { From 362658339a0856af3d4f012125913ee519af4eea Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Tue, 2 Sep 2025 23:34:08 +0800 Subject: [PATCH 03/12] feat: integrate file selection and upload functionality in KnowledgeFiles component (#9815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: integrate file selection and upload functionality in KnowledgeFiles component - Added useFiles hook to manage file selection. - Updated handleAddFile to utilize the new file selection logic, allowing multiple file uploads. - Improved user experience by handling file uploads asynchronously and logging the results. * feat: enhance file upload interaction in KnowledgeFiles component - Wrapped Dragger component in a div to allow for custom click handling. - Prevented default click behavior to improve user experience when adding files. - Maintained existing file upload functionality while enhancing the UI interaction. * refactor(KnowledgeFiles): 提取文件处理逻辑到独立函数 将重复的文件上传和处理逻辑提取到独立的processFiles函数中,提高代码复用性和可维护性 --------- Co-authored-by: icarus --- .../pages/knowledge/items/KnowledgeFiles.tsx | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx b/src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx index 9dfcd955b6..0e2e26c9f5 100644 --- a/src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx +++ b/src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import Ellipsis from '@renderer/components/Ellipsis' +import { useFiles } from '@renderer/hooks/useFiles' import { useKnowledge } from '@renderer/hooks/useKnowledge' import FileItem from '@renderer/pages/files/FileItem' import StatusIcon from '@renderer/pages/knowledge/components/StatusIcon' @@ -48,6 +49,7 @@ const getDisplayTime = (item: KnowledgeItem) => { const KnowledgeFiles: FC = ({ selectedBase, progressMap, preprocessMap }) => { const { t } = useTranslation() const [windowHeight, setWindowHeight] = useState(window.innerHeight) + const { onSelectFile, selecting } = useFiles({ extensions: fileTypes }) const { base, fileItems, addFiles, refreshItem, removeItem, getProcessingStatus } = useKnowledge( selectedBase.id || '' @@ -71,19 +73,12 @@ const KnowledgeFiles: FC = ({ selectedBase, progressMap, return null } - const handleAddFile = () => { - if (disabled) { + const handleAddFile = async () => { + if (disabled || selecting) { return } - const input = document.createElement('input') - input.type = 'file' - input.multiple = true - input.accept = fileTypes.join(',') - input.onchange = (e) => { - const files = (e.target as HTMLInputElement).files - files && handleDrop(Array.from(files)) - } - input.click() + const selectedFiles = await onSelectFile({ multipleSelections: true }) + processFiles(selectedFiles) } const handleDrop = async (files: File[]) => { @@ -118,8 +113,14 @@ const KnowledgeFiles: FC = ({ selectedBase, progressMap, } }) .filter(({ ext }) => fileTypes.includes(ext)) - const uploadedFiles = await FileManager.uploadFiles(_files) - logger.debug('uploadedFiles', uploadedFiles) + processFiles(_files) + } + } + + const processFiles = async (files: FileMetadata[]) => { + logger.debug('processFiles', files) + if (files.length > 0) { + const uploadedFiles = await FileManager.uploadFiles(files) addFiles(uploadedFiles) } } @@ -150,16 +151,23 @@ const KnowledgeFiles: FC = ({ selectedBase, progressMap, - handleDrop([file as File])} - multiple={true} - accept={fileTypes.join(',')}> -

{t('knowledge.drag_file')}

-

- {t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })} -

-
+
{ + e.stopPropagation() + handleAddFile() + }}> + handleDrop([file as File])} + multiple={true} + accept={fileTypes.join(',')} + openFileDialogOnClick={false}> +

{t('knowledge.drag_file')}

+

+ {t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })} +

+
+
{fileItems.length === 0 ? ( ) : ( From 76ac1bd8f79b606acd46894d1a681ac489d0359d Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 3 Sep 2025 11:43:42 +0800 Subject: [PATCH 04/12] fix: enhance provider selection logic in AssistantService - Updated getProviderByModel function to improve provider selection. - Added fallback logic to return a default or cherryin provider if the specified model provider is not found. - Ensured that the first provider is returned as a last resort, enhancing robustness in provider retrieval. --- src/renderer/src/services/AssistantService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index fe4c040d0d..567c59991c 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -134,8 +134,15 @@ export function getAssistantProvider(assistant: Assistant): Provider { export function getProviderByModel(model?: Model): Provider { const providers = store.getState().llm.providers - const providerId = model ? model.provider : getDefaultProvider().id - return providers.find((p) => p.id === providerId) as Provider + const provider = providers.find((p) => p.id === model?.provider) + + if (!provider) { + const defaultProvider = providers.find((p) => p.id === getDefaultModel()?.provider) + const cherryinProvider = providers.find((p) => p.id === 'cherryin') + return defaultProvider || cherryinProvider || providers[0] + } + + return provider } export function getProviderByModelId(modelId?: string) { From fdee510c8c3ad7b8cf04acc1405954a550074218 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 3 Sep 2025 11:55:56 +0800 Subject: [PATCH 05/12] feat: add 'invalid_model' translation key across multiple languages - Introduced a new translation key 'invalid_model' in English, Japanese, Russian, Chinese (Simplified and Traditional), Greek, Spanish, French, and Portuguese. - Updated the SelectModelButton component to display an error tag when no valid provider is found, enhancing user feedback. --- src/renderer/src/components/Preview/MermaidPreview.tsx | 1 + src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/i18n/translate/el-gr.json | 1 + src/renderer/src/i18n/translate/es-es.json | 1 + src/renderer/src/i18n/translate/fr-fr.json | 1 + src/renderer/src/i18n/translate/pt-pt.json | 1 + src/renderer/src/pages/home/components/SelectModelButton.tsx | 5 ++++- 11 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/Preview/MermaidPreview.tsx b/src/renderer/src/components/Preview/MermaidPreview.tsx index 4c41e3c4b9..60708fbee8 100644 --- a/src/renderer/src/components/Preview/MermaidPreview.tsx +++ b/src/renderer/src/components/Preview/MermaidPreview.tsx @@ -56,6 +56,7 @@ const MermaidPreview = ({ document.body.removeChild(measureEl) } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [diagramId, mermaid, forceRenderKey] ) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index c6b745ac85..365b0797d1 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1552,6 +1552,7 @@ "selected": "Selected tags" }, "function_calling": "Function Calling", + "invalid_model": "Invalid Model", "no_matches": "No models available", "parameter_name": "Parameter Name", "parameter_type": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a00750d71e..51a9f0d799 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1552,6 +1552,7 @@ "selected": "選択済みのタグ" }, "function_calling": "関数呼び出し", + "invalid_model": "無効なモデル", "no_matches": "利用可能なモデルがありません", "parameter_name": "パラメータ名", "parameter_type": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index d6c6d4d198..0fc5d567ac 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1552,6 +1552,7 @@ "selected": "Выбранные теги" }, "function_calling": "Вызов функции", + "invalid_model": "Недействительная модель", "no_matches": "Нет доступных моделей", "parameter_name": "Имя параметра", "parameter_type": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index bf3215a35c..feb7338be3 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1552,6 +1552,7 @@ "selected": "已选标签" }, "function_calling": "函数调用", + "invalid_model": "无效模型", "no_matches": "无可用模型", "parameter_name": "参数名称", "parameter_type": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 93881b95f9..b8337ac2c1 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1552,6 +1552,7 @@ "selected": "已選標籤" }, "function_calling": "函數調用", + "invalid_model": "無效模型", "no_matches": "無可用模型", "parameter_name": "參數名稱", "parameter_type": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 43602afe6f..e5cb29ec8a 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1550,6 +1550,7 @@ "selected": "Επιλεγμένη ετικέτα" }, "function_calling": "Ξεχωριστική Κλήση Συναρτήσεων", + "invalid_model": "Μη έγκυρο μοντέλο", "no_matches": "Δεν υπάρχουν διαθέσιμα μοντέλα", "parameter_name": "Όνομα παραμέτρου", "parameter_type": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 715401149f..6924a3731c 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1550,6 +1550,7 @@ "selected": "Etiquetas seleccionadas" }, "function_calling": "Llamada a función", + "invalid_model": "Modelo inválido", "no_matches": "No hay modelos disponibles", "parameter_name": "Nombre del parámetro", "parameter_type": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 794c76a35e..d3e216b809 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1550,6 +1550,7 @@ "selected": "Étiquette sélectionnée" }, "function_calling": "Appel de fonction", + "invalid_model": "Modèle invalide", "no_matches": "Aucun modèle disponible", "parameter_name": "Nom du paramètre", "parameter_type": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 57d26464e8..1595318214 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1550,6 +1550,7 @@ "selected": "Etiqueta selecionada" }, "function_calling": "Chamada de função", + "invalid_model": "Modelo inválido", "no_matches": "Nenhum modelo disponível", "parameter_name": "Nome do parâmetro", "parameter_type": { diff --git a/src/renderer/src/pages/home/components/SelectModelButton.tsx b/src/renderer/src/pages/home/components/SelectModelButton.tsx index 214ceb7185..bd6af86f7b 100644 --- a/src/renderer/src/pages/home/components/SelectModelButton.tsx +++ b/src/renderer/src/pages/home/components/SelectModelButton.tsx @@ -4,8 +4,9 @@ import { isLocalAi } from '@renderer/config/env' import { isEmbeddingModel, isRerankModel, isWebSearchModel } from '@renderer/config/models' import { useAssistant } from '@renderer/hooks/useAssistant' import { getProviderName } from '@renderer/services/ProviderService' +import { useAppSelector } from '@renderer/store' import { Assistant, Model } from '@renderer/types' -import { Button } from 'antd' +import { Button, Tag } from 'antd' import { ChevronsUpDown } from 'lucide-react' import { FC, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' @@ -19,6 +20,7 @@ const SelectModelButton: FC = ({ assistant }) => { const { model, updateAssistant } = useAssistant(assistant.id) const { t } = useTranslation() const timerRef = useRef(undefined) + const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === model?.provider)) const modelFilter = (model: Model) => !isEmbeddingModel(model) && !isRerankModel(model) @@ -60,6 +62,7 @@ const SelectModelButton: FC = ({ assistant }) => { + {!provider && {t('models.invalid_model')}} ) } From 9f7d2be463b8ecd1faeaa153f8769e5a1403e59e Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Wed, 3 Sep 2025 12:45:33 +0800 Subject: [PATCH 06/12] refactor(electron.vite.config.ts): streamline external dependencies and improve build configuration (#9835) - Removed hardcoded external dependencies and replaced them with dynamic extraction from package.json. - Cleaned up the configuration for better maintainability and flexibility in managing dependencies. --- electron.vite.config.ts | 17 +++-------------- package.json | 1 - 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index cf3ce3b5b0..7cf20902b9 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -4,6 +4,8 @@ import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import { resolve } from 'path' import { visualizer } from 'rollup-plugin-visualizer' +import pkg from './package.json' assert { type: 'json' } + const visualizerPlugin = (type: 'renderer' | 'main') => { return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : [] } @@ -26,20 +28,7 @@ export default defineConfig({ }, build: { rollupOptions: { - external: [ - '@libsql/client', - 'bufferutil', - 'utf-8-validate', - 'jsdom', - 'electron', - 'graceful-fs', - 'selection-hook', - '@napi-rs/system-ocr', - '@strongtz/win32-arm64-msvc', - 'os-proxy-config', - 'sharp', - 'turndown' - ], + external: ['bufferutil', 'utf-8-validate', 'electron', ...Object.keys(pkg.dependencies)], output: { manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包 inlineDynamicImports: true // 内联所有动态导入,这是关键配置 diff --git a/package.json b/package.json index 7c6826e47c..a6380c1ed3 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch", "@strongtz/win32-arm64-msvc": "^0.4.7", "graceful-fs": "^4.2.11", - "htmlparser2": "^10.0.0", "jsdom": "26.1.0", "node-stream-zip": "^1.15.0", "officeparser": "^4.2.0", From b725400428dde97802c1e060761d2fc4259e72de Mon Sep 17 00:00:00 2001 From: co63oc Date: Wed, 3 Sep 2025 13:14:06 +0800 Subject: [PATCH 07/12] fix typos (#9831) --- resources/data/agents-en.json | 8 ++++---- src/renderer/src/types/ocr.ts | 2 +- .../src/utils/__tests__/markdownConverter.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/data/agents-en.json b/resources/data/agents-en.json index c8c4bab393..b594f32a44 100644 --- a/resources/data/agents-en.json +++ b/resources/data/agents-en.json @@ -2089,7 +2089,7 @@ "Design", "Education" ], - "prompt": "I want you to act as a Graphviz DOT generator, an expert to create meaningful diagrams. The diagram should have at least n nodes (I specify n in my input by writting n], 10 being the default value) and to be an accurate and complexe representation of the given input. Each node is indexed by a number to reduce the size of the output, should not include any styling, and with layout=neato, overlap=false, node shape=rectangle] as parameters. The code should be valid, bugless and returned on a single line, without any explanation. Provide a clear and organized diagram, the relationships between the nodes have to make sense for an expert of that input. My first diagram is: \"The water cycle 8]\".\n\n", + "prompt": "I want you to act as a Graphviz DOT generator, an expert to create meaningful diagrams. The diagram should have at least n nodes (I specify n in my input by writing n], 10 being the default value) and to be an accurate and complex representation of the given input. Each node is indexed by a number to reduce the size of the output, should not include any styling, and with layout=neato, overlap=false, node shape=rectangle] as parameters. The code should be valid, bugless and returned on a single line, without any explanation. Provide a clear and organized diagram, the relationships between the nodes have to make sense for an expert of that input. My first diagram is: \"The water cycle 8]\".\n\n", "description": "Generate meaningful charts." }, { @@ -2148,7 +2148,7 @@ "Career", "Business" ], - "prompt": "Please acknowledge my following request. Please respond to me as a product manager. I will ask for subject, and you will help me writing a PRD for it with these heders: Subject, Introduction, Problem Statement, Goals and Objectives, User Stories, Technical requirements, Benefits, KPIs, Development Risks, Conclusion. Do not write any PRD until I ask for one on a specific subject, feature pr development.\n\n", + "prompt": "Please acknowledge my following request. Please respond to me as a product manager. I will ask for subject, and you will help me writing a PRD for it with these headers: Subject, Introduction, Problem Statement, Goals and Objectives, User Stories, Technical requirements, Benefits, KPIs, Development Risks, Conclusion. Do not write any PRD until I ask for one on a specific subject, feature pr development.\n\n", "description": "Help draft the Product Requirements Document." }, { @@ -2159,7 +2159,7 @@ "Entertainment", "General" ], - "prompt": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkeness I mentionned. Do not write explanations on replies. My first sentence is \"how are you?", + "prompt": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkenness I mentioned. Do not write explanations on replies. My first sentence is \"how are you?", "description": "Mimic the speech pattern of a drunk person." }, { @@ -3517,7 +3517,7 @@ "Tools", "Copywriting" ], - "prompt": "I want you to act as a scientific manuscript matcher. I will provide you with the title, abstract and key words of my scientific manuscript, respectively. Your task is analyzing my title, abstract and key words synthetically to find the most related, reputable journals for potential publication of my research based on an analysis of tens of millions of citation connections in database, such as Web of Science, Pubmed, Scopus, ScienceDirect and so on. You only need to provide me with the 15 most suitable journals. Your reply should include the name of journal, the cooresponding match score (The full score is ten). I want you to reply in text-based excel sheet and sort by matching scores in reverse order.\nMy title is \"XXX\" My abstract is \"XXX\" My key words are \"XXX\"\n\n", + "prompt": "I want you to act as a scientific manuscript matcher. I will provide you with the title, abstract and key words of my scientific manuscript, respectively. Your task is analyzing my title, abstract and key words synthetically to find the most related, reputable journals for potential publication of my research based on an analysis of tens of millions of citation connections in database, such as Web of Science, Pubmed, Scopus, ScienceDirect and so on. You only need to provide me with the 15 most suitable journals. Your reply should include the name of journal, the corresponding match score (The full score is ten). I want you to reply in text-based excel sheet and sort by matching scores in reverse order.\nMy title is \"XXX\" My abstract is \"XXX\" My key words are \"XXX\"\n\n", "description": "" }, { diff --git a/src/renderer/src/types/ocr.ts b/src/renderer/src/types/ocr.ts index cd279e62f5..692ae7283d 100644 --- a/src/renderer/src/types/ocr.ts +++ b/src/renderer/src/types/ocr.ts @@ -103,7 +103,7 @@ export const isBuiltinOcrProvider = (p: OcrProvider): p is BuiltinOcrProvider => return isBuiltinOcrProviderId(p.id) } -// Not sure compatiable api endpoint exists. May not support custom ocr provider +// Not sure compatible api endpoint exists. May not support custom ocr provider export type CustomOcrProvider = OcrProvider & { id: Exclude } diff --git a/src/renderer/src/utils/__tests__/markdownConverter.test.ts b/src/renderer/src/utils/__tests__/markdownConverter.test.ts index 2224f7f0e7..b6928d3d89 100644 --- a/src/renderer/src/utils/__tests__/markdownConverter.test.ts +++ b/src/renderer/src/utils/__tests__/markdownConverter.test.ts @@ -26,7 +26,7 @@ describe('markdownConverter', () => { it('should convert task list HTML back to Markdown with label', () => { const html = - '
' + '
' const result = htmlToMarkdown(html) expect(result).toBe('- [ ] abcd\n\n- [x] efgh') }) @@ -361,7 +361,7 @@ describe('markdownConverter', () => { }) describe('markdown image', () => { - it('should convert markdown iamge to HTML img tag', () => { + it('should convert markdown image to HTML img tag', () => { const markdown = '![foo](train.jpg)' const result = markdownToHtml(markdown) expect(result).toBe('

foo

\n') From 6d1f3a5729e0edb0f7373c070beab518cf21c593 Mon Sep 17 00:00:00 2001 From: one Date: Wed, 3 Sep 2025 14:19:06 +0800 Subject: [PATCH 08/12] fix(Markdown): regex for style (#9839) --- src/renderer/src/pages/home/Markdown/Markdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index 6ff47e57a8..af00712ca2 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -140,7 +140,7 @@ const Markdown: FC = ({ block, postProcess }) => { } as Partial }, [block.id]) - if (messageContent.includes('