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

336 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<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 {
ensureSelectedBotDetail: () => Promise<BotState | undefined>;
isZh: boolean;
notify: (message: string, options?: NotifyOptions) => void;
refresh: () => Promise<void>;
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<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);
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<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(),
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<string, string | number> = {};
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,
};
}