feat: Create OpenClaw dashboard page with status, filesystem, config, agents, skills, logs, and models tabs.

This commit is contained in:
Bruno Charest 2026-03-14 00:17:17 -04:00
parent 2a22990461
commit 8c6ef5acb8

View File

@ -246,76 +246,81 @@ function formatSize(bytes: number): string {
function ConfigTab() { function ConfigTab() {
const [config, setConfig] = useState<Record<string, unknown> | null>(null); const [config, setConfig] = useState<Record<string, unknown> | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['gateway', 'mcpServers'])); const [editing, setEditing] = useState(false);
useEffect(() => { const fetchConfig = () => {
setLoading(true);
api.openclawConfig() api.openclawConfig()
.then(d => { setConfig(d.config); setLoading(false); }) .then(d => { setConfig(d.config); setLoading(false); })
.catch(() => setLoading(false)); .catch(() => setLoading(false));
}, []); };
useEffect(() => { fetchConfig(); }, []);
if (loading) return <LoadingSpinner />; if (loading) return <LoadingSpinner />;
if (!config) return <EmptyState message="openclaw.json introuvable ou illisible" />; if (!config) return <EmptyState message="openclaw.json introuvable ou illisible" />;
const sections = Object.entries(config); const sections = Object.entries(config);
const toggleSection = (key: string) => {
setExpandedSections(prev => {
const next = new Set(prev);
next.has(key) ? next.delete(key) : next.add(key);
return next;
});
};
return ( return (
<div className="space-y-3"> <div className="space-y-6">
<div className="glass-card p-4 flex items-center gap-3 text-sm"> {editing && <ConfigFileEditor onClose={() => setEditing(false)} onSave={() => { setEditing(false); fetchConfig(); }} />}
<span className="text-lg">📋</span>
<span className="text-gray-400">Lecture de</span> {/* Premium Header */}
<code className="px-2 py-1 rounded-lg bg-surface-800 text-purple-300 font-mono text-xs">openclaw.json</code> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between glass-card p-6 border-l-4 border-l-purple-500 relative overflow-hidden group">
<span className="text-gray-500"> {sections.length} sections</span> <div className="absolute top-0 right-0 w-64 h-full bg-gradient-to-l from-purple-500/10 to-transparent opacity-50 group-hover:opacity-100 transition-opacity"></div>
<div className="relative z-10 mb-4 sm:mb-0">
<h2 className="text-xl font-bold text-white flex items-center gap-3">
<span></span> Configuration Globale
</h2>
<p className="text-gray-400 text-sm mt-1.5 flex items-center gap-2">
Paramètres système chargés depuis <code className="px-1.5 py-0.5 rounded bg-surface-800 text-purple-300 font-mono text-xs">openclaw.json</code>
</p>
</div>
<div className="relative z-10 shrink-0">
<button
onClick={() => setEditing(true)}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm font-semibold hover:from-purple-500 hover:to-indigo-500 transition-all shadow-[0_0_15px_rgba(147,51,234,0.3)] hover:scale-105 flex items-center gap-2"
>
<span></span> Modifier config
</button>
</div>
</div> </div>
{sections.map(([key, value]) => { {/* Grid of config sections */}
const isObject = typeof value === 'object' && value !== null; <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-5">
const isExpanded = expandedSections.has(key); {sections.map(([key, value]) => {
const isObject = typeof value === 'object' && value !== null;
return ( return (
<div key={key} className="glass-card overflow-hidden"> <div key={key} className="glass-card flex flex-col h-full border border-surface-700/50 hover:border-purple-500/30 transition-all group overflow-hidden">
<button <div className="bg-surface-800/80 p-4 border-b border-surface-700/50 flex justify-between items-center group-hover:bg-surface-700/50 transition-colors">
onClick={() => isObject && toggleSection(key)} <h3 className="font-bold text-white text-sm tracking-wide flex items-center gap-2">
className={`w-full flex items-center justify-between p-4 text-left transition-colors ${ <span className="text-purple-400 text-lg"></span> {key.toUpperCase()}
isObject ? 'hover:bg-glass-hover cursor-pointer' : '' </h3>
}`}
>
<div className="flex items-center gap-3">
<span className="text-base">
{isObject ? (isExpanded ? '▼' : '▶') : '•'}
</span>
<span className="text-white font-semibold text-sm">{key}</span>
{isObject && ( {isObject && (
<span className="text-[10px] text-gray-600 uppercase tracking-wider"> <span className="px-2 py-0.5 rounded-md bg-surface-900/50 text-gray-400 text-[10px] font-mono border border-surface-700">
{Array.isArray(value) {Array.isArray(value) ? `${(value as unknown[]).length} items` : `${Object.keys(value as object).length} clés`}
? `${(value as unknown[]).length} items`
: `${Object.keys(value as object).length} clés`
}
</span> </span>
)} )}
</div> </div>
{!isObject && ( <div className="p-4 bg-[#1e222a] flex-1 overflow-auto max-h-96 custom-scrollbar shrink-0">
<code className="text-sm text-purple-300 font-mono">{renderValue(value)}</code> {isObject ? (
)} <pre className="text-[11px] font-mono text-[#abb2bf] whitespace-pre-wrap leading-relaxed">
</button> {JSON.stringify(value, null, 2)}
{isObject && isExpanded && ( </pre>
<div className="border-t border-glass-border p-4 bg-surface-900/40"> ) : (
<pre className="text-xs font-mono text-gray-300 whitespace-pre-wrap overflow-x-auto max-h-96 leading-relaxed"> <div className="h-full flex items-center">
{JSON.stringify(value, null, 2)} <code className="text-[13px] text-green-300 font-mono bg-surface-900/50 px-3 py-2 rounded-lg break-all border border-green-500/10 w-full text-center">
</pre> {renderValue(value)}
</code>
</div>
)}
</div> </div>
)} </div>
</div> );
); })}
})} </div>
</div> </div>
); );
} }
@ -344,6 +349,7 @@ function AgentsTab() {
const [fileContent, setFileContent] = useState(''); const [fileContent, setFileContent] = useState('');
const [fileLoading, setFileLoading] = useState(false); const [fileLoading, setFileLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const loadAgents = () => { const loadAgents = () => {
setLoading(true); setLoading(true);
@ -373,9 +379,11 @@ function AgentsTab() {
const openFile = async (agentName: string, filename: string) => { const openFile = async (agentName: string, filename: string) => {
if (activeFile?.agent === agentName && activeFile?.file === filename) { if (activeFile?.agent === agentName && activeFile?.file === filename) {
setActiveFile(null); // toggle close setActiveFile(null); // toggle close
setIsFullscreen(false);
return; return;
} }
setActiveFile({ agent: agentName, file: filename }); setActiveFile({ agent: agentName, file: filename });
setIsFullscreen(false);
setFileLoading(true); setFileLoading(true);
try { try {
const data = await api.openclawReadAgentFile(agentName, filename); const data = await api.openclawReadAgentFile(agentName, filename);
@ -429,7 +437,7 @@ function AgentsTab() {
> >
<div <div
className={`p-5 cursor-pointer hover:bg-glass-hover transition-colors ${deleting === agent.name ? 'opacity-50 pointer-events-none' : ''}`} className={`p-5 cursor-pointer hover:bg-glass-hover transition-colors ${deleting === agent.name ? 'opacity-50 pointer-events-none' : ''}`}
onClick={() => { setExpandedAgent(expandedAgent === agent.name ? null : agent.name); setActiveFile(null); }} onClick={() => { setExpandedAgent(expandedAgent === agent.name ? null : agent.name); setActiveFile(null); setIsFullscreen(false); }}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -498,27 +506,37 @@ function AgentsTab() {
{/* Identity File Editor */} {/* Identity File Editor */}
{activeFile?.agent === agent.name && ( {activeFile?.agent === agent.name && (
<div className="rounded-xl border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in relative"> <div className={`rounded-xl border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in flex flex-col ${
<div className="flex items-center justify-between px-3 py-2 bg-surface-900/80 border-b border-glass-border"> isFullscreen ? 'fixed inset-4 z-[100] shadow-2xl' : 'h-[500px] relative'
<span className="text-xs font-mono text-purple-300">workspace/{agent.name}/{activeFile.file}</span> }`}>
<div className="flex gap-2"> <div className="flex items-center justify-between px-3 py-2 bg-surface-900/80 border-b border-glass-border shrink-0">
<button onClick={() => setActiveFile(null)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">Fermer</button> <span className="text-xs font-mono text-purple-300 truncate pr-4">workspace/{agent.name}/{activeFile.file}</span>
<div className="flex gap-2 shrink-0">
<button onClick={() => setIsFullscreen(!isFullscreen)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
{isFullscreen ? '🗗 Réduire' : '⛶ Plein écran'}
</button>
<button onClick={() => { setActiveFile(null); setIsFullscreen(false); }} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
Fermer
</button>
<button onClick={saveFile} disabled={saving} className="px-3 py-1 rounded bg-purple-600/80 hover:bg-purple-500 text-white text-[10px] font-medium transition-colors"> <button onClick={saveFile} disabled={saving} className="px-3 py-1 rounded bg-purple-600/80 hover:bg-purple-500 text-white text-[10px] font-medium transition-colors">
{saving ? '...' : 'Sauvegarder'} {saving ? '...' : 'Sauvegarder'}
</button> </button>
</div> </div>
</div> </div>
{fileLoading ? ( {fileLoading ? (
<div className="p-8 text-center text-gray-500 text-sm animate-pulse">Chargement</div> <div className="flex-1 flex items-center justify-center text-gray-500 text-sm animate-pulse">Chargement</div>
) : ( ) : (
<div className="max-h-80 overflow-y-auto custom-scrollbar [&_.cm-editor]:bg-transparent [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-xs"> <div className="flex-1 overflow-hidden relative min-h-0 bg-[#282c34]">
<CodeMirror <div className="absolute inset-0 custom-scrollbar [&_.cm-editor]:h-full [&_.cm-editor]:bg-transparent [&_.cm-scroller]:h-full [&_.cm-scroller]:overflow-auto [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-xs">
value={fileContent} <CodeMirror
theme={oneDark} value={fileContent}
extensions={[markdown()]} height="100%"
onChange={(val) => setFileContent(val)} theme={oneDark}
basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }} extensions={[markdown()]}
/> onChange={(val) => setFileContent(val)}
basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }}
/>
</div>
</div> </div>
)} )}
</div> </div>
@ -797,79 +815,123 @@ function LogsTab() {
function ModelsTab() { function ModelsTab() {
const [data, setData] = useState<OpenClawModels | null>(null); const [data, setData] = useState<OpenClawModels | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
useEffect(() => { const fetchModels = () => {
setLoading(true);
api.openclawModels() api.openclawModels()
.then(d => { setData(d); setLoading(false); }) .then(d => { setData(d); setLoading(false); })
.catch(() => setLoading(false)); .catch(() => setLoading(false));
}, []); };
useEffect(() => { fetchModels(); }, []);
if (loading) return <LoadingSpinner />; if (loading) return <LoadingSpinner />;
if (!data || data.error) return <EmptyState message={data?.error || 'Impossible de charger les modèles'} />; if (!data || data.error) return <EmptyState message={data?.error || 'Impossible de charger les modèles'} />;
return ( return (
<div className="space-y-6"> <div className="space-y-6 animate-fade-in">
{/* Providers */} {editing && <ConfigFileEditor onClose={() => setEditing(false)} onSave={() => { setEditing(false); fetchModels(); }} />}
{data.providers.length > 0 && (
<div className="glass-card p-6"> {/* Premium Header */}
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between glass-card p-6 border-l-4 border-l-cyan-500 relative overflow-hidden group">
<span>🔌</span> Providers / MCP Servers <div className="absolute top-0 right-0 w-64 h-full bg-gradient-to-l from-cyan-500/10 to-transparent opacity-50 group-hover:opacity-100 transition-opacity"></div>
<div className="relative z-10 mb-4 sm:mb-0">
<h2 className="text-xl font-bold text-white flex items-center gap-3">
<span>🧠</span> Intelligence Artificielle (LLMs & MCP)
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <p className="text-gray-400 text-sm mt-1.5 flex items-center gap-2">
Fournisseurs, serveurs MCP et modèles configurés
</p>
</div>
<div className="relative z-10 shrink-0">
<button
onClick={() => setEditing(true)}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-cyan-600 to-blue-600 text-white text-sm font-semibold hover:from-cyan-500 hover:to-blue-500 transition-all shadow-[0_0_15px_rgba(6,182,212,0.3)] hover:scale-105 flex items-center gap-2"
>
<span></span> Configurer Modèles
</button>
</div>
</div>
{/* Providers Grid */}
{data.providers.length > 0 && (
<div className="glass-card overflow-hidden">
<div className="p-5 bg-surface-800/50 border-b border-glass-border">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<span className="text-cyan-400">🔌</span> Providers / MCP Servers
</h2>
</div>
<div className="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.providers.map((p, i) => ( {data.providers.map((p, i) => (
<div key={i} className="p-4 rounded-xl bg-surface-800/50 border border-glass-border hover:border-purple-500/20 transition-colors"> <div key={i} className="p-5 rounded-xl bg-surface-900/60 border border-surface-700/50 hover:border-cyan-500/30 transition-all hover:bg-surface-800/80 group">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-3 relative">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500/20 to-indigo-600/10 border border-purple-500/20 flex items-center justify-center text-sm"> <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500/20 to-blue-600/10 border border-cyan-500/20 flex items-center justify-center text-lg shadow-inner group-hover:scale-110 transition-transform">
🔌 {p.type === 'mcp' ? '🧱' : '💬'}
</div> </div>
<div> <div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-sm">{String(p.name || `Provider ${i + 1}`)}</h3> <h3 className="text-white font-bold text-sm truncate pr-2" title={String(p.name || `Provider ${i + 1}`)}>
{p.type ? <span className="text-[10px] text-gray-500">{String(p.type)}</span> : null} {String(p.name || `Provider ${i + 1}`)}
</h3>
<div className="flex items-center gap-2 mt-1">
{Boolean(p.type) && (
<span className="px-2 py-0.5 rounded text-[9px] uppercase tracking-wider font-bold bg-surface-700 text-gray-300">
{String(p.type)}
</span>
)}
{p.enabled !== undefined && (
<span className={`px-2 py-0.5 rounded text-[9px] uppercase tracking-wider font-bold ${p.enabled ? 'bg-emerald-500/20 text-emerald-300' : 'bg-red-500/20 text-red-300'}`}>
{p.enabled ? 'actif' : 'inactif'}
</span>
)}
</div>
</div> </div>
{p.enabled !== undefined && (
<span className={`badge ml-auto text-[10px] ${p.enabled ? 'badge-completed' : 'badge-paused'}`}>
{p.enabled ? 'actif' : 'inactif'}
</span>
)}
</div> </div>
{p.model ? ( {Boolean(p.model) && (
<div className="text-xs text-purple-300 font-mono mt-1"> <div className="text-xs text-blue-300 font-mono mt-2 bg-blue-500/10 px-3 py-1.5 rounded-lg border border-blue-500/10 inline-block truncate max-w-full">
Modèle : {String(p.model)} <span className="opacity-50 mr-1">modèle:</span> {String(p.model)}
</div> </div>
) : null} )}
{p.endpoint ? ( {Boolean(p.endpoint) && (
<div className="text-[10px] text-gray-600 font-mono mt-1 truncate"> <div className="text-[11px] text-gray-500 font-mono mt-2 bg-surface-800 px-3 py-1.5 rounded-lg truncate max-w-full border border-surface-700">
{String(p.endpoint)} {String(p.endpoint)}
</div> </div>
) : null} )}
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
{/* Models */} {/* Models Table */}
{data.models.length > 0 && ( {data.models.length > 0 && (
<div className="glass-card p-6"> <div className="glass-card overflow-hidden">
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2"> <div className="p-5 bg-surface-800/50 border-b border-glass-border">
<span>🧠</span> Modèles configurés <h2 className="text-base font-bold text-white flex items-center gap-2">
</h2> <span className="text-purple-400">🤖</span> Modèles Configurés
</h2>
</div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead className="bg-surface-900/80">
<tr className="border-b border-glass-border"> <tr>
<th className="text-left text-xs text-gray-500 uppercase tracking-wider py-3 px-4">Nom</th> <th className="text-left text-[11px] text-gray-400 font-bold uppercase tracking-wider py-4 px-6 border-b border-surface-700/50 w-1/4">Identifiant Modèle</th>
<th className="text-left text-xs text-gray-500 uppercase tracking-wider py-3 px-4">Détails</th> <th className="text-left text-[11px] text-gray-400 font-bold uppercase tracking-wider py-4 px-6 border-b border-surface-700/50">Détails configuration (JSON)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody className="divide-y divide-surface-700/50">
{data.models.map((m, i) => ( {data.models.map((m, i) => (
<tr key={i} className="border-b border-glass-border/50 hover:bg-glass-hover transition-colors"> <tr key={i} className="hover:bg-surface-800/40 transition-colors">
<td className="py-3 px-4"> <td className="py-4 px-6 align-top">
<span className="text-white font-medium">{String(m.name || `Model ${i + 1}`)}</span> <div className="flex items-center gap-3">
<span className="text-lg opacity-80">🏷</span>
<span className="text-white font-bold bg-surface-800 px-3 py-1.5 rounded-lg border border-surface-600 shadow-sm inline-block">
{String(m.name || `Model ${i + 1}`)}
</span>
</div>
</td> </td>
<td className="py-3 px-4"> <td className="py-4 px-6">
<pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap"> <pre className="text-[11px] text-[#abb2bf] font-mono whitespace-pre-wrap leading-relaxed bg-[#1e222a] p-4 rounded-xl border border-surface-700 shadow-inner">
{JSON.stringify(m, null, 2)} {JSON.stringify(m, null, 2)}
</pre> </pre>
</td> </td>
@ -883,13 +945,13 @@ function ModelsTab() {
{/* Raw config keys */} {/* Raw config keys */}
{data.raw_keys && data.raw_keys.length > 0 && ( {data.raw_keys && data.raw_keys.length > 0 && (
<div className="glass-card p-6"> <div className="glass-card p-5 flex items-center gap-4 flex-wrap">
<h2 className="text-sm font-bold text-gray-400 mb-3 flex items-center gap-2"> <h2 className="text-xs font-bold text-gray-500 uppercase tracking-wider flex items-center gap-2 shrink-0">
<span>🔑</span> Clés de configuration détectées <span>🔑</span> Clés racines détectées :
</h2> </h2>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{data.raw_keys.map(k => ( {data.raw_keys.map(k => (
<span key={k} className="px-3 py-1 rounded-lg bg-surface-700 text-gray-400 text-xs font-mono">{k}</span> <span key={k} className="px-2 py-1 rounded bg-surface-800 text-gray-400 text-[10px] font-mono border border-surface-700">{k}</span>
))} ))}
</div> </div>
</div> </div>
@ -927,6 +989,111 @@ function EmptyState({ message }: { message: string }) {
); );
} }
// ═════════════════════════════════════════════════════════════════════════════════
// Shared Config Editor Overlay
// ═════════════════════════════════════════════════════════════════════════════════
function ConfigFileEditor({ onClose, onSave }: { onClose: () => void, onSave: () => void }) {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
// Lock body scroll
document.body.style.overflow = 'hidden';
api.openclawReadFile('openclaw.json')
.then(res => {
setContent(res.pretty || res.content || '');
setLoading(false);
})
.catch(err => {
console.error(err);
setContent('Erreur de chargement du fichier openclaw.json');
setLoading(false);
});
return () => {
// Restore scroll when unmounting
document.body.style.overflow = '';
};
}, []);
const handleSave = async () => {
setSaving(true);
try {
await api.openclawWriteFile('openclaw.json', content);
onSave();
} catch (err) {
console.error(err);
alert('Erreur lors de la sauvegarde : ' + String(err));
} finally {
setSaving(false);
}
};
return (
<div className="fixed inset-4 z-[200] glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_40px_80px_rgb(0,0,0,0.9)] bg-surface-900/95 backdrop-blur-xl animate-fade-in">
<div className="flex items-center justify-between px-5 py-4 bg-surface-800 border-b border-glass-border shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-600/10 border border-amber-500/20 flex items-center justify-center text-xl">
</div>
<div>
<h3 className="text-white font-bold text-base flex items-center gap-2">
Édition directe
</h3>
<div className="text-xs text-gray-400 font-mono mt-0.5">openclaw.json</div>
</div>
</div>
<div className="flex gap-3">
<button
onClick={onClose}
disabled={saving}
className="px-4 py-2 rounded-lg bg-surface-700 text-gray-300 text-sm font-medium hover:bg-surface-600 transition-colors"
>
Annuler
</button>
<button
onClick={handleSave}
disabled={saving}
className={`px-6 py-2 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 text-white text-sm font-bold transition-all shadow-[0_0_15px_rgba(16,185,129,0.4)] flex items-center gap-2 ${saving ? 'opacity-50 scale-95 cursor-wait' : 'hover:scale-105 hover:from-emerald-500 hover:to-teal-500'}`}
>
{saving ? 'Sauvegarde...' : '💾 Sauvegarder .json'}
</button>
</div>
</div>
<div className="flex-1 overflow-hidden relative bg-[#282c34]">
{loading ? (
<div className="absolute inset-0 flex items-center justify-center flex-col gap-4">
<span className="text-4xl animate-spin-slow opacity-50"></span>
<span className="text-gray-400 font-mono">Lecture de la configuration brute...</span>
</div>
) : (
<div className="h-full w-full absolute inset-0 [&_.cm-editor]:h-full [&_.cm-scroller]:h-full [&_.cm-scroller]:overflow-auto [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[13px] custom-scrollbar">
<CodeMirror
value={content}
height="100%"
theme={oneDark}
extensions={[json()]}
onChange={(val) => setContent(val)}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
tabSize: 2,
}}
/>
</div>
)}
</div>
</div>
);
}
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// Tab 7 — Filesystem (Arborescence) // Tab 7 — Filesystem (Arborescence)
@ -940,6 +1107,7 @@ function FilesystemTab() {
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [editContent, setEditContent] = useState(''); const [editContent, setEditContent] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => { useEffect(() => {
loadTree(); loadTree();
@ -1017,7 +1185,10 @@ function FilesystemTab() {
</div> </div>
{/* Editor Panel */} {/* Editor Panel */}
<div className="glass-card w-2/3 flex flex-col overflow-hidden border border-glass-border shadow-[0_8px_30px_rgb(0,0,0,0.5)]"> <div className={isFullscreen
? "fixed inset-4 z-[100] glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_30px_60px_rgb(0,0,0,0.8)] min-h-0"
: "glass-card w-2/3 flex flex-col overflow-hidden border border-glass-border shadow-[0_8px_30px_rgb(0,0,0,0.5)] min-h-0"
}>
{fileLoading ? ( {fileLoading ? (
<div className="flex-1 flex items-center justify-center bg-surface-900/40"> <div className="flex-1 flex items-center justify-center bg-surface-900/40">
<div className="text-gray-400 flex flex-col items-center gap-3"> <div className="text-gray-400 flex flex-col items-center gap-3">
@ -1047,6 +1218,13 @@ function FilesystemTab() {
</div> </div>
</div> </div>
<div className="flex gap-2 shrink-0"> <div className="flex gap-2 shrink-0">
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="px-3 py-1.5 rounded-lg bg-surface-700 text-gray-300 text-sm hover:bg-surface-600 transition-colors flex items-center justify-center font-bold"
title={isFullscreen ? "Réduire" : "Plein écran"}
>
{isFullscreen ? "🗗" : "⛶"}
</button>
{!selectedFile.binary && ( {!selectedFile.binary && (
<> <>
{editMode ? ( {editMode ? (
@ -1078,7 +1256,7 @@ function FilesystemTab() {
</div> </div>
</div> </div>
) : editMode ? ( ) : editMode ? (
<div className="h-full w-full absolute inset-0 [&_.cm-editor]:h-full [&_.cm-scroller]:h-full [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[13px]"> <div className="h-full w-full absolute inset-0 [&_.cm-editor]:h-full [&_.cm-scroller]:h-full [&_.cm-scroller]:overflow-auto [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[13px]">
<CodeMirror <CodeMirror
value={editContent} value={editContent}
height="100%" height="100%"