myaiforone 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 (315) hide show
  1. package/README.md +113 -0
  2. package/agents/_template/CLAUDE.md +18 -0
  3. package/agents/_template/agent.json +7 -0
  4. package/agents/platform/agentcreator/CLAUDE.md +300 -0
  5. package/agents/platform/appcreator/CLAUDE.md +158 -0
  6. package/agents/platform/gym/CLAUDE.md +486 -0
  7. package/agents/platform/gym/agent.json +40 -0
  8. package/agents/platform/gym/programs/agent-building/program.json +160 -0
  9. package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
  10. package/agents/platform/gym/programs/getting-started/program.json +124 -0
  11. package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
  12. package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
  13. package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
  14. package/agents/platform/gym/souls/alex.md +12 -0
  15. package/agents/platform/gym/souls/jordan.md +12 -0
  16. package/agents/platform/gym/souls/morgan.md +12 -0
  17. package/agents/platform/gym/souls/riley.md +12 -0
  18. package/agents/platform/gym/souls/sam.md +12 -0
  19. package/agents/platform/hub/CLAUDE.md +372 -0
  20. package/agents/platform/promptcreator/CLAUDE.md +130 -0
  21. package/agents/platform/skillcreator/CLAUDE.md +163 -0
  22. package/bin/cli.js +566 -0
  23. package/config.example.json +310 -0
  24. package/dist/agent-registry.d.ts +32 -0
  25. package/dist/agent-registry.d.ts.map +1 -0
  26. package/dist/agent-registry.js +144 -0
  27. package/dist/agent-registry.js.map +1 -0
  28. package/dist/channels/discord.d.ts +17 -0
  29. package/dist/channels/discord.d.ts.map +1 -0
  30. package/dist/channels/discord.js +114 -0
  31. package/dist/channels/discord.js.map +1 -0
  32. package/dist/channels/imessage.d.ts +23 -0
  33. package/dist/channels/imessage.d.ts.map +1 -0
  34. package/dist/channels/imessage.js +214 -0
  35. package/dist/channels/imessage.js.map +1 -0
  36. package/dist/channels/slack.d.ts +19 -0
  37. package/dist/channels/slack.d.ts.map +1 -0
  38. package/dist/channels/slack.js +167 -0
  39. package/dist/channels/slack.js.map +1 -0
  40. package/dist/channels/telegram.d.ts +19 -0
  41. package/dist/channels/telegram.d.ts.map +1 -0
  42. package/dist/channels/telegram.js +274 -0
  43. package/dist/channels/telegram.js.map +1 -0
  44. package/dist/channels/types.d.ts +44 -0
  45. package/dist/channels/types.d.ts.map +1 -0
  46. package/dist/channels/types.js +18 -0
  47. package/dist/channels/types.js.map +1 -0
  48. package/dist/channels/whatsapp.d.ts +23 -0
  49. package/dist/channels/whatsapp.d.ts.map +1 -0
  50. package/dist/channels/whatsapp.js +189 -0
  51. package/dist/channels/whatsapp.js.map +1 -0
  52. package/dist/config.d.ts +134 -0
  53. package/dist/config.d.ts.map +1 -0
  54. package/dist/config.js +127 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/cron.d.ts +8 -0
  57. package/dist/cron.d.ts.map +1 -0
  58. package/dist/cron.js +35 -0
  59. package/dist/cron.js.map +1 -0
  60. package/dist/decrypt-keys.d.ts +7 -0
  61. package/dist/decrypt-keys.d.ts.map +1 -0
  62. package/dist/decrypt-keys.js +53 -0
  63. package/dist/decrypt-keys.js.map +1 -0
  64. package/dist/encrypt-keys.d.ts +8 -0
  65. package/dist/encrypt-keys.d.ts.map +1 -0
  66. package/dist/encrypt-keys.js +62 -0
  67. package/dist/encrypt-keys.js.map +1 -0
  68. package/dist/executor.d.ts +31 -0
  69. package/dist/executor.d.ts.map +1 -0
  70. package/dist/executor.js +2009 -0
  71. package/dist/executor.js.map +1 -0
  72. package/dist/gemini-executor.d.ts +27 -0
  73. package/dist/gemini-executor.d.ts.map +1 -0
  74. package/dist/gemini-executor.js +160 -0
  75. package/dist/gemini-executor.js.map +1 -0
  76. package/dist/goals.d.ts +24 -0
  77. package/dist/goals.d.ts.map +1 -0
  78. package/dist/goals.js +189 -0
  79. package/dist/goals.js.map +1 -0
  80. package/dist/gym/activity-digest.d.ts +30 -0
  81. package/dist/gym/activity-digest.d.ts.map +1 -0
  82. package/dist/gym/activity-digest.js +506 -0
  83. package/dist/gym/activity-digest.js.map +1 -0
  84. package/dist/gym/dimension-scorer.d.ts +76 -0
  85. package/dist/gym/dimension-scorer.d.ts.map +1 -0
  86. package/dist/gym/dimension-scorer.js +236 -0
  87. package/dist/gym/dimension-scorer.js.map +1 -0
  88. package/dist/gym/gym-router.d.ts +7 -0
  89. package/dist/gym/gym-router.d.ts.map +1 -0
  90. package/dist/gym/gym-router.js +718 -0
  91. package/dist/gym/gym-router.js.map +1 -0
  92. package/dist/gym/index.d.ts +11 -0
  93. package/dist/gym/index.d.ts.map +1 -0
  94. package/dist/gym/index.js +11 -0
  95. package/dist/gym/index.js.map +1 -0
  96. package/dist/heartbeat.d.ts +21 -0
  97. package/dist/heartbeat.d.ts.map +1 -0
  98. package/dist/heartbeat.js +163 -0
  99. package/dist/heartbeat.js.map +1 -0
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +254 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/keystore.d.ts +22 -0
  105. package/dist/keystore.d.ts.map +1 -0
  106. package/dist/keystore.js +178 -0
  107. package/dist/keystore.js.map +1 -0
  108. package/dist/logger.d.ts +9 -0
  109. package/dist/logger.d.ts.map +1 -0
  110. package/dist/logger.js +45 -0
  111. package/dist/logger.js.map +1 -0
  112. package/dist/memory/daily.d.ts +22 -0
  113. package/dist/memory/daily.d.ts.map +1 -0
  114. package/dist/memory/daily.js +82 -0
  115. package/dist/memory/daily.js.map +1 -0
  116. package/dist/memory/embeddings.d.ts +15 -0
  117. package/dist/memory/embeddings.d.ts.map +1 -0
  118. package/dist/memory/embeddings.js +154 -0
  119. package/dist/memory/embeddings.js.map +1 -0
  120. package/dist/memory/index.d.ts +32 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +159 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/search.d.ts +21 -0
  125. package/dist/memory/search.d.ts.map +1 -0
  126. package/dist/memory/search.js +77 -0
  127. package/dist/memory/search.js.map +1 -0
  128. package/dist/memory/store.d.ts +23 -0
  129. package/dist/memory/store.d.ts.map +1 -0
  130. package/dist/memory/store.js +144 -0
  131. package/dist/memory/store.js.map +1 -0
  132. package/dist/ollama-executor.d.ts +17 -0
  133. package/dist/ollama-executor.d.ts.map +1 -0
  134. package/dist/ollama-executor.js +112 -0
  135. package/dist/ollama-executor.js.map +1 -0
  136. package/dist/openai-executor.d.ts +38 -0
  137. package/dist/openai-executor.d.ts.map +1 -0
  138. package/dist/openai-executor.js +197 -0
  139. package/dist/openai-executor.js.map +1 -0
  140. package/dist/router.d.ts +11 -0
  141. package/dist/router.d.ts.map +1 -0
  142. package/dist/router.js +185 -0
  143. package/dist/router.js.map +1 -0
  144. package/dist/test-message.d.ts +2 -0
  145. package/dist/test-message.d.ts.map +1 -0
  146. package/dist/test-message.js +60 -0
  147. package/dist/test-message.js.map +1 -0
  148. package/dist/utils/imsg-db-reader.d.ts +24 -0
  149. package/dist/utils/imsg-db-reader.d.ts.map +1 -0
  150. package/dist/utils/imsg-db-reader.js +92 -0
  151. package/dist/utils/imsg-db-reader.js.map +1 -0
  152. package/dist/utils/imsg-rpc.d.ts +25 -0
  153. package/dist/utils/imsg-rpc.d.ts.map +1 -0
  154. package/dist/utils/imsg-rpc.js +149 -0
  155. package/dist/utils/imsg-rpc.js.map +1 -0
  156. package/dist/utils/message-formatter.d.ts +3 -0
  157. package/dist/utils/message-formatter.d.ts.map +1 -0
  158. package/dist/utils/message-formatter.js +69 -0
  159. package/dist/utils/message-formatter.js.map +1 -0
  160. package/dist/web-ui.d.ts +12 -0
  161. package/dist/web-ui.d.ts.map +1 -0
  162. package/dist/web-ui.js +5784 -0
  163. package/dist/web-ui.js.map +1 -0
  164. package/dist/whatsapp-chats.d.ts +2 -0
  165. package/dist/whatsapp-chats.d.ts.map +1 -0
  166. package/dist/whatsapp-chats.js +76 -0
  167. package/dist/whatsapp-chats.js.map +1 -0
  168. package/dist/whatsapp-login.d.ts +2 -0
  169. package/dist/whatsapp-login.d.ts.map +1 -0
  170. package/dist/whatsapp-login.js +90 -0
  171. package/dist/whatsapp-login.js.map +1 -0
  172. package/dist/wiki-sync.d.ts +21 -0
  173. package/dist/wiki-sync.d.ts.map +1 -0
  174. package/dist/wiki-sync.js +147 -0
  175. package/dist/wiki-sync.js.map +1 -0
  176. package/docs/AddNewAgentGuide.md +100 -0
  177. package/docs/AddNewMcpGuide.md +72 -0
  178. package/docs/Architecture.md +795 -0
  179. package/docs/CLAUDE-AI-SETUP.md +166 -0
  180. package/docs/Setup.md +297 -0
  181. package/docs/ai-gym-architecture.md +1040 -0
  182. package/docs/ai-gym-build-plan.md +343 -0
  183. package/docs/ai-gym-onboarding.md +122 -0
  184. package/docs/appcreator_plan.md +348 -0
  185. package/docs/platform-mcp-audit.md +320 -0
  186. package/docs/server-deployment-plan.md +503 -0
  187. package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
  188. package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
  189. package/docs/user-guide.md +2016 -0
  190. package/mcp-catalog.json +628 -0
  191. package/package.json +63 -0
  192. package/public/MyAIforOne-logomark-512.svg +16 -0
  193. package/public/MyAIforOne-logomark-transparent.svg +15 -0
  194. package/public/activity.html +314 -0
  195. package/public/admin.html +1674 -0
  196. package/public/agent-dashboard.html +670 -0
  197. package/public/api-docs.html +1106 -0
  198. package/public/automations.html +722 -0
  199. package/public/canvas.css +223 -0
  200. package/public/canvas.js +588 -0
  201. package/public/changelog.html +231 -0
  202. package/public/gym.html +2766 -0
  203. package/public/home.html +1930 -0
  204. package/public/index.html +2809 -0
  205. package/public/lab.html +1643 -0
  206. package/public/library.html +1442 -0
  207. package/public/marketplace.html +1101 -0
  208. package/public/mcp-docs.html +441 -0
  209. package/public/mini.html +390 -0
  210. package/public/monitor.html +584 -0
  211. package/public/org.html +4304 -0
  212. package/public/projects.html +734 -0
  213. package/public/settings.html +645 -0
  214. package/public/tasks.html +932 -0
  215. package/public/trainers/alex.svg +12 -0
  216. package/public/trainers/jordan.svg +12 -0
  217. package/public/trainers/morgan.svg +12 -0
  218. package/public/trainers/riley.svg +12 -0
  219. package/public/trainers/sam.svg +12 -0
  220. package/public/user-guide.html +218 -0
  221. package/registry/agents.json +3 -0
  222. package/registry/apps.json +20 -0
  223. package/registry/installed-drafts.json +3 -0
  224. package/registry/mcps.json +1084 -0
  225. package/registry/prompts/personal/mcp-test-prompt.md +6 -0
  226. package/registry/prompts/personal/memory-recall.md +6 -0
  227. package/registry/prompts/platform/brainstorm.md +15 -0
  228. package/registry/prompts/platform/code-review.md +16 -0
  229. package/registry/prompts/platform/explain.md +16 -0
  230. package/registry/prompts.json +58 -0
  231. package/registry/skills/external/brainstorming.md +5 -0
  232. package/registry/skills/external/code-review.md +40 -0
  233. package/registry/skills/external/frontend-patterns.md +642 -0
  234. package/registry/skills/external/frontend-slides.md +184 -0
  235. package/registry/skills/external/systematic-debugging.md +5 -0
  236. package/registry/skills/external/tdd.md +328 -0
  237. package/registry/skills/external/verification-before-completion.md +5 -0
  238. package/registry/skills/external/writing-plans.md +5 -0
  239. package/registry/skills/platform/ai41_app_build.md +930 -0
  240. package/registry/skills/platform/ai41_app_deploy.md +168 -0
  241. package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
  242. package/registry/skills/platform/ai41_app_patterns.md +359 -0
  243. package/registry/skills/platform/ai41_app_register.md +85 -0
  244. package/registry/skills/platform/ai41_app_scaffold.md +421 -0
  245. package/registry/skills/platform/ai41_app_verify.md +107 -0
  246. package/registry/skills/platform/opProjectCreate.md +239 -0
  247. package/registry/skills/platform/op_devbrowser.md +136 -0
  248. package/registry/skills/platform/sop_brandguidelines.md +103 -0
  249. package/registry/skills/platform/sop_docx.md +117 -0
  250. package/registry/skills/platform/sop_frontenddesign.md +44 -0
  251. package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
  252. package/registry/skills/platform/sop_mcpbuilder.md +133 -0
  253. package/registry/skills/platform/sop_pdf.md +172 -0
  254. package/registry/skills/platform/sop_pptx.md +133 -0
  255. package/registry/skills/platform/sop_skillcreator.md +104 -0
  256. package/registry/skills/platform/sop_themefactory.md +128 -0
  257. package/registry/skills/platform/sop_webapptesting.md +75 -0
  258. package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
  259. package/registry/skills/platform/sop_xlsx.md +134 -0
  260. package/registry/skills.json +1055 -0
  261. package/scripts/discover-chats.sh +11 -0
  262. package/scripts/install-service-windows.ps1 +87 -0
  263. package/scripts/install-service.sh +52 -0
  264. package/scripts/seed-registry.ts +195 -0
  265. package/scripts/test-send.sh +5 -0
  266. package/scripts/tray-indicator.ps1 +35 -0
  267. package/scripts/uninstall-service-windows.ps1 +23 -0
  268. package/scripts/uninstall-service.sh +15 -0
  269. package/scripts/xbar-myagent.5s.sh +32 -0
  270. package/server/mcp-server/dist/index.d.ts +11 -0
  271. package/server/mcp-server/dist/index.js +1332 -0
  272. package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
  273. package/server/mcp-server/dist/lib/api-client.js +241 -0
  274. package/server/mcp-server/index.ts +1545 -0
  275. package/server/mcp-server/lib/api-client.ts +366 -0
  276. package/server/mcp-server/tsconfig.json +14 -0
  277. package/src/agent-registry.ts +180 -0
  278. package/src/channels/discord.ts +129 -0
  279. package/src/channels/imessage.ts +261 -0
  280. package/src/channels/slack.ts +208 -0
  281. package/src/channels/telegram.ts +307 -0
  282. package/src/channels/types.ts +62 -0
  283. package/src/channels/whatsapp.ts +227 -0
  284. package/src/config.ts +281 -0
  285. package/src/cron.ts +43 -0
  286. package/src/decrypt-keys.ts +60 -0
  287. package/src/encrypt-keys.ts +70 -0
  288. package/src/executor.ts +2190 -0
  289. package/src/gemini-executor.ts +212 -0
  290. package/src/goals.ts +240 -0
  291. package/src/gym/activity-digest.ts +546 -0
  292. package/src/gym/dimension-scorer.ts +297 -0
  293. package/src/gym/gym-router.ts +801 -0
  294. package/src/gym/index.ts +19 -0
  295. package/src/heartbeat.ts +220 -0
  296. package/src/index.ts +275 -0
  297. package/src/keystore.ts +190 -0
  298. package/src/logger.ts +51 -0
  299. package/src/memory/daily.ts +101 -0
  300. package/src/memory/embeddings.ts +185 -0
  301. package/src/memory/index.ts +218 -0
  302. package/src/memory/search.ts +124 -0
  303. package/src/memory/store.ts +189 -0
  304. package/src/ollama-executor.ts +126 -0
  305. package/src/openai-executor.ts +259 -0
  306. package/src/router.ts +230 -0
  307. package/src/test-message.ts +72 -0
  308. package/src/utils/imsg-db-reader.ts +109 -0
  309. package/src/utils/imsg-rpc.ts +178 -0
  310. package/src/utils/message-formatter.ts +90 -0
  311. package/src/web-ui.ts +5778 -0
  312. package/src/whatsapp-chats.ts +91 -0
  313. package/src/whatsapp-login.ts +110 -0
  314. package/src/wiki-sync.ts +199 -0
  315. package/tsconfig.json +19 -0
