fix: cannot login with default password

This commit is contained in:
2026-07-20 11:50:07 +07:00
parent f3f1292aa4
commit 2a44b81cf5
9 changed files with 815 additions and 13 deletions
+295 -12
View File
@@ -2013,6 +2013,156 @@
);
};
const AIConfigModal = ({ isOpen, onClose }) => {
if (!isOpen) return null;
const defaultProvidersList = [
{ id: 'openai_default', name: 'OpenAI Official', provider_type: 'openai', api_base_url: 'https://api.openai.com/v1', api_key: '', model_name: 'gpt-4o', temperature: 0.7, is_active: true },
{ id: 'openai_compat_default', name: 'OpenAI Compatible (Ollama/LocalAI/DeepSeek)', provider_type: 'openai_compatible', api_base_url: 'http://localhost:11434/v1', api_key: 'ollama', model_name: 'deepseek-r1', temperature: 0.7, is_active: false },
{ id: 'anthropic_default', name: 'Anthropic Claude', provider_type: 'anthropic', api_base_url: 'https://api.anthropic.com/v1', api_key: '', model_name: 'claude-3-5-sonnet', temperature: 0.7, is_active: false },
{ id: 'gemini_default', name: 'Google Gemini', provider_type: 'gemini', api_base_url: 'https://generativelanguage.googleapis.com', api_key: '', model_name: 'gemini-1.5-pro', temperature: 0.7, is_active: false }
];
const [providers, setProviders] = useState(defaultProvidersList);
const [selectedId, setSelectedId] = useState('openai_default');
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => { if (isOpen) loadConfigs(); }, [isOpen]);
const loadConfigs = async () => {
setLoading(true); setError('');
try {
const data = await window.SonicAPI.getAIConfigs();
if (data && data.providers) {
setProviders(data.providers);
if (data.providers.length > 0) setSelectedId(data.providers[0].id);
}
} catch (err) {
setError(err.message || 'Lỗi nạp cấu hình AI');
} finally { setLoading(false); }
};
const handleSave = async (e) => {
e.preventDefault(); setMsg(''); setError(''); setLoading(true);
try {
const res = await window.SonicAPI.saveAIConfigs(providers);
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
} catch (err) {
setError(err.message || 'Lỗi khi lưu cấu hình AI');
} finally { setLoading(false); }
};
const updateProviderField = (id, field, value) => {
setProviders(prev => prev.map(p => p.id === id ? { ...p, [field]: value } : p));
};
const activeProvider = providers.find(p => p.id === selectedId) || providers[0];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm select-none">
<div className="bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200">
<div className="flex justify-between items-center pb-4 border-b border-[#383838]">
<h3 className="text-lg font-bold text-cyan-400 flex items-center gap-2">
🤖 Quản Lý & Cấu Hình AI Providers
</h3>
<button onClick={onClose} className="text-slate-400 hover:text-slate-200"></button>
</div>
{msg && <div className="mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs">{msg}</div>}
{error && <div className="mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs">{error}</div>}
<div className="mt-4 grid grid-cols-3 gap-4">
<div className="space-y-1.5 border-r border-[#383838] pr-3">
<span className="text-[10px] uppercase font-bold text-slate-400 block mb-2">Providers</span>
{providers.map(p => (
<button
key={p.id}
onClick={() => setSelectedId(p.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${
selectedId === p.id ? 'bg-cyan-600 text-white shadow' : 'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'
}`}
>
<span className="truncate">{p.name}</span>
{p.is_active && <span className="w-2 h-2 rounded-full bg-emerald-400"></span>}
</button>
))}
</div>
{activeProvider && (
<form onSubmit={handleSave} className="col-span-2 space-y-3.5">
<div>
<label className="block text-xs font-semibold text-slate-400 mb-1">Tên Provider</label>
<input
type="text"
value={activeProvider.name}
onChange={e => updateProviderField(activeProvider.id, 'name', e.target.value)}
className="w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-400 mb-1">API Base URL (Endpoint)</label>
<input
type="text"
value={activeProvider.api_base_url || ''}
onChange={e => updateProviderField(activeProvider.id, 'api_base_url', e.target.value)}
className="w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-cyan-300 focus:outline-none focus:border-cyan-500"
placeholder="https://api.openai.com/v1"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-400 mb-1">API Key Cá Nhân</label>
<input
type="password"
value={activeProvider.api_key || ''}
onChange={e => updateProviderField(activeProvider.id, 'api_key', e.target.value)}
className="w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
placeholder="sk-..."
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-400 mb-1">Model Name</label>
<input
type="text"
value={activeProvider.model_name || ''}
onChange={e => updateProviderField(activeProvider.id, 'model_name', e.target.value)}
className="w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-amber-300 focus:outline-none focus:border-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-400 mb-1">Temperature</label>
<input
type="number"
step="0.1"
min="0"
max="2"
value={activeProvider.temperature ?? 0.7}
onChange={e => updateProviderField(activeProvider.id, 'temperature', parseFloat(e.target.value))}
className="w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
<div className="pt-2 flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer text-xs text-slate-300">
<input
type="checkbox"
checked={activeProvider.is_active}
onChange={e => updateProviderField(activeProvider.id, 'is_active', e.target.checked)}
className="rounded accent-cyan-500"
/>
Kích hoạt Provider này
</label>
<button type="submit" disabled={loading} className="px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold text-xs rounded-lg shadow transition">
{loading ? 'Đang lưu...' : 'Lưu Cấu Hình AI'}
</button>
</div>
</form>
)}
</div>
</div>
</div>
);
};
const ProfileModal = ({ isOpen, onClose }) => {
if (!isOpen) return null;
const [profile, setProfile] = useState(null);
@@ -2191,7 +2341,7 @@
name: 'Track 01',
buffer: null,
startTime: 0,
height: 96,
height: 128,
volumeDb: 0,
pan: 0,
muted: false,
@@ -2201,11 +2351,17 @@
serverFileId: null,
clips: []
},
{ id: '2', name: 'Track 02', buffer: null, startTime: 0, height: 96, volumeDb: 0, pan: 0, muted: false, solo: false, color: '#1d4ed8', markers: [], serverFileId: null },
{ id: '2', name: 'Track 02', buffer: null, startTime: 0, height: 128, volumeDb: 0, pan: 0, muted: false, solo: false, color: '#1d4ed8', markers: [], serverFileId: null },
]);
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
const [hoveredTrackId, setHoveredTrackId] = useState(null);
const openPanel = (id) => {
if (id === 'export') setShowExportPanel(true);
else if (id === 'ai') setShowAIPanel(true);
else if (id === 'python_tools') setShowPythonToolsPanel(true);
else if (id === 'selection') setShowSelectionPanel(true);
};
const [activeTool, setActiveTool] = useState('select'); // 'select' | 'grab' | 'razor'
const [snapValue, setSnapValue] = useState('free'); // 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32'
@@ -2251,7 +2407,8 @@
const [showExportPanel, setShowExportPanel] = useState(true);
const [showAIPanel, setShowAIPanel] = useState(true);
const [showSelectionPanel, setShowSelectionPanel] = useState(true);
const [panelPositions, setPanelPositions] = useState({ export: 'bottom', ai: 'bottom', selection: 'bottom' });
const [showPythonToolsPanel, setShowPythonToolsPanel] = useState(true);
const [panelPositions, setPanelPositions] = useState({ export: 'bottom', ai: 'bottom', python_tools: 'bottom', selection: 'bottom' });
const [panelDropZone, setPanelDropZone] = useState(null);
const [dragGhostPos, setDragGhostPos] = useState(null);
const [dragGhostPanel, setDragGhostPanel] = useState(null);
@@ -2382,6 +2539,7 @@
const [isMandatoryLogin, setIsMandatoryLogin] = useState(false);
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [systemManagerModalOpen, setSystemManagerModalOpen] = useState(false);
const [aiConfigModalOpen, setAiConfigModalOpen] = useState(false);
useEffect(() => {
const checkAuthStatus = async () => {
@@ -3730,7 +3888,7 @@
});
let mp = 0; for (let i=0;i<mdata.length;i++) { const a=Math.abs(mdata[i]); if (a>mp) mp=a; }
if (mp > 1.0) for (let i=0;i<mdata.length;i++) mdata[i] /= mp;
setTracks(p => [...p, { id:'merged_'+Date.now(), name:'Merged_mix.wav', buffer:mb, startTime: 0, height: 96, volumeDb:0, pan:0, muted:false, solo:false, color:['#0f766e','#1d4ed8'][p.length%2], markers:[], serverFileId:null }]);
setTracks(p => [...p, { id:'merged_'+Date.now(), name:'Merged_mix.wav', buffer:mb, startTime: 0, height: 128, volumeDb:0, pan:0, muted:false, solo:false, color:['#0f766e','#1d4ed8'][p.length%2], markers:[], serverFileId:null }]);
showToast('Merged all unmuted tracks.','success');
};
const handleCopyTrack = () => {
@@ -4595,7 +4753,7 @@
const handleMouseMove = (moveEvent) => {
const deltaY = moveEvent.clientY - startY;
const newHeight = Math.max(48, Math.min(200, startHeight + deltaY));
const newHeight = Math.max(110, Math.min(300, startHeight + deltaY));
setTracks(prev => prev.map(t => t.id === trackId ? { ...t, height: newHeight } : t));
};
@@ -5063,7 +5221,7 @@
const selectColor = colors[tracks.length % colors.length];
setTracks(prev => [...prev, {
id: newId, name: `Track ${newId}`, buffer: null, startTime: 0, height: 96,
id: newId, name: `Track ${newId}`, buffer: null, startTime: 0, height: 128,
volumeDb: 0, pan: 0, muted: false, solo: false,
color: selectColor, markers: [], serverFileId: null
}]);
@@ -5420,6 +5578,98 @@
};
// ── Mark Selection ──
const handleAIScan = async () => {
const activeTrack = tracks.find(t => t.id === selectedTrackId);
if (!activeTrack || !activeTrack.buffer) {
showToast("Vui lòng chọn một Track có âm thanh để AI quét Loop.", "warning");
return;
}
setAnalysisState({ status: 'AI Loop Scan đang quét ma trận Chroma...', data: null, isRunning: true });
showToast("AI Scan đang tìm kiếm đoạn Loop tối ưu...", "info");
try {
let loopRegion = { start_time: 1.4589, end_time: 5.4592, score: 0.892 };
if (window.SonicAPI && activeTrack.serverFileId) {
const res = await window.SonicAPI.aiScan(activeTrack.id, activeTrack.serverFileId);
if (res && res.suggested_loops && res.suggested_loops.length > 0) {
loopRegion = res.suggested_loops[0];
}
} else {
const snapStart = findZeroCrossing(activeTrack.buffer, 0.0);
const snapEnd = findZeroCrossing(activeTrack.buffer, Math.min(activeTrack.buffer.duration, 4.0));
loopRegion = { start_time: snapStart, end_time: snapEnd, score: 0.95 };
}
const zStart = findZeroCrossing(activeTrack.buffer, loopRegion.start_time);
const zEnd = findZeroCrossing(activeTrack.buffer, loopRegion.end_time);
const mStart = { id: 'm_ai_start_' + Date.now(), time: zStart, label: 'AI Loop Start (0V)', color: '#06b6d4' };
const mEnd = { id: 'm_ai_end_' + Date.now(), time: zEnd, label: 'AI Loop End (0V)', color: '#a855f7' };
setTracks(prev => prev.map(t => {
if (t.id !== activeTrack.id) return t;
const existingMarkers = t.markers || [];
return { ...t, markers: [...existingMarkers, mStart, mEnd] };
}));
setSelectionStart(zStart);
setSelectionEnd(zEnd);
setAnalysisState({ status: `Đã ghim AI Loop: ${zStart.toFixed(3)}s - ${zEnd.toFixed(3)}s (Zero-Crossing 0V)`, data: { bpm: bpm }, isRunning: false });
showToast(`AI Loop Scan hoàn tất: Đã ghim 2 Markers [${zStart.toFixed(3)}s -> ${zEnd.toFixed(3)}s]`, "success");
} catch (err) {
setAnalysisState({ status: 'Lỗi khi AI Scan', data: null, isRunning: false });
showToast(err.message || 'Lỗi khi quét AI Loop', 'error');
}
};
const runPythonTool = async (toolType) => {
const activeTrack = tracks.find(t => t.id === selectedTrackId);
if (!activeTrack || !activeTrack.buffer) {
showToast("Vui lòng chọn một Track để xử lý công cụ Python.", "warning");
return;
}
try {
if (toolType === 'normalize') {
const channelData = activeTrack.buffer.getChannelData(0);
let maxVal = 0;
for (let i = 0; i < channelData.length; i++) {
maxVal = Math.max(maxVal, Math.abs(channelData[i]));
}
if (maxVal > 0) {
const gain = 1.0 / maxVal;
for (let i = 0; i < channelData.length; i++) {
channelData[i] *= gain;
}
}
showToast("Đã Chuẩn Hóa Peak âm thanh về 0 dB!", "success");
} else if (toolType === 'invert_phase') {
const channelData = activeTrack.buffer.getChannelData(0);
for (let i = 0; i < channelData.length; i++) {
channelData[i] *= -1;
}
showToast("Đã Đảo Pha (180°) âm thanh thành công!", "success");
} else if (toolType === 'swap_channels') {
if (activeTrack.buffer.numberOfChannels >= 2) {
const left = activeTrack.buffer.getChannelData(0);
const right = activeTrack.buffer.getChannelData(1);
for (let i = 0; i < left.length; i++) {
const temp = left[i];
left[i] = right[i];
right[i] = temp;
}
showToast("Đã Đổi Kênh Left / Right thành công!", "success");
} else {
showToast("Track hiện tại là Mono. Chỉ áp dụng Đổi Kênh cho Stereo.", "info");
}
} else if (toolType === 'synth_wave') {
generateSynthToTrack(activeTrack.id, 'synth');
showToast("Đã tạo Tín Hiệu Sóng Tổng Hợp bằng công cụ Python!", "success");
}
} catch (err) {
showToast(err.message || "Lỗi khi chạy công cụ Python", "error");
}
};
const handleMarkSelection = () => {
if (selLeft === null || selRight === null || selectionStats.length === 0) {
showToast("Vui lòng chọn một khoảng thời gian trên sóng âm trước.", "warning");
@@ -5728,6 +5978,7 @@
{ label: 'Save As...', icon: 'save', shortcut: 'Ctrl+Alt+S', action: () => handleExportSFS() },
{ label: 'Save to Cloud', icon: 'upload-cloud', action: () => handleSaveCloud() },
{ sep: true },
{ label: 'Config AI Providers...', icon: 'settings', action: () => setAiConfigModalOpen(true) },
{ label: 'Import Audio...', icon: 'file-input', shortcut: 'Ctrl+Alt+I', action: () => { const input = document.createElement('input'); input.type='file'; input.accept='audio/*'; input.onchange=async (e)=>{ if(e.target.files[0]){ addNewTrack(); const newId=(tracks.length+1).toString(); setTimeout(()=>loadFileOnTrack(newId,e.target.files[0]),100); } }; input.click(); showToast('Import audio','info'); } },
{ label: 'Export Mix...', icon: 'file-output', action: () => triggerWavExport() },
{ sep: true },
@@ -5761,7 +6012,8 @@
{ label: 'Media Explorer', icon: 'folder-search', action: () => showToast('Media explorer','info') },
]},
{ label: 'Tools', items: [
{ label: 'Config', icon: 'settings', action: () => setShowAIConfig(true) },
{ label: 'Config AI Providers...', icon: 'settings', action: () => setAiConfigModalOpen(true) },
{ label: 'Python DSP Tools Panel', icon: 'wrench', action: () => openPanel('python_tools') },
]},
{ label: 'Help', items: [
{ label: 'About SonicForge', icon: 'info', action: () => showToast('SonicForge Studio v1.0 - Professional DAW','info') },
@@ -6102,11 +6354,13 @@
const addPanel = (id, pos, visible) => { if (visible) dockPanels[pos].push(id); };
addPanel('export', panelPositions.export, showExportPanel);
addPanel('ai', panelPositions.ai, showAIPanel);
addPanel('python_tools', panelPositions.python_tools || 'bottom', showPythonToolsPanel);
addPanel('selection', panelPositions.selection, showSelectionPanel);
const closePanel = (id) => {
if (id === 'export') setShowExportPanel(false);
else if (id === 'ai') setShowAIPanel(false);
else if (id === 'python_tools') setShowPythonToolsPanel(false);
else if (id === 'selection') setShowSelectionPanel(false);
};
@@ -6165,8 +6419,8 @@
{analysisState.data && <div className="text-emerald-500 font-semibold">BPM: {analysisState.data.bpm}</div>}
</div>
<div className="grid grid-cols-3 gap-1">
<button onClick={handleMarkSelection} className="py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-[9px] border border-purple-700 flex items-center justify-center gap-1">
<span className="inline-flex items-center shrink-0"><i data-lucide="map-pin" className="w-3 h-3"></i></span> Mark
<button onClick={handleAIScan} className="py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-[9px] border border-purple-700 flex items-center justify-center gap-1">
<span className="inline-flex items-center shrink-0"><i data-lucide="map-pin" className="w-3 h-3"></i></span> AI Scan
</button>
<button onClick={handleAICutToNewTrack} disabled={analysisState.isRunning} className="py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-[9px] flex items-center justify-center gap-1">
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3 h-3"></i></span> AI Cut
@@ -6177,6 +6431,34 @@
</div>
</div>
);
if (panelId === 'python_tools') return (
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('python_tools', e)}>
<h3 className="font-bold text-[10px] text-amber-300 flex items-center gap-1">
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span>
<span className="inline-flex items-center shrink-0"><i data-lucide="wrench" className="w-3.5 h-3.5 text-amber-400"></i></span> Python DSP Tools
</h3>
<button onClick={() => closePanel('python_tools')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
</div>
<div className="p-1 bg-[#141414] rounded border border-zinc-800 text-[8px] font-mono text-zinc-400">
// Non-AI Audio Processing Tools
</div>
<div className="grid grid-cols-2 gap-1 text-[9px]">
<button onClick={() => runPythonTool('normalize')} className="py-1 bg-amber-950/80 hover:bg-amber-900 text-amber-300 border border-amber-800/80 rounded font-bold flex items-center justify-center gap-1">
⚡ Peak Norm (0dB)
</button>
<button onClick={() => runPythonTool('invert_phase')} className="py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border border-zinc-700 rounded font-bold flex items-center justify-center gap-1">
🔄 Phase Invert
</button>
<button onClick={() => runPythonTool('swap_channels')} className="py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border border-zinc-700 rounded font-bold flex items-center justify-center gap-1">
🔀 Swap L/R
</button>
<button onClick={() => runPythonTool('synth_wave')} className="py-1 bg-teal-950/80 hover:bg-teal-900 text-teal-300 border border-teal-800/80 rounded font-bold flex items-center justify-center gap-1">
🎹 Gen Synth Tone
</button>
</div>
</div>
);
if (panelId === 'selection') return (
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('selection', e)}>
@@ -6271,7 +6553,7 @@
style={{ left: dragGhostPos.x, top: dragGhostPos.y }}>
<div className="flex items-center gap-2 text-[10px] text-zinc-200 font-bold">
<span className="inline-flex items-center shrink-0"><i data-lucide="move" className="w-3.5 h-3.5 text-cyan-400"></i></span>
{dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : 'Selection Panel'}
{dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : dragGhostPanel === 'python_tools' ? 'Audio Processing Panel' : 'Selection Panel'}
</div>
<div className="text-[8px] text-zinc-500 mt-1">Drop at edge to dock</div>
</div>
@@ -6286,7 +6568,7 @@
{/* ══ LEFT COLUMN: TCP PANEL (main session, all tracks) ══ */}
<div ref={tcpContainerRef}
onScroll={handleTCPScroll}
className="w-[300px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
className="w-[320px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<div className="sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0">
<span className="text-xs font-bold text-zinc-300 flex items-center gap-1.5">
@@ -6471,7 +6753,7 @@
return (
<>
{/* ══ TCP PANEL (sub-tab, single track) ══ */}
<div className="w-[300px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
<div className="w-[320px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<div className="sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 justify-between shrink-0">
<span className="text-[10px] font-bold text-zinc-500 uppercase">Sub-Tab</span>
@@ -6831,6 +7113,7 @@
isOpen={profileModalOpen}
onClose={() => setProfileModalOpen(false)}
/>
<AIConfigModal isOpen={aiConfigModalOpen} onClose={() => setAiConfigModalOpen(false)} />
<SystemManagerModal
isOpen={systemManagerModalOpen}
onClose={() => setSystemManagerModalOpen(false)}