openvoiceui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.env.example +104 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +638 -0
  5. package/SETUP.md +360 -0
  6. package/app.py +232 -0
  7. package/auto-approve-devices.js +111 -0
  8. package/cli/index.js +372 -0
  9. package/config/__init__.py +4 -0
  10. package/config/default.yaml +43 -0
  11. package/config/flags.yaml +67 -0
  12. package/config/loader.py +203 -0
  13. package/config/providers.yaml +71 -0
  14. package/config/speech_normalization.yaml +182 -0
  15. package/config/theme.json +4 -0
  16. package/data/greetings.json +25 -0
  17. package/default-pages/ai-image-creator.html +915 -0
  18. package/default-pages/bulk-image-uploader.html +492 -0
  19. package/default-pages/desktop.html +2865 -0
  20. package/default-pages/file-explorer.html +854 -0
  21. package/default-pages/interactive-map.html +655 -0
  22. package/default-pages/style-guide.html +1005 -0
  23. package/default-pages/website-setup.html +1623 -0
  24. package/deploy/openclaw/Dockerfile +46 -0
  25. package/deploy/openvoiceui.service +30 -0
  26. package/deploy/setup-nginx.sh +50 -0
  27. package/deploy/setup-sudo.sh +306 -0
  28. package/deploy/skill-runner/Dockerfile +19 -0
  29. package/deploy/skill-runner/requirements.txt +14 -0
  30. package/deploy/skill-runner/server.py +269 -0
  31. package/deploy/supertonic/Dockerfile +22 -0
  32. package/deploy/supertonic/server.py +79 -0
  33. package/docker-compose.pinokio.yml +11 -0
  34. package/docker-compose.yml +59 -0
  35. package/greetings.json +25 -0
  36. package/index.html +65 -0
  37. package/inject-device-identity.js +142 -0
  38. package/package.json +82 -0
  39. package/profiles/default.json +114 -0
  40. package/profiles/manager.py +354 -0
  41. package/profiles/schema.json +337 -0
  42. package/prompts/voice-system-prompt.md +149 -0
  43. package/providers/__init__.py +39 -0
  44. package/providers/base.py +63 -0
  45. package/providers/llm/__init__.py +12 -0
  46. package/providers/llm/base.py +71 -0
  47. package/providers/llm/clawdbot_provider.py +112 -0
  48. package/providers/llm/zai_provider.py +115 -0
  49. package/providers/registry.py +320 -0
  50. package/providers/stt/__init__.py +12 -0
  51. package/providers/stt/base.py +58 -0
  52. package/providers/stt/webspeech_provider.py +49 -0
  53. package/providers/stt/whisper_provider.py +100 -0
  54. package/providers/tts/__init__.py +20 -0
  55. package/providers/tts/base.py +91 -0
  56. package/providers/tts/groq_provider.py +74 -0
  57. package/providers/tts/supertonic_provider.py +72 -0
  58. package/requirements.txt +38 -0
  59. package/routes/__init__.py +10 -0
  60. package/routes/admin.py +515 -0
  61. package/routes/canvas.py +1315 -0
  62. package/routes/chat.py +51 -0
  63. package/routes/conversation.py +2158 -0
  64. package/routes/elevenlabs_hybrid.py +306 -0
  65. package/routes/greetings.py +98 -0
  66. package/routes/icons.py +279 -0
  67. package/routes/image_gen.py +364 -0
  68. package/routes/instructions.py +190 -0
  69. package/routes/music.py +838 -0
  70. package/routes/onboarding.py +43 -0
  71. package/routes/pi.py +62 -0
  72. package/routes/profiles.py +215 -0
  73. package/routes/report_issue.py +68 -0
  74. package/routes/static_files.py +533 -0
  75. package/routes/suno.py +664 -0
  76. package/routes/theme.py +81 -0
  77. package/routes/transcripts.py +199 -0
  78. package/routes/vision.py +348 -0
  79. package/routes/workspace.py +288 -0
  80. package/server.py +1510 -0
  81. package/services/__init__.py +1 -0
  82. package/services/auth.py +143 -0
  83. package/services/canvas_versioning.py +239 -0
  84. package/services/db_pool.py +107 -0
  85. package/services/gateway.py +16 -0
  86. package/services/gateway_manager.py +333 -0
  87. package/services/gateways/__init__.py +12 -0
  88. package/services/gateways/base.py +110 -0
  89. package/services/gateways/compat.py +264 -0
  90. package/services/gateways/openclaw.py +1134 -0
  91. package/services/health.py +100 -0
  92. package/services/memory_client.py +455 -0
  93. package/services/paths.py +26 -0
  94. package/services/speech_normalizer.py +285 -0
  95. package/services/tts.py +270 -0
  96. package/setup-config.js +262 -0
  97. package/sounds/air_horn.mp3 +0 -0
  98. package/sounds/bruh.mp3 +0 -0
  99. package/sounds/crowd_cheer.mp3 +0 -0
  100. package/sounds/gunshot.mp3 +0 -0
  101. package/sounds/impact.mp3 +0 -0
  102. package/sounds/lets_go.mp3 +0 -0
  103. package/sounds/record_stop.mp3 +0 -0
  104. package/sounds/rewind.mp3 +0 -0
  105. package/sounds/sad_trombone.mp3 +0 -0
  106. package/sounds/scratch_long.mp3 +0 -0
  107. package/sounds/yeah.mp3 +0 -0
  108. package/src/adapters/ClawdBotAdapter.js +264 -0
  109. package/src/adapters/_template.js +133 -0
  110. package/src/adapters/elevenlabs-classic.js +841 -0
  111. package/src/adapters/elevenlabs-hybrid.js +812 -0
  112. package/src/adapters/hume-evi.js +676 -0
  113. package/src/admin.html +1339 -0
  114. package/src/app.js +8802 -0
  115. package/src/core/Config.js +173 -0
  116. package/src/core/EmotionEngine.js +307 -0
  117. package/src/core/EventBridge.js +180 -0
  118. package/src/core/EventBus.js +117 -0
  119. package/src/core/VoiceSession.js +607 -0
  120. package/src/face/BaseFace.js +259 -0
  121. package/src/face/EyeFace.js +208 -0
  122. package/src/face/HaloSmokeFace.js +509 -0
  123. package/src/face/manifest.json +27 -0
  124. package/src/face/previews/eyes.svg +16 -0
  125. package/src/face/previews/orb.svg +29 -0
  126. package/src/features/MusicPlayer.js +620 -0
  127. package/src/features/Soundboard.js +128 -0
  128. package/src/providers/DeepgramSTT.js +472 -0
  129. package/src/providers/DeepgramStreamingSTT.js +766 -0
  130. package/src/providers/GroqSTT.js +559 -0
  131. package/src/providers/TTSPlayer.js +323 -0
  132. package/src/providers/WebSpeechSTT.js +479 -0
  133. package/src/providers/tts/BaseTTSProvider.js +81 -0
  134. package/src/providers/tts/HumeProvider.js +77 -0
  135. package/src/providers/tts/SupertonicProvider.js +174 -0
  136. package/src/providers/tts/index.js +140 -0
  137. package/src/shell/adapter-registry.js +154 -0
  138. package/src/shell/caller-bridge.js +35 -0
  139. package/src/shell/camera-bridge.js +28 -0
  140. package/src/shell/canvas-bridge.js +32 -0
  141. package/src/shell/commercial-bridge.js +44 -0
  142. package/src/shell/face-bridge.js +44 -0
  143. package/src/shell/music-bridge.js +60 -0
  144. package/src/shell/orchestrator.js +233 -0
  145. package/src/shell/profile-discovery.js +303 -0
  146. package/src/shell/sounds-bridge.js +28 -0
  147. package/src/shell/transcript-bridge.js +61 -0
  148. package/src/shell/waveform-bridge.js +33 -0
  149. package/src/styles/base.css +2862 -0
  150. package/src/styles/face.css +417 -0
  151. package/src/styles/pi-overrides.css +89 -0
  152. package/src/styles/theme-dark.css +67 -0
  153. package/src/test-tts.html +175 -0
  154. package/src/ui/AppShell.js +544 -0
  155. package/src/ui/ProfileSwitcher.js +228 -0
  156. package/src/ui/SessionControl.js +240 -0
  157. package/src/ui/face/FacePicker.js +195 -0
  158. package/src/ui/face/FaceRenderer.js +309 -0
  159. package/src/ui/settings/PlaylistEditor.js +366 -0
  160. package/src/ui/settings/SettingsPanel.css +684 -0
  161. package/src/ui/settings/SettingsPanel.js +419 -0
  162. package/src/ui/settings/TTSVoicePreview.js +210 -0
  163. package/src/ui/themes/ThemeManager.js +213 -0
  164. package/src/ui/visualizers/BaseVisualizer.js +29 -0
  165. package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
  166. package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
  167. package/static/emulators/jsdos/js-dos.css +1 -0
  168. package/static/emulators/jsdos/js-dos.js +22 -0
  169. package/static/favicon.svg +55 -0
  170. package/static/icons/apple-touch-icon.png +0 -0
  171. package/static/icons/favicon-32.png +0 -0
  172. package/static/icons/icon-192.png +0 -0
  173. package/static/icons/icon-512.png +0 -0
  174. package/static/install.html +449 -0
  175. package/static/manifest.json +26 -0
  176. package/static/sw.js +21 -0
  177. package/tts_providers/__init__.py +136 -0
  178. package/tts_providers/base_provider.py +319 -0
  179. package/tts_providers/groq_provider.py +155 -0
  180. package/tts_providers/hume_provider.py +226 -0
  181. package/tts_providers/providers_config.json +119 -0
  182. package/tts_providers/qwen3_provider.py +371 -0
  183. package/tts_providers/resemble_provider.py +315 -0
  184. package/tts_providers/supertonic_provider.py +557 -0
  185. package/tts_providers/supertonic_tts.py +399 -0
