import { useCallback, useEffect, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { BotState } from '../../../types/bot'; import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders'; import type { DashboardLabels } from '../localeTypes'; import type { AgentTab, BotEditForm, BotParamDraft, NanobotImage } from '../types'; import { clampCpuCores, clampMaxTokens, clampMemoryMb, clampStorageGb, clampTemperature } from '../utils'; const DEFAULT_EDIT_FORM: BotEditForm = { name: '', access_password: '', llm_provider: '', llm_model: '', image_tag: '', api_key: '', api_base: '', temperature: 0.2, top_p: 1, max_tokens: 8192, cpu_cores: 1, memory_mb: 1024, storage_gb: 10, system_timezone: '', agents_md: '', soul_md: '', user_md: '', tools_md: '', identity_md: '', }; const DEFAULT_PARAM_DRAFT: BotParamDraft = { max_tokens: '8192', cpu_cores: '1', memory_mb: '1024', storage_gb: '10', }; const agentFieldByTab: Record = { AGENTS: 'agents_md', SOUL: 'soul_md', USER: 'user_md', TOOLS: 'tools_md', IDENTITY: 'identity_md', }; type PromptTone = 'info' | 'success' | 'warning' | 'error'; interface NotifyOptions { title?: string; tone?: PromptTone; durationMs?: number; } interface UseDashboardBotEditorOptions { ensureSelectedBotDetail: () => Promise; isZh: boolean; notify: (message: string, options?: NotifyOptions) => void; refresh: () => Promise; selectedBotId: string; selectedBot?: BotState; setRuntimeMenuOpen: (open: boolean) => void; availableImages: NanobotImage[]; t: DashboardLabels; } export function useDashboardBotEditor({ ensureSelectedBotDetail, isZh, notify, refresh, selectedBotId, selectedBot, setRuntimeMenuOpen, availableImages, t, }: UseDashboardBotEditorOptions) { const [agentTab, setAgentTab] = useState('AGENTS'); const [isSaving, setIsSaving] = useState(false); const [isTestingProvider, setIsTestingProvider] = useState(false); const [providerTestResult, setProviderTestResult] = useState(''); const [editForm, setEditForm] = useState(DEFAULT_EDIT_FORM); const [paramDraft, setParamDraft] = useState(DEFAULT_PARAM_DRAFT); const [showBaseModal, setShowBaseModal] = useState(false); const [showParamModal, setShowParamModal] = useState(false); const [showAgentModal, setShowAgentModal] = useState(false); const applyEditFormFromBot = useCallback((bot?: BotState) => { if (!bot) return; const provider = String(bot.llm_provider || '').trim().toLowerCase(); setProviderTestResult(''); setEditForm({ name: bot.name || '', access_password: bot.access_password || '', llm_provider: provider, llm_model: bot.llm_model || '', image_tag: bot.image_tag || '', api_key: '', api_base: bot.api_base || '', temperature: clampTemperature(bot.temperature ?? 0.2), top_p: bot.top_p ?? 1, max_tokens: clampMaxTokens(bot.max_tokens ?? 8192), cpu_cores: clampCpuCores(bot.cpu_cores ?? 1), memory_mb: clampMemoryMb(bot.memory_mb ?? 1024), storage_gb: clampStorageGb(bot.storage_gb ?? 10), system_timezone: bot.system_timezone || '', agents_md: bot.agents_md || '', soul_md: bot.soul_md || '', user_md: bot.user_md || '', tools_md: bot.tools_md || '', identity_md: bot.identity_md || '', }); setParamDraft({ max_tokens: String(clampMaxTokens(bot.max_tokens ?? 8192)), cpu_cores: String(clampCpuCores(bot.cpu_cores ?? 1)), memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)), storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)), }); }, []); const updateEditForm = useCallback((patch: Partial) => { setEditForm((prev) => ({ ...prev, ...patch })); }, []); const updateParamDraft = useCallback((patch: Partial) => { setParamDraft((prev) => ({ ...prev, ...patch })); }, []); const updateAgentTabValue = useCallback((tab: AgentTab, nextValue: string) => { const field = agentFieldByTab[tab]; setEditForm((prev) => ({ ...prev, [field]: nextValue })); }, []); const onBaseProviderChange = useCallback((provider: string) => { setEditForm((prev) => { const nextProvider = String(provider || '').trim().toLowerCase(); const nextDefaultApiBase = getLlmProviderDefaultApiBase(nextProvider); return { ...prev, llm_provider: nextProvider, api_base: nextDefaultApiBase, }; }); setProviderTestResult(''); }, []); const closeModals = useCallback(() => { setShowBaseModal(false); setShowParamModal(false); setShowAgentModal(false); }, []); useEffect(() => { if (!selectedBotId) return; if (showBaseModal || showParamModal || showAgentModal) return; applyEditFormFromBot(selectedBot); }, [ applyEditFormFromBot, selectedBot, selectedBot?.id, selectedBot?.updated_at, selectedBotId, showAgentModal, showBaseModal, showParamModal, ]); const openBaseConfigModal = useCallback(async () => { setRuntimeMenuOpen(false); const detail = await ensureSelectedBotDetail(); applyEditFormFromBot(detail); setShowBaseModal(true); }, [applyEditFormFromBot, ensureSelectedBotDetail, setRuntimeMenuOpen]); const openParamConfigModal = useCallback(async () => { setRuntimeMenuOpen(false); const detail = await ensureSelectedBotDetail(); applyEditFormFromBot(detail); setShowParamModal(true); }, [applyEditFormFromBot, ensureSelectedBotDetail, setRuntimeMenuOpen]); const openAgentFilesModal = useCallback(async () => { setRuntimeMenuOpen(false); const detail = await ensureSelectedBotDetail(); applyEditFormFromBot(detail); setShowAgentModal(true); }, [applyEditFormFromBot, ensureSelectedBotDetail, setRuntimeMenuOpen]); const testProviderConnection = useCallback(async () => { if (!editForm.llm_provider || !editForm.llm_model || !editForm.api_key.trim()) { notify(t.providerRequired, { tone: 'warning' }); return; } setIsTestingProvider(true); setProviderTestResult(''); try { const res = await axios.post(`${APP_ENDPOINTS.apiBase}/providers/test`, { provider: editForm.llm_provider, model: editForm.llm_model, api_key: editForm.api_key.trim(), api_base: editForm.api_base.trim(), }); if (res.data?.ok) { const preview = (res.data.models_preview || []).slice(0, 3).join(', '); setProviderTestResult(t.connOk(preview)); } else { setProviderTestResult(t.connFail(res.data?.detail || 'unknown error')); } } catch (error: unknown) { const msg = resolveApiErrorMessage(error, 'request failed'); setProviderTestResult(t.connFail(msg)); } finally { setIsTestingProvider(false); } }, [editForm.api_base, editForm.api_key, editForm.llm_model, editForm.llm_provider, notify, t]); const saveBot = useCallback(async (mode: 'params' | 'agent' | 'base') => { const targetBotId = String(selectedBot?.id || selectedBotId || '').trim(); if (!targetBotId) { notify(isZh ? '未选中 Bot,无法保存。' : 'No bot selected.', { tone: 'warning' }); return; } setIsSaving(true); try { const payload: Record = {}; if (mode === 'base') { const normalizedSystemTimezone = editForm.system_timezone.trim(); if (!normalizedSystemTimezone) { notify(isZh ? '系统时区不能为空。' : 'System timezone is required.', { tone: 'warning' }); return; } payload.name = editForm.name; payload.access_password = editForm.access_password; payload.image_tag = editForm.image_tag; payload.system_timezone = normalizedSystemTimezone; const normalizedImageTag = String(editForm.image_tag || '').trim(); const selectedImage = availableImages.find((row) => String(row.tag || '').trim() === normalizedImageTag); const selectedImageStatus = String(selectedImage?.status || '').toUpperCase(); if (normalizedImageTag && (!selectedImage || selectedImageStatus !== 'READY')) { throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.'); } const normalizedCpuCores = clampCpuCores(Number(paramDraft.cpu_cores)); const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb)); const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb)); payload.cpu_cores = normalizedCpuCores; payload.memory_mb = normalizedMemoryMb; payload.storage_gb = normalizedStorageGb; setEditForm((prev) => ({ ...prev, cpu_cores: normalizedCpuCores, memory_mb: normalizedMemoryMb, storage_gb: normalizedStorageGb, })); setParamDraft((prev) => ({ ...prev, cpu_cores: String(normalizedCpuCores), memory_mb: String(normalizedMemoryMb), storage_gb: String(normalizedStorageGb), })); } if (mode === 'params') { payload.llm_provider = editForm.llm_provider; payload.llm_model = editForm.llm_model; payload.api_base = editForm.api_base.trim(); if (editForm.api_key.trim()) payload.api_key = editForm.api_key.trim(); payload.temperature = clampTemperature(Number(editForm.temperature)); payload.top_p = Number(editForm.top_p); const normalizedMaxTokens = clampMaxTokens(Number(paramDraft.max_tokens)); payload.max_tokens = normalizedMaxTokens; setEditForm((prev) => ({ ...prev, max_tokens: normalizedMaxTokens, })); setParamDraft((prev) => ({ ...prev, max_tokens: String(normalizedMaxTokens) })); } if (mode === 'agent') { payload.agents_md = editForm.agents_md; payload.soul_md = editForm.soul_md; payload.user_md = editForm.user_md; payload.tools_md = editForm.tools_md; payload.identity_md = editForm.identity_md; } await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${targetBotId}`, payload); await refresh(); closeModals(); notify(t.configUpdated, { tone: 'success' }); } catch (error: unknown) { const msg = resolveApiErrorMessage(error, t.saveFail); notify(msg, { tone: 'error' }); } finally { setIsSaving(false); } }, [ availableImages, closeModals, editForm, isZh, notify, paramDraft, refresh, selectedBot?.id, selectedBotId, t, ]); return { agentFieldByTab, agentTab, applyEditFormFromBot, editForm, isSaving, isTestingProvider, onBaseProviderChange, openAgentFilesModal, openBaseConfigModal, openParamConfigModal, paramDraft, providerTestResult, saveBot, setAgentTab, setShowAgentModal, setShowBaseModal, setShowParamModal, showAgentModal, showBaseModal, showParamModal, testProviderConnection, updateAgentTabValue, updateEditForm, updateParamDraft, }; }