@@ -0,0 +1,2766 @@
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>MyAIforOne — AI Gym</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
8
+ <style>
9
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
10
+ :root{
11
+ --bg-deep:#060a13;
12
+ --bg-surface:rgba(12,18,33,0.92);
13
+ --bg-card:rgba(16,22,40,0.85);
14
+ --bg-input:rgba(0,0,0,0.35);
15
+ --border-dim:rgba(56,189,248,0.08);
16
+ --border-glow:rgba(56,189,248,0.18);
17
+ --border-active:rgba(56,189,248,0.45);
18
+ --text-primary:rgba(255,255,255,0.92);
19
+ --text-secondary:rgba(255,255,255,0.68);
20
+ --text-muted:rgba(148,163,184,0.55);
21
+ --accent:#22d3ee;
22
+ --accent-soft:#38bdf8;
23
+ --accent-bg:rgba(6,182,212,0.15);
24
+ --purple:rgba(139,92,246,0.7);
25
+ --purple-bg:rgba(139,92,246,0.12);
26
+ --green:#4ade80;
27
+ --green-bg:rgba(74,222,128,0.1);
28
+ --amber:#fbbf24;
29
+ --amber-bg:rgba(251,191,36,0.1);
30
+ --red:#f87171;
31
+ --red-bg:rgba(248,113,113,0.1);
32
+ --shadow:0 2px 12px rgba(0,0,0,0.3);
33
+ --radius:12px;
34
+ --font-sans:'DM Sans',system-ui,-apple-system,sans-serif;
35
+ --font-mono:'IBM Plex Mono',monospace;
36
+ --font-display:'Syne',sans-serif;
37
+ }
38
+ [data-theme="light"]{
39
+ --bg-deep:#f4f6f9;
40
+ --bg-surface:rgba(255,255,255,0.95);
41
+ --bg-card:rgba(255,255,255,0.9);
42
+ --bg-input:rgba(0,0,0,0.04);
43
+ --border-dim:rgba(0,0,0,0.08);
44
+ --border-glow:rgba(14,116,144,0.18);
45
+ --border-active:rgba(14,116,144,0.45);
46
+ --text-primary:rgba(15,23,42,0.92);
47
+ --text-secondary:rgba(51,65,85,0.8);
48
+ --text-muted:rgba(100,116,139,0.6);
49
+ --accent:#0891b2;
50
+ --accent-soft:#0e7490;
51
+ --accent-bg:rgba(14,116,144,0.08);
52
+ --purple:rgba(109,40,217,0.75);
53
+ --purple-bg:rgba(139,92,246,0.08);
54
+ --green:#16a34a;
55
+ --green-bg:rgba(22,163,74,0.08);
56
+ --amber:#d97706;
57
+ --amber-bg:rgba(217,119,6,0.08);
58
+ --red:#dc2626;
59
+ --red-bg:rgba(220,38,38,0.08);
60
+ --shadow:0 1px 8px rgba(0,0,0,0.06);
61
+ }
62
+ html,body{width:100%;height:100%;overflow:hidden;background:var(--bg-deep);font-family:var(--font-sans);color:var(--text-primary);transition:background .3s,color .3s}
63
+
64
+ /* ─── Layout ──────────────────────────────── */
65
+ .gym-app{display:flex;flex-direction:column;height:100vh}
66
+
67
+ /* ─── Topbar ──────────────────────────────── */
68
+ .topbar{height:48px;display:flex;align-items:center;padding:0 20px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);flex-shrink:0;overflow:hidden;max-width:100vw}
69
+ .tab-group{display:flex;gap:0;flex-shrink:1;min-width:0}
70
+ .tab-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;padding:14px 12px;cursor:pointer;position:relative;transition:color .2s;text-decoration:none;display:block;white-space:nowrap;flex-shrink:0}
71
+ .tab-btn:hover{color:var(--text-secondary)}
72
+ .tab-btn.active{color:var(--accent)}
73
+ .tab-btn.active::after{content:'';position:absolute;bottom:0;left:10px;right:10px;height:2px;background:var(--accent);border-radius:1px}
74
+ .topbar-right{margin-left:auto;display:flex;align-items:center;gap:10px;flex-shrink:0}
75
+ .gym-nav-btn{font-family:var(--font-sans);font-size:12px;font-weight:700;color:#fff;background:var(--accent);border:none;border-radius:8px;padding:6px 14px;cursor:pointer;text-decoration:none;letter-spacing:.02em;transition:opacity .2s,transform .2s;white-space:nowrap}
76
+ .gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
77
+ .logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
78
+ .logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
79
+ .theme-toggle{background:none;border:1px solid var(--border-dim);border-radius:6px;padding:4px 8px;cursor:pointer;font-size:14px;color:var(--text-muted);transition:all .2s}
80
+ .theme-toggle:hover{border-color:var(--border-glow);color:var(--text-secondary)}
81
+
82
+ /* ─── Onboarding ──────────────────────────── */
83
+ .onboarding{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;overflow-y:auto}
84
+ .onboarding.hidden{display:none}
85
+ .ob-step{display:none;flex-direction:column;align-items:center;width:100%;max-width:800px;animation:fadeUp .4s ease}
86
+ .ob-step.active{display:flex}
87
+ @keyframes fadeUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
88
+
89
+ .ob-title{font-family:var(--font-display);font-size:26px;font-weight:800;color:var(--accent);margin-bottom:6px;text-align:center}
90
+ .ob-sub{font-size:14px;color:var(--text-muted);margin-bottom:32px;text-align:center}
91
+
92
+ /* Step indicators */
93
+ .step-dots{display:flex;gap:8px;margin-bottom:24px}
94
+ .step-dot{width:8px;height:8px;border-radius:50%;background:var(--border-dim);transition:all .3s}
95
+ .step-dot.active{background:var(--accent);box-shadow:0 0 8px rgba(34,211,238,0.4)}
96
+ .step-dot.done{background:var(--green)}
97
+
98
+ /* Trainer cards */
99
+ .trainer-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;width:100%;max-width:720px;margin-bottom:28px}
100
+ .trainer-card{
101
+ background:var(--bg-card);border:2px solid var(--border-dim);border-radius:var(--radius);
102
+ padding:20px 16px;cursor:pointer;transition:all .3s;text-align:center;
103
+ backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
104
+ }
105
+ .trainer-card:hover{border-color:var(--border-glow);transform:translateY(-2px);box-shadow:var(--shadow)}
106
+ .trainer-card.selected{border-color:var(--accent);box-shadow:0 0 20px rgba(34,211,238,0.15)}
107
+ .trainer-avatar{
108
+ width:56px;height:56px;border-radius:50%;margin:0 auto 12px;
109
+ display:flex;align-items:center;justify-content:center;
110
+ font-family:var(--font-display);font-size:18px;font-weight:800;color:#fff;
111
+ }
112
+ .trainer-name{font-family:var(--font-display);font-size:16px;font-weight:700;color:var(--text-primary);margin-bottom:4px}
113
+ .trainer-style{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:.04em;margin-bottom:10px}
114
+ .trainer-quote{font-size:12px;color:var(--text-secondary);line-height:1.5;font-style:italic}
115
+
116
+ /* Step 2 — Get to know you */
117
+ .ob-coach-header{display:flex;align-items:center;gap:12px;margin-bottom:20px}
118
+ .ob-coach-avatar{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:var(--font-display);font-size:15px;font-weight:800;color:#fff}
119
+ .ob-coach-name{font-family:var(--font-display);font-size:16px;font-weight:700;color:var(--text-primary)}
120
+ .ob-message{
121
+ background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);
122
+ padding:20px;font-size:13px;line-height:1.7;color:var(--text-secondary);
123
+ max-width:600px;width:100%;margin-bottom:20px;backdrop-filter:blur(12px);
124
+ }
125
+ .ob-textarea{
126
+ width:100%;max-width:600px;min-height:120px;padding:14px;border-radius:var(--radius);
127
+ border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);
128
+ font-family:var(--font-sans);font-size:13px;line-height:1.6;resize:vertical;outline:none;
129
+ transition:border-color .2s;
130
+ }
131
+ .ob-textarea:focus{border-color:var(--border-glow)}
132
+ .ob-textarea::placeholder{color:var(--text-muted)}
133
+
134
+ /* File attachment */
135
+ .ob-attach-zone{
136
+ width:100%;max-width:600px;margin-top:10px;padding:14px;border-radius:var(--radius);
137
+ border:2px dashed var(--border-dim);background:var(--bg-input);cursor:pointer;
138
+ text-align:center;color:var(--text-muted);font-size:12px;transition:all .2s;
139
+ }
140
+ .ob-attach-zone:hover,.ob-attach-zone.dragover{border-color:var(--accent);color:var(--text-secondary)}
141
+ .ob-attach-zone input[type="file"]{display:none}
142
+ .ob-file-list{width:100%;max-width:600px;margin-top:8px;display:flex;flex-direction:column;gap:6px}
143
+ .ob-file-pill{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;background:var(--bg-card);border:1px solid var(--border-dim);font-size:12px}
144
+ .ob-file-pill .fname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary)}
145
+ .ob-file-pill .fsize{color:var(--text-muted);font-size:11px;flex-shrink:0}
146
+ .ob-file-pill .fremove{color:var(--red);cursor:pointer;font-size:14px;flex-shrink:0;line-height:1}
147
+
148
+ /* Step 3 — Plan */
149
+ .ob-loading{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px}
150
+ .ob-spinner{width:40px;height:40px;border:3px solid var(--border-dim);border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite}
151
+ @keyframes spin{to{transform:rotate(360deg)}}
152
+ .ob-plan-summary{
153
+ background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);
154
+ padding:24px;max-width:600px;width:100%;backdrop-filter:blur(12px);
155
+ }
156
+ .ob-plan-summary.hidden{display:none}
157
+ .ob-bucket{margin-bottom:16px}
158
+ .ob-bucket-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--accent);margin-bottom:6px}
159
+ .ob-bucket-text{font-size:12px;color:var(--text-secondary);line-height:1.6}
160
+
161
+ /* Navigation buttons */
162
+ .ob-nav{display:flex;gap:12px;margin-top:24px}
163
+ .ob-btn{
164
+ font-family:var(--font-sans);font-size:13px;font-weight:600;padding:10px 24px;
165
+ border-radius:8px;border:1px solid var(--border-dim);background:transparent;
166
+ color:var(--text-secondary);cursor:pointer;transition:all .2s;
167
+ }
168
+ .ob-btn:hover{border-color:var(--border-glow);color:var(--text-primary)}
169
+ .ob-btn.primary{background:var(--accent);color:#000;border-color:var(--accent)}
170
+ .ob-btn.primary:hover{filter:brightness(1.1)}
171
+ .ob-btn:disabled{opacity:.4;cursor:not-allowed}
172
+
173
+ /* ─── Mode Picker ────────────────────────── */
174
+ .mode-picker{
175
+ flex:1;display:none;align-items:center;justify-content:center;
176
+ overflow:auto;padding:40px 20px;
177
+ }
178
+ .mode-picker.active{display:flex}
179
+ .mode-picker-inner{max-width:820px;width:100%;text-align:center}
180
+ .mode-greeting{
181
+ font-family:var(--font-display);font-size:28px;font-weight:700;
182
+ color:var(--text-primary);margin-bottom:36px;
183
+ }
184
+ .mode-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
185
+ .mode-tile{
186
+ position:relative;padding:28px 20px;border-radius:16px;
187
+ border:1px solid var(--border-dim);background:var(--bg-surface);
188
+ cursor:pointer;transition:all .25s ease;text-align:center;
189
+ display:flex;flex-direction:column;align-items:center;gap:8px;
190
+ }
191
+ .mode-tile:hover{border-color:var(--border-glow);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.2)}
192
+ .mode-tile.recommended{
193
+ border-color:var(--accent);background:var(--accent-bg);
194
+ box-shadow:0 0 20px rgba(34,211,238,0.08);
195
+ }
196
+ .mode-tile.recommended:hover{box-shadow:0 8px 32px rgba(34,211,238,0.15)}
197
+ .mode-tile-badge{
198
+ position:absolute;top:-10px;left:50%;transform:translateX(-50%);
199
+ background:var(--accent);color:var(--bg-deep);font-size:11px;font-weight:600;
200
+ padding:2px 12px;border-radius:10px;white-space:nowrap;
201
+ }
202
+ .mode-tile-icon{font-size:36px;margin-bottom:4px}
203
+ .mode-tile-title{font-family:var(--font-display);font-size:17px;font-weight:700;color:var(--text-primary)}
204
+ .mode-tile-desc{font-size:13px;color:var(--text-secondary);line-height:1.5;margin-top:4px}
205
+ .mode-streak{margin-top:28px;font-size:14px;color:var(--text-muted)}
206
+ @media(max-width:700px){
207
+ .mode-tiles{grid-template-columns:1fr}
208
+ .mode-greeting{font-size:22px;margin-bottom:24px}
209
+ }
210
+
211
+ /* ─── Main Gym View ───────────────────────── */
212
+ .gym-main{flex:1;display:none;overflow:hidden}
213
+ .gym-main.active{display:flex}
214
+
215
+ /* Left sidebar */
216
+ .gym-sidebar{width:320px;min-width:200px;max-width:600px;flex-shrink:0;background:var(--bg-surface);border-right:1px solid var(--border-dim);display:flex;flex-direction:column;overflow:hidden;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:relative}
217
+ .gym-sidebar-inner{flex:1;overflow-y:auto;padding:16px}
218
+ .gym-sidebar-inner::-webkit-scrollbar{width:3px}
219
+ .gym-sidebar-inner::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.2);border-radius:2px}
220
+ .gym-resize-handle{position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize;z-index:10;background:transparent;transition:background .2s}
221
+ .gym-resize-handle:hover,.gym-resize-handle.active{background:var(--accent);opacity:.4}
222
+
223
+ .streak-box{
224
+ background:var(--accent-bg);border:1px solid var(--border-glow);border-radius:var(--radius);
225
+ padding:14px;text-align:center;margin-bottom:20px;
226
+ }
227
+ .streak-fire{font-size:28px;margin-bottom:4px}
228
+ .streak-count{font-family:var(--font-display);font-size:22px;font-weight:800;color:var(--accent)}
229
+ .streak-label{font-size:10px;color:var(--text-muted);margin-top:2px}
230
+
231
+ .sidebar-section-title{
232
+ font-family:var(--font-display);font-size:11px;font-weight:700;color:var(--text-muted);
233
+ text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px;padding:0 2px;
234
+ }
235
+
236
+ /* Sidebar nav */
237
+ .gym-nav{display:flex;flex-direction:column;gap:2px;margin-bottom:20px}
238
+ .gym-nav-item{
239
+ display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:8px;
240
+ border:none;background:none;cursor:pointer;font-family:var(--font-sans);
241
+ font-size:13px;font-weight:600;color:var(--text-secondary);transition:all .2s;text-align:left;width:100%;
242
+ }
243
+ .gym-nav-item:hover{background:var(--accent-bg);color:var(--text-primary)}
244
+ .gym-nav-item.active{background:var(--accent-bg);color:var(--accent)}
245
+ .gym-nav-icon{font-size:15px;width:22px;text-align:center;flex-shrink:0}
246
+ .gym-nav-label{flex:1}
247
+
248
+ /* ─── Center Panel ────────────────────────── */
249
+ .gym-center{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
250
+ .gym-tabs{display:flex;border-bottom:1px solid var(--border-dim);flex-shrink:0;background:var(--bg-surface)}
251
+ .gym-tabs-right{display:flex;align-items:center;gap:2px;margin-right:8px}
252
+ .gym-tab{
253
+ font-family:var(--font-display);font-size:13px;font-weight:700;color:var(--text-muted);
254
+ padding:12px 20px;cursor:pointer;border:none;background:none;position:relative;transition:color .2s;
255
+ }
256
+ .gym-tab:hover{color:var(--text-secondary)}
257
+ .gym-tab.active{color:var(--accent)}
258
+ .gym-tab.active::after{content:'';position:absolute;bottom:0;left:12px;right:12px;height:2px;background:var(--accent);border-radius:1px}
259
+
260
+ /* View panels */
261
+ .gym-view{display:none;flex:1;flex-direction:column;overflow:hidden}
262
+ .gym-view.active{display:flex}
263
+
264
+ .coach-header{
265
+ display:flex;align-items:center;gap:10px;padding:12px 20px;
266
+ border-bottom:1px solid var(--border-dim);flex-shrink:0;background:var(--bg-surface);
267
+ }
268
+ .coach-avatar{
269
+ width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;
270
+ font-family:var(--font-display);font-size:11px;font-weight:800;color:#fff;
271
+ }
272
+ .coach-name{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary)}
273
+ .coach-status{font-size:10px;color:var(--text-muted)}
274
+
275
+ .chat-messages{
276
+ flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:12px;
277
+ }
278
+ .chat-messages::-webkit-scrollbar{width:4px}
279
+ .chat-messages::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.15);border-radius:2px}
280
+
281
+ .chat-msg{max-width:85%;padding:12px 16px;border-radius:var(--radius);font-size:13px;line-height:1.6}
282
+ .chat-msg.assistant{
283
+ background:var(--bg-card);border:1px solid var(--border-dim);color:var(--text-secondary);
284
+ align-self:flex-start;border-top-left-radius:4px;
285
+ }
286
+ .chat-msg.user{
287
+ background:var(--accent-bg);border:1px solid rgba(34,211,238,0.12);color:var(--text-primary);
288
+ align-self:flex-end;border-top-right-radius:4px;
289
+ }
290
+ .chat-msg pre{font-family:var(--font-mono);font-size:11px;background:var(--bg-input);padding:8px;border-radius:6px;overflow-x:auto;margin:8px 0}
291
+ .chat-msg code{font-family:var(--font-mono);font-size:11px;background:var(--bg-input);padding:1px 4px;border-radius:3px}
292
+ .chat-typing{display:flex;gap:4px;padding:8px 16px;align-self:flex-start}
293
+ .chat-typing span{width:6px;height:6px;border-radius:50%;background:var(--text-muted);animation:typeBounce 1.4s ease infinite}
294
+ .chat-typing span:nth-child(2){animation-delay:.2s}
295
+ .chat-typing span:nth-child(3){animation-delay:.4s}
296
+ @keyframes typeBounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
297
+
298
+ .chat-input-area{
299
+ display:flex;align-items:flex-end;gap:8px;padding:12px 20px;
300
+ border-top:1px solid var(--border-dim);background:var(--bg-surface);flex-shrink:0;
301
+ }
302
+ .chat-textarea{
303
+ flex:1;padding:10px 12px;border-radius:10px;border:1px solid var(--border-dim);
304
+ background:var(--bg-input);color:var(--text-primary);font-family:var(--font-sans);
305
+ font-size:13px;resize:none;outline:none;max-height:120px;line-height:1.5;
306
+ transition:border-color .2s;
307
+ }
308
+ .chat-textarea:focus{border-color:var(--border-glow)}
309
+ .chat-textarea::placeholder{color:var(--text-muted)}
310
+ .chat-send{
311
+ width:40px;height:40px;border-radius:10px;border:none;background:var(--accent);
312
+ color:#000;cursor:pointer;font-size:16px;font-weight:700;display:flex;
313
+ align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;
314
+ }
315
+ .chat-send:hover{filter:brightness(1.1);transform:scale(1.05)}
316
+ .chat-send:disabled{opacity:.4;cursor:not-allowed;transform:none}
317
+
318
+ .chat-input-wrap{display:flex;flex-direction:column;flex-shrink:0}
319
+ .chat-action-btns{display:flex;gap:6px;padding:6px 14px 10px;flex-wrap:wrap}
320
+ .chat-action-btns:empty{display:none}
321
+ .chat-action-btn{
322
+ border:1px solid var(--border-dim);padding:5px 12px;border-radius:8px;
323
+ font-size:11px;font-family:var(--font-sans);font-weight:600;cursor:pointer;
324
+ background:transparent;color:var(--text-secondary);transition:all .2s;white-space:nowrap;
325
+ }
326
+ .chat-action-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
327
+ .chat-action-btn.primary{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
328
+
329
+ /* ─── Chat toolbar buttons ────────────────── */
330
+ .gym-clip-btn,.gym-mic-btn,.gym-raw-btn{
331
+ width:36px;height:36px;border-radius:8px;border:1px solid var(--border-dim);
332
+ background:transparent;color:var(--text-muted);cursor:pointer;
333
+ display:flex;align-items:center;justify-content:center;
334
+ font-size:16px;transition:all .2s;flex-shrink:0;
335
+ }
336
+ .gym-clip-btn:hover,.gym-mic-btn:hover,.gym-raw-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
337
+ .gym-clip-btn.has-files{border-color:var(--accent);color:var(--accent)}
338
+ .gym-mic-btn.recording{border-color:rgba(239,68,68,.5);background:rgba(239,68,68,.1);color:#ef4444;animation:gymMicPulse 1s ease-in-out infinite}
339
+ .gym-raw-btn.active{border-color:var(--accent);color:var(--accent);background:rgba(139,92,246,0.1)}
340
+ @keyframes gymMicPulse{0%,100%{opacity:1}50%{opacity:.6}}
341
+ .gym-file-drop{
342
+ display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
343
+ padding:10px 14px;border:2px dashed var(--border-glow);border-radius:10px;
344
+ background:var(--bg-input);text-align:center;font-size:11px;color:var(--text-muted);
345
+ cursor:pointer;transition:all .2s;min-height:50px;
346
+ }
347
+ .gym-file-drop.hidden{display:none}
348
+ .gym-file-drop.dragover{border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
349
+ .gym-file-drop input[type=file]{display:none}
350
+ .gym-file-list{display:flex;flex-wrap:wrap;gap:4px;width:100%;justify-content:flex-start}
351
+ .gym-file-pill{
352
+ display:inline-flex;align-items:center;gap:4px;
353
+ padding:3px 8px;border-radius:6px;
354
+ background:var(--bg-surface);border:1px solid var(--border-dim);
355
+ font-family:var(--font-mono);font-size:9px;color:var(--text-primary);max-width:180px;
356
+ }
357
+ .gym-file-pill-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
358
+ .gym-file-pill-rm{cursor:pointer;color:var(--text-muted);font-size:12px}
359
+ .gym-file-pill-rm:hover{color:#ef4444}
360
+ .gym-raw-drawer{
361
+ display:none;max-height:200px;overflow-y:auto;padding:8px 12px;
362
+ background:var(--bg-input);border-top:1px solid var(--border-dim);
363
+ font-family:var(--font-mono);font-size:10px;color:var(--text-muted);border-radius:0 0 8px 8px;
364
+ }
365
+ .gym-raw-drawer.open{display:block}
366
+ .gym-raw-drawer pre{margin:0;white-space:pre-wrap;word-break:break-all}
367
+ .gym-raw-drawer .log-line{opacity:0.7}
368
+ .gym-raw-drawer .log-line:last-child{opacity:1}
369
+ .gym-raw-drawer .log-stderr{color:#ef4444}
370
+ .gym-raw-drawer .log-dim{opacity:0.4}
371
+ .gym-raw-header{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:4px}
372
+ .gym-raw-mode-btn{background:none;border:1px solid var(--border-dim);color:var(--text-muted);font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;font-family:var(--font-mono)}
373
+ .gym-raw-mode-btn:hover{border-color:var(--accent);color:var(--accent)}
374
+
375
+ /* ─── Progress Tab ────────────────────────── */
376
+ .progress-panel{padding:24px;overflow-y:auto}
377
+ .progress-panel::-webkit-scrollbar{width:4px}
378
+ .progress-panel::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.15);border-radius:2px}
379
+
380
+ /* Radar chart */
381
+ .radar-wrap{display:flex;justify-content:center;margin-bottom:32px}
382
+ .radar-container{position:relative;width:280px;height:280px}
383
+ .radar-svg{width:100%;height:100%}
384
+ .radar-grid{fill:none;stroke:var(--border-dim);stroke-width:1}
385
+ .radar-axis{stroke:var(--border-dim);stroke-width:0.5}
386
+ .radar-shape{fill:rgba(34,211,238,0.15);stroke:var(--accent);stroke-width:2;transition:all .6s ease}
387
+ .radar-point{fill:var(--accent);r:4;transition:all .6s ease}
388
+ .radar-label{font-family:var(--font-display);font-size:10px;font-weight:700;fill:var(--text-secondary);text-anchor:middle}
389
+
390
+ /* Dimension breakdown */
391
+ .dim-section{margin-bottom:28px}
392
+ .dim-section-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary);margin-bottom:14px}
393
+ .dim-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border-dim)}
394
+ .dim-row:last-child{border-bottom:none}
395
+ .dim-label{font-family:var(--font-display);font-size:12px;font-weight:700;color:var(--text-primary);min-width:110px}
396
+ .dim-bar-wrap{flex:1;height:6px;background:var(--bg-input);border-radius:3px;overflow:hidden}
397
+ .dim-bar{height:100%;border-radius:3px;transition:width .6s ease;background:var(--accent)}
398
+ .dim-score{font-family:var(--font-mono);font-size:11px;color:var(--text-muted);min-width:28px;text-align:right}
399
+ .dim-trend{font-size:12px;min-width:16px;text-align:center}
400
+ .dim-trend.up{color:var(--green)}
401
+ .dim-trend.down{color:var(--red)}
402
+ .dim-trend.stable{color:var(--text-muted)}
403
+
404
+ /* Next focus */
405
+ .next-focus{
406
+ background:var(--accent-bg);border:1px solid var(--border-glow);border-radius:var(--radius);
407
+ padding:16px;margin-bottom:28px;
408
+ }
409
+ .next-focus-title{font-family:var(--font-display);font-size:12px;font-weight:700;color:var(--accent);margin-bottom:6px}
410
+ .next-focus-text{font-size:12px;color:var(--text-secondary);line-height:1.5}
411
+
412
+ /* Milestones */
413
+ .milestones-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary);margin-bottom:14px}
414
+ .milestone-list{list-style:none}
415
+ .milestone-item{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid var(--border-dim)}
416
+ .milestone-item:last-child{border-bottom:none}
417
+ .milestone-dot{width:8px;height:8px;border-radius:50%;background:var(--accent);margin-top:5px;flex-shrink:0}
418
+ .milestone-content{flex:1}
419
+ .milestone-text{font-size:12px;color:var(--text-primary);font-weight:500;margin-bottom:2px}
420
+ .milestone-date{font-family:var(--font-mono);font-size:10px;color:var(--text-muted)}
421
+
422
+ /* ─── Bottom Strip ────────────────────────── */
423
+ /* Feed strip styles moved to new Feed section below */
424
+
425
+ /* ─── Scrollbar global ────────────────────── */
426
+ ::-webkit-scrollbar{width:4px}
427
+ ::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.15);border-radius:2px}
428
+ ::-webkit-scrollbar-track{background:transparent}
429
+
430
+ /* ─── History Chart ──────────────────────── */
431
+ .history-section{margin-top:28px}
432
+ .history-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary);margin-bottom:14px}
433
+ .history-empty{font-size:11px;color:var(--text-muted);padding:20px;text-align:center;background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius)}
434
+ .history-chart{position:relative;width:100%;height:200px;background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);padding:16px;overflow:hidden}
435
+ .history-svg{width:100%;height:100%}
436
+ .history-gridline{stroke:var(--border-dim);stroke-width:0.5;stroke-dasharray:4 4}
437
+ .history-line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:d .6s ease}
438
+ .history-dot{r:3;transition:all .3s}
439
+ .history-legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:10px}
440
+ .history-legend-item{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text-muted)}
441
+ .history-legend-swatch{width:12px;height:3px;border-radius:2px}
442
+ .history-x-label{font-family:var(--font-mono);font-size:8px;fill:var(--text-muted);text-anchor:middle}
443
+ .history-y-label{font-family:var(--font-mono);font-size:8px;fill:var(--text-muted);text-anchor:end}
444
+
445
+ /* ─── Guides Grid ────────────────────────── */
446
+ .guides-header{display:flex;align-items:center;justify-content:space-between;padding:20px;border-bottom:1px solid var(--border-dim);flex-shrink:0}
447
+ .guides-title{font-family:var(--font-display);font-size:18px;font-weight:700;color:var(--text-primary)}
448
+ .guides-search{
449
+ width:240px;padding:8px 14px;border-radius:8px;border:1px solid var(--border-dim);
450
+ background:var(--bg-input);color:var(--text-primary);font-family:var(--font-sans);
451
+ font-size:13px;outline:none;transition:border-color .2s;
452
+ }
453
+ .guides-search:focus{border-color:var(--border-glow)}
454
+ .guides-search::placeholder{color:var(--text-muted)}
455
+ .guides-filters{display:flex;gap:6px;padding:0 20px 12px;flex-shrink:0}
456
+ .guides-filter-pill{
457
+ font-family:var(--font-mono);font-size:10px;font-weight:600;padding:5px 12px;
458
+ border-radius:16px;border:1px solid var(--border-dim);background:none;
459
+ color:var(--text-muted);cursor:pointer;transition:all .2s;letter-spacing:.02em;
460
+ }
461
+ .guides-filter-pill:hover{border-color:var(--border-glow);color:var(--text-secondary)}
462
+ .guides-filter-pill.active{background:var(--accent-bg);border-color:var(--accent);color:var(--accent)}
463
+ .guides-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;padding:20px;overflow-y:auto;flex:1}
464
+ .guide-card{
465
+ background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);
466
+ padding:20px;cursor:pointer;transition:all .2s;display:flex;flex-direction:column;
467
+ backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
468
+ }
469
+ .guide-card:hover{border-color:var(--border-glow);transform:translateY(-2px);box-shadow:var(--shadow)}
470
+ .guide-card-title{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--text-primary);margin-bottom:6px}
471
+ .guide-card-desc{font-size:12px;color:var(--text-secondary);line-height:1.5;margin-bottom:12px;flex:1;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
472
+ .guide-card-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
473
+ .guide-card-tag{font-family:var(--font-mono);font-size:10px;padding:2px 8px;border-radius:4px;background:var(--accent-bg);color:var(--accent)}
474
+ .guide-card-tag.difficulty{background:var(--purple-bg);color:var(--purple)}
475
+ .guide-card-footer{display:flex;align-items:center;justify-content:space-between}
476
+ .guide-card-cta{
477
+ font-family:var(--font-sans);font-size:12px;font-weight:700;padding:8px 18px;
478
+ border-radius:8px;border:none;background:var(--accent);color:#fff;cursor:pointer;transition:all .2s;
479
+ }
480
+ .guide-card-cta:hover{opacity:.88;transform:translateY(-1px)}
481
+
482
+ /* ─── Guide Viewer ───────────────────────── */
483
+ .gv-wrapper{display:flex;flex:1;overflow:hidden}
484
+ .gv-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--border-dim);min-width:0}
485
+ .gv-topbar{display:flex;align-items:center;gap:12px;padding:12px 20px;border-bottom:1px solid var(--border-dim);flex-shrink:0;background:var(--bg-surface)}
486
+ .gv-back{font-family:var(--font-sans);font-size:12px;color:var(--accent);background:none;border:none;cursor:pointer;font-weight:600;white-space:nowrap}
487
+ .gv-back:hover{text-decoration:underline}
488
+ .gv-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
489
+ .gv-modules{display:flex;gap:0;overflow-x:auto;border-bottom:1px solid var(--border-dim);flex-shrink:0;background:var(--bg-surface)}
490
+ .gv-modules::-webkit-scrollbar{height:2px}
491
+ .gv-modules::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.2);border-radius:2px}
492
+ .gv-module-tab{
493
+ font-family:var(--font-display);font-size:11px;font-weight:700;color:var(--text-muted);
494
+ padding:10px 16px;cursor:pointer;border:none;background:none;position:relative;
495
+ transition:color .2s;white-space:nowrap;flex-shrink:0;
496
+ }
497
+ .gv-module-tab:hover{color:var(--text-secondary)}
498
+ .gv-module-tab.active{color:var(--accent)}
499
+ .gv-module-tab.active::after{content:'';position:absolute;bottom:0;left:8px;right:8px;height:2px;background:var(--accent);border-radius:1px}
500
+ .gv-step-pills{display:flex;gap:6px;padding:10px 20px;border-bottom:1px solid var(--border-dim);flex-shrink:0;overflow-x:auto;background:var(--bg-surface)}
501
+ .gv-step-pill{
502
+ font-family:var(--font-mono);font-size:10px;padding:4px 10px;border-radius:12px;
503
+ border:1px solid var(--border-dim);background:none;color:var(--text-muted);
504
+ cursor:pointer;transition:all .2s;white-space:nowrap;flex-shrink:0;
505
+ }
506
+ .gv-step-pill:hover{border-color:var(--border-glow);color:var(--text-secondary)}
507
+ .gv-step-pill.active{background:var(--accent-bg);border-color:var(--accent);color:var(--accent)}
508
+ .gv-step-pill.done{color:var(--green);border-color:rgba(74,222,128,0.3)}
509
+ .gv-step-content{flex:1;overflow-y:auto;padding:24px;font-size:13px;line-height:1.7;color:var(--text-secondary)}
510
+ .gv-step-content::-webkit-scrollbar{width:4px}
511
+ .gv-step-content::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.15);border-radius:2px}
512
+ .gv-step-content h1,.gv-step-content h2,.gv-step-content h3{font-family:var(--font-display);color:var(--text-primary);margin:16px 0 8px}
513
+ .gv-step-content h1{font-size:18px}.gv-step-content h2{font-size:15px}.gv-step-content h3{font-size:13px}
514
+ .gv-step-content ul,.gv-step-content ol{padding-left:20px;margin:8px 0}
515
+ .gv-step-content li{margin-bottom:4px}
516
+ .gv-step-content blockquote{border-left:3px solid var(--accent);padding:8px 16px;margin:12px 0;color:var(--text-muted);background:var(--accent-bg);border-radius:0 8px 8px 0}
517
+ .gv-step-footer{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid var(--border-dim);flex-shrink:0;background:var(--bg-surface)}
518
+ .gv-check{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-secondary);cursor:pointer}
519
+ .gv-check input{accent-color:var(--accent)}
520
+ .gv-nav-btns{display:flex;gap:8px}
521
+ .gv-nav-btn{
522
+ font-family:var(--font-sans);font-size:11px;font-weight:600;padding:6px 14px;
523
+ border-radius:6px;border:1px solid var(--border-dim);background:none;
524
+ color:var(--text-secondary);cursor:pointer;transition:all .2s;
525
+ }
526
+ .gv-nav-btn:hover{border-color:var(--border-glow);color:var(--text-primary)}
527
+ .gv-nav-btn:disabled{opacity:.3;cursor:not-allowed}
528
+ .gv-ai-actions{display:flex;gap:8px;padding:8px 20px;border-top:1px solid var(--border-dim);flex-shrink:0;background:var(--bg-surface)}
529
+ .gv-ai-btn{
530
+ font-family:var(--font-sans);font-size:11px;font-weight:600;padding:6px 12px;
531
+ border-radius:6px;border:1px solid var(--border-dim);background:var(--bg-card);
532
+ color:var(--text-secondary);cursor:pointer;transition:all .2s;
533
+ }
534
+ .gv-ai-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
535
+ .gv-chat{width:360px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden}
536
+ .gv-chat-header{display:flex;align-items:center;gap:10px;padding:10px 16px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);flex-shrink:0}
537
+ .gv-chat-avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0}
538
+ .gv-chat-name{font-family:var(--font-display);font-size:13px;font-weight:700;color:var(--text-primary)}
539
+ .gv-chat-context{font-size:10px;color:var(--text-muted);font-weight:400}
540
+
541
+ /* ─── AI Generator ───────────────────────── */
542
+ .gen-wrapper{display:flex;flex:1;overflow:hidden}
543
+ .gen-chat{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--border-dim)}
544
+ .gen-chat-header{padding:16px 20px;border-bottom:1px solid var(--border-dim);flex-shrink:0}
545
+ .gen-chat-title{font-family:var(--font-display);font-size:16px;font-weight:700;color:var(--text-primary)}
546
+ .gen-chat-sub{font-size:11px;color:var(--text-muted);display:block;margin-top:2px}
547
+ .gen-preview{width:380px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden}
548
+ .gen-preview-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border-dim);flex-shrink:0}
549
+ .gen-preview-header span{font-family:var(--font-display);font-size:13px;font-weight:700;color:var(--text-primary)}
550
+ .gen-preview-content{flex:1;overflow-y:auto;padding:16px}
551
+ .gen-preview-content::-webkit-scrollbar{width:3px}
552
+ .gen-preview-content::-webkit-scrollbar-thumb{background:rgba(34,211,238,0.15);border-radius:2px}
553
+ .gen-import-btn{
554
+ font-family:var(--font-sans);font-size:12px;font-weight:700;padding:6px 14px;
555
+ border-radius:6px;border:none;background:var(--accent);color:#fff;cursor:pointer;transition:all .2s;
556
+ }
557
+ .gen-import-btn:disabled{opacity:.4;cursor:not-allowed}
558
+ .gen-import-btn:hover:not(:disabled){opacity:.88}
559
+ .gen-preview-empty{font-size:12px;color:var(--text-muted);text-align:center;padding:40px 20px}
560
+ .gen-module-preview{margin-bottom:16px}
561
+ .gen-module-title{font-family:var(--font-display);font-size:13px;font-weight:700;color:var(--text-primary);margin-bottom:6px;padding:6px 0;border-bottom:1px solid var(--border-dim)}
562
+ .gen-step-preview{font-size:12px;color:var(--text-secondary);padding:4px 0 4px 12px;border-left:2px solid var(--border-dim)}
563
+
564
+ </style>
565
+ </head>
566
+ <body>
567
+ <div class="gym-app">
568
+
569
+ <!-- ─── Topbar ─── -->
570
+ <div class="topbar">
571
+ <a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
572
+ <img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
573
+ <span class="logo-text">MyAIforOne</span>
574
+ </a>
575
+ <nav class="tab-group" id="platformTabs">
576
+ <a class="tab-btn" href="/">Home</a>
577
+ <a class="tab-btn" href="/org">Agents</a>
578
+ <a class="tab-btn" href="/ui">Chat</a>
579
+ <a class="tab-btn" href="/library">Library</a>
580
+ <a class="tab-btn" href="/lab">Lab</a>
581
+ </nav>
582
+ <nav class="tab-group gym-tabs" id="gymTabs" style="display:none"></nav>
583
+ <div class="topbar-right">
584
+ <nav class="tab-group gym-tabs-right" id="gymTabsRight" style="display:none">
585
+ <button class="tab-btn active" data-gymtab="coach" onclick="switchView('coach')">Chat</button>
586
+ <button class="tab-btn" data-gymtab="progress" onclick="switchView('progress')">Progress</button>
587
+ <button class="tab-btn" data-gymtab="guides" onclick="switchView('guides')">Guides</button>
588
+ <button class="tab-btn" data-gymtab="generator" onclick="switchView('generator')">Create</button>
589
+ </nav>
590
+ <a class="gym-nav-btn" href="/gym" style="font-weight:700">Gym</a>
591
+ <button class="gym-nav-btn" onclick="openConfigOverlay()" title="Configure" style="background:transparent;border:1px solid var(--border-dim);color:var(--text-secondary);padding:4px 8px;font-size:16px;line-height:1">&#x2699;</button>
592
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">&#x1F319;</button>
593
+ </div>
594
+ </div>
595
+
596
+ <!-- ─── Onboarding View ─── -->
597
+ <div class="onboarding" id="onboarding">
598
+ <div class="step-dots" id="stepDots">
599
+ <div class="step-dot active" data-step="1"></div>
600
+ <div class="step-dot" data-step="2"></div>
601
+ <div class="step-dot" data-step="3"></div>
602
+ </div>
603
+
604
+ <!-- Step 1: Choose Your Trainer -->
605
+ <div class="ob-step active" id="obStep1">
606
+ <h1 class="ob-title">Choose Your Trainer</h1>
607
+ <p class="ob-sub">Pick the coaching style that works for you. You can switch anytime.</p>
608
+ <div class="trainer-grid" id="trainerGrid"></div>
609
+ <div class="ob-nav">
610
+ <button class="ob-btn primary" id="step1Next" onclick="goToStep(2)" disabled>Next</button>
611
+ </div>
612
+ </div>
613
+
614
+ <!-- Step 2: Get to Know You -->
615
+ <div class="ob-step" id="obStep2">
616
+ <div class="ob-coach-header" id="step2Header"></div>
617
+ <div class="ob-message" id="step2Message"></div>
618
+ <textarea class="ob-textarea" id="step2Input" placeholder="Tell me about yourself — your role, what you do, what you want to learn..." rows="5"></textarea>
619
+ <div class="ob-attach-zone" id="obAttachZone"
620
+ onclick="document.getElementById('obFileInput').click()"
621
+ ondragover="event.preventDefault();this.classList.add('dragover')"
622
+ ondragleave="this.classList.remove('dragover')"
623
+ ondrop="event.preventDefault();this.classList.remove('dragover');handleObFileDrop(event)">
624
+ <input type="file" id="obFileInput" multiple accept=".pdf,.doc,.docx,.txt,.md,.csv,.json,.html" onchange="handleObFileSelect(event)">
625
+ <span>Attach files (resume, bio, etc.) — drop here or click to browse</span>
626
+ </div>
627
+ <div class="ob-file-list" id="obFileList"></div>
628
+ <div class="ob-nav">
629
+ <button class="ob-btn" onclick="goToStep(1)">Back</button>
630
+ <button class="ob-btn primary" id="step2Send" onclick="submitStep2()">Send</button>
631
+ </div>
632
+ </div>
633
+
634
+ <!-- Step 3: Get Your Plan -->
635
+ <div class="ob-step" id="obStep3">
636
+ <h1 class="ob-title">Your Training Plan</h1>
637
+ <p class="ob-sub">Here is what we will work on together.</p>
638
+ <div class="ob-loading" id="planLoading">
639
+ <div class="ob-spinner"></div>
640
+ <span style="font-size:13px;color:var(--text-muted)">Your coach is building your plan...</span>
641
+ </div>
642
+ <div class="ob-plan-summary hidden" id="planSummary">
643
+ <div class="ob-bucket">
644
+ <div class="ob-bucket-title">On the Job Training</div>
645
+ <div class="ob-bucket-text" id="bucket1Text">Bring your real work to the platform. Your coach will observe, assist, and track growth as you use AI for actual tasks.</div>
646
+ </div>
647
+ <div class="ob-bucket">
648
+ <div class="ob-bucket-title">Platform-Driven Learning</div>
649
+ <div class="ob-bucket-text" id="bucket2Text">Curated modules and personalized suggestions based on your profile and activity patterns.</div>
650
+ </div>
651
+ </div>
652
+ <div class="ob-nav">
653
+ <button class="ob-btn" onclick="goToStep(2)">Back</button>
654
+ <button class="ob-btn primary hidden" id="enterGymBtn" onclick="enterGym()">Enter the Gym</button>
655
+ </div>
656
+ </div>
657
+ </div>
658
+
659
+ <!-- ─── Mode Picker ─── -->
660
+ <div class="mode-picker" id="modePicker">
661
+ <div class="mode-picker-inner">
662
+ <div class="mode-greeting" id="modeGreeting">What brings you to the gym?</div>
663
+ <div class="mode-tiles">
664
+ <div class="mode-tile" onclick="enterMode('task')">
665
+ <div class="mode-tile-icon">&#x1F6E0;</div>
666
+ <div class="mode-tile-title">I have work to do</div>
667
+ <div class="mode-tile-desc">Get it done while learning. Bring a real task — your coach helps you execute and teaches you along the way.</div>
668
+ </div>
669
+ <div class="mode-tile recommended" onclick="enterMode('coach')">
670
+ <div class="mode-tile-badge">Recommended</div>
671
+ <div class="mode-tile-icon">&#x1F3AF;</div>
672
+ <div class="mode-tile-title">You tell me</div>
673
+ <div class="mode-tile-desc">Based on your activity and gaps, your coach picks what to work on next. Zero decisions needed.</div>
674
+ </div>
675
+ <div class="mode-tile" onclick="enterMode('learn')">
676
+ <div class="mode-tile-icon">&#x1F4DA;</div>
677
+ <div class="mode-tile-title">I want to get smart</div>
678
+ <div class="mode-tile-desc">Pick a topic or program and dive deep. Structured learning at your own pace.</div>
679
+ </div>
680
+ </div>
681
+ <div class="mode-streak" id="modeStreak"></div>
682
+ </div>
683
+ </div>
684
+
685
+ <!-- ─── Main Gym View ─── -->
686
+ <div class="gym-main" id="gymMain">
687
+
688
+ <!-- Left sidebar -->
689
+ <div class="gym-sidebar" id="gymSidebar">
690
+ <div class="gym-sidebar-inner">
691
+ <div class="streak-box" id="streakBox">
692
+ <div class="streak-fire">&#x1F525;</div>
693
+ <div class="streak-count" id="streakCount">0</div>
694
+ <div class="streak-label">day streak</div>
695
+ </div>
696
+ <div id="sidebarGuides"></div>
697
+ </div>
698
+ <div class="gym-resize-handle" id="gymResizeHandle"></div>
699
+ </div>
700
+
701
+ <!-- Center panel -->
702
+ <div class="gym-center">
703
+
704
+ <!-- Coach View (default) -->
705
+ <div class="gym-view active" id="coachView">
706
+ <div class="coach-header" id="coachHeader"></div>
707
+ <div class="chat-messages" id="chatMessages"></div>
708
+ <div class="chat-input-wrap">
709
+ <div class="gym-file-drop hidden" id="gymFileDropZone"
710
+ onclick="document.getElementById('gymFileInput').click()"
711
+ ondragover="event.preventDefault();this.classList.add('dragover')"
712
+ ondragleave="this.classList.remove('dragover')"
713
+ ondrop="event.preventDefault();this.classList.remove('dragover');gymHandleFileDrop(event)">
714
+ <input type="file" id="gymFileInput" multiple onchange="gymHandleFileSelect(event)">
715
+ <div id="gymFileListArea"><span>Drop files here or click to browse</span></div>
716
+ </div>
717
+ <div class="chat-input-area">
718
+ <button class="gym-clip-btn" id="gymClipBtn" onclick="gymToggleFileZone()" title="Attach files">&#x1F4CE;</button>
719
+ <button class="gym-mic-btn" id="gymMicBtn" onclick="gymToggleVoice()" title="Click to speak">&#x1F3A4;</button>
720
+ <button class="gym-mic-btn" id="gymVoiceModeBtn" onclick="gymToggleVoiceMode()" title="Read responses aloud" style="font-size:14px">&#x1F507;</button>
721
+ <button class="gym-raw-btn" id="gymRawBtn" onclick="gymToggleRawLogs()" title="Raw logs (live feed)">&#x2318;</button>
722
+ <textarea class="chat-textarea" id="chatInput" placeholder="Message your coach..." rows="1"
723
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}"
724
+ oninput="autoResizeChat(this)"></textarea>
725
+ <button class="chat-send" id="chatSend" onclick="sendMessage()">&#x2192;</button>
726
+ </div>
727
+ <div class="chat-action-btns" id="chatActionBtns"></div>
728
+ <div class="gym-raw-drawer" id="gymRawDrawer">
729
+ <div class="gym-raw-header">
730
+ <span style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em">Activity</span>
731
+ <button class="gym-raw-mode-btn" id="gymRawModeBtn" onclick="gymToggleRawMode()">&#x25CB; Clean</button>
732
+ </div>
733
+ <pre id="gymRawContent"></pre>
734
+ </div>
735
+ </div>
736
+ </div>
737
+
738
+ <!-- Progress View -->
739
+ <div class="gym-view" id="progressView">
740
+ <div class="progress-panel">
741
+ <div class="radar-wrap">
742
+ <div class="radar-container">
743
+ <svg class="radar-svg" viewBox="0 0 280 280" id="radarChart"></svg>
744
+ </div>
745
+ </div>
746
+
747
+ <div class="dim-section">
748
+ <div class="dim-section-title">Dimension Breakdown</div>
749
+ <div id="dimRows"></div>
750
+ </div>
751
+
752
+ <div class="next-focus" id="nextFocus">
753
+ <div class="next-focus-title">Next Focus Area</div>
754
+ <div class="next-focus-text" id="nextFocusText">Loading...</div>
755
+ </div>
756
+
757
+ <div class="milestones-title">Milestones</div>
758
+ <ul class="milestone-list" id="milestoneList"></ul>
759
+
760
+ <div class="history-section" id="historySection">
761
+ <div class="history-title">Progress Over Time</div>
762
+ <div class="history-chart" id="historyChart">
763
+ <svg class="history-svg" id="historySvg" viewBox="0 0 600 160"></svg>
764
+ </div>
765
+ <div class="history-legend" id="historyLegend"></div>
766
+ </div>
767
+ </div>
768
+ </div>
769
+
770
+ <!-- Guides View -->
771
+ <div class="gym-view" id="guidesView">
772
+ <div class="guides-header">
773
+ <h2 class="guides-title">Training Guides</h2>
774
+ <input class="guides-search" id="guidesSearch" type="text" placeholder="Search guides..." oninput="filterGuides(this.value)">
775
+ </div>
776
+ <div class="guides-filters" id="guidesFilters">
777
+ <button class="guides-filter-pill active" data-source="all" onclick="filterBySource('all')">All</button>
778
+ <button class="guides-filter-pill" data-source="platform" onclick="filterBySource('platform')">Platform</button>
779
+ <button class="guides-filter-pill" data-source="coach" onclick="filterBySource('coach')">Coach Created</button>
780
+ <button class="guides-filter-pill" data-source="user" onclick="filterBySource('user')">My Guides</button>
781
+ </div>
782
+ <div class="guides-grid" id="guidesGrid"></div>
783
+ </div>
784
+
785
+ <!-- Guide Viewer -->
786
+ <div class="gym-view" id="guideViewerView">
787
+ <div class="gv-wrapper">
788
+ <div class="gv-content">
789
+ <div class="gv-topbar">
790
+ <button class="gv-back" onclick="exitGuideViewer()">&#x2190; Back to Guides</button>
791
+ <span class="gv-title" id="gvTitle"></span>
792
+ </div>
793
+ <div class="gv-modules" id="gvModules"></div>
794
+ <div class="gv-step-pills" id="gvStepPills"></div>
795
+ <div class="gv-step-content" id="gvStepContent"></div>
796
+ <div class="gv-ai-actions">
797
+ <button class="gv-ai-btn" onclick="guideAiAction('explain')">&#x1F4A1; Explain This</button>
798
+ <button class="gv-ai-btn" onclick="guideAiAction('quiz')">&#x2753; Quiz Me</button>
799
+ <button class="gv-ai-btn" onclick="guideAiAction('ask')">&#x1F916; Ask AI</button>
800
+ </div>
801
+ <div class="gv-step-footer">
802
+ <label class="gv-check">
803
+ <input type="checkbox" id="gvComplete" onchange="toggleStepComplete()">
804
+ <span>Mark as complete</span>
805
+ </label>
806
+ <div class="gv-nav-btns">
807
+ <button class="gv-nav-btn" id="gvPrev" onclick="prevStep()">&#x2190; Previous</button>
808
+ <button class="gv-nav-btn" id="gvNext" onclick="nextStep()">Next &#x2192;</button>
809
+ </div>
810
+ </div>
811
+ </div>
812
+ <div class="gv-chat">
813
+ <div class="gv-chat-header">
814
+ <div class="gv-chat-avatar" id="gvChatAvatar"></div>
815
+ <div>
816
+ <div class="gv-chat-name" id="gvChatName">Coach</div>
817
+ <div class="gv-chat-context" id="gvChatContext">Asking about this step</div>
818
+ </div>
819
+ </div>
820
+ <div class="chat-messages" id="gvChatMessages"></div>
821
+ <div class="chat-input-area">
822
+ <button class="gym-clip-btn" id="gvClipBtn" onclick="gvToggleFileZone()" title="Attach files">&#x1F4CE;</button>
823
+ <button class="gym-mic-btn" id="gvMicBtn" onclick="gvToggleVoice()" title="Click to speak">&#x1F3A4;</button>
824
+ <button class="gym-mic-btn" id="gvVoiceModeBtn" onclick="gvToggleVoiceMode()" title="Read responses aloud" style="font-size:14px">&#x1F507;</button>
825
+ <button class="gym-raw-btn" id="gvRawBtn" onclick="gvToggleRawLogs()" title="Raw logs">&#x2318;</button>
826
+ <textarea class="chat-textarea" id="gvChatInput" placeholder="Ask about this step..." rows="1"
827
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendGuideMessage()}"
828
+ oninput="autoResizeChat(this)"></textarea>
829
+ <button class="chat-send" id="gvChatSend" onclick="sendGuideMessage()">&#x2192;</button>
830
+ </div>
831
+ </div>
832
+ </div>
833
+ </div>
834
+
835
+ <!-- AI Generator View -->
836
+ <div class="gym-view" id="generatorView">
837
+ <div class="gen-wrapper">
838
+ <div class="gen-chat">
839
+ <div class="gen-chat-header">
840
+ <span class="gen-chat-title">AI Guide Generator</span>
841
+ <span class="gen-chat-sub">Describe the guide you want to create and I'll build it for you</span>
842
+ </div>
843
+ <div class="chat-messages" id="genChatMessages"></div>
844
+ <div class="chat-input-area">
845
+ <textarea class="chat-textarea" id="genChatInput"
846
+ placeholder="e.g. Create a beginner guide to writing effective AI prompts..."
847
+ rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendGeneratorMessage()}"
848
+ oninput="autoResizeChat(this)"></textarea>
849
+ <button class="chat-send" onclick="sendGeneratorMessage()">&#x2192;</button>
850
+ </div>
851
+ </div>
852
+ <div class="gen-preview">
853
+ <div class="gen-preview-header">
854
+ <span>Preview</span>
855
+ <button class="gen-import-btn" id="genImportBtn" onclick="importGeneratedGuide()" disabled>Import Guide</button>
856
+ </div>
857
+ <div class="gen-preview-content" id="genPreviewContent">
858
+ <div class="gen-preview-empty">Your guide structure will appear here as the AI generates it.</div>
859
+ </div>
860
+ </div>
861
+ </div>
862
+ </div>
863
+
864
+ </div>
865
+ </div>
866
+
867
+ <!-- Bottom feed strip removed — guides sidebar replaced it -->
868
+
869
+ </div>
870
+
871
+ <script>
872
+ /* ─── Constants ──────────────────────────── */
873
+ const API = '';
874
+ const TRAINERS = [
875
+ {
876
+ id: 'alex', name: 'Alex', initials: 'AX', color: '#22d3ee',
877
+ style: 'Collaborative \u00B7 Steady',
878
+ quote: "Hey, I'm Alex. I work right alongside you \u2014 we figure things out together.",
879
+ fullQuote: "Before we get started, tell me a bit about yourself \u2014 I'll use this to make everything we do together actually useful to you. You can share things like: your LinkedIn or any links (I'll read them), what you do for work, where you are with AI right now (total beginner, dabbler, daily user \u2014 give me an example), and what you're hoping to get out of this. One message, however you want to write it."
880
+ },
881
+ {
882
+ id: 'jordan', name: 'Jordan', initials: 'JD', color: '#f97316',
883
+ style: 'Direct \u00B7 Steady',
884
+ quote: "I'm Jordan. I'm direct, I don't sugarcoat, and I'll hold you accountable.",
885
+ fullQuote: "Before we get started, tell me a bit about yourself \u2014 I'll use this to make everything we do together actually useful to you. You can share things like: your LinkedIn or any links (I'll read them), what you do for work, where you are with AI right now (total beginner, dabbler, daily user \u2014 give me an example), and what you're hoping to get out of this. One message, however you want to write it."
886
+ },
887
+ {
888
+ id: 'morgan', name: 'Morgan', initials: 'MG', color: '#a78bfa',
889
+ style: 'Thoughtful \u00B7 Steady',
890
+ quote: "I'm Morgan. I care a lot about the 'why.' I won't just show you what to click.",
891
+ fullQuote: "Before we get started, tell me a bit about yourself \u2014 I'll use this to make everything we do together actually useful to you. You can share things like: your LinkedIn or any links (I'll read them), what you do for work, where you are with AI right now (total beginner, dabbler, daily user \u2014 give me an example), and what you're hoping to get out of this. One message, however you want to write it."
892
+ },
893
+ {
894
+ id: 'riley', name: 'Riley', initials: 'RL', color: '#ef4444',
895
+ style: 'Challenging \u00B7 Immersive',
896
+ quote: "I'm Riley. I'm going to push you \u2014 hard questions, uncomfortable exercises.",
897
+ fullQuote: "Before we get started, tell me a bit about yourself \u2014 I'll use this to make everything we do together actually useful to you. You can share things like: your LinkedIn or any links (I'll read them), what you do for work, where you are with AI right now (total beginner, dabbler, daily user \u2014 give me an example), and what you're hoping to get out of this. One message, however you want to write it."
898
+ },
899
+ {
900
+ id: 'sam', name: 'Sam', initials: 'SM', color: '#4ade80',
901
+ style: 'Patient \u00B7 Steady',
902
+ quote: "I'm Sam. I meet you exactly where you are \u2014 no pressure, no judgment.",
903
+ fullQuote: "Before we get started, tell me a bit about yourself \u2014 I'll use this to make everything we do together actually useful to you. You can share things like: your LinkedIn or any links (I'll read them), what you do for work, where you are with AI right now (total beginner, dabbler, daily user \u2014 give me an example), and what you're hoping to get out of this. One message, however you want to write it."
904
+ }
905
+ ];
906
+
907
+ const DIMENSIONS = [
908
+ { key: 'application', label: 'Application', angle: -90 },
909
+ { key: 'communication', label: 'Communication', angle: -18 },
910
+ { key: 'knowledge', label: 'Knowledge', angle: 54 },
911
+ { key: 'orchestration', label: 'Orchestration', angle: 126 },
912
+ { key: 'craft', label: 'Craft', angle: 198 }
913
+ ];
914
+
915
+ /* ─── State ──────────────────────────────── */
916
+ let state = {
917
+ onboardingComplete: false,
918
+ onboardingStep: 1,
919
+ selectedTrainer: null,
920
+ profile: null,
921
+ plan: null,
922
+ progress: null,
923
+ cards: [],
924
+ insights: { insights: [], topRecommendation: null },
925
+ programs: [],
926
+ chatHistory: [],
927
+ sending: false,
928
+ activeView: 'coach',
929
+ guideViewer: null,
930
+ _generatorSeeded: false,
931
+ _generatorMarkdown: '',
932
+ currentMode: null,
933
+ };
934
+
935
+ /* ─── Theme ──────────────────────────────── */
936
+ function toggleTheme() {
937
+ const html = document.documentElement;
938
+ const current = html.getAttribute('data-theme');
939
+ const next = current === 'light' ? 'dark' : 'light';
940
+ html.setAttribute('data-theme', next);
941
+ localStorage.setItem('gym-theme', next);
942
+ document.getElementById('themeBtn').textContent = next === 'light' ? '\u2600\uFE0F' : '\uD83C\uDF19';
943
+ }
944
+ (function initTheme() {
945
+ const saved = localStorage.getItem('gym-theme') || 'dark';
946
+ document.documentElement.setAttribute('data-theme', saved);
947
+ const btn = document.getElementById('themeBtn');
948
+ if (btn) btn.textContent = saved === 'light' ? '\u2600\uFE0F' : '\uD83C\uDF19';
949
+ })();
950
+
951
+ /* ─── Init ───────────────────────────────── */
952
+ async function init() {
953
+ try {
954
+ const [profileRes, planRes, progressRes, programsRes, insightsRes] = await Promise.all([
955
+ fetch(`${API}/api/gym/learner-profile`).then(r => r.ok ? r.json() : null).catch(() => null),
956
+ fetch(`${API}/api/gym/plan`).then(r => r.ok ? r.json() : null).catch(() => null),
957
+ fetch(`${API}/api/gym/progress`).then(r => r.ok ? r.json() : null).catch(() => null),
958
+ fetch(`${API}/api/gym/programs`).then(r => r.ok ? r.json() : null).catch(() => null),
959
+ fetch(`${API}/api/gym/insights`).then(r => r.ok ? r.json() : null).catch(() => null),
960
+ ]);
961
+
962
+ state.profile = profileRes;
963
+ state.plan = planRes;
964
+ state.progress = progressRes;
965
+ state.programs = Array.isArray(programsRes) ? programsRes : (programsRes?.programs || []);
966
+ state.insights = insightsRes || { insights: [], topRecommendation: null };
967
+
968
+ if (state.profile) {
969
+ state.onboardingComplete = !!state.profile.onboardingComplete;
970
+ state.selectedTrainer = state.profile.selectedTrainer || null;
971
+ state.onboardingStep = state.profile.onboardingStep || 1;
972
+ }
973
+ } catch (e) {
974
+ console.warn('Failed to load gym state, starting fresh:', e);
975
+ }
976
+
977
+ renderTrainerCards();
978
+ checkGymOnlyMode();
979
+
980
+ if (state.onboardingComplete) {
981
+ showMainGym();
982
+ } else {
983
+ showOnboarding();
984
+ if (state.selectedTrainer) {
985
+ selectTrainer(state.selectedTrainer, false);
986
+ }
987
+ if (state.onboardingStep > 1) {
988
+ goToStep(state.onboardingStep);
989
+ }
990
+ }
991
+ }
992
+
993
+ /* ─── Onboarding ─────────────────────────── */
994
+ function showOnboarding() {
995
+ document.getElementById('onboarding').classList.remove('hidden');
996
+ document.getElementById('modePicker').classList.remove('active');
997
+ document.getElementById('gymMain').classList.remove('active');
998
+ /* feedStrip removed */
999
+ }
1000
+
1001
+ function showMainGym() {
1002
+ document.getElementById('onboarding').classList.add('hidden');
1003
+ document.getElementById('gymMain').classList.remove('active');
1004
+ /* feedStrip removed */
1005
+ // Show mode picker instead of going straight to gym
1006
+ showModePicker();
1007
+ }
1008
+
1009
+ function showModePicker() {
1010
+ document.getElementById('modePicker').classList.add('active');
1011
+ document.getElementById('gymMain').classList.remove('active');
1012
+ /* feedStrip removed */
1013
+ // Greeting
1014
+ const hour = new Date().getHours();
1015
+ const greeting = hour < 12 ? 'Good morning' : hour < 17 ? 'Good afternoon' : 'Good evening';
1016
+ document.getElementById('modeGreeting').textContent = `${greeting}. What brings you to the gym?`;
1017
+ // Streak
1018
+ const streak = state.profile?.streak?.current || 0;
1019
+ const streakEl = document.getElementById('modeStreak');
1020
+ if (streak > 0) {
1021
+ streakEl.innerHTML = `&#x1F525; ${streak} day streak`;
1022
+ } else {
1023
+ streakEl.textContent = '';
1024
+ }
1025
+ }
1026
+
1027
+ function enterMode(mode) {
1028
+ state.currentMode = mode;
1029
+ document.getElementById('modePicker').classList.remove('active');
1030
+ document.getElementById('gymMain').classList.add('active');
1031
+ renderMainGym();
1032
+ // Switch to coach view for all modes
1033
+ switchView('coach');
1034
+ // Set chat placeholder based on mode
1035
+ const input = document.getElementById('chatInput');
1036
+ if (mode === 'task') {
1037
+ input.placeholder = 'Describe what you need to get done...';
1038
+ primeCoachForMode('task');
1039
+ } else if (mode === 'coach') {
1040
+ input.placeholder = 'Message your coach...';
1041
+ primeCoachForMode('coach');
1042
+ } else if (mode === 'learn') {
1043
+ input.placeholder = 'What do you want to learn about?';
1044
+ primeCoachForMode('learn');
1045
+ }
1046
+ }
1047
+
1048
+ async function primeCoachForMode(mode) {
1049
+ const msgs = document.getElementById('chatMessages');
1050
+ const trainer = TRAINERS.find(t => t.id === state.selectedTrainer) || TRAINERS[0];
1051
+ // Clear any previous primer messages
1052
+ document.querySelectorAll('.mode-primer').forEach(el => el.remove());
1053
+
1054
+ let primerText = '';
1055
+ if (mode === 'task') {
1056
+ primerText = "What are you working on? Describe the task and I'll help you get it done — I'll point out what you're learning along the way.";
1057
+ } else if (mode === 'coach') {
1058
+ primerText = "I'm here when you're ready. Ask me anything, or hit **Trigger Insights** below the chat to get a fresh analysis of your activity.";
1059
+ } else if (mode === 'learn') {
1060
+ // Check for in-progress programs
1061
+ const inProgress = findInProgressProgram();
1062
+ if (inProgress) {
1063
+ primerText = `You're in the middle of **${inProgress.title}** — want to pick up where you left off? Or you can browse other topics or tell me what you want to learn.`;
1064
+ } else {
1065
+ primerText = "What do you want to learn? Pick a topic, browse the programs in the sidebar, or just tell me what you're curious about.";
1066
+ }
1067
+ }
1068
+ if (primerText) {
1069
+ appendCoachMessage(primerText, trainer, 'mode-primer');
1070
+ }
1071
+ }
1072
+
1073
+ function findInProgressProgram() {
1074
+ const progress = state.progress?.programs || {};
1075
+ for (const [slug, prog] of Object.entries(progress)) {
1076
+ if (prog.startedAt && !prog.completedAt) {
1077
+ const program = (state.programs || []).find(p => (p.slug || p.id) === slug);
1078
+ if (program) return { slug, title: program.title || slug, ...prog };
1079
+ }
1080
+ }
1081
+ return null;
1082
+ }
1083
+
1084
+ function appendCoachMessage(text, trainer, extraClass = '') {
1085
+ const msgs = document.getElementById('chatMessages');
1086
+ const div = document.createElement('div');
1087
+ div.className = `chat-msg coach ${extraClass}`;
1088
+ div.innerHTML = `
1089
+ <div class="chat-avatar" style="background:${trainer.color}20;color:${trainer.color}">${trainer.initials}</div>
1090
+ <div class="chat-bubble">${text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')}</div>
1091
+ `;
1092
+ msgs.appendChild(div);
1093
+ msgs.scrollTop = msgs.scrollHeight;
1094
+ }
1095
+
1096
+ function renderTrainerCards() {
1097
+ const grid = document.getElementById('trainerGrid');
1098
+ grid.innerHTML = TRAINERS.map(t => `
1099
+ <div class="trainer-card${state.selectedTrainer === t.id ? ' selected' : ''}" onclick="selectTrainer('${t.id}')" data-trainer="${t.id}">
1100
+ <div class="trainer-avatar" style="background:${t.color}">${t.initials}</div>
1101
+ <div class="trainer-name">${t.name}</div>
1102
+ <div class="trainer-style">${t.style}</div>
1103
+ <div class="trainer-quote">"${t.quote}"</div>
1104
+ </div>
1105
+ `).join('');
1106
+ }
1107
+
1108
+ function selectTrainer(id, save = true) {
1109
+ state.selectedTrainer = id;
1110
+ document.querySelectorAll('.trainer-card').forEach(c => {
1111
+ c.classList.toggle('selected', c.dataset.trainer === id);
1112
+ });
1113
+ document.getElementById('step1Next').disabled = false;
1114
+ if (save) {
1115
+ fetch(`${API}/api/gym/learner-profile`, {
1116
+ method: 'PUT',
1117
+ headers: { 'Content-Type': 'application/json' },
1118
+ body: JSON.stringify({ selectedTrainer: id, onboardingStep: 1 })
1119
+ }).catch(() => {});
1120
+ }
1121
+ }
1122
+
1123
+ function goToStep(step) {
1124
+ state.onboardingStep = step;
1125
+ document.querySelectorAll('.ob-step').forEach(s => s.classList.remove('active'));
1126
+ document.getElementById(`obStep${step}`).classList.add('active');
1127
+
1128
+ document.querySelectorAll('.step-dot').forEach(d => {
1129
+ const s = parseInt(d.dataset.step);
1130
+ d.classList.toggle('active', s === step);
1131
+ d.classList.toggle('done', s < step);
1132
+ });
1133
+
1134
+ if (step === 2) renderStep2();
1135
+ if (step === 3) renderStep3();
1136
+ }
1137
+
1138
+ function renderStep2() {
1139
+ const trainer = TRAINERS.find(t => t.id === state.selectedTrainer);
1140
+ if (!trainer) return;
1141
+
1142
+ document.getElementById('step2Header').innerHTML = `
1143
+ <div class="ob-coach-avatar" style="background:${trainer.color}">${trainer.initials}</div>
1144
+ <div class="ob-coach-name">${trainer.name}</div>
1145
+ `;
1146
+ document.getElementById('step2Message').textContent = trainer.fullQuote;
1147
+ }
1148
+
1149
+ // ─── Onboarding File Attachments ─────────────────────────────
1150
+ let obPendingFiles = [];
1151
+
1152
+ function handleObFileDrop(e) {
1153
+ const files = e.dataTransfer?.files;
1154
+ if (files) addObFiles(files);
1155
+ }
1156
+ function handleObFileSelect(e) {
1157
+ const files = e.target?.files;
1158
+ if (files) addObFiles(files);
1159
+ e.target.value = '';
1160
+ }
1161
+ function addObFiles(fileList) {
1162
+ for (const f of fileList) {
1163
+ if (!obPendingFiles.find(p => p.name === f.name && p.size === f.size)) {
1164
+ obPendingFiles.push(f);
1165
+ }
1166
+ }
1167
+ renderObFileList();
1168
+ }
1169
+ function removeObFile(i) {
1170
+ obPendingFiles.splice(i, 1);
1171
+ renderObFileList();
1172
+ }
1173
+ function renderObFileList() {
1174
+ const el = document.getElementById('obFileList');
1175
+ if (!obPendingFiles.length) { el.innerHTML = ''; return; }
1176
+ el.innerHTML = obPendingFiles.map((f, i) => {
1177
+ const sz = f.size < 1024 ? f.size + ' B' : f.size < 1048576 ? (f.size/1024).toFixed(1) + ' KB' : (f.size/1048576).toFixed(1) + ' MB';
1178
+ return `<div class="ob-file-pill"><span class="fname">${f.name}</span><span class="fsize">${sz}</span><span class="fremove" onclick="removeObFile(${i})">×</span></div>`;
1179
+ }).join('');
1180
+ }
1181
+
1182
+ async function uploadObFiles() {
1183
+ const uploaded = [];
1184
+ for (const f of obPendingFiles) {
1185
+ try {
1186
+ const formData = new FormData();
1187
+ formData.append('file', f);
1188
+ formData.append('mode', 'permanent');
1189
+ const resp = await fetch(`${API}/api/upload/gym`, { method: 'POST', body: formData });
1190
+ const data = await resp.json();
1191
+ if (data.ok) uploaded.push({ name: data.fileName, path: data.path });
1192
+ } catch (err) {
1193
+ console.warn('Upload failed:', f.name, err);
1194
+ }
1195
+ }
1196
+ return uploaded;
1197
+ }
1198
+
1199
+ async function submitStep2() {
1200
+ const input = document.getElementById('step2Input');
1201
+ const text = input.value.trim();
1202
+ if (!text && obPendingFiles.length === 0) return;
1203
+
1204
+ const btn = document.getElementById('step2Send');
1205
+ btn.disabled = true;
1206
+ btn.textContent = obPendingFiles.length > 0 ? 'Uploading...' : 'Sending...';
1207
+
1208
+ try {
1209
+ // Upload attachments first
1210
+ let fileContext = '';
1211
+ if (obPendingFiles.length > 0) {
1212
+ const uploaded = await uploadObFiles();
1213
+ if (uploaded.length > 0) {
1214
+ fileContext = '\n\n[Attached files: ' + uploaded.map(u => u.path).join(', ') + '. Please read and incorporate these into my learner profile.]';
1215
+ }
1216
+ obPendingFiles = [];
1217
+ renderObFileList();
1218
+ }
1219
+
1220
+ btn.textContent = 'Sending...';
1221
+
1222
+ // Send message to gym chat (with file paths appended so coach can read them)
1223
+ const res = await fetch(`${API}/api/chat/gym/stream`, {
1224
+ method: 'POST',
1225
+ headers: { 'Content-Type': 'application/json' },
1226
+ body: JSON.stringify({ text: (text || 'See attached files.') + fileContext, senderId: 'web' })
1227
+ });
1228
+ const data = await res.json();
1229
+
1230
+ // Update profile
1231
+ await fetch(`${API}/api/gym/learner-profile`, {
1232
+ method: 'PUT',
1233
+ headers: { 'Content-Type': 'application/json' },
1234
+ body: JSON.stringify({ onboardingStep: 2 })
1235
+ });
1236
+
1237
+ // If we got a jobId, consume the SSE stream (for plan generation)
1238
+ if (data.jobId) {
1239
+ state._onboardingJobId = data.jobId;
1240
+ }
1241
+
1242
+ goToStep(3);
1243
+ } catch (e) {
1244
+ console.error('Step 2 submit error:', e);
1245
+ btn.textContent = 'Retry';
1246
+ } finally {
1247
+ btn.disabled = false;
1248
+ btn.textContent = 'Send';
1249
+ }
1250
+ }
1251
+
1252
+ async function renderStep3() {
1253
+ const loading = document.getElementById('planLoading');
1254
+ const summary = document.getElementById('planSummary');
1255
+ const enterBtn = document.getElementById('enterGymBtn');
1256
+
1257
+ loading.style.display = 'flex';
1258
+ summary.classList.add('hidden');
1259
+ enterBtn.classList.add('hidden');
1260
+
1261
+ // If we have a jobId from step 2, listen for the plan
1262
+ if (state._onboardingJobId) {
1263
+ try {
1264
+ await consumeSSE(state._onboardingJobId, (text) => {
1265
+ // Plan text received — try to parse buckets
1266
+ if (text) {
1267
+ document.getElementById('bucket1Text').textContent = extractBucket(text, 1) || 'Bring your real work to the platform. Your coach will observe, assist, and track growth as you use AI for actual tasks.';
1268
+ document.getElementById('bucket2Text').textContent = extractBucket(text, 2) || 'Curated modules and personalized suggestions based on your profile and activity patterns.';
1269
+ }
1270
+ });
1271
+ } catch (e) {
1272
+ console.warn('SSE error during plan generation:', e);
1273
+ }
1274
+ }
1275
+
1276
+ // Also try loading plan from API
1277
+ try {
1278
+ const planRes = await fetch(`${API}/api/gym/plan`).then(r => r.ok ? r.json() : null);
1279
+ if (planRes) {
1280
+ state.plan = planRes;
1281
+ if (planRes.bucket1) document.getElementById('bucket1Text').textContent = planRes.bucket1;
1282
+ if (planRes.bucket2) document.getElementById('bucket2Text').textContent = planRes.bucket2;
1283
+ }
1284
+ } catch (e) {}
1285
+
1286
+ // Show plan and enter button now that streaming is done
1287
+ loading.style.display = 'none';
1288
+ summary.classList.remove('hidden');
1289
+ enterBtn.classList.remove('hidden');
1290
+ }
1291
+
1292
+ function extractBucket(text, n) {
1293
+ // Simple extraction — look for bucket headers in the response
1294
+ const lines = text.split('\n');
1295
+ let inBucket = false;
1296
+ let result = [];
1297
+ const patterns = n === 1
1298
+ ? [/on.the.job/i, /bucket\s*1/i, /user.driven/i]
1299
+ : [/platform.driven/i, /bucket\s*2/i, /curated/i];
1300
+
1301
+ for (const line of lines) {
1302
+ if (patterns.some(p => p.test(line))) { inBucket = true; continue; }
1303
+ if (inBucket && /^(#{1,3}\s|bucket|\*\*bucket)/i.test(line)) break;
1304
+ if (inBucket && line.trim()) result.push(line.trim());
1305
+ }
1306
+ return result.join(' ').slice(0, 300) || null;
1307
+ }
1308
+
1309
+ async function enterGym() {
1310
+ try {
1311
+ await fetch(`${API}/api/gym/learner-profile`, {
1312
+ method: 'PUT',
1313
+ headers: { 'Content-Type': 'application/json' },
1314
+ body: JSON.stringify({ onboardingComplete: true, onboardingStep: 3 })
1315
+ });
1316
+ } catch (e) {}
1317
+
1318
+ state.onboardingComplete = true;
1319
+ showMainGym();
1320
+ }
1321
+
1322
+ /* ─── Main Gym Rendering ─────────────────── */
1323
+ function renderMainGym() {
1324
+ renderStreak();
1325
+ renderSidebarForMode();
1326
+ renderCoachHeader();
1327
+ renderProgress();
1328
+ }
1329
+
1330
+ function renderStreak() {
1331
+ let streak = 0;
1332
+ if (state.profile?.streak?.current) streak = state.profile.streak.current;
1333
+ else if (state.progress?.streak) streak = state.progress.streak;
1334
+ document.getElementById('streakCount').textContent = streak;
1335
+ }
1336
+
1337
+ /* ─── View Switching ─────────────────────── */
1338
+ function switchView(view) {
1339
+ state.activeView = view;
1340
+ // Update topbar gym tabs (both left and right nav)
1341
+ document.querySelectorAll('#gymTabs .tab-btn[data-gymtab], #gymTabsRight .tab-btn[data-gymtab]').forEach(n =>
1342
+ n.classList.toggle('active', n.dataset.gymtab === view || (view === 'guide-viewer' && n.dataset.gymtab === 'guides'))
1343
+ );
1344
+ ['coachView','progressView','guidesView','guideViewerView','generatorView'].forEach(id => {
1345
+ const el = document.getElementById(id);
1346
+ if (el) el.classList.remove('active');
1347
+ });
1348
+ const viewMap = { coach:'coachView', progress:'progressView', guides:'guidesView', 'guide-viewer':'guideViewerView', generator:'generatorView' };
1349
+ const target = document.getElementById(viewMap[view]);
1350
+ if (target) target.classList.add('active');
1351
+
1352
+ if (view === 'guides') renderGuidesView();
1353
+ if (view === 'generator' && !state._generatorSeeded) seedGenerator();
1354
+ }
1355
+
1356
+ function renderSidebarForMode() {
1357
+ const guidesEl = document.getElementById('sidebarGuides');
1358
+ if (!guidesEl) return;
1359
+ const mode = state.currentMode;
1360
+
1361
+ // Set the default filter based on mode
1362
+ if (mode === 'learn') {
1363
+ sidebarGuidesFilter = 'platform';
1364
+ } else if (mode === 'coach') {
1365
+ sidebarGuidesFilter = 'coach';
1366
+ } else if (mode === 'task') {
1367
+ sidebarGuidesFilter = 'all';
1368
+ }
1369
+
1370
+ guidesEl.style.display = '';
1371
+ renderSidebarGuides(guidesEl);
1372
+ renderChatActionButtons();
1373
+ }
1374
+
1375
+ function renderChatActionButtons() {
1376
+ const container = document.getElementById('chatActionBtns');
1377
+ if (!container) return;
1378
+ const mode = state.currentMode;
1379
+
1380
+ const buttons = {
1381
+ coach: [
1382
+ { label: '\u{1F504} Trigger Insights', action: 'insights', primary: true },
1383
+ ],
1384
+ learn: [],
1385
+ task: [],
1386
+ };
1387
+
1388
+ const btns = buttons[mode] || [];
1389
+ container.innerHTML = btns.map(b =>
1390
+ `<button class="chat-action-btn${b.primary ? ' primary' : ''}" onclick="handleSidebarAction('${b.action}')">${b.label}</button>`
1391
+ ).join('');
1392
+ }
1393
+
1394
+ function buildLearnCards() {
1395
+ const cards = [];
1396
+ const progress = state.progress?.programs || state.progress || {};
1397
+ const programs = state.programs || [];
1398
+ const profile = state.profile || {};
1399
+ const dimensions = profile.dimensions || {};
1400
+
1401
+ // Card: in-progress programs with progress bar
1402
+ for (const [slug, prog] of Object.entries(progress)) {
1403
+ if (prog.startedAt && !prog.completedAt) {
1404
+ const program = programs.find(p => (p.slug || p.id) === slug);
1405
+ if (!program) continue;
1406
+ const totalSteps = (program.modules || []).reduce((sum, m) => sum + (m.steps?.length || 0), 0);
1407
+ const completedSteps = Object.values(prog.steps || {}).filter(s => s === true || s?.completed).length;
1408
+ const pct = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
1409
+ // Find next incomplete step
1410
+ let nextStep = null;
1411
+ for (const m of (program.modules || [])) {
1412
+ for (const s of (m.steps || [])) {
1413
+ const stepDone = prog.steps?.[s.id] === true || prog.steps?.[s.id]?.completed;
1414
+ if (!stepDone) { nextStep = s.title; break; }
1415
+ }
1416
+ if (nextStep) break;
1417
+ }
1418
+ cards.push({
1419
+ title: `Continue: ${program.title}`,
1420
+ description: nextStep ? `Next: ${nextStep}` : `${pct}% complete`,
1421
+ progress: pct,
1422
+ cta: 'Resume',
1423
+ ctaAction: `I want to continue the ${program.title} program`,
1424
+ });
1425
+ }
1426
+ }
1427
+
1428
+ // Card: recommended programs based on weak dimensions
1429
+ const weakDims = Object.entries(dimensions)
1430
+ .filter(([, v]) => v.score < 3)
1431
+ .sort((a, b) => a[1].score - b[1].score);
1432
+ const dimToProgram = {
1433
+ knowledge: 'getting-started',
1434
+ communication: 'prompt-engineering',
1435
+ orchestration: 'automations',
1436
+ craft: 'agent-building',
1437
+ application: null,
1438
+ };
1439
+ for (const [dim] of weakDims.slice(0, 1)) {
1440
+ const recSlug = dimToProgram[dim];
1441
+ if (!recSlug) continue;
1442
+ // Don't recommend if already in progress or completed
1443
+ if (progress[recSlug]?.startedAt) continue;
1444
+ const program = programs.find(p => (p.slug || p.id) === recSlug);
1445
+ if (!program) continue;
1446
+ cards.push({
1447
+ title: `Recommended: ${program.title}`,
1448
+ description: `Your ${dim} is at ${dimensions[dim].label} — this program will help.`,
1449
+ cta: 'Start',
1450
+ ctaAction: `I want to start the ${program.title} program`,
1451
+ });
1452
+ }
1453
+
1454
+ // Card: recently completed program (celebration)
1455
+ for (const [slug, prog] of Object.entries(progress)) {
1456
+ if (prog.completedAt) {
1457
+ const completedDate = new Date(prog.completedAt);
1458
+ const daysSince = (Date.now() - completedDate.getTime()) / (1000 * 60 * 60 * 24);
1459
+ if (daysSince <= 7) {
1460
+ const program = programs.find(p => (p.slug || p.id) === slug);
1461
+ cards.push({
1462
+ title: `Completed: ${program?.title || slug}`,
1463
+ description: 'Great work! Look for your next challenge.',
1464
+ progress: 100,
1465
+ });
1466
+ }
1467
+ }
1468
+ }
1469
+
1470
+ return cards;
1471
+ }
1472
+
1473
+ /* dismissInsight removed — insights delivered through chat now */
1474
+
1475
+ /* ─── Sidebar Guides (Learn mode) ────────── */
1476
+ let sidebarGuidesFilter = 'all';
1477
+
1478
+ function renderSidebarGuides(container) {
1479
+ const mode = state.currentMode;
1480
+ let filtered = state.programs || [];
1481
+ if (sidebarGuidesFilter !== 'all') {
1482
+ filtered = filtered.filter(p => (p.source || 'platform') === sidebarGuidesFilter);
1483
+ }
1484
+
1485
+ // Mode-specific titles
1486
+ const modeTitles = {
1487
+ learn: 'Training Guides',
1488
+ coach: 'Coach Guides',
1489
+ task: 'Session Guides',
1490
+ };
1491
+ const title = modeTitles[mode] || 'Guides';
1492
+
1493
+ const filterHtml = `
1494
+ <div style="margin-bottom:12px">
1495
+ <div class="sidebar-section-title" style="margin-bottom:8px">${title}</div>
1496
+ <input type="text" id="sidebarGuidesSearch" placeholder="Search guides..." oninput="filterSidebarGuides()"
1497
+ style="width:100%;padding:8px 10px;border:1px solid var(--border-dim);border-radius:8px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:var(--font-sans);box-sizing:border-box;margin-bottom:8px">
1498
+ <div style="display:flex;gap:4px;flex-wrap:wrap">
1499
+ ${['all','platform','coach','user'].map(s => {
1500
+ const label = {all:'All',platform:'Platform',coach:'Coach',user:'My Guides'}[s];
1501
+ const active = sidebarGuidesFilter === s ? 'background:var(--accent);color:#000;font-weight:700' : 'background:var(--bg-input);color:var(--text-secondary)';
1502
+ return `<button onclick="setSidebarGuidesFilter('${s}')" style="border:none;padding:4px 10px;border-radius:6px;font-size:10px;font-family:var(--font-sans);cursor:pointer;${active}">${label}</button>`;
1503
+ }).join('')}
1504
+ </div>
1505
+ </div>
1506
+ `;
1507
+
1508
+ const emptyMessages = {
1509
+ learn: 'No platform guides available yet.',
1510
+ coach: 'No coaching guides yet — start a "You tell me" session to generate personalized guides.',
1511
+ task: 'Guides from your work sessions will appear here as you complete tasks.',
1512
+ };
1513
+ const emptyMsg = emptyMessages[mode] || 'No guides yet.';
1514
+ const cardsHtml = filtered.length === 0
1515
+ ? `<div style="font-size:12px;color:var(--text-muted);padding:20px 4px;text-align:center">${emptyMsg}</div>`
1516
+ : filtered.map(p => {
1517
+ const slug = p.slug || p.id;
1518
+ const moduleCount = p.modules?.length || 0;
1519
+ const stepCount = p.modules?.reduce((sum, m) => sum + (m.steps?.length || 0), 0) || 0;
1520
+ const progress = getGuideProgress(slug);
1521
+ const source = p.source || 'platform';
1522
+ const sourceLabel = {platform:'Platform',coach:'Coach',user:'My Guide'}[source] || source;
1523
+ const sourceColor = {platform:'var(--accent)',coach:'var(--purple)',user:'var(--green)'}[source] || 'var(--text-muted)';
1524
+ return `
1525
+ <div onclick="openGuide('${slug}')" style="background:var(--bg-card);border:1px solid var(--border-dim);border-radius:8px;padding:10px 12px;margin-bottom:6px;cursor:pointer;transition:all .2s" onmouseover="this.style.borderColor='var(--border-glow)'" onmouseout="this.style.borderColor='var(--border-dim)'">
1526
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
1527
+ <div style="font-size:13px;font-weight:600;color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.title || p.name || 'Untitled'}</div>
1528
+ <span style="font-family:var(--font-mono);font-size:8px;font-weight:700;padding:1px 6px;border-radius:4px;color:${sourceColor};background:${sourceColor}15;flex-shrink:0;margin-left:6px">${sourceLabel}</span>
1529
+ </div>
1530
+ <div style="font-size:11px;color:var(--text-secondary);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">${p.description || ''}</div>
1531
+ <div style="display:flex;gap:6px;margin-top:6px;flex-wrap:wrap">
1532
+ ${moduleCount ? `<span style="font-family:var(--font-mono);font-size:9px;color:var(--text-muted);background:var(--bg-input);padding:2px 6px;border-radius:4px">${moduleCount} modules</span>` : ''}
1533
+ ${stepCount ? `<span style="font-family:var(--font-mono);font-size:9px;color:var(--text-muted);background:var(--bg-input);padding:2px 6px;border-radius:4px">${stepCount} steps</span>` : ''}
1534
+ ${p.difficulty ? `<span style="font-family:var(--font-mono);font-size:9px;color:var(--accent);background:var(--accent-bg);padding:2px 6px;border-radius:4px">${p.difficulty}</span>` : ''}
1535
+ </div>
1536
+ ${progress > 0 ? `<div style="margin-top:6px;height:3px;border-radius:2px;background:var(--bg-input);overflow:hidden"><div style="height:100%;width:${Math.min(progress * 10, 100)}%;background:var(--accent);border-radius:2px"></div></div>` : ''}
1537
+ </div>`;
1538
+ }).join('');
1539
+
1540
+ container.innerHTML = filterHtml + cardsHtml;
1541
+ }
1542
+
1543
+ function setSidebarGuidesFilter(source) {
1544
+ sidebarGuidesFilter = source;
1545
+ const guidesEl = document.getElementById('sidebarGuides');
1546
+ if (guidesEl) renderSidebarGuides(guidesEl);
1547
+ }
1548
+
1549
+ function filterSidebarGuides() {
1550
+ const q = (document.getElementById('sidebarGuidesSearch')?.value || '').toLowerCase().trim();
1551
+ const guidesEl = document.getElementById('sidebarGuides');
1552
+ if (!guidesEl) return;
1553
+ // Re-render with search filter
1554
+ let filtered = state.programs || [];
1555
+ if (sidebarGuidesFilter !== 'all') {
1556
+ filtered = filtered.filter(p => (p.source || 'platform') === sidebarGuidesFilter);
1557
+ }
1558
+ if (q) {
1559
+ filtered = filtered.filter(p =>
1560
+ (p.title || p.name || '').toLowerCase().includes(q) ||
1561
+ (p.description || '').toLowerCase().includes(q)
1562
+ );
1563
+ }
1564
+ // Re-render just the cards portion
1565
+ const cards = guidesEl.querySelectorAll('div[onclick]');
1566
+ // Simpler: just re-render all
1567
+ renderSidebarGuides(guidesEl);
1568
+ }
1569
+
1570
+ /* ─── Coach Header ───────────────────────── */
1571
+ function renderCoachHeader() {
1572
+ const trainer = TRAINERS.find(t => t.id === state.selectedTrainer) || TRAINERS[0];
1573
+ document.getElementById('coachHeader').innerHTML = `
1574
+ <div class="coach-avatar" style="background:${trainer.color}">${trainer.initials}</div>
1575
+ <div>
1576
+ <div class="coach-name">${trainer.name}</div>
1577
+ <div class="coach-status">${trainer.style}</div>
1578
+ </div>
1579
+ `;
1580
+ }
1581
+
1582
+ /* ─── Sidebar Actions ────────────────────── */
1583
+ function handleSidebarAction(action) {
1584
+ const input = document.getElementById('chatInput');
1585
+ if (!input) return;
1586
+ const actionMessages = {
1587
+ insights: 'Run a fresh analysis of my activity and give me your coaching insights.',
1588
+ explain: 'Explain what we just covered — break it down simply.',
1589
+ quiz: 'Quiz me on what I should know so far.',
1590
+ ask: '',
1591
+ };
1592
+ const msg = actionMessages[action];
1593
+ if (msg) {
1594
+ input.value = msg;
1595
+ sendMessage();
1596
+ } else {
1597
+ input.focus();
1598
+ }
1599
+ }
1600
+
1601
+ /* ─── Resize Handle ──────────────────────── */
1602
+ (function initResizeHandle() {
1603
+ let isResizing = false;
1604
+ let startX = 0;
1605
+ let startWidth = 0;
1606
+
1607
+ document.addEventListener('mousedown', (e) => {
1608
+ if (e.target.id !== 'gymResizeHandle') return;
1609
+ isResizing = true;
1610
+ const sidebar = document.getElementById('gymSidebar');
1611
+ startX = e.clientX;
1612
+ startWidth = sidebar.offsetWidth;
1613
+ e.target.classList.add('active');
1614
+ document.body.style.cursor = 'col-resize';
1615
+ document.body.style.userSelect = 'none';
1616
+ e.preventDefault();
1617
+ });
1618
+
1619
+ document.addEventListener('mousemove', (e) => {
1620
+ if (!isResizing) return;
1621
+ const sidebar = document.getElementById('gymSidebar');
1622
+ const newWidth = startWidth + (e.clientX - startX);
1623
+ sidebar.style.width = Math.max(200, Math.min(600, newWidth)) + 'px';
1624
+ });
1625
+
1626
+ document.addEventListener('mouseup', () => {
1627
+ if (!isResizing) return;
1628
+ isResizing = false;
1629
+ const handle = document.getElementById('gymResizeHandle');
1630
+ if (handle) handle.classList.remove('active');
1631
+ document.body.style.cursor = '';
1632
+ document.body.style.userSelect = '';
1633
+ });
1634
+ })();
1635
+
1636
+ /* ─── Guides Grid ────────────────────────── */
1637
+ let guidesActiveSource = 'all';
1638
+
1639
+ function renderGuidesView() {
1640
+ applyGuidesFilter();
1641
+ }
1642
+
1643
+ function filterBySource(source) {
1644
+ guidesActiveSource = source;
1645
+ document.querySelectorAll('.guides-filter-pill').forEach(p =>
1646
+ p.classList.toggle('active', p.dataset.source === source)
1647
+ );
1648
+ applyGuidesFilter();
1649
+ }
1650
+
1651
+ function applyGuidesFilter() {
1652
+ const q = (document.getElementById('guidesSearch')?.value || '').toLowerCase().trim();
1653
+ let filtered = state.programs;
1654
+
1655
+ if (guidesActiveSource !== 'all') {
1656
+ filtered = filtered.filter(p => (p.source || 'platform') === guidesActiveSource);
1657
+ }
1658
+ if (q) {
1659
+ filtered = filtered.filter(p =>
1660
+ (p.title || p.name || '').toLowerCase().includes(q) ||
1661
+ (p.description || '').toLowerCase().includes(q) ||
1662
+ (p.difficulty || '').toLowerCase().includes(q)
1663
+ );
1664
+ }
1665
+ renderGuideCards(filtered, document.getElementById('guidesGrid'));
1666
+ }
1667
+
1668
+ function renderGuideCards(programs, container) {
1669
+ if (!programs.length) {
1670
+ container.innerHTML = '<div style="font-size:13px;color:var(--text-muted);text-align:center;padding:60px 20px">No guides available yet. Use the AI Generator to create one.</div>';
1671
+ return;
1672
+ }
1673
+ container.innerHTML = programs.map(p => {
1674
+ const moduleCount = p.modules?.length || 0;
1675
+ const stepCount = p.modules?.reduce((sum, m) => sum + (m.steps?.length || 0), 0) || 0;
1676
+ const slug = p.slug || p.id;
1677
+ const progress = getGuideProgress(slug);
1678
+ const ctaText = progress > 0 ? 'Continue' : 'Start';
1679
+ const source = p.source || 'platform';
1680
+ const sourceLabel = { platform: 'Platform', coach: 'Coach', user: 'My Guide' }[source] || source;
1681
+ const sourceColor = { platform: 'var(--accent)', coach: 'var(--purple)', user: 'var(--green)' }[source] || 'var(--text-muted)';
1682
+ return `
1683
+ <div class="guide-card" onclick="openGuide('${slug}')">
1684
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
1685
+ <div class="guide-card-title" style="margin-bottom:0">${p.title || p.name || 'Untitled'}</div>
1686
+ <span style="font-family:var(--font-mono);font-size:9px;font-weight:700;padding:2px 8px;border-radius:4px;color:${sourceColor};background:${sourceColor}15;flex-shrink:0">${sourceLabel}</span>
1687
+ </div>
1688
+ <div class="guide-card-desc">${p.description || ''}</div>
1689
+ <div class="guide-card-meta">
1690
+ ${moduleCount ? `<span class="guide-card-tag">${moduleCount} module${moduleCount !== 1 ? 's' : ''}</span>` : ''}
1691
+ ${stepCount ? `<span class="guide-card-tag">${stepCount} steps</span>` : ''}
1692
+ ${p.difficulty ? `<span class="guide-card-tag difficulty">${p.difficulty}</span>` : ''}
1693
+ ${p.estimatedTime ? `<span class="guide-card-tag">${p.estimatedTime}</span>` : ''}
1694
+ </div>
1695
+ <div class="guide-card-footer">
1696
+ <button class="guide-card-cta" onclick="event.stopPropagation();openGuide('${slug}')">${ctaText}</button>
1697
+ </div>
1698
+ </div>`;
1699
+ }).join('');
1700
+ }
1701
+
1702
+ function filterGuides() {
1703
+ applyGuidesFilter();
1704
+ }
1705
+
1706
+ function getGuideProgress(slug) {
1707
+ const prog = state.progress?.programs?.[slug];
1708
+ if (!prog) return 0;
1709
+ return prog.completedSteps?.length || 0;
1710
+ }
1711
+
1712
+ /* ─── Guide Viewer ───────────────────────── */
1713
+ async function openGuide(slug) {
1714
+ try {
1715
+ const res = await fetch(`${API}/api/gym/programs/${slug}`);
1716
+ if (!res.ok) throw new Error('Failed to load guide');
1717
+ const program = await res.json();
1718
+
1719
+ const savedProgress = state.progress?.programs?.[slug] || {};
1720
+ state.guideViewer = {
1721
+ slug,
1722
+ program,
1723
+ moduleIndex: savedProgress.lastModuleIndex || 0,
1724
+ stepIndex: savedProgress.lastStepIndex || 0,
1725
+ completedSteps: new Set(savedProgress.completedSteps || []),
1726
+ chatHistory: [],
1727
+ };
1728
+
1729
+ switchView('guide-viewer');
1730
+ renderGuideViewer();
1731
+ } catch (e) {
1732
+ console.error('Failed to open guide:', e);
1733
+ }
1734
+ }
1735
+
1736
+ function renderGuideViewer() {
1737
+ const gv = state.guideViewer;
1738
+ const p = gv.program;
1739
+ document.getElementById('gvTitle').textContent = p.title || p.name || 'Guide';
1740
+
1741
+ // Set trainer identity in chat header
1742
+ const trainer = TRAINERS.find(t => t.id === state.selectedTrainer) || TRAINERS[0];
1743
+ const avatarEl = document.getElementById('gvChatAvatar');
1744
+ const nameEl = document.getElementById('gvChatName');
1745
+ if (avatarEl) { avatarEl.textContent = trainer.initials; avatarEl.style.background = trainer.color; }
1746
+ if (nameEl) nameEl.textContent = trainer.name;
1747
+
1748
+ // Module tabs
1749
+ const modulesEl = document.getElementById('gvModules');
1750
+ modulesEl.innerHTML = (p.modules || []).map((m, i) => `
1751
+ <button class="gv-module-tab${i === gv.moduleIndex ? ' active' : ''}" onclick="switchGuideModule(${i})">
1752
+ ${m.title || 'Module ' + (i + 1)}
1753
+ </button>
1754
+ `).join('');
1755
+
1756
+ renderGuideStepPills();
1757
+ renderGuideStep();
1758
+ }
1759
+
1760
+ function renderGuideStepPills() {
1761
+ const gv = state.guideViewer;
1762
+ const module = gv.program.modules?.[gv.moduleIndex];
1763
+ if (!module?.steps?.length) return;
1764
+
1765
+ const pillsEl = document.getElementById('gvStepPills');
1766
+ pillsEl.innerHTML = module.steps.map((s, i) => {
1767
+ const key = `${module.id}:${s.id}`;
1768
+ const isDone = gv.completedSteps.has(key);
1769
+ const isActive = i === gv.stepIndex;
1770
+ return `<button class="gv-step-pill${isActive ? ' active' : ''}${isDone ? ' done' : ''}" onclick="goToGuideStep(${i})">${s.title || 'Step ' + (i + 1)}</button>`;
1771
+ }).join('');
1772
+ }
1773
+
1774
+ function renderGuideStep() {
1775
+ const gv = state.guideViewer;
1776
+ const module = gv.program.modules?.[gv.moduleIndex];
1777
+ const step = module?.steps?.[gv.stepIndex];
1778
+
1779
+ const contentEl = document.getElementById('gvStepContent');
1780
+ if (!step) {
1781
+ contentEl.innerHTML = '<div style="color:var(--text-muted);padding:40px;text-align:center">No content for this step.</div>';
1782
+ return;
1783
+ }
1784
+
1785
+ contentEl.innerHTML = `
1786
+ <h2 style="margin-top:0">${step.title || ''}</h2>
1787
+ ${renderStepContent(step.content || '')}
1788
+ ${step.verificationQuestions?.length ? `
1789
+ <div style="margin-top:24px;padding:16px;background:var(--accent-bg);border-radius:var(--radius);border:1px solid var(--border-glow)">
1790
+ <div style="font-weight:700;font-size:12px;color:var(--accent);margin-bottom:8px">Check Your Understanding</div>
1791
+ ${step.verificationQuestions.map(q => `<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px">• ${q}</div>`).join('')}
1792
+ </div>
1793
+ ` : ''}
1794
+ `;
1795
+ contentEl.scrollTop = 0;
1796
+
1797
+ // Update completion checkbox
1798
+ const key = `${module.id}:${step.id}`;
1799
+ document.getElementById('gvComplete').checked = gv.completedSteps.has(key);
1800
+
1801
+ // Update nav buttons
1802
+ const flatSteps = getFlatSteps();
1803
+ const flatIdx = getFlatStepIndex();
1804
+ document.getElementById('gvPrev').disabled = flatIdx <= 0;
1805
+ document.getElementById('gvNext').disabled = flatIdx >= flatSteps.length - 1;
1806
+
1807
+ // Update chat context
1808
+ document.getElementById('gvChatContext').textContent = 'Asking about: ' + (step.title || 'Step');
1809
+
1810
+ // Clear guide chat
1811
+ document.getElementById('gvChatMessages').innerHTML = '';
1812
+ }
1813
+
1814
+ function renderStepContent(text) {
1815
+ return text
1816
+ .replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
1817
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1818
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
1819
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
1820
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
1821
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
1822
+ .replace(/^\- (.+)$/gm, '<li>$1</li>')
1823
+ .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
1824
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
1825
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
1826
+ .replace(/\n\n/g, '</p><p>')
1827
+ .replace(/\n/g, '<br>');
1828
+ }
1829
+
1830
+ function getFlatSteps() {
1831
+ const modules = state.guideViewer.program.modules || [];
1832
+ const flat = [];
1833
+ modules.forEach((m, mi) => (m.steps || []).forEach((s, si) => flat.push({ moduleIndex: mi, stepIndex: si, step: s, module: m })));
1834
+ return flat;
1835
+ }
1836
+
1837
+ function getFlatStepIndex() {
1838
+ const gv = state.guideViewer;
1839
+ const flat = getFlatSteps();
1840
+ return flat.findIndex(f => f.moduleIndex === gv.moduleIndex && f.stepIndex === gv.stepIndex);
1841
+ }
1842
+
1843
+ function switchGuideModule(index) {
1844
+ state.guideViewer.moduleIndex = index;
1845
+ state.guideViewer.stepIndex = 0;
1846
+ renderGuideViewer();
1847
+ }
1848
+
1849
+ function goToGuideStep(index) {
1850
+ state.guideViewer.stepIndex = index;
1851
+ renderGuideStepPills();
1852
+ renderGuideStep();
1853
+ }
1854
+
1855
+ function prevStep() {
1856
+ const flat = getFlatSteps();
1857
+ const idx = getFlatStepIndex();
1858
+ if (idx <= 0) return;
1859
+ const prev = flat[idx - 1];
1860
+ state.guideViewer.moduleIndex = prev.moduleIndex;
1861
+ state.guideViewer.stepIndex = prev.stepIndex;
1862
+ renderGuideViewer();
1863
+ }
1864
+
1865
+ function nextStep() {
1866
+ const flat = getFlatSteps();
1867
+ const idx = getFlatStepIndex();
1868
+ if (idx >= flat.length - 1) return;
1869
+ const next = flat[idx + 1];
1870
+ state.guideViewer.moduleIndex = next.moduleIndex;
1871
+ state.guideViewer.stepIndex = next.stepIndex;
1872
+ renderGuideViewer();
1873
+ }
1874
+
1875
+ async function toggleStepComplete() {
1876
+ const gv = state.guideViewer;
1877
+ const module = gv.program.modules?.[gv.moduleIndex];
1878
+ const step = module?.steps?.[gv.stepIndex];
1879
+ if (!module || !step) return;
1880
+ const key = `${module.id}:${step.id}`;
1881
+
1882
+ if (gv.completedSteps.has(key)) gv.completedSteps.delete(key);
1883
+ else gv.completedSteps.add(key);
1884
+
1885
+ renderGuideStepPills();
1886
+
1887
+ const programs = state.progress?.programs || {};
1888
+ programs[gv.slug] = {
1889
+ completedSteps: Array.from(gv.completedSteps),
1890
+ lastModuleIndex: gv.moduleIndex,
1891
+ lastStepIndex: gv.stepIndex,
1892
+ updatedAt: new Date().toISOString(),
1893
+ };
1894
+ state.progress = { ...state.progress, programs };
1895
+ await fetch(`${API}/api/gym/progress`, {
1896
+ method: 'PUT',
1897
+ headers: { 'Content-Type': 'application/json' },
1898
+ body: JSON.stringify(state.progress)
1899
+ }).catch(() => {});
1900
+ }
1901
+
1902
+ function exitGuideViewer() {
1903
+ switchView('guides');
1904
+ }
1905
+
1906
+ function guideAiAction(action) {
1907
+ const gv = state.guideViewer;
1908
+ const module = gv.program.modules?.[gv.moduleIndex];
1909
+ const step = module?.steps?.[gv.stepIndex];
1910
+ if (!step) return;
1911
+
1912
+ const prompts = {
1913
+ explain: `Explain this concept to me in simpler terms: "${step.title}"`,
1914
+ quiz: `Quiz me on what I should have learned from this step: "${step.title}". Ask me 2-3 questions.`,
1915
+ };
1916
+ if (action === 'ask') {
1917
+ document.getElementById('gvChatInput').focus();
1918
+ } else {
1919
+ sendGuideMessage(prompts[action]);
1920
+ }
1921
+ }
1922
+
1923
+ function addChatTo(containerId, role, text) {
1924
+ const container = document.getElementById(containerId);
1925
+ const div = document.createElement('div');
1926
+ div.className = `chat-msg ${role}`;
1927
+ div.innerHTML = formatMessage(text);
1928
+ container.appendChild(div);
1929
+ container.scrollTop = container.scrollHeight;
1930
+ return div;
1931
+ }
1932
+
1933
+ async function sendGuideMessage(overrideText) {
1934
+ const input = document.getElementById('gvChatInput');
1935
+ const text = overrideText || input.value.trim();
1936
+ if (!text || state.sending) return;
1937
+
1938
+ const gv = state.guideViewer;
1939
+ const module = gv.program.modules?.[gv.moduleIndex];
1940
+ const step = module?.steps?.[gv.stepIndex];
1941
+ const contextPrefix = `[Context: User is viewing guide "${gv.program.title}", module "${module?.title}", step "${step?.title}". Step content: ${(step?.content || '').slice(0, 500)}]\n\n`;
1942
+
1943
+ state.sending = true;
1944
+ if (!overrideText) input.value = '';
1945
+
1946
+ addChatTo('gvChatMessages', 'user', text);
1947
+ showTypingIn('gvChatMessages');
1948
+
1949
+ try {
1950
+ const res = await fetch(`${API}/api/chat/gym/stream`, {
1951
+ method: 'POST',
1952
+ headers: { 'Content-Type': 'application/json' },
1953
+ body: JSON.stringify({ text: contextPrefix + text, senderId: 'web' })
1954
+ });
1955
+ const data = await res.json();
1956
+ if (data.jobId) {
1957
+ hideTypingIn('gvChatMessages');
1958
+ const msgDiv = addChatTo('gvChatMessages', 'assistant', '');
1959
+ await consumeSSE(data.jobId, (chunk) => {
1960
+ msgDiv.innerHTML = formatMessage(chunk);
1961
+ document.getElementById('gvChatMessages').scrollTop = document.getElementById('gvChatMessages').scrollHeight;
1962
+ });
1963
+ } else {
1964
+ hideTypingIn('gvChatMessages');
1965
+ addChatTo('gvChatMessages', 'assistant', data.response || data.text || 'Working on it...');
1966
+ }
1967
+ } catch (e) {
1968
+ hideTypingIn('gvChatMessages');
1969
+ addChatTo('gvChatMessages', 'assistant', 'Error connecting to coach. Please try again.');
1970
+ } finally {
1971
+ state.sending = false;
1972
+ }
1973
+ }
1974
+
1975
+ /* ─── AI Generator ───────────────────────── */
1976
+ function seedGenerator() {
1977
+ state._generatorSeeded = true;
1978
+ addChatTo('genChatMessages', 'assistant',
1979
+ "Hi! I can help you create a training guide. Just describe what you'd like to teach — the topic, audience, and goals — and I'll generate a structured guide with modules and steps.\n\nFor example: *\"Create a beginner guide to building MCP integrations for AI agents\"*"
1980
+ );
1981
+ }
1982
+
1983
+ async function sendGeneratorMessage() {
1984
+ const input = document.getElementById('genChatInput');
1985
+ const text = input.value.trim();
1986
+ if (!text || state.sending) return;
1987
+
1988
+ state.sending = true;
1989
+ input.value = '';
1990
+ autoResizeChat(input);
1991
+
1992
+ addChatTo('genChatMessages', 'user', text);
1993
+ showTypingIn('genChatMessages');
1994
+
1995
+ const prefix = '[Context: User is in the AI Guide Generator. Generate training guide content in structured markdown. Use # for the guide title, ## for modules (e.g. "## Module 1: Getting Started"), ### for steps within modules (e.g. "### Step 1: Setup"). Include detailed educational content under each step. Ask 2-3 clarifying questions first if the request is vague, then generate the full guide.]\n\n';
1996
+
1997
+ try {
1998
+ const res = await fetch(`${API}/api/chat/gym/stream`, {
1999
+ method: 'POST',
2000
+ headers: { 'Content-Type': 'application/json' },
2001
+ body: JSON.stringify({ text: prefix + text, senderId: 'web' })
2002
+ });
2003
+ const data = await res.json();
2004
+ if (data.jobId) {
2005
+ hideTypingIn('genChatMessages');
2006
+ const msgDiv = addChatTo('genChatMessages', 'assistant', '');
2007
+ let fullText = '';
2008
+ await consumeSSE(data.jobId, (chunk) => {
2009
+ fullText = chunk;
2010
+ msgDiv.innerHTML = formatMessage(chunk);
2011
+ document.getElementById('genChatMessages').scrollTop = document.getElementById('genChatMessages').scrollHeight;
2012
+ parseAndPreview(chunk);
2013
+ });
2014
+ state._generatorMarkdown = fullText;
2015
+ } else {
2016
+ hideTypingIn('genChatMessages');
2017
+ addChatTo('genChatMessages', 'assistant', data.response || data.text || 'Working on it...');
2018
+ }
2019
+ } catch (e) {
2020
+ hideTypingIn('genChatMessages');
2021
+ addChatTo('genChatMessages', 'assistant', 'Error connecting. Please try again.');
2022
+ } finally {
2023
+ state.sending = false;
2024
+ }
2025
+ }
2026
+
2027
+ function parseAndPreview(markdown) {
2028
+ const lines = markdown.split('\n');
2029
+ let title = '';
2030
+ const modules = [];
2031
+ let currentModule = null;
2032
+
2033
+ for (const line of lines) {
2034
+ const h1 = line.match(/^#\s+(.+)/);
2035
+ if (h1 && !line.startsWith('##')) { title = h1[1].replace(/\*\*/g, '').trim(); continue; }
2036
+ const h2 = line.match(/^##\s+(.+)/);
2037
+ if (h2 && !line.startsWith('###')) {
2038
+ currentModule = { title: h2[1].replace(/\*\*/g, '').trim(), steps: [] };
2039
+ modules.push(currentModule);
2040
+ continue;
2041
+ }
2042
+ const h3 = line.match(/^###\s+(.+)/);
2043
+ if (h3 && currentModule) {
2044
+ currentModule.steps.push(h3[1].replace(/\*\*/g, '').trim());
2045
+ }
2046
+ }
2047
+
2048
+ const preview = document.getElementById('genPreviewContent');
2049
+ if (title || modules.length) {
2050
+ let html = `<div style="font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--text-primary);margin-bottom:16px">${title || 'Untitled Guide'}</div>`;
2051
+ modules.forEach((m, i) => {
2052
+ html += `<div class="gen-module-preview">
2053
+ <div class="gen-module-title">Module ${i + 1}: ${m.title}</div>
2054
+ ${m.steps.map((s, j) => `<div class="gen-step-preview">${j + 1}. ${s}</div>`).join('')}
2055
+ </div>`;
2056
+ });
2057
+ preview.innerHTML = html;
2058
+ document.getElementById('genImportBtn').disabled = false;
2059
+ }
2060
+ }
2061
+
2062
+ async function importGeneratedGuide() {
2063
+ const markdown = state._generatorMarkdown;
2064
+ if (!markdown) return;
2065
+
2066
+ const btn = document.getElementById('genImportBtn');
2067
+ btn.disabled = true;
2068
+ btn.textContent = 'Importing...';
2069
+
2070
+ try {
2071
+ const res = await fetch(`${API}/api/gym/programs/import-markdown`, {
2072
+ method: 'POST',
2073
+ headers: { 'Content-Type': 'application/json' },
2074
+ body: JSON.stringify({ markdown })
2075
+ });
2076
+ const program = await res.json();
2077
+ if (program.error) throw new Error(program.error);
2078
+
2079
+ state.programs.push(program);
2080
+ btn.textContent = 'Imported!';
2081
+ addChatTo('genChatMessages', 'assistant', `Guide "${program.title || 'Untitled'}" has been imported! You can find it in the Guides section.`);
2082
+ setTimeout(() => { btn.textContent = 'Import Guide'; btn.disabled = true; }, 3000);
2083
+ } catch (e) {
2084
+ console.error('Import failed:', e);
2085
+ btn.textContent = 'Import Failed';
2086
+ setTimeout(() => { btn.textContent = 'Import Guide'; btn.disabled = false; }, 2000);
2087
+ }
2088
+ }
2089
+
2090
+ /* ─── Shared Chat Helpers ────────────────── */
2091
+ function showTypingIn(containerId) {
2092
+ const container = document.getElementById(containerId);
2093
+ const div = document.createElement('div');
2094
+ div.className = 'chat-typing';
2095
+ div.id = containerId + '_typing';
2096
+ div.innerHTML = '<span></span><span></span><span></span>';
2097
+ container.appendChild(div);
2098
+ container.scrollTop = container.scrollHeight;
2099
+ }
2100
+
2101
+ function hideTypingIn(containerId) {
2102
+ const el = document.getElementById(containerId + '_typing');
2103
+ if (el) el.remove();
2104
+ }
2105
+
2106
+ /* ─── Chat ───────────────────────────────── */
2107
+ function addChatMessage(role, text) {
2108
+ return addChatTo('chatMessages', role, text);
2109
+ }
2110
+
2111
+ function formatMessage(text) {
2112
+ // Markdown: headers, bold, italic, code blocks, inline code, lists, blockquotes
2113
+ let html = text
2114
+ .replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
2115
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
2116
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
2117
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
2118
+ .replace(/\n/g, '<br>');
2119
+ return html;
2120
+ }
2121
+
2122
+ function showTyping() {
2123
+ const container = document.getElementById('chatMessages');
2124
+ const div = document.createElement('div');
2125
+ div.className = 'chat-typing';
2126
+ div.id = 'typingIndicator';
2127
+ div.innerHTML = '<span style="width:auto;height:auto;background:none;animation:none;font-size:12px;color:var(--text-muted);font-family:var(--font-sans);margin-right:6px">Thinking</span><span></span><span></span><span></span>';
2128
+ container.appendChild(div);
2129
+ container.scrollTop = container.scrollHeight;
2130
+ }
2131
+
2132
+ function hideTyping() {
2133
+ const el = document.getElementById('typingIndicator');
2134
+ if (el) el.remove();
2135
+ }
2136
+
2137
+ async function sendMessage(overrideText) {
2138
+ const input = document.getElementById('chatInput');
2139
+ const text = overrideText || input.value.trim();
2140
+ if (!text || state.sending) return;
2141
+
2142
+ state.sending = true;
2143
+ if (!overrideText) input.value = '';
2144
+ autoResizeChat(input);
2145
+ document.getElementById('chatSend').disabled = true;
2146
+
2147
+ // Upload pending files
2148
+ let fileRefs = [];
2149
+ if (gymPendingFiles.length > 0) {
2150
+ fileRefs = await gymUploadFiles();
2151
+ }
2152
+
2153
+ const displayText = fileRefs.length
2154
+ ? text + '\n' + fileRefs.map(f => `[File ${f.mode === 'permanent' ? 'saved' : 'attached'}: ${f.name} at ${f.path}]`).join('\n')
2155
+ : text;
2156
+ const sendText = fileRefs.length
2157
+ ? text + '\n\n' + fileRefs.map(f => `[Attached file: ${f.name} at ${f.path}]`).join('\n')
2158
+ : text;
2159
+
2160
+ addChatMessage('user', displayText);
2161
+ showTyping();
2162
+
2163
+ try {
2164
+ const res = await fetch(`${API}/api/chat/gym/stream`, {
2165
+ method: 'POST',
2166
+ headers: { 'Content-Type': 'application/json' },
2167
+ body: JSON.stringify({ text: sendText, senderId: 'web' })
2168
+ });
2169
+ const data = await res.json();
2170
+
2171
+ if (data.jobId) {
2172
+ if (gymRawLogsOpen) gymStreamRawLogs(data.jobId);
2173
+ let msgDiv = null;
2174
+ let firstChunk = true;
2175
+ await consumeSSE(data.jobId, (chunk) => {
2176
+ if (firstChunk) { hideTyping(); msgDiv = addChatMessage('assistant', ''); firstChunk = false; }
2177
+ msgDiv.innerHTML = formatMessage(chunk);
2178
+ document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
2179
+ });
2180
+ if (firstChunk) hideTyping();
2181
+ // Speak response if voice mode on
2182
+ if (gymVoiceModeOn) gymSpeakText(msgDiv.textContent);
2183
+ } else if (data.response || data.text || data.message) {
2184
+ hideTyping();
2185
+ const reply = data.response || data.text || data.message;
2186
+ addChatMessage('assistant', reply);
2187
+ if (gymVoiceModeOn) gymSpeakText(reply);
2188
+ } else {
2189
+ hideTyping();
2190
+ addChatMessage('assistant', 'Received your message. Working on it...');
2191
+ }
2192
+ } catch (e) {
2193
+ hideTyping();
2194
+ addChatMessage('assistant', 'Sorry, there was an error connecting to the coach. Please try again.');
2195
+ console.error('Chat error:', e);
2196
+ } finally {
2197
+ state.sending = false;
2198
+ document.getElementById('chatSend').disabled = false;
2199
+ }
2200
+ }
2201
+
2202
+ function consumeSSE(jobId, onText) {
2203
+ return new Promise((resolve, reject) => {
2204
+ const sse = new EventSource(`${API}/api/chat/jobs/${jobId}/stream`);
2205
+ let fullText = '';
2206
+
2207
+ sse.onmessage = (event) => {
2208
+ try {
2209
+ const data = JSON.parse(event.data);
2210
+ if (data.type === 'text' || data.type === 'content') {
2211
+ fullText += data.data || data.text || data.content || '';
2212
+ onText(fullText);
2213
+ } else if (data.type === 'done' || data.type === 'end' || data.done) {
2214
+ if (data.data || data.text) {
2215
+ fullText = data.data || data.text;
2216
+ onText(fullText);
2217
+ }
2218
+ sse.close();
2219
+ resolve(fullText);
2220
+ } else if (data.type === 'error') {
2221
+ sse.close();
2222
+ reject(new Error(data.error || data.data || 'Stream error'));
2223
+ } else if (data.type === 'status') {
2224
+ // Status events (e.g. "Starting...") — ignore
2225
+ }
2226
+ } catch (e) {
2227
+ // Plain text event
2228
+ fullText += event.data;
2229
+ onText(fullText);
2230
+ }
2231
+ };
2232
+
2233
+ sse.addEventListener('done', () => { sse.close(); resolve(fullText); });
2234
+ sse.addEventListener('end', () => { sse.close(); resolve(fullText); });
2235
+ sse.addEventListener('result', (e) => {
2236
+ try {
2237
+ const data = JSON.parse(e.data);
2238
+ fullText = data.data || data.text || data.response || fullText;
2239
+ onText(fullText);
2240
+ } catch (err) {}
2241
+ sse.close();
2242
+ resolve(fullText);
2243
+ });
2244
+
2245
+ sse.onerror = () => {
2246
+ sse.close();
2247
+ if (fullText) resolve(fullText);
2248
+ else reject(new Error('SSE connection failed'));
2249
+ };
2250
+
2251
+ // Safety timeout
2252
+ setTimeout(() => {
2253
+ sse.close();
2254
+ resolve(fullText);
2255
+ }, 120000);
2256
+ });
2257
+ }
2258
+
2259
+ function autoResizeChat(el) {
2260
+ el.style.height = 'auto';
2261
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
2262
+ }
2263
+
2264
+ /* ─── File Attachments ──────────────────── */
2265
+ let gymPendingFiles = [];
2266
+
2267
+ function gymToggleFileZone() {
2268
+ const zone = document.getElementById('gymFileDropZone');
2269
+ if (!zone) return;
2270
+ zone.classList.toggle('hidden');
2271
+ const btn = document.getElementById('gymClipBtn');
2272
+ if (btn) btn.classList.toggle('has-files', !zone.classList.contains('hidden'));
2273
+ }
2274
+
2275
+ function gymHandleFileDrop(e) {
2276
+ const files = e.dataTransfer?.files;
2277
+ if (files) gymAddFiles(files);
2278
+ }
2279
+
2280
+ function gymHandleFileSelect(e) {
2281
+ const files = e.target?.files;
2282
+ if (files) gymAddFiles(files);
2283
+ e.target.value = '';
2284
+ }
2285
+
2286
+ function gymAddFiles(fileList) {
2287
+ for (const f of fileList) {
2288
+ if (!gymPendingFiles.find(p => p.file.name === f.name && p.file.size === f.size)) {
2289
+ gymPendingFiles.push({ file: f, mode: 'temp' });
2290
+ }
2291
+ }
2292
+ gymRenderFileList();
2293
+ }
2294
+
2295
+ function gymRemoveFile(idx) {
2296
+ gymPendingFiles.splice(idx, 1);
2297
+ gymRenderFileList();
2298
+ if (gymPendingFiles.length === 0) {
2299
+ const btn = document.getElementById('gymClipBtn');
2300
+ if (btn) btn.classList.remove('has-files');
2301
+ }
2302
+ }
2303
+
2304
+ function gymRenderFileList() {
2305
+ const area = document.getElementById('gymFileListArea');
2306
+ if (!area) return;
2307
+ if (gymPendingFiles.length === 0) {
2308
+ area.innerHTML = '<span>Drop files here or click to browse</span>';
2309
+ return;
2310
+ }
2311
+ area.innerHTML = '<div class="gym-file-list">' + gymPendingFiles.map((p, i) => {
2312
+ const size = p.file.size > 1024*1024
2313
+ ? (p.file.size/(1024*1024)).toFixed(1) + ' MB'
2314
+ : (p.file.size/1024).toFixed(0) + ' KB';
2315
+ return `<div class="gym-file-pill">
2316
+ <span class="gym-file-pill-name">${p.file.name}</span>
2317
+ <span style="color:var(--text-muted);font-size:8px">${size}</span>
2318
+ <span class="gym-file-pill-rm" onclick="gymRemoveFile(${i})">&times;</span>
2319
+ </div>`;
2320
+ }).join('') + '</div>';
2321
+ }
2322
+
2323
+ async function gymUploadFiles() {
2324
+ const uploaded = [];
2325
+ for (const p of gymPendingFiles) {
2326
+ try {
2327
+ const formData = new FormData();
2328
+ formData.append('file', p.file);
2329
+ formData.append('mode', p.mode);
2330
+ const resp = await fetch(`${API}/api/upload/gym`, { method: 'POST', body: formData });
2331
+ const data = await resp.json();
2332
+ if (data.ok) uploaded.push({ name: data.fileName, path: data.path, mode: data.mode });
2333
+ } catch (err) { console.warn('Upload failed:', p.file.name, err); }
2334
+ }
2335
+ gymPendingFiles = [];
2336
+ gymRenderFileList();
2337
+ const zone = document.getElementById('gymFileDropZone');
2338
+ if (zone) zone.classList.add('hidden');
2339
+ const btn = document.getElementById('gymClipBtn');
2340
+ if (btn) btn.classList.remove('has-files');
2341
+ return uploaded;
2342
+ }
2343
+
2344
+ /* ─── Voice Input ───────────────────────── */
2345
+ let gymRecognition = null;
2346
+ let gymIsRecording = false;
2347
+
2348
+ function gymToggleVoice() {
2349
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
2350
+ alert('Speech recognition not supported in this browser. Use Chrome or Edge.');
2351
+ return;
2352
+ }
2353
+ if (gymIsRecording) gymStopVoice(); else gymStartVoice();
2354
+ }
2355
+
2356
+ function gymStartVoice() {
2357
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
2358
+ gymRecognition = new SpeechRecognition();
2359
+ gymRecognition.continuous = true;
2360
+ gymRecognition.interimResults = true;
2361
+ gymRecognition.lang = 'en-US';
2362
+
2363
+ let finalTranscript = '';
2364
+ const input = document.getElementById('chatInput');
2365
+ const existingText = input ? input.value : '';
2366
+
2367
+ gymRecognition.onresult = (event) => {
2368
+ let interim = '';
2369
+ for (let i = event.resultIndex; i < event.results.length; i++) {
2370
+ if (event.results[i].isFinal) finalTranscript += event.results[i][0].transcript + ' ';
2371
+ else interim += event.results[i][0].transcript;
2372
+ }
2373
+ if (input) {
2374
+ input.value = existingText + (existingText ? ' ' : '') + finalTranscript + interim;
2375
+ autoResizeChat(input);
2376
+ }
2377
+ };
2378
+ gymRecognition.onerror = () => gymStopVoice();
2379
+ gymRecognition.onend = () => { if (gymIsRecording) gymStopVoice(); };
2380
+ gymRecognition.start();
2381
+ gymIsRecording = true;
2382
+ const btn = document.getElementById('gymMicBtn');
2383
+ if (btn) { btn.classList.add('recording'); btn.title = 'Click to stop'; }
2384
+ }
2385
+
2386
+ function gymStopVoice() {
2387
+ if (gymRecognition) { gymRecognition.stop(); gymRecognition = null; }
2388
+ gymIsRecording = false;
2389
+ const btn = document.getElementById('gymMicBtn');
2390
+ if (btn) { btn.classList.remove('recording'); btn.title = 'Click to speak'; }
2391
+ const input = document.getElementById('chatInput');
2392
+ if (input) input.focus();
2393
+ }
2394
+
2395
+ /* ─── Voice Read-Back ───────────────────── */
2396
+ let gymVoiceModeOn = false;
2397
+
2398
+ function gymToggleVoiceMode() {
2399
+ gymVoiceModeOn = !gymVoiceModeOn;
2400
+ const btn = document.getElementById('gymVoiceModeBtn');
2401
+ if (btn) { btn.innerHTML = gymVoiceModeOn ? '&#x1F50A;' : '&#x1F507;'; btn.style.color = gymVoiceModeOn ? 'var(--accent)' : ''; }
2402
+ if (!gymVoiceModeOn) window.speechSynthesis?.cancel();
2403
+ }
2404
+
2405
+ function gymSpeakText(text) {
2406
+ if (!gymVoiceModeOn || !window.speechSynthesis) return;
2407
+ const clean = text.replace(/[*_`#>~]/g, '').replace(/\n+/g, ' ').trim();
2408
+ if (!clean) return;
2409
+ window.speechSynthesis.cancel();
2410
+ const u = new SpeechSynthesisUtterance(clean);
2411
+ u.rate = 1.05;
2412
+ window.speechSynthesis.speak(u);
2413
+ }
2414
+
2415
+ /* ─── Raw Logs ──────────────────────────── */
2416
+ let gymRawLogsOpen = false;
2417
+ let gymRawLogsReader = null;
2418
+ let gymRawMode = false;
2419
+
2420
+ function gymToggleRawLogs() {
2421
+ gymRawLogsOpen = !gymRawLogsOpen;
2422
+ const drawer = document.getElementById('gymRawDrawer');
2423
+ const btn = document.getElementById('gymRawBtn');
2424
+ if (drawer) drawer.classList.toggle('open', gymRawLogsOpen);
2425
+ if (btn) btn.classList.toggle('active', gymRawLogsOpen);
2426
+ if (gymRawLogsOpen) {
2427
+ const el = document.getElementById('gymRawContent');
2428
+ if (el) el.textContent = 'No active job. Send a message to start streaming logs.\n';
2429
+ }
2430
+ }
2431
+
2432
+ /* ─── Guide Viewer toolbar delegates ────── */
2433
+ function gvToggleFileZone() { gymToggleFileZone(); }
2434
+ function gvToggleVoice() { gymToggleVoice(); }
2435
+ function gvToggleVoiceMode() { gymToggleVoiceMode(); }
2436
+ function gvToggleRawLogs() { gymToggleRawLogs(); }
2437
+
2438
+ async function gymStreamRawLogs(jobId) {
2439
+ const el = document.getElementById('gymRawContent');
2440
+ if (!el) return;
2441
+ el.textContent = '';
2442
+ try {
2443
+ const res = await fetch(`${API}/api/chat/jobs/${jobId}/raw?after=0`);
2444
+ const reader = res.body.getReader();
2445
+ gymRawLogsReader = reader;
2446
+ const decoder = new TextDecoder();
2447
+ let buffer = '';
2448
+ while (true) {
2449
+ const { done, value } = await reader.read();
2450
+ if (done) break;
2451
+ buffer += decoder.decode(value, { stream: true });
2452
+ const lines = buffer.split('\n');
2453
+ buffer = lines.pop() || '';
2454
+ for (const line of lines) {
2455
+ if (!line.startsWith('data: ')) continue;
2456
+ const data = line.slice(6);
2457
+ if (data === '[DONE]') { el.textContent += '\n--- Job complete ---\n'; break; }
2458
+ if (gymRawMode) {
2459
+ const span = document.createElement('span');
2460
+ span.className = data.startsWith('[stderr]') ? 'log-line log-stderr' : 'log-line';
2461
+ span.textContent = data + '\n';
2462
+ el.appendChild(span);
2463
+ } else {
2464
+ const fmt = gymFormatLogLine(data);
2465
+ if (fmt) {
2466
+ const span = document.createElement('span');
2467
+ span.className = 'log-line' + (fmt.cls ? ' ' + fmt.cls : '');
2468
+ span.textContent = fmt.icon + ' ' + fmt.text + '\n';
2469
+ el.appendChild(span);
2470
+ }
2471
+ }
2472
+ const drawer = document.getElementById('gymRawDrawer');
2473
+ if (drawer) drawer.scrollTop = drawer.scrollHeight;
2474
+ }
2475
+ }
2476
+ } catch { /* connection closed */ }
2477
+ }
2478
+
2479
+ function gymToggleRawMode() {
2480
+ gymRawMode = !gymRawMode;
2481
+ const btn = document.getElementById('gymRawModeBtn');
2482
+ if (btn) btn.innerHTML = gymRawMode ? '&#x25CF; Raw' : '&#x25CB; Clean';
2483
+ }
2484
+
2485
+ function gymFormatLogLine(data) {
2486
+ if (data.startsWith('[stderr]')) return { icon: '\u26A0', text: data.slice(9), cls: 'log-stderr' };
2487
+ try {
2488
+ let evt = JSON.parse(data);
2489
+ if (evt.type === 'stream_event' && evt.event) evt = evt.event;
2490
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'text_delta') return { icon: '\uD83D\uDCAC', text: evt.delta.text };
2491
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'thinking_delta') return { icon: '\uD83E\uDDE0', text: evt.delta.thinking };
2492
+ if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use') return { icon: '\uD83D\uDD27', text: evt.content_block.name };
2493
+ if (evt.type === 'result') return { icon: '\u2705', text: (evt.result || '').slice(0, 80) };
2494
+ if (evt.type === 'content_block_stop' || evt.type === 'message_stop' || evt.type === 'message_delta') return null;
2495
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'input_json_delta') return null;
2496
+ if (evt.type === 'rate_limit_event') return null;
2497
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'signature_delta') return null;
2498
+ if (evt.type === 'system' && evt.subtype === 'init') return { icon: '\u25CF', text: 'Session started \u2014 ' + (evt.model || '') + ' | ' + (evt.mcp_servers?.length || 0) + ' MCPs' };
2499
+ if (evt.type === 'system') return { icon: '\u2699', text: evt.subtype || 'system' };
2500
+ return { icon: '\u2022', text: evt.type || JSON.stringify(data).slice(0, 60), cls: 'log-dim' };
2501
+ } catch { return { icon: '\u2022', text: data.slice(0, 80), cls: 'log-dim' }; }
2502
+ }
2503
+
2504
+ /* ─── Progress / Radar Chart ─────────────── */
2505
+ function renderProgress() {
2506
+ const dims = state.profile?.dimensions || state.progress?.dimensions || {
2507
+ application: { score: 0, trend: 'stable' },
2508
+ communication: { score: 0, trend: 'stable' },
2509
+ knowledge: { score: 0, trend: 'stable' },
2510
+ orchestration: { score: 0, trend: 'stable' },
2511
+ craft: { score: 0, trend: 'stable' }
2512
+ };
2513
+
2514
+ renderRadar(dims);
2515
+ renderDimRows(dims);
2516
+ renderNextFocus(dims);
2517
+ renderMilestones();
2518
+ renderHistoryChart();
2519
+ }
2520
+
2521
+ function renderRadar(dims) {
2522
+ const svg = document.getElementById('radarChart');
2523
+ const cx = 140, cy = 140, maxR = 110;
2524
+ let html = '';
2525
+
2526
+ // Draw grid rings (1-5)
2527
+ for (let level = 1; level <= 5; level++) {
2528
+ const r = (level / 5) * maxR;
2529
+ const points = DIMENSIONS.map(d => {
2530
+ const rad = (d.angle * Math.PI) / 180;
2531
+ return `${cx + r * Math.cos(rad)},${cy + r * Math.sin(rad)}`;
2532
+ }).join(' ');
2533
+ html += `<polygon class="radar-grid" points="${points}" />`;
2534
+ }
2535
+
2536
+ // Draw axis lines
2537
+ DIMENSIONS.forEach(d => {
2538
+ const rad = (d.angle * Math.PI) / 180;
2539
+ const x2 = cx + maxR * Math.cos(rad);
2540
+ const y2 = cy + maxR * Math.sin(rad);
2541
+ html += `<line class="radar-axis" x1="${cx}" y1="${cy}" x2="${x2}" y2="${y2}" />`;
2542
+ });
2543
+
2544
+ // Draw data shape
2545
+ const dataPoints = DIMENSIONS.map(d => {
2546
+ const score = dims[d.key]?.score || 0;
2547
+ const r = (score / 5) * maxR;
2548
+ const rad = (d.angle * Math.PI) / 180;
2549
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
2550
+ });
2551
+
2552
+ const shapePoints = dataPoints.map(p => `${p.x},${p.y}`).join(' ');
2553
+ html += `<polygon class="radar-shape" points="${shapePoints}" />`;
2554
+
2555
+ // Draw data points
2556
+ dataPoints.forEach(p => {
2557
+ html += `<circle class="radar-point" cx="${p.x}" cy="${p.y}" />`;
2558
+ });
2559
+
2560
+ // Draw labels
2561
+ DIMENSIONS.forEach(d => {
2562
+ const rad = (d.angle * Math.PI) / 180;
2563
+ const lx = cx + (maxR + 24) * Math.cos(rad);
2564
+ const ly = cy + (maxR + 24) * Math.sin(rad);
2565
+ html += `<text class="radar-label" x="${lx}" y="${ly}" dy="3">${d.label}</text>`;
2566
+ });
2567
+
2568
+ svg.innerHTML = html;
2569
+ }
2570
+
2571
+ function renderDimRows(dims) {
2572
+ const container = document.getElementById('dimRows');
2573
+ container.innerHTML = DIMENSIONS.map(d => {
2574
+ const dim = dims[d.key] || { score: 0, trend: 'stable' };
2575
+ const pct = (dim.score / 5) * 100;
2576
+ const trendIcon = dim.trend === 'up' ? '\u2191' : dim.trend === 'down' ? '\u2193' : '\u2192';
2577
+ const trendClass = dim.trend || 'stable';
2578
+ return `
2579
+ <div class="dim-row">
2580
+ <div class="dim-label">${d.label}</div>
2581
+ <div class="dim-bar-wrap"><div class="dim-bar" style="width:${pct}%"></div></div>
2582
+ <div class="dim-score">${dim.score}/5</div>
2583
+ <div class="dim-trend ${trendClass}">${trendIcon}</div>
2584
+ </div>`;
2585
+ }).join('');
2586
+ }
2587
+
2588
+ function renderNextFocus(dims) {
2589
+ // Find weakest dimension
2590
+ let weakest = null;
2591
+ let minScore = 6;
2592
+ DIMENSIONS.forEach(d => {
2593
+ const score = dims[d.key]?.score || 0;
2594
+ if (score < minScore) { minScore = score; weakest = d; }
2595
+ });
2596
+
2597
+ const focusText = weakest
2598
+ ? `Your ${weakest.label.toLowerCase()} dimension is at ${minScore}/5. Focus on building this area to round out your AI skills.`
2599
+ : 'Keep up the great work across all dimensions!';
2600
+
2601
+ document.getElementById('nextFocusText').textContent = focusText;
2602
+ }
2603
+
2604
+ function renderMilestones() {
2605
+ const list = document.getElementById('milestoneList');
2606
+ const milestones = state.progress?.milestones || state.profile?.milestones || [];
2607
+
2608
+ if (!milestones.length) {
2609
+ list.innerHTML = '<li style="font-size:11px;color:var(--text-muted);padding:10px 0">Complete your first program to earn milestones.</li>';
2610
+ return;
2611
+ }
2612
+
2613
+ list.innerHTML = milestones.map(m => `
2614
+ <li class="milestone-item">
2615
+ <div class="milestone-dot"></div>
2616
+ <div class="milestone-content">
2617
+ <div class="milestone-text">${m.text || m.title || m.description}</div>
2618
+ <div class="milestone-date">${m.date || m.achievedAt || ''}</div>
2619
+ </div>
2620
+ </li>
2621
+ `).join('');
2622
+ }
2623
+
2624
+ /* ─── History Chart ─────────────────────── */
2625
+ async function renderHistoryChart() {
2626
+ try {
2627
+ const res = await fetch(`${API}/api/gym/dimensions/history`);
2628
+ const history = await res.json();
2629
+
2630
+ if (!Array.isArray(history) || history.length < 2) {
2631
+ document.getElementById('historyChart').innerHTML = '<div class="history-empty">Your progress chart will fill in as weeks go by. Check back after your second weekly snapshot.</div>';
2632
+ document.getElementById('historyLegend').innerHTML = '';
2633
+ return;
2634
+ }
2635
+
2636
+ const svg = document.getElementById('historySvg');
2637
+ const colors = {
2638
+ application: '#22d3ee',
2639
+ communication: '#f97316',
2640
+ knowledge: '#a78bfa',
2641
+ orchestration: '#ef4444',
2642
+ craft: '#4ade80'
2643
+ };
2644
+
2645
+ const W = 600, H = 160, padL = 30, padR = 10, padT = 10, padB = 24;
2646
+ const plotW = W - padL - padR;
2647
+ const plotH = H - padT - padB;
2648
+
2649
+ let html = '';
2650
+
2651
+ // Y-axis gridlines (0-5)
2652
+ for (let i = 0; i <= 5; i++) {
2653
+ const y = padT + plotH - (i / 5) * plotH;
2654
+ html += `<line class="history-gridline" x1="${padL}" y1="${y}" x2="${W - padR}" y2="${y}" />`;
2655
+ html += `<text class="history-y-label" x="${padL - 4}" y="${y + 3}">${i}</text>`;
2656
+ }
2657
+
2658
+ // X-axis dates
2659
+ const step = Math.max(1, Math.floor(history.length / 6));
2660
+ history.forEach((snap, i) => {
2661
+ if (i % step === 0 || i === history.length - 1) {
2662
+ const x = padL + (i / (history.length - 1)) * plotW;
2663
+ const label = snap.date ? snap.date.slice(5) : ''; // MM-DD
2664
+ html += `<text class="history-x-label" x="${x}" y="${H - 2}">${label}</text>`;
2665
+ }
2666
+ });
2667
+
2668
+ // Lines for each dimension
2669
+ const dims = ['application', 'communication', 'knowledge', 'orchestration', 'craft'];
2670
+ for (const dim of dims) {
2671
+ const points = history.map((snap, i) => {
2672
+ const x = padL + (i / Math.max(history.length - 1, 1)) * plotW;
2673
+ const score = snap.dimensions?.[dim] || 0;
2674
+ const y = padT + plotH - (score / 5) * plotH;
2675
+ return { x, y };
2676
+ });
2677
+
2678
+ const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
2679
+ html += `<path class="history-line" d="${pathD}" stroke="${colors[dim]}" />`;
2680
+
2681
+ // Dots on last point
2682
+ const last = points[points.length - 1];
2683
+ html += `<circle class="history-dot" cx="${last.x}" cy="${last.y}" fill="${colors[dim]}" />`;
2684
+ }
2685
+
2686
+ svg.innerHTML = html;
2687
+
2688
+ // Legend
2689
+ document.getElementById('historyLegend').innerHTML = dims.map(d =>
2690
+ `<div class="history-legend-item"><div class="history-legend-swatch" style="background:${colors[d]}"></div>${d.charAt(0).toUpperCase() + d.slice(1)}</div>`
2691
+ ).join('');
2692
+
2693
+ } catch (e) {
2694
+ console.warn('Failed to load history chart:', e);
2695
+ document.getElementById('historyChart').innerHTML = '<div class="history-empty">Unable to load progress history.</div>';
2696
+ }
2697
+ }
2698
+
2699
+ /* ─── Gym-Only Mode ─────────────────────── */
2700
+ async function checkGymOnlyMode() {
2701
+ try {
2702
+ const res = await fetch(`${API}/api/gym/config`);
2703
+ const config = await res.json();
2704
+ // Gym page: always show gym tabs, always hide platform tabs
2705
+ document.getElementById('gymTabs').style.display = 'flex';
2706
+ document.getElementById('gymTabsRight').style.display = 'flex';
2707
+ document.getElementById('platformTabs').style.display = 'none';
2708
+ } catch (e) {}
2709
+ }
2710
+
2711
+ /* ─── Config Overlay (iframe) ──────────────── */
2712
+ function openConfigOverlay() {
2713
+ const overlay = document.getElementById('configOverlay');
2714
+ const iframe = document.getElementById('configIframe');
2715
+ iframe.src = '/org?edit=gym';
2716
+ overlay.classList.add('show');
2717
+ }
2718
+ function closeConfigOverlay() {
2719
+ const overlay = document.getElementById('configOverlay');
2720
+ const iframe = document.getElementById('configIframe');
2721
+ overlay.classList.remove('show');
2722
+ setTimeout(() => { iframe.src = 'about:blank'; }, 300);
2723
+ }
2724
+ window.addEventListener('message', (e) => {
2725
+ if (e.data?.type === 'configModalClosed') closeConfigOverlay();
2726
+ });
2727
+
2728
+ /* ─── Start ──────────────────────────────── */
2729
+ init();
2730
+ </script>
2731
+
2732
+ <!-- Config Overlay -->
2733
+ <div class="config-overlay" id="configOverlay" onclick="closeConfigOverlay()">
2734
+ <div class="config-overlay-inner" onclick="event.stopPropagation()">
2735
+ <button class="config-overlay-close" onclick="closeConfigOverlay()">&#10005;</button>
2736
+ <iframe id="configIframe" src="about:blank"></iframe>
2737
+ </div>
2738
+ </div>
2739
+ <style>
2740
+ .config-overlay{
2741
+ position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;
2742
+ background:rgba(0,0,0,0.7);backdrop-filter:blur(4px);
2743
+ display:none;align-items:center;justify-content:center;
2744
+ opacity:0;transition:opacity .25s ease;
2745
+ }
2746
+ .config-overlay.show{display:flex;opacity:1}
2747
+ .config-overlay-inner{
2748
+ position:relative;width:90vw;max-width:680px;height:85vh;
2749
+ border-radius:16px;overflow:hidden;
2750
+ border:1px solid var(--border-glow);
2751
+ box-shadow:0 8px 40px rgba(0,0,0,0.5);
2752
+ }
2753
+ .config-overlay-inner iframe{
2754
+ width:100%;height:100%;border:none;background:var(--bg-deep);
2755
+ }
2756
+ .config-overlay-close{
2757
+ position:absolute;top:10px;right:14px;z-index:10;
2758
+ width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
2759
+ background:var(--bg-surface);color:var(--text-secondary);cursor:pointer;
2760
+ font-size:16px;display:flex;align-items:center;justify-content:center;
2761
+ transition:all .2s;
2762
+ }
2763
+ .config-overlay-close:hover{border-color:var(--border-glow);color:var(--text-primary)}
2764
+ </style>
2765
+ </body>
2766
+ </html>