feat(assistants): merge import/subscribe popups and add export to manage (#11946)

feat(assistants): merge import and subscribe popups, add export to manage

- Merge import and subscribe buttons into single unified popup
- Add export functionality to manage assistant presets
- Change delete mode to manage mode with both export and delete options
- Show import count in success message
- Default to manage mode when opening manage popup
- Fix unsubscribe button to clear URL properly
- Fix file import not working issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
亢奋猫 2025-12-17 17:54:44 +08:00 committed by GitHub
parent 99d7223a0a
commit e85009fcd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 260 additions and 148 deletions

View File

@ -472,6 +472,7 @@
"button": "Import",
"error": {
"fetch_failed": "Failed to fetch from URL",
"file_required": "Please select a file first",
"invalid_format": "Invalid assistant format: missing required fields",
"url_required": "Please enter a URL"
},
@ -486,11 +487,14 @@
},
"manage": {
"batch_delete": {
"button": "Batch Delete",
"button": "Delete",
"confirm": "Are you sure you want to delete the selected {{count}} assistants?"
},
"batch_export": {
"button": "Export"
},
"mode": {
"delete": "Delete",
"manage": "Manage",
"sort": "Sort"
},
"title": "Manage Assistants"
@ -1248,11 +1252,13 @@
}
},
"stop": "Stop",
"subscribe": "Subscribe",
"success": "Success",
"swap": "Swap",
"topics": "Topics",
"unknown": "Unknown",
"unnamed": "Unnamed",
"unsubscribe": "Unsubscribe",
"update_success": "Update successfully",
"upload_files": "Upload file",
"warning": "Warning",
@ -1747,7 +1753,7 @@
"import": {
"error": "Import failed"
},
"imported": "Imported successfully"
"imported": "Successfully imported {{count}} assistant(s)"
},
"api": {
"check": {

View File

@ -472,6 +472,7 @@
"button": "导入",
"error": {
"fetch_failed": "从 URL 获取数据失败",
"file_required": "请先选择文件",
"invalid_format": "无效的助手格式:缺少必填字段",
"url_required": "请输入 URL"
},
@ -486,11 +487,14 @@
},
"manage": {
"batch_delete": {
"button": "批量删除",
"button": "删除",
"confirm": "确定要删除选中的 {{count}} 个助手吗?"
},
"batch_export": {
"button": "导出"
},
"mode": {
"delete": "删除",
"manage": "管理",
"sort": "排序"
},
"title": "管理助手"
@ -1248,11 +1252,13 @@
}
},
"stop": "停止",
"subscribe": "订阅",
"success": "成功",
"swap": "交换",
"topics": "话题",
"unknown": "未知",
"unnamed": "未命名",
"unsubscribe": "退订",
"update_success": "更新成功",
"upload_files": "上传文件",
"warning": "警告",
@ -1747,7 +1753,7 @@
"import": {
"error": "导入失败"
},
"imported": "导入成功"
"imported": "成功导入 {{count}} 个助手"
},
"api": {
"check": {

View File

@ -472,6 +472,7 @@
"button": "匯入",
"error": {
"fetch_failed": "從 URL 取得資料失敗",
"file_required": "請先選擇一個檔案",
"invalid_format": "無效的助手格式:缺少必填欄位",
"url_required": "請輸入 URL"
},
@ -489,8 +490,11 @@
"button": "批次刪除",
"confirm": "確定要刪除所選的 {{count}} 個助手嗎?"
},
"batch_export": {
"button": "匯出"
},
"mode": {
"delete": "刪除",
"manage": "管理",
"sort": "排序"
},
"title": "管理助手"
@ -1248,11 +1252,13 @@
}
},
"stop": "停止",
"subscribe": "訂閱",
"success": "成功",
"swap": "交換",
"topics": "話題",
"unknown": "未知",
"unnamed": "未命名",
"unsubscribe": "取消訂閱",
"update_success": "更新成功",
"upload_files": "上傳檔案",
"warning": "警告",

View File

