moved_repo_first_update

This commit is contained in:
rpotter6298
2026-02-24 10:39:48 +01:00
commit 9894a23f09
98 changed files with 35387 additions and 0 deletions
+1973
View File
File diff suppressed because it is too large Load Diff
+481
View File
@@ -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=">">&gt;</option>
<option value=">=">&gt;=</option>
<option value="=">=</option>
<option value="!=">!=</option>
<option value="<=">&lt;=</option>
<option value="<">&lt;</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
View File
@@ -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}`);
});
+363
View File
@@ -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;
}
}