mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 21:35:52 +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 { 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user