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 { 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
}
}

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 Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
@ -27,6 +27,7 @@ const Assistants: FC<AssistantsTabProps> = ({
}) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const [dragging, setDragging] = useState(false)
const [collapsedTags, setCollapsedTags] = useState<Record<string, boolean>>({})
const { addAgent } = useAgents()
const { t } = useTranslation()
const { getGroupedAssistants } = useTags()
@ -45,6 +46,13 @@ const Assistants: FC<AssistantsTabProps> = ({
[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<AssistantsTabProps> = ({
[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 (
<Container className="assistants-tab" ref={containerRef}>
@ -59,27 +84,45 @@ const Assistants: FC<AssistantsTabProps> = ({
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
<GroupTitle>
<GroupTitle onClick={() => toggleTagCollapse(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>
<Divider style={{ margin: '12px 0' }}></Divider>
</GroupTitle>
)}
{group.assistants.map((assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addAgent={addAgent}
addAssistant={addAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
))}
{!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
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addAgent={addAgent}
addAssistant={addAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DragableList>
</div>
)}
</TagsContainer>
))}
</div>
@ -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`

View File

@ -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<Props> = ({ 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<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
return (
@ -66,13 +82,37 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
transitionName="animation-move-down"
centered>
<Container>
{allTags.map((tag) => (
<TagItem key={tag}>
<Box mr={8}>{tag}</Box>
<Button type="text" icon={<Trash size={16} />} danger onClick={() => onDelete(tag)} />
</TagItem>
))}
{allTags.length === 0 && <Empty description="" />}
{tags.length > 0 ? (
<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>
<Button
type="text"
icon={<Trash size={16} />}
danger
onClick={(e) => {
e.stopPropagation()
onDelete(tag)
}}
/>
</TagItem>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
) : (
<Empty description="" />
)}
</Container>
</Modal>
)

View File

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