dashboard-nanobot/frontend/src/modules/dashboard/utils.tsx

187 lines
6.3 KiB
TypeScript

import { normalizeAttachmentPaths } from '../../shared/workspace/utils';
import { normalizeDashboardAttachmentPath } from '../../shared/workspace/workspaceMarkdown';
import type {
CronJob,
MCPConfigResponse,
MCPServerDraft,
} from './types';
const COMPOSER_DRAFT_STORAGE_PREFIX = 'nanobot-dashboard-composer-draft:v1:';
export function stateLabel(s?: string) {
return (s || 'IDLE').toUpperCase();
}
export function normalizeRuntimeState(s?: string) {
const raw = stateLabel(s);
if (raw.includes('ERROR') || raw.includes('FAIL')) return 'ERROR';
if (raw.includes('TOOL') || raw.includes('EXEC') || raw.includes('ACTION')) return 'TOOL_CALL';
if (raw.includes('THINK') || raw.includes('PLAN') || raw.includes('REASON') || raw === 'RUNNING') return 'THINKING';
if (raw.includes('SUCCESS') || raw.includes('DONE') || raw.includes('COMPLETE')) return 'SUCCESS';
if (raw.includes('IDLE') || raw.includes('STOP')) return 'IDLE';
return raw;
}
export interface ComposerDraftStorage {
command: string;
attachments: string[];
updated_at_ms: number;
}
export function getComposerDraftStorageKey(botId: string): string {
return `${COMPOSER_DRAFT_STORAGE_PREFIX}${String(botId || '').trim()}`;
}
export function loadComposerDraft(botId: string): ComposerDraftStorage | null {
const id = String(botId || '').trim();
if (!id || typeof window === 'undefined') return null;
try {
const raw = window.localStorage.getItem(getComposerDraftStorageKey(id));
if (!raw) return null;
const parsed = JSON.parse(raw) as Partial<ComposerDraftStorage> | null;
const command = String(parsed?.command || '');
const attachments = normalizeAttachmentPaths(parsed?.attachments)
.map(normalizeDashboardAttachmentPath)
.filter(Boolean);
return {
command,
attachments,
updated_at_ms: Number(parsed?.updated_at_ms || Date.now()),
};
} catch {
return null;
}
}
export function persistComposerDraft(botId: string, commandRaw: string, attachmentsRaw: string[]): void {
const id = String(botId || '').trim();
if (!id || typeof window === 'undefined') return;
const command = String(commandRaw || '');
const attachments = normalizeAttachmentPaths(attachmentsRaw)
.map(normalizeDashboardAttachmentPath)
.filter(Boolean);
const key = getComposerDraftStorageKey(id);
try {
if (!command.trim() && attachments.length === 0) {
window.localStorage.removeItem(key);
return;
}
const payload: ComposerDraftStorage = {
command,
attachments,
updated_at_ms: Date.now(),
};
window.localStorage.setItem(key, JSON.stringify(payload));
} catch {
// ignore localStorage write failures
}
}
export function clampTemperature(value: number) {
if (Number.isNaN(value)) return 0.2;
return Math.min(1, Math.max(0, value));
}
export function clampMaxTokens(value: number) {
if (Number.isNaN(value)) return 8192;
return Math.min(32768, Math.max(256, Math.round(value)));
}
export function clampCpuCores(value: number) {
if (Number.isNaN(value)) return 1;
if (value === 0) return 0;
return Math.min(16, Math.max(0.1, Math.round(value * 10) / 10));
}
export function clampMemoryMb(value: number) {
if (Number.isNaN(value)) return 1024;
if (value === 0) return 0;
return Math.min(65536, Math.max(256, Math.round(value)));
}
export function clampStorageGb(value: number) {
if (Number.isNaN(value)) return 10;
if (value === 0) return 0;
return Math.min(1024, Math.max(1, Math.round(value)));
}
export function formatBytes(bytes: number): string {
const value = Number(bytes || 0);
if (!Number.isFinite(value) || value <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const idx = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)));
const scaled = value / Math.pow(1024, idx);
return `${scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)} ${units[idx]}`;
}
export function formatPercent(value: number): string {
const n = Number(value || 0);
if (!Number.isFinite(n)) return '0.00%';
return `${Math.max(0, n).toFixed(2)}%`;
}
export function formatWorkspaceTime(raw: string | undefined, isZh: boolean): string {
const text = String(raw || '').trim();
if (!text) return '-';
const dt = new Date(text);
if (Number.isNaN(dt.getTime())) return '-';
try {
return dt.toLocaleString(isZh ? 'zh-CN' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
} catch {
return dt.toLocaleString();
}
}
export function formatCronSchedule(job: CronJob, isZh: boolean) {
const s = job.schedule || {};
if (s.kind === 'every' && Number(s.everyMs) > 0) {
const sec = Math.round(Number(s.everyMs) / 1000);
return isZh ? `${sec}s` : `every ${sec}s`;
}
if (s.kind === 'cron') {
if (s.tz) return `${s.expr || '-'} (${s.tz})`;
return s.expr || '-';
}
if (s.kind === 'at' && Number(s.atMs) > 0) {
return new Date(Number(s.atMs)).toLocaleString();
}
return '-';
}
export function mapMcpResponseToDrafts(payload?: MCPConfigResponse | null): MCPServerDraft[] {
const rows = payload?.mcp_servers && typeof payload.mcp_servers === 'object' ? payload.mcp_servers : {};
return Object.entries(rows).map(([name, cfg]) => {
const rawHeaders = cfg?.headers && typeof cfg.headers === 'object' ? cfg.headers : {};
const headers: Record<string, string> = {};
Object.entries(rawHeaders).forEach(([k, v]) => {
const key = String(k || '').trim();
if (!key) return;
headers[key] = String(v ?? '').trim();
});
const headerEntries = Object.entries(headers);
const botIdHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-id');
const botSecretHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-secret');
const type: MCPServerDraft['type'] = String(cfg?.type || 'streamableHttp') === 'sse' ? 'sse' : 'streamableHttp';
return {
name: String(name || '').trim(),
type,
url: String(cfg?.url || '').trim(),
botId: String(botIdHeader?.[1] || '').trim(),
botSecret: String(botSecretHeader?.[1] || '').trim(),
toolTimeout: String(Number(cfg?.toolTimeout || 60) || 60),
headers,
locked: Boolean(cfg?.locked),
originName: String(name || '').trim(),
};
});
}