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
This commit is contained in:
自由的世界人 2025-06-08 11:03:39 +08:00 committed by GitHub
parent 1de6698162
commit 598b73f7cb
4 changed files with 172 additions and 31 deletions

View File

@ -1,3 +1,5 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setTagsOrder, updateAssistants } from '@renderer/store/assistants'
import { flatMap, groupBy, uniq } from 'lodash' import { flatMap, groupBy, uniq } from 'lodash'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -11,17 +13,48 @@ import { useAssistants } from './useAssistant'
export const useTags = () => { export const useTags = () => {
const { assistants } = useAssistants() const { assistants } = useAssistants()
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch()
const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || [])
// 计算所有标签 // 计算所有标签
const allTags = useMemo(() => { const allTags = useMemo(() => {
return uniq(flatMap(assistants, (assistant) => assistant.tags || [])) const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || []))
}, [assistants]) if (savedTagsOrder.length > 0) {
return [
...savedTagsOrder.filter((tag) => tags.includes(tag)),
...tags.filter((tag) => !savedTagsOrder.includes(tag))
]
}
return tags
}, [assistants, savedTagsOrder])
const getAssistantsByTag = useCallback( const getAssistantsByTag = useCallback(
(tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)), (tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)),
[assistants] [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 getGroupedAssistants = useMemo(() => {
// 按标签分组,处理多标签的情况 // 按标签分组,处理多标签的情况
const assistantsByTags = flatMap(assistants, (assistant) => { const assistantsByTags = flatMap(assistants, (assistant) => {
@ -42,12 +75,30 @@ export const useTags = () => {
grouped.unshift(untagged) 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 return grouped
}, [assistants, t]) }, [assistants, t, savedTagsOrder])
return { return {
allTags, allTags,
getAssistantsByTag, getAssistantsByTag,
getGroupedAssistants getGroupedAssistants,
updateTagsOrder
} }
} }

View File

@ -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 DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
@ -27,6 +27,7 @@ const Assistants: FC<AssistantsTabProps> = ({
}) => { }) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants() const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const [dragging, setDragging] = useState(false) const [dragging, setDragging] = useState(false)
const [collapsedTags, setCollapsedTags] = useState<Record<string, boolean>>({})
const { addAgent } = useAgents() const { addAgent } = useAgents()
const { t } = useTranslation() const { t } = useTranslation()
const { getGroupedAssistants } = useTags() const { getGroupedAssistants } = useTags()
@ -45,6 +46,13 @@ const Assistants: FC<AssistantsTabProps> = ({
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant] [activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
) )
const toggleTagCollapse = useCallback((tag: string) => {
setCollapsedTags((prev) => ({
...prev,
[tag]: !prev[tag]
}))
}, [])
const handleSortByChange = useCallback( const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => { (sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType) setAssistantsTabSortType(sortType)
@ -52,6 +60,23 @@ const Assistants: FC<AssistantsTabProps> = ({
[setAssistantsTabSortType] [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') { if (assistantsTabSortType === 'tags') {
return ( return (
<Container className="assistants-tab" ref={containerRef}> <Container className="assistants-tab" ref={containerRef}>
@ -59,14 +84,29 @@ const Assistants: FC<AssistantsTabProps> = ({
{getGroupedAssistants.map((group) => ( {getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}> <TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && ( {group.tag !== t('assistants.tags.untagged') && (
<GroupTitle> <GroupTitle onClick={() => toggleTagCollapse(group.tag)}>
<Tooltip title={group.tag}> <Tooltip title={group.tag}>
<GroupTitleName>{group.tag}</GroupTitleName> <GroupTitleName>
{collapsedTags[group.tag] ? (
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
) : (
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
)}
{group.tag}
</GroupTitleName>
</Tooltip> </Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider> <Divider style={{ margin: '12px 0' }}></Divider>
</GroupTitle> </GroupTitle>
)} )}
{group.assistants.map((assistant) => ( {!collapsedTags[group.tag] && (
<div>
<DragableList
list={group.assistants}
onUpdate={(newList) => handleGroupReorder(group.tag, newList)}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem <AssistantItem
key={assistant.id} key={assistant.id}
assistant={assistant} assistant={assistant}
@ -79,7 +119,10 @@ const Assistants: FC<AssistantsTabProps> = ({
onCreateDefaultAssistant={onCreateDefaultAssistant} onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange} handleSortByChange={handleSortByChange}
/> />
))} )}
</DragableList>
</div>
)}
</TagsContainer> </TagsContainer>
))} ))}
</div> </div>
@ -164,12 +207,13 @@ const AssistantAddItem = styled.div`
` `
const GroupTitle = styled.div` const GroupTitle = styled.div`
padding: 8px 0px; padding: 8px 0;
position: relative; position: relative;
color: var(--color-text-2); color: var(--color-text-2);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
margin-bottom: -8px; margin-bottom: -8px;
cursor: pointer;
` `
const GroupTitleName = styled.div` const GroupTitleName = styled.div`

View File

@ -1,3 +1,4 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { Box } from '@renderer/components/Layout' import { Box } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
@ -19,9 +20,10 @@ interface Props extends ShowParams {
const PopupContainer: React.FC<Props> = ({ title, resolve }) => { const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { allTags, getAssistantsByTag } = useTags() const { allTags, getAssistantsByTag, updateTagsOrder } = useTags()
const { assistants, updateAssistants } = useAssistants() const { assistants, updateAssistants } = useAssistants()
const { t } = useTranslation() const { t } = useTranslation()
const [tags, setTags] = useState(allTags)
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
@ -49,10 +51,24 @@ const PopupContainer: React.FC<Props> = ({ 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 AssistantTagsPopup.hide = onCancel
return ( return (
@ -66,13 +82,37 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
transitionName="animation-move-down" transitionName="animation-move-down"
centered> centered>
<Container> <Container>
{allTags.map((tag) => ( {tags.length > 0 ? (
<TagItem key={tag}> <DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="tags">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{tags.map((tag, index) => (
<Draggable key={tag} draggableId={tag} index={index}>
{(provided) => (
<TagItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Box mr={8}>{tag}</Box> <Box mr={8}>{tag}</Box>
<Button type="text" icon={<Trash size={16} />} danger onClick={() => onDelete(tag)} /> <Button
type="text"
icon={<Trash size={16} />}
danger
onClick={(e) => {
e.stopPropagation()
onDelete(tag)
}}
/>
</TagItem> </TagItem>
)}
</Draggable>
))} ))}
{allTags.length === 0 && <Empty description="" />} {provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
) : (
<Empty description="" />
)}
</Container> </Container>
</Modal> </Modal>
) )

View File

@ -8,11 +8,13 @@ import { isEmpty, uniqBy } from 'lodash'
export interface AssistantsState { export interface AssistantsState {
defaultAssistant: Assistant defaultAssistant: Assistant
assistants: Assistant[] assistants: Assistant[]
tagsOrder: string[]
} }
const initialState: AssistantsState = { const initialState: AssistantsState = {
defaultAssistant: getDefaultAssistant(), defaultAssistant: getDefaultAssistant(),
assistants: [getDefaultAssistant()] assistants: [getDefaultAssistant()],
tagsOrder: []
} }
const assistantsSlice = createSlice({ const assistantsSlice = createSlice({
@ -128,6 +130,9 @@ const assistantsSlice = createSlice({
} }
: assistant : assistant
) )
},
setTagsOrder: (state, action: PayloadAction<string[]>) => {
state.tagsOrder = action.payload
} }
} }
}) })
@ -144,6 +149,7 @@ export const {
updateTopics, updateTopics,
removeAllTopics, removeAllTopics,
setModel, setModel,
setTagsOrder,
updateAssistantSettings updateAssistantSettings
} = assistantsSlice.actions } = assistantsSlice.actions