mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
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:
parent
99d7223a0a
commit
e85009fcd6
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": "警告",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Προσοχή",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "警告",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Предупреждение",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -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,35 +24,53 @@ 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))
|
||||
if (!Array.isArray(presets)) {
|
||||
presets = [presets]
|
||||
}
|
||||
} else {
|
||||
return
|
||||
presets = JSON.parse(new TextDecoder('utf-8').decode(selectedFile!.content))
|
||||
if (!Array.isArray(presets)) {
|
||||
presets = [presets]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
<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 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>
|
||||
|
||||
{importType === 'url' && (
|
||||
<Form.Item
|
||||
name="url"
|
||||
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' && (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{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')} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{importType === 'file' && (
|
||||
<Form.Item>
|
||||
<Button onClick={() => form.submit()}>{t('assistants.presets.import.select_file')}</Button>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,15 +137,24 @@ const PopupContainer: React.FC = () => {
|
||||
<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>
|
||||
{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"
|
||||
icon={<DeleteIcon size={14} />}
|
||||
disabled={selectedIds.size === 0}
|
||||
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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user