Spaces:
Paused
Paused
Upload index.html
Browse files- 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>
|