mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
feat(assistants): enhance ManageAssistantPresetsPopup with sort and batch delete modes (#11835)
* feat(assistants): enhance ManageAssistantPresetsPopup with sort and batch delete modes - Merge sorting and batch delete functionality into a single popup - Add Segmented control to switch between sort and delete modes - Sort mode: drag and drop to reorder assistants using DraggableList - Delete mode: select and batch delete assistants with checkbox - Add "+100" button for quick batch selection when there are many presets - Add manage button to AssistantPresetsPage header - Update AssistantPresetCard menu to use the new ManageAssistantPresetsPopup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(assistants): improve selection logic in ManageAssistantPresetsPopup - Update the "+100" button functionality to select the next 100 unselected presets starting from the last selected preset. - Enhance user experience by ensuring that the selection continues from the correct index, allowing for more intuitive batch selection of presets. * feat(assistants): adjust initial mode in ManageAssistantPresetsPopup based on preset count - Modify the initial state of the mode to switch between 'delete' and 'sort' based on the number of presets available, enhancing user experience by optimizing the default action for larger preset collections. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a91c69982c
commit
595a0f194a
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "Enter JSON URL"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Batch Delete",
|
||||
"confirm": "Are you sure you want to delete the selected {{count}} assistants?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Delete",
|
||||
"sort": "Sort"
|
||||
},
|
||||
"title": "Manage Assistants"
|
||||
},
|
||||
"my_agents": "My Assistants",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "Saved",
|
||||
"search": "Search",
|
||||
"select": "Select",
|
||||
"select_all": "Select All",
|
||||
"selected": "Selected",
|
||||
"selectedItems": "Selected {{count}} items",
|
||||
"selectedMessages": "Selected {{count}} messages",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "输入 JSON URL"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "批量删除",
|
||||
"confirm": "确定要删除选中的 {{count}} 个助手吗?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "删除",
|
||||
"sort": "排序"
|
||||
},
|
||||
"title": "管理助手"
|
||||
},
|
||||
"my_agents": "我的助手",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "已保存",
|
||||
"search": "搜索",
|
||||
"select": "选择",
|
||||
"select_all": "全选",
|
||||
"selected": "已选择",
|
||||
"selectedItems": "已选择 {{count}} 项",
|
||||
"selectedMessages": "选中 {{count}} 条消息",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "輸入 JSON URL"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "批次刪除",
|
||||
"confirm": "您確定要刪除所選的 {{count}} 個助理嗎?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "刪除",
|
||||
"sort": "排序"
|
||||
},
|
||||
"title": "管理助手"
|
||||
},
|
||||
"my_agents": "我的助手",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "已儲存",
|
||||
"search": "搜尋",
|
||||
"select": "選擇",
|
||||
"select_all": "全選",
|
||||
"selected": "已選擇",
|
||||
"selectedItems": "已選擇 {{count}} 項",
|
||||
"selectedMessages": "選中 {{count}} 條訊息",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "JSON-URL eingeben"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Stapel löschen",
|
||||
"confirm": "Sind Sie sicher, dass Sie die ausgewählten {{count}} Assistenten löschen möchten?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Löschen",
|
||||
"sort": "Sortieren"
|
||||
},
|
||||
"title": "Assistenten verwalten"
|
||||
},
|
||||
"my_agents": "Meine Assistenten",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "Gespeichert",
|
||||
"search": "Suchen",
|
||||
"select": "Auswählen",
|
||||
"select_all": "Alle auswählen",
|
||||
"selected": "Ausgewählt",
|
||||
"selectedItems": "{{count}} Elemente ausgewählt",
|
||||
"selectedMessages": "{{count}} Nachrichten ausgewählt",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "Εισάγετε JSON URL"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Μαζική Διαγραφή",
|
||||
"confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους επιλεγμένους {{count}} βοηθούς;"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Διαγραφή",
|
||||
"sort": "Ταξινόμηση"
|
||||
},
|
||||
"title": "Διαχείριση βοηθών"
|
||||
},
|
||||
"my_agents": "Οι βοηθοί μου",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "Αποθηκεύτηκε",
|
||||
"search": "Αναζήτηση",
|
||||
"select": "Επιλογή",
|
||||
"select_all": "Επιλογή Όλων",
|
||||
"selected": "Επιλεγμένο",
|
||||
"selectedItems": "Επιλέχθηκαν {{count}} αντικείμενα",
|
||||
"selectedMessages": "Επιλέχθηκαν {{count}} μηνύματα",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "Introducir URL JSON"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Eliminación por lotes",
|
||||
"confirm": "¿Estás seguro de que quieres eliminar los {{count}} asistentes seleccionados?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Eliminar",
|
||||
"sort": "Ordenar"
|
||||
},
|
||||
"title": "Gestionar asistentes"
|
||||
},
|
||||
"my_agents": "Mis asistentes",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "Guardado",
|
||||
"search": "Buscar",
|
||||
"select": "Seleccionar",
|
||||
"select_all": "Seleccionar todo",
|
||||
"selected": "Seleccionado",
|
||||
"selectedItems": "{{count}} elementos seleccionados",
|
||||
"selectedMessages": "{{count}} mensajes seleccionados",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "Saisir l'URL JSON"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Suppression par lot",
|
||||
"confirm": "Êtes-vous sûr de vouloir supprimer les {{count}} assistants sélectionnés ?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Supprimer",
|
||||
"sort": "Trier"
|
||||
},
|
||||
"title": "Gérer les assistants"
|
||||
},
|
||||
"my_agents": "Mes assistants",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "enregistré",
|
||||
"search": "Rechercher",
|
||||
"select": "Sélectionner",
|
||||
"select_all": "Tout sélectionner",
|
||||
"selected": "Sélectionné",
|
||||
"selectedItems": "{{count}} éléments sélectionnés",
|
||||
"selectedMessages": "{{count}} messages sélectionnés",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "JSON URLを入力"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "バッチ削除",
|
||||
"confirm": "選択した{{count}}件のアシスタントを削除してもよろしいですか?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "削除",
|
||||
"sort": "並べ替え"
|
||||
},
|
||||
"title": "アシスタントを管理"
|
||||
},
|
||||
"my_agents": "マイアシスタント",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "保存されました",
|
||||
"search": "検索",
|
||||
"select": "選択",
|
||||
"select_all": "すべて選択",
|
||||
"selected": "選択済み",
|
||||
"selectedItems": "{{count}}件の項目を選択しました",
|
||||
"selectedMessages": "{{count}}件のメッセージを選択しました",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "Inserir URL JSON"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Exclusão em Lote",
|
||||
"confirm": "Tem certeza de que deseja excluir os {{count}} assistentes selecionados?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Excluir",
|
||||
"sort": "Ordenar"
|
||||
},
|
||||
"title": "Gerir assistentes"
|
||||
},
|
||||
"my_agents": "Os meus assistentes",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "Guardado",
|
||||
"search": "Pesquisar",
|
||||
"select": "Selecionar",
|
||||
"select_all": "Selecionar Tudo",
|
||||
"selected": "Selecionado",
|
||||
"selectedItems": "{{count}} itens selecionados",
|
||||
"selectedMessages": "{{count}} mensagens selecionadas",
|
||||
|
||||
@ -485,6 +485,14 @@
|
||||
"url_placeholder": "Введите JSON URL"
|
||||
},
|
||||
"manage": {
|
||||
"batch_delete": {
|
||||
"button": "Массовое удаление",
|
||||
"confirm": "Вы уверены, что хотите удалить выбранных {{count}} ассистентов?"
|
||||
},
|
||||
"mode": {
|
||||
"delete": "Удалить",
|
||||
"sort": "Сортировать"
|
||||
},
|
||||
"title": "Управление помощниками"
|
||||
},
|
||||
"my_agents": "Мои помощники",
|
||||
@ -1199,6 +1207,7 @@
|
||||
"saved": "Сохранено",
|
||||
"search": "Поиск",
|
||||
"select": "Выбрать",
|
||||
"select_all": "Выбрать все",
|
||||
"selected": "Выбрано",
|
||||
"selectedItems": "Выбрано {{count}} элементов",
|
||||
"selectedMessages": "Выбрано {{count}} сообщений",
|
||||
|
||||
@ -11,7 +11,7 @@ import type { AssistantPreset } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Button, Empty, Flex, Input } from 'antd'
|
||||
import { omit } from 'lodash'
|
||||
import { Import, Plus, Rss, Search } from 'lucide-react'
|
||||
import { Import, Plus, Rss, Search, Settings2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -25,6 +25,7 @@ import AssistantPresetCard from './components/AssistantPresetCard'
|
||||
import { AssistantPresetGroupIcon } from './components/AssistantPresetGroupIcon'
|
||||
import AssistantsSubscribeUrlSettings from './components/AssistantsSubscribeUrlSettings'
|
||||
import ImportAssistantPresetPopup from './components/ImportAssistantPresetPopup'
|
||||
import ManageAssistantPresetsPopup from './components/ManageAssistantPresetsPopup'
|
||||
|
||||
const AssistantPresetsPage: FC = () => {
|
||||
const [search, setSearch] = useState('')
|
||||
@ -185,6 +186,10 @@ const AssistantPresetsPage: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleManageAgents = () => {
|
||||
ManageAssistantPresetsPopup.show()
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
@ -290,6 +295,9 @@ const AssistantPresetsPage: FC = () => {
|
||||
<Button type="text" onClick={handleSubscribeSettings} icon={<Rss size={18} color="var(--color-icon)" />}>
|
||||
{t('assistants.presets.settings.title')}
|
||||
</Button>
|
||||
<Button type="text" onClick={handleManageAgents} icon={<Settings2 size={18} color="var(--color-icon)" />}>
|
||||
{t('assistants.presets.manage.title')}
|
||||
</Button>
|
||||
<Button type="text" onClick={handleAddAgent} icon={<Plus size={18} color="var(--color-icon)" />}>
|
||||
{t('assistants.presets.add.title')}
|
||||
</Button>
|
||||
|
||||
@ -8,7 +8,7 @@ import { getLeadingEmoji } from '@renderer/utils'
|
||||
import { Button, Dropdown } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { isArray } from 'lodash'
|
||||
import { ArrowDownAZ, Ellipsis, PlusIcon, SquareArrowOutUpRight } from 'lucide-react'
|
||||
import { Ellipsis, PlusIcon, Settings2, SquareArrowOutUpRight } from 'lucide-react'
|
||||
import { type FC, memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -77,9 +77,9 @@ const AssistantPresetCard: FC<Props> = ({ preset, onClick, activegroup, getLocal
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'sort',
|
||||
label: t('assistants.presets.sorting.title'),
|
||||
icon: <ArrowDownAZ size={14} />,
|
||||
key: 'manage',
|
||||
label: t('assistants.presets.manage.title'),
|
||||
icon: <Settings2 size={14} />,
|
||||
onClick: (e: any) => {
|
||||
e.domEvent.stopPropagation()
|
||||
ManageAssistantPresetsPopup.show()
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import { MenuOutlined } from '@ant-design/icons'
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { Box, HStack } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { Empty, Modal } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { AssistantPreset } from '@renderer/types'
|
||||
import { Button, Checkbox, Empty, Modal, Segmented } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Mode = 'sort' | 'delete'
|
||||
|
||||
const PopupContainer: React.FC = () => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const { presets, setAssistantPresets } = useAssistantPresets()
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
const [mode, setMode] = useState<Mode>(() => (presets.length > 50 ? 'delete' : 'sort'))
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
@ -25,17 +27,74 @@ const PopupContainer: React.FC = () => {
|
||||
ManageAssistantPresetsPopup.hide()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (presets.length === 0) {
|
||||
setOpen(false)
|
||||
const handleModeChange = (value: Mode) => {
|
||||
setMode(value)
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
}, [presets])
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedIds.size === presets.length) {
|
||||
setSelectedIds(new Set())
|
||||
} else {
|
||||
setSelectedIds(new Set(presets.map((p) => p.id)))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectNext100 = () => {
|
||||
// Find the last selected preset's index
|
||||
let startIndex = 0
|
||||
if (selectedIds.size > 0) {
|
||||
for (let i = presets.length - 1; i >= 0; i--) {
|
||||
if (selectedIds.has(presets[i].id)) {
|
||||
startIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select next 100 unselected presets starting from startIndex
|
||||
const newSelected = new Set(selectedIds)
|
||||
let count = 0
|
||||
for (let i = startIndex; i < presets.length && count < 100; i++) {
|
||||
if (!newSelected.has(presets[i].id)) {
|
||||
newSelected.add(presets[i].id)
|
||||
count++
|
||||
}
|
||||
}
|
||||
setSelectedIds(newSelected)
|
||||
}
|
||||
|
||||
const handleSelect = (preset: AssistantPreset) => {
|
||||
const newSelected = new Set(selectedIds)
|
||||
if (newSelected.has(preset.id)) {
|
||||
newSelected.delete(preset.id)
|
||||
} else {
|
||||
newSelected.add(preset.id)
|
||||
}
|
||||
setSelectedIds(newSelected)
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedIds.size === 0) return
|
||||
|
||||
window.modal.confirm({
|
||||
centered: true,
|
||||
content: t('assistants.presets.manage.batch_delete.confirm', { count: selectedIds.size }),
|
||||
onOk: () => {
|
||||
const remainingPresets = presets.filter((p) => !selectedIds.has(p.id))
|
||||
setAssistantPresets(remainingPresets)
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isAllSelected = presets.length > 0 && selectedIds.size === presets.length
|
||||
const isIndeterminate = selectedIds.size > 0 && selectedIds.size < presets.length
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('assistants.presets.manage.title')}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
footer={null}
|
||||
@ -43,6 +102,47 @@ const PopupContainer: React.FC = () => {
|
||||
centered>
|
||||
<Container>
|
||||
{presets.length > 0 && (
|
||||
<>
|
||||
<ActionBar>
|
||||
{mode === 'delete' ? (
|
||||
<HStack alignItems="center">
|
||||
<Checkbox checked={isAllSelected} indeterminate={isIndeterminate} onChange={handleSelectAll}>
|
||||
{t('common.select_all')}
|
||||
</Checkbox>
|
||||
{presets.length > 100 && selectedIds.size < presets.length && (
|
||||
<Button type="link" size="small" onClick={handleSelectNext100} style={{ padding: 0 }}>
|
||||
+100
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<HStack gap="8px" alignItems="center">
|
||||
{mode === 'delete' && (
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
icon={<DeleteIcon size={14} />}
|
||||
disabled={selectedIds.size === 0}
|
||||
onClick={handleBatchDelete}>
|
||||
{t('assistants.presets.manage.batch_delete.button')} ({selectedIds.size})
|
||||
</Button>
|
||||
)}
|
||||
<Segmented
|
||||
size="small"
|
||||
value={mode}
|
||||
onChange={(value) => handleModeChange(value as Mode)}
|
||||
options={[
|
||||
{ label: t('assistants.presets.manage.mode.sort'), value: 'sort' },
|
||||
{ label: t('assistants.presets.manage.mode.delete'), value: 'delete' }
|
||||
]}
|
||||
/>
|
||||
</HStack>
|
||||
</ActionBar>
|
||||
|
||||
{mode === 'sort' ? (
|
||||
<AgentList>
|
||||
<DraggableList list={presets} onUpdate={setAssistantPresets}>
|
||||
{(item) => (
|
||||
<AgentItem>
|
||||
@ -55,6 +155,25 @@ const PopupContainer: React.FC = () => {
|
||||
</AgentItem>
|
||||
)}
|
||||
</DraggableList>
|
||||
</AgentList>
|
||||
) : (
|
||||
<AgentList>
|
||||
{presets.map((item) => (
|
||||
<SelectableAgentItem
|
||||
key={item.id}
|
||||
onClick={() => handleSelect(item)}
|
||||
$selected={selectedIds.has(item.id)}>
|
||||
<HStack alignItems="center" gap="8px">
|
||||
<Checkbox checked={selectedIds.has(item.id)} onChange={() => handleSelect(item)} />
|
||||
<Box>
|
||||
{item.emoji} {item.name}
|
||||
</Box>
|
||||
</HStack>
|
||||
</SelectableAgentItem>
|
||||
))}
|
||||
</AgentList>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{presets.length === 0 && <Empty description="" />}
|
||||
</Container>
|
||||
@ -65,6 +184,21 @@ const PopupContainer: React.FC = () => {
|
||||
const Container = styled.div`
|
||||
padding: 12px 0;
|
||||
height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const ActionBar = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 12px;
|
||||
`
|
||||
|
||||
const AgentList = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@ -90,6 +224,23 @@ const AgentItem = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const SelectableAgentItem = styled.div<{ $selected: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
background-color: ${(props) => (props.$selected ? 'var(--color-primary-mute)' : 'var(--color-background-soft)')};
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
&:hover {
|
||||
background-color: ${(props) => (props.$selected ? 'var(--color-primary-mute)' : 'var(--color-background-mute)')};
|
||||
}
|
||||
`
|
||||
|
||||
export default class ManageAssistantPresetsPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user