This commit is contained in:
@@ -54,6 +54,17 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="graphFullToggle" name="full_graph" />
|
||||
Attempt entire reference graph
|
||||
</label>
|
||||
<p class="field-hint">
|
||||
Includes every video that references another (ignores depth; may be slow). Max nodes still
|
||||
applies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="graphLabelSize">Labels</label>
|
||||
<select id="graphLabelSize" name="label_size">
|
||||
|
||||
134
static/graph.js
134
static/graph.js
@@ -7,6 +7,7 @@
|
||||
const depthInput = document.getElementById("graphDepth");
|
||||
const maxNodesInput = document.getElementById("graphMaxNodes");
|
||||
const labelSizeInput = document.getElementById("graphLabelSize");
|
||||
const fullGraphToggle = document.getElementById("graphFullToggle");
|
||||
const statusEl = document.getElementById("graphStatus");
|
||||
const container = document.getElementById("graphContainer");
|
||||
const isEmbedded =
|
||||
@@ -133,6 +134,7 @@
|
||||
let currentDepth = sanitizeDepth(depthInput.value);
|
||||
let currentMaxNodes = sanitizeMaxNodes(maxNodesInput.value);
|
||||
let currentSimulation = null;
|
||||
let currentFullGraph = false;
|
||||
|
||||
function setStatus(message, isError = false) {
|
||||
if (!statusEl) return;
|
||||
@@ -148,10 +150,40 @@
|
||||
return (value || "").trim();
|
||||
}
|
||||
|
||||
async function fetchGraph(videoId, depth, maxNodes) {
|
||||
function isFullGraphMode(forceValue) {
|
||||
if (typeof forceValue === "boolean") {
|
||||
return forceValue;
|
||||
}
|
||||
return fullGraphToggle ? !!fullGraphToggle.checked : false;
|
||||
}
|
||||
|
||||
function applyFullGraphState(forceValue) {
|
||||
const enabled = isFullGraphMode(forceValue);
|
||||
if (typeof forceValue === "boolean" && fullGraphToggle) {
|
||||
fullGraphToggle.checked = forceValue;
|
||||
}
|
||||
if (depthInput) {
|
||||
depthInput.disabled = enabled;
|
||||
}
|
||||
if (videoInput) {
|
||||
if (enabled) {
|
||||
videoInput.removeAttribute("required");
|
||||
} else {
|
||||
videoInput.setAttribute("required", "required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGraph(videoId, depth, maxNodes, fullGraphMode = false) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("video_id", videoId);
|
||||
params.set("depth", String(depth));
|
||||
if (videoId) {
|
||||
params.set("video_id", videoId);
|
||||
}
|
||||
if (fullGraphMode) {
|
||||
params.set("full_graph", "1");
|
||||
} else {
|
||||
params.set("depth", String(depth));
|
||||
}
|
||||
params.set("max_nodes", String(maxNodes));
|
||||
const response = await fetch(`/api/graph?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
@@ -399,24 +431,40 @@
|
||||
currentSimulation = simulation;
|
||||
}
|
||||
|
||||
async function loadGraph(videoId, depth, maxNodes, { updateInputs = false } = {}) {
|
||||
async function loadGraph(
|
||||
videoId,
|
||||
depth,
|
||||
maxNodes,
|
||||
{ updateInputs = false, fullGraph } = {}
|
||||
) {
|
||||
const wantsFull = isFullGraphMode(
|
||||
typeof fullGraph === "boolean" ? fullGraph : undefined
|
||||
);
|
||||
const sanitizedId = sanitizeId(videoId);
|
||||
if (!sanitizedId) {
|
||||
if (!wantsFull && !sanitizedId) {
|
||||
setStatus("Please enter a video ID.", true);
|
||||
return;
|
||||
}
|
||||
const safeDepth = sanitizeDepth(depth);
|
||||
const safeDepth = wantsFull ? 0 : sanitizeDepth(depth);
|
||||
const safeMaxNodes = sanitizeMaxNodes(maxNodes);
|
||||
|
||||
if (updateInputs) {
|
||||
videoInput.value = sanitizedId;
|
||||
depthInput.value = String(safeDepth);
|
||||
depthInput.value = String(wantsFull ? currentDepth || 1 : safeDepth);
|
||||
maxNodesInput.value = String(safeMaxNodes);
|
||||
applyFullGraphState(wantsFull);
|
||||
} else {
|
||||
applyFullGraphState();
|
||||
}
|
||||
|
||||
setStatus("Loading graph…");
|
||||
setStatus(wantsFull ? "Loading full reference graph…" : "Loading graph…");
|
||||
try {
|
||||
const data = await fetchGraph(sanitizedId, safeDepth, safeMaxNodes);
|
||||
const data = await fetchGraph(
|
||||
sanitizedId,
|
||||
safeDepth,
|
||||
safeMaxNodes,
|
||||
wantsFull
|
||||
);
|
||||
if (!data.nodes || data.nodes.length === 0) {
|
||||
setStatus("No nodes returned for this video.", true);
|
||||
container.innerHTML = "";
|
||||
@@ -428,12 +476,21 @@
|
||||
currentGraphData = data;
|
||||
currentDepth = safeDepth;
|
||||
currentMaxNodes = safeMaxNodes;
|
||||
currentFullGraph = wantsFull;
|
||||
renderGraph(data, getLabelSize());
|
||||
renderLegend(data.nodes);
|
||||
setStatus(
|
||||
`Showing ${data.nodes.length} nodes and ${data.links.length} links (depth ${data.depth})`
|
||||
`Showing ${data.nodes.length} nodes and ${data.links.length} links (${
|
||||
data.meta?.mode === "full" ? "full graph" : `depth ${data.depth}`
|
||||
})`
|
||||
);
|
||||
updateUrlState(
|
||||
sanitizedId,
|
||||
safeDepth,
|
||||
safeMaxNodes,
|
||||
getLabelSize(),
|
||||
wantsFull
|
||||
);
|
||||
updateUrlState(sanitizedId, safeDepth, safeMaxNodes, getLabelSize());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setStatus(err.message || "Failed to build graph.", true);
|
||||
@@ -448,6 +505,7 @@
|
||||
event.preventDefault();
|
||||
await loadGraph(videoInput.value, depthInput.value, maxNodesInput.value, {
|
||||
updateInputs: true,
|
||||
fullGraph: isFullGraphMode(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -559,13 +617,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrlState(videoId, depth, maxNodes, labelSize) {
|
||||
function updateUrlState(videoId, depth, maxNodes, labelSize, fullGraphMode) {
|
||||
if (isEmbedded) {
|
||||
return;
|
||||
}
|
||||
const next = new URL(window.location.href);
|
||||
next.searchParams.set("video_id", videoId);
|
||||
next.searchParams.set("depth", String(depth));
|
||||
if (videoId) {
|
||||
next.searchParams.set("video_id", videoId);
|
||||
} else {
|
||||
next.searchParams.delete("video_id");
|
||||
}
|
||||
if (fullGraphMode) {
|
||||
next.searchParams.set("full_graph", "1");
|
||||
next.searchParams.delete("depth");
|
||||
} else {
|
||||
next.searchParams.set("depth", String(depth));
|
||||
next.searchParams.delete("full_graph");
|
||||
}
|
||||
next.searchParams.set("max_nodes", String(maxNodes));
|
||||
if (labelSize && labelSize !== "normal") {
|
||||
next.searchParams.set("label_size", labelSize);
|
||||
@@ -581,25 +649,40 @@
|
||||
const depth = sanitizeDepth(params.get("depth") || "");
|
||||
const maxNodes = sanitizeMaxNodes(params.get("max_nodes") || "");
|
||||
const labelSizeParam = params.get("label_size");
|
||||
const fullGraphParam = params.get("full_graph");
|
||||
const viewFull =
|
||||
fullGraphParam && ["1", "true", "yes"].includes(fullGraphParam.toLowerCase());
|
||||
if (videoId) {
|
||||
videoInput.value = videoId;
|
||||
}
|
||||
depthInput.value = String(depth);
|
||||
maxNodesInput.value = String(maxNodes);
|
||||
if (fullGraphToggle) {
|
||||
fullGraphToggle.checked = !!viewFull;
|
||||
}
|
||||
applyFullGraphState();
|
||||
if (labelSizeParam && isValidLabelSize(labelSizeParam)) {
|
||||
setLabelSizeInput(labelSizeParam);
|
||||
} else {
|
||||
setLabelSizeInput(getLabelSize());
|
||||
}
|
||||
if (!videoId || isEmbedded) {
|
||||
if ((isEmbedded && !viewFull) || (!videoId && !viewFull)) {
|
||||
return;
|
||||
}
|
||||
loadGraph(videoId, depth, maxNodes, { updateInputs: false });
|
||||
loadGraph(videoId, depth, maxNodes, {
|
||||
updateInputs: false,
|
||||
fullGraph: viewFull,
|
||||
});
|
||||
}
|
||||
|
||||
resizeContainer();
|
||||
window.addEventListener("resize", resizeContainer);
|
||||
form.addEventListener("submit", handleSubmit);
|
||||
if (fullGraphToggle) {
|
||||
fullGraphToggle.addEventListener("change", () => {
|
||||
applyFullGraphState();
|
||||
});
|
||||
}
|
||||
labelSizeInput.addEventListener("change", () => {
|
||||
const size = getLabelSize();
|
||||
if (currentGraphData) {
|
||||
@@ -610,7 +693,8 @@
|
||||
sanitizeId(videoInput.value),
|
||||
currentDepth,
|
||||
currentMaxNodes,
|
||||
size
|
||||
size,
|
||||
currentFullGraph
|
||||
);
|
||||
});
|
||||
initFromQuery();
|
||||
@@ -619,8 +703,23 @@
|
||||
load(videoId, depth, maxNodes, options = {}) {
|
||||
const targetDepth = depth != null ? depth : currentDepth;
|
||||
const targetMax = maxNodes != null ? maxNodes : currentMaxNodes;
|
||||
const explicitFull =
|
||||
typeof options.fullGraph === "boolean"
|
||||
? options.fullGraph
|
||||
: undefined;
|
||||
if (fullGraphToggle && typeof explicitFull === "boolean") {
|
||||
fullGraphToggle.checked = explicitFull;
|
||||
}
|
||||
applyFullGraphState(
|
||||
typeof explicitFull === "boolean" ? explicitFull : undefined
|
||||
);
|
||||
const fullFlag =
|
||||
typeof explicitFull === "boolean"
|
||||
? explicitFull
|
||||
: isFullGraphMode();
|
||||
return loadGraph(videoId, targetDepth, targetMax, {
|
||||
updateInputs: options.updateInputs !== false,
|
||||
fullGraph: fullFlag,
|
||||
});
|
||||
},
|
||||
setLabelSize(size) {
|
||||
@@ -659,6 +758,7 @@
|
||||
labelSize: getLabelSize(),
|
||||
nodes: currentGraphData ? currentGraphData.nodes.slice() : [],
|
||||
links: currentGraphData ? currentGraphData.links.slice() : [],
|
||||
fullGraph: currentFullGraph,
|
||||
};
|
||||
},
|
||||
isEmbedded,
|
||||
|
||||
@@ -196,6 +196,13 @@ body.dimmed {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.graph-controls .field-hint {
|
||||
font-size: 10px;
|
||||
color: #3c3c3c;
|
||||
margin: 0;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.graph-controls input,
|
||||
.graph-controls select {
|
||||
min-width: 160px;
|
||||
|
||||
Reference in New Issue
Block a user