feat: implement OpenClaw integration with new API endpoints, frontend page, client, and tests.
This commit is contained in:
parent
8c6ef5acb8
commit
3b82d69b22
@ -12,6 +12,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
|
import sqlite3
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
@ -473,6 +474,77 @@ async def list_openclaw_skills():
|
|||||||
return {"skills": skills, "count": len(skills)}
|
return {"skills": skills, "count": len(skills)}
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 4.a Agent Memory (QMD)
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@router.get("/agents/{agent_name}/memory")
|
||||||
|
async def get_agent_memory(agent_name: str, limit: int = 100):
|
||||||
|
"""Attempt to read the agent's memory (QMD) SQLite database."""
|
||||||
|
ws_dir = _workspace() / agent_name
|
||||||
|
if not ws_dir.is_dir():
|
||||||
|
return {"has_db": False, "error": "Workspace not found"}
|
||||||
|
|
||||||
|
db_files = list(ws_dir.rglob("*.sqlite")) + list(ws_dir.rglob("*.db")) + list(ws_dir.rglob("*.qmd"))
|
||||||
|
if not db_files:
|
||||||
|
return {"has_db": False, "message": "No SQLite memory database found"}
|
||||||
|
|
||||||
|
db_path = db_files[0]
|
||||||
|
result = {
|
||||||
|
"has_db": True,
|
||||||
|
"db_path": str(db_path.relative_to(_home())),
|
||||||
|
"size": db_path.stat().st_size,
|
||||||
|
"timeline": [],
|
||||||
|
"stats": {"total_thoughts": 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _read_db():
|
||||||
|
timeline = []
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if common thought tables exist
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||||
|
tables = [row["name"] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
result["tables"] = tables
|
||||||
|
|
||||||
|
# Very heuristic approach to grab 'thoughts' or 'history'
|
||||||
|
target_table = None
|
||||||
|
for t in ["thoughts", "messages", "history", "runs", "memory", "qmd_memory", "events"]:
|
||||||
|
if t in tables:
|
||||||
|
target_table = t
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_table:
|
||||||
|
# Get column names
|
||||||
|
cursor.execute(f"PRAGMA table_info('{target_table}')")
|
||||||
|
columns = [c[1] for c in cursor.fetchall()]
|
||||||
|
|
||||||
|
query = f"SELECT * FROM {target_table} ORDER BY rowid DESC LIMIT ?"
|
||||||
|
if "timestamp" in columns or "created_at" in columns:
|
||||||
|
order_col = "timestamp" if "timestamp" in columns else "created_at"
|
||||||
|
query = f"SELECT * FROM {target_table} ORDER BY {order_col} DESC LIMIT ?"
|
||||||
|
|
||||||
|
cursor.execute(query, (limit,))
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
timeline = [dict(r) for r in rows]
|
||||||
|
|
||||||
|
# Count total
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {target_table}")
|
||||||
|
result["stats"]["total_thoughts"] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return timeline
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error reading QMD db: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
result["timeline"] = await asyncio.to_thread(_read_db)
|
||||||
|
return result
|
||||||
|
|
||||||
# ═════════════════════════════════════════════════════════════════════════════════
|
# ═════════════════════════════════════════════════════════════════════════════════
|
||||||
# 5. Logs
|
# 5. Logs
|
||||||
# ═════════════════════════════════════════════════════════════════════════════════
|
# ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
47
backend/test_qmd.py
Normal file
47
backend/test_qmd.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def explore_qmd():
|
||||||
|
foxy_home_str = os.environ.get("FOXY_HOME", "/home/foxy/.openclaw")
|
||||||
|
# If running on windows local, this might resolve weirdly.
|
||||||
|
# We will try the raw string first.
|
||||||
|
home = Path(foxy_home_str)
|
||||||
|
|
||||||
|
# Alternatively try C:\home\foxy\.openclaw if windows
|
||||||
|
if not home.exists() and os.name == 'nt':
|
||||||
|
alt_home = Path("C:") / foxy_home_str
|
||||||
|
if alt_home.exists():
|
||||||
|
home = alt_home
|
||||||
|
|
||||||
|
workspace = home / "workspace"
|
||||||
|
print("Exploring workspace:", workspace)
|
||||||
|
print("Exists:", workspace.exists())
|
||||||
|
|
||||||
|
if not workspace.exists():
|
||||||
|
print("Cannot find workspace.")
|
||||||
|
return
|
||||||
|
|
||||||
|
sqlite_files = list(workspace.rglob("*.sqlite")) + list(workspace.rglob("*.db")) + list(workspace.rglob("*.qmd"))
|
||||||
|
print("Found sqlite files:", len(sqlite_files))
|
||||||
|
|
||||||
|
for f in sqlite_files:
|
||||||
|
print("\n--- SCHEMA FOR", f)
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(f)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||||
|
tables = cursor.fetchall()
|
||||||
|
for t in tables:
|
||||||
|
table_name = t[0]
|
||||||
|
print(f"Table: {table_name}")
|
||||||
|
cursor.execute(f"PRAGMA table_info('{table_name}')")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
for c in columns:
|
||||||
|
print(f" - {c[1]} ({c[2]})")
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print("Error reading db:", e)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
explore_qmd()
|
||||||
@ -246,6 +246,17 @@ export interface AgentFileResult {
|
|||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentMemoryResult {
|
||||||
|
has_db: boolean;
|
||||||
|
db_path?: string;
|
||||||
|
size?: number;
|
||||||
|
tables?: string[];
|
||||||
|
timeline: Record<string, any>[];
|
||||||
|
stats: { total_thoughts: number };
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── HTTP Helpers ───────────────────────────────────────────────────────────
|
// ─── HTTP Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
@ -339,6 +350,8 @@ export const api = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ content }),
|
body: JSON.stringify({ content }),
|
||||||
}),
|
}),
|
||||||
|
openclawAgentMemory: (name: string, limit: number = 100) =>
|
||||||
|
request<AgentMemoryResult>(`/api/openclaw/agents/${encodeURIComponent(name)}/memory?limit=${limit}`),
|
||||||
openclawSkills: () => request<{ skills: OpenClawSkill[]; count: number }>('/api/openclaw/skills'),
|
openclawSkills: () => request<{ skills: OpenClawSkill[]; count: number }>('/api/openclaw/skills'),
|
||||||
openclawLogs: (params: { lines?: number; level?: string }) => {
|
openclawLogs: (params: { lines?: number; level?: string }) => {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { yaml } from '@codemirror/lang-yaml';
|
|||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import type {
|
import type {
|
||||||
OpenClawStatus, OpenClawAgent, OpenClawSkill, OpenClawLogs,
|
OpenClawStatus, OpenClawAgent, OpenClawSkill, OpenClawLogs,
|
||||||
OpenClawModels, FileTreeNode, ReadFileResult
|
OpenClawModels, FileTreeNode, ReadFileResult, AgentMemoryResult
|
||||||
} from '../api/client';
|
} from '../api/client';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
|
|
||||||
@ -336,13 +336,107 @@ function renderValue(v: unknown): string {
|
|||||||
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
// Tab 3 — Agents
|
// Tab 3 — Agents (with Memory Viewer)
|
||||||
// ═════════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function AgentMemoryViewer({ agentName }: { agentName: string }) {
|
||||||
|
const [memory, setMemory] = useState<AgentMemoryResult | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
api.openclawAgentMemory(agentName)
|
||||||
|
.then(res => {
|
||||||
|
if (active) {
|
||||||
|
setMemory(res);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
if (active) setLoading(false);
|
||||||
|
});
|
||||||
|
return () => { active = false; };
|
||||||
|
}, [agentName]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 text-gray-400">
|
||||||
|
<span className="text-3xl animate-spin-slow mb-4">⌛</span>
|
||||||
|
<p className="animate-pulse">Connexion à la mémoire QMD de l'agent...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memory || !memory.has_db) {
|
||||||
|
return (
|
||||||
|
<div className="bg-surface-800/50 border border-glass-border rounded-xl p-8 text-center flex flex-col items-center justify-center">
|
||||||
|
<span className="text-5xl opacity-30 grayscale mb-4">🗄️</span>
|
||||||
|
<h3 className="text-white font-semibold mb-2">Mémoire Introuvable</h3>
|
||||||
|
<p className="text-gray-400 text-sm max-w-md mx-auto">
|
||||||
|
Aucune base de données SQLite QMD n'a été trouvée pour cet agent dans le workspace.
|
||||||
|
Lumina ne dispose pas (encore) d'une trame chronologique ici.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 font-mono mt-4">Astuce : Vérifiez la configuration du dossier workspace sous FOXY_HOME.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center px-4 py-3 bg-gradient-to-r from-blue-900/20 to-indigo-900/20 rounded-xl border border-blue-500/10">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-blue-300 font-bold flex items-center gap-2">
|
||||||
|
<span>🧠</span> Labyrinth Memoire QMD
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-blue-300/60 font-mono mt-1">{memory.db_path} ({formatSize(memory.size || 0)})</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-2xl font-bold text-white">{memory.stats.total_thoughts}</div>
|
||||||
|
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Pensées / Actions</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative pl-6 before:absolute before:inset-0 before:left-[11px] before:w-0.5 before:bg-gradient-to-b before:from-blue-500/50 before:to-transparent pt-2">
|
||||||
|
{memory.timeline.length === 0 ? (
|
||||||
|
<div className="text-gray-500 text-sm py-4">La mémoire existe mais elle est actuellement vide.</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{memory.timeline.map((item: Record<string, any>, idx: number) => (
|
||||||
|
<div key={idx} className="relative group">
|
||||||
|
{/* Node dot */}
|
||||||
|
<div className="absolute -left-[31px] top-1.5 w-4 h-4 rounded-full bg-surface-900 border-2 border-blue-500/50 group-hover:bg-blue-500 group-hover:shadow-[0_0_10px_rgba(59,130,246,0.6)] transition-all z-10"></div>
|
||||||
|
|
||||||
|
{/* Content Card */}
|
||||||
|
<div className="glass-card p-4 border border-surface-700/50 hover:border-blue-500/30 transition-colors">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<span className="text-[10px] font-mono text-gray-500 bg-surface-800 px-2 py-0.5 rounded">
|
||||||
|
{item.timestamp || item.created_at || 'Inconnu'}
|
||||||
|
</span>
|
||||||
|
{(item.type || item.event_type || item.role) && (
|
||||||
|
<span className="text-[10px] uppercase font-bold text-indigo-300 bg-indigo-500/10 px-2 py-0.5 rounded">
|
||||||
|
{item.type || item.event_type || item.role}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<pre className="text-sm text-gray-300 whitespace-pre-wrap font-sans leading-relaxed">
|
||||||
|
{item.content || item.message || JSON.stringify(item, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AgentsTab() {
|
function AgentsTab() {
|
||||||
const [agents, setAgents] = useState<OpenClawAgent[]>([]);
|
const [agents, setAgents] = useState<OpenClawAgent[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
||||||
|
const [activeView, setActiveView] = useState<'identity' | 'memory'>('identity');
|
||||||
|
|
||||||
const [deleting, setDeleting] = useState<string | null>(null);
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||||||
const [activeFile, setActiveFile] = useState<{agent: string, file: string} | null>(null);
|
const [activeFile, setActiveFile] = useState<{agent: string, file: string} | null>(null);
|
||||||
@ -437,7 +531,12 @@ 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); setIsFullscreen(false); }}
|
onClick={() => {
|
||||||
|
setExpandedAgent(expandedAgent === agent.name ? null : agent.name);
|
||||||
|
setActiveFile(null);
|
||||||
|
setIsFullscreen(false);
|
||||||
|
setActiveView('identity');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<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">
|
||||||
@ -476,13 +575,33 @@ function AgentsTab() {
|
|||||||
{expandedAgent === agent.name && (
|
{expandedAgent === agent.name && (
|
||||||
<div className="border-t border-glass-border p-5 bg-surface-900/40 space-y-6">
|
<div className="border-t border-glass-border p-5 bg-surface-900/40 space-y-6">
|
||||||
|
|
||||||
{/* Agent Identity Files Manager */}
|
{/* View Tabs */}
|
||||||
{agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && (
|
<div className="flex gap-2 border-b border-surface-700/50 pb-3 -mx-2 px-2">
|
||||||
<div>
|
<button
|
||||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-3 flex items-center gap-2">
|
onClick={() => setActiveView('identity')}
|
||||||
<span>🧠</span> Fichiers d'identité
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${activeView === 'identity' ? 'bg-surface-800 text-white border border-surface-600 shadow-md' : 'text-gray-500 hover:text-gray-300 hover:bg-surface-800/50'}`}
|
||||||
</h4>
|
>
|
||||||
<div className="flex gap-2 flex-wrap mb-3">
|
<span>🗂️</span> Paramètres & Fichiers
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('memory')}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${activeView === 'memory' ? 'bg-indigo-900/30 text-indigo-300 border border-indigo-500/20 shadow-[0_0_15px_rgba(79,70,229,0.1)]' : 'text-gray-500 hover:text-indigo-400 hover:bg-indigo-900/10'}`}
|
||||||
|
>
|
||||||
|
<span>🧠</span> Mémoires d'agent
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeView === 'memory' ? (
|
||||||
|
<AgentMemoryViewer agentName={agent.name} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Agent Identity Files Manager */}
|
||||||
|
{agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||||
|
<span>⚙️</span> Fichiers d'identité
|
||||||
|
</h4>
|
||||||
|
<div className="flex gap-2 flex-wrap mb-3">
|
||||||
{agent.identity_files.map(file => {
|
{agent.identity_files.map(file => {
|
||||||
const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name;
|
const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name;
|
||||||
return (
|
return (
|
||||||
@ -506,10 +625,10 @@ 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 flex flex-col ${
|
<div className={`border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in flex flex-col ${
|
||||||
isFullscreen ? 'fixed inset-4 z-[100] shadow-2xl' : 'h-[500px] relative'
|
isFullscreen ? 'fixed inset-0 w-screen h-screen z-[9999] m-0 p-0 rounded-none' : 'h-[500px] rounded-xl relative'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex items-center justify-between px-3 py-2 bg-surface-900/80 border-b border-glass-border shrink-0">
|
<div className={`flex items-center justify-between px-3 py-2 bg-surface-900/80 border-b border-glass-border shrink-0 ${isFullscreen ? 'h-14 px-6' : ''}`}>
|
||||||
<span className="text-xs font-mono text-purple-300 truncate pr-4">workspace/{agent.name}/{activeFile.file}</span>
|
<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">
|
<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">
|
<button onClick={() => setIsFullscreen(!isFullscreen)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
|
||||||
@ -527,7 +646,7 @@ function AgentsTab() {
|
|||||||
<div className="flex-1 flex items-center justify-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="flex-1 overflow-hidden relative min-h-0 bg-[#282c34]">
|
<div className="flex-1 overflow-hidden relative min-h-0 bg-[#282c34]">
|
||||||
<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">
|
<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 pb-10">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
height="100%"
|
height="100%"
|
||||||
@ -555,7 +674,7 @@ function AgentsTab() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Configuration (Read-only) */}
|
{/* Configuration (Read-only) */}
|
||||||
{agent.config && !activeFile && (
|
{agent.config && !activeFile && activeView === 'identity' && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-2 flex justify-between items-center">
|
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-2 flex justify-between items-center">
|
||||||
<span>Configuration JSON/YAML</span>
|
<span>Configuration JSON/YAML</span>
|
||||||
@ -566,6 +685,8 @@ function AgentsTab() {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1033,8 +1154,8 @@ function ConfigFileEditor({ onClose, onSave }: { onClose: () => void, onSave: ()
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="fixed inset-0 w-screen h-screen z-[9999] m-0 p-0 rounded-none 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 justify-between px-6 py-4 bg-surface-800 border-b border-glass-border shrink-0 h-16">
|
||||||
<div className="flex items-center gap-3">
|
<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 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">
|
||||||
✏️
|
✏️
|
||||||
@ -1072,7 +1193,7 @@ function ConfigFileEditor({ onClose, onSave }: { onClose: () => void, onSave: ()
|
|||||||
<span className="text-gray-400 font-mono">Lecture de la configuration brute...</span>
|
<span className="text-gray-400 font-mono">Lecture de la configuration brute...</span>
|
||||||
</div>
|
</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">
|
<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 pb-10">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={content}
|
value={content}
|
||||||
height="100%"
|
height="100%"
|
||||||
@ -1186,7 +1307,7 @@ function FilesystemTab() {
|
|||||||
|
|
||||||
{/* Editor Panel */}
|
{/* Editor Panel */}
|
||||||
<div className={isFullscreen
|
<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"
|
? "fixed inset-0 w-screen h-screen z-[9999] m-0 p-0 rounded-none glass-card flex flex-col overflow-hidden border border-glass-border bg-surface-900 shadow-[0_30px_60px_rgb(0,0,0,0.9)] 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"
|
: "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 ? (
|
||||||
@ -1204,7 +1325,7 @@ function FilesystemTab() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Editor Header */}
|
{/* Editor Header */}
|
||||||
<div className="p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0">
|
<div className={`p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0 ${isFullscreen ? 'h-16 px-6' : ''}`}>
|
||||||
<div className="min-w-0 pr-4">
|
<div className="min-w-0 pr-4">
|
||||||
<h3 className="text-white font-medium flex items-center gap-2 text-base truncate">
|
<h3 className="text-white font-medium flex items-center gap-2 text-base truncate">
|
||||||
<span className="text-xl">📄</span> {selectedFile.path.split('/').pop()}
|
<span className="text-xl">📄</span> {selectedFile.path.split('/').pop()}
|
||||||
@ -1256,7 +1377,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]:overflow-auto [&_.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] pb-10">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={editContent}
|
value={editContent}
|
||||||
height="100%"
|
height="100%"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user