diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 705d9c9b40..dff0a94a37 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 1559ff7554..c84bc483d4 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", 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/main/services/ObsidianVaultService.ts b/src/main/services/ObsidianVaultService.ts index 93c5421eef..d90def8524 100644 --- a/src/main/services/ObsidianVaultService.ts +++ b/src/main/services/ObsidianVaultService.ts @@ -32,7 +32,8 @@ class ObsidianVaultService { ) } else { // Linux - this.obsidianConfigPath = path.join(app.getPath('home'), '.config', 'obsidian', 'obsidian.json') + this.obsidianConfigPath = this.resolveLinuxObsidianConfigPath() + logger.debug(`Resolved Obsidian config path (linux): ${this.obsidianConfigPath}`) } } @@ -164,6 +165,57 @@ class ObsidianVaultService { return [] } } + + /** + * 在 Linux 下解析 Obsidian 配置文件路径,兼容多种安装方式。 + * 优先返回第一个存在的路径;若均不存在,则返回 XDG 默认路径。 + */ + private resolveLinuxObsidianConfigPath(): string { + const home = app.getPath('home') + const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config') + + // 常见目录名与文件名大小写差异做兼容 + const configDirs = ['obsidian', 'Obsidian'] + const fileNames = ['obsidian.json', 'Obsidian.json'] + + const candidates: string[] = [] + + // 1) AppImage/DEB(XDG 标准路径) + for (const dir of configDirs) { + for (const file of fileNames) { + candidates.push(path.join(xdgConfigHome, dir, file)) + } + } + + // 2) Snap 安装: + // - 常见:~/snap/obsidian/current/.config/obsidian/obsidian.json + // - 兼容:~/snap/obsidian/common/.config/obsidian/obsidian.json + for (const dir of configDirs) { + for (const file of fileNames) { + candidates.push(path.join(home, 'snap', 'obsidian', 'current', '.config', dir, file)) + candidates.push(path.join(home, 'snap', 'obsidian', 'common', '.config', dir, file)) + } + } + + // 3) Flatpak 安装:~/.var/app/md.obsidian.Obsidian/config/obsidian/obsidian.json + for (const dir of configDirs) { + for (const file of fileNames) { + candidates.push(path.join(home, '.var', 'app', 'md.obsidian.Obsidian', 'config', dir, file)) + } + } + + const existing = candidates.find((p) => { + try { + return fs.existsSync(p) + } catch { + return false + } + }) + + if (existing) return existing + + return path.join(xdgConfigHome, 'obsidian', 'obsidian.json') + } } export default ObsidianVaultService 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 6d7a0d3bc7..599e2d5afd 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: React.ReactNode - expanded?: boolean - wrapped?: boolean 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/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/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/components/TooltipIcons/HelpTooltip.tsx b/src/renderer/src/components/TooltipIcons/HelpTooltip.tsx new file mode 100644 index 0000000000..6ce75d5140 --- /dev/null +++ b/src/renderer/src/components/TooltipIcons/HelpTooltip.tsx @@ -0,0 +1,20 @@ +import { Tooltip, TooltipProps } from 'antd' +import { HelpCircle } from 'lucide-react' + +type InheritedTooltipProps = Omit + +interface HelpTooltipProps extends InheritedTooltipProps { + iconColor?: string + iconSize?: string | number + iconStyle?: React.CSSProperties +} + +const HelpTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: HelpTooltipProps) => { + return ( + + + + ) +} + +export default HelpTooltip diff --git a/src/renderer/src/components/InfoTooltip.tsx b/src/renderer/src/components/TooltipIcons/InfoTooltip.tsx similarity index 89% rename from src/renderer/src/components/InfoTooltip.tsx rename to src/renderer/src/components/TooltipIcons/InfoTooltip.tsx index 02c64c4d2d..7a0e608a31 100644 --- a/src/renderer/src/components/InfoTooltip.tsx +++ b/src/renderer/src/components/TooltipIcons/InfoTooltip.tsx @@ -9,7 +9,7 @@ interface InfoTooltipProps extends InheritedTooltipProps { iconStyle?: React.CSSProperties } -const InfoTooltip = ({ iconColor = 'var(--color-text-3)', iconSize = 14, iconStyle, ...rest }: InfoTooltipProps) => { +const InfoTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: InfoTooltipProps) => { return ( diff --git a/src/renderer/src/components/WarnTooltip.tsx b/src/renderer/src/components/TooltipIcons/WarnTooltip.tsx similarity index 100% rename from src/renderer/src/components/WarnTooltip.tsx rename to src/renderer/src/components/TooltipIcons/WarnTooltip.tsx diff --git a/src/renderer/src/components/__tests__/InfoTooltip.test.tsx b/src/renderer/src/components/TooltipIcons/__tests__/InfoTooltip.test.tsx similarity index 100% rename from src/renderer/src/components/__tests__/InfoTooltip.test.tsx rename to src/renderer/src/components/TooltipIcons/__tests__/InfoTooltip.test.tsx diff --git a/src/renderer/src/components/__tests__/__snapshots__/InfoTooltip.test.tsx.snap b/src/renderer/src/components/TooltipIcons/__tests__/__snapshots__/InfoTooltip.test.tsx.snap similarity index 100% rename from src/renderer/src/components/__tests__/__snapshots__/InfoTooltip.test.tsx.snap rename to src/renderer/src/components/TooltipIcons/__tests__/__snapshots__/InfoTooltip.test.tsx.snap diff --git a/src/renderer/src/components/TooltipIcons/index.ts b/src/renderer/src/components/TooltipIcons/index.ts new file mode 100644 index 0000000000..6c32f23c48 --- /dev/null +++ b/src/renderer/src/components/TooltipIcons/index.ts @@ -0,0 +1,3 @@ +export { default as HelpTooltip } from './HelpTooltip' +export { default as InfoTooltip } from './InfoTooltip' +export { default as WarnTooltip } from './WarnTooltip' 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/hooks/useShowWorkspace.ts b/src/renderer/src/hooks/useShowWorkspace.ts new file mode 100644 index 0000000000..187c9025dd --- /dev/null +++ b/src/renderer/src/hooks/useShowWorkspace.ts @@ -0,0 +1,14 @@ +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { selectNotesSettings, updateNotesSettings } from '@renderer/store/note' + +export function useShowWorkspace() { + const dispatch = useAppDispatch() + const settings = useAppSelector(selectNotesSettings) + const showWorkspace = settings.showWorkspace + + return { + showWorkspace, + setShowWorkspace: (show: boolean) => dispatch(updateNotesSettings({ showWorkspace: show })), + toggleShowWorkspace: () => dispatch(updateNotesSettings({ showWorkspace: !showWorkspace })) + } +} diff --git a/src/renderer/src/hooks/useStore.ts b/src/renderer/src/hooks/useStore.ts index 3115813ed4..1b731e74c7 100644 --- a/src/renderer/src/hooks/useStore.ts +++ b/src/renderer/src/hooks/useStore.ts @@ -3,10 +3,8 @@ import { setAssistantsTabSortType, setShowAssistants, setShowTopics, - setShowWorkspace, toggleShowAssistants, - toggleShowTopics, - toggleShowWorkspace + toggleShowTopics } from '@renderer/store/settings' import { AssistantsSortType } from '@renderer/types' @@ -41,14 +39,3 @@ export function useAssistantsTabSortType() { setAssistantsTabSortType: (sortType: AssistantsSortType) => dispatch(setAssistantsTabSortType(sortType)) } } - -export function useShowWorkspace() { - const showWorkspace = useAppSelector((state) => state.settings.showWorkspace) - const dispatch = useAppDispatch() - - return { - showWorkspace, - setShowWorkspace: (show: boolean) => dispatch(setShowWorkspace(show)), - toggleShowWorkspace: () => dispatch(toggleShowWorkspace()) - } -} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8eafbdce02..2898a58d5e 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -538,7 +538,10 @@ "tip": "The run button will be displayed in the toolbar of executable code blocks, please do not execute dangerous code!", "title": "Code Execution" }, - "code_image_tools": "Enable preview tools", + "code_image_tools": { + "label": "Enable preview tools", + "tip": "Enable preview tools for images rendered from code blocks such as mermaid" + }, "code_wrappable": "Code block wrappable", "context_count": { "label": "Context", @@ -1560,6 +1563,7 @@ "selected": "Selected tags" }, "function_calling": "Function Calling", + "invalid_model": "Invalid Model", "no_matches": "No models available", "parameter_name": "Parameter Name", "parameter_type": { @@ -1633,6 +1637,7 @@ "only_markdown": "Only Markdown files are supported", "only_one_file_allowed": "Only one file can be uploaded", "open_folder": "Open an external folder", + "open_outside": "Open from external", "rename": "Rename", "rename_changed": "Due to security policies, the filename has been changed from {{original}} to {{final}}", "save": "Save to Notes", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 8cebd30160..9503f0ca5f 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -538,7 +538,10 @@ "tip": "実行可能なコードブロックのツールバーには実行ボタンが表示されます。危険なコードを実行しないでください!", "title": "コード実行" }, - "code_image_tools": "プレビューツールを有効にする", + "code_image_tools": { + "label": "プレビューツールを有効にする", + "tip": "mermaid などのコードブロックから生成された画像に対してプレビューツールを有効にする" + }, "code_wrappable": "コードブロック折り返し", "context_count": { "label": "コンテキスト", @@ -1560,6 +1563,7 @@ "selected": "選択済みのタグ" }, "function_calling": "関数呼び出し", + "invalid_model": "無効なモデル", "no_matches": "利用可能なモデルがありません", "parameter_name": "パラメータ名", "parameter_type": { @@ -1633,6 +1637,7 @@ "only_markdown": "Markdown ファイルのみをアップロードできます", "only_one_file_allowed": "アップロードできるファイルは1つだけです", "open_folder": "外部フォルダーを開きます", + "open_outside": "外部から開く", "rename": "名前の変更", "rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました", "save": "メモに保存する", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 81061cb4f3..731c507c17 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -538,7 +538,10 @@ "tip": "Выполнение кода в блоке кода возможно, но не рекомендуется выполнять опасный код!", "title": "Выполнение кода" }, - "code_image_tools": "Включить инструменты предпросмотра", + "code_image_tools": { + "label": "Включить инструменты предпросмотра", + "tip": "Включить инструменты предпросмотра для изображений, сгенерированных из блоков кода (например mermaid)" + }, "code_wrappable": "Блок кода можно переносить", "context_count": { "label": "Контекст", @@ -1560,6 +1563,7 @@ "selected": "Выбранные теги" }, "function_calling": "Вызов функции", + "invalid_model": "Недействительная модель", "no_matches": "Нет доступных моделей", "parameter_name": "Имя параметра", "parameter_type": { @@ -1633,6 +1637,7 @@ "only_markdown": "Только Markdown", "only_one_file_allowed": "Можно загрузить только один файл", "open_folder": "Откройте внешнюю папку", + "open_outside": "открыть снаружи", "rename": "переименовать", "rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}", "save": "Сохранить в заметки", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8be613d86c..88f8655fde 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -538,7 +538,10 @@ "tip": "可执行的代码块工具栏中会显示运行按钮,注意不要执行危险代码!", "title": "代码执行" }, - "code_image_tools": "启用预览工具", + "code_image_tools": { + "label": "启用预览工具", + "tip": "为 mermaid 等代码块渲染后的图像启用预览工具" + }, "code_wrappable": "代码块可换行", "context_count": { "label": "上下文数", @@ -1560,6 +1563,7 @@ "selected": "已选标签" }, "function_calling": "函数调用", + "invalid_model": "无效模型", "no_matches": "无可用模型", "parameter_name": "参数名称", "parameter_type": { @@ -1633,6 +1637,7 @@ "only_markdown": "仅支持 Markdown 格式", "only_one_file_allowed": "只能上传一个文件", "open_folder": "打开外部文件夹", + "open_outside": "从外部打开", "rename": "重命名", "rename_changed": "由于安全策略,文件名已从 {{original}} 更改为 {{final}}", "save": "保存到笔记", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index bf3e9a1603..92e486bea9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -538,7 +538,10 @@ "tip": "可執行的程式碼塊工具欄中會顯示運行按鈕,注意不要執行危險程式碼!", "title": "程式碼執行" }, - "code_image_tools": "啟用預覽工具", + "code_image_tools": { + "label": "啟用預覽工具", + "tip": "為 mermaid 等程式碼區塊渲染後的圖像啟用預覽工具" + }, "code_wrappable": "程式碼區塊可自動換行", "context_count": { "label": "上下文", @@ -1560,6 +1563,7 @@ "selected": "已選標籤" }, "function_calling": "函數調用", + "invalid_model": "無效模型", "no_matches": "無可用模型", "parameter_name": "參數名稱", "parameter_type": { @@ -1633,6 +1637,7 @@ "only_markdown": "僅支援 Markdown 格式", "only_one_file_allowed": "只能上傳一個文件", "open_folder": "打開外部文件夾", + "open_outside": "從外部打開", "rename": "重命名", "rename_changed": "由於安全策略,文件名已從 {{original}} 更改為 {{final}}", "save": "儲存到筆記", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index bab56f93b2..6ff2980f9c 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -677,6 +677,7 @@ "model_placeholder": "Επιλέξτε το μοντέλο που θα χρησιμοποιήσετε", "model_required": "Επιλέξτε μοντέλο", "select_folder": "Επιλογή φακέλου", + "supported_providers": "υποστηριζόμενοι πάροχοι", "title": "Εργαλεία κώδικα", "update_options": "Ενημέρωση επιλογών", "working_directory": "κατάλογος εργασίας" @@ -1319,7 +1320,8 @@ "delete": { "content": "Η διαγραφή της ομάδας θα διαγράψει τις ερωτήσεις των χρηστών και όλες τις απαντήσεις του αστρόναυτη", "title": "Διαγραφή ομάδας" - } + }, + "retry_failed": "Αποτυχημένο μήνυμα επανάληψης" }, "ignore": { "knowledge": { @@ -1550,6 +1552,7 @@ "selected": "Επιλεγμένη ετικέτα" }, "function_calling": "Ξεχωριστική Κλήση Συναρτήσεων", + "invalid_model": "Μη έγκυρο μοντέλο", "no_matches": "Δεν υπάρχουν διαθέσιμα μοντέλα", "parameter_name": "Όνομα παραμέτρου", "parameter_type": { @@ -1619,9 +1622,13 @@ "new_folder": "Νέος φάκελος", "new_note": "Δημιουργία νέας σημείωσης", "no_content_to_copy": "Δεν υπάρχει περιεχόμενο προς αντιγραφή", + "no_file_selected": "Επιλέξτε το αρχείο για μεταφόρτωση", "only_markdown": "Υποστηρίζεται μόνο η μορφή Markdown", + "only_one_file_allowed": "Μπορείτε να ανεβάσετε μόνο ένα αρχείο", "open_folder": "Άνοιγμα εξωτερικού φακέλου", + "open_outside": "Από το εξωτερικό", "rename": "μετονομασία", + "rename_changed": "Λόγω πολιτικής ασφάλειας, το όνομα του αρχείου έχει αλλάξει από {{original}} σε {{final}}", "save": "αποθήκευση στις σημειώσεις", "settings": { "data": { @@ -3343,6 +3350,8 @@ "label": "Καταγραφή στοιχείων στο grid" }, "input": { + "confirm_delete_message": "Επιβεβαίωση πριν τη διαγραφή μηνύματος", + "confirm_regenerate_message": "Επιβεβαίωση πριν από την επαναδημιουργία του μηνύματος", "enable_quick_triggers": "Ενεργοποίηση των '/' και '@' για γρήγορη πρόσβαση σε μενού", "paste_long_text_as_file": "Επικόλληση μεγάλου κειμένου ως αρχείο", "paste_long_text_threshold": "Όριο μεγάλου κειμένου", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index c09d7e16ea..a2a14f7464 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -677,6 +677,7 @@ "model_placeholder": "Seleccionar el modelo que se va a utilizar", "model_required": "Seleccione el modelo", "select_folder": "Seleccionar carpeta", + "supported_providers": "Proveedores de servicios compatibles", "title": "Herramientas de código", "update_options": "Opciones de actualización", "working_directory": "directorio de trabajo" @@ -1319,7 +1320,8 @@ "delete": { "content": "Eliminar el mensaje del grupo eliminará la pregunta del usuario y todas las respuestas del asistente", "title": "Eliminar mensaje del grupo" - } + }, + "retry_failed": "Reintentar el mensaje con error" }, "ignore": { "knowledge": { @@ -1550,6 +1552,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": { @@ -1619,9 +1622,13 @@ "new_folder": "Nueva carpeta", "new_note": "Crear nota nueva", "no_content_to_copy": "No hay contenido para copiar", + "no_file_selected": "Por favor, seleccione el archivo a subir", "only_markdown": "Solo se admite el formato Markdown", + "only_one_file_allowed": "solo se puede subir un archivo", "open_folder": "abrir carpeta externa", + "open_outside": "Abrir desde el exterior", "rename": "renombrar", + "rename_changed": "Debido a políticas de seguridad, el nombre del archivo ha cambiado de {{original}} a {{final}}", "save": "Guardar en notas", "settings": { "data": { @@ -3343,6 +3350,8 @@ "label": "Desencadenante de detalles de cuadrícula" }, "input": { + "confirm_delete_message": "Confirmar antes de eliminar mensaje", + "confirm_regenerate_message": "confirmar antes de regenerar el mensaje", "enable_quick_triggers": "Habilitar menú rápido con '/' y '@'", "paste_long_text_as_file": "Pegar texto largo como archivo", "paste_long_text_threshold": "Límite de longitud de texto largo", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index b91a50aead..8c65585ce5 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -677,6 +677,7 @@ "model_placeholder": "Sélectionnez le modèle à utiliser", "model_required": "Veuillez sélectionner le modèle", "select_folder": "Sélectionner le dossier", + "supported_providers": "fournisseurs pris en charge", "title": "Outils de code", "update_options": "Options de mise à jour", "working_directory": "répertoire de travail" @@ -1319,7 +1320,8 @@ "delete": { "content": "La suppression du groupe de messages supprimera les questions des utilisateurs et toutes les réponses des assistants", "title": "Supprimer le groupe de messages" - } + }, + "retry_failed": "message d'erreur de nouvelle tentative" }, "ignore": { "knowledge": { @@ -1550,6 +1552,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": { @@ -1619,9 +1622,13 @@ "new_folder": "Nouveau dossier", "new_note": "Nouvelle note", "no_content_to_copy": "Aucun contenu à copier", + "no_file_selected": "Veuillez sélectionner le fichier à télécharger", "only_markdown": "uniquement le format Markdown est pris en charge", + "only_one_file_allowed": "On ne peut télécharger qu'un seul fichier", "open_folder": "ouvrir le dossier externe", + "open_outside": "Ouvrir depuis l'extérieur", "rename": "renommer", + "rename_changed": "En raison de la politique de sécurité, le nom du fichier a été changé de {{original}} à {{final}}", "save": "sauvegarder dans les notes", "settings": { "data": { @@ -3343,6 +3350,8 @@ "label": "Déclencheur de popover de la grille" }, "input": { + "confirm_delete_message": "Confirmer avant de supprimer le message", + "confirm_regenerate_message": "Confirmer avant de régénérer le message", "enable_quick_triggers": "Activer les menus rapides avec '/' et '@'", "paste_long_text_as_file": "Coller le texte long sous forme de fichier", "paste_long_text_threshold": "Seuil de longueur de texte", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index c560da1283..2ece88f325 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -677,6 +677,7 @@ "model_placeholder": "Selecione o modelo a ser utilizado", "model_required": "Selecione o modelo", "select_folder": "Selecionar pasta", + "supported_providers": "Provedores de serviço suportados", "title": "Ferramenta de código", "update_options": "Opções de atualização", "working_directory": "diretório de trabalho" @@ -1319,7 +1320,8 @@ "delete": { "content": "Excluir mensagens de grupo removerá as perguntas dos usuários e todas as respostas do assistente", "title": "Excluir mensagens de grupo" - } + }, + "retry_failed": "Repetir mensagem com erro" }, "ignore": { "knowledge": { @@ -1550,6 +1552,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": { @@ -1619,9 +1622,13 @@ "new_folder": "Nova pasta", "new_note": "Nova nota", "no_content_to_copy": "Não há conteúdo para copiar", + "no_file_selected": "Selecione o arquivo a ser enviado", "only_markdown": "Apenas o formato Markdown é suportado", + "only_one_file_allowed": "só é possível enviar um arquivo", "open_folder": "Abrir pasta externa", + "open_outside": "Abrir externamente", "rename": "renomear", + "rename_changed": "Devido às políticas de segurança, o nome do arquivo foi alterado de {{original}} para {{final}}", "save": "salvar em notas", "settings": { "data": { @@ -3343,6 +3350,8 @@ "label": "Disparador de detalhes da grade" }, "input": { + "confirm_delete_message": "confirmar antes de excluir a mensagem", + "confirm_regenerate_message": "Confirmar antes de regenerar a mensagem", "enable_quick_triggers": "Ativar menu rápido com '/' e '@'", "paste_long_text_as_file": "Colar texto longo como arquivo", "paste_long_text_threshold": "Limite de texto longo", diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index b77358f821..f2d65e9a7c 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -139,7 +139,7 @@ const Markdown: FC = ({ block, postProcess }) => { } as Partial }, [block.id]) - if (messageContent.includes('