broadfield-dev commited on
Commit
2116fdd
Β·
verified Β·
1 Parent(s): ea588b8

Upload index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +1676 -0
templates/index.html ADDED
@@ -0,0 +1,1676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
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; }
10
+ body {
11
+ font-family:'Segoe UI',system-ui,sans-serif;
12
+ background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
13
+ color:#e0e0e0; overflow:hidden; height:100vh; display:flex; flex-direction:column;
14
+ }
15
+ body.light { background: #f5f7fa; color: #2c2c2c; }
16
+ body.light .top-bar { background: rgba(255,255,255,0.85); border-bottom: 1px solid rgba(0,0,0,0.08); }
17
+ body.light .prompt-bar { background: rgba(255,255,255,0.5); border-bottom: 1px solid rgba(0,0,0,0.06); }
18
+ body.light .prompt-bar textarea { background: rgba(255,255,255,0.8); color: #333; border-color: rgba(0,0,0,0.15); }
19
+ body.light .btn-secondary { background: rgba(0,0,0,0.05); border-color: rgba(0,0,0,0.12); color: #444; }
20
+ body.light .btn-secondary:hover { background: rgba(0,0,0,0.1); }
21
+ body.light .tile-rect { fill: rgba(255,255,255,0.92) !important; stroke: rgba(0,0,0,0.13) !important; }
22
+ body.light .tile-label-line1,
23
+ body.light .tile-label-line2 { fill: #d35400 !important; }
24
+ body.light .tile-type { fill: #555 !important; }
25
+ body.light .tile-badge-depth { fill: #7c5cbf !important; }
26
+ body.light .tile-badge-children { fill: #2e86ab !important; }
27
+ body.light .link-line { stroke: rgba(0,0,0,0.2) !important; }
28
+ body.light .link-line.path-highlight { stroke: #e67e22 !important; }
29
+ body.light .sidebar { background: rgba(255,255,255,0.96); border-left: 1px solid rgba(0,0,0,0.08); }
30
+ body.light .sidebar textarea { background: rgba(0,0,0,0.05); border: 1px solid rgba(0,0,0,0.12); color: #333; }
31
+ body.light .toolbar { background: rgba(255,255,255,0.7); border-top: 1px solid rgba(0,0,0,0.06); }
32
+ body.light .stats span { background: rgba(0,0,0,0.04); color: #555; }
33
+ body.light .stats .highlight { color: #e67e22; }
34
+ body.light .context-menu { background: rgba(255,255,255,0.97); border: 1px solid rgba(0,0,0,0.12); }
35
+ body.light .context-menu-item:hover { background: rgba(0,0,0,0.06); }
36
+ body.light .onboarding-card { background: rgba(255,255,255,0.95); border: 1px solid rgba(0,0,0,0.1); }
37
+ body.light .onboarding-card p { color: #555; }
38
+ body.light .path-card { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.1); }
39
+ body.light .path-card:hover { background: rgba(0,0,0,0.08); }
40
+ body.light .path-card .card-desc { color: #555; }
41
+ body.light .path-card .card-tip { background: rgba(0,0,0,0.04); }
42
+ body.light .fo-popup-box { background: rgba(255,255,255,0.96); color: #333; border-color: rgba(0,0,0,0.15); }
43
+ body.light .fo-popup-box h4 { color: #d35400; }
44
+ body.light .fo-popup-box .full-desc { color: #555; }
45
+ body.light .fo-popup-box textarea { background: rgba(0,0,0,0.05); border-color: rgba(0,0,0,0.15); color: #333; }
46
+ body.light #graph-container { background: #f0f4f8; }
47
+
48
+ .top-bar {
49
+ display: flex; align-items: center; justify-content: space-between;
50
+ padding: 10px 28px; background: rgba(0,0,0,0.4);
51
+ border-bottom: 1px solid rgba(255,255,255,0.06);
52
+ backdrop-filter: blur(14px); flex-shrink:0; z-index:10; flex-wrap: wrap; gap:8px;
53
+ }
54
+ .logo h1 {
55
+ font-weight: 600; font-size: 1.3rem; letter-spacing: 0.5px;
56
+ background: linear-gradient(120deg, #f7971e, #ffd200);
57
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
58
+ }
59
+ .logo span { font-size: 0.7rem; color: #888; letter-spacing: 1px; margin-left: 4px; }
60
+ .top-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
61
+ .top-controls label { font-size: 0.75rem; color: #bbb; margin-right: 1px; }
62
+ .top-controls input[type="number"] {
63
+ width: 48px; background: rgba(255,255,255,0.06);
64
+ border: 1px solid rgba(255,255,255,0.15); border-radius: 6px;
65
+ padding: 5px 4px; color: #fff; text-align: center; font-size: 0.8rem;
66
+ }
67
+ .top-controls input[type="number"]:focus { border-color: #f7971e; outline: none; }
68
+ .btn {
69
+ padding: 6px 16px; border-radius: 8px; border: none;
70
+ font-weight: 600; font-size: 0.78rem; cursor: pointer;
71
+ transition: all 0.2s ease; white-space: nowrap;
72
+ }
73
+ .btn-primary { background: linear-gradient(135deg, #f7971e, #ffd200); color: #1a1a2e; }
74
+ .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(247,151,30,0.35); }
75
+ .btn-secondary { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); color: #ddd; }
76
+ .btn-secondary:hover { background: rgba(255,255,255,0.14); }
77
+ .btn-icon { padding: 6px 10px; font-size: 1.1rem; line-height: 1; min-width: 32px; text-align: center; }
78
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; }
79
+ .btn-sm { padding: 3px 10px; font-size: 0.7rem; }
80
+
81
+ .prompt-bar {
82
+ display: flex; gap: 10px; padding: 12px 28px; align-items: center;
83
+ justify-content: center; flex-wrap: wrap; background: rgba(0,0,0,0.2);
84
+ border-bottom: 1px solid rgba(255,255,255,0.04); flex-shrink:0;
85
+ }
86
+ .prompt-bar .input-group { display: flex; gap: 8px; flex: 1; max-width: 700px; min-width: 280px; }
87
+ .prompt-bar textarea {
88
+ flex: 1; background: rgba(0,0,0,0.45);
89
+ border: 1px solid rgba(255,255,255,0.1); border-radius: 10px;
90
+ padding: 10px 14px; color: #fff; font-size: 0.88rem;
91
+ resize: vertical; min-height: 42px; max-height: 80px; outline: none;
92
+ }
93
+ .prompt-bar textarea:focus { border-color: #f7971e; }
94
+ .prompt-bar .btn-generate { padding: 10px 24px; font-size: 0.85rem; }
95
+ .prompt-bar .sample-btn { padding: 6px 12px; font-size: 0.72rem; }
96
+
97
+ #graph-container {
98
+ flex:1; position:relative; overflow:hidden; cursor:grab;
99
+ background: radial-gradient(ellipse at center, #1a1a3a 0%, #0f0c29 70%);
100
+ }
101
+ #graph-container:active { cursor:grabbing; }
102
+ #graph-svg { width:100%; height:100%; }
103
+ .node-group { cursor:grab; }
104
+ .node-group:active { cursor:grabbing; }
105
+ .node-group.selected .tile-rect { stroke:#ffd700; stroke-width:3px; filter:drop-shadow(0 0 6px #ffd700); }
106
+ .node-group.path-highlight .tile-rect { stroke:#ffd200; stroke-width:2.5px; filter:drop-shadow(0 0 8px rgba(255,210,0,0.5)); }
107
+ .node-group.root-pinned .tile-rect { stroke:#ffd700; stroke-width:3px; filter:drop-shadow(0 0 8px rgba(255,215,0,0.6)); }
108
+ .tile-rect.type-root { fill:rgba(25,25,55,0.92); stroke:#ffd700; stroke-width:2.5px; filter:drop-shadow(0 0 6px rgba(255,215,0,0.3)); }
109
+ .tile-rect.type-input { fill:rgba(25,25,55,0.92); stroke:#ff6b9d; stroke-width:2px; filter:drop-shadow(0 0 4px rgba(255,107,157,0.2)); }
110
+ .tile-rect.type-outcome { fill:rgba(25,25,55,0.92); stroke:#00d4ff; stroke-width:2px; filter:drop-shadow(0 0 4px rgba(0,212,255,0.2)); }
111
+ .tile-label-line1,
112
+ .tile-label-line2 { fill:#ffd700; font-size:12px; font-weight:700; pointer-events:none; text-anchor:middle; }
113
+ .tile-type { font-size:9px; pointer-events:none; text-anchor:middle; }
114
+ .tile-type.type-root-label { fill:#ffd700; }
115
+ .tile-type.type-input-label { fill:#ff6b9d; }
116
+ .tile-type.type-outcome-label { fill:#00d4ff; }
117
+ .tile-badge-depth { fill:#c9aaff; font-size:10px; pointer-events:none; text-anchor:start; }
118
+ .tile-badge-children { fill:#7ecfff; font-size:10px; pointer-events:none; text-anchor:end; }
119
+ .link-line { stroke:rgba(255,210,0,0.2); stroke-width:1.5px; fill:none; transition:stroke-width 0.2s,stroke 0.2s; }
120
+ .link-line.path-highlight { stroke:#ffd700; stroke-width:3px; filter:drop-shadow(0 0 4px #ffd700); }
121
+
122
+ .fo-popup-box {
123
+ background:rgba(20,16,40,0.96);
124
+ border:2px solid #ffd700;
125
+ border-radius:10px;
126
+ padding:12px;
127
+ width:300px;
128
+ max-height:480px;
129
+ overflow-y:auto;
130
+ box-shadow:0 8px 32px rgba(0,0,0,0.6);
131
+ font-family:'Segoe UI',system-ui,sans-serif;
132
+ color:#e0e0e0;
133
+ pointer-events:all;
134
+ user-select:text;
135
+ }
136
+ .fo-popup-box h4 { color:#ffd700; margin-bottom:6px; font-size:0.95em; padding-right:20px; }
137
+ .fo-popup-box .type-badge { display:inline-block; padding:2px 6px; border-radius:3px; font-size:0.7em; margin-bottom:6px; font-weight:700; }
138
+ .fo-popup-box .type-badge.type-root { background:rgba(255,215,0,0.15); color:#ffd700; }
139
+ .fo-popup-box .type-badge.type-input { background:rgba(255,107,157,0.15); color:#ff6b9d; }
140
+ .fo-popup-box .type-badge.type-outcome { background:rgba(0,212,255,0.15); color:#00d4ff; }
141
+ .fo-popup-box .full-desc { color:#c0c0c0; font-size:0.8em; line-height:1.4; margin-bottom:10px; white-space:pre-wrap; max-height:120px; overflow-y:auto; }
142
+ .fo-popup-box textarea { width:100%; padding:8px; border-radius:6px; border:1px solid rgba(255,255,255,0.15); background:rgba(0,0,0,0.3); color:#e0e0e0; font-size:0.8em; resize:vertical; min-height:50px; outline:none; }
143
+ .fo-popup-box textarea:focus { border-color:#ffd700; }
144
+ .fo-popup-box .btn-row { display:flex; gap:6px; margin-top:8px; flex-wrap:wrap; }
145
+ .fo-popup-box .btn-row button { flex:1; min-width:80px; padding:6px 10px; border-radius:5px; border:none; cursor:pointer; font-weight:600; font-size:0.75em; }
146
+ .btn-explore-outcome { background:#00d4ff; color:#1a1a2e; }
147
+ .btn-explore-outcome:hover { background:#00b8e0; }
148
+ .btn-explore-input { background:#ff6b9d; color:#1a1a2e; }
149
+ .btn-explore-input:hover { background:#e05a88; }
150
+ .btn-regenerate { background:#5b8cc5; color:#fff; }
151
+ .btn-regenerate:hover { background:#4a7ab5; }
152
+ .btn-add-options { background:#7ec8a0; color:#1a1a2e; }
153
+ .btn-add-options:hover { background:#5ea87e; }
154
+ .fo-close-btn { position:absolute; top:4px; right:8px; background:none; border:none; color:#c9aaff; font-size:16px; cursor:pointer; z-index:5; }
155
+
156
+ #sidebar-tab {
157
+ position: fixed; top: 50%; right: 0; transform: translateY(-50%);
158
+ background: rgba(247,151,30,0.9); color: #1a1a2e;
159
+ border: none; border-radius: 8px 0 0 8px;
160
+ padding: 10px 6px; cursor: pointer; z-index: 1000;
161
+ font-size: 1rem; font-weight: bold; writing-mode: vertical-rl;
162
+ text-orientation: mixed; letter-spacing: 2px;
163
+ box-shadow: -2px 0 8px rgba(0,0,0,0.3);
164
+ transition: transform 0.35s cubic-bezier(0.22,0.61,0.36,1), background 0.2s;
165
+ user-select: none;
166
+ }
167
+ #sidebar-tab:hover { background: #ffd200; }
168
+ #sidebar-tab.hidden { transform: translate(340px, -50%); }
169
+
170
+ #path-sidebar {
171
+ position:fixed; top:0; right:0; width:340px; height:100vh;
172
+ background:rgba(10,10,35,0.97);
173
+ border-left:1px solid rgba(255,255,255,0.08);
174
+ backdrop-filter:blur(20px);
175
+ transform:translateX(100%);
176
+ transition:transform 0.35s cubic-bezier(0.22,0.61,0.36,1);
177
+ z-index:999; display:flex; flex-direction:column;
178
+ box-shadow:-4px 0 20px rgba(0,0,0,0.4);
179
+ }
180
+ #path-sidebar.open { transform:translateX(0); }
181
+ #sidebar-header {
182
+ padding:14px 18px; border-bottom:1px solid rgba(255,255,255,0.06);
183
+ display:flex; justify-content:space-between; align-items:center; flex-shrink:0;
184
+ }
185
+ #sidebar-header h3 { color:#ffd700; font-size:0.9em; }
186
+ #sidebar-close { background:none; border:none; color:#c9aaff; font-size:1.2em; cursor:pointer; padding:4px; }
187
+ #sidebar-close:hover { color:#ffd700; }
188
+ #path-list { flex:1; overflow-y:auto; padding:8px 12px; }
189
+ .path-card {
190
+ background:rgba(25,25,55,0.85); border:1px solid rgba(255,255,255,0.08); border-radius:8px;
191
+ padding:8px 10px; margin-bottom:6px; cursor:pointer;
192
+ transition:background 0.2s, border-color 0.2s; position:relative;
193
+ }
194
+ .path-card:hover { background:rgba(35,35,75,0.9); border-color:#ffd700; }
195
+ .path-card.active { border-color:#ffd700; background:rgba(35,35,75,0.9); }
196
+ .path-card .card-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:4px; }
197
+ .path-card .card-label { color:#e0e0e0; font-size:0.78em; font-weight:600; word-break:break-word; }
198
+ .path-card .card-type { font-size:0.65em; padding:1px 5px; border-radius:3px; font-weight:600; }
199
+ .path-card .card-type.root-type { background:rgba(255,215,0,0.15); color:#ffd700; }
200
+ .path-card .card-type.input-type { background:rgba(255,107,157,0.15); color:#ff6b9d; }
201
+ .path-card .card-type.outcome-type { background:rgba(0,212,255,0.15); color:#00d4ff; }
202
+ .path-card .card-desc { font-size:0.7em; color:#aaa; line-height:1.3; margin-bottom:4px; }
203
+ .path-card .card-tip { font-size:0.68em; color:#7ec8a0; background:rgba(126,200,160,0.08); padding:4px 6px; border-radius:4px; margin-top:4px; display:none; }
204
+ .path-card .card-tip.show { display:block; }
205
+ .path-arrow { color:rgba(255,255,255,0.15); text-align:center; font-size:0.7em; margin:-2px 0; }
206
+ #sidebar-summary {
207
+ padding:8px 16px; border-top:1px solid rgba(255,255,255,0.06);
208
+ font-size:0.7em; color:#aaa; flex-shrink:0;
209
+ }
210
+ #sidebar-export-row {
211
+ display: flex; gap: 4px; padding: 6px 12px;
212
+ border-top: 1px solid rgba(255,255,255,0.06); flex-shrink:0;
213
+ justify-content: center; flex-wrap: wrap;
214
+ }
215
+ #sidebar-export-row .btn { font-size: 0.65rem; padding: 3px 8px; }
216
+
217
+ .toolbar {
218
+ display: flex; gap: 10px; justify-content: center; align-items: center;
219
+ padding: 8px 20px; background: rgba(0,0,0,0.35);
220
+ border-top: 1px solid rgba(255,255,255,0.04); flex-shrink:0; z-index:50;
221
+ flex-wrap: wrap;
222
+ }
223
+ .toolbar .toolbar-group { display: flex; gap: 4px; align-items: center; }
224
+ .toolbar .toolbar-divider { width: 1px; height: 24px; background: rgba(255,255,255,0.1); margin: 0 4px; }
225
+ .stats { display: inline-flex; gap: 8px; align-items: center; font-size: 0.72rem; color: #aaa; }
226
+ .stats span { background: rgba(255,255,255,0.04); padding: 3px 10px; border-radius: 12px; }
227
+ .stats .highlight { color: #ffd700; font-weight: 600; }
228
+
229
+ #context-menu {
230
+ position: fixed; z-index: 300; background: rgba(15,12,41,0.98);
231
+ border: 1px solid rgba(255,255,255,0.1); border-radius: 10px;
232
+ padding: 6px 0; min-width: 190px; backdrop-filter: blur(12px);
233
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6); display: none;
234
+ }
235
+ #context-menu.visible { display: block; }
236
+ .ctx-item { padding:8px 16px; font-size:0.8em; cursor:pointer; color:#c0c0c0; display:flex; align-items:center; gap:8px; }
237
+ .ctx-item:hover { background:rgba(247,151,30,0.15); color:#ffd700; }
238
+ .ctx-item .kbd { margin-left:auto; font-size:0.65rem; color:#666; background:rgba(255,255,255,0.06); padding:1px 6px; border-radius:4px; }
239
+ .ctx-divider { height:1px; background:rgba(255,255,255,0.06); margin:4px 0; }
240
+
241
+ .toast {
242
+ position: fixed; top: 60px; right: 20px; z-index: 600;
243
+ padding: 12px 20px; border-radius: 10px; background: rgba(20,16,40,0.95);
244
+ border: 1px solid rgba(255,255,255,0.1); backdrop-filter: blur(10px);
245
+ color: #e0e0e0; font-size: 0.82rem; transform: translateX(120%);
246
+ transition: transform 0.3s ease; max-width: 320px;
247
+ }
248
+ .toast.show { transform: translateX(0); }
249
+ .toast.success { border-color: #2ecc71; }
250
+ .toast.error { border-color: #e74c3c; }
251
+ .toast.info { border-color: #3498db; }
252
+
253
+ @media (max-width:800px) {
254
+ .top-bar { padding:8px 14px; }
255
+ .prompt-bar { padding:8px 14px; }
256
+ .prompt-bar .input-group { min-width:100%; }
257
+ .toolbar { padding:6px 10px; gap:4px; }
258
+ .top-controls label { display:none; }
259
+ .logo h1 { font-size:1rem; }
260
+ #path-sidebar { width:100%; }
261
+ #sidebar-tab { padding: 8px 4px; font-size: 0.8rem; }
262
+ }
263
+
264
+ #ensurePngContainer {
265
+ position: absolute;
266
+ left: -9999px;
267
+ top: 0;
268
+ width: 1px;
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
+
295
+ <div id="graph-container">
296
+ <svg id="graph-svg"></svg>
297
+ </div>
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>
310
+ </div>
311
+ <div id="sidebar-summary">
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>
338
+ <span>Depth: <span class="highlight" id="stat-depth">0</span></span>
339
+ <span>Leaves: <span class="highlight" id="stat-leaves">0</span></span>
340
+ <span>RAM: <span class="highlight" id="stat-ram">0</span> MB</span>
341
+ </div>
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;">
354
+ <div class="spinner" style="width:40px;height:40px;border:4px solid rgba(255,255,255,0.1);border-top-color:#ffd700;border-radius:50%;animation:spin 0.8s linear infinite;margin-bottom:16px;"></div>
355
+ <p id="loading-text" style="color:#ffd700;font-size:0.9em;">Loading...</p>
356
+ </div>
357
+
358
+ <div class="toast" id="toast"></div>
359
+
360
+ <style>
361
+ @keyframes spin { to { transform: rotate(360deg); } }
362
+ </style>
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 = [];
373
+ let links = [];
374
+ let selectedNodeId = null;
375
+ let parentMap = new Map();
376
+ let expandedSet = new Set();
377
+ let treeId = null;
378
+ let svg, gLinks, gNodes, gMain, zoomBehavior;
379
+ let foreignPopup = null;
380
+ let isDark = true;
381
+ let historyStack = { undo: [], redo: [] };
382
+ let treeDataRoot = null;
383
+
384
+ const CARD_W = 230, CARD_H = 130;
385
+ const HEAVY_W = 300, HEAVY_H = 480;
386
+ const MIN_HORIZONTAL_GAP = 40;
387
+ const VERTICAL_SPACING = 200;
388
+ const ROOT_Y = 80;
389
+
390
+ const container = document.getElementById('graph-container');
391
+ const inputEl = document.getElementById('decision-input');
392
+ const generateBtn = document.getElementById('generate-btn');
393
+ const branchInput = document.getElementById('branch-input');
394
+ const loadingOverlay = document.getElementById('loading-overlay');
395
+ const loadingText = document.getElementById('loading-text');
396
+
397
+ function showLoading(msg) {
398
+ loadingOverlay.style.display = 'flex';
399
+ loadingText.textContent = msg;
400
+ }
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' };
409
+ return map[type] || 'Outcomes';
410
+ }
411
+ function getNextType(current) {
412
+ const map = { 'root': 'input', 'input': 'outcome', 'outcome': 'input' };
413
+ return map[current] || 'outcome';
414
+ }
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);
422
+ toast._t = setTimeout(() => toast.classList.remove('show'), 3000);
423
+ }
424
+
425
+ function escapeHtml(str) {
426
+ if (!str) return '';
427
+ const div = document.createElement('div');
428
+ div.textContent = str;
429
+ return div.innerHTML;
430
+ }
431
+
432
+ function splitLabel(label) {
433
+ const maxLen = 22;
434
+ if (!label) return ['', ''];
435
+ if (label.length <= maxLen) return [label, ''];
436
+ let splitIdx = label.lastIndexOf(' ', Math.min(maxLen, label.length));
437
+ if (splitIdx < 5) splitIdx = maxLen;
438
+ return [label.substring(0, splitIdx).trim(), label.substring(splitIdx).trim()];
439
+ }
440
+
441
+ // ---------- LAYOUT ----------
442
+ function computeTreeLayout() {
443
+ if (nodes.length === 0) return;
444
+ const childrenMap = {};
445
+ nodes.forEach(n => { childrenMap[n.id] = []; });
446
+ parentMap.forEach((parentId, childId) => {
447
+ if (childrenMap[parentId]) childrenMap[parentId].push(childId);
448
+ });
449
+ const rootId = nodes.find(n => n._type === 'root')?.id || nodes[0].id;
450
+ const depthGroups = {};
451
+ nodes.forEach(n => {
452
+ const d = n.depth || 0;
453
+ if (!depthGroups[d]) depthGroups[d] = [];
454
+ depthGroups[d].push(n);
455
+ });
456
+ const maxDepth = Math.max(...Object.keys(depthGroups).map(Number), 0);
457
+ const canvasW = container.clientWidth;
458
+
459
+ const subtreeWidth = {};
460
+ for (let d = maxDepth; d >= 0; d--) {
461
+ (depthGroups[d] || []).forEach(node => {
462
+ const kids = childrenMap[node.id] || [];
463
+ if (kids.length === 0) {
464
+ subtreeWidth[node.id] = CARD_W + MIN_HORIZONTAL_GAP;
465
+ } else {
466
+ let total = kids.reduce((sum, cid) => sum + (subtreeWidth[cid] || (CARD_W + MIN_HORIZONTAL_GAP)), 0);
467
+ total = Math.max(CARD_W + MIN_HORIZONTAL_GAP, total + MIN_HORIZONTAL_GAP * (kids.length - 1));
468
+ subtreeWidth[node.id] = total;
469
+ }
470
+ });
471
+ }
472
+
473
+ const positions = {};
474
+ const root = nodes.find(n => n.id === rootId);
475
+ if (root) positions[rootId] = canvasW / 2;
476
+ function positionChildren(nodeId, parentX, availableW) {
477
+ const kids = childrenMap[nodeId] || [];
478
+ if (kids.length === 0) return;
479
+ const totalChildW = kids.reduce((sum, cid) => sum + (subtreeWidth[cid] || (CARD_W + MIN_HORIZONTAL_GAP)), 0);
480
+ const extraSpace = Math.max(0, availableW - totalChildW);
481
+ const gap = kids.length > 1 ? extraSpace / (kids.length - 1) : 0;
482
+ let xCursor = parentX - totalChildW / 2;
483
+ kids.forEach(cid => {
484
+ const sw = subtreeWidth[cid] || (CARD_W + MIN_HORIZONTAL_GAP);
485
+ positions[cid] = xCursor + sw / 2;
486
+ xCursor += sw + gap;
487
+ positionChildren(cid, positions[cid], sw);
488
+ });
489
+ }
490
+ if (root) positionChildren(rootId, positions[rootId], subtreeWidth[rootId] || canvasW);
491
+
492
+ const ITER = 50;
493
+ for (let iter = 0; iter < ITER; iter++) {
494
+ let converged = true;
495
+ for (let d = 0; d <= maxDepth; d++) {
496
+ const group = [...(depthGroups[d] || [])].sort((a, b) => (positions[a.id] || 0) - (positions[b.id] || 0));
497
+ for (let i = 0; i < group.length - 1; i++) {
498
+ const a = group[i], b = group[i + 1];
499
+ if (!a || !b) continue;
500
+ const xA = positions[a.id] || 0;
501
+ const xB = positions[b.id] || 0;
502
+ const minDist = CARD_W + MIN_HORIZONTAL_GAP;
503
+ if (xB - xA < minDist) {
504
+ const shift = minDist - (xB - xA);
505
+ positions[b.id] += shift;
506
+ let anc = parentMap.get(b.id);
507
+ while (anc && positions[anc] !== undefined) {
508
+ positions[anc] += shift * 0.7;
509
+ anc = parentMap.get(anc);
510
+ }
511
+ converged = false;
512
+ }
513
+ }
514
+ }
515
+ if (converged) break;
516
+ }
517
+
518
+ let minX = Infinity, maxX = -Infinity;
519
+ Object.values(positions).forEach(x => { minX = Math.min(minX, x - CARD_W / 2); maxX = Math.max(maxX, x + CARD_W / 2); });
520
+ const treeW = maxX - minX;
521
+ const offsetX = (canvasW - treeW) / 2 - minX;
522
+ Object.keys(positions).forEach(id => { positions[id] += offsetX; });
523
+
524
+ const totalW = Math.max(canvasW, treeW + 80);
525
+ const totalH = ROOT_Y + (maxDepth + 1) * VERTICAL_SPACING + 60;
526
+
527
+ nodes.forEach(n => {
528
+ const x = positions[n.id] !== undefined ? positions[n.id] : canvasW / 2;
529
+ const y = ROOT_Y + (n.depth || 0) * VERTICAL_SPACING;
530
+ n.x = x; n.y = y;
531
+ });
532
+
533
+ if (svg) {
534
+ svg.attr('viewBox', `0 0 ${totalW} ${totalH}`).attr('width', totalW).attr('height', totalH);
535
+ }
536
+ return { totalW, totalH };
537
+ }
538
+
539
+ // ---------- SVG INIT ----------
540
+ function initSvg() {
541
+ const w = container.clientWidth, h = container.clientHeight;
542
+ svg = d3.select('#graph-svg').attr('viewBox', `0 0 ${w} ${h}`).attr('width', w).attr('height', h);
543
+ svg.selectAll('*').remove();
544
+ zoomBehavior = d3.zoom().scaleExtent([0.2, 10]).filter(ev => ev.type === 'wheel' || ev.target === svg.node()).on('zoom', (ev) => {
545
+ if (gMain) gMain.attr('transform', ev.transform);
546
+ });
547
+ svg.call(zoomBehavior);
548
+ gMain = svg.append('g').attr('class','main-group');
549
+ gLinks = gMain.append('g').attr('class','links-group');
550
+ gNodes = gMain.append('g').attr('class','nodes-group');
551
+ foreignPopup = null;
552
+ }
553
+
554
+ function renderCanvas() {
555
+ computeTreeLayout();
556
+ initSvg();
557
+ updateLinks();
558
+ updateNodes();
559
+ updateTileTexts();
560
+ if (selectedNodeId) {
561
+ const sn = nodes.find(n => n.id === selectedNodeId);
562
+ if (sn) showPopupForNode(sn);
563
+ }
564
+ updateStats();
565
+ }
566
+
567
+ function updateLinks() {
568
+ const linkJoin = gLinks.selectAll('line').data(links, d => {
569
+ const sid = typeof d.source === 'object' ? d.source.id : d.source;
570
+ const tid = typeof d.target === 'object' ? d.target.id : d.target;
571
+ return sid + '-' + tid;
572
+ });
573
+ linkJoin.exit().remove();
574
+ const linkEnter = linkJoin.enter().append('line').attr('class','link-line');
575
+ const allLinks = linkJoin.merge(linkEnter);
576
+ allLinks
577
+ .attr('x1', d => (typeof d.source === 'object' ? d.source.x : (nodes.find(n=>n.id===d.source)||{}).x) || 0)
578
+ .attr('y1', d => (typeof d.source === 'object' ? d.source.y : (nodes.find(n=>n.id===d.source)||{}).y) || 0)
579
+ .attr('x2', d => (typeof d.target === 'object' ? d.target.x : (nodes.find(n=>n.id===d.target)||{}).x) || 0)
580
+ .attr('y2', d => (typeof d.target === 'object' ? d.target.y : (nodes.find(n=>n.id===d.target)||{}).y) || 0);
581
+ if (selectedNodeId) highlightPath(selectedNodeId);
582
+ }
583
+
584
+ function updateNodes() {
585
+ const nodeJoin = gNodes.selectAll('.node-group').data(nodes, d => d.id);
586
+ nodeJoin.exit().remove();
587
+ const nodeEnter = nodeJoin.enter().append('g').attr('class','node-group')
588
+ .attr('transform', d => `translate(${d.x},${d.y})`)
589
+ .on('contextmenu', function(ev, d) { ev.preventDefault(); showContextMenu(ev, d); });
590
+ nodeEnter.append('rect').attr('class','tile-rect').attr('width',CARD_W).attr('height',CARD_H).attr('x',-CARD_W/2).attr('y',-CARD_H/2);
591
+ nodeEnter.style('opacity', 0).transition().duration(400).style('opacity', 1);
592
+ nodeEnter.append('text').attr('class','tile-label-line1').attr('y',-24);
593
+ nodeEnter.append('text').attr('class','tile-label-line2').attr('y',-8);
594
+ nodeEnter.append('text').attr('class','tile-type').attr('y',10);
595
+ nodeEnter.append('text').attr('class','tile-badge-depth').attr('x',-CARD_W/2+10).attr('y',CARD_H/2-10);
596
+ nodeEnter.append('text').attr('class','tile-badge-children').attr('x',CARD_W/2-10).attr('y',CARD_H/2-10);
597
+
598
+ const allNodes = nodeEnter.merge(nodeJoin);
599
+ allNodes.attr('transform', d => `translate(${d.x},${d.y})`);
600
+ allNodes.on('click', function(ev, d) {
601
+ ev.stopPropagation();
602
+ selectNode(d);
603
+ });
604
+ svg.on('click', function(ev) {
605
+ const target = ev.target;
606
+ if (foreignPopup) {
607
+ const foNode = foreignPopup.node();
608
+ if (foNode && (foNode === target || foNode.contains(target))) return;
609
+ }
610
+ selectedNodeId = null;
611
+ removeForeignPopup();
612
+ clearHighlights();
613
+ });
614
+ }
615
+
616
+ function updateTileTexts() {
617
+ gNodes.selectAll('.node-group').each(function(d) {
618
+ const g = d3.select(this);
619
+ const label = d._label || d.label || '?';
620
+ const [line1, line2] = splitLabel(label);
621
+ const type = d._type || 'outcome';
622
+ g.select('.tile-label-line1').text(line1);
623
+ g.select('.tile-label-line2').text(line2 || '');
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
+ });
631
+ }
632
+
633
+ function centerViewOnNode(node, alsoPopup = true) {
634
+ if (!svg || !zoomBehavior || !node) return;
635
+ const containerRect = container.getBoundingClientRect();
636
+ const cw = containerRect.width, ch = containerRect.height;
637
+ let left = node.x - CARD_W/2;
638
+ let right = node.x + CARD_W/2;
639
+ let top = node.y - CARD_H/2;
640
+ let bottom = node.y + CARD_H/2;
641
+ if (alsoPopup && selectedNodeId === node.id && foreignPopup) {
642
+ const popupX = node.x + CARD_W/2 + 10;
643
+ const popupY = node.y - HEAVY_H/2;
644
+ right = Math.max(right, popupX + HEAVY_W + 30);
645
+ top = Math.min(top, popupY);
646
+ bottom = Math.max(bottom, popupY + HEAVY_H + 40);
647
+ }
648
+ const centerX = (left + right) / 2;
649
+ const centerY = (top + bottom) / 2;
650
+ const width = right - left;
651
+ const height = bottom - top;
652
+ const padding = 60;
653
+ const scale = Math.min(cw / (width + padding * 2), ch / (height + padding * 2), 1.8);
654
+ const tx = cw/2 - centerX * scale;
655
+ const ty = ch/2 - centerY * scale;
656
+ svg.transition().duration(500).call(
657
+ zoomBehavior.transform,
658
+ d3.zoomIdentity.translate(tx, ty).scale(scale)
659
+ );
660
+ }
661
+
662
+ // ---------- POPUP ----------
663
+ function removeForeignPopup() {
664
+ if (foreignPopup) { foreignPopup.remove(); foreignPopup = null; }
665
+ }
666
+
667
+ function showPopupForNode(node, centerView = true) {
668
+ removeForeignPopup();
669
+ const type = node._type || 'outcome';
670
+ const isExpanded = expandedSet.has(node.id);
671
+ const nextLabel = getNextTypeLabel(type);
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>
689
+ `;
690
+ const px = node.x + CARD_W/2 + 10;
691
+ const py = node.y - HEAVY_H/2;
692
+ foreignPopup = gMain.append('foreignObject')
693
+ .attr('x', px).attr('y', py)
694
+ .attr('width', HEAVY_W + 30).attr('height', HEAVY_H + 40)
695
+ .html(html);
696
+ setTimeout(() => {
697
+ const closeBtn = document.getElementById('fo-close');
698
+ if (closeBtn) closeBtn.addEventListener('click', (ev) => {
699
+ ev.stopPropagation();
700
+ selectedNodeId = null;
701
+ removeForeignPopup();
702
+ clearHighlights();
703
+ });
704
+ const exploreBtn = document.getElementById('fo-explore');
705
+ if (exploreBtn) exploreBtn.addEventListener('click', async (ev) => {
706
+ ev.stopPropagation();
707
+ if (!selectedNodeId) return;
708
+ const count = parseInt(branchInput.value) || 3;
709
+ const commentEl = document.getElementById('fo-comment');
710
+ const comment = commentEl ? commentEl.value.trim() || null : null;
711
+ await exploreNode(selectedNodeId, count, comment);
712
+ });
713
+ const regenBtn = document.getElementById('fo-regen');
714
+ if (regenBtn) regenBtn.addEventListener('click', async (ev) => {
715
+ ev.stopPropagation();
716
+ if (!selectedNodeId) return;
717
+ const count = parseInt(branchInput.value) || 3;
718
+ const commentEl = document.getElementById('fo-comment');
719
+ const comment = commentEl ? commentEl.value.trim() : '';
720
+ if (!comment) { showToast('Add a comment first', 'info'); return; }
721
+ await exploreNode(selectedNodeId, count, comment);
722
+ });
723
+ const addBtn = document.getElementById('fo-add');
724
+ if (addBtn) addBtn.addEventListener('click', async (ev) => {
725
+ ev.stopPropagation();
726
+ if (!selectedNodeId) return;
727
+ const count = parseInt(branchInput.value) || 3;
728
+ await addOptionsToNode(selectedNodeId, count);
729
+ });
730
+ }, 50);
731
+ if (centerView) centerViewOnNode(node, true);
732
+ }
733
+
734
+ function selectNode(node) {
735
+ if (selectedNodeId === node.id && foreignPopup) return;
736
+ selectedNodeId = node.id;
737
+ clearHighlights();
738
+ highlightPath(node.id);
739
+ gNodes.selectAll('.node-group').classed('selected', d => d.id === node.id);
740
+ showPopupForNode(node, true);
741
+ updatePathSidebar();
742
+ }
743
+
744
+ function highlightPath(nodeId) {
745
+ const pathIds = new Set();
746
+ let current = nodeId;
747
+ while (current) { pathIds.add(current); current = parentMap.get(current); }
748
+ gNodes.selectAll('.node-group').classed('path-highlight', d => pathIds.has(d.id));
749
+ gLinks.selectAll('.link-line').classed('path-highlight', d => {
750
+ const sid = typeof d.source === 'object' ? d.source.id : d.source;
751
+ const tid = typeof d.target === 'object' ? d.target.id : d.target;
752
+ return pathIds.has(sid) && pathIds.has(tid);
753
+ });
754
+ }
755
+
756
+ function clearHighlights() {
757
+ gNodes.selectAll('.node-group').classed('selected', false).classed('path-highlight', false);
758
+ gLinks.selectAll('.link-line').classed('path-highlight', false);
759
+ }
760
+
761
+ // ---------- SIDEBAR ----------
762
+ const sidebarTab = document.getElementById('sidebar-tab');
763
+ const sidebarEl = document.getElementById('path-sidebar');
764
+
765
+ sidebarTab.addEventListener('click', () => {
766
+ const isOpen = sidebarEl.classList.contains('open');
767
+ if (isOpen) {
768
+ sidebarEl.classList.remove('open');
769
+ sidebarTab.classList.remove('hidden');
770
+ } else {
771
+ sidebarEl.classList.add('open');
772
+ sidebarTab.classList.add('hidden');
773
+ updatePathSidebar();
774
+ }
775
+ });
776
+
777
+ document.getElementById('sidebar-close').addEventListener('click', () => {
778
+ sidebarEl.classList.remove('open');
779
+ sidebarTab.classList.remove('hidden');
780
+ });
781
+
782
+ function updatePathSidebar() {
783
+ const list = document.getElementById('path-list');
784
+ if (!selectedNodeId || !nodes.length) {
785
+ list.innerHTML = '<div style="color:#5a6088; font-size:0.75em; padding:20px; text-align:center;">Click a node to see its path</div>';
786
+ document.getElementById('path-depth').textContent = '0';
787
+ document.getElementById('path-tips-count').textContent = '0';
788
+ return;
789
+ }
790
+ const path = [];
791
+ let current = selectedNodeId;
792
+ while (current) {
793
+ const node = nodes.find(n => n.id === current);
794
+ if (node) path.unshift(node);
795
+ current = parentMap.get(current);
796
+ }
797
+ let html = '';
798
+ let tipCount = 0;
799
+ path.forEach((node, i) => {
800
+ const type = node._type || 'outcome';
801
+ const hasTip = node._tips && node._tips.length > 0;
802
+ if (hasTip) tipCount++;
803
+ const isLast = i === path.length - 1;
804
+ html += `<div class="path-card ${isLast ? 'active' : ''}" data-id="${node.id}">`;
805
+ html += `<div class="card-header"><span class="card-label">${escapeHtml(node._label || '')}</span><span class="card-type ${type}-type">${type.toUpperCase()}</span></div>`;
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;
815
+ document.getElementById('path-tips-count').textContent = tipCount;
816
+ list.querySelectorAll('.path-card').forEach(card => {
817
+ card.addEventListener('click', () => {
818
+ const id = card.dataset.id;
819
+ const node = nodes.find(n => n.id === id);
820
+ if (node) selectNode(node);
821
+ });
822
+ });
823
+ }
824
+
825
+ // ---------- API CALLS ----------
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 {
833
+ const res = await fetch('/create_tree', {
834
+ method:'POST', headers:{'Content-Type':'application/json'},
835
+ body:JSON.stringify({ decision })
836
+ });
837
+ const data = await res.json();
838
+ if (data.error) { showToast(data.error, 'error'); return; }
839
+ treeId = data.tree_id;
840
+ const rootNode = data.root_node;
841
+ nodes = []; links = []; parentMap = new Map(); expandedSet = new Set(); selectedNodeId = null;
842
+ historyStack = { undo: [], redo: [] };
843
+ addNodeToGraph(rootNode, null);
844
+ renderCanvas();
845
+ showLoading('Exploring first outcomes...');
846
+ await exploreNode(rootNode.id, parseInt(branchInput.value) || 3, null);
847
+ } catch(e) {
848
+ console.error(e);
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
+ }
856
+ }
857
+
858
+ generateBtn.addEventListener('click', generateTree);
859
+ inputEl.addEventListener('keydown', (e) => { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); generateTree(); } });
860
+
861
+ async function exploreNode(nodeId, count, comment) {
862
+ const node = nodes.find(n => n.id === nodeId);
863
+ if (!node) return;
864
+ showLoading('Generating ' + getNextTypeLabel(node._type || 'outcome') + '...');
865
+ try {
866
+ const params = new URLSearchParams({ node_id: nodeId, count, node_type: node._type || 'outcome' });
867
+ if (comment) params.append('comment', comment);
868
+ const res = await fetch(`/get_children?${params}`);
869
+ const data = await res.json();
870
+ if (data.error) { showToast(data.error, 'error'); return; }
871
+ if (expandedSet.has(nodeId)) { removeChildrenOf(nodeId); }
872
+ const children = data.children || [];
873
+ children.forEach(child => addNodeToGraph(child, nodeId));
874
+ expandedSet.add(nodeId);
875
+ node._childrenCount = children.length;
876
+ historyStack.undo.push({ type: 'expand', nodeId, count: children.length });
877
+ historyStack.redo = [];
878
+ renderCanvas();
879
+ removeForeignPopup();
880
+ selectedNodeId = null;
881
+ clearHighlights();
882
+ selectedNodeId = nodeId;
883
+ const parentNode = nodes.find(n => n.id === nodeId);
884
+ if (parentNode) showPopupForNode(parentNode, true);
885
+ updatePathSidebar();
886
+ } catch(e) {
887
+ console.error(e);
888
+ showToast('Failed: '+e.message, 'error');
889
+ } finally { hideLoading(); }
890
+ }
891
+
892
+ async function addOptionsToNode(nodeId, count) {
893
+ showLoading('Adding new ...');
894
+ try {
895
+ const res = await fetch('/add_options', {
896
+ method:'POST', headers:{'Content-Type':'application/json'},
897
+ body:JSON.stringify({ node_id: nodeId, count })
898
+ });
899
+ const data = await res.json();
900
+ if (data.error) { showToast(data.error, 'error'); return; }
901
+ const newChildren = data.children || [];
902
+ newChildren.forEach(child => addNodeToGraph(child, nodeId));
903
+ const parentNode = nodes.find(n => n.id === nodeId);
904
+ if (parentNode) parentNode._childrenCount = (parentNode._childrenCount||0) + newChildren.length;
905
+ historyStack.undo.push({ type: 'add', nodeId, count: newChildren.length });
906
+ historyStack.redo = [];
907
+ renderCanvas();
908
+ removeForeignPopup();
909
+ selectedNodeId = null;
910
+ clearHighlights();
911
+ selectedNodeId = nodeId;
912
+ if (parentNode) showPopupForNode(parentNode, true);
913
+ updatePathSidebar();
914
+ } catch(e) {
915
+ showToast('Failed: '+e.message, 'error');
916
+ } finally { hideLoading(); }
917
+ }
918
+
919
+ function addNodeToGraph(nodeObj, parentId) {
920
+ nodeObj._label = nodeObj.label || 'Untitled';
921
+ nodeObj._description = nodeObj.description || '';
922
+ nodeObj._comments = nodeObj.comments || [];
923
+ nodeObj._childrenCount = (nodeObj.children && nodeObj.children.length) || 0;
924
+ nodeObj._type = nodeObj.type || 'outcome';
925
+ nodeObj._tips = nodeObj.tips || [];
926
+ nodeObj.depth = parentId ? (nodes.find(n=>n.id===parentId)?.depth ?? 0) + 1 : 0;
927
+ nodeObj.x = 0; nodeObj.y = 0;
928
+ nodes.push(nodeObj);
929
+ if (parentId !== null && parentId !== undefined) {
930
+ parentMap.set(nodeObj.id, parentId);
931
+ links.push({ source: parentId, target: nodeObj.id });
932
+ }
933
+ }
934
+
935
+ function removeChildrenOf(nodeId) {
936
+ const toRemove = new Set();
937
+ function collectDescendants(id) {
938
+ const childIds = [];
939
+ parentMap.forEach((pid, cid) => { if (pid === id) childIds.push(cid); });
940
+ childIds.forEach(cid => { toRemove.add(cid); collectDescendants(cid); });
941
+ }
942
+ collectDescendants(nodeId);
943
+ nodes = nodes.filter(n => !toRemove.has(n.id));
944
+ links = links.filter(l => {
945
+ const src = typeof l.source === 'object' ? l.source.id : l.source;
946
+ const tgt = typeof l.target === 'object' ? l.target.id : l.target;
947
+ return !toRemove.has(src) && !toRemove.has(tgt);
948
+ });
949
+ for (const id of toRemove) { parentMap.delete(id); expandedSet.delete(id); }
950
+ if (toRemove.has(selectedNodeId)) {
951
+ selectedNodeId = null;
952
+ removeForeignPopup();
953
+ clearHighlights();
954
+ updatePathSidebar();
955
+ }
956
+ }
957
+
958
+ function updateStats() {
959
+ document.getElementById('stat-nodes').textContent = nodes.length;
960
+ const maxD = nodes.length > 0 ? Math.max(...nodes.map(n=>n.depth||0)) : 0;
961
+ document.getElementById('stat-depth').textContent = maxD;
962
+ const leaves = nodes.filter(n => (n._childrenCount||0) === 0).length;
963
+ document.getElementById('stat-leaves').textContent = leaves;
964
+ const mb = (nodes.length * 0.08).toFixed(1);
965
+ document.getElementById('stat-ram').textContent = mb;
966
+ }
967
+
968
+ // ---------- CONTEXT MENU ----------
969
+ function showContextMenu(ev, d) {
970
+ const menu = document.getElementById('context-menu');
971
+ menu.style.display = 'block';
972
+ menu.style.left = Math.min(ev.clientX, window.innerWidth - 200) + 'px';
973
+ menu.style.top = Math.min(ev.clientY, window.innerHeight - 200) + 'px';
974
+ menu.dataset.nodeId = d.id;
975
+ }
976
+
977
+ document.addEventListener('click', (ev) => {
978
+ const menu = document.getElementById('context-menu');
979
+ if (menu && !menu.contains(ev.target)) menu.style.display = 'none';
980
+ });
981
+
982
+ document.getElementById('context-menu').addEventListener('click', async (ev) => {
983
+ const item = ev.target.closest('.ctx-item');
984
+ if (!item) return;
985
+ const action = item.dataset.action;
986
+ const nodeId = document.getElementById('context-menu').dataset.nodeId;
987
+ if (!nodeId) return;
988
+ document.getElementById('context-menu').style.display = 'none';
989
+ const node = nodes.find(n => n.id === nodeId);
990
+ if (!node) return;
991
+ switch(action) {
992
+ case 'expand':
993
+ selectNode(node);
994
+ if (!expandedSet.has(nodeId)) await exploreNode(nodeId, parseInt(branchInput.value)||3, null);
995
+ break;
996
+ case 'regenerate':
997
+ selectNode(node);
998
+ const comment = prompt('Enter your thought to guide regeneration:');
999
+ if (comment) await exploreNode(nodeId, parseInt(branchInput.value)||3, comment);
1000
+ break;
1001
+ case 'add':
1002
+ selectNode(node);
1003
+ await addOptionsToNode(nodeId, parseInt(branchInput.value)||3);
1004
+ break;
1005
+ case 'copy-path':
1006
+ const path = [];
1007
+ let cur = nodeId;
1008
+ while (cur) {
1009
+ const n = nodes.find(n => n.id === cur);
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':
1017
+ const nodeX = node.x, nodeY = node.y;
1018
+ const rect = container.getBoundingClientRect();
1019
+ const scale = zoomBehavior ? d3.zoomTransform(svg.node()).k : 1;
1020
+ const targetX = rect.width/2 - nodeX*scale;
1021
+ const targetY = rect.height/2 - nodeY*scale;
1022
+ svg.transition().duration(500).call(zoomBehavior.transform, d3.zoomIdentity.translate(targetX, targetY).scale(scale));
1023
+ break;
1024
+ }
1025
+ });
1026
+
1027
+ // =============================================
1028
+ // EXPORT FUNCTIONS β€” v20 (Fixed centering + root description/tips for full tree)
1029
+ // =============================================
1030
+
1031
+ function computeTextLines(text, maxLen) {
1032
+ if (!text) return 0;
1033
+ let lines = 0;
1034
+ let remaining = text;
1035
+ while (remaining.length > 0 && lines < 10) {
1036
+ lines++;
1037
+ if (remaining.length <= maxLen) break;
1038
+ let split = remaining.lastIndexOf(' ', maxLen);
1039
+ if (split < 5) split = maxLen;
1040
+ remaining = remaining.substring(split).trim();
1041
+ }
1042
+ return lines;
1043
+ }
1044
+
1045
+ 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;
1053
+ const baseHeight = 60;
1054
+ return Math.max(baseHeight + extraHeight, CARD_H);
1055
+ }
1056
+
1057
+ function computeExportLayout(exportNodes, exportLinks, canvasW) {
1058
+ if (!exportNodes.length) return null;
1059
+ const childrenMap = {};
1060
+ exportNodes.forEach(n => { childrenMap[n.id] = []; });
1061
+ exportLinks.forEach(l => {
1062
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1063
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1064
+ if (childrenMap[sid]) childrenMap[sid].push(tid);
1065
+ });
1066
+
1067
+ const depthGroups = {};
1068
+ let maxDepth = 0;
1069
+ exportNodes.forEach(n => {
1070
+ const d = n.depth || 0;
1071
+ if (!depthGroups[d]) depthGroups[d] = [];
1072
+ depthGroups[d].push(n);
1073
+ if (d > maxDepth) maxDepth = d;
1074
+ });
1075
+
1076
+ const rootId = exportNodes.find(n => n._type === 'root')?.id || exportNodes[0].id;
1077
+
1078
+ const subtreeWidth = {};
1079
+ for (let d = maxDepth; d >= 0; d--) {
1080
+ (depthGroups[d] || []).forEach(node => {
1081
+ const kids = childrenMap[node.id] || [];
1082
+ if (kids.length === 0) {
1083
+ subtreeWidth[node.id] = CARD_W + MIN_HORIZONTAL_GAP;
1084
+ } else {
1085
+ let total = kids.reduce((sum, cid) => sum + (subtreeWidth[cid] || (CARD_W + MIN_HORIZONTAL_GAP)), 0);
1086
+ total = Math.max(CARD_W + MIN_HORIZONTAL_GAP, total + MIN_HORIZONTAL_GAP * (kids.length - 1));
1087
+ subtreeWidth[node.id] = total;
1088
+ }
1089
+ });
1090
+ }
1091
+
1092
+ const positions = {};
1093
+ const root = exportNodes.find(n => n.id === rootId);
1094
+ if (root) positions[rootId] = canvasW / 2;
1095
+
1096
+ function positionChildren(nodeId, parentX, availableW) {
1097
+ const kids = childrenMap[nodeId] || [];
1098
+ if (kids.length === 0) return;
1099
+ const totalChildW = kids.reduce((sum, cid) => sum + (subtreeWidth[cid] || (CARD_W + MIN_HORIZONTAL_GAP)), 0);
1100
+ const extraSpace = Math.max(0, availableW - totalChildW);
1101
+ const gap = kids.length > 1 ? extraSpace / (kids.length - 1) : 0;
1102
+ let xCursor = parentX - totalChildW / 2;
1103
+ kids.forEach(cid => {
1104
+ const sw = subtreeWidth[cid] || (CARD_W + MIN_HORIZONTAL_GAP);
1105
+ positions[cid] = xCursor + sw / 2;
1106
+ xCursor += sw + gap;
1107
+ positionChildren(cid, positions[cid], sw);
1108
+ });
1109
+ }
1110
+ if (root) positionChildren(rootId, positions[rootId], subtreeWidth[rootId] || canvasW);
1111
+
1112
+ const ITER = 30;
1113
+ for (let iter = 0; iter < ITER; iter++) {
1114
+ let converged = true;
1115
+ for (let d = 0; d <= maxDepth; d++) {
1116
+ const group = [...(depthGroups[d] || [])].sort((a, b) => (positions[a.id] || 0) - (positions[b.id] || 0));
1117
+ for (let i = 0; i < group.length - 1; i++) {
1118
+ const a = group[i], b = group[i + 1];
1119
+ if (!a || !b) continue;
1120
+ const xA = positions[a.id] || 0;
1121
+ const xB = positions[b.id] || 0;
1122
+ const minDist = CARD_W + MIN_HORIZONTAL_GAP;
1123
+ if (xB - xA < minDist) {
1124
+ const shift = minDist - (xB - xA);
1125
+ positions[b.id] += shift;
1126
+ converged = false;
1127
+ }
1128
+ }
1129
+ }
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;
1160
+ (depthGroups[d] || []).forEach(n => {
1161
+ const h = computePathTileHeight(n);
1162
+ if (h > maxRowH) maxRowH = h;
1163
+ });
1164
+ rowHeights[d] = maxRowH;
1165
+ }
1166
+
1167
+ const Y_PADDING = 40;
1168
+ let totalH = ROOT_Y + 40;
1169
+ for (let d = 0; d <= maxDepth; d++) {
1170
+ totalH += rowHeights[d] + Y_PADDING;
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
+
1189
+ return { totalW: finalTotalW, totalH };
1190
+ }
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) => {
1198
+ const h = computePathTileHeight(n);
1199
+ nodeHeights.push(h);
1200
+ });
1201
+
1202
+ let totalH = ROOT_Y + nodeHeights.reduce((sum, h) => sum + h + GAP, 0) + 60;
1203
+ const totalW = Math.max(canvasW, 800);
1204
+
1205
+ let currentY = ROOT_Y;
1206
+ exportNodes.forEach((n, idx) => {
1207
+ const h = nodeHeights[idx];
1208
+ n._exportX = totalW / 2;
1209
+ n._exportY = currentY + h / 2;
1210
+ n._exportTileH = h;
1211
+ currentY += h + GAP;
1212
+ });
1213
+
1214
+ return { totalW, totalH };
1215
+ }
1216
+
1217
+ function buildExportSvg(isPathOnly = false) {
1218
+ const SVG_NS = 'http://www.w3.org/2000/svg';
1219
+ const ns = SVG_NS;
1220
+
1221
+ let exportNodes = nodes;
1222
+ let exportLinks = links;
1223
+ if (isPathOnly && selectedNodeId) {
1224
+ const pathIds = new Set();
1225
+ let cur = selectedNodeId;
1226
+ while (cur) { pathIds.add(cur); cur = parentMap.get(cur); }
1227
+ exportNodes = nodes.filter(n => pathIds.has(n.id));
1228
+ exportLinks = links.filter(l => {
1229
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1230
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1231
+ return pathIds.has(sid) && pathIds.has(tid);
1232
+ });
1233
+ }
1234
+
1235
+ if (!exportNodes.length) return null;
1236
+
1237
+ const canvasW = 1400;
1238
+ const layout = isPathOnly ? computePathLayoutLinear(exportNodes, exportLinks, canvasW) : computeExportLayout(exportNodes, exportLinks, canvasW);
1239
+ if (!layout) return null;
1240
+ const { totalW, totalH } = layout;
1241
+
1242
+ const svgDoc = document.createElementNS(ns, 'svg');
1243
+ svgDoc.setAttribute('xmlns', ns);
1244
+ svgDoc.setAttribute('viewBox', `0 0 ${totalW} ${totalH}`);
1245
+ svgDoc.setAttribute('width', totalW);
1246
+ svgDoc.setAttribute('height', totalH);
1247
+
1248
+ const bg = document.createElementNS(ns, 'rect');
1249
+ bg.setAttribute('width', totalW);
1250
+ bg.setAttribute('height', totalH);
1251
+ bg.setAttribute('fill', isDark ? '#0f0c29' : '#f5f7fa');
1252
+ svgDoc.appendChild(bg);
1253
+
1254
+ exportLinks.forEach(l => {
1255
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1256
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1257
+ const srcNode = exportNodes.find(n => n.id === sid);
1258
+ const tgtNode = exportNodes.find(n => n.id === tid);
1259
+ if (!srcNode || !tgtNode) return;
1260
+ const srcH = srcNode._exportTileH || CARD_H;
1261
+ const tgtH = tgtNode._exportTileH || CARD_H;
1262
+ const line = document.createElementNS(ns, 'line');
1263
+ line.setAttribute('x1', srcNode._exportX);
1264
+ line.setAttribute('y1', srcNode._exportY + srcH/2);
1265
+ line.setAttribute('x2', tgtNode._exportX);
1266
+ line.setAttribute('y2', tgtNode._exportY - tgtH/2);
1267
+ line.setAttribute('stroke', isDark ? '#ffd700' : '#2c3e50');
1268
+ line.setAttribute('stroke-width', '2');
1269
+ line.setAttribute('opacity', '0.3');
1270
+ svgDoc.appendChild(line);
1271
+ });
1272
+
1273
+ const strokeColors = { root: '#ffd700', input: '#ff6b9d', outcome: '#00d4ff' };
1274
+ const fillColor = isDark ? 'rgba(25,25,55,0.92)' : 'rgba(255,255,255,0.92)';
1275
+ const labelColor = isDark ? '#ffd700' : '#d35400';
1276
+ const typeFillColor = isDark ? '#c0c0c0' : '#555';
1277
+
1278
+ exportNodes.forEach((n) => {
1279
+ if (n._exportX === undefined || n._exportY === undefined) return;
1280
+ const tileH = n._exportTileH || CARD_H;
1281
+ const g = document.createElementNS(ns, 'g');
1282
+ g.setAttribute('transform', `translate(${n._exportX},${n._exportY})`);
1283
+
1284
+ const rect = document.createElementNS(ns, 'rect');
1285
+ rect.setAttribute('width', CARD_W);
1286
+ rect.setAttribute('height', tileH);
1287
+ rect.setAttribute('x', -CARD_W/2);
1288
+ rect.setAttribute('y', -tileH/2);
1289
+ rect.setAttribute('rx', '8');
1290
+ rect.setAttribute('fill', fillColor);
1291
+ rect.setAttribute('stroke', strokeColors[n._type] || '#00d4ff');
1292
+ rect.setAttribute('stroke-width', '2');
1293
+ g.appendChild(rect);
1294
+
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);
1302
+ txt1.setAttribute('text-anchor', 'middle');
1303
+ txt1.setAttribute('fill', labelColor);
1304
+ txt1.setAttribute('font-size', '12');
1305
+ txt1.setAttribute('font-weight', '700');
1306
+ txt1.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
1307
+ txt1.textContent = line1 || '';
1308
+ g.appendChild(txt1);
1309
+
1310
+ if (line2) {
1311
+ const txt2 = document.createElementNS(ns, 'text');
1312
+ txt2.setAttribute('x', '0');
1313
+ txt2.setAttribute('y', -(tileH/2) + 36);
1314
+ txt2.setAttribute('text-anchor', 'middle');
1315
+ txt2.setAttribute('fill', labelColor);
1316
+ txt2.setAttribute('font-size', '12');
1317
+ txt2.setAttribute('font-weight', '700');
1318
+ txt2.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
1319
+ txt2.textContent = line2;
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);
1327
+ typeTxt.setAttribute('text-anchor', 'middle');
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) {
1339
+ const descLines = [];
1340
+ let remaining = desc;
1341
+ while (remaining.length > 0 && descLines.length < 3) {
1342
+ if (remaining.length <= maxDescLen) {
1343
+ descLines.push(remaining);
1344
+ break;
1345
+ }
1346
+ let split = remaining.lastIndexOf(' ', maxDescLen);
1347
+ if (split < 5) split = maxDescLen;
1348
+ descLines.push(remaining.substring(0, split).trim());
1349
+ remaining = remaining.substring(split).trim();
1350
+ }
1351
+ descLines.forEach((line, i) => {
1352
+ const dtxt = document.createElementNS(ns, 'text');
1353
+ dtxt.setAttribute('x', '0');
1354
+ dtxt.setAttribute('y', -(tileH/2) + 66 + i * 13);
1355
+ dtxt.setAttribute('text-anchor', 'middle');
1356
+ dtxt.setAttribute('fill', isDark ? '#aaa' : '#666');
1357
+ dtxt.setAttribute('font-size', '8');
1358
+ dtxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
1359
+ dtxt.textContent = line;
1360
+ g.appendChild(dtxt);
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) {
1372
+ tipLines.push(remainingTip);
1373
+ break;
1374
+ }
1375
+ let split = remainingTip.lastIndexOf(' ', maxTipLen);
1376
+ if (split < 5) split = maxTipLen;
1377
+ tipLines.push(remainingTip.substring(0, split).trim());
1378
+ remainingTip = remainingTip.substring(split).trim();
1379
+ }
1380
+ const descLinesCount = Math.min(computeTextLines(desc, maxDescLen), 3);
1381
+ const tipStartY = -(tileH/2) + 66 + descLinesCount * 13 + 4;
1382
+ tipLines.forEach((line, i) => {
1383
+ const ttxt = document.createElementNS(ns, 'text');
1384
+ ttxt.setAttribute('x', '0');
1385
+ ttxt.setAttribute('y', tipStartY + i * 11);
1386
+ ttxt.setAttribute('text-anchor', 'middle');
1387
+ ttxt.setAttribute('fill', '#7ec8a0');
1388
+ ttxt.setAttribute('font-size', '8');
1389
+ ttxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
1390
+ ttxt.textContent = line;
1391
+ g.appendChild(ttxt);
1392
+ });
1393
+ }
1394
+
1395
+ const depthTxt = document.createElementNS(ns, 'text');
1396
+ depthTxt.setAttribute('x', -CARD_W/2 + 10);
1397
+ depthTxt.setAttribute('y', tileH/2 - 10);
1398
+ depthTxt.setAttribute('fill', isDark ? '#c9aaff' : '#7c5cbf');
1399
+ depthTxt.setAttribute('font-size', '10');
1400
+ depthTxt.setAttribute('font-family', 'Segoe UI, system-ui, sans-serif');
1401
+ depthTxt.textContent = 'D' + (n.depth || 0);
1402
+ g.appendChild(depthTxt);
1403
+
1404
+ const chTxt = document.createElementNS(ns, 'text');
1405
+ chTxt.setAttribute('x', CARD_W/2 - 10);
1406
+ chTxt.setAttribute('y', tileH/2 - 10);
1407
+ chTxt.setAttribute('text-anchor', 'end');
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);
1415
+ });
1416
+
1417
+ const title = document.createElementNS(ns, 'text');
1418
+ title.setAttribute('x', totalW / 2);
1419
+ title.setAttribute('y', 30);
1420
+ title.setAttribute('text-anchor', 'middle');
1421
+ title.setAttribute('fill', labelColor);
1422
+ title.setAttribute('font-size', '18');
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;
1430
+ }
1431
+
1432
+ function exportSvg() {
1433
+ const svgDoc = buildExportSvg(false);
1434
+ if (!svgDoc) { showToast('Generate a tree first', 'error'); return; }
1435
+ const svgData = new XMLSerializer().serializeToString(svgDoc);
1436
+ const blob = new Blob([svgData], {type:'image/svg+xml'});
1437
+ const url = URL.createObjectURL(blob);
1438
+ const a = document.createElement('a'); a.href=url; a.download='overthinker_tree.svg'; a.click();
1439
+ URL.revokeObjectURL(url);
1440
+ showToast('SVG exported!','success');
1441
+ }
1442
+
1443
+ function exportPathSvg() {
1444
+ if (!selectedNodeId) { showToast('Select a node first', 'error'); return; }
1445
+ const svgDoc = buildExportSvg(true);
1446
+ if (!svgDoc) { showToast('Path export failed', 'error'); return; }
1447
+ const svgData = new XMLSerializer().serializeToString(svgDoc);
1448
+ const blob = new Blob([svgData], {type:'image/svg+xml'});
1449
+ const url = URL.createObjectURL(blob);
1450
+ const a = document.createElement('a'); a.href=url; a.download='overthinker_path.svg'; a.click();
1451
+ URL.revokeObjectURL(url);
1452
+ showToast('Path SVG exported!','success');
1453
+ }
1454
+
1455
+ async function exportJson() {
1456
+ if (!treeId) { showToast('No tree to export', 'error'); return; }
1457
+ try {
1458
+ const res = await fetch('/export_json', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ tree_id: treeId }) });
1459
+ const data = await res.json();
1460
+ if (data.error) { showToast(data.error, 'error'); return; }
1461
+ const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
1462
+ const url = URL.createObjectURL(blob);
1463
+ const a = document.createElement('a'); a.href=url; a.download='overthinker_tree.json'; a.click();
1464
+ URL.revokeObjectURL(url);
1465
+ showToast('JSON exported!','success');
1466
+ } catch(e) { showToast('Export failed: '+e.message, 'error'); }
1467
+ }
1468
+
1469
+ async function exportPathJson() {
1470
+ if (!selectedNodeId) { showToast('Select a node first', 'error'); return; }
1471
+ if (!treeId) { showToast('No tree', 'error'); return; }
1472
+ try {
1473
+ const res = await fetch('/export_path_json', {
1474
+ method:'POST', headers:{'Content-Type':'application/json'},
1475
+ body:JSON.stringify({ node_id: selectedNodeId, tree_id: treeId })
1476
+ });
1477
+ const data = await res.json();
1478
+ if (data.error) { showToast(data.error, 'error'); return; }
1479
+ const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
1480
+ const url = URL.createObjectURL(blob);
1481
+ const a = document.createElement('a'); a.href=url; a.download='overthinker_path.json'; a.click();
1482
+ URL.revokeObjectURL(url);
1483
+ showToast('Path JSON exported!','success');
1484
+ } catch(e) { showToast('Export failed: '+e.message, 'error'); }
1485
+ }
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); });
1506
+ childIds.forEach(cid => collectMd(cid, depth+1));
1507
+ }
1508
+ if (root) collectMd(root.id, 0);
1509
+ const blob = new Blob([md], {type:'text/markdown'});
1510
+ const url = URL.createObjectURL(blob);
1511
+ const a = document.createElement('a'); a.href=url; a.download='overthinker_tree.md'; a.click();
1512
+ URL.revokeObjectURL(url);
1513
+ showToast('Markdown exported!','success');
1514
+ }
1515
+
1516
+ async function exportPathMd() {
1517
+ if (!selectedNodeId) { showToast('Select a node first', 'error'); return; }
1518
+ if (!treeId) { showToast('No tree', 'error'); return; }
1519
+ try {
1520
+ const res = await fetch('/export_path_md', {
1521
+ method:'POST', headers:{'Content-Type':'application/json'},
1522
+ body:JSON.stringify({ node_id: selectedNodeId, tree_id: treeId })
1523
+ });
1524
+ const md = await res.text();
1525
+ const blob = new Blob([md], {type:'text/markdown'});
1526
+ const url = URL.createObjectURL(blob);
1527
+ const a = document.createElement('a'); a.href=url; a.download='overthinker_path.md'; a.click();
1528
+ URL.revokeObjectURL(url);
1529
+ showToast('Path Markdown exported!','success');
1530
+ } catch(e) { showToast('Export failed: '+e.message, 'error'); }
1531
+ }
1532
+
1533
+ function svgToPng(svgDoc, filename) {
1534
+ const svgData = new XMLSerializer().serializeToString(svgDoc);
1535
+ const blob = new Blob([svgData], {type:'image/svg+xml;charset=utf-8'});
1536
+ const url = URL.createObjectURL(blob);
1537
+ const img = new Image();
1538
+ const container = document.getElementById('ensurePngContainer');
1539
+ container.appendChild(img);
1540
+
1541
+ img.onload = function() {
1542
+ try {
1543
+ const cv = document.createElement('canvas');
1544
+ const ctx = cv.getContext('2d');
1545
+ cv.width = img.width * 2;
1546
+ cv.height = img.height * 2;
1547
+ ctx.scale(2, 2);
1548
+ ctx.drawImage(img, 0, 0);
1549
+ URL.revokeObjectURL(url);
1550
+ cv.toBlob(function(pngBlob) {
1551
+ if (!pngBlob) { showToast('PNG export failed', 'error'); container.removeChild(img); return; }
1552
+ const pngUrl = URL.createObjectURL(pngBlob);
1553
+ const a = document.createElement('a'); a.href=pngUrl; a.download=filename; a.click();
1554
+ URL.revokeObjectURL(pngUrl);
1555
+ container.removeChild(img);
1556
+ showToast('PNG exported!','success');
1557
+ }, 'image/png');
1558
+ } catch(e) {
1559
+ showToast('PNG export failed: ' + e.message, 'error');
1560
+ container.removeChild(img);
1561
+ }
1562
+ };
1563
+ img.onerror = function(e) {
1564
+ showToast('PNG export failed, try SVG', 'error');
1565
+ URL.revokeObjectURL(url);
1566
+ container.removeChild(img);
1567
+ };
1568
+ img.src = url;
1569
+ }
1570
+
1571
+ function exportPng() {
1572
+ const svgDoc = buildExportSvg(false);
1573
+ if (!svgDoc) { showToast('Generate a tree first', 'error'); return; }
1574
+ showToast('Generating PNG...', 'info');
1575
+ svgToPng(svgDoc, 'overthinker_tree.png');
1576
+ }
1577
+
1578
+ function exportPathPng() {
1579
+ if (!selectedNodeId) { showToast('Select a node first', 'error'); return; }
1580
+ const svgDoc = buildExportSvg(true);
1581
+ if (!svgDoc) { showToast('Path export failed', 'error'); return; }
1582
+ showToast('Generating Path PNG...', 'info');
1583
+ svgToPng(svgDoc, 'overthinker_path.png');
1584
+ }
1585
+
1586
+ document.getElementById('export-svg-btn').addEventListener('click', exportSvg);
1587
+ document.getElementById('export-json-btn').addEventListener('click', exportJson);
1588
+ document.getElementById('export-md-btn').addEventListener('click', exportMarkdown);
1589
+ document.getElementById('export-png-btn').addEventListener('click', exportPng);
1590
+
1591
+ document.getElementById('path-export-svg').addEventListener('click', exportPathSvg);
1592
+ document.getElementById('path-export-json').addEventListener('click', exportPathJson);
1593
+ document.getElementById('path-export-md').addEventListener('click', exportPathMd);
1594
+ document.getElementById('path-export-png').addEventListener('click', exportPathPng);
1595
+
1596
+ // ---------- FIT / CLEAR ----------
1597
+ function fitView() {
1598
+ if (svg && zoomBehavior) {
1599
+ svg.transition().duration(500).call(zoomBehavior.transform, d3.zoomIdentity);
1600
+ }
1601
+ }
1602
+
1603
+ function clearTree() {
1604
+ nodes=[]; links=[]; parentMap=new Map(); expandedSet.clear(); selectedNodeId=null; treeId=null; treeDataRoot=null;
1605
+ if (svg) svg.selectAll('*').remove();
1606
+ document.getElementById('toolbar').style.display = 'none';
1607
+ removeForeignPopup();
1608
+ sidebarEl.classList.remove('open');
1609
+ sidebarTab.classList.remove('hidden');
1610
+ updateStats();
1611
+ inputEl.value = '';
1612
+ showToast('Tree cleared', 'info');
1613
+ }
1614
+
1615
+ document.getElementById('fit-btn').addEventListener('click', fitView);
1616
+ document.getElementById('clear-btn').addEventListener('click', clearTree);
1617
+
1618
+ // ---------- KEYBOARD SHORTCUTS ----------
1619
+ document.addEventListener('keydown', (ev) => {
1620
+ if (ev.target.tagName === 'TEXTAREA' || ev.target.tagName === 'INPUT') return;
1621
+ const key = ev.key.toLowerCase();
1622
+ if (key === 'f') { fitView(); ev.preventDefault(); }
1623
+ if (key === 'c') { clearTree(); ev.preventDefault(); }
1624
+ if (key === 'e' && selectedNodeId) {
1625
+ const node = nodes.find(n => n.id === selectedNodeId);
1626
+ if (node && !expandedSet.has(selectedNodeId)) exploreNode(selectedNodeId, parseInt(branchInput.value)||3, null);
1627
+ ev.preventDefault();
1628
+ }
1629
+ if (key === 'r' && selectedNodeId) {
1630
+ const comment = prompt('Enter your thought:');
1631
+ if (comment) exploreNode(selectedNodeId, parseInt(branchInput.value)||3, comment);
1632
+ ev.preventDefault();
1633
+ }
1634
+ if (key === 'a' && selectedNodeId) { addOptionsToNode(selectedNodeId, parseInt(branchInput.value)||3); ev.preventDefault(); }
1635
+ if (key === 'z' && ev.ctrlKey && !ev.shiftKey) { showToast('Undo supported via API', 'info'); ev.preventDefault(); }
1636
+ if (key === 'z' && ev.ctrlKey && ev.shiftKey) { showToast('Redo supported via API', 'info'); ev.preventDefault(); }
1637
+ if (key === 'escape') {
1638
+ document.getElementById('context-menu').style.display = 'none';
1639
+ selectedNodeId = null; removeForeignPopup(); clearHighlights();
1640
+ sidebarEl.classList.remove('open');
1641
+ sidebarTab.classList.remove('hidden');
1642
+ }
1643
+ });
1644
+
1645
+ // ---------- THEME ----------
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 = [
1657
+ "Should I quit my job to start a startup?",
1658
+ "Should I move to a new city for a job?",
1659
+ "Should I invest my savings in the stock market?",
1660
+ "Should I learn to code or go to business school?",
1661
+ "Should I buy a house now or wait another year?",
1662
+ "Should I confront my friend about a problem?",
1663
+ "Should I adopt a pet?"
1664
+ ];
1665
+ inputEl.value = samples[Math.floor(Math.random() * samples.length)];
1666
+ generateTree();
1667
+ });
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>