moved_repo_first_update
This commit is contained in:
+1973
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,481 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hypertower V2 Builder</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<section class="pane left">
|
||||
<header class="pane-header">
|
||||
<div>
|
||||
<h1>Hypertower V2 Builder</h1>
|
||||
<p class="subtitle">
|
||||
Draft your towers, bridges, and transforms, then inspect the generated JSON.
|
||||
</p>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span id="node-count">0 nodes</span>
|
||||
<span id="edge-count">0 edges</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Nodes</h2>
|
||||
<div class="button-grid">
|
||||
<button data-add="data">Add Data Source</button>
|
||||
<button data-add="filter">Add Filter</button>
|
||||
<button data-add="transform">Add Transform</button>
|
||||
<button data-add="loader">Add Loader</button>
|
||||
<button data-add="image_tower">Add Tower</button>
|
||||
<button data-add="bridge">Add Bridge</button>
|
||||
<button data-add="classifier">Add Classifier</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Presets</h2>
|
||||
<div class="field">
|
||||
<label for="preset-select">Preset</label>
|
||||
<select id="preset-select"></select>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="preset-apply">Apply Preset</button>
|
||||
<button id="preset-save" class="ghost">Save Selection</button>
|
||||
</div>
|
||||
<p class="hint">Use Shift + drag on the graph to select multiple nodes.</p>
|
||||
<div class="field">
|
||||
<label for="import-class-select">Import class</label>
|
||||
<select id="import-class-select">
|
||||
<option value="">Select…</option>
|
||||
<option value="PapilaData">PapilaData</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="import-class-btn">Import Class</button>
|
||||
</div>
|
||||
<p class="hint">Adds data sources for the selected dataset.</p>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Selection</h2>
|
||||
<div class="field">
|
||||
<label>Selected Node</label>
|
||||
<div id="selected-node" class="pill muted">None</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="node-label">Label</label>
|
||||
<input id="node-label" placeholder="Rename node" />
|
||||
</div>
|
||||
|
||||
<div id="selection-generic">
|
||||
<div class="field">
|
||||
<label for="node-slot">Input Slot</label>
|
||||
<input id="node-slot" placeholder="e.g. image_1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="node-output">Output Slot</label>
|
||||
<input id="node-output" placeholder="e.g. embedding_1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-loader" class="selection-block hidden">
|
||||
<div class="field">
|
||||
<label>Input Type</label>
|
||||
<select id="loader-input-type">
|
||||
<option value="image">image</option>
|
||||
<option value="matrix">matrix</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Input Index</label>
|
||||
<select id="loader-input-index">
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Output Type</label>
|
||||
<select id="loader-output-type" disabled>
|
||||
<option value="image">image</option>
|
||||
<option value="matrix">matrix</option>
|
||||
</select>
|
||||
<div class="hint">Output mirrors input type.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-filter" class="selection-block hidden">
|
||||
<div class="field">
|
||||
<label>Input Type</label>
|
||||
<select id="filter-input-type">
|
||||
<option value="image">image</option>
|
||||
<option value="matrix">matrix</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Output Type</label>
|
||||
<select id="filter-output-type" disabled>
|
||||
<option value="image">image</option>
|
||||
<option value="matrix">matrix</option>
|
||||
</select>
|
||||
<div class="hint">Output mirrors input type.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Filter Type</label>
|
||||
<select id="filter-type">
|
||||
<option value="regex">regex</option>
|
||||
<option value="column">column</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="filter-regex-fields">
|
||||
<div class="field">
|
||||
<label for="filter-regex-pattern">Regex Pattern</label>
|
||||
<input id="filter-regex-pattern" placeholder="e.g. OD|_OD|Right" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="filter-column-fields" class="hidden">
|
||||
<div class="field">
|
||||
<label for="filter-column-name">Column</label>
|
||||
<input id="filter-column-name" placeholder="e.g. IOP" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="filter-column-operator">Operator</label>
|
||||
<select id="filter-column-operator">
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value="<="><=</option>
|
||||
<option value="<"><</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="filter-column-value">Value</label>
|
||||
<input id="filter-column-value" placeholder="e.g. 21" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-transform" class="selection-block hidden">
|
||||
<div class="field">
|
||||
<label>Transform Type</label>
|
||||
<select id="transform-type">
|
||||
<option value="roi_crop">roi_crop</option>
|
||||
<option value="center_crop">center_crop</option>
|
||||
<option value="jitter_bundle">jitter_bundle</option>
|
||||
<option value="resize">resize</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="transform-roi-fields">
|
||||
<div class="field">
|
||||
<label>Mask Source</label>
|
||||
<select id="transform-roi-mask-source">
|
||||
<option value="gt">gt</option>
|
||||
<option value="unet">unet</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="transform-roi-scale">Scale</label>
|
||||
<input id="transform-roi-scale" type="number" step="0.1" value="2.5" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="transform-roi-target">Target Size</label>
|
||||
<input id="transform-roi-target" type="number" step="1" value="224" />
|
||||
<div class="hint">Set empty to skip resizing.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="transform-roi-fallback" type="checkbox" checked />
|
||||
Fallback to original if mask missing
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transform-center-fields" class="hidden">
|
||||
<div class="field">
|
||||
<label for="transform-center-size">Center Crop Size</label>
|
||||
<input id="transform-center-size" type="number" step="1" value="224" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transform-jitter-fields" class="hidden">
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="transform-jitter-hflip" type="checkbox" checked />
|
||||
Random horizontal flip
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="transform-jitter-vflip" type="checkbox" checked />
|
||||
Random vertical flip
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="transform-jitter-rotation">Rotation (deg)</label>
|
||||
<input id="transform-jitter-rotation" type="number" step="1" value="15" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="transform-jitter-color-enabled" type="checkbox" checked />
|
||||
Color jitter
|
||||
</label>
|
||||
<input
|
||||
id="transform-jitter-color"
|
||||
placeholder="0.1,0.1,0.1,0.05"
|
||||
value="0.1,0.1,0.1,0.05"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transform-resize-fields" class="hidden">
|
||||
<div class="field">
|
||||
<label for="transform-resize-size">Resize Size</label>
|
||||
<input id="transform-resize-size" type="number" step="1" value="256" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-tower" class="selection-block hidden">
|
||||
<div class="field">
|
||||
<label>Tower Type</label>
|
||||
<select id="tower-type">
|
||||
<option value="image">image</option>
|
||||
<option value="metadata">metadata</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="tower-image-fields">
|
||||
<div class="field">
|
||||
<label>Backbone</label>
|
||||
<select id="tower-backbone">
|
||||
<option value="efficientnet_b0">efficientnet_b0</option>
|
||||
<option value="resnet50">resnet50</option>
|
||||
<option value="densenet121">densenet121</option>
|
||||
<option value="vgg16">vgg16</option>
|
||||
<option value="mobilenet_v2">mobilenet_v2</option>
|
||||
<option value="inception_v3">inception_v3</option>
|
||||
<option value="refugelike">refugelike</option>
|
||||
<option value="refuge_densenet">refuge_densenet</option>
|
||||
<option value="refuge_efficient_b0">refuge_efficient_b0</option>
|
||||
<option value="refuge_efficient_b7">refuge_efficient_b7</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tower-freeze-ratio">Freeze Ratio</label>
|
||||
<input id="tower-freeze-ratio" type="number" step="0.05" value="0" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="tower-augment" type="checkbox" checked />
|
||||
Image augmentation
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tower-geometry-dim">Geometry Dim</label>
|
||||
<input id="tower-geometry-dim" type="number" step="1" value="0" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="tower-use-se" type="checkbox" />
|
||||
Use SE
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tower-se-reduction">SE Reduction</label>
|
||||
<input id="tower-se-reduction" type="number" step="1" value="16" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="tower-se-pre-norm" type="checkbox" checked />
|
||||
SE Pre-Norm
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tower-md-fields" class="hidden">
|
||||
<div class="field">
|
||||
<label for="tower-md-hidden">Hidden Dim</label>
|
||||
<input id="tower-md-hidden" type="number" step="1" value="128" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tower-md-dropout">Dropout</label>
|
||||
<input id="tower-md-dropout" type="number" step="0.05" value="0.1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="tower-md-use-se" type="checkbox" />
|
||||
Use SE
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tower-md-se-reduction">SE Reduction</label>
|
||||
<input id="tower-md-se-reduction" type="number" step="1" value="16" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="tower-md-se-pre-norm" type="checkbox" checked />
|
||||
SE Pre-Norm
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tower-md-freeze-ratio">Freeze Ratio</label>
|
||||
<input id="tower-md-freeze-ratio" type="number" step="0.05" value="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-bridge" class="selection-block hidden">
|
||||
<div class="field">
|
||||
<label>Bridge Method</label>
|
||||
<select id="bridge-method">
|
||||
<option value="fusion">fusion</option>
|
||||
<option value="consensus">consensus</option>
|
||||
</select>
|
||||
<div class="hint">Fusion uses embeddings; consensus combines logits.</div>
|
||||
</div>
|
||||
<div id="bridge-fusion-fields">
|
||||
<div class="field">
|
||||
<label for="bridge-fusion-dim">Fusion Dim</label>
|
||||
<input id="bridge-fusion-dim" type="number" step="1" value="256" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="bridge-use-se" type="checkbox" checked />
|
||||
Use SE
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="bridge-se-reduction">SE Reduction</label>
|
||||
<input id="bridge-se-reduction" type="number" step="1" value="16" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input id="bridge-se-pre-norm" type="checkbox" checked />
|
||||
SE Pre-Norm
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-classifier" class="selection-block hidden">
|
||||
<div class="hint">
|
||||
Classifier settings are managed at the Hypertower level.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selection-data" class="selection-block hidden">
|
||||
<div class="field">
|
||||
<label>Output Type (select first)</label>
|
||||
<select id="data-output-type">
|
||||
<option value="">Select…</option>
|
||||
<option value="image">image</option>
|
||||
<option value="matrix">matrix</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Source</label>
|
||||
<div class="button-row">
|
||||
<button id="data-browse" class="ghost">Browse…</button>
|
||||
</div>
|
||||
<div id="data-source-status" class="hint">No source selected.</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="duplicate-data" class="ghost">Duplicate Data Source</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button id="apply-node">Apply</button>
|
||||
<button id="delete-node" class="danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>State</h2>
|
||||
<div class="button-row">
|
||||
<button id="auto-layout">Auto Layout</button>
|
||||
<button id="reset-graph" class="ghost">Reset</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="save-local">Save Local</button>
|
||||
<button id="load-local">Load Local</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="export-json">Export JSON</button>
|
||||
<label class="file-upload">
|
||||
Import JSON
|
||||
<input id="import-json" type="file" accept="application/json" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane right">
|
||||
<header class="pane-header">
|
||||
<div>
|
||||
<h2>Model Graph</h2>
|
||||
<div class="hint">Drag to pan. Scroll to zoom.</div>
|
||||
</div>
|
||||
<div class="graph-controls">
|
||||
<button id="connect-mode">Connect</button>
|
||||
<button id="disconnect-selected">Remove Selected Edges</button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="graph-container">
|
||||
<svg id="graph" viewBox="0 0 1200 900" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane bottom">
|
||||
<header class="pane-header">
|
||||
<h2>Configuration JSON</h2>
|
||||
<button id="toggle-json" class="ghost">Collapse</button>
|
||||
</header>
|
||||
<pre id="json-view" class="json-view"></pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="modal-overlay" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<header class="modal-header">
|
||||
<div>
|
||||
<h3>Browse Repository</h3>
|
||||
<p id="modal-mode" class="hint">Select a directory.</p>
|
||||
</div>
|
||||
<button id="modal-close" class="ghost">Close</button>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div id="breadcrumb" class="breadcrumb"></div>
|
||||
<div id="modal-list" class="modal-list"></div>
|
||||
</div>
|
||||
<footer class="modal-footer">
|
||||
<button id="modal-select-dir" class="ghost">Use this directory</button>
|
||||
<button id="modal-select-file" class="ghost">Use selected file</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="preview-overlay" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<header class="modal-header">
|
||||
<div>
|
||||
<h3>Data Browser</h3>
|
||||
<p id="preview-subtitle" class="hint">No source selected.</p>
|
||||
</div>
|
||||
<button id="preview-close" class="ghost">Close</button>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div id="preview-body" class="preview-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const url = require("url");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
const WEBROOT = path.resolve(__dirname);
|
||||
|
||||
function sendJson(res, status, payload) {
|
||||
const body = JSON.stringify(payload, null, 2);
|
||||
res.writeHead(status, {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function sendFile(res, filePath) {
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mime =
|
||||
ext === ".html"
|
||||
? "text/html"
|
||||
: ext === ".css"
|
||||
? "text/css"
|
||||
: ext === ".js"
|
||||
? "application/javascript"
|
||||
: "application/octet-stream";
|
||||
res.writeHead(200, { "Content-Type": mime });
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
function listDir(requestedPath) {
|
||||
const cleaned = (requestedPath || "").replace(/^\/+/, "");
|
||||
const safePath = path.resolve(ROOT, cleaned || ".");
|
||||
if (!safePath.startsWith(ROOT)) {
|
||||
return { error: "Path outside repo root." };
|
||||
}
|
||||
if (!fs.existsSync(safePath)) {
|
||||
return { error: "Path not found." };
|
||||
}
|
||||
const stat = fs.statSync(safePath);
|
||||
if (!stat.isDirectory()) {
|
||||
return { error: "Path is not a directory." };
|
||||
}
|
||||
const entries = fs.readdirSync(safePath, { withFileTypes: true });
|
||||
const formatted = entries.map((entry) => {
|
||||
const entryPath = path.join(safePath, entry.name);
|
||||
const relPath = path.relative(ROOT, entryPath);
|
||||
const isDir = entry.isDirectory();
|
||||
const size = isDir ? 0 : fs.statSync(entryPath).size;
|
||||
return {
|
||||
name: entry.name,
|
||||
type: isDir ? "dir" : "file",
|
||||
relPath: relPath.replace(/\\/g, "/"),
|
||||
size,
|
||||
};
|
||||
});
|
||||
formatted.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
return { path: path.relative(ROOT, safePath).replace(/\\/g, "/"), entries: formatted };
|
||||
}
|
||||
|
||||
function readTabularFile(requestedPath, limit = 50) {
|
||||
const cleaned = (requestedPath || "").replace(/^\/+/, "");
|
||||
const safePath = path.resolve(ROOT, cleaned || ".");
|
||||
if (!safePath.startsWith(ROOT)) {
|
||||
return { error: "Path outside repo root." };
|
||||
}
|
||||
if (!fs.existsSync(safePath)) {
|
||||
return { error: "Path not found." };
|
||||
}
|
||||
const stat = fs.statSync(safePath);
|
||||
if (!stat.isFile()) {
|
||||
return { error: "Path is not a file." };
|
||||
}
|
||||
const ext = path.extname(safePath).toLowerCase();
|
||||
if ([".xlsx", ".xls"].includes(ext)) {
|
||||
const script = `
|
||||
import json
|
||||
import pandas as pd
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
df = pd.read_excel(path)
|
||||
df = df.fillna("")
|
||||
header = [str(c) for c in df.columns.tolist()]
|
||||
total_rows = int(df.shape[0])
|
||||
if limit > 0:
|
||||
df = df.head(limit)
|
||||
rows = df.astype(str).values.tolist()
|
||||
print(json.dumps({"header": header, "rows": rows, "rows_total": total_rows}))
|
||||
`;
|
||||
const proc = spawnSync("python3", ["-c", script, safePath, String(limit || 0)], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (proc.status !== 0) {
|
||||
const err = proc.stderr || proc.stdout || "python3 failed to read excel";
|
||||
return { error: err.trim() };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(proc.stdout);
|
||||
return {
|
||||
path: path.relative(ROOT, safePath).replace(/\\/g, "/"),
|
||||
delimiter: "excel",
|
||||
header: parsed.header || [],
|
||||
rows: parsed.rows || [],
|
||||
rows_total: parsed.rows_total,
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: "Failed to parse Excel preview output." };
|
||||
}
|
||||
}
|
||||
if (![".csv", ".tsv", ".txt"].includes(ext)) {
|
||||
return { error: "Only .csv, .tsv, .txt, .xlsx, or .xls are supported for preview." };
|
||||
}
|
||||
const content = fs.readFileSync(safePath, "utf8");
|
||||
const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
||||
if (!lines.length) {
|
||||
return { error: "File is empty." };
|
||||
}
|
||||
const delimiter = ext === ".tsv" ? "\t" : ",";
|
||||
const header = lines[0].split(delimiter).map((v) => v.trim());
|
||||
const totalRows = lines.length - 1;
|
||||
const rowLimit = limit && limit > 0 ? limit : totalRows;
|
||||
const rows = lines
|
||||
.slice(1, rowLimit + 1)
|
||||
.map((line) => line.split(delimiter).map((v) => v.trim()));
|
||||
return {
|
||||
path: path.relative(ROOT, safePath).replace(/\\/g, "/"),
|
||||
delimiter,
|
||||
header,
|
||||
rows,
|
||||
rows_total: totalRows,
|
||||
};
|
||||
}
|
||||
|
||||
function runPython(script, args = []) {
|
||||
const proc = spawnSync("python3", ["-c", script, ...args], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, PYTHONWARNINGS: "ignore" },
|
||||
});
|
||||
if (proc.status !== 0) {
|
||||
const err = (proc.stderr || proc.stdout || "python3 failed").trim();
|
||||
return { error: err || "python3 failed" };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(proc.stdout);
|
||||
} catch (err) {
|
||||
return { error: "Failed to parse python output." };
|
||||
}
|
||||
}
|
||||
|
||||
function importPapila() {
|
||||
const script = `
|
||||
import json
|
||||
from classes.v2 import PapilaData
|
||||
|
||||
pd = PapilaData.from_dirs(
|
||||
image_dir="Papila/FundusImages",
|
||||
clinical_dir="Papila/ClinicalData",
|
||||
label_col="Diagnosis",
|
||||
cat_cols=["Gender", "Phakic/Pseudophakic"],
|
||||
)
|
||||
out = {
|
||||
"image_dir": str(pd.clinical.image_dir),
|
||||
"clinical_dir": str(pd.clinical.clinical_dir) if pd.clinical.clinical_dir else None,
|
||||
"label_col": pd.label_col,
|
||||
"df_rows": int(pd.df.shape[0]),
|
||||
"df_cols": int(pd.df.shape[1]),
|
||||
"df_columns": list(pd.df.columns),
|
||||
}
|
||||
print(json.dumps(out))
|
||||
`;
|
||||
return runPython(script);
|
||||
}
|
||||
|
||||
function papilaDfPreview(limit = 50) {
|
||||
const script = `
|
||||
import json
|
||||
from classes.v2 import PapilaData
|
||||
|
||||
pd = PapilaData.from_dirs(
|
||||
image_dir="Papila/FundusImages",
|
||||
clinical_dir="Papila/ClinicalData",
|
||||
label_col="Diagnosis",
|
||||
cat_cols=["Gender", "Phakic/Pseudophakic"],
|
||||
)
|
||||
df = pd.df.fillna("")
|
||||
header = [str(c) for c in df.columns.tolist()]
|
||||
limit = int(${Number(limit)}) if ${Number(limit)} else 0
|
||||
if limit > 0:
|
||||
df = df.head(limit)
|
||||
rows = df.astype(str).values.tolist()
|
||||
print(json.dumps({"header": header, "rows": rows, "rows_total": int(pd.df.shape[0])}))
|
||||
`;
|
||||
return runPython(script);
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const parsed = url.parse(req.url, true);
|
||||
if (parsed.pathname === "/api/fs" || parsed.pathname === "/api/fs/") {
|
||||
const requestedPath = parsed.query.path || "";
|
||||
const payload = listDir(requestedPath);
|
||||
if (payload.error) {
|
||||
sendJson(res, 400, payload);
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, payload);
|
||||
return;
|
||||
}
|
||||
if (parsed.pathname === "/api/import/papila" || parsed.pathname === "/api/import/papila/") {
|
||||
const payload = importPapila();
|
||||
if (payload.error) {
|
||||
sendJson(res, 400, payload);
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, payload);
|
||||
return;
|
||||
}
|
||||
if (parsed.pathname === "/api/papila/df" || parsed.pathname === "/api/papila/df/") {
|
||||
const rawLimit = parsed.query.limit;
|
||||
const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 50;
|
||||
const payload = papilaDfPreview(Number.isFinite(limit) ? limit : 50);
|
||||
if (payload.error) {
|
||||
sendJson(res, 400, payload);
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, payload);
|
||||
return;
|
||||
}
|
||||
if (parsed.pathname === "/api/file" || parsed.pathname === "/api/file/") {
|
||||
const requestedPath = parsed.query.path || "";
|
||||
const rawLimit = parsed.query.limit;
|
||||
const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 50;
|
||||
const payload = readTabularFile(requestedPath, Number.isFinite(limit) ? limit : 50);
|
||||
if (payload.error) {
|
||||
sendJson(res, 400, payload);
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
let filePath = parsed.pathname === "/" ? "index.html" : parsed.pathname.slice(1);
|
||||
filePath = path.join(WEBROOT, filePath);
|
||||
if (!filePath.startsWith(WEBROOT)) {
|
||||
res.writeHead(403);
|
||||
res.end("Forbidden");
|
||||
return;
|
||||
}
|
||||
sendFile(res, filePath);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 5173;
|
||||
server.listen(port, () => {
|
||||
console.log(`Hypertower UI running at http://localhost:${port}`);
|
||||
console.log(`Repo root: ${ROOT}`);
|
||||
});
|
||||
@@ -0,0 +1,363 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap");
|
||||
|
||||
:root {
|
||||
--bg: #f3efe9;
|
||||
--panel: #fff9f2;
|
||||
--panel-border: #e0d7cc;
|
||||
--ink: #1c1b1a;
|
||||
--muted: #5b5a57;
|
||||
--accent: #c84630;
|
||||
--accent-2: #2f5d62;
|
||||
--accent-3: #f6c453;
|
||||
--shadow: 0 18px 40px rgba(28, 27, 26, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
||||
color: var(--ink);
|
||||
background: radial-gradient(circle at top left, #f9f2e7 0%, #f0e8dd 45%, #e8e0d5 100%);
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 38% 62%;
|
||||
grid-template-rows: 60% 40%;
|
||||
gap: 18px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pane {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pane-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
background: linear-gradient(120deg, #fef7ec, #f7efe3);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pane-header h1,
|
||||
.pane-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.graph-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pane.left {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pane.right {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.pane.bottom {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.95rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button,
|
||||
.file-upload {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--panel-border);
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.file-upload:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 14px rgba(28, 27, 26, 0.12);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
button.ghost,
|
||||
.file-upload {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--panel-border);
|
||||
padding: 10px 12px;
|
||||
font-family: inherit;
|
||||
background: #fffefb;
|
||||
}
|
||||
|
||||
select {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--panel-border);
|
||||
padding: 10px 12px;
|
||||
font-family: inherit;
|
||||
background: #fffefb;
|
||||
}
|
||||
|
||||
.pill {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--panel-border);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#graph-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at 20% 20%, rgba(200, 70, 48, 0.08), transparent 55%),
|
||||
radial-gradient(circle at 80% 30%, rgba(47, 93, 98, 0.1), transparent 60%);
|
||||
}
|
||||
|
||||
.json-view {
|
||||
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
font-size: 0.82rem;
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
flex: 1;
|
||||
background: #11110f;
|
||||
color: #f3e9dc;
|
||||
}
|
||||
|
||||
.json-view.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.selection-block {
|
||||
padding: 10px 0 0;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 14, 13, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(820px, 90vw);
|
||||
background: #fffdf7;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--panel-border);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-footer {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid var(--panel-border);
|
||||
border-bottom: none;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 12px 18px 18px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.modal-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--panel-border);
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.modal-item.selected {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 6px 14px rgba(200, 70, 48, 0.18);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.breadcrumb span {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--panel-border);
|
||||
background: #fff;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.preview-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.preview-table th,
|
||||
.preview-table td {
|
||||
border: 1px solid var(--panel-border);
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preview-table th {
|
||||
background: #f1e7dc;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.app {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
.pane.left,
|
||||
.pane.right,
|
||||
.pane.bottom {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user