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
Files changed (1) hide show
  1. 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
- <main class="max-w-7xl w-full mx-auto px-6 mt-8 flex-1 grid grid-cols-1 lg:grid-cols-3 gap-8">
862
- <!-- Left: Models Grid -->
863
- <div class="lg:col-span-2 space-y-6">
 
 
 
 
 
 
 
 
 
 
 
 
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 5 mins</span>
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
- <!-- Right: Log Viewer -->
875
- <div class="space-y-6 flex flex-col h-full">
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 === 'ONLINE';
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);