@@ -0,0 +1,492 @@
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">
6
+ <title>Upload Center</title>
7
+ <style>
8
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
9
+ :root{
10
+ --bg:#0a0a0a;--bg-1:#111113;--bg-2:#19191b;--bg-3:#222225;
11
+ --border:#2a2a2d;--border-h:#3a3a3d;
12
+ --tx:#ececef;--tx-2:#a0a0a6;--tx-3:#6e6e76;
13
+ --blue:#3b82f6;--blue-dim:rgba(59,130,246,.1);--blue-glow:rgba(59,130,246,.08);
14
+ --green:#22c55e;--green-dim:rgba(34,197,94,.1);
15
+ --red:#ef4444;--red-dim:rgba(239,68,68,.1);
16
+ --amber:#f59e0b;--amber-dim:rgba(245,158,11,.1);
17
+ --cyan:#06b6d4;--cyan-dim:rgba(6,182,212,.1);
18
+ --r:8px;--r-lg:12px;
19
+ }
20
+ html{font-size:15px}
21
+ body{
22
+ font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
23
+ background:var(--bg);color:var(--tx-2);
24
+ min-height:100vh;padding:16px;
25
+ -webkit-font-smoothing:antialiased;
26
+ }
27
+
28
+ /* ── Layout ── */
29
+ .container{max-width:920px;margin:0 auto}
30
+
31
+ /* ── Header ── */
32
+ .hdr{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px;flex-wrap:wrap}
33
+ .hdr-left{display:flex;align-items:center;gap:12px}
34
+ .hdr-icon{width:38px;height:38px;background:var(--blue-dim);border:1px solid rgba(59,130,246,.2);border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
35
+ .hdr-icon svg{width:20px;height:20px;stroke:var(--blue);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
36
+ .hdr h1{font-size:1.15rem;font-weight:600;color:var(--tx);letter-spacing:-.01em}
37
+ .hdr-sub{font-size:.75rem;color:var(--tx-3)}
38
+ .stor{display:flex;align-items:center;gap:8px;font-size:.73rem;color:var(--tx-3)}
39
+ .stor-bar{width:80px;height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden}
40
+ .stor-fill{height:100%;background:var(--blue);border-radius:2px;transition:width .4s}
41
+
42
+ /* ── Drop Zone ── */
43
+ .drop{
44
+ border:1.5px dashed var(--border);border-radius:var(--r-lg);
45
+ padding:36px 20px;text-align:center;cursor:pointer;
46
+ transition:all .2s;position:relative;margin-bottom:20px;
47
+ background:var(--bg-1);
48
+ }
49
+ .drop:hover,.drop.hover{border-color:var(--blue);background:var(--blue-glow)}
50
+ .drop.hover{border-style:solid;box-shadow:0 0 0 4px rgba(59,130,246,.06)}
51
+ .drop-ic{
52
+ width:48px;height:48px;margin:0 auto 12px;
53
+ background:var(--bg-2);border:1px solid var(--border);border-radius:12px;
54
+ display:flex;align-items:center;justify-content:center;
55
+ }
56
+ .drop-ic svg{width:22px;height:22px;stroke:var(--tx-3);fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;transition:stroke .2s}
57
+ .drop:hover .drop-ic svg,.drop.hover .drop-ic svg{stroke:var(--blue)}
58
+ .drop h2{font-size:.92rem;color:var(--tx);font-weight:600;margin-bottom:2px}
59
+ .drop p{font-size:.78rem;color:var(--tx-3);margin-bottom:14px}
60
+ .drop-btns{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
61
+ .btn{
62
+ display:inline-flex;align-items:center;gap:5px;
63
+ padding:7px 16px;border-radius:6px;border:1px solid var(--border);
64
+ background:var(--bg-2);color:var(--tx);font-size:.78rem;font-weight:500;
65
+ cursor:pointer;transition:all .15s;white-space:nowrap;
66
+ }
67
+ .btn:hover{border-color:var(--border-h);background:var(--bg-3)}
68
+ .btn.pri{background:var(--blue);border-color:var(--blue);color:#fff}
69
+ .btn.pri:hover{background:#2563eb}
70
+ .btn svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
71
+ .drop-hint{font-size:.68rem;color:var(--tx-3);margin-top:10px}
72
+ .drop-hint kbd{background:var(--bg-3);border:1px solid var(--border);border-radius:3px;padding:0 4px;font-family:inherit;font-size:.9em}
73
+ input[type="file"]{display:none}
74
+
75
+ /* ── Queue ── */
76
+ .queue{margin-bottom:20px;display:none}
77
+ .queue.on{display:block}
78
+ .lbl{font-size:.72rem;font-weight:600;color:var(--tx-3);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;display:flex;align-items:center;gap:6px}
79
+ .lbl .ct{background:var(--blue-dim);color:var(--blue);padding:0 7px;border-radius:8px;font-size:.85em}
80
+ .q-list{display:flex;flex-direction:column;gap:4px}
81
+ .qi{
82
+ display:grid;grid-template-columns:36px 1fr auto auto;align-items:center;gap:10px;
83
+ background:var(--bg-1);border:1px solid var(--border);border-radius:var(--r);
84
+ padding:8px 12px;transition:border-color .15s;
85
+ }
86
+ .qi:hover{border-color:var(--border-h)}
87
+ .qi-ic{
88
+ width:36px;height:36px;border-radius:6px;
89
+ display:flex;align-items:center;justify-content:center;
90
+ overflow:hidden;flex-shrink:0;
91
+ }
92
+ .qi-ic img{width:100%;height:100%;object-fit:cover;border-radius:6px}
93
+ .qi-ic svg{width:18px;height:18px;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
94
+ .qi-ic.t-img{background:var(--blue-dim)}.qi-ic.t-img svg{stroke:var(--blue)}
95
+ .qi-ic.t-doc{background:var(--cyan-dim)}.qi-ic.t-doc svg{stroke:var(--cyan)}
96
+ .qi-ic.t-aud{background:var(--amber-dim)}.qi-ic.t-aud svg{stroke:var(--amber)}
97
+ .qi-ic.t-vid{background:var(--red-dim)}.qi-ic.t-vid svg{stroke:var(--red)}
98
+ .qi-ic.t-code{background:var(--green-dim)}.qi-ic.t-code svg{stroke:var(--green)}
99
+ .qi-ic.t-oth{background:rgba(255,255,255,.04)}.qi-ic.t-oth svg{stroke:var(--tx-3)}
100
+ .qi-body{min-width:0}
101
+ .qi-name{font-size:.8rem;color:var(--tx);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
102
+ .qi-row{display:flex;align-items:center;gap:8px;margin-top:3px}
103
+ .qi-size{font-size:.7rem;color:var(--tx-3);white-space:nowrap}
104
+ .qi-bar{flex:1;height:3px;background:var(--bg-3);border-radius:2px;overflow:hidden;min-width:60px}
105
+ .qi-bar-f{height:100%;border-radius:2px;transition:width .12s linear;background:var(--blue)}
106
+ .qi-bar-f.ok{background:var(--green)}.qi-bar-f.err{background:var(--red)}
107
+ .qi-st{font-size:.72rem;font-weight:600;white-space:nowrap;min-width:36px;text-align:right}
108
+ .qi-st.up{color:var(--blue)}.qi-st.ok{color:var(--green)}.qi-st.err{color:var(--red)}
109
+ .q-btn{
110
+ width:26px;height:26px;display:flex;align-items:center;justify-content:center;
111
+ border-radius:5px;border:none;background:none;color:var(--tx-3);cursor:pointer;transition:all .12s;
112
+ }
113
+ .q-btn:hover{background:var(--bg-3);color:var(--tx)}
114
+ .q-btn svg{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
115
+
116
+ /* ── Filters ── */
117
+ .gal-hdr{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;flex-wrap:wrap}
118
+ .tabs{display:flex;gap:2px;background:var(--bg-1);border:1px solid var(--border);border-radius:var(--r);padding:2px;flex-wrap:wrap}
119
+ .tab{
120
+ padding:5px 12px;border-radius:6px;border:none;
121
+ background:none;color:var(--tx-3);font-size:.75rem;font-weight:500;
122
+ cursor:pointer;transition:all .15s;white-space:nowrap;
123
+ }
124
+ .tab:hover{color:var(--tx-2)}
125
+ .tab.on{background:var(--bg-3);color:var(--tx)}
126
+ .tab .n{font-size:.85em;opacity:.6;margin-left:3px}
127
+ .search{
128
+ display:flex;align-items:center;gap:6px;
129
+ background:var(--bg-1);border:1px solid var(--border);border-radius:var(--r);
130
+ padding:5px 10px;transition:border-color .15s;
131
+ }
132
+ .search:focus-within{border-color:var(--blue)}
133
+ .search svg{width:14px;height:14px;stroke:var(--tx-3);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
134
+ .search input{background:none;border:none;outline:none;color:var(--tx);font-size:.78rem;width:120px}
135
+ .search input::placeholder{color:var(--tx-3)}
136
+
137
+ /* ── Gallery Grid ── */
138
+ .grid{
139
+ display:grid;
140
+ grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
141
+ gap:8px;margin-bottom:20px;
142
+ }
143
+ .card{
144
+ background:var(--bg-1);border:1px solid var(--border);border-radius:var(--r);
145
+ overflow:hidden;cursor:pointer;transition:all .15s;
146
+ }
147
+ .card:hover{border-color:var(--border-h);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.3)}
148
+ .card-thumb{
149
+ width:100%;aspect-ratio:1;background:var(--bg-2);
150
+ display:flex;align-items:center;justify-content:center;
151
+ overflow:hidden;position:relative;
152
+ }
153
+ .card-thumb img{width:100%;height:100%;object-fit:cover}
154
+ .card-thumb .ct-ic{display:flex;flex-direction:column;align-items:center;gap:4px}
155
+ .card-thumb .ct-ic svg{width:28px;height:28px;fill:none;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}
156
+ .card-thumb .ct-ext{font-size:.6rem;font-weight:700;text-transform:uppercase;padding:1px 5px;border-radius:3px}
157
+ .card-info{padding:7px 9px}
158
+ .card-name{font-size:.72rem;color:var(--tx);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
159
+ .card-meta{font-size:.65rem;color:var(--tx-3);display:flex;justify-content:space-between;margin-top:1px}
160
+ .card-badge{
161
+ position:absolute;top:5px;right:5px;padding:1px 6px;border-radius:3px;
162
+ font-size:.58rem;font-weight:700;text-transform:uppercase;
163
+ background:rgba(0,0,0,.6);backdrop-filter:blur(4px);
164
+ }
165
+
166
+ /* Type colors */
167
+ .c-img{color:var(--blue);stroke:var(--blue)}.bg-img{background:var(--blue-dim)}
168
+ .c-doc{color:var(--cyan);stroke:var(--cyan)}.bg-doc{background:var(--cyan-dim)}
169
+ .c-aud{color:var(--amber);stroke:var(--amber)}.bg-aud{background:var(--amber-dim)}
170
+ .c-vid{color:var(--red);stroke:var(--red)}.bg-vid{background:var(--red-dim)}
171
+ .c-code{color:var(--green);stroke:var(--green)}.bg-code{background:var(--green-dim)}
172
+ .c-oth{color:var(--tx-3);stroke:var(--tx-3)}.bg-oth{background:rgba(255,255,255,.04)}
173
+
174
+ /* ── Empty ── */
175
+ .empty{text-align:center;padding:48px 20px;color:var(--tx-3)}
176
+ .empty svg{width:40px;height:40px;stroke:var(--border);fill:none;stroke-width:1.2;margin-bottom:10px;stroke-linecap:round;stroke-linejoin:round}
177
+ .empty p{font-size:.82rem}
178
+
179
+ /* ── Storage ── */
180
+ .stor-card{background:var(--bg-1);border:1px solid var(--border);border-radius:var(--r-lg);padding:14px 16px;margin-bottom:20px}
181
+ .stor-main{width:100%;height:6px;background:var(--bg-3);border-radius:3px;overflow:hidden;display:flex;margin-top:8px}
182
+ .stor-main>div{height:100%;transition:width .4s}
183
+ .stor-rows{display:flex;flex-direction:column;gap:4px;margin-top:10px}
184
+ .stor-row{display:flex;align-items:center;gap:8px;font-size:.73rem}
185
+ .stor-row .d{width:6px;height:6px;border-radius:50%;flex-shrink:0}
186
+ .stor-row .l{flex:1;color:var(--tx-2)}
187
+ .stor-row .v{color:var(--tx-3);font-variant-numeric:tabular-nums}
188
+
189
+ /* ── Lightbox ── */
190
+ .lb{position:fixed;inset:0;background:rgba(0,0,0,.88);display:none;align-items:center;justify-content:center;z-index:200;backdrop-filter:blur(4px)}
191
+ .lb.on{display:flex}
192
+ .lb img,.lb video,.lb audio{max-width:90vw;max-height:82vh;border-radius:6px}
193
+ .lb-x{position:absolute;top:14px;right:14px;width:32px;height:32px;background:rgba(255,255,255,.08);border:none;border-radius:6px;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center}
194
+ .lb-x svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
195
+ .lb-bar{
196
+ position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
197
+ background:var(--bg-1);border:1px solid var(--border);border-radius:var(--r);
198
+ padding:8px 14px;display:flex;align-items:center;gap:10px;font-size:.78rem;
199
+ }
200
+ .lb-bar .nm{color:var(--tx);font-weight:500}
201
+ .lb-bar .sz{color:var(--tx-3)}
202
+ .lb-btn{
203
+ padding:4px 10px;border-radius:5px;border:1px solid var(--border);
204
+ background:var(--bg-2);color:var(--tx);font-size:.75rem;cursor:pointer;
205
+ display:inline-flex;align-items:center;gap:4px;text-decoration:none;transition:all .12s;
206
+ }
207
+ .lb-btn:hover{border-color:var(--border-h);background:var(--bg-3)}
208
+ .lb-btn svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
209
+
210
+ /* ── Toast ── */
211
+ .toast{
212
+ position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(60px);
213
+ background:var(--bg-2);border:1px solid var(--border);color:var(--tx);
214
+ padding:8px 18px;border-radius:var(--r);font-size:.78rem;font-weight:500;
215
+ box-shadow:0 8px 24px rgba(0,0,0,.5);transition:transform .25s;z-index:300;pointer-events:none;
216
+ }
217
+ .toast.on{transform:translateX(-50%) translateY(0)}
218
+
219
+ /* ── Responsive ── */
220
+ @media(max-width:640px){
221
+ body{padding:12px}
222
+ .hdr{flex-direction:column;align-items:flex-start}
223
+ .drop{padding:28px 16px}
224
+ .drop-btns{flex-direction:column;align-items:stretch}
225
+ .btn{justify-content:center}
226
+ .grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:6px}
227
+ .gal-hdr{flex-direction:column;align-items:stretch}
228
+ .tabs{overflow-x:auto;flex-wrap:nowrap}
229
+ .search input{width:100%}
230
+ .qi{grid-template-columns:32px 1fr auto;gap:8px}
231
+ .qi .q-btn{display:none}
232
+ }
233
+ @media(max-width:380px){
234
+ .grid{grid-template-columns:repeat(2,1fr)}
235
+ }
236
+ </style>
237
+ </head>
238
+ <body>
239
+ <div class="container">
240
+
241
+ <!-- Header -->
242
+ <div class="hdr">
243
+ <div class="hdr-left">
244
+ <div class="hdr-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M7.5 13v2.5a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V13"/><line x1="12" y1="15" x2="12" y2="7.5"/><polyline points="9 10 12 7 15 10"/></svg></div>
245
+ <div><h1>Upload Center</h1><div class="hdr-sub">Get files into your workspace</div></div>
246
+ </div>
247
+ <div class="stor" id="storPill"><div class="stor-bar"><div class="stor-fill" id="storFill" style="width:0"></div></div><span id="storLabel">--</span></div>
248
+ </div>
249
+
250
+ <!-- Drop Zone -->
251
+ <div class="drop" id="drop" tabindex="0" role="button" aria-label="Drop files here or click to browse">
252
+ <div class="drop-ic"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M7.5 13v2.5a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V13"/><line x1="12" y1="15" x2="12" y2="7.5"/><polyline points="9 10 12 7 15 10"/></svg></div>
253
+ <h2>Drop files here</h2>
254
+ <p>Images, documents, audio, video, code</p>
255
+ <div class="drop-btns">
256
+ <button class="btn pri" onclick="$('fi').click()"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>Browse Files</button>
257
+ <button class="btn" onclick="$('fo').click()"><svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>Folder</button>
258
+ <button class="btn" onclick="urlImport()"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>URL</button>
259
+ </div>
260
+ <div class="drop-hint"><kbd>Ctrl</kbd>+<kbd>V</kbd> to paste from clipboard</div>
261
+ <input type="file" id="fi" multiple>
262
+ <input type="file" id="fo" webkitdirectory multiple>
263
+ </div>
264
+
265
+ <!-- Upload Queue -->
266
+ <div class="queue" id="queue">
267
+ <div class="lbl">Uploading <span class="ct" id="qCt">0</span></div>
268
+ <div class="q-list" id="qList"></div>
269
+ </div>
270
+
271
+ <!-- Storage -->
272
+ <div class="stor-card" id="storCard">
273
+ <div class="lbl">Storage</div>
274
+ <div class="stor-main" id="storBar"></div>
275
+ <div class="stor-rows" id="storRows"></div>
276
+ </div>
277
+
278
+ <!-- Gallery -->
279
+ <div class="gal-hdr">
280
+ <div class="tabs" id="tabs" role="tablist"></div>
281
+ <div class="search"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input type="text" id="srch" placeholder="Search files..." aria-label="Search files"></div>
282
+ </div>
283
+ <div class="grid" id="grid"></div>
284
+ <div class="empty" id="empty" style="display:none"><svg viewBox="0 0 24 24"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg><p>No files yet — drop something above</p></div>
285
+
286
+ <!-- Lightbox -->
287
+ <div class="lb" id="lb"><button class="lb-x" onclick="lbClose()"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button><div id="lbC"></div><div class="lb-bar" id="lbBar"></div></div>
288
+
289
+ <!-- Toast -->
290
+ <div class="toast" id="toast"></div>
291
+ </div>
292
+
293
+ <script>
294
+ const $=id=>document.getElementById(id);
295
+ const IMG=new Set('jpg jpeg png gif webp bmp tiff svg'.split(' '));
296
+ const DOC=new Set('pdf docx xlsx pptx txt md csv log'.split(' '));
297
+ const AUD=new Set('mp3 wav ogg flac m4a aac'.split(' '));
298
+ const VID=new Set('mp4 webm mov avi mkv'.split(' '));
299
+ const COD=new Set('py js ts json yaml yml html css'.split(' '));
300
+
301
+ let files=[],filter='all',qid=0,ups=[];
302
+
303
+ function ext(n){return(n.split('.').pop()||'').toLowerCase()}
304
+ function cat(e){return IMG.has(e)?'img':DOC.has(e)?'doc':AUD.has(e)?'aud':VID.has(e)?'vid':COD.has(e)?'code':'oth'}
305
+ function sz(b){if(!b)return'0 B';const k=1024,s=['B','KB','MB','GB'],i=Math.floor(Math.log(b)/Math.log(k));return(b/Math.pow(k,i)).toFixed(i?1:0)+' '+s[i]}
306
+ function ago(t){const d=Date.now()/1000-t;return d<60?'now':d<3600?Math.floor(d/60)+'m':d<86400?Math.floor(d/3600)+'h':d<604800?Math.floor(d/86400)+'d':new Date(t*1000).toLocaleDateString()}
307
+ function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
308
+ function toast(m){const t=$('toast');t.textContent=m;t.classList.add('on');setTimeout(()=>t.classList.remove('on'),2e3)}
309
+ function cpUrl(u){navigator.clipboard.writeText(u).then(()=>toast('URL copied'))}
310
+
311
+ // SVG icons per type
312
+ const IC={
313
+ img:'<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
314
+ doc:'<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
315
+ aud:'<svg viewBox="0 0 24 24"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
316
+ vid:'<svg viewBox="0 0 24 24"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>',
317
+ code:'<svg viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
318
+ oth:'<svg viewBox="0 0 24 24"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>'
319
+ };
320
+ const COLORS={img:'var(--blue)',doc:'var(--cyan)',aud:'var(--amber)',vid:'var(--red)',code:'var(--green)',oth:'var(--tx-3)'};
321
+
322
+ // ── Drag & Drop ──
323
+ let dc=0;const dz=$('drop');
324
+ document.addEventListener('dragenter',e=>{e.preventDefault();dc++;dz.classList.add('hover')});
325
+ document.addEventListener('dragleave',e=>{e.preventDefault();dc--;if(dc<=0){dc=0;dz.classList.remove('hover')}});
326
+ document.addEventListener('dragover',e=>e.preventDefault());
327
+ document.addEventListener('drop',e=>{e.preventDefault();dc=0;dz.classList.remove('hover');if(e.dataTransfer.files.length)handle(e.dataTransfer.files)});
328
+ $('fi').onchange=e=>{if(e.target.files.length)handle(e.target.files);e.target.value=''};
329
+ $('fo').onchange=e=>{if(e.target.files.length)handle(e.target.files);e.target.value=''};
330
+
331
+ // ── Clipboard ──
332
+ document.addEventListener('paste',e=>{
333
+ const items=e.clipboardData?.items;if(!items)return;
334
+ const f=[];for(const i of items)if(i.kind==='file'){const file=i.getAsFile();if(file)f.push(file)}
335
+ if(f.length){e.preventDefault();handle(f)}
336
+ });
337
+
338
+ // ── URL Import ──
339
+ function urlImport(){
340
+ const u=prompt('URL to download:');if(!u||!u.trim())return;
341
+ const id=++qid,name=u.trim().split('/').pop().split('?')[0]||'download';
342
+ addQI(id,name,0,'oth');
343
+ setQI(id,-1,'dl');
344
+ fetch(u.trim()).then(r=>{if(!r.ok)throw new Error(r.status);return r.blob()})
345
+ .then(b=>{upload(new File([b],name,{type:b.type}),id)})
346
+ .catch(e=>setQI(id,0,'err',e.message));
347
+ }
348
+
349
+ function handle(fl){Array.from(fl).forEach(f=>{const id=++qid;addQI(id,f.name,f.size,cat(ext(f.name)),f);upload(f,id)})}
350
+
351
+ // ── Upload ──
352
+ function upload(file,id){
353
+ const xhr=new XMLHttpRequest();ups.push({id,xhr});
354
+ xhr.upload.onprogress=e=>{if(e.lengthComputable)setQI(id,Math.round(e.loaded/e.total*100),'up')};
355
+ xhr.onload=()=>{
356
+ ups=ups.filter(u=>u.id!==id);
357
+ if(xhr.status>=200&&xhr.status<300){
358
+ try{
359
+ const r=JSON.parse(xhr.responseText);
360
+ setQI(id,100,'ok',null,r);
361
+ const e=ext(r.original_name||file.name);
362
+ files.unshift({name:r.original_name||file.name,url:r.url,size:file.size,ext:e,cat:cat(e),modified:Date.now()/1000});
363
+ render();updateStor();
364
+ }catch(e){setQI(id,0,'err','Parse error')}
365
+ }else{
366
+ let m='Error '+xhr.status;try{m=JSON.parse(xhr.responseText).error||m}catch(e){}
367
+ setQI(id,0,'err',m);
368
+ }
369
+ };
370
+ xhr.onerror=()=>{setQI(id,0,'err','Network error');ups=ups.filter(u=>u.id!==id)};
371
+ const fd=new FormData();fd.append('file',file);xhr.open('POST','/api/upload');
372
+ if(window._canvasAuthToken)xhr.setRequestHeader('Authorization','Bearer '+window._canvasAuthToken);
373
+ xhr.send(fd);
374
+ }
375
+
376
+ // ── Queue UI ──
377
+ function addQI(id,name,size,c,file){
378
+ $('queue').classList.add('on');
379
+ let th='';
380
+ if(file&&file.type&&file.type.startsWith('image/')){const u=URL.createObjectURL(file);th=`<img src="${u}" onload="try{URL.revokeObjectURL(this.src)}catch(e){}">`}
381
+ else th=IC[c]||IC.oth;
382
+ const el=document.createElement('div');el.className='qi';el.id='qi'+id;
383
+ el.innerHTML=`<div class="qi-ic t-${c}">${th}</div><div class="qi-body"><div class="qi-name">${esc(name)}</div><div class="qi-row"><span class="qi-size">${sz(size)}</span><div class="qi-bar"><div class="qi-bar-f" id="qb${id}"></div></div></div></div><div class="qi-st up" id="qs${id}">0%</div><button class="q-btn" title="Cancel" onclick="cancelQ(${id})"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`;
384
+ $('qList').prepend(el);updQCt();
385
+ }
386
+
387
+ function setQI(id,pct,st,msg,res){
388
+ const b=$('qb'+id),s=$('qs'+id);if(!b||!s)return;
389
+ if(st==='up'){b.style.width=pct+'%';b.className='qi-bar-f';s.textContent=pct+'%';s.className='qi-st up'}
390
+ else if(st==='dl'){b.style.width='50%';s.textContent='Fetching';s.className='qi-st up'}
391
+ else if(st==='ok'){
392
+ b.style.width='100%';b.className='qi-bar-f ok';s.textContent='Done';s.className='qi-st ok';
393
+ const el=$('qi'+id);if(el&&res){const a=el.querySelector('.q-btn');if(a)a.outerHTML=`<button class="q-btn" title="Copy URL" onclick="cpUrl('${res.url}')"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>`}
394
+ }
395
+ else if(st==='err'){b.style.width='100%';b.className='qi-bar-f err';s.textContent=msg||'Error';s.className='qi-st err'}
396
+ updQCt();
397
+ }
398
+
399
+ function cancelQ(id){const u=ups.find(x=>x.id===id);if(u){u.xhr.abort();ups=ups.filter(x=>x.id!==id)}const el=$('qi'+id);if(el)el.remove();updQCt()}
400
+ function updQCt(){const active=document.querySelectorAll('.qi-st.up').length;$('qCt').textContent=active;if(!document.querySelectorAll('.qi').length)$('queue').classList.remove('on')}
401
+
402
+ // ── Tabs ──
403
+ const TABS=[{k:'all',l:'All'},{k:'img',l:'Images'},{k:'doc',l:'Docs'},{k:'aud',l:'Audio'},{k:'vid',l:'Video'},{k:'code',l:'Code'}];
404
+ function renderTabs(){
405
+ const cts={all:files.length};TABS.forEach(t=>{if(t.k!=='all')cts[t.k]=files.filter(f=>f.cat===t.k).length});
406
+ $('tabs').innerHTML=TABS.map(t=>`<button class="tab${filter===t.k?' on':''}" data-f="${t.k}" role="tab" aria-selected="${filter===t.k}">${t.l}<span class="n">${cts[t.k]||0}</span></button>`).join('');
407
+ }
408
+ $('tabs').onclick=e=>{const t=e.target.closest('.tab');if(!t)return;filter=t.dataset.f;render()};
409
+ $('srch').oninput=()=>render();
410
+
411
+ // ── Gallery ──
412
+ function render(){
413
+ renderTabs();
414
+ const s=$('srch').value.toLowerCase();
415
+ let list=files;
416
+ if(filter!=='all')list=list.filter(f=>f.cat===filter);
417
+ if(s)list=list.filter(f=>f.name.toLowerCase().includes(s));
418
+ if(!list.length){$('grid').innerHTML='';$('empty').style.display='block';return}
419
+ $('empty').style.display='none';
420
+ $('grid').innerHTML=list.map((f,i)=>{
421
+ const isImg=f.cat==='img';const e=f.ext.toUpperCase();
422
+ return`<div class="card" onclick="lbOpen(${i},'${filter}','${s.replace(/'/g,"\\'")}')" role="button" tabindex="0" aria-label="${esc(f.name)}">
423
+ <div class="card-thumb">${isImg?`<img src="${f.url}" loading="lazy" alt="">`:`<div class="ct-ic">${IC[f.cat]?.replace(/stroke-width="[^"]*"/,'stroke-width="1.4"').replace(/<svg/,`<svg style="stroke:${COLORS[f.cat]}"`)}<span class="ct-ext bg-${f.cat} c-${f.cat}">${e}</span></div>`}<div class="card-badge c-${f.cat}">${e}</div></div>
424
+ <div class="card-info"><div class="card-name">${esc(f.name)}</div><div class="card-meta"><span>${sz(f.size)}</span><span>${ago(f.modified)}</span></div></div></div>`}).join('');
425
+ }
426
+
427
+ // ── Lightbox ──
428
+ function lbOpen(i,flt,srch){
429
+ let list=files;if(flt!=='all')list=list.filter(f=>f.cat===flt);if(srch)list=list.filter(f=>f.name.toLowerCase().includes(srch));
430
+ const f=list[i];if(!f)return;
431
+ const c=$('lbC'),bar=$('lbBar');
432
+ if(f.cat==='img')c.innerHTML=`<img src="${f.url}" alt="${esc(f.name)}">`;
433
+ else if(f.cat==='vid')c.innerHTML=`<video src="${f.url}" controls autoplay style="max-width:90vw;max-height:80vh"></video>`;
434
+ else if(f.cat==='aud')c.innerHTML=`<div style="background:var(--bg-1);padding:32px 48px;border-radius:12px;text-align:center"><div style="opacity:.4;margin-bottom:12px">${IC.aud.replace(/<svg/,'<svg style="width:48px;height:48px;stroke:var(--tx-3)"')}</div><div style="color:var(--tx);font-weight:500;margin-bottom:14px">${esc(f.name)}</div><audio src="${f.url}" controls autoplay style="width:100%"></audio></div>`;
435
+ else c.innerHTML=`<div style="background:var(--bg-1);padding:32px 48px;border-radius:12px;text-align:center"><div style="opacity:.3;margin-bottom:10px">${(IC[f.cat]||IC.oth).replace(/<svg/,'<svg style="width:48px;height:48px;stroke:var(--tx-3)"')}</div><div style="color:var(--tx);font-weight:500">${esc(f.name)}</div><div style="color:var(--tx-3);font-size:.82rem;margin-top:4px">${f.ext.toUpperCase()} &middot; ${sz(f.size)}</div></div>`;
436
+ const full=location.origin+f.url;
437
+ bar.innerHTML=`<span class="nm">${esc(f.name)}</span><span class="sz">${sz(f.size)}</span><button class="lb-btn" onclick="cpUrl('${full}')"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Copy</button><a class="lb-btn" href="${f.url}" download="${esc(f.name)}" style="text-decoration:none"><svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Download</a>`;
438
+ $('lb').classList.add('on');
439
+ }
440
+ function lbClose(){$('lb').classList.remove('on');$('lb').querySelectorAll('video,audio').forEach(e=>{e.pause();e.src=''});$('lbC').innerHTML=''}
441
+ $('lb').onclick=e=>{if(e.target===$('lb'))lbClose()};
442
+ document.addEventListener('keydown',e=>{if(e.key==='Escape')lbClose()});
443
+
444
+ // ── Storage ──
445
+ async function updateStor(){
446
+ try{
447
+ const r=await (window.authFetch||fetch)('/api/workspace/browse?path=Uploads');if(!r.ok)return;
448
+ const d=await r.json();
449
+ let tot=0;const bk={img:0,doc:0,aud:0,vid:0,code:0,oth:0};
450
+ (d.entries||[]).filter(e=>e.type==='file').forEach(e=>{
451
+ const c=cat(ext(e.name)),s=e.size||0;tot+=s;bk[c]+=s;
452
+ });
453
+ const max=10*1024*1024*1024,pct=Math.min(100,tot/max*100);
454
+ $('storFill').style.width=pct+'%';
455
+ $('storLabel').textContent=sz(tot)+' used';
456
+ const colors={img:'var(--blue)',doc:'var(--cyan)',aud:'var(--amber)',vid:'var(--red)',code:'var(--green)',oth:'var(--tx-3)'};
457
+ const names={img:'Images',doc:'Documents',aud:'Audio',vid:'Video',code:'Code',oth:'Other'};
458
+ let bHtml='',rHtml='';
459
+ for(const[k,v]of Object.entries(bk)){if(!v)continue;
460
+ bHtml+=`<div style="width:${Math.max(1,v/Math.max(tot,1)*100)}%;background:${colors[k]}"></div>`;
461
+ rHtml+=`<div class="stor-row"><div class="d" style="background:${colors[k]}"></div><div class="l">${names[k]}</div><div class="v">${sz(v)}</div></div>`;
462
+ }
463
+ $('storBar').innerHTML=bHtml;$('storRows').innerHTML=rHtml;
464
+ }catch(e){$('storLabel').textContent='--'}
465
+ }
466
+
467
+ // ── Load existing ──
468
+ async function load(){
469
+ try{
470
+ const r=await (window.authFetch||fetch)('/api/workspace/browse?path=Uploads');if(!r.ok)return;
471
+ const d=await r.json();
472
+ files=(d.entries||[]).filter(e=>e.type==='file').map(e=>{
473
+ const x=ext(e.name);return{name:e.name,url:'/uploads/'+e.name,size:e.size||0,ext:x,cat:cat(x),modified:e.modified||0}
474
+ }).sort((a,b)=>b.modified-a.modified);
475
+ render();
476
+ }catch(e){}
477
+ }
478
+
479
+ // Wait briefly for auth token from parent before loading (token arrives via postMessage).
480
+ // If no token arrives within 200ms (e.g. auth disabled), proceed without it.
481
+ function init(){load();updateStor()}
482
+ if(window._canvasAuthToken){init()}else{
483
+ let inited=false;
484
+ const origHandler=window.onmessage;
485
+ window.addEventListener('message',function onAuth(e){
486
+ if(e.data&&e.data.type==='auth-token'&&!inited){inited=true;init()}
487
+ });
488
+ setTimeout(()=>{if(!inited){inited=true;init()}},200);
489
+ }
490
+ </script>
491
+ </body>
492
+ </html>