@ -472,6 +472,7 @@
"button": "Importieren",
"error": {
"fetch_failed": "Daten von URL abrufen fehlgeschlagen",
"file_required": "Bitte wählen Sie zuerst eine Datei aus",
"invalid_format": "Ungültiges Assistentenformat: Pflichtfelder fehlen",
"url_required": "Bitte geben Sie eine URL ein"
},
@ -489,8 +490,11 @@
"button": "Stapel löschen",
"confirm": "Sind Sie sicher, dass Sie die ausgewählten {{count}} Assistenten löschen möchten?"
},
"batch_export": {
"button": "Exportieren"
},
"mode": {
"delete": "Löschen",
"manage": "Verwalten",
"sort": "Sortieren"
},
"title": "Assistenten verwalten"
@ -1248,11 +1252,13 @@
}
},
"stop": "Stoppen",
"subscribe": "Abonnieren",
"success": "Erfolgreich",
"swap": "Tauschen",
"topics": "Themen",
"unknown": "Unbekannt",
"unnamed": "Unbenannt",
"unsubscribe": "Abmelden",
"update_success": "Erfolgreich aktualisiert",
"upload_files": "Dateien hochladen",
"warning": "Warnung",

View File

@ -472,6 +472,7 @@
"button": "Εισαγωγή",
"error": {
"fetch_failed": "Αποτυχία λήψης δεδομένων από το URL",
"file_required": "Παρακαλώ επιλέξτε πρώτα ένα αρχείο",
"invalid_format": "Μη έγκυρη μορφή βοηθού: λείπουν υποχρεωτικά πεδία",
"url_required": "Παρακαλώ εισάγετε ένα URL"
},
@ -489,8 +490,11 @@
"button": "Μαζική Διαγραφή",
"confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους επιλεγμένους {{count}} βοηθούς;"
},
"batch_export": {
"button": "Εξαγωγή"
},
"mode": {
"delete": "Διαγραφή",
"manage": "Διαχειριστείτε",
"sort": "Ταξινόμηση"
},
"title": "Διαχείριση βοηθών"
@ -1248,11 +1252,13 @@
}
},
"stop": "σταματήστε",
"subscribe": "Εγγραφείτε",
"success": "Επιτυχία",
"swap": "Εναλλαγή",
"topics": "Θέματα",
"unknown": "Άγνωστο",
"unnamed": "Χωρίς όνομα",
"unsubscribe": "Απεγγραφή",
"update_success": "Επιτυχής ενημέρωση",
"upload_files": "Ανέβασμα αρχείου",
"warning": "Προσοχή",

View File

@ -472,6 +472,7 @@
"button": "Importar",
"error": {
"fetch_failed": "Error al obtener datos desde la URL",
"file_required": "Por favor, selecciona primero un archivo",
"invalid_format": "Formato de asistente inválido: faltan campos obligatorios",
"url_required": "Por favor introduce una URL"
},
@ -489,8 +490,11 @@
"button": "Eliminación por lotes",
"confirm": "¿Estás seguro de que quieres eliminar los {{count}} asistentes seleccionados?"
},
"batch_export": {
"button": "Exportar"
},
"mode": {
"delete": "Eliminar",
"manage": "Gestionar",
"sort": "Ordenar"
},
"title": "Gestionar asistentes"
@ -1248,11 +1252,13 @@
}
},
"stop": "Detener",
"subscribe": "Suscribirse",
"success": "Éxito",
"swap": "Intercambiar",
"topics": "Temas",
"unknown": "Desconocido",
"unnamed": "Sin nombre",
"unsubscribe": "Cancelar suscripción",
"update_success": "Actualización exitosa",
"upload_files": "Subir archivo",
"warning": "Advertencia",

View File

