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:
亢奋猫 2025-12-11 16:50:52 +08:00 committed by GitHub
parent a91c69982c
commit 595a0f194a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 275 additions and 26 deletions

View File

@ -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",

View File

@ -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}} 条消息",

View File

@ -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}} 條訊息",

View File

@ -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",

View File

@ -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}} μηνύματα",

View File

@ -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",

View File

@ -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",

View File

@ -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}}件のメッセージを選択しました",

View File

@ -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",

View File

@ -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}} сообщений",

View File

@ -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>

View File

@ -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()

View File

@ -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())
}
const handleSelectAll = () => {
if (selectedIds.size === presets.length) {
setSelectedIds(new Set())
} else {
setSelectedIds(new Set(presets.map((p) => p.id)))
}
}, [presets])
}
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,18 +102,78 @@ const PopupContainer: React.FC = () => {
centered>
<Container>
{presets.length > 0 && (
<DraggableList list={presets} onUpdate={setAssistantPresets}>
{(item) => (
<AgentItem>
<Box mr={8}>
{item.emoji} {item.name}
</Box>
<HStack gap="15px">
<MenuOutlined style={{ cursor: 'move' }} />
<>
<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>
</AgentItem>
) : (
<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>
<Box mr={8}>
{item.emoji} {item.name}
</Box>
<HStack gap="15px">
<MenuOutlined style={{ cursor: 'move' }} />
</HStack>
</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>
)}
</DraggableList>
</>
)}
{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() {