mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 17:47:06 +08:00
perf: improve ui (#7178)
* perf: enhance layout responsiveness and text handling in stats page * perf: enhance subagent, future task UI - Updated the overall structure of the SubAgentPage component for better readability and maintainability. - Introduced a new dashboard layout with improved header and action buttons. - Replaced the old settings card with a more visually appealing setting card design. - Enhanced the agent list section with a more user-friendly display and added empty state handling. - Implemented unsaved changes notification and confirmation dialogs for better user interaction. - Refactored code for better clarity and organization, including the use of computed properties for state management.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 252 icons */
|
||||
/* Auto-generated MDI subset – 256 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -128,10 +128,22 @@
|
||||
content: "\F00ED";
|
||||
}
|
||||
|
||||
.mdi-calendar-blank-outline::before {
|
||||
content: "\F0B66";
|
||||
}
|
||||
|
||||
.mdi-calendar-clock-outline::before {
|
||||
content: "\F16E1";
|
||||
}
|
||||
|
||||
.mdi-calendar-edit::before {
|
||||
content: "\F08A7";
|
||||
}
|
||||
|
||||
.mdi-calendar-multiple::before {
|
||||
content: "\F00F1";
|
||||
}
|
||||
|
||||
.mdi-calendar-plus::before {
|
||||
content: "\F00F3";
|
||||
}
|
||||
@@ -832,6 +844,10 @@
|
||||
content: "\F048A";
|
||||
}
|
||||
|
||||
.mdi-send-clock-outline::before {
|
||||
content: "\F1164";
|
||||
}
|
||||
|
||||
.mdi-server::before {
|
||||
content: "\F048B";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -31,6 +31,10 @@
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"umoDisplay": {
|
||||
"parsed": "Parsed",
|
||||
"raw": "UMO"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
@@ -97,6 +101,8 @@
|
||||
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed",
|
||||
"exportSuccess": "Export successful",
|
||||
"exportError": "Export failed",
|
||||
"noItemSelectedForExport": "Please select conversations to export first"
|
||||
"noItemSelectedForExport": "Please select conversations to export first",
|
||||
"copySuccess": "Copied",
|
||||
"copyError": "Copy failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"header": {
|
||||
"eyebrow": "Automation",
|
||||
"live": "Live Sync"
|
||||
},
|
||||
"page": {
|
||||
"title": "Future Task Management",
|
||||
"beta": "Experimental",
|
||||
"subtitle": "See scheduled tasks for AstrBot. AstrBot will wake up, run them, and deliver the results.",
|
||||
"proactive": {
|
||||
"supported": "Proactive delivery is available on: {platforms}",
|
||||
"supported": "Proactive delivery is available only on the configured platforms below",
|
||||
"unsupported": "No proactive messaging platforms enabled. Turn them on in Platform settings."
|
||||
}
|
||||
},
|
||||
@@ -15,8 +19,41 @@
|
||||
"cancel": "Cancel",
|
||||
"submit": "Create"
|
||||
},
|
||||
"overview": {
|
||||
"totalTasks": "Total Tasks",
|
||||
"totalTasksNote": "Registered future tasks",
|
||||
"enabledTasks": "Enabled Tasks",
|
||||
"enabledTasksNote": "Tasks ready to wake and run",
|
||||
"oneOffTasks": "One-off Tasks",
|
||||
"recurringTasksNote": "{count} recurring tasks",
|
||||
"proactivePlatforms": "Proactive Platforms",
|
||||
"proactivePlatformsNote": "Platforms able to deliver results proactively"
|
||||
},
|
||||
"section": {
|
||||
"registered": {
|
||||
"title": "Task List",
|
||||
"subtitle": "Review the registered tasks, execution timing, and current state"
|
||||
},
|
||||
"delivery": {
|
||||
"title": "Delivery Status",
|
||||
"subtitle": "After a task runs, results are proactively sent back through supported platforms",
|
||||
"support": "Proactive delivery",
|
||||
"available": "Available",
|
||||
"unavailable": "Unavailable",
|
||||
"enabledPlatforms": "Enabled platforms"
|
||||
},
|
||||
"quickCreate": {
|
||||
"title": "Quick Create",
|
||||
"runMode": "Task mode",
|
||||
"target": "Targeting"
|
||||
},
|
||||
"platforms": {
|
||||
"title": "Supported Platforms"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"title": "Registered Tasks",
|
||||
"subtitle": "Track cron setup, session target, last run history, and enabled state",
|
||||
"empty": "No tasks yet.",
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"header": {
|
||||
"eyebrow": "Orchestration"
|
||||
},
|
||||
"page": {
|
||||
"title": "SubAgent Orchestration",
|
||||
"beta": "Experimental",
|
||||
@@ -8,9 +11,20 @@
|
||||
"refresh": "Refresh",
|
||||
"save": "Save",
|
||||
"add": "Add SubAgent",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse",
|
||||
"delete": "Delete",
|
||||
"close": "Close"
|
||||
},
|
||||
"overview": {
|
||||
"totalAgents": "Total SubAgents",
|
||||
"totalAgentsNote": "Configured delegate agents",
|
||||
"enabledAgents": "Enabled Agents",
|
||||
"enabledAgentsNote": "Agents available for handoff",
|
||||
"mainOrchestration": "Main Orchestration",
|
||||
"boundPersonas": "Bound Personas",
|
||||
"boundPersonasNote": "Agents with an attached persona"
|
||||
},
|
||||
"switches": {
|
||||
"enable": "Enable SubAgent orchestration",
|
||||
"enableHint": "Enable sub-agent functionality",
|
||||
@@ -23,7 +37,9 @@
|
||||
},
|
||||
"section": {
|
||||
"title": "SubAgents",
|
||||
"globalSettings": "Global Settings"
|
||||
"subtitle": "Configure delegate agents, personas, and descriptions for the main LLM",
|
||||
"globalSettings": "Global Settings",
|
||||
"agentSetup": "Agent Setup"
|
||||
},
|
||||
"cards": {
|
||||
"statusEnabled": "Enabled",
|
||||
@@ -33,7 +49,9 @@
|
||||
"switchLabel": "Enable",
|
||||
"previewTitle": "Preview: handoff tool shown to the main LLM",
|
||||
"personaChip": "Persona: {id}",
|
||||
"personaPreview": "PERSONA PREVIEW"
|
||||
"personaPreview": "Persona Preview",
|
||||
"noDescription": "No description yet",
|
||||
"previewHint": "Review the currently selected persona to verify the handoff target."
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Agent name (used for transfer_to_{name})",
|
||||
@@ -48,6 +66,9 @@
|
||||
"messages": {
|
||||
"loadConfigFailed": "Failed to load config",
|
||||
"loadPersonaFailed": "Failed to load persona list",
|
||||
"unsavedChangesNotice": "You have unsaved changes on this page. Save before leaving.",
|
||||
"unsavedChangesLeaveConfirm": "You have unsaved changes. Leaving will discard them. Continue?",
|
||||
"unsavedChangesReloadConfirm": "You have unsaved changes. Reloading will discard them. Continue?",
|
||||
"nameMissing": "A SubAgent is missing a name",
|
||||
"nameInvalid": "Invalid SubAgent name: only lowercase letters/numbers/underscores, starting with a letter",
|
||||
"nameDuplicate": "Duplicate SubAgent name: {name}",
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
"createdAt": "Создан",
|
||||
"updatedAt": "Обновлен",
|
||||
"actions": "Действия"
|
||||
},
|
||||
"umoDisplay": {
|
||||
"parsed": "Разобрано",
|
||||
"raw": "UMO"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
@@ -97,6 +101,8 @@
|
||||
"batchDeletePartial": "Удаление завершено: успешно {deleted}, ошибок {failed}",
|
||||
"exportSuccess": "Экспорт завершен",
|
||||
"exportError": "Ошибка экспорта",
|
||||
"noItemSelectedForExport": "Сначала выберите диалоги для экспорта"
|
||||
"noItemSelectedForExport": "Сначала выберите диалоги для экспорта",
|
||||
"copySuccess": "Скопировано",
|
||||
"copyError": "Не удалось скопировать"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"header": {
|
||||
"eyebrow": "Automation",
|
||||
"live": "Живая синхронизация"
|
||||
},
|
||||
"page": {
|
||||
"title": "Запланированные задачи",
|
||||
"beta": "Экспериментальные функции",
|
||||
"subtitle": "Управление будущими задачами AstrBot. Бот автоматически проснется, выполнит задачу и отправит результат. Требуется включить «Проактивные способности» в конфигурации.",
|
||||
"proactive": {
|
||||
"supported": "Отправка результатов поддерживается на платформах: {platforms}",
|
||||
"supported": "Отправка результатов поддерживается только на указанных ниже настроенных платформах",
|
||||
"unsupported": "Нет платформ, поддерживающих проактивные сообщения. Включите их в настройках платформ."
|
||||
}
|
||||
},
|
||||
@@ -15,8 +19,41 @@
|
||||
"cancel": "Отмена",
|
||||
"submit": "Создать"
|
||||
},
|
||||
"overview": {
|
||||
"totalTasks": "Всего задач",
|
||||
"totalTasksNote": "Все зарегистрированные будущие задачи",
|
||||
"enabledTasks": "Активные задачи",
|
||||
"enabledTasksNote": "Задачи, которые будут автоматически выполнены",
|
||||
"oneOffTasks": "Разовые задачи",
|
||||
"recurringTasksNote": "Повторяющихся задач: {count}",
|
||||
"proactivePlatforms": "Проактивные платформы",
|
||||
"proactivePlatformsNote": "Платформы, способные отправлять результат сами"
|
||||
},
|
||||
"section": {
|
||||
"registered": {
|
||||
"title": "Список задач",
|
||||
"subtitle": "Просматривайте зарегистрированные задачи, время запуска и состояние"
|
||||
},
|
||||
"delivery": {
|
||||
"title": "Статус доставки",
|
||||
"subtitle": "После выполнения задачи результат будет отправлен обратно через поддерживаемые платформы",
|
||||
"support": "Проактивная доставка",
|
||||
"available": "Доступна",
|
||||
"unavailable": "Недоступна",
|
||||
"enabledPlatforms": "Включенные платформы"
|
||||
},
|
||||
"quickCreate": {
|
||||
"title": "Быстрое создание",
|
||||
"runMode": "Режим задачи",
|
||||
"target": "Целевой контекст"
|
||||
},
|
||||
"platforms": {
|
||||
"title": "Поддерживаемые платформы"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"title": "Список задач",
|
||||
"subtitle": "Отслеживайте cron, целевую сессию, историю запусков и состояние",
|
||||
"empty": "Задач пока нет.",
|
||||
"headers": {
|
||||
"name": "Имя",
|
||||
@@ -63,4 +100,4 @@
|
||||
"createSuccess": "Задача создана",
|
||||
"createFailed": "Ошибка создания"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"header": {
|
||||
"eyebrow": "Orchestration"
|
||||
},
|
||||
"page": {
|
||||
"title": "Оркестрация SubAgent",
|
||||
"beta": "Экспериментально",
|
||||
@@ -8,9 +11,20 @@
|
||||
"refresh": "Обновить",
|
||||
"save": "Сохранить",
|
||||
"add": "Добавить SubAgent",
|
||||
"expand": "Развернуть",
|
||||
"collapse": "Свернуть",
|
||||
"delete": "Удалить",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"overview": {
|
||||
"totalAgents": "Всего SubAgent",
|
||||
"totalAgentsNote": "Количество настроенных под-агентов",
|
||||
"enabledAgents": "Активные агенты",
|
||||
"enabledAgentsNote": "Под-агенты, доступные для handoff",
|
||||
"mainOrchestration": "Основная оркестрация",
|
||||
"boundPersonas": "Привязанные персонажи",
|
||||
"boundPersonasNote": "Под-агенты с выбранной персоной"
|
||||
},
|
||||
"switches": {
|
||||
"enable": "Включить оркестрацию SubAgent",
|
||||
"enableHint": "Включить функциональность под-агентов",
|
||||
@@ -23,7 +37,9 @@
|
||||
},
|
||||
"section": {
|
||||
"title": "Субагенты",
|
||||
"globalSettings": "Глобальные настройки"
|
||||
"subtitle": "Настройте делегируемых агентов, персонажей и описания для основного LLM",
|
||||
"globalSettings": "Глобальные настройки",
|
||||
"agentSetup": "Настройка агента"
|
||||
},
|
||||
"cards": {
|
||||
"statusEnabled": "Включено",
|
||||
@@ -33,7 +49,9 @@
|
||||
"switchLabel": "Включить",
|
||||
"previewTitle": "Предпросмотр: инструмент handoff, видимый основному LLM",
|
||||
"personaChip": "Персонаж: {id}",
|
||||
"personaPreview": "ПРЕДПРОСМОТР ПЕРСОНАЖА"
|
||||
"personaPreview": "Предпросмотр персонажа",
|
||||
"noDescription": "Описание пока не задано",
|
||||
"previewHint": "Просмотрите выбранную персону, чтобы проверить цель handoff."
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Имя агента (используется для transfer_to_{name})",
|
||||
@@ -48,6 +66,9 @@
|
||||
"messages": {
|
||||
"loadConfigFailed": "Не удалось загрузить конфигурацию",
|
||||
"loadPersonaFailed": "Не удалось загрузить список персонажей",
|
||||
"unsavedChangesNotice": "На странице есть несохраненные изменения. Сохраните их перед уходом.",
|
||||
"unsavedChangesLeaveConfirm": "Есть несохраненные изменения. При уходе со страницы они будут потеряны. Продолжить?",
|
||||
"unsavedChangesReloadConfirm": "Есть несохраненные изменения. Обновление конфигурации их сбросит. Продолжить?",
|
||||
"nameMissing": "У SubAgent отсутствует имя",
|
||||
"nameInvalid": "Недопустимое имя SubAgent: только строчные латинские буквы/цифры/подчеркивания, должно начинаться с буквы",
|
||||
"nameDuplicate": "Дублирующееся имя SubAgent: {name}",
|
||||
@@ -62,4 +83,4 @@
|
||||
"subtitle": "Добавьте первого под-агента, чтобы начать",
|
||||
"action": "Создать первого агента"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"providers": "模型提供商",
|
||||
"commands": "指令管理",
|
||||
"persona": "人格设定",
|
||||
"subagent": "SubAgent 编排",
|
||||
"subagent": "子代理编排",
|
||||
"toolUse": "MCP",
|
||||
"extension": "插件",
|
||||
"extensionTabs": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "对话标题",
|
||||
"title": "标题",
|
||||
"platform": "机器人 ID",
|
||||
"type": "消息类型",
|
||||
"cid": "对话 ID",
|
||||
@@ -31,6 +31,10 @@
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"actions": "操作"
|
||||
},
|
||||
"umoDisplay": {
|
||||
"parsed": "解析后",
|
||||
"raw": "UMO"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
@@ -97,6 +101,8 @@
|
||||
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个",
|
||||
"exportSuccess": "导出成功",
|
||||
"exportError": "导出失败",
|
||||
"noItemSelectedForExport": "请先选择要导出的对话"
|
||||
"noItemSelectedForExport": "请先选择要导出的对话",
|
||||
"copySuccess": "复制成功",
|
||||
"copyError": "复制失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,59 @@
|
||||
{
|
||||
"header": {
|
||||
"eyebrow": "Automation",
|
||||
"live": "实时同步"
|
||||
},
|
||||
"page": {
|
||||
"title": "未来任务管理",
|
||||
"beta": "实验性",
|
||||
"subtitle": "查看给 AstrBot 布置的未来任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。需要先在配置文件中启用“主动型能力”。",
|
||||
"subtitle": "AstrBot 可以被自动唤醒然后执行任务,并将结果告知任务布置方。需要先在配置文件中启用“主动型能力”。",
|
||||
"proactive": {
|
||||
"supported": "主动发送结果仅支持以下您已配置的平台:{platforms}",
|
||||
"supported": "主动发送结果仅支持以下您已配置的平台",
|
||||
"unsupported": "暂无支持主动消息的平台,请在平台设置中开启。"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"create": "新建任务",
|
||||
"create": "新建",
|
||||
"refresh": "刷新",
|
||||
"delete": "删除",
|
||||
"cancel": "取消",
|
||||
"submit": "创建"
|
||||
},
|
||||
"overview": {
|
||||
"totalTasks": "任务总数",
|
||||
"totalTasksNote": "当前已注册的未来任务",
|
||||
"enabledTasks": "启用任务",
|
||||
"enabledTasksNote": "执行器会按计划唤醒并运行",
|
||||
"oneOffTasks": "一次性任务",
|
||||
"recurringTasksNote": "循环任务 {count} 个",
|
||||
"proactivePlatforms": "主动平台",
|
||||
"proactivePlatformsNote": "已开启主动消息的平台数量"
|
||||
},
|
||||
"section": {
|
||||
"registered": {
|
||||
"title": "任务列表",
|
||||
"subtitle": "集中查看当前注册任务、执行时间与启用状态"
|
||||
},
|
||||
"delivery": {
|
||||
"title": "投递状态",
|
||||
"subtitle": "未来任务执行后的结果,会通过支持主动消息的平台回传",
|
||||
"support": "主动投递",
|
||||
"available": "可用",
|
||||
"unavailable": "不可用",
|
||||
"enabledPlatforms": "已启用平台"
|
||||
},
|
||||
"quickCreate": {
|
||||
"title": "快速创建",
|
||||
"runMode": "任务模式",
|
||||
"target": "目标定位"
|
||||
},
|
||||
"platforms": {
|
||||
"title": "适用平台"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"title": "已注册任务",
|
||||
"subtitle": "支持查看 cron、会话、最近执行记录与启用状态",
|
||||
"empty": "暂无任务。",
|
||||
"headers": {
|
||||
"name": "名称",
|
||||
|
||||
@@ -1,39 +1,57 @@
|
||||
{
|
||||
"header": {
|
||||
"eyebrow": "Subagent Orchestration"
|
||||
},
|
||||
"page": {
|
||||
"title": "SubAgent 编排",
|
||||
"title": "子代理编排",
|
||||
"beta": "实验性",
|
||||
"subtitle": "主 LLM 可直接使用自身工具,也可通过 handoff 分派给各个 SubAgent。"
|
||||
"subtitle": "主代理可直接使用自身工具,也可将部分任务委派给各个子代理,完成更复杂的任务,避免主代理上下文过长。"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "刷新",
|
||||
"save": "保存",
|
||||
"add": "新增 SubAgent",
|
||||
"add": "新增子代理",
|
||||
"expand": "展开设置",
|
||||
"collapse": "收起设置",
|
||||
"delete": "删除",
|
||||
"close": "关闭"
|
||||
},
|
||||
"overview": {
|
||||
"totalAgents": "子代理总数",
|
||||
"totalAgentsNote": "当前已配置的子代理数量",
|
||||
"enabledAgents": "启用中的 Agent",
|
||||
"enabledAgentsNote": "会参与 handoff 编排的子代理",
|
||||
"mainOrchestration": "主编排状态",
|
||||
"boundPersonas": "已绑定 Persona",
|
||||
"boundPersonasNote": "完成角色绑定的子代理数量"
|
||||
},
|
||||
"switches": {
|
||||
"enable": "启用 SubAgent 编排",
|
||||
"enable": "启用子代理编排",
|
||||
"enableHint": "启用子代理功能",
|
||||
"dedupe": "主 LLM 去重重复工具(与 SubAgent 重叠的工具将被隐藏)",
|
||||
"dedupeHint": "从主代理中移除重复工具"
|
||||
"dedupe": "主 LLM 去重重复工具(与子代理重叠的工具将被隐藏)",
|
||||
"dedupeHint": "从主代理中移除与子代理重复的工具"
|
||||
},
|
||||
"description": {
|
||||
"disabled": "不启动:SubAgent 关闭;主 LLM 按 persona 规则挂载工具(默认全部),并直接调用。",
|
||||
"enabled": "启动:主 LLM 会保留自身工具并挂载 transfer_to_* 委派工具。若开启“去重重复工具”,与 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除。"
|
||||
"disabled": "未启动子代理编排功能。",
|
||||
"enabled": "子代理将作为工具放在主代理的工具集中,主代理会在适当的时机调用子代理完成任务。"
|
||||
},
|
||||
"section": {
|
||||
"title": "SubAgents 配置",
|
||||
"globalSettings": "全局设置"
|
||||
"title": "子代理配置",
|
||||
"subtitle": "为主代理配置可委派的子代理、人格与描述信息",
|
||||
"globalSettings": "全局设置",
|
||||
"agentSetup": "Agent 设置"
|
||||
},
|
||||
"cards": {
|
||||
"statusEnabled": "启用",
|
||||
"statusDisabled": "停用",
|
||||
"unnamed": "未命名 SubAgent",
|
||||
"unnamed": "未命名子代理",
|
||||
"transferPrefix": "transfer_to_{name}",
|
||||
"switchLabel": "启用",
|
||||
"previewTitle": "预览:主 LLM 将看到的 handoff 工具",
|
||||
"personaChip": "Persona: {id}",
|
||||
"noDescription": "暂无描述"
|
||||
"noDescription": "暂无描述",
|
||||
"personaPreview": "人格预览",
|
||||
"previewHint": "这里展示当前所选 Persona 的预览,便于确认 handoff 目标。"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Agent 名称(用于 transfer_to_{name})",
|
||||
@@ -41,7 +59,7 @@
|
||||
"providerLabel": "Chat Provider(可选)",
|
||||
"providerHint": "留空表示跟随全局默认 provider。",
|
||||
"personaLabel": "选择人格设定",
|
||||
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。在人格设定页管理和新建人格。",
|
||||
"personaHint": "子代理 将直接继承所选 Persona 的系统设定与工具。在人格设定页管理和新建人格。",
|
||||
"personaPreview": "人格预览",
|
||||
"descriptionLabel": "对主 LLM 的描述(用于决定是否 handoff)",
|
||||
"descriptionHint": "这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
|
||||
@@ -49,17 +67,20 @@
|
||||
"messages": {
|
||||
"loadConfigFailed": "获取配置失败",
|
||||
"loadPersonaFailed": "获取 Persona 列表失败",
|
||||
"nameMissing": "存在未填写名称的 SubAgent",
|
||||
"nameInvalid": "SubAgent 名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头",
|
||||
"nameDuplicate": "SubAgent 名称重复:{name}",
|
||||
"personaMissing": "SubAgent {name} 未选择 Persona",
|
||||
"unsavedChangesNotice": "当前页面有未保存修改,请在离开前保存。",
|
||||
"unsavedChangesLeaveConfirm": "当前页面有未保存修改,离开后将丢失这些更改。是否继续离开?",
|
||||
"unsavedChangesReloadConfirm": "当前页面有未保存修改,刷新配置会丢失这些更改。是否继续刷新?",
|
||||
"nameMissing": "存在未填写名称的子代理",
|
||||
"nameInvalid": "子代理名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头",
|
||||
"nameDuplicate": "子代理名称重复:{name}",
|
||||
"personaMissing": "子代理 {name} 未选择 Persona",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"nameRequired": "名称必填",
|
||||
"namePattern": "仅支持小写字母、数字和下划线"
|
||||
},
|
||||
"empty": {
|
||||
"title": "未配置 SubAgent",
|
||||
"title": "未配置子代理",
|
||||
"subtitle": "添加一个新的子代理以开始",
|
||||
"action": "创建第一个 Agent"
|
||||
}
|
||||
|
||||
292
dashboard/src/styles/dashboard-shell.css
Normal file
292
dashboard/src/styles/dashboard-shell.css
Normal file
@@ -0,0 +1,292 @@
|
||||
.dashboard-page {
|
||||
--dashboard-bg: rgb(var(--v-theme-background));
|
||||
--dashboard-surface: rgb(var(--v-theme-surface));
|
||||
--dashboard-text: rgb(var(--v-theme-on-surface));
|
||||
--dashboard-muted: rgba(var(--v-theme-on-surface), 0.68);
|
||||
--dashboard-subtle: rgba(var(--v-theme-on-surface), 0.56);
|
||||
--dashboard-border: rgba(var(--v-theme-on-surface), 0.1);
|
||||
--dashboard-border-strong: rgba(var(--v-theme-on-surface), 0.14);
|
||||
--dashboard-soft: rgba(var(--v-theme-primary), 0.08);
|
||||
--dashboard-soft-strong: rgba(var(--v-theme-primary), 0.14);
|
||||
min-height: 100%;
|
||||
background: var(--dashboard-bg);
|
||||
}
|
||||
|
||||
.dashboard-page.is-dark {
|
||||
--dashboard-border: rgba(var(--v-theme-on-surface), 0.14);
|
||||
--dashboard-border-strong: rgba(var(--v-theme-on-surface), 0.18);
|
||||
--dashboard-soft: rgba(var(--v-theme-primary), 0.12);
|
||||
--dashboard-soft-strong: rgba(var(--v-theme-primary), 0.18);
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
max-width: 1560px;
|
||||
margin: 0 auto;
|
||||
color: var(--dashboard-text);
|
||||
font-family: "SF Pro Display", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.dashboard-header-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-eyebrow {
|
||||
margin-bottom: 8px;
|
||||
color: var(--dashboard-subtle);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
margin: 0;
|
||||
font-size: clamp(32px, 4vw, 44px);
|
||||
line-height: 1.04;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
margin: 10px 0 0;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.dashboard-header-meta,
|
||||
.dashboard-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--dashboard-border);
|
||||
border-radius: 999px;
|
||||
background: var(--dashboard-surface);
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dashboard-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
border: 1px solid var(--dashboard-border);
|
||||
border-radius: 16px;
|
||||
background: var(--dashboard-surface);
|
||||
}
|
||||
|
||||
.dashboard-card--padded {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.dashboard-overview-card {
|
||||
padding: 20px 20px 18px;
|
||||
}
|
||||
|
||||
.dashboard-card-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
background: var(--dashboard-soft);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.dashboard-card-label {
|
||||
margin-top: 12px;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dashboard-card-value {
|
||||
margin-top: 8px;
|
||||
font-size: clamp(24px, 2vw, 34px);
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-card-note {
|
||||
margin-top: 8px;
|
||||
color: var(--dashboard-subtle);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-section-title {
|
||||
font-size: 19px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.3;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-section-subtitle {
|
||||
margin-top: 6px;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dashboard-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard-split-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 0.9fr;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dashboard-split-grid > * {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-side-column {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
align-content: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-meta-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--dashboard-border);
|
||||
}
|
||||
|
||||
.dashboard-meta-row,
|
||||
.dashboard-list-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-meta-row {
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dashboard-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboard-list-row {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--dashboard-border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dashboard-list-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard-list-label {
|
||||
color: var(--dashboard-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-empty {
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dashboard-scrollable {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.dashboard-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dashboard-form-grid--single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-dialog-card {
|
||||
border: 1px solid var(--dashboard-border);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.dashboard-overview-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.dashboard-split-grid,
|
||||
.dashboard-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.dashboard-header,
|
||||
.dashboard-section-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dashboard-overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-card--padded,
|
||||
.dashboard-overview-card {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
@@ -71,30 +71,60 @@
|
||||
class="elevation-0" :items-per-page="pagination.page_size"
|
||||
:items-per-page-options="pageSizeOptions" show-select return-object
|
||||
:disabled="loading" @update:options="handleTableOptions">
|
||||
<template v-slot:item.title="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.title || tm('status.noTitle') }}</span>
|
||||
<template v-slot:header.umo_source>
|
||||
<div class="umo-header-cell">
|
||||
<span>{{ tm('table.headers.umo') }}</span>
|
||||
<v-btn-toggle
|
||||
v-model="umoDisplayMode"
|
||||
mandatory
|
||||
density="compact"
|
||||
divided
|
||||
variant="outlined"
|
||||
class="umo-header-toggle"
|
||||
>
|
||||
<v-btn value="parsed" size="x-small">
|
||||
{{ tm('table.umoDisplay.parsed') }}
|
||||
</v-btn>
|
||||
<v-btn value="raw" size="x-small">
|
||||
{{ tm('table.umoDisplay.raw') }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.platform="{ item }">
|
||||
<v-chip size="small" label>
|
||||
{{ item.sessionInfo.platform || tm('status.unknown') }}
|
||||
</v-chip>
|
||||
<template v-slot:item.title="{ item }">
|
||||
<div class="conversation-title-cell">
|
||||
<span class="conversation-title-text">{{ item.title || tm('status.noTitle') }}</span>
|
||||
<span class="conversation-title-meta">{{ item.cid || tm('status.unknown') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.messageType="{ item }">
|
||||
<v-chip size="small" label>
|
||||
{{ getMessageTypeDisplay(item.sessionInfo.messageType) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.cid="{ item }">
|
||||
<span class="text-truncate">{{ item.cid || tm('status.unknown') }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.sessionId="{ item }">
|
||||
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
|
||||
<template v-slot:item.umo_source="{ item }">
|
||||
<div class="umo-source-cell">
|
||||
<div class="umo-source-content">
|
||||
<template v-if="umoDisplayMode === 'parsed'">
|
||||
<v-chip size="x-small" label>
|
||||
{{ item.sessionInfo.platform || tm('status.unknown') }}
|
||||
</v-chip>
|
||||
<span class="umo-separator">:</span>
|
||||
<v-chip size="x-small" label>
|
||||
{{ getMessageTypeDisplay(item.sessionInfo.messageType) }}
|
||||
</v-chip>
|
||||
<span class="umo-separator">:</span>
|
||||
<span class="umo-session-id">{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
|
||||
</template>
|
||||
<span v-else class="umo-raw-text">{{ item.user_id || tm('status.unknown') }}</span>
|
||||
</div>
|
||||
<v-btn
|
||||
icon
|
||||
variant="plain"
|
||||
size="x-small"
|
||||
class="umo-copy-button"
|
||||
@click.stop="copyUmoSource(item)"
|
||||
>
|
||||
<v-icon size="16">mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.created_at="{ item }">
|
||||
@@ -415,6 +445,7 @@ export default {
|
||||
editedHistory: '',
|
||||
savingHistory: false,
|
||||
monacoEditor: null,
|
||||
umoDisplayMode: 'parsed',
|
||||
|
||||
commonStore: useCommonStore()
|
||||
}
|
||||
@@ -445,17 +476,8 @@ export default {
|
||||
// 动态表头
|
||||
tableHeaders() {
|
||||
return [
|
||||
{ title: this.tm('table.headers.title'), key: 'title', sortable: true },
|
||||
{ title: this.tm('table.headers.cid'), key: 'cid', sortable: true, width: '100px' },
|
||||
{
|
||||
title: this.tm('table.headers.umo'),
|
||||
align: 'center',
|
||||
children: [
|
||||
{ title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' },
|
||||
{ title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' },
|
||||
{ title: this.tm('table.headers.sessionId'), key: 'sessionId', sortable: true, width: '100px' },
|
||||
],
|
||||
},
|
||||
{ title: this.tm('table.headers.title'), key: 'title', sortable: true, minWidth: '80px', width: '200px' },
|
||||
{ title: this.tm('table.headers.umo'), key: 'umo_source', sortable: false, minWidth: '280px', width: '360px' },
|
||||
{ title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' },
|
||||
{ title: this.tm('table.headers.updatedAt'), key: 'updated_at', sortable: true, width: '180px' },
|
||||
{ title: this.tm('table.headers.actions'), key: 'actions', sortable: false, align: 'center' }
|
||||
@@ -590,6 +612,30 @@ export default {
|
||||
return typeMap[messageType] || typeMap.default;
|
||||
},
|
||||
|
||||
formatUmoSource(item) {
|
||||
if (!item?.sessionInfo) {
|
||||
return item?.user_id || this.tm('status.unknown');
|
||||
}
|
||||
|
||||
if (this.umoDisplayMode === 'raw') {
|
||||
return item.user_id || this.tm('status.unknown');
|
||||
}
|
||||
|
||||
const platform = item.sessionInfo.platform || this.tm('status.unknown');
|
||||
const messageType = this.getMessageTypeDisplay(item.sessionInfo.messageType);
|
||||
const sessionId = item.sessionInfo.sessionId || this.tm('status.unknown');
|
||||
return `${platform}:${messageType}:${sessionId}`;
|
||||
},
|
||||
|
||||
async copyUmoSource(item) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.formatUmoSource(item));
|
||||
this.showSuccessMessage(this.tm('messages.copySuccess'));
|
||||
} catch (error) {
|
||||
this.showErrorMessage(this.tm('messages.copyError'));
|
||||
}
|
||||
},
|
||||
|
||||
// 获取对话列表
|
||||
fetchConversations: (() => {
|
||||
let controller = new AbortController();
|
||||
@@ -1136,6 +1182,72 @@ export default {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conversation-title-cell {
|
||||
padding: 6px 0px;
|
||||
min-width: 100px;
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
.conversation-title-text {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conversation-title-meta {
|
||||
display: block;
|
||||
color: rgba(var(--v-theme-on-surface), 0.58);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.umo-header-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.umo-header-toggle {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.umo-source-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.umo-source-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.umo-separator {
|
||||
color: rgba(var(--v-theme-on-surface), 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.umo-session-id,
|
||||
.umo-raw-text {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.umo-copy-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
|
||||
@@ -1,112 +1,231 @@
|
||||
<template>
|
||||
<div class="cron-page">
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div>
|
||||
<div class="d-flex align-center" style="gap: 8px;">
|
||||
<h2 class="text-h5 font-weight-bold">{{ tm('page.title') }}</h2>
|
||||
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>{{ tm('page.beta') }}</v-chip>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('page.subtitle') }}
|
||||
<span v-if="proactivePlatforms.length">
|
||||
{{ tm('page.proactive.supported', { platforms: proactivePlatformText }) }}
|
||||
</span>
|
||||
<span v-else>{{ tm('page.proactive.unsupported') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center" style="gap: 8px;">
|
||||
<v-btn variant="tonal" color="primary" @click="openCreate">{{ tm('actions.create') }}</v-btn>
|
||||
<v-btn variant="tonal" color="primary" :loading="loading" @click="loadJobs">{{ tm('actions.refresh') }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card class="rounded-lg" variant="flat">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between mb-3">
|
||||
<div class="text-subtitle-1 font-weight-bold">{{ tm('table.title') }}</div>
|
||||
</div>
|
||||
|
||||
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">{{ tm('table.empty') }}</v-alert>
|
||||
|
||||
<v-data-table :items="jobs" :headers="headers" :loading="loading" item-key="job_id" density="comfortable"
|
||||
class="elevation-0">
|
||||
<template #item.name="{ item }">
|
||||
<div class="py-4">
|
||||
<div class="font-weight-medium">{{ item.name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.type="{ item }">
|
||||
<v-chip size="small" :color="item.run_once ? 'orange' : 'primary'" variant="tonal">
|
||||
{{ jobTypeLabel(item) }}
|
||||
<div class="dashboard-page cron-page" :class="{ 'is-dark': isDark }">
|
||||
<v-container fluid class="dashboard-shell pa-4 pa-md-6">
|
||||
<div class="dashboard-header">
|
||||
<div class="dashboard-header-main">
|
||||
<div class="dashboard-eyebrow">{{ tm('header.eyebrow') }}</div>
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 8px;">
|
||||
<h1 class="dashboard-title">{{ tm('page.title') }}</h1>
|
||||
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>
|
||||
{{ tm('page.beta') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template #item.cron_expression="{ item }">
|
||||
<div v-if="item.run_once">{{ formatTime(item.run_at) }}</div>
|
||||
<div v-else>
|
||||
<div>{{ item.cron_expression || tm('table.notAvailable') }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.timezone || tm('table.timezoneLocal') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.session="{ item }">
|
||||
<div>{{ item.session || tm('table.notAvailable') }}</div>
|
||||
</template>
|
||||
<template #item.next_run_time="{ item }">{{ formatTime(item.next_run_time) }}</template>
|
||||
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
|
||||
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex align-center flex-nowrap" style="gap: 12px; min-width: 140px;">
|
||||
<v-switch v-model="item.enabled" inset density="compact" hide-details color="primary"
|
||||
class="mt-0" @change="toggleJob(item)" />
|
||||
<v-btn size="small" variant="text" color="error" @click="deleteJob(item)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
<p class="dashboard-subtitle">
|
||||
{{ tm('page.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="2600">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
<div class="dashboard-header-actions">
|
||||
<v-btn variant="text" color="primary" :loading="loading" prepend-icon="mdi-refresh" @click="loadJobs">
|
||||
{{ tm('actions.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn variant="tonal" color="primary" prepend-icon="mdi-plus" @click="openCreate">
|
||||
{{ tm('actions.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="createDialog" max-width="560">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ tm('form.title') }}</v-card-title>
|
||||
<v-card-subtitle class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.chatHint') }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-switch v-model="newJob.run_once" :label="tm('form.runOnce')" inset color="primary" hide-details />
|
||||
<v-text-field v-model="newJob.name" :label="tm('form.name')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.note" :label="tm('form.note')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-if="!newJob.run_once" v-model="newJob.cron_expression" :label="tm('form.cron')"
|
||||
:placeholder="tm('form.cronPlaceholder')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-else v-model="newJob.run_at" :label="tm('form.runAt')" type="datetime-local"
|
||||
variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.session" :label="tm('form.session')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.timezone" :label="tm('form.timezone')" variant="outlined"
|
||||
density="comfortable" />
|
||||
<v-switch v-model="newJob.enabled" :label="tm('form.enabled')" inset color="primary" hide-details />
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn variant="text" @click="createDialog = false">{{ tm('actions.cancel') }}</v-btn>
|
||||
<v-btn variant="tonal" color="primary" :loading="creating" @click="createJob">{{ tm('actions.submit')
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<div class="dashboard-overview-grid">
|
||||
<section
|
||||
v-for="card in overviewCards"
|
||||
:key="card.label"
|
||||
class="dashboard-card dashboard-overview-card"
|
||||
>
|
||||
<div class="dashboard-card-icon">
|
||||
<v-icon size="18">{{ card.icon }}</v-icon>
|
||||
</div>
|
||||
<div class="dashboard-card-label">{{ card.label }}</div>
|
||||
<div class="dashboard-card-value">{{ card.value }}</div>
|
||||
<div class="dashboard-card-note">{{ card.note }}</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section-head">
|
||||
<div>
|
||||
<div class="dashboard-section-title">{{ tm('section.platforms.title') }}</div>
|
||||
<div class="dashboard-section-subtitle">
|
||||
{{
|
||||
proactivePlatforms.length
|
||||
? tm('page.proactive.supported', { platforms: proactivePlatformText })
|
||||
: tm('page.proactive.unsupported')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="proactivePlatforms.length" class="platform-section">
|
||||
<div class="platform-chip-wrap">
|
||||
<v-chip
|
||||
v-for="platform in proactivePlatforms"
|
||||
:key="platform.id"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
{{ platform.display_name || platform.name }} · {{ platform.id }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</section>
|
||||
<div v-else class="dashboard-empty platform-empty">
|
||||
{{ tm('page.proactive.unsupported') }}
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section-head">
|
||||
<div>
|
||||
<div class="dashboard-section-title">{{ tm('table.title') }}</div>
|
||||
<div class="dashboard-section-subtitle">{{ tm('table.subtitle') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="task-surface">
|
||||
<div v-if="loading && !jobs.length" class="state-panel">
|
||||
<v-progress-circular indeterminate size="22" width="2" color="primary" />
|
||||
<span>{{ tm('actions.refresh') }}...</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!jobs.length" class="state-panel">
|
||||
<v-icon size="20" color="primary">mdi-calendar-blank-outline</v-icon>
|
||||
<span>{{ tm('table.empty') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-table-wrap">
|
||||
<table class="task-table">
|
||||
<colgroup>
|
||||
<col class="col-name" />
|
||||
<col class="col-type" />
|
||||
<col class="col-cron" />
|
||||
<col class="col-session" />
|
||||
<col class="col-next-run" />
|
||||
<col class="col-last-run" />
|
||||
<col class="col-actions" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm('table.headers.name') }}</th>
|
||||
<th>{{ tm('table.headers.type') }}</th>
|
||||
<th>{{ tm('table.headers.cron') }}</th>
|
||||
<th>{{ tm('table.headers.session') }}</th>
|
||||
<th>{{ tm('table.headers.nextRun') }}</th>
|
||||
<th>{{ tm('table.headers.lastRun') }}</th>
|
||||
<th class="actions-col">{{ tm('table.headers.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in sortedJobs" :key="item.job_id">
|
||||
<td class="name-col">
|
||||
<div class="task-name">{{ item.name || tm('table.notAvailable') }}</div>
|
||||
<div class="task-subline">{{ item.description || item.job_id }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip size="small" :color="item.run_once ? 'orange' : 'primary'" variant="tonal">
|
||||
{{ jobTypeLabel(item) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>
|
||||
<div class="task-text">{{ scheduleLabel(item) }}</div>
|
||||
<div class="task-subline">{{ scheduleMeta(item) }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="task-session">{{ item.session || tm('table.notAvailable') }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="task-text">{{ formatTime(item.next_run_time) }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="task-text">{{ formatTime(item.last_run_at) }}</div>
|
||||
</td>
|
||||
<td class="actions-col">
|
||||
<div class="table-actions">
|
||||
<v-switch
|
||||
v-model="item.enabled"
|
||||
inset
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
class="mt-0"
|
||||
@change="toggleJob(item)"
|
||||
/>
|
||||
<v-btn size="small" variant="text" color="error" @click="deleteJob(item)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="2600">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
|
||||
<v-dialog v-model="createDialog" max-width="640">
|
||||
<v-card class="dashboard-dialog-card">
|
||||
<v-card-title class="text-h6 pt-5 px-5">{{ tm('form.title') }}</v-card-title>
|
||||
<v-card-subtitle class="px-5 text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.chatHint') }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="px-5 pb-2">
|
||||
<div class="dashboard-form-grid dashboard-form-grid--single">
|
||||
<v-switch
|
||||
v-model="newJob.run_once"
|
||||
:label="tm('form.runOnce')"
|
||||
inset
|
||||
color="primary"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field v-model="newJob.name" :label="tm('form.name')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.note" :label="tm('form.note')" variant="outlined" density="comfortable" />
|
||||
<v-text-field
|
||||
v-if="!newJob.run_once"
|
||||
v-model="newJob.cron_expression"
|
||||
:label="tm('form.cron')"
|
||||
:placeholder="tm('form.cronPlaceholder')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="newJob.run_at"
|
||||
:label="tm('form.runAt')"
|
||||
type="datetime-local"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
<v-text-field v-model="newJob.session" :label="tm('form.session')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.timezone" :label="tm('form.timezone')" variant="outlined" density="comfortable" />
|
||||
<v-switch
|
||||
v-model="newJob.enabled"
|
||||
:label="tm('form.enabled')"
|
||||
inset
|
||||
color="primary"
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end px-5 pb-5">
|
||||
<v-btn variant="text" @click="createDialog = false">{{ tm('actions.cancel') }}</v-btn>
|
||||
<v-btn variant="tonal" color="primary" :loading="creating" @click="createJob">
|
||||
{{ tm('actions.submit') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const { tm } = useModuleI18n('features/cron')
|
||||
const theme = useTheme()
|
||||
|
||||
const isDark = computed(() => theme.global.current.value.dark)
|
||||
const loading = ref(false)
|
||||
const jobs = ref<any[]>([])
|
||||
const proactivePlatforms = ref<{ id: string; name: string; display_name?: string }[]>([])
|
||||
@@ -129,26 +248,59 @@ const proactivePlatformText = computed(() =>
|
||||
proactivePlatforms.value.map((p) => `${p.display_name || p.name}(${p.id})`).join(' / ')
|
||||
)
|
||||
|
||||
const headers = computed(() => [
|
||||
{ title: tm('table.headers.name'), key: 'name', minWidth: '200px' },
|
||||
{ title: tm('table.headers.type'), key: 'type', width: 110 },
|
||||
{ title: tm('table.headers.cron'), key: 'cron_expression', minWidth: '160px' },
|
||||
{ title: tm('table.headers.session'), key: 'session', minWidth: '200px' },
|
||||
{ title: tm('table.headers.nextRun'), key: 'next_run_time', minWidth: '160px' },
|
||||
{ title: tm('table.headers.lastRun'), key: 'last_run_at', minWidth: '160px' },
|
||||
{ title: tm('table.headers.note'), key: 'note', minWidth: '220px' },
|
||||
{ title: tm('table.headers.actions'), key: 'actions', width: 160, sortable: false }
|
||||
const enabledJobsCount = computed(() => jobs.value.filter((job) => job.enabled).length)
|
||||
const runOnceCount = computed(() => jobs.value.filter((job) => job.run_once).length)
|
||||
const recurringCount = computed(() => jobs.value.filter((job) => !job.run_once).length)
|
||||
|
||||
const sortedJobs = computed(() =>
|
||||
[...jobs.value].sort((a, b) => {
|
||||
if (a.enabled !== b.enabled) {
|
||||
return a.enabled ? -1 : 1
|
||||
}
|
||||
|
||||
const nextA = parseTimeValue(a.next_run_time ?? a.run_at)
|
||||
const nextB = parseTimeValue(b.next_run_time ?? b.run_at)
|
||||
|
||||
if (nextA !== nextB) {
|
||||
if (!nextA) return 1
|
||||
if (!nextB) return -1
|
||||
return nextA - nextB
|
||||
}
|
||||
|
||||
return String(a.name || '').localeCompare(String(b.name || ''))
|
||||
})
|
||||
)
|
||||
|
||||
const overviewCards = computed(() => [
|
||||
{
|
||||
label: tm('overview.totalTasks'),
|
||||
value: String(jobs.value.length),
|
||||
note: tm('overview.totalTasksNote'),
|
||||
icon: 'mdi-calendar-multiple'
|
||||
},
|
||||
{
|
||||
label: tm('overview.enabledTasks'),
|
||||
value: String(enabledJobsCount.value),
|
||||
note: tm('overview.enabledTasksNote'),
|
||||
icon: 'mdi-check-circle-outline'
|
||||
}
|
||||
])
|
||||
|
||||
function toast(message: string, color: 'success' | 'error' | 'warning' = 'success') {
|
||||
snackbar.value = { show: true, message, color }
|
||||
}
|
||||
|
||||
function parseTimeValue(value: any): number {
|
||||
if (!value) return 0
|
||||
const ts = new Date(value).getTime()
|
||||
return Number.isNaN(ts) ? 0 : ts
|
||||
}
|
||||
|
||||
function formatTime(val: any): string {
|
||||
if (!val) return tm('table.notAvailable')
|
||||
try {
|
||||
return new Date(val).toLocaleString()
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return String(val)
|
||||
}
|
||||
}
|
||||
@@ -163,6 +315,20 @@ function jobTypeLabel(item: any): string {
|
||||
return map[type] || tm('table.type.unknown', { type })
|
||||
}
|
||||
|
||||
function scheduleLabel(item: any): string {
|
||||
if (item.run_once) {
|
||||
return formatTime(item.run_at)
|
||||
}
|
||||
return item.cron_expression || tm('table.notAvailable')
|
||||
}
|
||||
|
||||
function scheduleMeta(item: any): string {
|
||||
if (item.run_once) {
|
||||
return tm('table.type.once')
|
||||
}
|
||||
return item.timezone || tm('table.timezoneLocal')
|
||||
}
|
||||
|
||||
async function loadJobs() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -195,8 +361,8 @@ async function loadPlatforms() {
|
||||
display_name: p?.meta?.display_name || p?.display_name
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore platform fetch errors in UI; subtitle will show fallback
|
||||
} catch {
|
||||
// Ignore platform fetch failures and keep the fallback state.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +384,7 @@ async function deleteJob(job: any) {
|
||||
const res = await axios.delete(`/api/cron/jobs/${job.job_id}`)
|
||||
if (res.data.status === 'ok') {
|
||||
toast(tm('messages.deleteSuccess'))
|
||||
jobs.value = jobs.value.filter((j) => j.job_id !== job.job_id)
|
||||
jobs.value = jobs.value.filter((item) => item.job_id !== job.job_id)
|
||||
} else {
|
||||
toast(res.data.message || tm('messages.deleteFailed'), 'error')
|
||||
}
|
||||
@@ -262,10 +428,10 @@ async function createJob() {
|
||||
toast(tm('messages.runAtRequired'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const payload: any = { ...newJob.value }
|
||||
const res = await axios.post('/api/cron/jobs', payload)
|
||||
const res = await axios.post('/api/cron/jobs', { ...newJob.value })
|
||||
if (res.data.status === 'ok') {
|
||||
toast(tm('messages.createSuccess'))
|
||||
createDialog.value = false
|
||||
@@ -288,9 +454,153 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/styles/dashboard-shell.css';
|
||||
|
||||
.cron-page {
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.task-surface {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.platform-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.platform-chip-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.platform-empty {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.task-table-wrap {
|
||||
border: 1px solid var(--dashboard-border);
|
||||
border-radius: 14px;
|
||||
overflow: auto;
|
||||
background: var(--dashboard-surface);
|
||||
}
|
||||
|
||||
.task-table {
|
||||
width: 100%;
|
||||
min-width: 1120px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.task-table .col-name {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.task-table .col-type {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.task-table .col-cron {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.task-table .col-session {
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
.task-table .col-next-run,
|
||||
.task-table .col-last-run {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.task-table .col-actions {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.task-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
background: rgba(var(--v-theme-primary), 0.04);
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--dashboard-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-table td {
|
||||
padding: 16px;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid var(--dashboard-border);
|
||||
}
|
||||
|
||||
.task-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.name-col {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.task-name,
|
||||
.task-text {
|
||||
color: var(--dashboard-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-subline {
|
||||
margin-top: 6px;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-session {
|
||||
max-width: 340px;
|
||||
color: var(--dashboard-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.actions-col {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.state-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 220px;
|
||||
border: 1px dashed var(--dashboard-border-strong);
|
||||
border-radius: 14px;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cron-page :deep(.dashboard-overview-grid) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.table-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,144 +1,131 @@
|
||||
<template>
|
||||
<div class="subagent-page">
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<div>
|
||||
<div class="d-flex align-center gap-2 mb-1">
|
||||
<h2 class="text-h5 font-weight-bold">{{ tm('page.title') }}</h2>
|
||||
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label class="font-weight-bold">
|
||||
{{ tm('page.beta') }}
|
||||
</v-chip>
|
||||
<div class="dashboard-page subagent-page" :class="{ 'is-dark': isDark }">
|
||||
<v-container fluid class="dashboard-shell pa-4 pa-md-6">
|
||||
<div class="dashboard-header">
|
||||
<div class="dashboard-header-main">
|
||||
<div class="dashboard-eyebrow">{{ tm('header.eyebrow') }}</div>
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 8px;">
|
||||
<h1 class="dashboard-title">{{ tm('page.title') }}</h1>
|
||||
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>
|
||||
{{ tm('page.beta') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<p class="dashboard-subtitle">{{ tm('page.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('page.subtitle') }}
|
||||
|
||||
<div class="dashboard-header-actions">
|
||||
<v-btn variant="text" color="primary" prepend-icon="mdi-refresh" :loading="loading" @click="reload">
|
||||
{{ tm('actions.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn variant="tonal" color="primary" prepend-icon="mdi-content-save" :loading="saving" @click="save">
|
||||
{{ tm('actions.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="loading"
|
||||
@click="reload"
|
||||
>
|
||||
{{ tm('actions.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="primary"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="saving"
|
||||
@click="save"
|
||||
>
|
||||
{{ tm('actions.save') }}
|
||||
</v-btn>
|
||||
<div v-if="hasUnsavedChanges" class="unsaved-banner">
|
||||
<v-icon size="18" color="warning">mdi-alert-circle-outline</v-icon>
|
||||
<span>{{ tm('messages.unsavedChangesNotice') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Settings Card -->
|
||||
<v-card class="rounded-lg mb-6 border-thin" variant="flat" border>
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-bold mb-1">{{ tm('section.globalSettings') || 'Global Settings' }}</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ mainStateDescription }}
|
||||
<div class="dashboard-section-head">
|
||||
<div>
|
||||
<div class="dashboard-section-title">{{ tm('section.globalSettings') }}</div>
|
||||
<div class="dashboard-section-subtitle">{{ mainStateDescription }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-form-grid global-settings-grid mb-5">
|
||||
<div class="setting-card">
|
||||
<div class="setting-card-head">
|
||||
<div>
|
||||
<div class="setting-title">{{ tm('switches.enable') }}</div>
|
||||
<div class="setting-subtitle">{{ tm('switches.enableHint') }}</div>
|
||||
</div>
|
||||
<v-switch
|
||||
v-model="cfg.main_enable"
|
||||
color="primary"
|
||||
hide-details
|
||||
inset
|
||||
density="comfortable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch
|
||||
v-model="cfg.main_enable"
|
||||
:label="tm('switches.enable')"
|
||||
color="primary"
|
||||
hide-details
|
||||
inset
|
||||
density="comfortable"
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-body-2 font-weight-medium">{{ tm('switches.enable') }}</span>
|
||||
<span class="text-caption text-medium-emphasis">{{ tm('switches.enableHint') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="setting-card">
|
||||
<div class="setting-card-head">
|
||||
<div>
|
||||
<div class="setting-title">{{ tm('switches.dedupe') }}</div>
|
||||
<div class="setting-subtitle">{{ tm('switches.dedupeHint') }}</div>
|
||||
</div>
|
||||
<v-switch
|
||||
v-model="cfg.remove_main_duplicate_tools"
|
||||
:disabled="!cfg.main_enable"
|
||||
:label="tm('switches.dedupe')"
|
||||
color="primary"
|
||||
hide-details
|
||||
inset
|
||||
density="comfortable"
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-body-2 font-weight-medium">{{ tm('switches.dedupe') }}</span>
|
||||
<span class="text-caption text-medium-emphasis">{{ tm('switches.dedupeHint') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Agents List Section -->
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-icon icon="mdi-robot" color="primary" size="small" />
|
||||
<div class="text-h6 font-weight-bold">{{ tm('section.title') }}</div>
|
||||
<v-chip size="small" variant="tonal" color="primary" class="ml-2">
|
||||
{{ cfg.agents.length }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
color="primary"
|
||||
@click="addAgent"
|
||||
>
|
||||
{{ tm('actions.add') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-expansion-panels variant="popout" class="subagent-panels">
|
||||
<v-expansion-panel
|
||||
v-for="(agent, idx) in cfg.agents"
|
||||
:key="agent.__key"
|
||||
elevation="0"
|
||||
class="border-thin mb-2 rounded-lg"
|
||||
:class="{ 'border-primary': agent.enabled }"
|
||||
>
|
||||
<v-expansion-panel-title class="py-3">
|
||||
<div class="d-flex align-center w-100 gap-4">
|
||||
<!-- Status Indicator -->
|
||||
<v-badge
|
||||
dot
|
||||
:color="agent.enabled ? 'success' : 'grey'"
|
||||
inline
|
||||
class="mr-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Info -->
|
||||
<div class="d-flex flex-column flex-grow-1" style="min-width: 0;">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span class="text-subtitle-1 font-weight-bold text-truncate">
|
||||
{{ agent.name || tm('cards.unnamed') }}
|
||||
</span>
|
||||
<div class="dashboard-section-head">
|
||||
<div>
|
||||
<div class="dashboard-section-title">{{ tm('section.title') }}</div>
|
||||
<div class="dashboard-section-subtitle">{{ tm('section.subtitle') }}</div>
|
||||
</div>
|
||||
<div class="dashboard-section-actions">
|
||||
<div class="dashboard-pill">
|
||||
<v-icon size="16">mdi-robot-outline</v-icon>
|
||||
<span>{{ cfg.agents.length }}</span>
|
||||
</div>
|
||||
<v-btn color="primary" variant="tonal" prepend-icon="mdi-plus" @click="addAgent">
|
||||
{{ tm('actions.add') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cfg.agents.length === 0" class="dashboard-card dashboard-card--padded empty-card">
|
||||
<div class="empty-wrap">
|
||||
<v-icon icon="mdi-robot-off" size="60" class="mb-4" />
|
||||
<div class="empty-title">{{ tm('empty.title') }}</div>
|
||||
<div class="dashboard-empty mb-4">{{ tm('empty.subtitle') }}</div>
|
||||
<v-btn color="primary" variant="tonal" @click="addAgent">
|
||||
{{ tm('empty.action') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="subagent-list">
|
||||
<section
|
||||
v-for="(agent, idx) in cfg.agents"
|
||||
:key="agent.__key"
|
||||
class="dashboard-card dashboard-card--padded agent-panel"
|
||||
>
|
||||
<div class="agent-summary">
|
||||
<div class="agent-summary-main">
|
||||
<div class="agent-summary-top">
|
||||
<v-badge dot :color="agent.enabled ? 'success' : 'grey'" inline />
|
||||
<span class="agent-name">{{ agent.name || tm('cards.unnamed') }}</span>
|
||||
<v-chip size="x-small" variant="tonal" :color="agent.enabled ? 'success' : 'default'">
|
||||
{{ agent.enabled ? tm('cards.statusEnabled') : tm('cards.statusDisabled') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis text-truncate">
|
||||
<div class="agent-summary-desc">
|
||||
{{ agent.public_description || tm('cards.noDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls (stop propagation on clicks) -->
|
||||
<div class="d-flex align-center gap-2 flex-shrink-0" @click.stop>
|
||||
<div class="agent-summary-actions">
|
||||
<v-btn
|
||||
:append-icon="isAgentExpanded(agent.__key) ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
variant="text"
|
||||
color="default"
|
||||
density="comfortable"
|
||||
@click="toggleAgentExpanded(agent.__key)"
|
||||
>
|
||||
{{ isAgentExpanded(agent.__key) ? tm('actions.collapse') : tm('actions.expand') }}
|
||||
</v-btn>
|
||||
<v-switch
|
||||
v-model="agent.enabled"
|
||||
color="success"
|
||||
@@ -155,28 +142,24 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<v-divider class="mb-4" />
|
||||
<v-row>
|
||||
<!-- Left Column: Form -->
|
||||
<v-col cols="12" md="6">
|
||||
<div class="d-flex flex-column gap-4">
|
||||
<v-text-field
|
||||
v-model="agent.name"
|
||||
:label="tm('form.nameLabel')"
|
||||
:rules="[v => !!v || tm('messages.nameRequired'), v => /^[a-z][a-z0-9_]*$/.test(v) || tm('messages.namePattern')]"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
<v-expand-transition>
|
||||
<div v-show="isAgentExpanded(agent.__key)" class="agent-edit-grid">
|
||||
<section class="dashboard-card dashboard-card--padded inner-card">
|
||||
<div class="dashboard-section-title section-mini-title">{{ tm('section.agentSetup') }}</div>
|
||||
<div class="dashboard-form-grid dashboard-form-grid--single">
|
||||
<v-text-field
|
||||
v-model="agent.name"
|
||||
:label="tm('form.nameLabel')"
|
||||
:rules="[v => !!v || tm('messages.nameRequired'), v => /^[a-z][a-z0-9_]*$/.test(v) || tm('messages.namePattern')]"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="text-caption text-medium-emphasis ml-1">{{ tm('form.providerLabel') }}</div>
|
||||
<v-card variant="outlined" class="pa-0 border-thin rounded bg-transparent" style="border-color: rgba(var(--v-border-color), var(--v-border-opacity));">
|
||||
<div class="pa-3">
|
||||
<div class="selector-wrap">
|
||||
<div class="selector-label">{{ tm('form.providerLabel') }}</div>
|
||||
<div class="selector-card">
|
||||
<ProviderSelector
|
||||
v-model="agent.provider_id"
|
||||
provider-type="chat_completion"
|
||||
@@ -185,78 +168,60 @@
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="text-caption text-medium-emphasis ml-1">{{ tm('form.personaLabel') }}</div>
|
||||
<v-card variant="outlined" class="pa-0 border-thin rounded bg-transparent" style="border-color: rgba(var(--v-border-color), var(--v-border-opacity));">
|
||||
<div class="pa-3">
|
||||
<PersonaSelector
|
||||
v-model="agent.persona_id"
|
||||
/>
|
||||
<div class="selector-wrap">
|
||||
<div class="selector-label">{{ tm('form.personaLabel') }}</div>
|
||||
<div class="selector-card">
|
||||
<PersonaSelector v-model="agent.persona_id" />
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<v-textarea
|
||||
v-model="agent.public_description"
|
||||
:label="tm('form.descriptionLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
auto-grow
|
||||
hide-details="auto"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<v-textarea
|
||||
v-model="agent.public_description"
|
||||
:label="tm('form.descriptionLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
auto-grow
|
||||
hide-details="auto"
|
||||
prepend-inner-icon="mdi-text"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Column: Preview -->
|
||||
<v-col cols="12" md="6">
|
||||
<div class="h-100">
|
||||
<div class="text-caption font-weight-bold text-medium-emphasis mb-2 ml-1">
|
||||
{{ tm('cards.personaPreview') }}
|
||||
<section class="dashboard-card dashboard-card--padded inner-card">
|
||||
<div class="dashboard-section-title section-mini-title">{{ tm('cards.personaPreview') }}</div>
|
||||
<div class="dashboard-section-subtitle">{{ tm('cards.previewHint') }}</div>
|
||||
<div class="persona-preview-wrap">
|
||||
<PersonaQuickPreview :model-value="agent.persona_id" class="h-100" />
|
||||
</div>
|
||||
<PersonaQuickPreview
|
||||
:model-value="agent.persona_id"
|
||||
class="h-100"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</section>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="cfg.agents.length === 0" class="d-flex flex-column align-center justify-center py-12 text-medium-emphasis">
|
||||
<v-icon icon="mdi-robot-off" size="64" class="mb-4 opacity-50" />
|
||||
<div class="text-h6">{{ tm('empty.title') }}</div>
|
||||
<div class="text-body-2 mb-4">{{ tm('empty.subtitle') }}</div>
|
||||
<v-btn color="primary" variant="tonal" @click="addAgent">
|
||||
{{ tm('empty.action') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000" location="top">
|
||||
{{ snackbar.message }}
|
||||
<template #actions>
|
||||
<v-btn variant="text" @click="snackbar.show = false">{{ tm('actions.close') }}</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000" location="top">
|
||||
{{ snackbar.message }}
|
||||
<template #actions>
|
||||
<v-btn variant="text" @click="snackbar.show = false">{{ tm('actions.close') }}</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import ProviderSelector from '@/components/shared/ProviderSelector.vue'
|
||||
import PersonaSelector from '@/components/shared/PersonaSelector.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { useTheme } from 'vuetify'
|
||||
import PersonaQuickPreview from '@/components/shared/PersonaQuickPreview.vue'
|
||||
import PersonaSelector from '@/components/shared/PersonaSelector.vue'
|
||||
import ProviderSelector from '@/components/shared/ProviderSelector.vue'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'
|
||||
|
||||
type SubAgentItem = {
|
||||
|
||||
__key: string
|
||||
name: string
|
||||
persona_id: string
|
||||
@@ -272,15 +237,21 @@ type SubAgentConfig = {
|
||||
}
|
||||
|
||||
const { tm } = useModuleI18n('features/subagent')
|
||||
const theme = useTheme()
|
||||
const confirmDialog = useConfirmDialog()
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const isDark = computed(() => theme.global.current.value.dark)
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
})
|
||||
const expandedAgents = ref<Record<string, boolean>>({})
|
||||
const initialSnapshot = ref('')
|
||||
const hasLoaded = ref(false)
|
||||
|
||||
function toast(message: string, color: 'success' | 'error' | 'warning' = 'success') {
|
||||
snackbar.value = { show: true, message, color }
|
||||
@@ -296,37 +267,48 @@ const mainStateDescription = computed(() =>
|
||||
cfg.value.main_enable ? tm('description.enabled') : tm('description.disabled')
|
||||
)
|
||||
|
||||
const hasUnsavedChanges = computed(() => hasLoaded.value && serializeConfig(cfg.value) !== initialSnapshot.value)
|
||||
|
||||
function normalizeConfig(raw: any): SubAgentConfig {
|
||||
const main_enable = !!raw?.main_enable
|
||||
const remove_main_duplicate_tools = !!raw?.remove_main_duplicate_tools
|
||||
const agentsRaw = Array.isArray(raw?.agents) ? raw.agents : []
|
||||
|
||||
const agents: SubAgentItem[] = agentsRaw.map((a: any, i: number) => {
|
||||
const name = (a?.name ?? '').toString()
|
||||
const persona_id = (a?.persona_id ?? '').toString()
|
||||
const public_description = (a?.public_description ?? '').toString()
|
||||
const enabled = a?.enabled !== false
|
||||
const provider_id = (a?.provider_id ?? undefined) as string | undefined
|
||||
|
||||
return {
|
||||
__key: `${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`,
|
||||
name,
|
||||
persona_id,
|
||||
public_description,
|
||||
enabled,
|
||||
provider_id
|
||||
}
|
||||
})
|
||||
const agents: SubAgentItem[] = agentsRaw.map((a: any, i: number) => ({
|
||||
__key: `${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`,
|
||||
name: (a?.name ?? '').toString(),
|
||||
persona_id: (a?.persona_id ?? '').toString(),
|
||||
public_description: (a?.public_description ?? '').toString(),
|
||||
enabled: a?.enabled !== false,
|
||||
provider_id: (a?.provider_id ?? undefined) as string | undefined
|
||||
}))
|
||||
|
||||
return { main_enable, remove_main_duplicate_tools, agents }
|
||||
}
|
||||
|
||||
function serializeConfig(config: SubAgentConfig): string {
|
||||
return JSON.stringify({
|
||||
main_enable: config.main_enable,
|
||||
remove_main_duplicate_tools: config.remove_main_duplicate_tools,
|
||||
agents: config.agents.map((agent) => ({
|
||||
name: agent.name,
|
||||
persona_id: agent.persona_id,
|
||||
public_description: agent.public_description,
|
||||
enabled: agent.enabled,
|
||||
provider_id: agent.provider_id ?? null
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await axios.get('/api/subagent/config')
|
||||
if (res.data.status === 'ok') {
|
||||
cfg.value = normalizeConfig(res.data.data)
|
||||
expandedAgents.value = Object.fromEntries(cfg.value.agents.map((agent) => [agent.__key, false]))
|
||||
initialSnapshot.value = serializeConfig(cfg.value)
|
||||
hasLoaded.value = true
|
||||
} else {
|
||||
toast(res.data.message || tm('messages.loadConfigFailed'), 'error')
|
||||
}
|
||||
@@ -338,25 +320,39 @@ async function loadConfig() {
|
||||
}
|
||||
|
||||
function addAgent() {
|
||||
const key = `${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
cfg.value.agents.push({
|
||||
__key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
__key: key,
|
||||
name: '',
|
||||
persona_id: '',
|
||||
public_description: '',
|
||||
enabled: true,
|
||||
provider_id: undefined
|
||||
})
|
||||
expandedAgents.value[key] = false
|
||||
}
|
||||
|
||||
function removeAgent(idx: number) {
|
||||
cfg.value.agents.splice(idx, 1)
|
||||
const [removed] = cfg.value.agents.splice(idx, 1)
|
||||
if (removed) {
|
||||
delete expandedAgents.value[removed.__key]
|
||||
}
|
||||
}
|
||||
|
||||
function isAgentExpanded(key: string): boolean {
|
||||
return expandedAgents.value[key] !== false
|
||||
}
|
||||
|
||||
function toggleAgentExpanded(key: string) {
|
||||
expandedAgents.value[key] = !isAgentExpanded(key)
|
||||
}
|
||||
|
||||
function validateBeforeSave(): boolean {
|
||||
const nameRe = /^[a-z][a-z0-9_]{0,63}$/
|
||||
const seen = new Set<string>()
|
||||
for (const a of cfg.value.agents) {
|
||||
const name = (a.name || '').trim()
|
||||
|
||||
for (const agent of cfg.value.agents) {
|
||||
const name = (agent.name || '').trim()
|
||||
if (!name) {
|
||||
toast(tm('messages.nameMissing'), 'warning')
|
||||
return false
|
||||
@@ -370,11 +366,12 @@ function validateBeforeSave(): boolean {
|
||||
return false
|
||||
}
|
||||
seen.add(name)
|
||||
if (!a.persona_id) {
|
||||
if (!agent.persona_id) {
|
||||
toast(tm('messages.personaMissing', { name }), 'warning')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -385,17 +382,19 @@ async function save() {
|
||||
const payload = {
|
||||
main_enable: cfg.value.main_enable,
|
||||
remove_main_duplicate_tools: cfg.value.remove_main_duplicate_tools,
|
||||
agents: cfg.value.agents.map((a) => ({
|
||||
name: a.name,
|
||||
persona_id: a.persona_id,
|
||||
public_description: a.public_description,
|
||||
enabled: a.enabled,
|
||||
provider_id: a.provider_id
|
||||
agents: cfg.value.agents.map((agent) => ({
|
||||
name: agent.name,
|
||||
persona_id: agent.persona_id,
|
||||
public_description: agent.public_description,
|
||||
enabled: agent.enabled,
|
||||
provider_id: agent.provider_id
|
||||
}))
|
||||
}
|
||||
|
||||
const res = await axios.post('/api/subagent/config', payload)
|
||||
if (res.data.status === 'ok') {
|
||||
initialSnapshot.value = serializeConfig(cfg.value)
|
||||
hasLoaded.value = true
|
||||
toast(res.data.message || tm('messages.saveSuccess'), 'success')
|
||||
} else {
|
||||
toast(res.data.message || tm('messages.saveFailed'), 'error')
|
||||
@@ -408,31 +407,221 @@ async function save() {
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
await Promise.all([loadConfig()])
|
||||
if (hasUnsavedChanges.value) {
|
||||
const confirmed = await askForConfirmation(
|
||||
tm('messages.unsavedChangesReloadConfirm'),
|
||||
confirmDialog
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
await loadConfig()
|
||||
}
|
||||
|
||||
async function confirmLeaveIfNeeded(): Promise<boolean> {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
return askForConfirmation(
|
||||
tm('messages.unsavedChangesLeaveConfirm'),
|
||||
confirmDialog
|
||||
)
|
||||
}
|
||||
|
||||
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
reload()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(async () => {
|
||||
return await confirmLeaveIfNeeded()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/styles/dashboard-shell.css';
|
||||
|
||||
.subagent-page {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.subagent-panels :deep(.v-expansion-panel-text__wrapper) {
|
||||
padding: 16px;
|
||||
padding-bottom: 42px;
|
||||
.unsaved-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 18px;
|
||||
border: 1px solid rgba(var(--v-theme-warning), 0.22);
|
||||
border-radius: 12px;
|
||||
background: rgba(var(--v-theme-warning), 0.08);
|
||||
color: var(--dashboard-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
.setting-card {
|
||||
border: 1px solid var(--dashboard-border);
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
background: rgba(var(--v-theme-primary), 0.02);
|
||||
}
|
||||
|
||||
.setting-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.setting-subtitle {
|
||||
margin-top: 6px;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.empty-wrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: var(--dashboard-muted);
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 20px;
|
||||
font-weight: 650;
|
||||
color: var(--dashboard-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subagent-list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.agent-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.agent-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-summary-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-summary-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.agent-summary-desc {
|
||||
margin-top: 8px;
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.agent-summary-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-edit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.inner-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.section-mini-title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.selector-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
.selector-label {
|
||||
color: var(--dashboard-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selector-card {
|
||||
border: 1px solid var(--dashboard-border);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.persona-preview-wrap {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.agent-edit-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.setting-card-head,
|
||||
.agent-summary {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user