@ -472,6 +472,7 @@
"button": "Importer",
"error": {
"fetch_failed": "Échec de la récupération des données depuis l'URL",
"file_required": "Veuillez d'abord sélectionner un fichier",
"invalid_format": "Format d'assistant invalide : champs obligatoires manquants",
"url_required": "Veuillez saisir une URL"
},
@ -489,8 +490,11 @@
"button": "Suppression par lot",
"confirm": "Êtes-vous sûr de vouloir supprimer les {{count}} assistants sélectionnés ?"
},
"batch_export": {
"button": "Exporter"
},
"mode": {
"delete": "Supprimer",
"manage": "Gérer",
"sort": "Trier"
},
"title": "Gérer les assistants"
@ -1248,11 +1252,13 @@
}
},
"stop": "Arrêter",
"subscribe": "S'abonner",
"success": "Succès",
"swap": "Échanger",
"topics": "Sujets",
"unknown": "Inconnu",
"unnamed": "Sans nom",
"unsubscribe": "Se désabonner",
"update_success": "Mise à jour réussie",
"upload_files": "Uploader des fichiers",
"warning": "Avertissement",

View File

@ -472,6 +472,7 @@
"button": "インポート",
"error": {
"fetch_failed": "URLからのデータ取得に失敗しました",
"file_required": "まずファイルを選択してください",
"invalid_format": "無効なアシスタント形式:必須フィールドが不足しています",
"url_required": "URLを入力してください"
},
@ -489,8 +490,11 @@
"button": "バッチ削除",
"confirm": "選択した{{count}}件のアシスタントを削除してもよろしいですか?"
},
"batch_export": {
"button": "エクスポート"
},
"mode": {
"delete": "削除",
"manage": "管理",
"sort": "並べ替え"
},
"title": "アシスタントを管理"
@ -1248,11 +1252,13 @@
}
},
"stop": "停止",
"subscribe": "購読",
"success": "成功",
"swap": "交換",
"topics": "トピック",
"unknown": "Unknown",
"unnamed": "無題",
"unsubscribe": "配信停止",
"update_success": "更新成功",
"upload_files": "ファイルをアップロードする",
"warning": "警告",

View File

@ -472,6 +472,7 @@
"button": "Importar",
"error": {
"fetch_failed": "Falha ao obter dados do URL",
"file_required": "Por favor, selecione um arquivo primeiro",
"invalid_format": "Formato de assistente inválido: campos obrigatórios em falta",
"url_required": "Por favor insere um URL"
},
@ -489,8 +490,11 @@
"button": "Exclusão em Lote",
"confirm": "Tem certeza de que deseja excluir os {{count}} assistentes selecionados?"
},
"batch_export": {
"button": "Exportar"
},
"mode": {
"delete": "Excluir",
"manage": "Gerenciar",
"sort": "Ordenar"
},
"title": "Gerir assistentes"
@ -1248,11 +1252,13 @@
}
},
"stop": "Parar",
"subscribe": "Subscrever",
"success": "Sucesso",
"swap": "Trocar",
"topics": "Tópicos",
"unknown": "Desconhecido",
"unnamed": "Sem nome",
"unsubscribe": "Cancelar inscrição",
"update_success": "Atualização bem-sucedida",
"upload_files": "Carregar arquivo",
"warning": "Aviso",

View File

@ -472,6 +472,7 @@
"button": "Импортировать",
"error": {
"fetch_failed": "Ошибка получения данных с URL",
"file_required": "Сначала выберите файл",
"invalid_format": "Неверный формат помощника: отсутствуют обязательные поля",
"url_required": "Пожалуйста, введите URL"
},
@ -489,8 +490,11 @@
"button": "Массовое удаление",
"confirm": "Вы уверены, что хотите удалить выбранных {{count}} ассистентов?"
},
"batch_export": {
"button": "Экспорт"
},
"mode": {
"delete": "Удалить",
"manage": "Управлять",
"sort": "Сортировать"
},
"title": "Управление помощниками"
@ -1248,11 +1252,13 @@
}
},
"stop": "остановить",
"subscribe": "Подписаться",
"success": "Успешно",
"swap": "Поменять местами",
"topics": "Топики",
"unknown": "Неизвестно",
"unnamed": "Без имени",
"unsubscribe": "Отписаться",
"update_success": "Обновление выполнено успешно",
"upload_files": "Загрузить файл",
"warning": "Предупреждение",

View File

