dashboard-nanobot/frontend/src/modules/dashboard/hooks/useDashboardBotEditor.ts

336 lines
11 KiB
TypeScript
Raw Normal View History

2026-03-31 04:31:47 +00:00
import { useCallback, useEffect, useState } from 'react';
import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env';
2026-04-13 13:03:07 +00:00
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { BotState } from '../../../types/bot';
2026-03-31 04:31:47 +00:00
import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders';
2026-04-13 13:03:07 +00:00
import type { DashboardLabels } from '../localeTypes';
2026-03-31 04:31:47 +00:00
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<AgentTab, keyof BotEditForm> = {
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 {
2026-04-13 13:03:07 +00:00
ensureSelectedBotDetail: () => Promise<BotState | undefined>;
2026-03-31 04:31:47 +00:00
isZh: boolean;
notify: (message: string, options?: NotifyOptions) => void;
refresh: () => Promise<void>;
selectedBotId: string;
2026-04-13 13:03:07 +00:00
selectedBot?: BotState;
2026-03-31 04:31:47 +00:00
setRuntimeMenuOpen: (open: boolean) => void;
availableImages: NanobotImage[];
2026-04-13 13:03:07 +00:00
t: DashboardLabels;
2026-03-31 04:31:47 +00:00
}
export function useDashboardBotEditor({
ensureSelectedBotDetail,
isZh,
notify,
refresh,
selectedBotId,
selectedBot,
setRuntimeMenuOpen,
availableImages,
t,
}: UseDashboardBotEditorOptions) {
const [agentTab, setAgentTab] = useState<AgentTab>('AGENTS');
const [isSaving, setIsSaving] = useState(false);
const [isTestingProvider, setIsTestingProvider] = useState(false);
const [providerTestResult, setProviderTestResult] = useState('');
const [editForm, setEditForm] = useState<BotEditForm>(DEFAULT_EDIT_FORM);
const [paramDraft, setParamDraft] = useState<BotParamDraft>(DEFAULT_PARAM_DRAFT);
const [showBaseModal, setShowBaseModal] = useState(false);
const [showParamModal, setShowParamModal] = useState(false);
const [showAgentModal, setShowAgentModal] = useState(false);
2026-04-13 13:03:07 +00:00
const applyEditFormFromBot = useCallback((bot?: BotState) => {
2026-03-31 04:31:47 +00:00
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: '',
2026-04-14 02:04:12 +00:00
api_base: bot.api_base || '',
2026-03-31 04:31:47 +00:00
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 || '',
2026-04-14 02:04:12 +00:00
soul_md: bot.soul_md || '',
2026-03-31 04:31:47 +00:00
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<BotEditForm>) => {
setEditForm((prev) => ({ ...prev, ...patch }));
}, []);
const updateParamDraft = useCallback((patch: Partial<BotParamDraft>) => {
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(),
2026-04-14 02:04:12 +00:00
api_base: editForm.api_base.trim(),
2026-03-31 04:31:47 +00:00
});
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'));
}
2026-04-13 13:03:07 +00:00
} catch (error: unknown) {
const msg = resolveApiErrorMessage(error, 'request failed');
2026-03-31 04:31:47 +00:00
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<string, string | number> = {};
if (mode === 'base') {
2026-04-14 02:04:12 +00:00
const normalizedSystemTimezone = editForm.system_timezone.trim();
if (!normalizedSystemTimezone) {
notify(isZh ? '系统时区不能为空。' : 'System timezone is required.', { tone: 'warning' });
return;
}
2026-03-31 04:31:47 +00:00
payload.name = editForm.name;
payload.access_password = editForm.access_password;
payload.image_tag = editForm.image_tag;
2026-04-14 02:04:12 +00:00
payload.system_timezone = normalizedSystemTimezone;
2026-03-31 04:31:47 +00:00
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;
2026-04-14 02:04:12 +00:00
payload.api_base = editForm.api_base.trim();
2026-03-31 04:31:47 +00:00
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' });
2026-04-13 13:03:07 +00:00
} catch (error: unknown) {
const msg = resolveApiErrorMessage(error, t.saveFail);
2026-03-31 04:31:47 +00:00
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,
};
}