From da5badc189ba699c176965513690ee7ca5d21001 Mon Sep 17 00:00:00 2001 From: one Date: Tue, 8 Jul 2025 17:05:40 +0800 Subject: [PATCH] perf: draggable virtual list (#7904) * perf(TopicsTab): use DraggableVirtualList for the topic list - Add a DraggableVirtualList implemented using react-virtual - Rename DragableList to DraggableList - Add tests * refactor: improve props, fix drag area --- .../src/components/DraggableList/index.tsx | 2 + .../index.tsx => DraggableList/list.tsx} | 4 +- .../components/DraggableList/virtual-list.tsx | 212 ++++++++++++++++++ .../src/components/Scrollbar/index.tsx | 2 +- ...leList.test.tsx => DraggableList.test.tsx} | 52 ++--- .../__tests__/DraggableVirtualList.test.tsx | 164 ++++++++++++++ ...t.tsx.snap => DraggableList.test.tsx.snap} | 2 +- .../DraggableVirtualList.test.tsx.snap | 91 ++++++++ src/renderer/src/components/app/Sidebar.tsx | 6 +- .../agents/components/ManageAgentsPopup.tsx | 6 +- .../src/pages/home/Tabs/AssistantsTab.tsx | 10 +- .../src/pages/home/Tabs/TopicsTab.tsx | 151 ++++++------- .../src/pages/knowledge/KnowledgePage.tsx | 6 +- .../paintings/components/PaintingsList.tsx | 6 +- .../AssistantRegularPromptsSettings.tsx | 6 +- .../settings/MCPSettings/McpServersList.tsx | 6 +- .../settings/QuickPhraseSettings/index.tsx | 6 +- 17 files changed, 597 insertions(+), 135 deletions(-) create mode 100644 src/renderer/src/components/DraggableList/index.tsx rename src/renderer/src/components/{DragableList/index.tsx => DraggableList/list.tsx} (97%) create mode 100644 src/renderer/src/components/DraggableList/virtual-list.tsx rename src/renderer/src/components/__tests__/{DragableList.test.tsx => DraggableList.test.tsx} (87%) create mode 100644 src/renderer/src/components/__tests__/DraggableVirtualList.test.tsx rename src/renderer/src/components/__tests__/__snapshots__/{DragableList.test.tsx.snap => DraggableList.test.tsx.snap} (95%) create mode 100644 src/renderer/src/components/__tests__/__snapshots__/DraggableVirtualList.test.tsx.snap diff --git a/src/renderer/src/components/DraggableList/index.tsx b/src/renderer/src/components/DraggableList/index.tsx new file mode 100644 index 0000000000..de98dd00d5 --- /dev/null +++ b/src/renderer/src/components/DraggableList/index.tsx @@ -0,0 +1,2 @@ +export { default as DraggableList } from './list' +export { default as DraggableVirtualList } from './virtual-list' diff --git a/src/renderer/src/components/DragableList/index.tsx b/src/renderer/src/components/DraggableList/list.tsx similarity index 97% rename from src/renderer/src/components/DragableList/index.tsx rename to src/renderer/src/components/DraggableList/list.tsx index cc281e7b01..0f23a69978 100644 --- a/src/renderer/src/components/DragableList/index.tsx +++ b/src/renderer/src/components/DraggableList/list.tsx @@ -23,7 +23,7 @@ interface Props { droppableProps?: Partial } -const DragableList: FC> = ({ +const DraggableList: FC> = ({ children, list, style, @@ -78,4 +78,4 @@ const DragableList: FC> = ({ ) } -export default DragableList +export default DraggableList diff --git a/src/renderer/src/components/DraggableList/virtual-list.tsx b/src/renderer/src/components/DraggableList/virtual-list.tsx new file mode 100644 index 0000000000..b8e51642e9 --- /dev/null +++ b/src/renderer/src/components/DraggableList/virtual-list.tsx @@ -0,0 +1,212 @@ +import { + DragDropContext, + Draggable, + Droppable, + DroppableProps, + DropResult, + OnDragEndResponder, + OnDragStartResponder, + ResponderProvided +} from '@hello-pangea/dnd' +import Scrollbar from '@renderer/components/Scrollbar' +import { droppableReorder } from '@renderer/utils' +import { useVirtualizer } from '@tanstack/react-virtual' +import { type Key, memo, useCallback, useRef } from 'react' + +/** + * 泛型 Props,用于配置 DraggableVirtualList。 + * + * @template T 列表元素的类型 + * @property {string} [className] 根节点附加 class + * @property {React.CSSProperties} [style] 根节点附加样式 + * @property {React.CSSProperties} [itemStyle] 元素内容区域的附加样式 + * @property {React.CSSProperties} [itemContainerStyle] 元素拖拽容器的附加样式 + * @property {Partial} [droppableProps] 透传给 Droppable 的额外配置 + * @property {(list: T[]) => void} onUpdate 拖拽排序完成后的回调,返回新的列表顺序 + * @property {OnDragStartResponder} [onDragStart] 开始拖拽时的回调 + * @property {OnDragEndResponder} [onDragEnd] 结束拖拽时的回调 + * @property {T[]} list 渲染的数据源 + * @property {(index: number) => Key} [itemKey] 提供给虚拟列表的行 key,若不提供默认使用 index + * @property {number} [overscan=5] 前后额外渲染的行数,提升快速滚动时的体验 + * @property {(item: T, index: number) => React.ReactNode} children 列表项渲染函数 + */ +interface DraggableVirtualListProps { + ref?: React.Ref + className?: string + style?: React.CSSProperties + itemStyle?: React.CSSProperties + itemContainerStyle?: React.CSSProperties + droppableProps?: Partial + onUpdate: (list: T[]) => void + onDragStart?: OnDragStartResponder + onDragEnd?: OnDragEndResponder + list: T[] + itemKey?: (index: number) => Key + overscan?: number + children: (item: T, index: number) => React.ReactNode +} + +/** + * 带虚拟滚动与拖拽排序能力的(垂直)列表组件。 + * - 滚动容器由该组件内部管理。 + * @template T 列表元素的类型 + * @param {DraggableVirtualListProps} props 组件参数 + * @returns {React.ReactElement} + */ +function DraggableVirtualList({ + ref, + className, + style, + itemStyle, + itemContainerStyle, + droppableProps, + onDragStart, + onUpdate, + onDragEnd, + list, + itemKey, + overscan = 5, + children +}: DraggableVirtualListProps): React.ReactElement { + const _onDragEnd = (result: DropResult, provided: ResponderProvided) => { + onDragEnd?.(result, provided) + if (result.destination) { + const sourceIndex = result.source.index + const destIndex = result.destination.index + const reorderAgents = droppableReorder(list, sourceIndex, destIndex) + onUpdate(reorderAgents) + } + } + + // 虚拟列表滚动容器的 ref + const parentRef = useRef(null) + + const virtualizer = useVirtualizer({ + count: list.length, + getScrollElement: useCallback(() => parentRef.current, []), + getItemKey: itemKey, + estimateSize: useCallback(() => 50, []), + overscan + }) + + return ( +
+ + { + const item = list[rubric.source.index] + return ( +
+ {item && children(item, rubric.source.index)} +
+ ) + }} + {...droppableProps}> + {(provided) => { + // 让 dnd 和虚拟列表共享同一个滚动容器 + const setRefs = (el: HTMLDivElement | null) => { + provided.innerRef(el) + parentRef.current = el + } + + return ( + +
+ {virtualizer.getVirtualItems().map((virtualItem) => ( + + ))} +
+
+ ) + }} +
+
+
+ ) +} + +/** + * 渲染单个可拖拽的虚拟列表项,高度为动态测量 + */ +const VirtualRow = memo(({ virtualItem, list, children, itemStyle, itemContainerStyle, virtualizer }: any) => { + const item = list[virtualItem.index] + const draggableId = String(virtualItem.key) + return ( + + {(provided) => { + const setDragRefs = (el: HTMLElement | null) => { + provided.innerRef(el) + virtualizer.measureElement(el) + } + + const dndStyle = provided.draggableProps.style + const virtualizerTransform = `translateY(${virtualItem.start}px)` + + // dnd 的 transform 负责拖拽时的位移和让位动画, + // virtualizer 的 translateY 负责将项定位到虚拟列表的正确位置, + // 它们拼接起来可以同时实现拖拽视觉效果和虚拟化定位。 + const combinedTransform = dndStyle?.transform + ? `${dndStyle.transform} ${virtualizerTransform}` + : virtualizerTransform + + return ( +
+
+ {item && children(item, virtualItem.index)} +
+
+ ) + }} +
+ ) +}) + +export default DraggableVirtualList diff --git a/src/renderer/src/components/Scrollbar/index.tsx b/src/renderer/src/components/Scrollbar/index.tsx index c3bd1b2d0c..60258d8c8b 100644 --- a/src/renderer/src/components/Scrollbar/index.tsx +++ b/src/renderer/src/components/Scrollbar/index.tsx @@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' interface Props extends Omit, 'onScroll'> { - ref?: React.RefObject + ref?: React.Ref onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll } diff --git a/src/renderer/src/components/__tests__/DragableList.test.tsx b/src/renderer/src/components/__tests__/DraggableList.test.tsx similarity index 87% rename from src/renderer/src/components/__tests__/DragableList.test.tsx rename to src/renderer/src/components/__tests__/DraggableList.test.tsx index d40849fff8..4878fd4838 100644 --- a/src/renderer/src/components/__tests__/DragableList.test.tsx +++ b/src/renderer/src/components/__tests__/DraggableList.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' -import DragableList from '../DragableList' +import { DraggableList } from '../DraggableList' // mock @hello-pangea/dnd 组件 vi.mock('@hello-pangea/dnd', () => { @@ -49,7 +49,7 @@ declare global { } } -describe('DragableList', () => { +describe('DraggableList', () => { describe('rendering', () => { it('should render all list items', () => { const list = [ @@ -58,9 +58,9 @@ describe('DragableList', () => { { id: 'c', name: 'C' } ] render( - {}}> + {}}> {(item) =>
{item.name}
} -
+ ) const items = screen.getAllByTestId('item') expect(items.length).toBe(3) @@ -74,9 +74,9 @@ describe('DragableList', () => { const style = { background: 'red' } const listStyle = { color: 'blue' } render( - {}}> + {}}> {(item) =>
{item.name}
} -
+ ) // 检查 style 是否传递到外层容器 const virtualList = screen.getByTestId('virtual-list') @@ -85,9 +85,9 @@ describe('DragableList', () => { it('should render nothing when list is empty', () => { render( - {}}> + {}}> {(item) =>
{item.name}
} -
+ ) // 虚拟列表存在但无内容 const items = screen.queryAllByTestId('item') @@ -106,9 +106,9 @@ describe('DragableList', () => { const onUpdate = vi.fn() render( - + {(item) =>
{item.name}
} -
+ ) // 直接调用 window.triggerOnDragEnd 模拟拖拽结束 @@ -128,9 +128,9 @@ describe('DragableList', () => { const onDragEnd = vi.fn() render( - {}} onDragStart={onDragStart} onDragEnd={onDragEnd}> + {}} onDragStart={onDragStart} onDragEnd={onDragEnd}> {(item) =>
{item.name}
} -
+ ) // 先手动调用 onDragStart @@ -150,9 +150,9 @@ describe('DragableList', () => { const onUpdate = vi.fn() render( - + {(item) =>
{item.name}
} -
+ ) // 模拟拖拽到自身 @@ -168,9 +168,9 @@ describe('DragableList', () => { const onUpdate = vi.fn() render( - + {(item) =>
{item.name}
} -
+ ) // 拖拽自身 @@ -188,9 +188,9 @@ describe('DragableList', () => { // 不传 onDragStart/onDragEnd expect(() => { render( - {}}> + {}}> {(item) =>
{item.name}
} -
+ ) window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 1 } }, {}) }).not.toThrow() @@ -201,9 +201,9 @@ describe('DragableList', () => { const onUpdate = vi.fn() render( - + {(item) =>
{item}
} -
+ ) // 拖拽第0项到第2项 @@ -222,9 +222,9 @@ describe('DragableList', () => { ] render( - {}}> + {}}> {(item) =>
{item.name}
} -
+ ) // placeholder 应该在初始渲染时就存在 @@ -240,9 +240,9 @@ describe('DragableList', () => { ] const onUpdate = vi.fn() render( - + {(item) =>
{item.name}
} -
+ ) // 拖拽第2项到第0项 @@ -272,9 +272,9 @@ describe('DragableList', () => { { id: 'c', name: 'C' } ] const { container } = render( - {}}> + {}}> {(item) =>
{item.name}
} -
+ ) expect(container).toMatchSnapshot() }) diff --git a/src/renderer/src/components/__tests__/DraggableVirtualList.test.tsx b/src/renderer/src/components/__tests__/DraggableVirtualList.test.tsx new file mode 100644 index 0000000000..b82181ef42 --- /dev/null +++ b/src/renderer/src/components/__tests__/DraggableVirtualList.test.tsx @@ -0,0 +1,164 @@ +/// + +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import DraggableVirtualList from '../DraggableList/virtual-list' + +// Mock 依赖项 +vi.mock('@hello-pangea/dnd', () => ({ + __esModule: true, + DragDropContext: ({ children, onDragEnd, onDragStart }) => { + // 挂载到 window 以便测试用例直接调用 + window.triggerOnDragEnd = (result = { source: { index: 0 }, destination: { index: 1 } }, provided = {}) => { + onDragEnd?.(result, provided) + } + window.triggerOnDragStart = (result = { source: { index: 0 } }, provided = {}) => { + onDragStart?.(result, provided) + } + return
{children}
+ }, + Droppable: ({ children, renderClone }) => ( +
+ {/* 模拟 renderClone 的调用 */} + {renderClone && + renderClone({ draggableProps: {}, dragHandleProps: {}, innerRef: vi.fn() }, {}, { source: { index: 0 } })} + {children({ droppableProps: {}, innerRef: vi.fn() })} +
+ ), + Draggable: ({ children, draggableId, index }) => ( +
+ {children({ draggableProps: {}, dragHandleProps: {}, innerRef: vi.fn() }, {})} +
+ ) +})) + +vi.mock('@tanstack/react-virtual', () => ({ + useVirtualizer: ({ count }) => ({ + getVirtualItems: () => + Array.from({ length: count }, (_, index) => ({ + index, + key: index, + start: index * 50, + size: 50 + })), + getTotalSize: () => count * 50, + measureElement: vi.fn() + }) +})) + +vi.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: ({ children }) =>
{children({ height: 500, width: 300 })}
+})) + +vi.mock('@renderer/components/Scrollbar', () => ({ + __esModule: true, + default: ({ ref, children, ...props }) => ( +
+ {children} +
+ ) +})) + +declare global { + interface Window { + triggerOnDragEnd: (result?: any, provided?: any) => void + triggerOnDragStart: (result?: any, provided?: any) => void + } +} + +describe('DraggableVirtualList', () => { + const sampleList = [ + { id: 'a', name: 'Item A' }, + { id: 'b', name: 'Item B' }, + { id: 'c', name: 'Item C' } + ] + + describe('rendering', () => { + it('should render all list items provided', () => { + render( + {}}> + {(item) =>
{item.name}
} +
+ ) + const items = screen.getAllByTestId('test-item') + // 我们的 mock 中,renderClone 会渲染一个额外的 item + expect(items.length).toBe(sampleList.length + 1) + expect(items[0]).toHaveTextContent('Item A') + expect(items[1]).toHaveTextContent('Item A') + expect(items[2]).toHaveTextContent('Item B') + expect(items[3]).toHaveTextContent('Item C') + }) + + it('should render nothing when the list is empty', () => { + render( + {}}> + {/* @ts-ignore test*/} + {(item) =>
{item.name}
} +
+ ) + const items = screen.queryAllByTestId('test-item') + expect(items.length).toBe(0) + }) + }) + + describe('drag and drop', () => { + it('should call onUpdate with the new order after a drag operation', () => { + const onUpdate = vi.fn() + render( + + {(item) =>
{item.name}
} +
+ ) + + window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 2 } }) + const expectedOrder = [sampleList[1], sampleList[2], sampleList[0]] // B, C, A + expect(onUpdate).toHaveBeenCalledWith(expectedOrder) + }) + + it('should call onDragStart and onDragEnd callbacks', () => { + const onDragStart = vi.fn() + const onDragEnd = vi.fn() + render( + {}} onDragStart={onDragStart} onDragEnd={onDragEnd}> + {(item) =>
{item.name}
} +
+ ) + + window.triggerOnDragStart() + expect(onDragStart).toHaveBeenCalledTimes(1) + + window.triggerOnDragEnd() + expect(onDragEnd).toHaveBeenCalledTimes(1) + }) + + it('should not call onUpdate if destination is not defined', () => { + const onUpdate = vi.fn() + render( + + {(item) =>
{item.name}
} +
+ ) + + window.triggerOnDragEnd({ source: { index: 0 }, destination: null }) + expect(onUpdate).not.toHaveBeenCalled() + }) + }) + + describe('snapshot', () => { + it('should match snapshot with custom styles', () => { + const { container } = render( + {}} + className="custom-class" + style={{ border: '1px solid red' }} + itemStyle={{ background: 'blue' }}> + {(item) =>
{item.name}
} +
+ ) + expect(container).toMatchSnapshot() + }) + }) +}) diff --git a/src/renderer/src/components/__tests__/__snapshots__/DragableList.test.tsx.snap b/src/renderer/src/components/__tests__/__snapshots__/DraggableList.test.tsx.snap similarity index 95% rename from src/renderer/src/components/__tests__/__snapshots__/DragableList.test.tsx.snap rename to src/renderer/src/components/__tests__/__snapshots__/DraggableList.test.tsx.snap index a7ddaaaf11..f85a3e07bd 100644 --- a/src/renderer/src/components/__tests__/__snapshots__/DragableList.test.tsx.snap +++ b/src/renderer/src/components/__tests__/__snapshots__/DraggableList.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`DragableList > snapshot > should match snapshot 1`] = ` +exports[`DraggableList > snapshot > should match snapshot 1`] = `
snapshot > should match snapshot with custom styles 1`] = ` +
+
+
+
+
+
+ Item A +
+
+
+
+
+
+
+
+ Item A +
+
+
+
+
+
+
+
+ Item B +
+
+
+
+
+
+
+
+ Item C +
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 5e4365c3c9..4e03cee5ff 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -33,7 +33,7 @@ import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' -import DragableList from '../DragableList' +import { DraggableList } from '../DraggableList' import MinAppIcon from '../Icons/MinAppIcon' import UserPopup from '../Popups/UserPopup' @@ -288,7 +288,7 @@ const PinnedApps: FC = () => { const { openMinappKeepAlive } = useMinappPopup() return ( - + {(app) => { const menuItems: MenuProps['items'] = [ { @@ -316,7 +316,7 @@ const PinnedApps: FC = () => { ) }} - + ) } diff --git a/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx b/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx index 5307293b1e..ab03f71f02 100644 --- a/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx +++ b/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx @@ -1,5 +1,5 @@ import { MenuOutlined } from '@ant-design/icons' -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import { Box, HStack } from '@renderer/components/Layout' import { TopView } from '@renderer/components/TopView' import { useAgents } from '@renderer/hooks/useAgents' @@ -43,7 +43,7 @@ const PopupContainer: React.FC = () => { centered> {agents.length > 0 && ( - + {(item) => ( @@ -54,7 +54,7 @@ const PopupContainer: React.FC = () => { )} - + )} {agents.length === 0 && } diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index b164e7b492..048feb83bd 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -1,5 +1,5 @@ import { DownOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons' -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import Scrollbar from '@renderer/components/Scrollbar' import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants } from '@renderer/hooks/useAssistant' @@ -92,7 +92,7 @@ const Assistants: FC = ({ )} {!collapsedTags[group.tag] && (
- handleGroupReorder(group.tag, newList)} onDragStart={() => setDragging(true)} @@ -111,7 +111,7 @@ const Assistants: FC = ({ handleSortByChange={handleSortByChange} /> )} - +
)} @@ -129,7 +129,7 @@ const Assistants: FC = ({ return ( - setDragging(true)} @@ -148,7 +148,7 @@ const Assistants: FC = ({ handleSortByChange={handleSortByChange} /> )} - + {!dragging && ( diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 01a548b8c1..b5fc93e054 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -9,11 +9,10 @@ import { QuestionCircleOutlined, UploadOutlined } from '@ant-design/icons' -import DragableList from '@renderer/components/DragableList' +import { DraggableVirtualList as DraggableList } from '@renderer/components/DraggableList' import CopyIcon from '@renderer/components/Icons/CopyIcon' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup' -import Scrollbar from '@renderer/components/Scrollbar' import { isMac } from '@renderer/config/constant' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' import { modelGenerating } from '@renderer/hooks/useRuntime' @@ -447,92 +446,86 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic }, [assistant.topics, pinTopicsToTop]) return ( - - - - {(topic) => { - const isActive = topic.id === activeTopic?.id - const topicName = topic.name.replace('`', '') - const topicPrompt = topic.prompt - const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt + + {(topic) => { + const isActive = topic.id === activeTopic?.id + const topicName = topic.name.replace('`', '') + const topicPrompt = topic.prompt + const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt - const getTopicNameClassName = () => { - if (isRenaming(topic.id)) return 'shimmer' - if (isNewlyRenamed(topic.id)) return 'typing' - return '' - } + const getTopicNameClassName = () => { + if (isRenaming(topic.id)) return 'shimmer' + if (isNewlyRenamed(topic.id)) return 'typing' + return '' + } - return ( - setTargetTopic(topic)} - className={isActive ? 'active' : ''} - onClick={() => onSwitchTopic(topic)} - style={{ borderRadius }}> - {isPending(topic.id) && !isActive && } - - - {topicName} - - {!topic.pinned && ( - -
- {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} -
+ return ( + + setTargetTopic(topic)} + className={isActive ? 'active' : ''} + onClick={() => onSwitchTopic(topic)} + style={{ borderRadius }}> + {isPending(topic.id) && !isActive && } + + + {topicName} + + {!topic.pinned && ( + +
+ {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
- }> - { - if (e.ctrlKey || e.metaKey) { - handleConfirmDelete(topic, e) - } else if (deletingTopicId === topic.id) { - handleConfirmDelete(topic, e) - } else { - handleDeleteClick(topic.id, e) - } - }}> - {deletingTopicId === topic.id ? ( - - ) : ( - - )} - -
- )} - {topic.pinned && ( - - +
+ }> + { + if (e.ctrlKey || e.metaKey) { + handleConfirmDelete(topic, e) + } else if (deletingTopicId === topic.id) { + handleConfirmDelete(topic, e) + } else { + handleDeleteClick(topic.id, e) + } + }}> + {deletingTopicId === topic.id ? ( + + ) : ( + + )} - )} - - {topicPrompt && ( - - {fullTopicPrompt} - + )} - {showTopicTime && ( - {dayjs(topic.createdAt).format('MM/DD HH:mm')} + {topic.pinned && ( + + + )} - - ) - }} - -
- - + + {topicPrompt && ( + + {fullTopicPrompt} + + )} + {showTopicTime && {dayjs(topic.createdAt).format('MM/DD HH:mm')}} + + + ) + }} + ) } -const Container = styled(Scrollbar)` - display: flex; - flex-direction: column; - padding: 10px; -` - const TopicListItem = styled.div` padding: 7px 12px; border-radius: var(--list-item-border-radius); diff --git a/src/renderer/src/pages/knowledge/KnowledgePage.tsx b/src/renderer/src/pages/knowledge/KnowledgePage.tsx index a321e8b72e..aeb52b83eb 100644 --- a/src/renderer/src/pages/knowledge/KnowledgePage.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgePage.tsx @@ -1,6 +1,6 @@ import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import ListItem from '@renderer/components/ListItem' import PromptPopup from '@renderer/components/Popups/PromptPopup' import Scrollbar from '@renderer/components/Scrollbar' @@ -98,7 +98,7 @@ const KnowledgePage: FC = () => { - {
)} - + {!isDragging && ( diff --git a/src/renderer/src/pages/paintings/components/PaintingsList.tsx b/src/renderer/src/pages/paintings/components/PaintingsList.tsx index 391c3381f2..fc119af098 100644 --- a/src/renderer/src/pages/paintings/components/PaintingsList.tsx +++ b/src/renderer/src/pages/paintings/components/PaintingsList.tsx @@ -1,5 +1,5 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons' -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import Scrollbar from '@renderer/components/Scrollbar' import { usePaintings } from '@renderer/hooks/usePaintings' import FileManager from '@renderer/services/FileManager' @@ -38,7 +38,7 @@ const PaintingsList: FC = ({ )} - updatePaintings(namespace, value)} onDragStart={() => setDragging(true)} @@ -61,7 +61,7 @@ const PaintingsList: FC = ({ )} - + ) } diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantRegularPromptsSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantRegularPromptsSettings.tsx index 13194325f7..ecdd03cda9 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantRegularPromptsSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantRegularPromptsSettings.tsx @@ -1,5 +1,5 @@ import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons' -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import FileItem from '@renderer/pages/files/FileItem' import { Assistant, QuickPhrase } from '@renderer/types' import { Button, Flex, Input, Modal, Popconfirm, Space } from 'antd' @@ -87,7 +87,7 @@ const AssistantRegularPromptsSettings: FC - handleUpdateOrder([...newPrompts].reverse())} style={{ paddingBottom: dragging ? '34px' : 0 }} @@ -120,7 +120,7 @@ const AssistantRegularPromptsSettings: FC }} /> )} - + diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index de0b83c137..4168099c0d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -1,6 +1,6 @@ import { EditOutlined } from '@ant-design/icons' import { nanoid } from '@reduxjs/toolkit' -import DragableList from '@renderer/components/DragableList' +import { DraggableList } from '@renderer/components/DraggableList' import Scrollbar from '@renderer/components/Scrollbar' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { MCPServer } from '@renderer/types' @@ -117,7 +117,7 @@ const McpServersList: FC = () => { - + {(server: MCPServer) => ( navigate(`/settings/mcp/settings`, { state: { server } })}> @@ -171,7 +171,7 @@ const McpServersList: FC = () => { )} - + {mcpServers.length === 0 && ( { - handleUpdateOrder([...newPhrases].reverse())} style={{ paddingBottom: dragging ? '34px' : 0 }} @@ -109,7 +109,7 @@ const QuickPhraseSettings: FC = () => { }} /> )} - +