@ -1,7 +1,6 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem'
import GeneralPopup from '@renderer/components/Popups/GeneralPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
@ -11,7 +10,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, Settings2 } from 'lucide-react'
import { Import, Plus, Search, Settings2 } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -23,7 +22,6 @@ import { groupTranslations } from './assistantPresetGroupTranslations'
import AddAssistantPresetPopup from './components/AddAssistantPresetPopup'
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'
@ -177,15 +175,6 @@ const AssistantPresetsPage: FC = () => {
}
}
const handleSubscribeSettings = () => {
GeneralPopup.show({
title: t('assistants.presets.settings.title'),
content: <AssistantsSubscribeUrlSettings />,
footer: null,
width: 600
})
}
const handleManageAgents = () => {
ManageAssistantPresetsPopup.show()
}
@ -292,9 +281,6 @@ const AssistantPresetsPage: FC = () => {
<Button type="text" onClick={handleImportAgent} icon={<Import size={18} color="var(--color-icon)" />}>
{t('assistants.presets.import.title')}
</Button>
<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>

View File

@ -1,58 +0,0 @@
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import { setAgentssubscribeUrl } from '@renderer/store/settings'
import Input from 'antd/es/input/Input'
import { HelpCircle } from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
const AssistantsSubscribeUrlSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { agentssubscribeUrl } = useSettings()
const handleAgentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setAgentssubscribeUrl(e.target.value))
}
const handleHelpClick = () => {
window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank')
}
return (
<SettingGroup theme={theme}>
<HStack alignItems="center" gap="8px">
<SettingTitle>
{t('assistants.presets.tag.agent')}
{t('settings.tool.websearch.subscribe_add')}
</SettingTitle>
<HelpCircle
size={16}
color="var(--color-icon)"
onClick={handleHelpClick}
className="hover:!text-[var(--color-primary)] cursor-pointer transition-colors"
/>
</HStack>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tool.websearch.subscribe_url')}</SettingRowTitle>
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
<Input
type="text"
value={agentssubscribeUrl || ''}
onChange={handleAgentChange}
style={{ width: 315 }}
placeholder={t('settings.tool.websearch.subscribe_url')}
/>
</HStack>
</SettingRow>
</SettingGroup>
)
}
export default AssistantsSubscribeUrlSettings

View File

