diff --git a/backend/services/bot_service.py b/backend/services/bot_service.py index 2e26818..c169d2c 100644 --- a/backend/services/bot_service.py +++ b/backend/services/bot_service.py @@ -195,6 +195,8 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]: def _serialize_bot(bot: BotInstance) -> Dict[str, Any]: runtime = _read_bot_runtime_snapshot(bot) + created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None + updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None return { "id": bot.id, "name": bot.name, @@ -226,11 +228,13 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]: "docker_status": bot.docker_status, "current_state": bot.current_state, "last_action": bot.last_action, - "created_at": bot.created_at, - "updated_at": bot.updated_at, + "created_at": created_at, + "updated_at": updated_at, } def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]: + created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None + updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None return { "id": bot.id, "name": bot.name, @@ -240,7 +244,8 @@ def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]: "docker_status": bot.docker_status, "current_state": bot.current_state, "last_action": bot.last_action, - "updated_at": bot.updated_at, + "created_at": created_at, + "updated_at": updated_at, } def _sync_workspace_channels( diff --git a/backend/services/cache_service.py b/backend/services/cache_service.py index cd697eb..3e4153d 100644 --- a/backend/services/cache_service.py +++ b/backend/services/cache_service.py @@ -2,10 +2,10 @@ from typing import Optional from core.cache import cache def _cache_key_bots_list() -> str: - return "bot:list:v2" + return "bot:list:v3" def _cache_key_bot_detail(bot_id: str) -> str: - return f"bot:detail:v2:{bot_id}" + return f"bot:detail:v3:{bot_id}" def _cache_key_bot_messages(bot_id: str, limit: int) -> str: return f"bot:messages:list:v2:{bot_id}:limit:{limit}" diff --git a/backend/services/platform_overview_service.py b/backend/services/platform_overview_service.py index 866f442..62e5e1e 100644 --- a/backend/services/platform_overview_service.py +++ b/backend/services/platform_overview_service.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any, Dict from sqlmodel import Session, select @@ -14,7 +14,9 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str, deleted = prune_expired_activity_events(session, force=False) if deleted > 0: session.commit() - bots = session.exec(select(BotInstance)).all() + bots = session.exec( + select(BotInstance).order_by(BotInstance.created_at.desc(), BotInstance.id.asc()) + ).all() images = session.exec(select(NanobotImage).order_by(NanobotImage.created_at.desc())).all() settings = get_platform_settings(session) @@ -30,7 +32,6 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str, live_memory_used_total = 0 live_memory_limit_total = 0 - bot_rows: List[Dict[str, Any]] = [] for bot in bots: enabled = bool(getattr(bot, "enabled", True)) runtime_status = docker_manager.get_bot_status(bot.id) if docker_manager else str(bot.docker_status or "STOPPED") @@ -60,23 +61,6 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str, else: stopped += 1 - bot_rows.append( - { - "id": bot.id, - "name": bot.name, - "enabled": enabled, - "docker_status": runtime_status, - "image_tag": bot.image_tag, - "llm_provider": getattr(bot, "llm_provider", None), - "llm_model": getattr(bot, "llm_model", None), - "current_state": bot.current_state, - "last_action": bot.last_action, - "resources": resources, - "workspace_usage_bytes": workspace_used, - "workspace_limit_bytes": workspace_limit if workspace_limit > 0 else None, - } - ) - usage = list_usage(session, limit=20) events = list_activity_events(session, limit=20) @@ -114,7 +98,6 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str, } for row in images ], - "bots": bot_rows, "settings": settings.model_dump(), "usage": usage, "events": events, diff --git a/frontend/src/components/BotManagementView.tsx b/frontend/src/components/BotManagementView.tsx deleted file mode 100644 index 99eb800..0000000 --- a/frontend/src/components/BotManagementView.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ManagementModule as BotManagementView } from '../modules/management/ManagementModule'; diff --git a/frontend/src/components/CreateBotModal.tsx b/frontend/src/components/CreateBotModal.tsx deleted file mode 100644 index 8d1ba26..0000000 --- a/frontend/src/components/CreateBotModal.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CreateBotModal } from '../modules/management/components/CreateBotModal'; diff --git a/frontend/src/components/KernelManagerModal.tsx b/frontend/src/components/KernelManagerModal.tsx deleted file mode 100644 index f6f7489..0000000 --- a/frontend/src/components/KernelManagerModal.tsx +++ /dev/null @@ -1 +0,0 @@ -export { KernelManagerModal } from '../modules/management/components/KernelManagerModal'; diff --git a/frontend/src/components/VisualDeckView.tsx b/frontend/src/components/VisualDeckView.tsx deleted file mode 100644 index dcfc25f..0000000 --- a/frontend/src/components/VisualDeckView.tsx +++ /dev/null @@ -1 +0,0 @@ -export { DashboardModule as VisualDeckView } from '../modules/dashboard/DashboardModule'; diff --git a/frontend/src/components/VoxelBot.tsx b/frontend/src/components/VoxelBot.tsx deleted file mode 100644 index d7954e5..0000000 --- a/frontend/src/components/VoxelBot.tsx +++ /dev/null @@ -1 +0,0 @@ -export { VoxelBot } from '../modules/dashboard/components/VoxelBot'; diff --git a/frontend/src/components/WorkingDeck.tsx b/frontend/src/components/WorkingDeck.tsx deleted file mode 100644 index 21e9e20..0000000 --- a/frontend/src/components/WorkingDeck.tsx +++ /dev/null @@ -1 +0,0 @@ -export { WorkingDeck } from '../modules/dashboard/components/WorkingDeck'; diff --git a/frontend/src/i18n/legacy-deck.en.ts b/frontend/src/i18n/legacy-deck.en.ts deleted file mode 100644 index 2927248..0000000 --- a/frontend/src/i18n/legacy-deck.en.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const legacyDeckEn = { - operationsDeck: 'OPERATIONS DECK', - sandbox: 'Neural Network Sandbox', - activeNodes: 'ACTIVE NODES', - globalEvents: 'Global Events', - listening: 'listening for commands', - standby: 'All units in standby mode. Waiting for deployment...', - deckTitle: 'Digital Employee Deck', - deckSub: 'Realtime bot execution and data flow visibility', - idle: 'IDLE', -}; diff --git a/frontend/src/i18n/legacy-deck.zh-cn.ts b/frontend/src/i18n/legacy-deck.zh-cn.ts deleted file mode 100644 index ed6c180..0000000 --- a/frontend/src/i18n/legacy-deck.zh-cn.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const legacyDeckZhCn = { - operationsDeck: '运维控制台', - sandbox: '神经网络沙盒', - activeNodes: '活跃节点', - globalEvents: '全局事件', - listening: '等待指令', - standby: '所有单元待命中,等待部署...', - deckTitle: '数字员工工位', - deckSub: '实时观察机器人执行状态与数据流转', - idle: 'IDLE', -}; diff --git a/frontend/src/index.css b/frontend/src/index.css index d3147e6..5843beb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,3 +5,16 @@ body, width: 100%; min-height: 100vh; } + +.animate-spin { + animation: app-spin 1s linear infinite; +} + +@keyframes app-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/frontend/src/modules/dashboard/DashboardModule.tsx b/frontend/src/modules/dashboard/DashboardModule.tsx deleted file mode 100644 index 6c7d707..0000000 --- a/frontend/src/modules/dashboard/DashboardModule.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Activity, Radio, Terminal } from 'lucide-react'; -import { useAppStore } from '../../store/appStore'; -import { WorkingDeck } from './components/WorkingDeck'; -import { pickLocale } from '../../i18n'; -import { legacyDeckZhCn } from '../../i18n/legacy-deck.zh-cn'; -import { legacyDeckEn } from '../../i18n/legacy-deck.en'; - -export function DashboardModule() { - const activeBots = useAppStore((state) => state.activeBots); - const locale = useAppStore((state) => state.locale); - const t = pickLocale(locale, { 'zh-cn': legacyDeckZhCn, en: legacyDeckEn }); - const runningBots = Object.values(activeBots).filter((b) => b.docker_status === 'RUNNING'); - - return ( -
-
- -
- -
-
-
- -
-
-

- {t.operationsDeck} -

-

{t.sandbox}

-
-
-
-
- {t.activeNodes} - - {runningBots.length} / {Object.keys(activeBots).length} - -
-
-
-
-
-
- -
- {runningBots.slice(0, 3).map((bot) => ( -
-
- -
-
-
- {bot.name} - {bot.current_state || t.idle} -
-

{bot.last_action || t.standby}

-
-
- ))} -
- -
-
- - {t.globalEvents} -
-
-
- {runningBots.map((b) => `[${b.name}] ${b.last_action || t.listening}`).join(' • ') || - t.standby} -
-
-
-
- ); -} diff --git a/frontend/src/modules/dashboard/components/BotListPanel.tsx b/frontend/src/modules/dashboard/components/BotListPanel.tsx index 8e0443f..070d84d 100644 --- a/frontend/src/modules/dashboard/components/BotListPanel.tsx +++ b/frontend/src/modules/dashboard/components/BotListPanel.tsx @@ -215,10 +215,12 @@ export function BotListPanel({ const controlState = controlStateByBot[bot.id]; const isOperating = operatingBotId === bot.id; const isEnabled = bot.enabled !== false; + const isStarting = controlState === 'starting'; + const isStopping = controlState === 'stopping'; const isEnabling = controlState === 'enabling'; const isDisabling = controlState === 'disabling'; const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING'; - const showActionPending = isOperating && !isEnabling && !isDisabling; + const showActionPending = isStarting || isStopping; return (
(null); - - useFrame((state) => { - if (!meshRef.current) return; - if (currentState === 'THINKING') { - meshRef.current.position.y = Math.sin(state.clock.elapsedTime * 10) * 0.1; - } else if (currentState === 'TOOL_CALL') { - meshRef.current.rotation.y += 0.1; - } else { - meshRef.current.position.y = 0; - } - }); - - const stateColor = - currentState === 'THINKING' - ? '#3b82f6' - : currentState === 'TOOL_CALL' - ? '#f59e0b' - : currentState === 'SUCCESS' - ? '#10b981' - : '#4b5563'; - - return ( - - - - - - - -
-

{name}

- {lastAction &&

{lastAction}

} -
- -
- ); -} diff --git a/frontend/src/modules/dashboard/components/WorkingDeck.tsx b/frontend/src/modules/dashboard/components/WorkingDeck.tsx deleted file mode 100644 index e6a6251..0000000 --- a/frontend/src/modules/dashboard/components/WorkingDeck.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Canvas } from '@react-three/fiber'; -import { ContactShadows, OrbitControls } from '@react-three/drei'; -import { useAppStore } from '../../../store/appStore'; -import { VoxelBot } from './VoxelBot'; -import { pickLocale } from '../../../i18n'; -import { legacyDeckZhCn } from '../../../i18n/legacy-deck.zh-cn'; -import { legacyDeckEn } from '../../../i18n/legacy-deck.en'; - -export function WorkingDeck() { - const activeBots = useAppStore((state) => state.activeBots); - const locale = useAppStore((state) => state.locale); - const t = pickLocale(locale, { 'zh-cn': legacyDeckZhCn, en: legacyDeckEn }); - - return ( -
- - - - - - - - - - - - - {Object.values(activeBots).map((bot, index) => { - const x = (index % 5) * 2 - 4; - const z = Math.floor(index / 5) * 2 - 2; - return ( - - ); - })} - - - -
-

- - {t.deckTitle} -

-

{t.deckSub}

-
-
- ); -} diff --git a/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts b/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts index 0636e6b..c275484 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { BotState } from '../../../types/bot'; import type { CompactPanelTab, RuntimeViewMode } from '../types'; -import { parseBotTimestamp } from '../utils'; +import { sortBotsByCreatedAtDesc } from '../utils'; interface UseDashboardShellStateOptions { activeBots: Record; @@ -38,13 +38,7 @@ export function useDashboardShellState({ const controlCommandPanelRef = useRef(null); const bots = useMemo( - () => - Object.values(activeBots).sort((a, b) => { - const aCreated = parseBotTimestamp(a.created_at); - const bCreated = parseBotTimestamp(b.created_at); - if (aCreated !== bCreated) return bCreated - aCreated; - return String(a.id || '').localeCompare(String(b.id || '')); - }), + () => sortBotsByCreatedAtDesc(Object.values(activeBots)), [activeBots], ); diff --git a/frontend/src/modules/dashboard/utils.tsx b/frontend/src/modules/dashboard/utils.tsx index 9d85763..109e883 100644 --- a/frontend/src/modules/dashboard/utils.tsx +++ b/frontend/src/modules/dashboard/utils.tsx @@ -150,7 +150,7 @@ export function normalizeRuntimeState(s?: string) { return raw; } -export function parseBotTimestamp(raw?: string | number) { +export function parseBotTimestamp(raw?: string | number | null) { if (typeof raw === 'number' && Number.isFinite(raw)) return raw; const text = String(raw || '').trim(); if (!text) return 0; @@ -158,6 +158,17 @@ export function parseBotTimestamp(raw?: string | number) { return Number.isFinite(ms) ? ms : 0; } +export function sortBotsByCreatedAtDesc( + bots: readonly T[], +): T[] { + return [...bots].sort((left, right) => { + const leftCreated = parseBotTimestamp(left.created_at); + const rightCreated = parseBotTimestamp(right.created_at); + if (leftCreated !== rightCreated) return rightCreated - leftCreated; + return String(left.id || '').localeCompare(String(right.id || '')); + }); +} + export function normalizeWorkspaceExtension(raw: unknown): string { const value = String(raw ?? '').trim().toLowerCase(); if (!value) return ''; diff --git a/frontend/src/modules/management/ManagementModule.tsx b/frontend/src/modules/management/ManagementModule.tsx deleted file mode 100644 index d340015..0000000 --- a/frontend/src/modules/management/ManagementModule.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import { Power, PowerOff, Terminal, ShieldCheck, Plus, Bot, Cpu, Layers, RefreshCw } from 'lucide-react'; -import { ProtectedSearchInput } from '../../components/ProtectedSearchInput'; -import { LucentIconButton } from '../../components/lucent/LucentIconButton'; -import { CreateBotModal } from './components/CreateBotModal'; -import { KernelManagerModal } from './components/KernelManagerModal'; -import { WorkspaceEntriesList } from '../dashboard/components/WorkspaceEntriesList'; -import { WorkspaceHoverCard } from '../dashboard/components/WorkspaceHoverCard'; -import { WorkspacePreviewModal } from '../dashboard/components/WorkspacePreviewModal'; -import { formatBytes, formatWorkspaceTime } from '../dashboard/utils'; -import { useManagementModule } from './hooks/useManagementModule'; -import '../dashboard/components/BotListPanel.css'; -import '../dashboard/components/RuntimePanel.css'; -import '../dashboard/components/DashboardShared.css'; -import '../../components/ui/SharedUi.css'; - -export function ManagementModule() { - const management = useManagementModule(); - const { - closeWorkspacePreview, - copyWorkspacePreviewPath, - copyWorkspacePreviewUrl, - filteredWorkspaceEntries, - getWorkspaceDownloadHref, - getWorkspaceRawHref, - hideWorkspaceHoverCard, - loadWorkspaceTree, - openWorkspaceFilePreview, - saveWorkspacePreviewMarkdown, - setWorkspaceAutoRefresh, - setWorkspacePreviewDraft, - setWorkspacePreviewFullscreen, - setWorkspacePreviewMode, - setWorkspaceQuery, - showWorkspaceHoverCard, - workspaceAutoRefresh, - workspaceCurrentPath, - workspaceDownloadExtensionSet, - workspaceError, - workspaceFileLoading, - workspaceFiles, - workspaceHoverCard, - workspaceLoading, - workspaceParentPath, - workspacePathDisplay, - workspacePreview, - workspacePreviewCanEdit, - workspacePreviewDraft, - workspacePreviewEditorEnabled, - workspacePreviewFullscreen, - workspacePreviewMarkdownComponents, - workspacePreviewSaving, - workspaceQuery, - workspaceSearchLoading, - } = management.workspace; - - return ( -
-
-
-

{management.t.botInstances}

-
- - -
-
- -
- {Object.values(management.activeBots).map((bot) => ( -
management.setSelectedBotId(bot.id)} - className={`p-4 rounded-xl border cursor-pointer transition-all ${ - management.selectedBotId === bot.id ? 'bg-blue-600/10 border-blue-500/50' : 'bg-slate-900/50 border-white/5 hover:border-white/10' - }`} - > -
-
-
- -
-
-

{bot.name}

-

{bot.id}

-
-
- -
-
- ))} -
-
- -
- {management.selectedBot ? ( - <> -
-
-
-

{management.selectedBot.name}

-

{management.t.botOverview}

-
-
- {management.selectedBot.docker_status} -
-
- -
-
-
- {management.t.kernel} -
-

{management.selectedBot.image_tag || 'nanobot-base:v0.1.4'}

-
-
-
- {management.t.model} -
-

{management.selectedBot.llm_model || '-'}

-
-
-
- {management.t.status} -
-

{management.selectedBot.current_state || management.t.idle}

-
-
-
- -
-
-
-
-
{management.t.workspace}
-

{management.dashboardT.workspaceHint}

-
-
- - {workspaceError ?
{workspaceError}
: null} - -
-
-
- {workspacePathDisplay} -
-
-
- { - if (!management.selectedBot) return; - void loadWorkspaceTree(management.selectedBot.id, workspaceCurrentPath); - }} - tooltip={management.isZh ? '刷新工作区' : 'Refresh workspace'} - aria-label={management.isZh ? '刷新工作区' : 'Refresh workspace'} - > - - - -
-
- -
- setWorkspaceQuery('')} - onSearchAction={() => setWorkspaceQuery(workspaceQuery.trim())} - debounceMs={200} - placeholder={management.dashboardT.workspaceSearchPlaceholder} - ariaLabel={management.dashboardT.workspaceSearchPlaceholder} - clearTitle={management.dashboardT.clearSearch} - searchTitle={management.dashboardT.searchAction} - name={management.workspaceSearchInputName} - id={management.workspaceSearchInputName} - /> -
- -
-
- {workspaceLoading || workspaceSearchLoading ? ( -
{management.dashboardT.loadingDir}
- ) : filteredWorkspaceEntries.length === 0 && workspaceParentPath === null ? ( -
- {workspaceQuery.trim() ? management.dashboardT.workspaceSearchNoResult : management.dashboardT.emptyDir} -
- ) : ( - - )} -
-
- {workspaceFileLoading ? management.dashboardT.openingPreview : management.dashboardT.workspaceHint} -
-
- - {!workspaceFiles.length ? ( -
{management.dashboardT.noPreviewFile}
- ) : null} -
- -
-
-
- - {management.t.dockerLogs} -
- {management.t.dockerLogsHint} -
-
- {management.recentLogs.length > 0 ? management.recentLogs.map((log, i) => { - const totalLogs = management.selectedBot?.logs.length ?? management.recentLogs.length; - const logNumber = (totalLogs - management.recentLogs.length + i + 1).toString().padStart(3, '0'); - return ( -
- {logNumber} - {log} -
- ); - }) : ( -
{management.t.dockerLogsEmpty}
- )} -
-
-
- - ) : ( -
- -

{management.t.selectHint}

-
- )} -
- - management.setIsModalOpen(false)} onSuccess={management.fetchBots} /> - management.setIsKernelModalOpen(false)} /> - setWorkspacePreviewFullscreen((value) => !value)} - onCopyPreviewPath={copyWorkspacePreviewPath} - onCopyPreviewUrl={copyWorkspacePreviewUrl} - onPreviewDraftChange={setWorkspacePreviewDraft} - onSavePreviewMarkdown={saveWorkspacePreviewMarkdown} - onEnterEditMode={() => setWorkspacePreviewMode('edit')} - onExitEditMode={() => { - setWorkspacePreviewMode('preview'); - setWorkspacePreviewDraft(workspacePreview?.content || ''); - }} - getWorkspaceDownloadHref={getWorkspaceDownloadHref} - getWorkspaceRawHref={getWorkspaceRawHref} - /> - -
- ); -} diff --git a/frontend/src/modules/management/components/CreateBotModal.tsx b/frontend/src/modules/management/components/CreateBotModal.tsx deleted file mode 100644 index 4c14944..0000000 --- a/frontend/src/modules/management/components/CreateBotModal.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useState, useEffect } from 'react'; -import { X, Save, Bot, Cpu, Key, FileText, Layers } from 'lucide-react'; -import axios from 'axios'; -import { APP_ENDPOINTS } from '../../../config/env'; -import { useAppStore } from '../../../store/appStore'; -import { pickLocale } from '../../../i18n'; -import { managementZhCn } from '../../../i18n/management.zh-cn'; -import { managementEn } from '../../../i18n/management.en'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; -import { LucentSelect } from '../../../components/lucent/LucentSelect'; -import { PasswordInput } from '../../../components/PasswordInput'; - -interface CreateBotModalProps { - isOpen: boolean; - onClose: () => void; - onSuccess: () => void; -} - -interface NanobotImage { - tag: string; - status: string; - source_dir?: string; -} - -export function CreateBotModal({ isOpen, onClose, onSuccess }: CreateBotModalProps) { - const locale = useAppStore((s) => s.locale); - const { notify } = useLucentPrompt(); - const t = pickLocale(locale, { 'zh-cn': managementZhCn, en: managementEn }).create; - const passwordToggleLabels = locale === 'zh' - ? { show: '显示密码', hide: '隐藏密码' } - : { show: 'Show password', hide: 'Hide password' }; - const [formData, setFormData] = useState({ - id: '', - name: '', - llm_provider: 'openai', - llm_model: 'gpt-4o', - api_key: '', - system_prompt: t.systemPrompt, - image_tag: 'nanobot-base:v0.1.4', - }); - - const [availableImages, setAvailableImages] = useState([]); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - if (!isOpen) { - return; - } - axios - .get(`${APP_ENDPOINTS.apiBase}/images`) - .then((res) => setAvailableImages(res.data.filter((img) => img.status === 'READY' || img.status === 'UNKNOWN'))) - .catch((err) => console.error('Failed to fetch images', err)); - }, [isOpen]); - - if (!isOpen) return null; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - try { - await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, formData); - onSuccess(); - onClose(); - } catch { - notify(t.createFail, { tone: 'error' }); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
-
-
-
-
- -
-

{t.title}

-
- -
- -
-
-
- - setFormData({ ...formData, id: e.target.value })} - /> -
-
- - setFormData({ ...formData, name: e.target.value })} - /> -
-
- -
- - setFormData({ ...formData, image_tag: e.target.value })} - value={formData.image_tag} - > - {availableImages.map((img) => ( - - ))} - {availableImages.length === 0 && } - -

{t.imageHint}

-
- -
-
- - setFormData({ ...formData, llm_provider: e.target.value })} - > - - - - - - - -
-
- - setFormData({ ...formData, llm_model: e.target.value })} - /> -
-
- -
- - setFormData({ ...formData, api_key: e.target.value })} - toggleLabels={passwordToggleLabels} - /> -
- -
- -