broadfield-dev commited on
Commit
23e86f2
·
verified ·
1 Parent(s): 7033a2e

Update templates/index.html

Browse files
Files changed (1) hide show
  1. 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 v20 — 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,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>🧠 Overthinker <span>v20</span></h1></div>
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">🌓</button>
283
- <button class="btn btn-secondary" id="onboarding-btn" title="Show Tour">❓</button>
 
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">🌳 Generate</button>
291
- <button class="btn btn-secondary sample-btn" id="sample-btn">🎯 Sample</button>
292
  </div>
293
  </div>
294
 
@@ -298,12 +340,82 @@ body.light #graph-container { background: #f0f4f8; }
298
 
299
  <div id="ensurePngContainer"></div>
300
 
301
- <button id="sidebar-tab">📍 Path</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
  <div id="path-sidebar">
304
  <div id="sidebar-header">
305
- <h3>📍 Decision Path</h3>
306
- <button id="sidebar-close">✕</button>
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;">📤 Export Path:</span>
316
- <button class="btn btn-secondary btn-sm" id="path-export-svg">🖼 SVG</button>
317
- <button class="btn btn-secondary btn-sm" id="path-export-json">📄 JSON</button>
318
- <button class="btn btn-secondary btn-sm" id="path-export-md">📝 MD</button>
319
- <button class="btn btn-secondary btn-sm" id="path-export-png">📷 PNG</button>
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)">↩</button>
326
- <button class="btn btn-secondary btn-icon" id="redo-btn" title="Redo (Ctrl+Shift+Z)">↪</button>
327
  <div class="toolbar-divider"></div>
328
- <button class="btn btn-secondary btn-icon" id="fit-btn" title="Fit View (F)">🎯</button>
329
- <button class="btn btn-secondary btn-icon" id="clear-btn" title="Clear (C)">🗑</button>
330
  <div class="toolbar-divider"></div>
331
- <button class="btn btn-secondary" id="export-svg-btn">🖼 SVG</button>
332
- <button class="btn btn-secondary" id="export-json-btn">📄 JSON</button>
333
- <button class="btn btn-secondary" id="export-md-btn">📝 Markdown</button>
334
- <button class="btn btn-secondary" id="export-png-btn">📷 PNG</button>
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">🔍 Explore <span class="kbd">E</span></div>
346
- <div class="ctx-item" data-action="regenerate">🔄 Regenerate <span class="kbd">R</span></div>
347
- <div class="ctx-item" data-action="add">➕ Add Options <span class="kbd">A</span></div>
348
  <div class="ctx-divider"></div>
349
- <div class="ctx-item" data-action="copy-path">📋 Copy Path</div>
350
- <div class="ctx-item" data-action="center">🎯 Center Node</div>
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 v20 — Gradio Server Backend
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': '🌳 Root', 'input': '🧠 Input', 'outcome': '📊 Outcome' };
405
- return map[type] || '📊 Outcome';
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 = '⚠️ ' + msg;
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('↳'+(d._childrenCount||0));
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">🔍 Explore ${nextLabel}</button>`;
676
  } else {
677
- buttonsHTML = `<button class="btn-regenerate" id="fo-regen">🔄 Regenerate</button><button class="btn-add-options" id="fo-add">➕ Add Options</button>`;
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">✕</button>
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;">💡 ${node._tips[0]}</div>` : ''}
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 ? '💡 ' + escapeHtml(node._tips[0]) : ''}</div>`;
810
  html += '</div>';
811
- if (!isLast) html += '<div class="path-arrow">↓</div>';
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 = '⏳ Overthinking...';
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 = '🌳 Generate';
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(' → ')).then(() => showToast('Path copied!','success'))
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 — v20 (Fixed centering + root description/tips for full tree)
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]) ? '💡 ' + node._tips[0] : '', 35);
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 < d; 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 = nodeY;
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: '🌳 ROOT', input: '🧠 INPUT', outcome: '📊 OUTCOME' };
1332
- typeTxt.textContent = typeLabels[type] || '📊 OUTCOME';
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 = '💡 ' + tip;
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 = '↳' + (n._childrenCount || 0);
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 ? `📍 Path: ${rootLabel}` : `🧠 ${rootLabel}`;
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 = '# 🧠 Overthinker Decision Tree\n\n';
1490
  const root = nodes.find(n => n.depth === 0);
1491
  if (root) {
1492
- md += `## 🌳 Root Decision\n**${root._label}**\n\n`;
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' ? '🌳' : node._type === 'outcome' ? '📊' : '🧠';
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} - 💡 ${node._tips[0]}\n`;
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('overthinker_v20_theme', isDark ? 'dark' : 'light');
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('overthinker_v20_theme') === 'light') toggleTheme();
1672
 
1673
- console.log('🧠 Overthinker v20 — Fixed export centering + root description/tips');
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>