Spaces:
Sleeping
Sleeping
Cyber Catalyst Team commited on
Commit Β·
ebf8c61
1
Parent(s): 0bc1706
Expose /api/workspace endpoints and integrate Workspace Explorer IDE tab in dashboard
Browse files- backend.py +210 -12
backend.py
CHANGED
|
@@ -815,6 +815,61 @@ async def list_models(authorization: str = Header(None)):
|
|
| 815 |
# /health β Health check
|
| 816 |
# ---------------------------------------------------------------------------
|
| 817 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
# ---------------------------------------------------------------------------
|
| 819 |
# Dashboard and Status API
|
| 820 |
# ---------------------------------------------------------------------------
|
|
@@ -858,24 +913,34 @@ DASHBOARD_HTML = """
|
|
| 858 |
</div>
|
| 859 |
</header>
|
| 860 |
|
| 861 |
-
<
|
| 862 |
-
|
| 863 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
<div class="flex items-center justify-between">
|
| 865 |
<h2 class="text-lg font-bold tracking-tight text-gray-300">Nvidia NIM Models & Health Status</h2>
|
| 866 |
-
<span class="text-xs text-gray-500">Checked every
|
| 867 |
</div>
|
| 868 |
-
|
| 869 |
-
<div id="models-container" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 870 |
<!-- Dynamically loaded models go here -->
|
| 871 |
</div>
|
| 872 |
</div>
|
| 873 |
|
| 874 |
-
<!--
|
| 875 |
-
<div class="space-y-6
|
| 876 |
<h2 class="text-lg font-bold tracking-tight text-gray-300">System Activity Logs</h2>
|
| 877 |
-
|
| 878 |
-
<div class="border border-gray-800 rounded-lg overflow-hidden bg-gray-950 flex flex-col flex-1 min-h-[400px]">
|
| 879 |
<div class="bg-gray-900 px-4 py-2 border-b border-gray-800 flex items-center justify-between">
|
| 880 |
<span class="text-xs text-gray-400 font-semibold code-font">agent-stdout.log</span>
|
| 881 |
<div class="flex space-x-1.5">
|
|
@@ -884,14 +949,81 @@ DASHBOARD_HTML = """
|
|
| 884 |
<span class="w-2.5 h-2.5 rounded-full bg-green-500/30"></span>
|
| 885 |
</div>
|
| 886 |
</div>
|
| 887 |
-
<div id="terminal-content" class="p-4 flex-1 overflow-y-auto code-font text-xs text-green-400 bg-black/90 space-y-1 select-all">
|
| 888 |
<!-- Logs go here -->
|
| 889 |
</div>
|
| 890 |
</div>
|
| 891 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
</main>
|
| 893 |
|
| 894 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
async function fetchSystemData() {
|
| 896 |
try {
|
| 897 |
const res = await fetch('/health');
|
|
@@ -914,7 +1046,7 @@ DASHBOARD_HTML = """
|
|
| 914 |
|
| 915 |
models.forEach(model => {
|
| 916 |
const isRec = model.is_recommended;
|
| 917 |
-
const isOnline = model.status
|
| 918 |
|
| 919 |
const card = document.createElement('div');
|
| 920 |
card.className = `p-4 border rounded-xl bg-gray-950 transition-all ${
|
|
@@ -944,6 +1076,7 @@ DASHBOARD_HTML = """
|
|
| 944 |
}
|
| 945 |
|
| 946 |
async function fetchLogs() {
|
|
|
|
| 947 |
try {
|
| 948 |
const res = await fetch('/api/logs');
|
| 949 |
if (!res.ok) return;
|
|
@@ -960,6 +1093,71 @@ DASHBOARD_HTML = """
|
|
| 960 |
}
|
| 961 |
}
|
| 962 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 963 |
setInterval(fetchSystemData, 3000);
|
| 964 |
setInterval(fetchModels, 3000);
|
| 965 |
setInterval(fetchLogs, 2000);
|
|
|
|
| 815 |
# /health β Health check
|
| 816 |
# ---------------------------------------------------------------------------
|
| 817 |
|
| 818 |
+
@app.get("/api/workspace/tree")
|
| 819 |
+
async def get_workspace_tree():
|
| 820 |
+
def build_tree(current_path: Path, relative_to: Path) -> dict:
|
| 821 |
+
name = current_path.name
|
| 822 |
+
try:
|
| 823 |
+
rel_path = str(current_path.relative_to(relative_to)).replace("\\", "/")
|
| 824 |
+
except ValueError:
|
| 825 |
+
rel_path = ""
|
| 826 |
+
if rel_path == ".":
|
| 827 |
+
rel_path = ""
|
| 828 |
+
|
| 829 |
+
if current_path.is_dir():
|
| 830 |
+
children = []
|
| 831 |
+
try:
|
| 832 |
+
for child in sorted(current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name)):
|
| 833 |
+
if child.name in [".git", "node_modules", ".next", "__pycache__", ".agents", ".gemini"]:
|
| 834 |
+
continue
|
| 835 |
+
children.append(build_tree(child, relative_to))
|
| 836 |
+
except Exception:
|
| 837 |
+
pass
|
| 838 |
+
return {
|
| 839 |
+
"name": name or "workspace",
|
| 840 |
+
"path": rel_path,
|
| 841 |
+
"type": "directory",
|
| 842 |
+
"children": children
|
| 843 |
+
}
|
| 844 |
+
else:
|
| 845 |
+
return {
|
| 846 |
+
"name": name,
|
| 847 |
+
"path": rel_path,
|
| 848 |
+
"type": "file",
|
| 849 |
+
"size": current_path.stat().st_size if current_path.exists() else 0
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
try:
|
| 853 |
+
w_path = Path(WORKSPACE_DIR).resolve()
|
| 854 |
+
if not w_path.exists():
|
| 855 |
+
w_path.mkdir(parents=True, exist_ok=True)
|
| 856 |
+
return build_tree(w_path, w_path)
|
| 857 |
+
except Exception as e:
|
| 858 |
+
return {"error": str(e)}
|
| 859 |
+
|
| 860 |
+
|
| 861 |
+
@app.get("/api/workspace/file")
|
| 862 |
+
async def get_workspace_file(path: str):
|
| 863 |
+
try:
|
| 864 |
+
safe_p = _safe_path(path)
|
| 865 |
+
if not safe_p.exists() or not safe_p.is_file():
|
| 866 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 867 |
+
content = safe_p.read_text(encoding="utf-8", errors="replace")
|
| 868 |
+
return {"path": path, "content": content}
|
| 869 |
+
except Exception as e:
|
| 870 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 871 |
+
|
| 872 |
+
|
| 873 |
# ---------------------------------------------------------------------------
|
| 874 |
# Dashboard and Status API
|
| 875 |
# ---------------------------------------------------------------------------
|
|
|
|
| 913 |
</div>
|
| 914 |
</header>
|
| 915 |
|
| 916 |
+
<!-- Navigation Tabs -->
|
| 917 |
+
<div class="border-b border-gray-800 max-w-7xl w-full mx-auto px-6 mt-6 flex space-x-6 text-sm">
|
| 918 |
+
<button onclick="switchTab('models')" id="tab-btn-models" class="pb-3 border-b-2 border-blue-500 font-semibold text-blue-400 transition-all">NIM Models</button>
|
| 919 |
+
<button onclick="switchTab('logs')" id="tab-btn-logs" class="pb-3 border-b-2 border-transparent text-gray-400 hover:text-gray-200 font-semibold transition-all">Live Logs</button>
|
| 920 |
+
<button onclick="switchTab('explorer')" id="tab-btn-explorer" class="pb-3 border-b-2 border-transparent text-gray-400 hover:text-gray-200 font-semibold flex items-center space-x-1 transition-all">
|
| 921 |
+
<span>Workspace Explorer (IDE)</span>
|
| 922 |
+
<span class="px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 text-[10px] font-bold">VS Code View</span>
|
| 923 |
+
</button>
|
| 924 |
+
</div>
|
| 925 |
+
|
| 926 |
+
<!-- MAIN SECTIONS -->
|
| 927 |
+
<main class="max-w-7xl w-full mx-auto px-6 mt-8 flex-1">
|
| 928 |
+
|
| 929 |
+
<!-- SECTION: Models -->
|
| 930 |
+
<div id="section-models" class="space-y-6">
|
| 931 |
<div class="flex items-center justify-between">
|
| 932 |
<h2 class="text-lg font-bold tracking-tight text-gray-300">Nvidia NIM Models & Health Status</h2>
|
| 933 |
+
<span class="text-xs text-gray-500">Checked every 15 mins</span>
|
| 934 |
</div>
|
| 935 |
+
<div id="models-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
|
| 936 |
<!-- Dynamically loaded models go here -->
|
| 937 |
</div>
|
| 938 |
</div>
|
| 939 |
|
| 940 |
+
<!-- SECTION: Logs -->
|
| 941 |
+
<div id="section-logs" class="hidden space-y-6">
|
| 942 |
<h2 class="text-lg font-bold tracking-tight text-gray-300">System Activity Logs</h2>
|
| 943 |
+
<div class="border border-gray-800 rounded-lg overflow-hidden bg-gray-950 flex flex-col min-h-[500px]">
|
|
|
|
| 944 |
<div class="bg-gray-900 px-4 py-2 border-b border-gray-800 flex items-center justify-between">
|
| 945 |
<span class="text-xs text-gray-400 font-semibold code-font">agent-stdout.log</span>
|
| 946 |
<div class="flex space-x-1.5">
|
|
|
|
| 949 |
<span class="w-2.5 h-2.5 rounded-full bg-green-500/30"></span>
|
| 950 |
</div>
|
| 951 |
</div>
|
| 952 |
+
<div id="terminal-content" class="p-4 flex-1 overflow-y-auto code-font text-xs text-green-400 bg-black/90 space-y-1 select-all h-[450px]">
|
| 953 |
<!-- Logs go here -->
|
| 954 |
</div>
|
| 955 |
</div>
|
| 956 |
</div>
|
| 957 |
+
|
| 958 |
+
<!-- SECTION: Workspace Explorer -->
|
| 959 |
+
<div id="section-explorer" class="hidden space-y-6">
|
| 960 |
+
<div class="flex items-center justify-between">
|
| 961 |
+
<h2 class="text-lg font-bold tracking-tight text-gray-300">Visual Workspace IDE</h2>
|
| 962 |
+
<button onclick="refreshFileTree()" class="text-xs px-2.5 py-1 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 hover:bg-blue-500/20 transition-all font-semibold">
|
| 963 |
+
π Refresh Tree
|
| 964 |
+
</button>
|
| 965 |
+
</div>
|
| 966 |
+
|
| 967 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 border border-gray-800 rounded-xl bg-gray-950 overflow-hidden h-[600px]">
|
| 968 |
+
<!-- File Tree Sidebar -->
|
| 969 |
+
<div class="border-r border-gray-800 flex flex-col bg-gray-950 h-full">
|
| 970 |
+
<div class="px-4 py-2 border-b border-gray-800 bg-gray-900 text-xs font-semibold tracking-wider text-gray-400 code-font">
|
| 971 |
+
π EXPLORER: WORKSPACE
|
| 972 |
+
</div>
|
| 973 |
+
<div id="file-tree" class="p-3 flex-1 overflow-y-auto space-y-0.5 select-none">
|
| 974 |
+
<!-- Tree will be loaded here -->
|
| 975 |
+
<span class="text-xs text-gray-500 italic px-2">Loading directory tree...</span>
|
| 976 |
+
</div>
|
| 977 |
+
</div>
|
| 978 |
+
|
| 979 |
+
<!-- Editor panel -->
|
| 980 |
+
<div class="md:col-span-2 flex flex-col bg-black/40 h-full">
|
| 981 |
+
<div class="px-4 py-2 border-b border-gray-800 bg-gray-900 flex items-center justify-between">
|
| 982 |
+
<span id="editor-title" class="text-xs font-semibold text-gray-400 code-font">π Welcome screen</span>
|
| 983 |
+
<div class="flex space-x-1.5">
|
| 984 |
+
<span class="w-2 h-2 rounded-full bg-gray-700"></span>
|
| 985 |
+
<span class="w-2 h-2 rounded-full bg-gray-700"></span>
|
| 986 |
+
</div>
|
| 987 |
+
</div>
|
| 988 |
+
<div class="flex-1 p-4 overflow-auto code-font text-xs text-gray-200">
|
| 989 |
+
<pre id="editor-content" class="whitespace-pre overflow-x-auto select-text h-[500px]">
|
| 990 |
+
Welcome to Claude Code Workspace Explorer.
|
| 991 |
+
Select a file from the sidebar explorer on the left to read its code contents in real-time.
|
| 992 |
+
</pre>
|
| 993 |
+
</div>
|
| 994 |
+
</div>
|
| 995 |
+
</div>
|
| 996 |
+
</div>
|
| 997 |
+
|
| 998 |
</main>
|
| 999 |
|
| 1000 |
<script>
|
| 1001 |
+
let currentTab = 'models';
|
| 1002 |
+
|
| 1003 |
+
function switchTab(tabId) {
|
| 1004 |
+
currentTab = tabId;
|
| 1005 |
+
// Toggle sections
|
| 1006 |
+
document.getElementById('section-models').classList.add('hidden');
|
| 1007 |
+
document.getElementById('section-logs').classList.add('hidden');
|
| 1008 |
+
document.getElementById('section-explorer').classList.add('hidden');
|
| 1009 |
+
|
| 1010 |
+
document.getElementById('tab-btn-models').className = 'pb-3 border-b-2 border-transparent text-gray-400 hover:text-gray-200 font-semibold transition-all';
|
| 1011 |
+
document.getElementById('tab-btn-logs').className = 'pb-3 border-b-2 border-transparent text-gray-400 hover:text-gray-200 font-semibold transition-all';
|
| 1012 |
+
document.getElementById('tab-btn-explorer').className = 'pb-3 border-b-2 border-transparent text-gray-400 hover:text-gray-200 font-semibold flex items-center space-x-1 transition-all';
|
| 1013 |
+
|
| 1014 |
+
if (tabId === 'models') {
|
| 1015 |
+
document.getElementById('section-models').classList.remove('hidden');
|
| 1016 |
+
document.getElementById('tab-btn-models').className = 'pb-3 border-b-2 border-blue-500 font-semibold text-blue-400 transition-all';
|
| 1017 |
+
} else if (tabId === 'logs') {
|
| 1018 |
+
document.getElementById('section-logs').classList.remove('hidden');
|
| 1019 |
+
document.getElementById('tab-btn-logs').className = 'pb-3 border-b-2 border-blue-500 font-semibold text-blue-400 transition-all';
|
| 1020 |
+
} else if (tabId === 'explorer') {
|
| 1021 |
+
document.getElementById('section-explorer').classList.remove('hidden');
|
| 1022 |
+
document.getElementById('tab-btn-explorer').className = 'pb-3 border-b-2 border-blue-500 font-semibold text-blue-400 flex items-center space-x-1 transition-all';
|
| 1023 |
+
refreshFileTree();
|
| 1024 |
+
}
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
async function fetchSystemData() {
|
| 1028 |
try {
|
| 1029 |
const res = await fetch('/health');
|
|
|
|
| 1046 |
|
| 1047 |
models.forEach(model => {
|
| 1048 |
const isRec = model.is_recommended;
|
| 1049 |
+
const isOnline = model.status.includes('ONLINE');
|
| 1050 |
|
| 1051 |
const card = document.createElement('div');
|
| 1052 |
card.className = `p-4 border rounded-xl bg-gray-950 transition-all ${
|
|
|
|
| 1076 |
}
|
| 1077 |
|
| 1078 |
async function fetchLogs() {
|
| 1079 |
+
if (currentTab !== 'logs') return;
|
| 1080 |
try {
|
| 1081 |
const res = await fetch('/api/logs');
|
| 1082 |
if (!res.ok) return;
|
|
|
|
| 1093 |
}
|
| 1094 |
}
|
| 1095 |
|
| 1096 |
+
// File Explorer Logic
|
| 1097 |
+
async function refreshFileTree() {
|
| 1098 |
+
try {
|
| 1099 |
+
const res = await fetch('/api/workspace/tree');
|
| 1100 |
+
if (!res.ok) return;
|
| 1101 |
+
const root = await res.json();
|
| 1102 |
+
|
| 1103 |
+
const container = document.getElementById('file-tree');
|
| 1104 |
+
container.innerHTML = renderNode(root);
|
| 1105 |
+
} catch (e) {
|
| 1106 |
+
console.error(e);
|
| 1107 |
+
}
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
function renderNode(node, depth = 0) {
|
| 1111 |
+
const isDir = node.type === 'directory';
|
| 1112 |
+
const icon = isDir ? 'π' : 'π';
|
| 1113 |
+
const indent = depth * 12;
|
| 1114 |
+
|
| 1115 |
+
let html = `
|
| 1116 |
+
<div class="flex items-center py-1 px-2 hover:bg-gray-800 rounded cursor-pointer transition-all text-xs"
|
| 1117 |
+
style="padding-left: ${indent}px"
|
| 1118 |
+
onclick="${isDir ? `toggleDir('${node.path}')` : `openFile('${node.path}')`}">
|
| 1119 |
+
<span class="mr-2">${icon}</span>
|
| 1120 |
+
<span class="truncate ${isDir ? 'text-gray-300 font-medium' : 'text-gray-400'}">${node.name}</span>
|
| 1121 |
+
</div>
|
| 1122 |
+
`;
|
| 1123 |
+
|
| 1124 |
+
if (isDir && node.children && node.children.length > 0) {
|
| 1125 |
+
html += `<div id="dir-${node.path.replace(/\\/g, '-').replace(/\\//g, '-')}" class="space-y-0.5">`;
|
| 1126 |
+
node.children.forEach(child => {
|
| 1127 |
+
html += renderNode(child, depth + 1);
|
| 1128 |
+
});
|
| 1129 |
+
html += `</div>`;
|
| 1130 |
+
} else if (isDir && (!node.children || node.children.length === 0)) {
|
| 1131 |
+
html += `<div class="text-[10px] text-gray-600 italic" style="padding-left: ${indent + 16}px">(empty)</div>`;
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
return html;
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
async function openFile(path) {
|
| 1138 |
+
document.getElementById('editor-title').innerText = `π ${path}`;
|
| 1139 |
+
document.getElementById('editor-content').innerText = "Loading file content...";
|
| 1140 |
+
try {
|
| 1141 |
+
const res = await fetch(`/api/workspace/file?path=${encodeURIComponent(path)}`);
|
| 1142 |
+
if (!res.ok) {
|
| 1143 |
+
document.getElementById('editor-content').innerText = "Error: Failed to fetch file content.";
|
| 1144 |
+
return;
|
| 1145 |
+
}
|
| 1146 |
+
const data = await res.json();
|
| 1147 |
+
document.getElementById('editor-content').innerText = data.content;
|
| 1148 |
+
} catch (e) {
|
| 1149 |
+
document.getElementById('editor-content').innerText = `Error: ${e.message}`;
|
| 1150 |
+
}
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
function toggleDir(path) {
|
| 1154 |
+
const safeId = `dir-${path.replace(/\\/g, '-').replace(/\\//g, '-')}`;
|
| 1155 |
+
const elem = document.getElementById(safeId);
|
| 1156 |
+
if (elem) {
|
| 1157 |
+
elem.classList.toggle('hidden');
|
| 1158 |
+
}
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
setInterval(fetchSystemData, 3000);
|
| 1162 |
setInterval(fetchModels, 3000);
|
| 1163 |
setInterval(fetchLogs, 2000);
|