mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
1de6698162
commit
598b73f7cb
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user