701 lines
22 KiB
HTML
701 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GooseFactory — Decision Tree</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #111;
|
|
--card: #1a1a2e;
|
|
--text: #e0e0e0;
|
|
--muted: #777;
|
|
--border: #2a2a3e;
|
|
--green: #00CC55;
|
|
--red: #FF4444;
|
|
--amber: #FFaa00;
|
|
--blue: #4488FF;
|
|
--radius: 14px;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.tree-header {
|
|
text-align: center;
|
|
padding: 20px 16px 12px;
|
|
}
|
|
.tree-header .label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
color: var(--muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
.tree-header h2 {
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
}
|
|
.tree-header .item-name {
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* ── Tree Path Visualization ── */
|
|
.path-visual {
|
|
padding: 8px 20px;
|
|
min-height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
.path-node {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
transition: all 0.3s;
|
|
}
|
|
.path-node.active { border-color: var(--blue); color: var(--blue); }
|
|
.path-node.yes { border-color: var(--green); color: var(--green); }
|
|
.path-node.no { border-color: var(--red); color: var(--red); }
|
|
.path-arrow {
|
|
color: #444;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ── Tree Body ── */
|
|
.tree-body {
|
|
flex: 1;
|
|
padding: 8px 16px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
}
|
|
|
|
/* ── Question Node ── */
|
|
.question-node {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
margin-bottom: 12px;
|
|
}
|
|
.question-node.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
.question-node.answered {
|
|
opacity: 0.4;
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
/* Connector line */
|
|
.connector {
|
|
width: 2px;
|
|
height: 20px;
|
|
background: linear-gradient(to bottom, var(--border), transparent);
|
|
margin: 0 auto;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.connector.visible { opacity: 1; background: linear-gradient(to bottom, var(--blue), var(--border)); }
|
|
|
|
.q-card {
|
|
background: var(--card);
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.q-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--blue), var(--green));
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.question-node.current .q-card::before { opacity: 1; }
|
|
.question-node.current .q-card { border-color: rgba(68,136,255,0.3); }
|
|
|
|
.q-depth {
|
|
font-size: 9px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.5px;
|
|
color: var(--muted);
|
|
margin-bottom: 6px;
|
|
}
|
|
.q-text {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Answer buttons */
|
|
.q-answers {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
.answer-btn {
|
|
flex: 1;
|
|
padding: 14px 12px;
|
|
border-radius: 12px;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
border: 2px solid;
|
|
background: transparent;
|
|
transition: all 0.2s;
|
|
min-height: 48px;
|
|
-webkit-tap-highlight-color: transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
.answer-btn:active { transform: scale(0.96); }
|
|
|
|
.answer-btn.yes-btn {
|
|
border-color: rgba(0,204,85,0.3);
|
|
color: var(--green);
|
|
}
|
|
.answer-btn.yes-btn:hover { background: rgba(0,204,85,0.08); border-color: var(--green); }
|
|
|
|
.answer-btn.no-btn {
|
|
border-color: rgba(255,68,68,0.3);
|
|
color: var(--red);
|
|
}
|
|
.answer-btn.no-btn:hover { background: rgba(255,68,68,0.08); border-color: var(--red); }
|
|
|
|
.answer-btn.option-btn {
|
|
border-color: rgba(68,136,255,0.3);
|
|
color: var(--blue);
|
|
}
|
|
.answer-btn.option-btn:hover { background: rgba(68,136,255,0.08); border-color: var(--blue); }
|
|
|
|
.answer-btn.selected {
|
|
background: rgba(68,136,255,0.1);
|
|
}
|
|
.answer-btn.yes-btn.selected { background: rgba(0,204,85,0.15); border-color: var(--green); }
|
|
.answer-btn.no-btn.selected { background: rgba(255,68,68,0.15); border-color: var(--red); }
|
|
|
|
.answer-btn:disabled {
|
|
opacity: 0.3;
|
|
cursor: default;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── Leaf / Decision ── */
|
|
.decision-leaf {
|
|
background: var(--card);
|
|
border: 2px solid;
|
|
border-radius: 16px;
|
|
padding: 24px 20px;
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
.decision-leaf.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
.decision-leaf.approve { border-color: var(--green); }
|
|
.decision-leaf.reject { border-color: var(--red); }
|
|
.decision-leaf.revise { border-color: var(--amber); }
|
|
|
|
.leaf-icon { font-size: 40px; margin-bottom: 8px; }
|
|
.leaf-decision {
|
|
font-size: 20px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.leaf-decision.approve { color: var(--green); }
|
|
.leaf-decision.reject { color: var(--red); }
|
|
.leaf-decision.revise { color: var(--amber); }
|
|
|
|
.leaf-summary {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
margin-bottom: 16px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Path recap in leaf */
|
|
.path-recap {
|
|
text-align: left;
|
|
background: rgba(0,0,0,0.2);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.recap-step {
|
|
display: flex;
|
|
gap: 8px;
|
|
font-size: 11px;
|
|
padding: 3px 0;
|
|
color: var(--muted);
|
|
}
|
|
.recap-step .recap-q { color: var(--text); flex: 1; }
|
|
.recap-step .recap-a { font-weight: 700; flex-shrink: 0; }
|
|
.recap-step .recap-a.yes { color: var(--green); }
|
|
.recap-step .recap-a.no { color: var(--red); }
|
|
|
|
.leaf-btn {
|
|
width: 100%;
|
|
padding: 14px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
color: #000;
|
|
transition: all 0.2s;
|
|
}
|
|
.leaf-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.4); }
|
|
.leaf-btn:active { transform: scale(0.98); }
|
|
.leaf-btn.approve { background: var(--green); }
|
|
.leaf-btn.reject { background: var(--red); }
|
|
.leaf-btn.revise { background: var(--amber); }
|
|
|
|
/* ── Undo button ── */
|
|
.undo-section {
|
|
text-align: center;
|
|
padding: 8px;
|
|
min-height: 36px;
|
|
}
|
|
.undo-btn {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
transition: all 0.2s;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.undo-btn.visible { opacity: 1; pointer-events: auto; }
|
|
.undo-btn:hover { color: var(--text); background: rgba(255,255,255,0.05); }
|
|
|
|
/* ── Done ── */
|
|
.done-overlay {
|
|
position: fixed; inset: 0; background: var(--bg);
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
opacity: 0; pointer-events: none; transition: opacity 0.5s; z-index: 100;
|
|
}
|
|
.done-overlay.visible { opacity: 1; pointer-events: auto; }
|
|
.done-overlay .done-emoji { font-size: 64px; }
|
|
.done-overlay .done-text { font-size: 16px; color: var(--muted); margin-top: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="tree-header">
|
|
<div class="label">Decision Tree</div>
|
|
<h2>Navigate to Your Verdict</h2>
|
|
<div class="item-name" id="itemName">Loading…</div>
|
|
</div>
|
|
|
|
<div class="path-visual" id="pathVisual"></div>
|
|
|
|
<div class="undo-section">
|
|
<button class="undo-btn" id="undoBtn" title="Go back">← Undo last answer</button>
|
|
</div>
|
|
|
|
<div class="tree-body" id="treeBody"></div>
|
|
|
|
<div class="done-overlay" id="doneOverlay">
|
|
<div class="done-emoji" id="doneEmoji">✅</div>
|
|
<div class="done-text">Decision path recorded</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const ctx = window.__FACTORY_CONTEXT__ || {};
|
|
const modalType = 'decision-tree';
|
|
const modalVersion = '1.0.0';
|
|
const startTime = Date.now();
|
|
|
|
function post(msg) { try { window.parent.postMessage(msg, '*'); } catch(e) {} }
|
|
post({ type: 'factory_modal_ready', modalType, version: modalVersion });
|
|
|
|
document.getElementById('itemName').textContent = ctx.itemName || ctx.pipelineName || 'MCP Server Output';
|
|
|
|
// ── Decision Tree Definition ──
|
|
// Each node: { id, question, depth_label, answers: [{ text, btnClass, next }] }
|
|
// `next` can be a node id string or a leaf object: { decision, icon, label, summary, btnClass }
|
|
const TREE = {
|
|
root: {
|
|
id: 'root',
|
|
question: 'Does this meet the core requirement?',
|
|
depthLabel: 'Step 1 · Foundation',
|
|
answers: [
|
|
{ text: '✓ Yes', btnClass: 'yes-btn', next: 'quality' },
|
|
{ text: '✗ No', btnClass: 'no-btn', next: 'fixable' },
|
|
],
|
|
},
|
|
quality: {
|
|
id: 'quality',
|
|
question: "How's the overall quality?",
|
|
depthLabel: 'Step 2 · Quality Assessment',
|
|
answers: [
|
|
{ text: '🌟 Excellent', btnClass: 'yes-btn', next: 'ready_to_ship' },
|
|
{ text: '👍 Good enough', btnClass: 'option-btn', next: 'polish_needed' },
|
|
{ text: '👎 Needs work', btnClass: 'no-btn', next: 'rework_quality' },
|
|
],
|
|
},
|
|
fixable: {
|
|
id: 'fixable',
|
|
question: 'Is it fixable with reasonable effort?',
|
|
depthLabel: 'Step 2 · Salvage Assessment',
|
|
answers: [
|
|
{ text: '✓ Yes, quick fix', btnClass: 'yes-btn', next: 'fix_scope' },
|
|
{ text: '✗ No, fundamental issues', btnClass: 'no-btn', next: { decision: 'reject', icon: '🚫', label: 'Reject', summary: 'Fundamental issues that cannot be reasonably fixed. Needs complete rethink.', btnClass: 'reject' } },
|
|
],
|
|
},
|
|
ready_to_ship: {
|
|
id: 'ready_to_ship',
|
|
question: 'Any final concerns before shipping?',
|
|
depthLabel: 'Step 3 · Final Check',
|
|
answers: [
|
|
{ text: '🚀 None, ship it!', btnClass: 'yes-btn', next: { decision: 'approve', icon: '✅', label: 'Approve', summary: 'Meets requirements, excellent quality, no concerns. Ready to ship!', btnClass: 'approve' } },
|
|
{ text: '⚠️ Minor concern', btnClass: 'option-btn', next: { decision: 'approve_with_notes', icon: '✅', label: 'Approve with Notes', summary: 'Excellent quality but flagged a minor concern for tracking.', btnClass: 'approve' } },
|
|
],
|
|
},
|
|
polish_needed: {
|
|
id: 'polish_needed',
|
|
question: 'What kind of polish does it need?',
|
|
depthLabel: 'Step 3 · Polish Scope',
|
|
answers: [
|
|
{ text: '🎨 Cosmetic only', btnClass: 'yes-btn', next: { decision: 'approve_with_notes', icon: '✅', label: 'Approve — Cosmetic Polish', summary: 'Core is solid. Approve with cosmetic improvements noted.', btnClass: 'approve' } },
|
|
{ text: '🔧 Functional tweaks', btnClass: 'option-btn', next: { decision: 'revise', icon: '🔄', label: 'Revise', summary: 'Meets requirements but needs functional refinement before shipping.', btnClass: 'revise' } },
|
|
{ text: '📝 Docs/tests only', btnClass: 'option-btn', next: { decision: 'approve_with_notes', icon: '✅', label: 'Approve — Needs Docs/Tests', summary: 'Code is good. Approve with note to improve documentation and tests.', btnClass: 'approve' } },
|
|
],
|
|
},
|
|
rework_quality: {
|
|
id: 'rework_quality',
|
|
question: 'Is the approach salvageable?',
|
|
depthLabel: 'Step 3 · Rework Scope',
|
|
answers: [
|
|
{ text: '✓ Yes, iterate', btnClass: 'yes-btn', next: { decision: 'revise', icon: '🔄', label: 'Revise & Iterate', summary: 'Right direction but quality isn\'t there yet. Send back for another pass.', btnClass: 'revise' } },
|
|
{ text: '✗ Wrong approach', btnClass: 'no-btn', next: { decision: 'reject', icon: '🚫', label: 'Reject — Wrong Approach', summary: 'Meets requirements superficially but approach is wrong. Start over.', btnClass: 'reject' } },
|
|
],
|
|
},
|
|
fix_scope: {
|
|
id: 'fix_scope',
|
|
question: 'How much effort to fix?',
|
|
depthLabel: 'Step 3 · Effort Estimate',
|
|
answers: [
|
|
{ text: '⚡ Trivial (<30min)', btnClass: 'yes-btn', next: { decision: 'revise', icon: '🔄', label: 'Quick Fix Needed', summary: 'Doesn\'t meet requirements but fixable with minimal effort.', btnClass: 'revise' } },
|
|
{ text: '🔧 Moderate (hours)', btnClass: 'option-btn', next: { decision: 'revise', icon: '🔄', label: 'Moderate Rework', summary: 'Fixable but needs significant rework. Send back with detailed feedback.', btnClass: 'revise' } },
|
|
{ text: '🏗️ Major (days)', btnClass: 'no-btn', next: { decision: 'reject', icon: '🚫', label: 'Reject — Too Much Work', summary: 'Technically fixable but effort exceeds value. Better to restart.', btnClass: 'reject' } },
|
|
],
|
|
},
|
|
};
|
|
|
|
// ── State ──
|
|
let path = []; // [{ nodeId, answerId, answerText, timestamp }]
|
|
let currentNodeId = 'root';
|
|
let interactions = 0;
|
|
let firstInteraction = null;
|
|
let questionTimings = {}; // { nodeId: { shownAt, answeredAt, deliberationMs } }
|
|
|
|
function track() { interactions++; if (!firstInteraction) firstInteraction = Date.now(); }
|
|
|
|
// ── Render ──
|
|
const treeBody = document.getElementById('treeBody');
|
|
|
|
function renderCurrentNode() {
|
|
const nodeDef = TREE[currentNodeId];
|
|
if (!nodeDef) return;
|
|
|
|
questionTimings[currentNodeId] = { shownAt: Date.now(), answeredAt: null, deliberationMs: 0 };
|
|
|
|
// Connector
|
|
if (path.length > 0) {
|
|
const conn = document.createElement('div');
|
|
conn.className = 'connector';
|
|
conn.id = 'conn_' + currentNodeId;
|
|
treeBody.appendChild(conn);
|
|
requestAnimationFrame(() => conn.classList.add('visible'));
|
|
}
|
|
|
|
// Question node
|
|
const nodeEl = document.createElement('div');
|
|
nodeEl.className = 'question-node';
|
|
nodeEl.id = 'node_' + currentNodeId;
|
|
nodeEl.dataset.nodeId = currentNodeId;
|
|
|
|
let answersHTML = nodeDef.answers.map((a, i) =>
|
|
`<button class="answer-btn ${a.btnClass}" data-answer="${i}">${a.text}</button>`
|
|
).join('');
|
|
|
|
nodeEl.innerHTML = `
|
|
<div class="q-card">
|
|
<div class="q-depth">${nodeDef.depthLabel}</div>
|
|
<div class="q-text">${nodeDef.question}</div>
|
|
<div class="q-answers">${answersHTML}</div>
|
|
</div>
|
|
`;
|
|
|
|
// Wire up answer buttons
|
|
nodeEl.querySelectorAll('.answer-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
track();
|
|
const answerIdx = parseInt(btn.dataset.answer);
|
|
handleAnswer(nodeDef, answerIdx, btn, nodeEl);
|
|
});
|
|
});
|
|
|
|
treeBody.appendChild(nodeEl);
|
|
|
|
// Animate in
|
|
requestAnimationFrame(() => {
|
|
nodeEl.classList.add('visible', 'current');
|
|
});
|
|
|
|
// Scroll to it
|
|
setTimeout(() => nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100);
|
|
|
|
updatePathVisual();
|
|
updateUndoBtn();
|
|
}
|
|
|
|
function handleAnswer(nodeDef, answerIdx, btn, nodeEl) {
|
|
const answer = nodeDef.answers[answerIdx];
|
|
|
|
// Record timing
|
|
if (questionTimings[currentNodeId]) {
|
|
questionTimings[currentNodeId].answeredAt = Date.now();
|
|
questionTimings[currentNodeId].deliberationMs = Date.now() - questionTimings[currentNodeId].shownAt;
|
|
}
|
|
|
|
// Record path
|
|
path.push({
|
|
nodeId: currentNodeId,
|
|
answerIdx,
|
|
answerText: answer.text.replace(/[^\w\s]/g, '').trim(),
|
|
isYes: answer.btnClass === 'yes-btn',
|
|
timestamp: Date.now() - startTime,
|
|
});
|
|
|
|
// Mark selected & disable
|
|
nodeEl.querySelectorAll('.answer-btn').forEach(b => b.disabled = true);
|
|
btn.classList.add('selected');
|
|
nodeEl.classList.remove('current');
|
|
nodeEl.classList.add('answered');
|
|
|
|
// Navigate
|
|
if (typeof answer.next === 'string') {
|
|
currentNodeId = answer.next;
|
|
setTimeout(() => renderCurrentNode(), 300);
|
|
} else {
|
|
// Leaf reached
|
|
setTimeout(() => renderLeaf(answer.next), 300);
|
|
}
|
|
|
|
updatePathVisual();
|
|
updateUndoBtn();
|
|
}
|
|
|
|
function renderLeaf(leaf) {
|
|
// Connector
|
|
const conn = document.createElement('div');
|
|
conn.className = 'connector';
|
|
treeBody.appendChild(conn);
|
|
requestAnimationFrame(() => conn.classList.add('visible'));
|
|
|
|
const leafEl = document.createElement('div');
|
|
leafEl.className = 'decision-leaf ' + leaf.btnClass;
|
|
leafEl.id = 'leafNode';
|
|
|
|
// Build path recap
|
|
let recapHTML = path.map(step => {
|
|
const nodeDef = TREE[step.nodeId];
|
|
return `<div class="recap-step">
|
|
<span class="recap-q">${nodeDef ? nodeDef.question : step.nodeId}</span>
|
|
<span class="recap-a ${step.isYes ? 'yes' : 'no'}">${step.answerText}</span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
leafEl.innerHTML = `
|
|
<div class="leaf-icon">${leaf.icon}</div>
|
|
<div class="leaf-decision ${leaf.btnClass}">${leaf.label}</div>
|
|
<div class="leaf-summary">${leaf.summary}</div>
|
|
<div class="path-recap">${recapHTML}</div>
|
|
<button class="leaf-btn ${leaf.btnClass}">Confirm: ${leaf.label}</button>
|
|
`;
|
|
|
|
leafEl.querySelector('.leaf-btn').addEventListener('click', () => {
|
|
track();
|
|
submitDecision(leaf);
|
|
});
|
|
|
|
treeBody.appendChild(leafEl);
|
|
|
|
requestAnimationFrame(() => leafEl.classList.add('visible'));
|
|
setTimeout(() => leafEl.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100);
|
|
|
|
// Hide undo once at leaf
|
|
document.getElementById('undoBtn').classList.remove('visible');
|
|
}
|
|
|
|
// ── Path Visual ──
|
|
function updatePathVisual() {
|
|
const visual = document.getElementById('pathVisual');
|
|
visual.innerHTML = '';
|
|
|
|
path.forEach((step, i) => {
|
|
if (i > 0) {
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'path-arrow';
|
|
arrow.textContent = '→';
|
|
visual.appendChild(arrow);
|
|
}
|
|
const node = document.createElement('span');
|
|
node.className = 'path-node ' + (step.isYes ? 'yes' : 'no');
|
|
node.textContent = step.answerText;
|
|
visual.appendChild(node);
|
|
});
|
|
|
|
// Current node indicator
|
|
if (TREE[currentNodeId] && !document.getElementById('leafNode')) {
|
|
if (path.length > 0) {
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'path-arrow';
|
|
arrow.textContent = '→';
|
|
visual.appendChild(arrow);
|
|
}
|
|
const cur = document.createElement('span');
|
|
cur.className = 'path-node active';
|
|
cur.textContent = '?';
|
|
visual.appendChild(cur);
|
|
}
|
|
}
|
|
|
|
// ── Undo ──
|
|
function updateUndoBtn() {
|
|
document.getElementById('undoBtn').classList.toggle('visible', path.length > 0 && !document.getElementById('leafNode'));
|
|
}
|
|
|
|
document.getElementById('undoBtn').addEventListener('click', () => {
|
|
if (path.length === 0) return;
|
|
track();
|
|
|
|
const lastStep = path.pop();
|
|
currentNodeId = lastStep.nodeId;
|
|
|
|
// Remove last question node + connector
|
|
const lastNodeEl = treeBody.lastElementChild;
|
|
if (lastNodeEl) lastNodeEl.remove();
|
|
const lastConn = treeBody.lastElementChild;
|
|
if (lastConn && lastConn.classList.contains('connector')) lastConn.remove();
|
|
|
|
// Re-enable the previous node
|
|
const prevNodeEl = document.getElementById('node_' + currentNodeId);
|
|
if (prevNodeEl) {
|
|
prevNodeEl.classList.remove('answered');
|
|
prevNodeEl.classList.add('current');
|
|
prevNodeEl.querySelectorAll('.answer-btn').forEach(b => {
|
|
b.disabled = false;
|
|
b.classList.remove('selected');
|
|
});
|
|
}
|
|
|
|
updatePathVisual();
|
|
updateUndoBtn();
|
|
});
|
|
|
|
// ── Submit ──
|
|
function submitDecision(leaf) {
|
|
const decisionPath = path.map(step => ({
|
|
question: TREE[step.nodeId] ? TREE[step.nodeId].question : step.nodeId,
|
|
answer: step.answerText,
|
|
isAffirmative: step.isYes,
|
|
timeInStepMs: questionTimings[step.nodeId] ? questionTimings[step.nodeId].deliberationMs : 0,
|
|
}));
|
|
|
|
const payload = {
|
|
type: 'factory_modal_response',
|
|
modalType,
|
|
modalVersion,
|
|
pipelineId: ctx.pipelineId || 'unknown',
|
|
itemId: ctx.itemId || 'unknown',
|
|
sessionId: ctx.sessionId || 'sess_' + Date.now(),
|
|
timestamp: new Date().toISOString(),
|
|
responseTimeMs: Date.now() - startTime,
|
|
feedback: {
|
|
decisionTree: {
|
|
decision: leaf.decision,
|
|
label: leaf.label,
|
|
summary: leaf.summary,
|
|
path: decisionPath,
|
|
depth: path.length,
|
|
},
|
|
},
|
|
meta: {
|
|
timeToFirstInteractionMs: firstInteraction ? firstInteraction - startTime : null,
|
|
timeToDecisionMs: Date.now() - startTime,
|
|
totalInteractions: interactions,
|
|
fieldsModified: path.map(s => s.nodeId),
|
|
deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop',
|
|
viewportSize: { width: window.innerWidth, height: window.innerHeight },
|
|
questionDeliberation: Object.fromEntries(
|
|
Object.entries(questionTimings).map(([id, t]) => [id, t.deliberationMs])
|
|
),
|
|
treeDepth: path.length,
|
|
undoCount: interactions - path.length - 1, // approximate
|
|
},
|
|
};
|
|
|
|
post(payload);
|
|
|
|
// Done
|
|
const emoji = leaf.decision === 'approve' || leaf.decision === 'approve_with_notes' ? '✅' : leaf.decision === 'revise' ? '🔄' : '🚫';
|
|
document.getElementById('doneEmoji').textContent = emoji;
|
|
document.getElementById('doneOverlay').classList.add('visible');
|
|
|
|
setTimeout(() => {
|
|
post({ type: 'factory_modal_close', reason: 'completed' });
|
|
}, 1200);
|
|
}
|
|
|
|
// ── Init ──
|
|
renderCurrentNode();
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|