feat: Create OpenClaw dashboard page with status, filesystem, config, agents, skills, logs, and models tabs.
This commit is contained in:
parent
2a22990461
commit
8c6ef5acb8
@ -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%"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user