352 lines
11 KiB
JavaScript
352 lines
11 KiB
JavaScript
const state = {
|
|
manifest: null,
|
|
graph: null,
|
|
trace: null,
|
|
report: null,
|
|
filter: "all",
|
|
};
|
|
|
|
const ids = {
|
|
manifestUrl: document.getElementById("manifest-url"),
|
|
manifestUrlLoad: document.getElementById("manifest-url-load"),
|
|
manifestFile: document.getElementById("manifest-file"),
|
|
graphFile: document.getElementById("graph-file"),
|
|
traceFile: document.getElementById("trace-file"),
|
|
reportFile: document.getElementById("report-file"),
|
|
summaryStatus: document.getElementById("summary-status"),
|
|
graphStatus: document.getElementById("graph-status"),
|
|
traceStatus: document.getElementById("trace-status"),
|
|
stats: document.getElementById("stats"),
|
|
analysisMetrics: document.getElementById("analysis-metrics"),
|
|
nodeChips: document.getElementById("node-chips"),
|
|
edgeList: document.getElementById("edge-list"),
|
|
traceList: document.getElementById("trace-list"),
|
|
traceTemplate: document.getElementById("trace-card-template"),
|
|
filterButtons: Array.from(document.querySelectorAll(".filter")),
|
|
};
|
|
|
|
ids.manifestUrlLoad.addEventListener("click", () => loadManifestFromUrl());
|
|
ids.manifestUrl.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
loadManifestFromUrl();
|
|
}
|
|
});
|
|
ids.manifestFile.addEventListener("change", (event) => loadJsonFile(event, "manifest"));
|
|
ids.graphFile.addEventListener("change", (event) => loadJsonFile(event, "graph"));
|
|
ids.traceFile.addEventListener("change", (event) => loadJsonFile(event, "trace"));
|
|
ids.reportFile.addEventListener("change", (event) => loadJsonFile(event, "report"));
|
|
|
|
for (const button of ids.filterButtons) {
|
|
button.addEventListener("click", () => {
|
|
state.filter = button.dataset.filter ?? "all";
|
|
syncFilterButtons();
|
|
renderTrace();
|
|
});
|
|
}
|
|
|
|
syncFilterButtons();
|
|
renderSummary();
|
|
renderGraph();
|
|
renderTrace();
|
|
|
|
async function loadJsonFile(event, kind) {
|
|
const input = event.currentTarget;
|
|
const file = input.files?.[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const text = await file.text();
|
|
const parsed = JSON.parse(text);
|
|
if (kind === "manifest") {
|
|
validateManifest(parsed);
|
|
state.manifest = parsed;
|
|
ids.summaryStatus.textContent =
|
|
`Manifest loaded: schema ${parsed.schema_version}. ` +
|
|
"For full auto-load, use the manifest URL field while serving the artifacts over HTTP.";
|
|
return;
|
|
}
|
|
const envelope = validateEnvelope(parsed, expectedArtifactType(kind));
|
|
state[kind] = envelope.payload;
|
|
if (kind === "graph") {
|
|
renderGraph();
|
|
} else if (kind === "trace") {
|
|
renderTrace();
|
|
} else if (kind === "report") {
|
|
renderSummary();
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
window.alert(`Failed to load ${kind} JSON: ${message}`);
|
|
} finally {
|
|
input.value = "";
|
|
}
|
|
}
|
|
|
|
async function loadManifestFromUrl() {
|
|
const manifestUrl = ids.manifestUrl.value.trim();
|
|
if (!manifestUrl) {
|
|
window.alert("Enter a manifest URL first.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const manifestResponse = await fetch(manifestUrl);
|
|
if (!manifestResponse.ok) {
|
|
throw new Error(`Manifest request failed with status ${manifestResponse.status}`);
|
|
}
|
|
const manifest = await manifestResponse.json();
|
|
validateManifest(manifest);
|
|
state.manifest = manifest;
|
|
|
|
const baseUrl = new URL(manifestUrl, window.location.href);
|
|
const parent = new URL("./", baseUrl);
|
|
const artifactMap = Object.fromEntries(
|
|
manifest.artifacts.map((artifact) => [artifact.artifact_type, artifact.file_name])
|
|
);
|
|
|
|
const graph = await fetchArtifactJson(parent, artifactMap.graph_schema, "graph_schema");
|
|
const trace = await fetchArtifactJson(
|
|
parent,
|
|
artifactMap.execution_trace,
|
|
"execution_trace"
|
|
);
|
|
const report = await fetchArtifactJson(parent, artifactMap.run_report, "run_report");
|
|
|
|
state.graph = graph.payload;
|
|
state.trace = trace.payload;
|
|
state.report = report.payload;
|
|
|
|
ids.summaryStatus.textContent = `Loaded manifest and artifacts from ${baseUrl.origin}.`;
|
|
renderGraph();
|
|
renderTrace();
|
|
renderSummary();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
window.alert(`Failed to load manifest URL: ${message}`);
|
|
}
|
|
}
|
|
|
|
async function fetchArtifactJson(baseUrl, fileName, expectedType) {
|
|
if (!fileName) {
|
|
throw new Error(`Manifest does not include ${expectedType}`);
|
|
}
|
|
const artifactUrl = new URL(fileName, baseUrl);
|
|
const response = await fetch(artifactUrl);
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`${expectedType} request failed with status ${response.status}`
|
|
);
|
|
}
|
|
const json = await response.json();
|
|
return validateEnvelope(json, expectedType);
|
|
}
|
|
|
|
function expectedArtifactType(kind) {
|
|
if (kind === "graph") return "graph_schema";
|
|
if (kind === "trace") return "execution_trace";
|
|
if (kind === "report") return "run_report";
|
|
return kind;
|
|
}
|
|
|
|
function validateEnvelope(value, expectedType) {
|
|
if (
|
|
!value ||
|
|
typeof value !== "object" ||
|
|
typeof value.artifact_type !== "string" ||
|
|
typeof value.schema_version !== "string" ||
|
|
!("payload" in value) ||
|
|
typeof value.metadata !== "object"
|
|
) {
|
|
throw new Error("Invalid artifact envelope");
|
|
}
|
|
if (value.artifact_type !== expectedType) {
|
|
throw new Error(
|
|
`Unexpected artifact type: expected ${expectedType}, got ${value.artifact_type}`
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateManifest(value) {
|
|
if (
|
|
!value ||
|
|
typeof value !== "object" ||
|
|
typeof value.schema_version !== "string" ||
|
|
!Array.isArray(value.artifacts)
|
|
) {
|
|
throw new Error("Invalid artifact manifest");
|
|
}
|
|
}
|
|
|
|
function syncFilterButtons() {
|
|
for (const button of ids.filterButtons) {
|
|
const active = button.dataset.filter === state.filter;
|
|
button.classList.toggle("is-active", active);
|
|
}
|
|
}
|
|
|
|
function renderSummary() {
|
|
ids.stats.replaceChildren();
|
|
ids.analysisMetrics.replaceChildren();
|
|
|
|
if (!state.report) {
|
|
ids.summaryStatus.textContent = "Load a report to populate metrics.";
|
|
return;
|
|
}
|
|
|
|
ids.summaryStatus.textContent = "Report loaded.";
|
|
appendStat("Accepted", state.report.accepted_count, ids.stats);
|
|
appendStat("Attempts", state.report.attempt_count, ids.stats);
|
|
appendStat(
|
|
"Avg / Accept",
|
|
formatNumber(state.report.average_attempts_per_accept),
|
|
ids.stats
|
|
);
|
|
appendStat("Seconds", formatNumber(state.report.total_seconds), ids.stats);
|
|
|
|
const analysis = state.report.sequence_analysis ?? {};
|
|
if (Object.keys(analysis).length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty-note";
|
|
empty.textContent = "No sequence analysis in this report.";
|
|
ids.analysisMetrics.append(empty);
|
|
return;
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(analysis)) {
|
|
appendMetric(key, value, ids.analysisMetrics);
|
|
}
|
|
}
|
|
|
|
function renderGraph() {
|
|
ids.nodeChips.replaceChildren();
|
|
ids.edgeList.replaceChildren();
|
|
|
|
if (!state.graph) {
|
|
ids.graphStatus.textContent = "Load a graph schema to inspect node wiring.";
|
|
return;
|
|
}
|
|
|
|
ids.graphStatus.textContent = `${state.graph.nodes.length} nodes, ${state.graph.edges.length} edges.`;
|
|
|
|
for (const node of state.graph.nodes) {
|
|
const chip = document.createElement("article");
|
|
chip.className = "chip";
|
|
chip.innerHTML = `
|
|
<p class="chip-type">${escapeHtml(node.node_type)}</p>
|
|
<h3>${escapeHtml(node.node_id)}</h3>
|
|
<p>${escapeHtml(node.input_names.join(", ") || "no inputs")} -> ${escapeHtml(
|
|
node.output_names.join(", ") || "no outputs"
|
|
)}</p>
|
|
`;
|
|
ids.nodeChips.append(chip);
|
|
}
|
|
|
|
for (const edge of state.graph.edges) {
|
|
const item = document.createElement("li");
|
|
item.innerHTML = `<code>${escapeHtml(edge.source_node_id)}.${escapeHtml(
|
|
edge.source_output
|
|
)}</code> -> <code>${escapeHtml(edge.target_node_id)}.${escapeHtml(
|
|
edge.target_input
|
|
)}</code>`;
|
|
ids.edgeList.append(item);
|
|
}
|
|
}
|
|
|
|
function renderTrace() {
|
|
ids.traceList.replaceChildren();
|
|
|
|
if (!state.trace) {
|
|
ids.traceStatus.textContent =
|
|
"Load a trace to inspect candidate-by-candidate behavior.";
|
|
return;
|
|
}
|
|
|
|
const attempts = state.trace.attempts ?? [];
|
|
const filtered = attempts.filter((attempt) => {
|
|
if (state.filter === "accepted") {
|
|
return attempt.accepted;
|
|
}
|
|
if (state.filter === "rejected") {
|
|
return !attempt.accepted;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
ids.traceStatus.textContent = `${filtered.length} shown of ${attempts.length} attempts.`;
|
|
|
|
filtered.forEach((trace, index) => {
|
|
const fragment = ids.traceTemplate.content.cloneNode(true);
|
|
const card = fragment.querySelector(".trace-card");
|
|
const traceLabel = fragment.querySelector(".trace-label");
|
|
const traceTitle = fragment.querySelector(".trace-title");
|
|
const badge = fragment.querySelector(".badge");
|
|
const metrics = fragment.querySelector(".metric-list");
|
|
const metadata = fragment.querySelector(".json-block");
|
|
const transition = fragment.querySelector(".transition-block");
|
|
|
|
card.dataset.accepted = String(trace.accepted);
|
|
traceLabel.textContent = `Attempt ${index + 1}`;
|
|
traceTitle.textContent = `Candidate ${JSON.stringify(trace.candidate)}`;
|
|
badge.textContent = trace.accepted ? "Accepted" : "Rejected";
|
|
badge.classList.toggle("accepted", trace.accepted);
|
|
badge.classList.toggle("rejected", !trace.accepted);
|
|
|
|
appendMetric("elapsed_seconds", trace.elapsed_seconds, metrics);
|
|
|
|
const score =
|
|
trace.metadata?.critique?.outputs?.[0] ?? trace.metadata?.decision?.accepted;
|
|
if (score !== undefined) {
|
|
appendMetric("signal", score, metrics);
|
|
}
|
|
|
|
metadata.textContent = JSON.stringify(trace.metadata, null, 2);
|
|
transition.textContent = JSON.stringify(
|
|
{
|
|
previous_state: trace.previous_state,
|
|
next_state: trace.next_state,
|
|
},
|
|
null,
|
|
2
|
|
);
|
|
|
|
ids.traceList.append(fragment);
|
|
});
|
|
}
|
|
|
|
function appendStat(label, value, container) {
|
|
const article = document.createElement("article");
|
|
article.className = "stat-card";
|
|
article.innerHTML = `<p>${escapeHtml(label)}</p><h3>${escapeHtml(String(value))}</h3>`;
|
|
container.append(article);
|
|
}
|
|
|
|
function appendMetric(label, value, container) {
|
|
const dt = document.createElement("dt");
|
|
dt.textContent = label;
|
|
const dd = document.createElement("dd");
|
|
dd.textContent =
|
|
typeof value === "number" ? formatNumber(value) : JSON.stringify(value);
|
|
container.append(dt, dd);
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
if (typeof value !== "number") {
|
|
return String(value);
|
|
}
|
|
if (Number.isInteger(value)) {
|
|
return String(value);
|
|
}
|
|
return value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|