branchjam / src /render.js
abhid1234's picture
Upload folder using huggingface_hub
2fe29a7 verified
Raw
History Blame Contribute Delete
11.2 kB
import { constraintsFor, getArtifact, getConstraint } from "./catalog.js";
import { historyPreorder, MAX_VOTES_PER_OUTCOME } from "./domain.js";
const button = (label, className, action, dataset = {}) => {
const element = document.createElement("button");
element.type = "button";
element.className = className;
element.textContent = label;
Object.assign(element.dataset, dataset);
element.addEventListener("click", action);
return element;
};
const setFocusKey = (element, key) => {
element.dataset.focusKey = key;
return element;
};
export function voteCountLabel(count) {
return `${count} ${count === 1 ? "vote" : "votes"}`;
}
export function historyNodeMetadata(node, artifactTitle, parentTitle, isLineage, selected) {
const status = node.status === "not-selected"
? "Non-selected sibling"
: node.status[0].toUpperCase() + node.status.slice(1);
const labels = [
node.depth === 0 ? "Seed origin" : `Round ${node.depth}`,
node.depth === 0 ? null : status,
isLineage ? "Active lineage" : null,
selected ? "Currently inspected" : null
].filter(Boolean);
const relationship = node.depth === 0 ? "no parent" : `parent ${parentTitle}`;
return {
accessibleName: `${artifactTitle}, ${labels[0]}, ${relationship}${labels.slice(1).map((label) => `, ${label.toLowerCase()}`).join("")}`,
visibleText: labels.join(" · ")
};
}
export function renderRound(container, state, source, actions) {
container.replaceChildren();
const phase = document.createElement("p");
phase.className = "phase-status";
phase.textContent = state.phase === "choose"
? "Current phase: choose a constraint. Next: reveal two fixed outcomes."
: state.phase === "compare"
? "Current phase: compare both outcomes. Next: record at least one vote."
: state.phase === "vote"
? "Current phase: operator records participant preferences. Next: resolve the vote."
: state.phase === "resolved"
? "Current phase: decision recorded. Next: continue from the winner."
: "Current phase: branch complete. Next: explore any runnable history node.";
container.append(phase);
const kicker = document.createElement("p");
kicker.className = "section-kicker";
kicker.textContent = state.phase === "choose" ? "Shape the next fork" : `Round ${Object.keys(state.roundsById).length}`;
container.append(kicker);
if (state.phase === "choose") {
const heading = document.createElement("h2");
heading.id = "round-heading";
heading.tabIndex = -1;
heading.textContent = state.activeNodeId === state.seedNodeId ? "Choose a constraint" : "Continue the winning branch";
container.append(heading);
const intro = document.createElement("p");
intro.textContent = "Select one curated rule, then reveal its two fixed interpretations.";
container.append(intro);
const group = document.createElement("fieldset");
const legend = document.createElement("legend");
legend.textContent = "Available constraints";
group.append(legend);
for (const constraint of constraintsFor(source, state.nodesById[state.activeNodeId].artifactId)) {
const label = document.createElement("label");
label.className = "constraint-card";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "constraint";
radio.value = constraint.id;
setFocusKey(radio, `constraint-${constraint.id}`);
radio.checked = state.selectedConstraintId === constraint.id;
radio.addEventListener("change", () => actions.selectConstraint(constraint.id));
const copy = document.createElement("span");
const name = document.createElement("strong");
name.textContent = constraint.title;
const detail = document.createElement("small");
detail.textContent = constraint.description;
copy.append(name, detail);
label.append(radio, copy);
group.append(label);
}
container.append(group);
const reveal = button("Reveal outcomes", "primary-button", actions.reveal);
setFocusKey(reveal, "reveal-outcomes");
reveal.disabled = !state.selectedConstraintId;
container.append(reveal);
return;
}
if (state.phase === "complete") {
const heading = document.createElement("h2");
heading.id = "round-heading";
heading.tabIndex = -1;
heading.textContent = "Branch complete";
container.append(heading);
const end = document.createElement("p");
end.className = "end-card";
end.textContent = "This pre-authored branch ends here in the local MVP.";
container.append(end);
return;
}
const round = state.roundsById[state.currentRoundId];
const recipe = source.recipes.find((item) => item.id === round.recipeId);
const constraint = getConstraint(source, recipe.constraintId);
const heading = document.createElement("h2");
heading.id = state.phase === "resolved" ? "result-heading" : "comparison-heading";
heading.tabIndex = -1;
heading.textContent = state.phase === "resolved" ? "The branch is decided" : `Compare: ${constraint.title}`;
container.append(heading);
const local = document.createElement("p");
local.className = "context-disclosure";
local.textContent = "These two outcomes were selected from a pre-authored local catalog for this artifact and constraint. The same choices always produce the same results.";
container.append(local);
const cards = document.createElement("div");
cards.className = "outcome-grid";
recipe.outcomes.forEach((outcome) => {
const artifact = getArtifact(source, outcome.artifactId);
const card = document.createElement("article");
const selected = state.compareSelection === outcome.slot;
card.className = `outcome-card branch-${outcome.slot}${selected ? " is-selected" : ""}`;
const title = document.createElement("h3");
title.textContent = `${outcome.slot.toUpperCase()}${artifact.title}`;
const summary = document.createElement("p");
summary.textContent = artifact.summary;
const divergence = document.createElement("p");
divergence.className = "divergence-note";
const divergenceLabel = document.createElement("strong");
divergenceLabel.textContent = "Meaningful split: ";
divergence.append(divergenceLabel);
divergence.append(outcome.divergenceNote);
const play = button(selected ? "Playing this outcome" : "Play this outcome", "compare-button", () => actions.compare(outcome.slot));
setFocusKey(play, `compare-${outcome.slot}`);
play.setAttribute("aria-pressed", String(selected));
if (selected) {
play.disabled = true;
}
card.append(title, summary, divergence, play);
cards.append(card);
});
container.append(cards);
if (state.phase === "resolved") {
const winnerArtifact = getArtifact(source, state.nodesById[state.activeNodeId].artifactId);
const result = document.createElement("p");
result.className = "result-card";
result.textContent = `${round.winnerSlot.toUpperCase()} wins: ${winnerArtifact.title}. ${round.resolution === "authored-tie-rule" ? "The displayed authored tie rule decided this round." : "The higher recorded vote total decided this round."}`;
const continueButton = button("Continue from winner", "primary-button", actions.continue);
setFocusKey(continueButton, "continue-winner");
container.append(result, continueButton);
return;
}
const voting = document.createElement("section");
voting.className = "voting";
voting.setAttribute("aria-labelledby", "voting-heading");
const voteHeading = document.createElement("h3");
voteHeading.id = "voting-heading";
voteHeading.textContent = "Operator-recorded voting";
const disclosure = document.createElement("p");
disclosure.className = "vote-disclosure";
disclosure.textContent = "Votes are entered by the local operator in this browser on this device. Participant devices are not connected.";
const tie = document.createElement("p");
tie.textContent = `Tie rule, shown before resolution: outcome ${recipe.tieWinnerSlot.toUpperCase()} wins an equal total.`;
voting.append(voteHeading, disclosure, tie);
for (const slot of ["a", "b"]) {
const row = document.createElement("div");
row.className = "vote-row";
const name = document.createElement("strong");
name.textContent = `Outcome ${slot.toUpperCase()}: ${voteCountLabel(round.votes[slot])}`;
const subtract = button("−", "vote-button", () => {
if (round.votes[slot] > 0) actions.vote(slot, -1);
});
setFocusKey(subtract, `vote-${slot}-subtract`);
subtract.setAttribute("aria-label", `Remove one vote from outcome ${slot.toUpperCase()}`);
subtract.disabled = round.votes[slot] === 0;
const add = button("+", "vote-button", () => actions.vote(slot, 1));
setFocusKey(add, `vote-${slot}-add`);
add.setAttribute("aria-label", `Add one vote to outcome ${slot.toUpperCase()}`);
add.disabled = round.votes[slot] >= MAX_VOTES_PER_OUTCOME;
row.append(name, subtract, add);
voting.append(row);
}
const resolve = button("Resolve vote", "primary-button", actions.resolve);
setFocusKey(resolve, "resolve-vote");
resolve.disabled = state.phase !== "vote";
voting.append(resolve);
container.append(voting);
}
export function renderHistory(container, state, source, inspect) {
const nodes = historyPreorder(state);
const activeLineage = new Set();
for (let id = state.activeNodeId; id; id = state.nodesById[id].parentNodeId) activeLineage.add(id);
const children = new Map();
for (const node of nodes) {
const key = node.parentNodeId ?? "root";
children.set(key, [...(children.get(key) ?? []), node]);
}
const build = (parent) => {
const list = document.createElement("ul");
for (const node of children.get(parent) ?? []) {
const item = document.createElement("li");
const selected = state.inspectedNodeId === node.nodeId;
item.className = `history-node status-${node.status}${activeLineage.has(node.nodeId) ? " is-lineage" : ""}${selected ? " is-inspected" : ""}`;
const artifact = getArtifact(source, node.artifactId);
const parentTitle = node.parentNodeId ? getArtifact(source, state.nodesById[node.parentNodeId].artifactId).title : "none";
const control = button(artifact.title, "history-button", () => inspect(node.nodeId));
setFocusKey(control, `history-${node.nodeId}`);
const metadata = historyNodeMetadata(node, artifact.title, parentTitle, activeLineage.has(node.nodeId), selected);
control.setAttribute("aria-label", metadata.accessibleName);
if (selected) control.setAttribute("aria-current", "true");
control.setAttribute("aria-pressed", String(selected));
const row = document.createElement("div");
row.className = "history-node-row";
const meta = document.createElement("span");
meta.className = "node-meta";
meta.textContent = metadata.visibleText;
row.append(control, meta);
item.append(row);
if (children.has(node.nodeId)) item.append(build(node.nodeId));
list.append(item);
}
return list;
};
container.replaceChildren(build("root"));
}