Spaces:
Paused
Paused
Update templates/index.html
Browse files- templates/index.html +192 -77
templates/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 6 |
-
<title>Overthinker
|
| 7 |
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
| 8 |
<style>
|
| 9 |
* { margin:0; padding:0; box-sizing:border-box; }
|
|
@@ -261,6 +261,48 @@ body.light #graph-container { background: #f0f4f8; }
|
|
| 261 |
#sidebar-tab { padding: 8px 4px; font-size: 0.8rem; }
|
| 262 |
}
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
#ensurePngContainer {
|
| 265 |
position: absolute;
|
| 266 |
left: -9999px;
|
|
@@ -269,26 +311,26 @@ body.light #graph-container { background: #f0f4f8; }
|
|
| 269 |
height: 1px;
|
| 270 |
overflow: hidden;
|
| 271 |
}
|
| 272 |
-
|
| 273 |
</style>
|
| 274 |
</head>
|
| 275 |
<body>
|
| 276 |
|
| 277 |
<div class="top-bar">
|
| 278 |
-
<div class="logo"><h1>
|
| 279 |
<div class="top-controls">
|
| 280 |
<label>Branches:</label>
|
| 281 |
<input type="number" id="branch-input" value="3" min="2" max="6">
|
| 282 |
-
<button class="btn btn-secondary btn-icon" id="theme-btn" title="Toggle Theme">
|
| 283 |
-
<button class="btn btn-secondary" id="
|
|
|
|
| 284 |
</div>
|
| 285 |
</div>
|
| 286 |
|
| 287 |
<div class="prompt-bar">
|
| 288 |
<div class="input-group">
|
| 289 |
<textarea id="decision-input" placeholder="What decision are you overthinking? e.g. 'Should I quit my job to start a startup?'..." rows="1"></textarea>
|
| 290 |
-
<button class="btn btn-primary btn-generate" id="generate-btn">
|
| 291 |
-
<button class="btn btn-secondary sample-btn" id="sample-btn">
|
| 292 |
</div>
|
| 293 |
</div>
|
| 294 |
|
|
@@ -298,12 +340,82 @@ body.light #graph-container { background: #f0f4f8; }
|
|
| 298 |
|
| 299 |
<div id="ensurePngContainer"></div>
|
| 300 |
|
| 301 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
<div id="path-sidebar">
|
| 304 |
<div id="sidebar-header">
|
| 305 |
-
<h3>
|
| 306 |
-
<button id="sidebar-close">
|
| 307 |
</div>
|
| 308 |
<div id="path-list">
|
| 309 |
<div style="color:#5a6088; font-size:0.75em; padding:20px; text-align:center;">Click a node to see its path</div>
|
|
@@ -312,26 +424,26 @@ body.light #graph-container { background: #f0f4f8; }
|
|
| 312 |
<div><b>Depth:</b> <span id="path-depth">0</span> | <b>Tips:</b> <span id="path-tips-count">0</span></div>
|
| 313 |
</div>
|
| 314 |
<div id="sidebar-export-row">
|
| 315 |
-
<span style="color:#888;font-size:0.65em;width:100%;text-align:center;margin-bottom:2px;">
|
| 316 |
-
<button class="btn btn-secondary btn-sm" id="path-export-svg">
|
| 317 |
-
<button class="btn btn-secondary btn-sm" id="path-export-json">
|
| 318 |
-
<button class="btn btn-secondary btn-sm" id="path-export-md">
|
| 319 |
-
<button class="btn btn-secondary btn-sm" id="path-export-png">
|
| 320 |
</div>
|
| 321 |
</div>
|
| 322 |
|
| 323 |
<div class="toolbar" id="toolbar" style="display:none;">
|
| 324 |
<div class="toolbar-group">
|
| 325 |
-
<button class="btn btn-secondary btn-icon" id="undo-btn" title="Undo (Ctrl+Z)">
|
| 326 |
-
<button class="btn btn-secondary btn-icon" id="redo-btn" title="Redo (Ctrl+Shift+Z)">
|
| 327 |
<div class="toolbar-divider"></div>
|
| 328 |
-
<button class="btn btn-secondary btn-icon" id="fit-btn" title="Fit View (F)">
|
| 329 |
-
<button class="btn btn-secondary btn-icon" id="clear-btn" title="Clear (C)">
|
| 330 |
<div class="toolbar-divider"></div>
|
| 331 |
-
<button class="btn btn-secondary" id="export-svg-btn">
|
| 332 |
-
<button class="btn btn-secondary" id="export-json-btn">
|
| 333 |
-
<button class="btn btn-secondary" id="export-md-btn">
|
| 334 |
-
<button class="btn btn-secondary" id="export-png-btn">
|
| 335 |
</div>
|
| 336 |
<div class="stats">
|
| 337 |
<span>Nodes: <span class="highlight" id="stat-nodes">0</span></span>
|
|
@@ -342,12 +454,12 @@ body.light #graph-container { background: #f0f4f8; }
|
|
| 342 |
</div>
|
| 343 |
|
| 344 |
<div id="context-menu">
|
| 345 |
-
<div class="ctx-item" data-action="expand">
|
| 346 |
-
<div class="ctx-item" data-action="regenerate">
|
| 347 |
-
<div class="ctx-item" data-action="add">
|
| 348 |
<div class="ctx-divider"></div>
|
| 349 |
-
<div class="ctx-item" data-action="copy-path">
|
| 350 |
-
<div class="ctx-item" data-action="center">
|
| 351 |
</div>
|
| 352 |
|
| 353 |
<div id="loading-overlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:999;align-items:center;justify-content:center;flex-direction:column;">
|
|
@@ -363,10 +475,9 @@ body.light #graph-container { background: #f0f4f8; }
|
|
| 363 |
|
| 364 |
<script>
|
| 365 |
// =============================================
|
| 366 |
-
// OVER THINKER
|
| 367 |
// - Dual prompts: Options for input, Outcomes for outcome
|
| 368 |
-
// - Model: nvidia/nemotron-3-nano-30b-a3b
|
| 369 |
-
// - Fixed: SVG/PNG export centering + root description/tips
|
| 370 |
// =============================================
|
| 371 |
|
| 372 |
let nodes = [];
|
|
@@ -401,8 +512,8 @@ function showLoading(msg) {
|
|
| 401 |
function hideLoading() { loadingOverlay.style.display = 'none'; }
|
| 402 |
|
| 403 |
function getNodeTypeLabel(type) {
|
| 404 |
-
const map = { 'root': '
|
| 405 |
-
return map[type] || '
|
| 406 |
}
|
| 407 |
function getNextTypeLabel(type) {
|
| 408 |
const map = { 'root': 'Options/Choices', 'input': 'Outcomes', 'outcome': 'Options/Choices' };
|
|
@@ -415,7 +526,7 @@ function getNextType(current) {
|
|
| 415 |
|
| 416 |
function showToast(msg, type='info') {
|
| 417 |
const toast = document.getElementById('toast');
|
| 418 |
-
toast.textContent = '
|
| 419 |
toast.className = 'toast ' + type;
|
| 420 |
toast.classList.add('show');
|
| 421 |
clearTimeout(toast._t);
|
|
@@ -624,7 +735,7 @@ function updateTileTexts() {
|
|
| 624 |
g.select('.tile-type').text(getNodeTypeLabel(type).split(' ')[0]);
|
| 625 |
g.select('.tile-type').attr('class', 'tile-type type-' + type + '-label');
|
| 626 |
g.select('.tile-badge-depth').text('D'+(d.depth||0));
|
| 627 |
-
g.select('.tile-badge-children').text('
|
| 628 |
g.select('.tile-rect').attr('class', 'tile-rect type-' + type);
|
| 629 |
if (d.depth === 0) g.classed('root-pinned', true);
|
| 630 |
});
|
|
@@ -672,17 +783,17 @@ function showPopupForNode(node, centerView = true) {
|
|
| 672 |
const typeLabel = getNodeTypeLabel(type);
|
| 673 |
let buttonsHTML = '';
|
| 674 |
if (!isExpanded) {
|
| 675 |
-
buttonsHTML = `<button class="${type === 'outcome' ? 'btn-explore-input' : 'btn-explore-outcome'}" id="fo-explore">
|
| 676 |
} else {
|
| 677 |
-
buttonsHTML = `<button class="btn-regenerate" id="fo-regen">
|
| 678 |
}
|
| 679 |
const html = `
|
| 680 |
<div xmlns="http://www.w3.org/1999/xhtml" class="fo-popup-box">
|
| 681 |
-
<button class="fo-close-btn" id="fo-close">
|
| 682 |
<h4>${escapeHtml(node._label || node.label || 'Node')}</h4>
|
| 683 |
<div class="type-badge type-${type}">${typeLabel}</div>
|
| 684 |
<div class="full-desc">${escapeHtml(node._description || node.description || '')}</div>
|
| 685 |
-
${node._tips && node._tips.length ? `<div style="font-size:0.72em;color:#7ec8a0;margin-bottom:8px;padding:6px;background:rgba(126,200,160,0.08);border-radius:4px;">
|
| 686 |
<textarea id="fo-comment" placeholder="Your thought about this..."></textarea>
|
| 687 |
<div class="btn-row">${buttonsHTML}</div>
|
| 688 |
</div>
|
|
@@ -806,9 +917,9 @@ function updatePathSidebar() {
|
|
| 806 |
if (node._description) {
|
| 807 |
html += `<div class="card-desc">${escapeHtml(node._description).substring(0, 80)}${node._description.length > 80 ? '...' : ''}</div>`;
|
| 808 |
}
|
| 809 |
-
html += `<div class="card-tip ${hasTip ? 'show' : ''}">${hasTip ? '
|
| 810 |
html += '</div>';
|
| 811 |
-
if (!isLast) html += '<div class="path-arrow">
|
| 812 |
});
|
| 813 |
list.innerHTML = html;
|
| 814 |
document.getElementById('path-depth').textContent = path.length;
|
|
@@ -826,7 +937,7 @@ function updatePathSidebar() {
|
|
| 826 |
async function generateTree() {
|
| 827 |
const decision = inputEl.value.trim();
|
| 828 |
if (!decision) return;
|
| 829 |
-
generateBtn.textContent = '
|
| 830 |
generateBtn.disabled = true;
|
| 831 |
showLoading('Creating decision root...');
|
| 832 |
try {
|
|
@@ -849,7 +960,7 @@ async function generateTree() {
|
|
| 849 |
showToast('Generation failed: '+e.message, 'error');
|
| 850 |
} finally {
|
| 851 |
hideLoading();
|
| 852 |
-
generateBtn.textContent = '
|
| 853 |
generateBtn.disabled = false;
|
| 854 |
document.getElementById('toolbar').style.display = 'flex';
|
| 855 |
}
|
|
@@ -1010,7 +1121,7 @@ document.getElementById('context-menu').addEventListener('click', async (ev) =>
|
|
| 1010 |
if (n) path.unshift(n._label);
|
| 1011 |
cur = parentMap.get(cur);
|
| 1012 |
}
|
| 1013 |
-
navigator.clipboard.writeText(path.join('
|
| 1014 |
.catch(() => showToast('Copy failed','error'));
|
| 1015 |
break;
|
| 1016 |
case 'center':
|
|
@@ -1025,7 +1136,7 @@ document.getElementById('context-menu').addEventListener('click', async (ev) =>
|
|
| 1025 |
});
|
| 1026 |
|
| 1027 |
// =============================================
|
| 1028 |
-
// EXPORT FUNCTIONS
|
| 1029 |
// =============================================
|
| 1030 |
|
| 1031 |
function computeTextLines(text, maxLen) {
|
|
@@ -1046,7 +1157,7 @@ function computePathTileHeight(node) {
|
|
| 1046 |
const maxDescLen = 35;
|
| 1047 |
const descLines = computeTextLines(node._description || '', maxDescLen);
|
| 1048 |
const cappedDescLines = Math.min(descLines, 3);
|
| 1049 |
-
const tipLines = computeTextLines((node._tips && node._tips[0]) ? '
|
| 1050 |
const cappedTipLines = Math.min(tipLines, 2);
|
| 1051 |
let extraHeight = cappedDescLines * 13;
|
| 1052 |
if (cappedTipLines > 0) extraHeight += 4 + cappedTipLines * 11;
|
|
@@ -1130,30 +1241,24 @@ function computeExportLayout(exportNodes, exportLinks, canvasW) {
|
|
| 1130 |
if (converged) break;
|
| 1131 |
}
|
| 1132 |
|
| 1133 |
-
// Compute actual bounding box for centering (include half card width)
|
| 1134 |
let minX = Infinity, maxX = -Infinity;
|
| 1135 |
Object.values(positions).forEach(x => { minX = Math.min(minX, x - CARD_W / 2); maxX = Math.max(maxX, x + CARD_W / 2); });
|
| 1136 |
const treeW = maxX - minX;
|
| 1137 |
-
// Compute dynamic tile heights for all nodes
|
| 1138 |
let maxExportTileH = CARD_H;
|
| 1139 |
exportNodes.forEach(n => {
|
| 1140 |
const h = computePathTileHeight(n);
|
| 1141 |
if (h > maxExportTileH) maxExportTileH = h;
|
| 1142 |
});
|
| 1143 |
-
// Ensure totalW covers the tree with padding and is at least canvasW
|
| 1144 |
const minTotalW = Math.max(canvasW, treeW + 80);
|
| 1145 |
const totalW = minTotalW;
|
| 1146 |
-
// Center the tree horizontally within totalW
|
| 1147 |
const offsetX = (totalW - treeW) / 2 - minX;
|
| 1148 |
Object.keys(positions).forEach(id => { positions[id] += offsetX; });
|
| 1149 |
|
| 1150 |
-
// Recompute bounding box after offset (should be centered)
|
| 1151 |
let newMinX = Infinity, newMaxX = -Infinity;
|
| 1152 |
Object.values(positions).forEach(x => { newMinX = Math.min(newMinX, x - CARD_W / 2); newMaxX = Math.max(newMaxX, x + CARD_W / 2); });
|
| 1153 |
const finalTreeW = newMaxX - newMinX;
|
| 1154 |
const finalTotalW = Math.max(totalW, finalTreeW + 40);
|
| 1155 |
|
| 1156 |
-
// Use per-node tile heights for vertical spacing
|
| 1157 |
const rowHeights = {};
|
| 1158 |
for (let d = 0; d <= maxDepth; d++) {
|
| 1159 |
let maxRowH = CARD_H;
|
|
@@ -1171,18 +1276,13 @@ function computeExportLayout(exportNodes, exportLinks, canvasW) {
|
|
| 1171 |
}
|
| 1172 |
totalH += 20;
|
| 1173 |
|
| 1174 |
-
let currentY = ROOT_Y;
|
| 1175 |
exportNodes.forEach(n => {
|
| 1176 |
-
const d = n.depth || 0;
|
| 1177 |
-
const rowH = rowHeights[d];
|
| 1178 |
-
// Compute y position for this node: center of its row
|
| 1179 |
let yOffset = 0;
|
| 1180 |
-
for (let i = 0; i <
|
| 1181 |
yOffset += rowHeights[i] + Y_PADDING;
|
| 1182 |
}
|
| 1183 |
-
const nodeY = ROOT_Y + yOffset + rowH / 2;
|
| 1184 |
n._exportX = positions[n.id] !== undefined ? positions[n.id] : canvasW / 2;
|
| 1185 |
-
n._exportY =
|
| 1186 |
n._exportTileH = computePathTileHeight(n);
|
| 1187 |
});
|
| 1188 |
|
|
@@ -1191,7 +1291,6 @@ function computeExportLayout(exportNodes, exportLinks, canvasW) {
|
|
| 1191 |
|
| 1192 |
function computePathLayoutLinear(exportNodes, exportLinks, canvasW) {
|
| 1193 |
if (!exportNodes.length) return null;
|
| 1194 |
-
let cumulativeY = ROOT_Y;
|
| 1195 |
const GAP = 40;
|
| 1196 |
const nodeHeights = [];
|
| 1197 |
exportNodes.forEach((n, idx) => {
|
|
@@ -1295,7 +1394,6 @@ function buildExportSvg(isPathOnly = false) {
|
|
| 1295 |
const type = n._type || 'outcome';
|
| 1296 |
const [line1, line2] = splitLabel(n._label || '');
|
| 1297 |
|
| 1298 |
-
// Label line 1
|
| 1299 |
const txt1 = document.createElementNS(ns, 'text');
|
| 1300 |
txt1.setAttribute('x', '0');
|
| 1301 |
txt1.setAttribute('y', -(tileH/2) + 20);
|
|
@@ -1320,7 +1418,6 @@ function buildExportSvg(isPathOnly = false) {
|
|
| 1320 |
g.appendChild(txt2);
|
| 1321 |
}
|
| 1322 |
|
| 1323 |
-
// Type label
|
| 1324 |
const typeTxt = document.createElementNS(ns, 'text');
|
| 1325 |
typeTxt.setAttribute('x', '0');
|
| 1326 |
typeTxt.setAttribute('y', -(tileH/2) + 52);
|
|
@@ -1328,11 +1425,10 @@ function buildExportSvg(isPathOnly = false) {
|
|
| 1328 |
typeTxt.setAttribute('fill', typeFillColor);
|
| 1329 |
typeTxt.setAttribute('font-size', '9');
|
| 1330 |
typeTxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
|
| 1331 |
-
const typeLabels = { root: '
|
| 1332 |
-
typeTxt.textContent = typeLabels[type] || '
|
| 1333 |
g.appendChild(typeTxt);
|
| 1334 |
|
| 1335 |
-
// Description (always render for all nodes in both full tree and path)
|
| 1336 |
const maxDescLen = 35;
|
| 1337 |
const desc = n._description || '';
|
| 1338 |
if (desc.length > 0) {
|
|
@@ -1361,11 +1457,10 @@ function buildExportSvg(isPathOnly = false) {
|
|
| 1361 |
});
|
| 1362 |
}
|
| 1363 |
|
| 1364 |
-
// Tip (always render for all nodes)
|
| 1365 |
const tip = (n._tips && n._tips.length) ? n._tips[0] : '';
|
| 1366 |
if (tip) {
|
| 1367 |
const tipLines = [];
|
| 1368 |
-
let remainingTip = '
|
| 1369 |
const maxTipLen = 35;
|
| 1370 |
while (remainingTip.length > 0 && tipLines.length < 2) {
|
| 1371 |
if (remainingTip.length <= maxTipLen) {
|
|
@@ -1408,7 +1503,7 @@ function buildExportSvg(isPathOnly = false) {
|
|
| 1408 |
chTxt.setAttribute('fill', isDark ? '#7ecfff' : '#2e86ab');
|
| 1409 |
chTxt.setAttribute('font-size', '10');
|
| 1410 |
chTxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
|
| 1411 |
-
chTxt.textContent = '
|
| 1412 |
g.appendChild(chTxt);
|
| 1413 |
|
| 1414 |
svgDoc.appendChild(g);
|
|
@@ -1423,7 +1518,7 @@ function buildExportSvg(isPathOnly = false) {
|
|
| 1423 |
title.setAttribute('font-weight', '700');
|
| 1424 |
title.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
|
| 1425 |
const rootLabel = exportNodes.find(n => n.depth === 0)?._label || 'Decision Tree';
|
| 1426 |
-
title.textContent = isPathOnly ? `
|
| 1427 |
svgDoc.appendChild(title);
|
| 1428 |
|
| 1429 |
return svgDoc;
|
|
@@ -1486,20 +1581,20 @@ async function exportPathJson() {
|
|
| 1486 |
|
| 1487 |
function exportMarkdown() {
|
| 1488 |
if (!nodes.length) { showToast('Generate a tree first', 'error'); return; }
|
| 1489 |
-
let md = '#
|
| 1490 |
const root = nodes.find(n => n.depth === 0);
|
| 1491 |
if (root) {
|
| 1492 |
-
md += `##
|
| 1493 |
if (root._description) md += `> ${root._description}\n\n`;
|
| 1494 |
}
|
| 1495 |
function collectMd(nodeId, depth) {
|
| 1496 |
const indent = ' '.repeat(depth);
|
| 1497 |
const node = nodes.find(n => n.id === nodeId);
|
| 1498 |
if (!node) return;
|
| 1499 |
-
const emoji = node._type === 'root' ? '
|
| 1500 |
md += `${indent}- ${emoji} **[${node._type.toUpperCase()}] ${node._label}**\n`;
|
| 1501 |
if (node._description) md += `${indent} - _${node._description.substring(0, 100)}_\n`;
|
| 1502 |
-
if (node._tips && node._tips.length) md += `${indent} -
|
| 1503 |
md += '\n';
|
| 1504 |
const childIds = [];
|
| 1505 |
parentMap.forEach((pid, cid) => { if (pid === nodeId) childIds.push(cid); });
|
|
@@ -1646,11 +1741,31 @@ document.addEventListener('keydown', (ev) => {
|
|
| 1646 |
function toggleTheme() {
|
| 1647 |
isDark = !isDark;
|
| 1648 |
document.body.classList.toggle('light', !isDark);
|
| 1649 |
-
document.getElementById('theme-btn').textContent = isDark ? '
|
| 1650 |
-
localStorage.setItem('
|
| 1651 |
}
|
| 1652 |
document.getElementById('theme-btn').addEventListener('click', toggleTheme);
|
| 1653 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1654 |
// ---------- SAMPLE ----------
|
| 1655 |
document.getElementById('sample-btn').addEventListener('click', () => {
|
| 1656 |
const samples = [
|
|
@@ -1668,9 +1783,9 @@ document.getElementById('sample-btn').addEventListener('click', () => {
|
|
| 1668 |
|
| 1669 |
// ---------- INIT ----------
|
| 1670 |
initSvg();
|
| 1671 |
-
if (localStorage.getItem('
|
| 1672 |
|
| 1673 |
-
console.log('
|
| 1674 |
</script>
|
| 1675 |
</body>
|
| 1676 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>Overthinker — Gradio Server</title>
|
| 7 |
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
| 8 |
<style>
|
| 9 |
* { margin:0; padding:0; box-sizing:border-box; }
|
|
|
|
| 261 |
#sidebar-tab { padding: 8px 4px; font-size: 0.8rem; }
|
| 262 |
}
|
| 263 |
|
| 264 |
+
/* ---------- Info / Help Dropdown ---------- */
|
| 265 |
+
.dropdown-overlay {
|
| 266 |
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
| 267 |
+
z-index: 2000; display: none;
|
| 268 |
+
align-items: center; justify-content: center;
|
| 269 |
+
background: rgba(0,0,0,0.55);
|
| 270 |
+
backdrop-filter: blur(4px);
|
| 271 |
+
}
|
| 272 |
+
.dropdown-overlay.show { display: flex; }
|
| 273 |
+
.dropdown-box {
|
| 274 |
+
background: rgba(15,12,41,0.97);
|
| 275 |
+
border: 2px solid #ffd700;
|
| 276 |
+
border-radius: 14px;
|
| 277 |
+
padding: 24px 28px;
|
| 278 |
+
max-width: 560px;
|
| 279 |
+
width: 90%;
|
| 280 |
+
max-height: 80vh;
|
| 281 |
+
overflow-y: auto;
|
| 282 |
+
color: #e0e0e0;
|
| 283 |
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
| 284 |
+
position: relative;
|
| 285 |
+
box-shadow: 0 12px 48px rgba(0,0,0,0.7);
|
| 286 |
+
}
|
| 287 |
+
body.light .dropdown-box { background: rgba(255,255,255,0.97); color: #333; border-color: #e67e22; }
|
| 288 |
+
.dropdown-box h3 { color: #ffd700; font-size: 1.15rem; margin-bottom: 16px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 10px; }
|
| 289 |
+
body.light .dropdown-box h3 { color: #e67e22; border-bottom-color: rgba(0,0,0,0.08); }
|
| 290 |
+
.dropdown-close { position: absolute; top: 12px; right: 16px; background: none; border: none; color: #c9aaff; font-size: 20px; cursor: pointer; }
|
| 291 |
+
.dropdown-close:hover { color: #ffd700; }
|
| 292 |
+
body.light .dropdown-close { color: #888; }
|
| 293 |
+
body.light .dropdown-close:hover { color: #e67e22; }
|
| 294 |
+
.dropdown-section { margin-bottom: 14px; }
|
| 295 |
+
.dropdown-section h4 { color: #ffd700; font-size: 0.88rem; margin-bottom: 4px; }
|
| 296 |
+
body.light .dropdown-section h4 { color: #e67e22; }
|
| 297 |
+
.dropdown-section p { font-size: 0.82rem; line-height: 1.55; color: #c0c0c0; }
|
| 298 |
+
body.light .dropdown-section p { color: #555; }
|
| 299 |
+
.dropdown-section ul { margin: 4px 0 0 14px; font-size: 0.82rem; line-height: 1.6; color: #c0c0c0; }
|
| 300 |
+
body.light .dropdown-section ul { color: #555; }
|
| 301 |
+
.dropdown-section code { background: rgba(255,255,255,0.08); padding: 1px 5px; border-radius: 3px; font-size: 0.78rem; }
|
| 302 |
+
body.light .dropdown-section code { background: rgba(0,0,0,0.06); }
|
| 303 |
+
.dropdown-section kbd { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 5px; font-size: 0.75rem; font-family: monospace; }
|
| 304 |
+
body.light .dropdown-section kbd { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.12); }
|
| 305 |
+
|
| 306 |
#ensurePngContainer {
|
| 307 |
position: absolute;
|
| 308 |
left: -9999px;
|
|
|
|
| 311 |
height: 1px;
|
| 312 |
overflow: hidden;
|
| 313 |
}
|
|
|
|
| 314 |
</style>
|
| 315 |
</head>
|
| 316 |
<body>
|
| 317 |
|
| 318 |
<div class="top-bar">
|
| 319 |
+
<div class="logo"><h1>\U0001f9e0 Overthinker</h1></div>
|
| 320 |
<div class="top-controls">
|
| 321 |
<label>Branches:</label>
|
| 322 |
<input type="number" id="branch-input" value="3" min="2" max="6">
|
| 323 |
+
<button class="btn btn-secondary btn-icon" id="theme-btn" title="Toggle Theme">\U0001f313</button>
|
| 324 |
+
<button class="btn btn-secondary" id="info-btn" title="Info">\u2139\ufe0f</button>
|
| 325 |
+
<button class="btn btn-secondary" id="help-btn" title="Help">\u2753</button>
|
| 326 |
</div>
|
| 327 |
</div>
|
| 328 |
|
| 329 |
<div class="prompt-bar">
|
| 330 |
<div class="input-group">
|
| 331 |
<textarea id="decision-input" placeholder="What decision are you overthinking? e.g. 'Should I quit my job to start a startup?'..." rows="1"></textarea>
|
| 332 |
+
<button class="btn btn-primary btn-generate" id="generate-btn">\U0001f333 Generate</button>
|
| 333 |
+
<button class="btn btn-secondary sample-btn" id="sample-btn">\U0001f3af Sample</button>
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
|
|
|
|
| 340 |
|
| 341 |
<div id="ensurePngContainer"></div>
|
| 342 |
|
| 343 |
+
<!-- Hackathon Info Dropdown -->
|
| 344 |
+
<div id="info-dropdown" class="dropdown-overlay">
|
| 345 |
+
<div class="dropdown-box">
|
| 346 |
+
<button class="dropdown-close" id="info-close">\u2715</button>
|
| 347 |
+
<h3>\u2139\ufe0f About Overthinker</h3>
|
| 348 |
+
<div class="dropdown-section">
|
| 349 |
+
<h4>\U0001f916 Model</h4>
|
| 350 |
+
<p>Powered by <strong>nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8</strong> via <strong>OpenRouter</strong>. Optimized for structured decision tree generation with dual-prompt architecture (options vs outcomes).</p>
|
| 351 |
+
</div>
|
| 352 |
+
<div class="dropdown-section">
|
| 353 |
+
<h4>\U0001f30d Framework</h4>
|
| 354 |
+
<p>Built on <strong>Gradio.Server</strong> with integrated FastAPI routes. Swagger API docs available at <code>/docs</code>. Fully client-side rendering with D3.js for interactive tree visualization.</p>
|
| 355 |
+
</div>
|
| 356 |
+
<div class="dropdown-section">
|
| 357 |
+
<h4>\u2728 Key Features</h4>
|
| 358 |
+
<ul>
|
| 359 |
+
<li>\U0001f333 Generate trees from any decision prompt</li>
|
| 360 |
+
<li>\U0001f9e0 Dual prompt design: Options vs Outcomes</li>
|
| 361 |
+
<li>\U0001f50d Interactive explore / regenerate / add options</li>
|
| 362 |
+
<li>\U0001f4cd Path sidebar with depth tracking & tips</li>
|
| 363 |
+
<li>\U0001f5bc Export: SVG, JSON, Markdown, PNG</li>
|
| 364 |
+
<li>\U0001f313 Dark/Light theme toggle</li>
|
| 365 |
+
<li>\u2328\ufe0f Keyboard shortcuts (E, R, A, F, C)</li>
|
| 366 |
+
<li>\u21a9 Undo/Redo support</li>
|
| 367 |
+
<li>\U0001f504 Regenerate with user context</li>
|
| 368 |
+
</ul>
|
| 369 |
+
</div>
|
| 370 |
+
<div class="dropdown-section">
|
| 371 |
+
<h4>\U0001f4ac OpenRouter</h4>
|
| 372 |
+
<p>API calls go through <code>openrouter.ai/api/v1/chat/completions</code> with model <code>nvidia/nemotron-3-nano-30b-a3b</code>. Temperature: 0.8, Max tokens: 2048.</p>
|
| 373 |
+
</div>
|
| 374 |
+
</div>
|
| 375 |
+
</div>
|
| 376 |
+
|
| 377 |
+
<!-- Help Dropdown -->
|
| 378 |
+
<div id="help-dropdown" class="dropdown-overlay">
|
| 379 |
+
<div class="dropdown-box">
|
| 380 |
+
<button class="dropdown-close" id="help-close">\u2715</button>
|
| 381 |
+
<h3>\u2753 How to Use</h3>
|
| 382 |
+
<div class="dropdown-section">
|
| 383 |
+
<h4>1. Enter a Decision</h4>
|
| 384 |
+
<p>Type or paste a decision into the text box (e.g., "Should I quit my job to start a startup?") and click <strong>Generate</strong> or press <kbd>Enter</kbd>.</p>
|
| 385 |
+
</div>
|
| 386 |
+
<div class="dropdown-section">
|
| 387 |
+
<h4>2. Explore the Tree</h4>
|
| 388 |
+
<p>Click any node to see its details. Use the popup to <strong>Explore</strong> (generate more branches), <strong>Regenerate</strong> (with your feedback), or <strong>Add Options</strong>.</p>
|
| 389 |
+
</div>
|
| 390 |
+
<div class="dropdown-section">
|
| 391 |
+
<h4>3. Navigate</h4>
|
| 392 |
+
<ul>
|
| 393 |
+
<li>Click node = select + details popup</li>
|
| 394 |
+
<li>Right-click node = context menu</li>
|
| 395 |
+
<li>Drag canvas = pan view</li>
|
| 396 |
+
<li>Scroll = zoom in/out</li>
|
| 397 |
+
<li><kbd>E</kbd> = Explore, <kbd>R</kbd> = Regenerate, <kbd>A</kbd> = Add Options</li>
|
| 398 |
+
<li><kbd>F</kbd> = Fit view, <kbd>C</kbd> = Clear</li>
|
| 399 |
+
<li><kbd>Esc</kbd> = Close popups</li>
|
| 400 |
+
</ul>
|
| 401 |
+
</div>
|
| 402 |
+
<div class="dropdown-section">
|
| 403 |
+
<h4>4. Track Your Path</h4>
|
| 404 |
+
<p>Click the <strong>Path</strong> tab on the right to see your current decision path with depth and tips. Export path as SVG, JSON, Markdown, or PNG.</p>
|
| 405 |
+
</div>
|
| 406 |
+
<div class="dropdown-section">
|
| 407 |
+
<h4>5. Export</h4>
|
| 408 |
+
<p>Use the toolbar buttons to export the entire tree or just the selected path in your preferred format: SVG, JSON, Markdown, or PNG.</p>
|
| 409 |
+
</div>
|
| 410 |
+
</div>
|
| 411 |
+
</div>
|
| 412 |
+
|
| 413 |
+
<button id="sidebar-tab">\U0001f4cd Path</button>
|
| 414 |
|
| 415 |
<div id="path-sidebar">
|
| 416 |
<div id="sidebar-header">
|
| 417 |
+
<h3>\U0001f4cd Decision Path</h3>
|
| 418 |
+
<button id="sidebar-close">\u2715</button>
|
| 419 |
</div>
|
| 420 |
<div id="path-list">
|
| 421 |
<div style="color:#5a6088; font-size:0.75em; padding:20px; text-align:center;">Click a node to see its path</div>
|
|
|
|
| 424 |
<div><b>Depth:</b> <span id="path-depth">0</span> | <b>Tips:</b> <span id="path-tips-count">0</span></div>
|
| 425 |
</div>
|
| 426 |
<div id="sidebar-export-row">
|
| 427 |
+
<span style="color:#888;font-size:0.65em;width:100%;text-align:center;margin-bottom:2px;">\U0001f4e4 Export Path:</span>
|
| 428 |
+
<button class="btn btn-secondary btn-sm" id="path-export-svg">\U0001f5bc SVG</button>
|
| 429 |
+
<button class="btn btn-secondary btn-sm" id="path-export-json">\U0001f4c4 JSON</button>
|
| 430 |
+
<button class="btn btn-secondary btn-sm" id="path-export-md">\U0001f4dd MD</button>
|
| 431 |
+
<button class="btn btn-secondary btn-sm" id="path-export-png">\U0001f4f7 PNG</button>
|
| 432 |
</div>
|
| 433 |
</div>
|
| 434 |
|
| 435 |
<div class="toolbar" id="toolbar" style="display:none;">
|
| 436 |
<div class="toolbar-group">
|
| 437 |
+
<button class="btn btn-secondary btn-icon" id="undo-btn" title="Undo (Ctrl+Z)">\u21a9</button>
|
| 438 |
+
<button class="btn btn-secondary btn-icon" id="redo-btn" title="Redo (Ctrl+Shift+Z)">\u21aa</button>
|
| 439 |
<div class="toolbar-divider"></div>
|
| 440 |
+
<button class="btn btn-secondary btn-icon" id="fit-btn" title="Fit View (F)">\U0001f3af</button>
|
| 441 |
+
<button class="btn btn-secondary btn-icon" id="clear-btn" title="Clear (C)">\U0001f5d1</button>
|
| 442 |
<div class="toolbar-divider"></div>
|
| 443 |
+
<button class="btn btn-secondary" id="export-svg-btn">\U0001f5bc SVG</button>
|
| 444 |
+
<button class="btn btn-secondary" id="export-json-btn">\U0001f4c4 JSON</button>
|
| 445 |
+
<button class="btn btn-secondary" id="export-md-btn">\U0001f4dd Markdown</button>
|
| 446 |
+
<button class="btn btn-secondary" id="export-png-btn">\U0001f4f7 PNG</button>
|
| 447 |
</div>
|
| 448 |
<div class="stats">
|
| 449 |
<span>Nodes: <span class="highlight" id="stat-nodes">0</span></span>
|
|
|
|
| 454 |
</div>
|
| 455 |
|
| 456 |
<div id="context-menu">
|
| 457 |
+
<div class="ctx-item" data-action="expand">\U0001f50d Explore <span class="kbd">E</span></div>
|
| 458 |
+
<div class="ctx-item" data-action="regenerate">\U0001f504 Regenerate <span class="kbd">R</span></div>
|
| 459 |
+
<div class="ctx-item" data-action="add">\u2795 Add Options <span class="kbd">A</span></div>
|
| 460 |
<div class="ctx-divider"></div>
|
| 461 |
+
<div class="ctx-item" data-action="copy-path">\U0001f4cb Copy Path</div>
|
| 462 |
+
<div class="ctx-item" data-action="center">\U0001f3af Center Node</div>
|
| 463 |
</div>
|
| 464 |
|
| 465 |
<div id="loading-overlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:999;align-items:center;justify-content:center;flex-direction:column;">
|
|
|
|
| 475 |
|
| 476 |
<script>
|
| 477 |
// =============================================
|
| 478 |
+
// OVER THINKER — Gradio Server Backend
|
| 479 |
// - Dual prompts: Options for input, Outcomes for outcome
|
| 480 |
+
// - Model: nvidia/nemotron-3-nano-30b-a3b via OpenRouter
|
|
|
|
| 481 |
// =============================================
|
| 482 |
|
| 483 |
let nodes = [];
|
|
|
|
| 512 |
function hideLoading() { loadingOverlay.style.display = 'none'; }
|
| 513 |
|
| 514 |
function getNodeTypeLabel(type) {
|
| 515 |
+
const map = { 'root': '\U0001f333 Root', 'input': '\U0001f9e0 Input', 'outcome': '\U0001f4ca Outcome' };
|
| 516 |
+
return map[type] || '\U0001f4ca Outcome';
|
| 517 |
}
|
| 518 |
function getNextTypeLabel(type) {
|
| 519 |
const map = { 'root': 'Options/Choices', 'input': 'Outcomes', 'outcome': 'Options/Choices' };
|
|
|
|
| 526 |
|
| 527 |
function showToast(msg, type='info') {
|
| 528 |
const toast = document.getElementById('toast');
|
| 529 |
+
toast.textContent = '\u26a0\ufe0f ' + msg;
|
| 530 |
toast.className = 'toast ' + type;
|
| 531 |
toast.classList.add('show');
|
| 532 |
clearTimeout(toast._t);
|
|
|
|
| 735 |
g.select('.tile-type').text(getNodeTypeLabel(type).split(' ')[0]);
|
| 736 |
g.select('.tile-type').attr('class', 'tile-type type-' + type + '-label');
|
| 737 |
g.select('.tile-badge-depth').text('D'+(d.depth||0));
|
| 738 |
+
g.select('.tile-badge-children').text('\u21b3'+(d._childrenCount||0));
|
| 739 |
g.select('.tile-rect').attr('class', 'tile-rect type-' + type);
|
| 740 |
if (d.depth === 0) g.classed('root-pinned', true);
|
| 741 |
});
|
|
|
|
| 783 |
const typeLabel = getNodeTypeLabel(type);
|
| 784 |
let buttonsHTML = '';
|
| 785 |
if (!isExpanded) {
|
| 786 |
+
buttonsHTML = `<button class="${type === 'outcome' ? 'btn-explore-input' : 'btn-explore-outcome'}" id="fo-explore">\U0001f50d Explore ${nextLabel}</button>`;
|
| 787 |
} else {
|
| 788 |
+
buttonsHTML = `<button class="btn-regenerate" id="fo-regen">\U0001f504 Regenerate</button><button class="btn-add-options" id="fo-add">\u2795 Add Options</button>`;
|
| 789 |
}
|
| 790 |
const html = `
|
| 791 |
<div xmlns="http://www.w3.org/1999/xhtml" class="fo-popup-box">
|
| 792 |
+
<button class="fo-close-btn" id="fo-close">\u2715</button>
|
| 793 |
<h4>${escapeHtml(node._label || node.label || 'Node')}</h4>
|
| 794 |
<div class="type-badge type-${type}">${typeLabel}</div>
|
| 795 |
<div class="full-desc">${escapeHtml(node._description || node.description || '')}</div>
|
| 796 |
+
${node._tips && node._tips.length ? `<div style="font-size:0.72em;color:#7ec8a0;margin-bottom:8px;padding:6px;background:rgba(126,200,160,0.08);border-radius:4px;">\U0001f4a1 ${node._tips[0]}</div>` : ''}
|
| 797 |
<textarea id="fo-comment" placeholder="Your thought about this..."></textarea>
|
| 798 |
<div class="btn-row">${buttonsHTML}</div>
|
| 799 |
</div>
|
|
|
|
| 917 |
if (node._description) {
|
| 918 |
html += `<div class="card-desc">${escapeHtml(node._description).substring(0, 80)}${node._description.length > 80 ? '...' : ''}</div>`;
|
| 919 |
}
|
| 920 |
+
html += `<div class="card-tip ${hasTip ? 'show' : ''}">${hasTip ? '\U0001f4a1 ' + escapeHtml(node._tips[0]) : ''}</div>`;
|
| 921 |
html += '</div>';
|
| 922 |
+
if (!isLast) html += '<div class="path-arrow">\u2193</div>';
|
| 923 |
});
|
| 924 |
list.innerHTML = html;
|
| 925 |
document.getElementById('path-depth').textContent = path.length;
|
|
|
|
| 937 |
async function generateTree() {
|
| 938 |
const decision = inputEl.value.trim();
|
| 939 |
if (!decision) return;
|
| 940 |
+
generateBtn.textContent = '\u23f3 Overthinking...';
|
| 941 |
generateBtn.disabled = true;
|
| 942 |
showLoading('Creating decision root...');
|
| 943 |
try {
|
|
|
|
| 960 |
showToast('Generation failed: '+e.message, 'error');
|
| 961 |
} finally {
|
| 962 |
hideLoading();
|
| 963 |
+
generateBtn.textContent = '\U0001f333 Generate';
|
| 964 |
generateBtn.disabled = false;
|
| 965 |
document.getElementById('toolbar').style.display = 'flex';
|
| 966 |
}
|
|
|
|
| 1121 |
if (n) path.unshift(n._label);
|
| 1122 |
cur = parentMap.get(cur);
|
| 1123 |
}
|
| 1124 |
+
navigator.clipboard.writeText(path.join(' \u2192 ')).then(() => showToast('Path copied!','success'))
|
| 1125 |
.catch(() => showToast('Copy failed','error'));
|
| 1126 |
break;
|
| 1127 |
case 'center':
|
|
|
|
| 1136 |
});
|
| 1137 |
|
| 1138 |
// =============================================
|
| 1139 |
+
// EXPORT FUNCTIONS
|
| 1140 |
// =============================================
|
| 1141 |
|
| 1142 |
function computeTextLines(text, maxLen) {
|
|
|
|
| 1157 |
const maxDescLen = 35;
|
| 1158 |
const descLines = computeTextLines(node._description || '', maxDescLen);
|
| 1159 |
const cappedDescLines = Math.min(descLines, 3);
|
| 1160 |
+
const tipLines = computeTextLines((node._tips && node._tips[0]) ? '\U0001f4a1 ' + node._tips[0] : '', 35);
|
| 1161 |
const cappedTipLines = Math.min(tipLines, 2);
|
| 1162 |
let extraHeight = cappedDescLines * 13;
|
| 1163 |
if (cappedTipLines > 0) extraHeight += 4 + cappedTipLines * 11;
|
|
|
|
| 1241 |
if (converged) break;
|
| 1242 |
}
|
| 1243 |
|
|
|
|
| 1244 |
let minX = Infinity, maxX = -Infinity;
|
| 1245 |
Object.values(positions).forEach(x => { minX = Math.min(minX, x - CARD_W / 2); maxX = Math.max(maxX, x + CARD_W / 2); });
|
| 1246 |
const treeW = maxX - minX;
|
|
|
|
| 1247 |
let maxExportTileH = CARD_H;
|
| 1248 |
exportNodes.forEach(n => {
|
| 1249 |
const h = computePathTileHeight(n);
|
| 1250 |
if (h > maxExportTileH) maxExportTileH = h;
|
| 1251 |
});
|
|
|
|
| 1252 |
const minTotalW = Math.max(canvasW, treeW + 80);
|
| 1253 |
const totalW = minTotalW;
|
|
|
|
| 1254 |
const offsetX = (totalW - treeW) / 2 - minX;
|
| 1255 |
Object.keys(positions).forEach(id => { positions[id] += offsetX; });
|
| 1256 |
|
|
|
|
| 1257 |
let newMinX = Infinity, newMaxX = -Infinity;
|
| 1258 |
Object.values(positions).forEach(x => { newMinX = Math.min(newMinX, x - CARD_W / 2); newMaxX = Math.max(newMaxX, x + CARD_W / 2); });
|
| 1259 |
const finalTreeW = newMaxX - newMinX;
|
| 1260 |
const finalTotalW = Math.max(totalW, finalTreeW + 40);
|
| 1261 |
|
|
|
|
| 1262 |
const rowHeights = {};
|
| 1263 |
for (let d = 0; d <= maxDepth; d++) {
|
| 1264 |
let maxRowH = CARD_H;
|
|
|
|
| 1276 |
}
|
| 1277 |
totalH += 20;
|
| 1278 |
|
|
|
|
| 1279 |
exportNodes.forEach(n => {
|
|
|
|
|
|
|
|
|
|
| 1280 |
let yOffset = 0;
|
| 1281 |
+
for (let i = 0; i < (n.depth || 0); i++) {
|
| 1282 |
yOffset += rowHeights[i] + Y_PADDING;
|
| 1283 |
}
|
|
|
|
| 1284 |
n._exportX = positions[n.id] !== undefined ? positions[n.id] : canvasW / 2;
|
| 1285 |
+
n._exportY = ROOT_Y + yOffset + (rowHeights[n.depth || 0] || CARD_H) / 2;
|
| 1286 |
n._exportTileH = computePathTileHeight(n);
|
| 1287 |
});
|
| 1288 |
|
|
|
|
| 1291 |
|
| 1292 |
function computePathLayoutLinear(exportNodes, exportLinks, canvasW) {
|
| 1293 |
if (!exportNodes.length) return null;
|
|
|
|
| 1294 |
const GAP = 40;
|
| 1295 |
const nodeHeights = [];
|
| 1296 |
exportNodes.forEach((n, idx) => {
|
|
|
|
| 1394 |
const type = n._type || 'outcome';
|
| 1395 |
const [line1, line2] = splitLabel(n._label || '');
|
| 1396 |
|
|
|
|
| 1397 |
const txt1 = document.createElementNS(ns, 'text');
|
| 1398 |
txt1.setAttribute('x', '0');
|
| 1399 |
txt1.setAttribute('y', -(tileH/2) + 20);
|
|
|
|
| 1418 |
g.appendChild(txt2);
|
| 1419 |
}
|
| 1420 |
|
|
|
|
| 1421 |
const typeTxt = document.createElementNS(ns, 'text');
|
| 1422 |
typeTxt.setAttribute('x', '0');
|
| 1423 |
typeTxt.setAttribute('y', -(tileH/2) + 52);
|
|
|
|
| 1425 |
typeTxt.setAttribute('fill', typeFillColor);
|
| 1426 |
typeTxt.setAttribute('font-size', '9');
|
| 1427 |
typeTxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
|
| 1428 |
+
const typeLabels = { root: '\U0001f333 ROOT', input: '\U0001f9e0 INPUT', outcome: '\U0001f4ca OUTCOME' };
|
| 1429 |
+
typeTxt.textContent = typeLabels[type] || '\U0001f4ca OUTCOME';
|
| 1430 |
g.appendChild(typeTxt);
|
| 1431 |
|
|
|
|
| 1432 |
const maxDescLen = 35;
|
| 1433 |
const desc = n._description || '';
|
| 1434 |
if (desc.length > 0) {
|
|
|
|
| 1457 |
});
|
| 1458 |
}
|
| 1459 |
|
|
|
|
| 1460 |
const tip = (n._tips && n._tips.length) ? n._tips[0] : '';
|
| 1461 |
if (tip) {
|
| 1462 |
const tipLines = [];
|
| 1463 |
+
let remainingTip = '\U0001f4a1 ' + tip;
|
| 1464 |
const maxTipLen = 35;
|
| 1465 |
while (remainingTip.length > 0 && tipLines.length < 2) {
|
| 1466 |
if (remainingTip.length <= maxTipLen) {
|
|
|
|
| 1503 |
chTxt.setAttribute('fill', isDark ? '#7ecfff' : '#2e86ab');
|
| 1504 |
chTxt.setAttribute('font-size', '10');
|
| 1505 |
chTxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
|
| 1506 |
+
chTxt.textContent = '\u21b3' + (n._childrenCount || 0);
|
| 1507 |
g.appendChild(chTxt);
|
| 1508 |
|
| 1509 |
svgDoc.appendChild(g);
|
|
|
|
| 1518 |
title.setAttribute('font-weight', '700');
|
| 1519 |
title.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
|
| 1520 |
const rootLabel = exportNodes.find(n => n.depth === 0)?._label || 'Decision Tree';
|
| 1521 |
+
title.textContent = isPathOnly ? `\U0001f4cd Path: ${rootLabel}` : `\U0001f9e0 ${rootLabel}`;
|
| 1522 |
svgDoc.appendChild(title);
|
| 1523 |
|
| 1524 |
return svgDoc;
|
|
|
|
| 1581 |
|
| 1582 |
function exportMarkdown() {
|
| 1583 |
if (!nodes.length) { showToast('Generate a tree first', 'error'); return; }
|
| 1584 |
+
let md = '# \U0001f9e0 Overthinker Decision Tree\n\n';
|
| 1585 |
const root = nodes.find(n => n.depth === 0);
|
| 1586 |
if (root) {
|
| 1587 |
+
md += `## \U0001f333 Root Decision\n**${root._label}**\n\n`;
|
| 1588 |
if (root._description) md += `> ${root._description}\n\n`;
|
| 1589 |
}
|
| 1590 |
function collectMd(nodeId, depth) {
|
| 1591 |
const indent = ' '.repeat(depth);
|
| 1592 |
const node = nodes.find(n => n.id === nodeId);
|
| 1593 |
if (!node) return;
|
| 1594 |
+
const emoji = node._type === 'root' ? '\U0001f333' : node._type === 'outcome' ? '\U0001f4ca' : '\U0001f9e0';
|
| 1595 |
md += `${indent}- ${emoji} **[${node._type.toUpperCase()}] ${node._label}**\n`;
|
| 1596 |
if (node._description) md += `${indent} - _${node._description.substring(0, 100)}_\n`;
|
| 1597 |
+
if (node._tips && node._tips.length) md += `${indent} - \U0001f4a1 ${node._tips[0]}\n`;
|
| 1598 |
md += '\n';
|
| 1599 |
const childIds = [];
|
| 1600 |
parentMap.forEach((pid, cid) => { if (pid === nodeId) childIds.push(cid); });
|
|
|
|
| 1741 |
function toggleTheme() {
|
| 1742 |
isDark = !isDark;
|
| 1743 |
document.body.classList.toggle('light', !isDark);
|
| 1744 |
+
document.getElementById('theme-btn').textContent = isDark ? '\U0001f313' : '\U0001f31e';
|
| 1745 |
+
localStorage.setItem('overthinker_theme', isDark ? 'dark' : 'light');
|
| 1746 |
}
|
| 1747 |
document.getElementById('theme-btn').addEventListener('click', toggleTheme);
|
| 1748 |
|
| 1749 |
+
// ---------- INFO / HELP DROPDOWN ----------
|
| 1750 |
+
document.getElementById('info-btn').addEventListener('click', () => {
|
| 1751 |
+
document.getElementById('info-dropdown').classList.add('show');
|
| 1752 |
+
});
|
| 1753 |
+
document.getElementById('help-btn').addEventListener('click', () => {
|
| 1754 |
+
document.getElementById('help-dropdown').classList.add('show');
|
| 1755 |
+
});
|
| 1756 |
+
document.getElementById('info-close').addEventListener('click', () => {
|
| 1757 |
+
document.getElementById('info-dropdown').classList.remove('show');
|
| 1758 |
+
});
|
| 1759 |
+
document.getElementById('help-close').addEventListener('click', () => {
|
| 1760 |
+
document.getElementById('help-dropdown').classList.remove('show');
|
| 1761 |
+
});
|
| 1762 |
+
// Close on overlay click
|
| 1763 |
+
document.querySelectorAll('.dropdown-overlay').forEach(el => {
|
| 1764 |
+
el.addEventListener('click', (e) => {
|
| 1765 |
+
if (e.target === el) el.classList.remove('show');
|
| 1766 |
+
});
|
| 1767 |
+
});
|
| 1768 |
+
|
| 1769 |
// ---------- SAMPLE ----------
|
| 1770 |
document.getElementById('sample-btn').addEventListener('click', () => {
|
| 1771 |
const samples = [
|
|
|
|
| 1783 |
|
| 1784 |
// ---------- INIT ----------
|
| 1785 |
initSvg();
|
| 1786 |
+
if (localStorage.getItem('overthinker_theme') === 'light') toggleTheme();
|
| 1787 |
|
| 1788 |
+
console.log('\U0001f9e0 Overthinker \u2014 Production Ready');
|
| 1789 |
</script>
|
| 1790 |
</body>
|
| 1791 |
</html>
|