@ -1,11 +1,15 @@
import { TopView } from '@renderer/components/TopView'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import { getDefaultModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setAgentssubscribeUrl } from '@renderer/store/settings'
import type { AssistantPreset } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Button, Flex, Form, Input, Modal, Radio } from 'antd'
import { Button, Divider, Flex, Form, Input, Modal, Radio, Typography } from 'antd'
import { HelpCircle } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -20,36 +24,54 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { addAssistantPreset } = useAssistantPresets()
const [importType, setImportType] = useState<'url' | 'file'>('url')
const [loading, setLoading] = useState(false)
const [subscribeLoading, setSubscribeLoading] = useState(false)
const { setTimeoutTimer } = useTimer()
const dispatch = useAppDispatch()
const { agentssubscribeUrl } = useSettings()
const [subscribeUrl, setSubscribeUrl] = useState(agentssubscribeUrl || '')
const [selectedFile, setSelectedFile] = useState<{ name: string; content: Uint8Array } | null>(null)
const [urlValue, setUrlValue] = useState('')
const isImportDisabled = importType === 'url' ? !urlValue.trim() : !selectedFile
const isSubscribed = !!agentssubscribeUrl
const handleSelectFile = async () => {
const result = await window.api.file.open({
filters: [{ name: t('assistants.presets.import.file_filter'), extensions: ['json'] }]
})
if (result) {
setSelectedFile({ name: result.fileName, content: result.content })
}
}
const onFinish = async () => {
// Validate before setting loading
if (importType === 'url' && !urlValue.trim()) {
window.toast.error(t('assistants.presets.import.error.url_required'))
return
}
if (importType === 'file' && !selectedFile) {
window.toast.error(t('assistants.presets.import.error.file_required'))
return
}
const onFinish = async (values: { url?: string }) => {
setLoading(true)
try {
let presets: AssistantPreset[] = []
if (importType === 'url') {
if (!values.url) {
throw new Error(t('assistants.presets.import.error.url_required'))
}
const response = await fetch(values.url)
const response = await fetch(urlValue.trim())
if (!response.ok) {
throw new Error(t('assistants.presets.import.error.fetch_failed'))
}
const data = await response.json()
presets = Array.isArray(data) ? data : [data]
} else {
const result = await window.api.file.open({
filters: [{ name: t('assistants.presets.import.file_filter'), extensions: ['json'] }]
})
if (result) {
presets = JSON.parse(new TextDecoder('utf-8').decode(result.content))
presets = JSON.parse(new TextDecoder('utf-8').decode(selectedFile!.content))
if (!Array.isArray(presets)) {
presets = [presets]
}
} else {
return
}
}
// Validate and process agents
@ -74,7 +96,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
addAssistantPreset(newPreset)
}
window.toast.success(t('message.agents.imported'))
window.toast.success(t('message.agents.imported', { count: presets.length }))
setTimeoutTimer('onFinish', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
setOpen(false)
@ -88,7 +110,42 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const onCancel = () => {
setOpen(false)
resolve(null)
}
const handleSubscribeUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSubscribeUrl(e.target.value)
}
const handleSubscribe = async () => {
// If already subscribed, unsubscribe
if (isSubscribed) {
dispatch(setAgentssubscribeUrl(''))
setSubscribeUrl('')
window.location.reload()
return
}
if (!subscribeUrl.trim()) {
return
}
setSubscribeLoading(true)
try {
const response = await fetch(subscribeUrl)
if (!response.ok) {
throw new Error(t('assistants.presets.import.error.fetch_failed'))
}
dispatch(setAgentssubscribeUrl(subscribeUrl))
window.location.reload()
} catch (error) {
window.toast.error(error instanceof Error ? error.message : t('message.agents.import.error'))
} finally {
setSubscribeLoading(false)
}
}
const handleHelpClick = () => {
window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank')
}
return (
@ -96,39 +153,79 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
title={t('assistants.presets.import.title')}
open={open}
onCancel={onCancel}
maskClosable={false}
footer={
<Flex justify="end" gap={8}>
<Button onClick={onCancel}>{t('common.cancel')}</Button>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t('assistants.presets.import.button')}
</Button>
</Flex>
}
afterClose={() => resolve(null)}
footer={null}
transitionName="animation-move-down"
styles={{ body: { padding: '16px' } }}
centered>
<Form form={form} onFinish={onFinish} layout="vertical">
<Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Flex align="center" gap={12} style={{ width: '100%' }}>
<Radio.Group value={importType} onChange={(e) => setImportType(e.target.value)}>
<Radio.Button value="url">{t('assistants.presets.import.type.url')}</Radio.Button>
<Radio.Button value="file">{t('assistants.presets.import.type.file')}</Radio.Button>
</Radio.Group>
</Form.Item>
{importType === 'url' && (
<Form.Item
name="url"
rules={[{ required: true, message: t('assistants.presets.import.error.url_required') }]}>
<Input placeholder={t('assistants.presets.import.url_placeholder')} />
rules={[{ required: true, message: t('assistants.presets.import.error.url_required') }]}
style={{ flex: 1, marginBottom: 0 }}>
<Input
placeholder={t('assistants.presets.import.url_placeholder')}
value={urlValue}
onChange={(e) => setUrlValue(e.target.value)}
/>
</Form.Item>
)}
{importType === 'file' && (
<Form.Item>
<Button onClick={() => form.submit()}>{t('assistants.presets.import.select_file')}</Button>
</Form.Item>
<>
<Button onClick={handleSelectFile}>{t('assistants.presets.import.select_file')}</Button>
{selectedFile && (
<Typography.Text type="secondary" ellipsis style={{ maxWidth: 200 }}>
{selectedFile.name}
</Typography.Text>
)}
<div style={{ flex: 1 }} />
</>
)}
<Button type="primary" onClick={onFinish} loading={loading} disabled={isImportDisabled}>
{t('assistants.presets.import.button')}
</Button>
</Flex>
</Form.Item>
</Form>
<Divider style={{ margin: '16px 0' }} />
<Flex align="center" gap={4}>
<Typography.Text strong style={{ flexShrink: 0, fontSize: 16 }}>
{t('assistants.presets.tag.agent')}
{t('settings.tool.websearch.subscribe_add')}
</Typography.Text>
<HelpCircle
size={16}
color="var(--color-icon)"
onClick={handleHelpClick}
className="hover:!text-[var(--color-primary)] cursor-pointer transition-colors"
style={{ flexShrink: 0 }}
/>
</Flex>
<Flex align="center" gap={12} style={{ marginTop: 10 }}>
<Input
type="text"
value={subscribeUrl}
onChange={handleSubscribeUrlChange}
style={{ flex: 1 }}
placeholder={t('settings.tool.websearch.subscribe_url')}
/>
<Button type="primary" onClick={handleSubscribe} loading={subscribeLoading} disabled={!subscribeUrl.trim()}>
{isSubscribed ? t('common.unsubscribe') : t('common.subscribe')}
</Button>
</Flex>
</Modal>
)
}

