From 598b73f7cb8bdcd5b21e1a129a9dfef610cdaa6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sun, 8 Jun 2025 11:03:39 +0800 Subject: [PATCH] fix: Implement label folding, drag-and-drop sorting of assistants within labels, and drag-and-drop sorting of labels (#6735) * fix: add collapsible tags in AssistantsTab for better organization * fix: implement drag-and-drop functionality for reordering assistants in tags * fix: implement drag-and-drop functionality for reordering tags in AssistantTagsPopup * fix: eslint error --- src/renderer/src/hooks/useTags.ts | 59 +++++++++++++- .../src/pages/home/Tabs/AssistantsTab.tsx | 80 ++++++++++++++----- .../Tabs/components/AssistantTagsPopup.tsx | 56 +++++++++++-- src/renderer/src/store/assistants.ts | 8 +- 4 files changed, 172 insertions(+), 31 deletions(-) diff --git a/src/renderer/src/hooks/useTags.ts b/src/renderer/src/hooks/useTags.ts index 50b3f2a782..427d237760 100644 --- a/src/renderer/src/hooks/useTags.ts +++ b/src/renderer/src/hooks/useTags.ts @@ -1,3 +1,5 @@ +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setTagsOrder, updateAssistants } from '@renderer/store/assistants' import { flatMap, groupBy, uniq } from 'lodash' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -11,17 +13,48 @@ import { useAssistants } from './useAssistant' export const useTags = () => { const { assistants } = useAssistants() const { t } = useTranslation() + const dispatch = useAppDispatch() + const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || []) // 计算所有标签 const allTags = useMemo(() => { - return uniq(flatMap(assistants, (assistant) => assistant.tags || [])) - }, [assistants]) + const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || [])) + if (savedTagsOrder.length > 0) { + return [ + ...savedTagsOrder.filter((tag) => tags.includes(tag)), + ...tags.filter((tag) => !savedTagsOrder.includes(tag)) + ] + } + return tags + }, [assistants, savedTagsOrder]) const getAssistantsByTag = useCallback( (tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)), [assistants] ) + const updateTagsOrder = useCallback( + (newOrder: string[]) => { + dispatch(setTagsOrder(newOrder)) + updateAssistants( + assistants.map((assistant) => { + if (!assistant.tags || assistant.tags.length === 0) { + return assistant + } + const newTags = [...assistant.tags] + newTags.sort((a, b) => { + return newOrder.indexOf(a) - newOrder.indexOf(b) + }) + return { + ...assistant, + tags: newTags + } + }) + ) + }, + [assistants, dispatch] + ) + const getGroupedAssistants = useMemo(() => { // 按标签分组,处理多标签的情况 const assistantsByTags = flatMap(assistants, (assistant) => { @@ -42,12 +75,30 @@ export const useTags = () => { grouped.unshift(untagged) } + // 根据savedTagsOrder对标签组进行排序 + if (savedTagsOrder.length > 0) { + const untagged = grouped.length > 0 && grouped[0].tag === t('assistants.tags.untagged') ? grouped.shift() : null + grouped.sort((a, b) => { + const indexA = savedTagsOrder.indexOf(a.tag) + const indexB = savedTagsOrder.indexOf(b.tag) + if (indexA === -1 && indexB === -1) return 0 + if (indexA === -1) return 1 + if (indexB === -1) return -1 + + return indexA - indexB + }) + if (untagged) { + grouped.unshift(untagged) + } + } + return grouped - }, [assistants, t]) + }, [assistants, t, savedTagsOrder]) return { allTags, getAssistantsByTag, - getGroupedAssistants + getGroupedAssistants, + updateTagsOrder } } diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index ac1851df38..cbcd272791 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -1,4 +1,4 @@ -import { PlusOutlined } from '@ant-design/icons' +import { DownOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons' import DragableList from '@renderer/components/DragableList' import Scrollbar from '@renderer/components/Scrollbar' import { useAgents } from '@renderer/hooks/useAgents' @@ -27,6 +27,7 @@ const Assistants: FC = ({ }) => { const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants() const [dragging, setDragging] = useState(false) + const [collapsedTags, setCollapsedTags] = useState>({}) const { addAgent } = useAgents() const { t } = useTranslation() const { getGroupedAssistants } = useTags() @@ -45,6 +46,13 @@ const Assistants: FC = ({ [activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant] ) + const toggleTagCollapse = useCallback((tag: string) => { + setCollapsedTags((prev) => ({ + ...prev, + [tag]: !prev[tag] + })) + }, []) + const handleSortByChange = useCallback( (sortType: AssistantsSortType) => { setAssistantsTabSortType(sortType) @@ -52,6 +60,23 @@ const Assistants: FC = ({ [setAssistantsTabSortType] ) + const handleGroupReorder = useCallback( + (tag: string, newGroupList: Assistant[]) => { + let insertIndex = 0 + const newGlobal = assistants.map((a) => { + const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')] + if (tags.includes(tag)) { + const replaced = newGroupList[insertIndex] + insertIndex += 1 + return replaced + } + return a + }) + updateAssistants(newGlobal) + }, + [assistants, t, updateAssistants] + ) + if (assistantsTabSortType === 'tags') { return ( @@ -59,27 +84,45 @@ const Assistants: FC = ({ {getGroupedAssistants.map((group) => ( {group.tag !== t('assistants.tags.untagged') && ( - + toggleTagCollapse(group.tag)}> - {group.tag} + + {collapsedTags[group.tag] ? ( + + ) : ( + + )} + {group.tag} + )} - {group.assistants.map((assistant) => ( - - ))} + {!collapsedTags[group.tag] && ( +
+ handleGroupReorder(group.tag, newList)} + style={{ paddingBottom: dragging ? '34px' : 0 }} + onDragStart={() => setDragging(true)} + onDragEnd={() => setDragging(false)}> + {(assistant) => ( + + )} + +
+ )}
))} @@ -164,12 +207,13 @@ const AssistantAddItem = styled.div` ` const GroupTitle = styled.div` - padding: 8px 0px; + padding: 8px 0; position: relative; color: var(--color-text-2); font-size: 12px; font-weight: 500; margin-bottom: -8px; + cursor: pointer; ` const GroupTitleName = styled.div` diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx index 978aca0212..ad0c6ba9c6 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx @@ -1,3 +1,4 @@ +import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd' import { Box } from '@renderer/components/Layout' import { TopView } from '@renderer/components/TopView' import { useAssistants } from '@renderer/hooks/useAssistant' @@ -19,9 +20,10 @@ interface Props extends ShowParams { const PopupContainer: React.FC = ({ title, resolve }) => { const [open, setOpen] = useState(true) - const { allTags, getAssistantsByTag } = useTags() + const { allTags, getAssistantsByTag, updateTagsOrder } = useTags() const { assistants, updateAssistants } = useAssistants() const { t } = useTranslation() + const [tags, setTags] = useState(allTags) const onOk = () => { setOpen(false) @@ -49,10 +51,24 @@ const PopupContainer: React.FC = ({ title, resolve }) => { }) ) } + const newTags = tags.filter((tag) => tag !== removedTag) + setTags(newTags) + updateTagsOrder(newTags) } }) } + const handleDragEnd = (result) => { + if (!result.destination) return + + const items = Array.from(tags) + const [reorderedItem] = items.splice(result.source.index, 1) + items.splice(result.destination.index, 0, reorderedItem) + + setTags(items) + updateTagsOrder(items) + } + AssistantTagsPopup.hide = onCancel return ( @@ -66,13 +82,37 @@ const PopupContainer: React.FC = ({ title, resolve }) => { transitionName="animation-move-down" centered> - {allTags.map((tag) => ( - - {tag} -