dashboard-nanobot/frontend/src/modules/management/components/CreateBotModal.tsx

194 lines
8.1 KiB
TypeScript

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<NanobotImage[]>([]);
useEffect(() => {
if (!isOpen) {
return;
}
axios
.get<NanobotImage[]>(`${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();
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, formData);
onSuccess();
onClose();
} catch {
notify(t.createFail, { tone: 'error' });
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200">
<div className="flex justify-between items-center p-6 border-b border-slate-800 bg-slate-800/50">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-600/20 rounded-lg text-white">
<Bot className="text-blue-400" size={24} />
</div>
<h2 className="text-xl font-bold text-white">{t.title}</h2>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-700 rounded-full transition-colors text-white">
<X size={20} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6 overflow-y-auto max-h-[80vh] scrollbar-thin scrollbar-thumb-white/10 pr-2">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">{t.idLabel}</label>
<input
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
placeholder={t.idPlaceholder}
onChange={(e) => setFormData({ ...formData, id: e.target.value })}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">{t.nameLabel}</label>
<input
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
placeholder={t.namePlaceholder}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400 flex items-center gap-2">
<Layers size={14} className="text-blue-500" /> {t.imageLabel}
</label>
<LucentSelect
onChange={(e) => setFormData({ ...formData, image_tag: e.target.value })}
value={formData.image_tag}
>
{availableImages.map((img) => (
<option key={img.tag} value={img.tag}>
{img.tag} ({img.source_dir ? `${t.source}: ${img.source_dir}` : t.pypi})
</option>
))}
{availableImages.length === 0 && <option disabled>{t.noImage}</option>}
</LucentSelect>
<p className="text-[10px] text-slate-600 italic">{t.imageHint}</p>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400 flex items-center gap-2 text-white">
<Cpu size={14} /> {t.providerLabel}
</label>
<LucentSelect
onChange={(e) => setFormData({ ...formData, llm_provider: e.target.value })}
>
<option value="openai">OpenAI</option>
<option value="vllm">vLLM (OpenAI-compatible)</option>
<option value="deepseek">DeepSeek</option>
<option value="kimi">Kimi (Moonshot)</option>
<option value="minimax">MiniMax</option>
<option value="ollama">Ollama (Local)</option>
</LucentSelect>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400 text-white">{t.modelLabel}</label>
<input
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
placeholder={t.modelPlaceholder}
defaultValue="gpt-4o"
onChange={(e) => setFormData({ ...formData, llm_model: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400 flex items-center gap-2 text-white">
<Key size={14} /> API Key
</label>
<PasswordInput
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
placeholder="sk-..."
onChange={(e) => setFormData({ ...formData, api_key: e.target.value })}
toggleLabels={passwordToggleLabels}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400 flex items-center gap-2 text-white">
<FileText size={14} /> {t.soulLabel}
</label>
<textarea
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm h-32 focus:border-blue-500 outline-none transition-all resize-none text-white"
defaultValue={formData.system_prompt}
onChange={(e) => setFormData({ ...formData, system_prompt: e.target.value })}
/>
</div>
<div className="pt-4 flex gap-3">
<button
type="button"
onClick={onClose}
className="flex-1 bg-slate-800 hover:bg-slate-700 py-3 rounded-xl font-bold transition-all text-white"
>
{t.cancel}
</button>
<button
type="submit"
className="flex-1 bg-blue-600 hover:bg-blue-500 py-3 rounded-xl font-bold flex items-center justify-center gap-2 transition-all shadow-lg shadow-blue-900/40 text-white"
>
<Save size={18} /> {t.submit}
</button>
</div>
</form>
</div>
</div>
);
}