View File

@ -1,4 +1,4 @@
import { MenuOutlined } from '@ant-design/icons'
import { ExportOutlined, MenuOutlined } from '@ant-design/icons'
import { DraggableList } from '@renderer/components/DraggableList'
import { DeleteIcon } from '@renderer/components/Icons'
import { Box, HStack } from '@renderer/components/Layout'
@ -10,13 +10,13 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
type Mode = 'sort' | 'delete'
type Mode = 'sort' | 'manage'
const PopupContainer: React.FC = () => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { presets, setAssistantPresets } = useAssistantPresets()
const [mode, setMode] = useState<Mode>(() => (presets.length > 50 ? 'delete' : 'sort'))
const [mode, setMode] = useState<Mode>('manage')
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const onCancel = () => {
@ -88,6 +88,23 @@ const PopupContainer: React.FC = () => {
})
}
const handleBatchExport = async () => {
if (selectedIds.size === 0) return
const selectedPresets = presets.filter((p) => selectedIds.has(p.id))
const exportData = selectedPresets.map((p) => ({
name: p.name,
emoji: p.emoji,
prompt: p.prompt,
description: p.description,
group: p.group
}))
const fileName = selectedIds.size === 1 ? `${selectedPresets[0].name}.json` : `assistants_${selectedIds.size}.json`
await window.api.file.save(fileName, JSON.stringify(exportData, null, 2))
}
const isAllSelected = presets.length > 0 && selectedIds.size === presets.length
const isIndeterminate = selectedIds.size > 0 && selectedIds.size < presets.length
@ -98,13 +115,14 @@ const PopupContainer: React.FC = () => {
onCancel={onCancel}
afterClose={onClose}
footer={null}
width={600}
transitionName="animation-move-down"
centered>
<Container>
{presets.length > 0 && (
<>
<ActionBar>
{mode === 'delete' ? (
{mode === 'manage' ? (
<HStack alignItems="center">
<Checkbox checked={isAllSelected} indeterminate={isIndeterminate} onChange={handleSelectAll}>
{t('common.select_all')}
@ -119,7 +137,15 @@ const PopupContainer: React.FC = () => {
<div />
)}
<HStack gap="8px" alignItems="center">
{mode === 'delete' && (
{mode === 'manage' && (
<>
<Button
type="text"
icon={<ExportOutlined />}
disabled={selectedIds.size === 0}
onClick={handleBatchExport}>
{t('assistants.presets.manage.batch_export.button')} ({selectedIds.size})
</Button>
<Button
danger
type="text"
@ -128,6 +154,7 @@ const PopupContainer: React.FC = () => {
onClick={handleBatchDelete}>
{t('assistants.presets.manage.batch_delete.button')} ({selectedIds.size})
</Button>
</>
)}
<Segmented
size="small"
@ -135,7 +162,7 @@ const PopupContainer: React.FC = () => {
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' }
{ label: t('assistants.presets.manage.mode.manage'), value: 'manage' }
]}
/>
</HStack>