@jungjaehoon/mama-os 0.1.1

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 (356) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +643 -0
  3. package/dist/agent/agent-loop.d.ts +98 -0
  4. package/dist/agent/agent-loop.d.ts.map +1 -0
  5. package/dist/agent/agent-loop.js +417 -0
  6. package/dist/agent/agent-loop.js.map +1 -0
  7. package/dist/agent/auto-recall.d.ts +48 -0
  8. package/dist/agent/auto-recall.d.ts.map +1 -0
  9. package/dist/agent/auto-recall.js +178 -0
  10. package/dist/agent/auto-recall.js.map +1 -0
  11. package/dist/agent/claude-cli-wrapper.d.ts +130 -0
  12. package/dist/agent/claude-cli-wrapper.d.ts.map +1 -0
  13. package/dist/agent/claude-cli-wrapper.js +227 -0
  14. package/dist/agent/claude-cli-wrapper.js.map +1 -0
  15. package/dist/agent/claude-client.d.ts +50 -0
  16. package/dist/agent/claude-client.d.ts.map +1 -0
  17. package/dist/agent/claude-client.js +214 -0
  18. package/dist/agent/claude-client.js.map +1 -0
  19. package/dist/agent/gateway-tool-executor.d.ts +75 -0
  20. package/dist/agent/gateway-tool-executor.d.ts.map +1 -0
  21. package/dist/agent/gateway-tool-executor.js +348 -0
  22. package/dist/agent/gateway-tool-executor.js.map +1 -0
  23. package/dist/agent/index.d.ts +13 -0
  24. package/dist/agent/index.d.ts.map +1 -0
  25. package/dist/agent/index.js +18 -0
  26. package/dist/agent/index.js.map +1 -0
  27. package/dist/agent/mcp-executor.d.ts +75 -0
  28. package/dist/agent/mcp-executor.d.ts.map +1 -0
  29. package/dist/agent/mcp-executor.js +307 -0
  30. package/dist/agent/mcp-executor.js.map +1 -0
  31. package/dist/agent/session-pool.d.ts +148 -0
  32. package/dist/agent/session-pool.d.ts.map +1 -0
  33. package/dist/agent/session-pool.js +272 -0
  34. package/dist/agent/session-pool.js.map +1 -0
  35. package/dist/agent/streaming-callback-manager.d.ts +85 -0
  36. package/dist/agent/streaming-callback-manager.d.ts.map +1 -0
  37. package/dist/agent/streaming-callback-manager.js +103 -0
  38. package/dist/agent/streaming-callback-manager.js.map +1 -0
  39. package/dist/agent/types.d.ts +437 -0
  40. package/dist/agent/types.d.ts.map +1 -0
  41. package/dist/agent/types.js +29 -0
  42. package/dist/agent/types.js.map +1 -0
  43. package/dist/api/cron-handler.d.ts +44 -0
  44. package/dist/api/cron-handler.d.ts.map +1 -0
  45. package/dist/api/cron-handler.js +195 -0
  46. package/dist/api/cron-handler.js.map +1 -0
  47. package/dist/api/error-handler.d.ts +22 -0
  48. package/dist/api/error-handler.d.ts.map +1 -0
  49. package/dist/api/error-handler.js +104 -0
  50. package/dist/api/error-handler.js.map +1 -0
  51. package/dist/api/heartbeat-handler.d.ts +49 -0
  52. package/dist/api/heartbeat-handler.d.ts.map +1 -0
  53. package/dist/api/heartbeat-handler.js +91 -0
  54. package/dist/api/heartbeat-handler.js.map +1 -0
  55. package/dist/api/index.d.ts +61 -0
  56. package/dist/api/index.d.ts.map +1 -0
  57. package/dist/api/index.js +145 -0
  58. package/dist/api/index.js.map +1 -0
  59. package/dist/api/types.d.ts +156 -0
  60. package/dist/api/types.d.ts.map +1 -0
  61. package/dist/api/types.js +62 -0
  62. package/dist/api/types.js.map +1 -0
  63. package/dist/auth/index.d.ts +7 -0
  64. package/dist/auth/index.d.ts.map +1 -0
  65. package/dist/auth/index.js +11 -0
  66. package/dist/auth/index.js.map +1 -0
  67. package/dist/auth/oauth-manager.d.ts +59 -0
  68. package/dist/auth/oauth-manager.d.ts.map +1 -0
  69. package/dist/auth/oauth-manager.js +237 -0
  70. package/dist/auth/oauth-manager.js.map +1 -0
  71. package/dist/auth/types.d.ts +92 -0
  72. package/dist/auth/types.d.ts.map +1 -0
  73. package/dist/auth/types.js +23 -0
  74. package/dist/auth/types.js.map +1 -0
  75. package/dist/cli/commands/init.d.ts +19 -0
  76. package/dist/cli/commands/init.d.ts.map +1 -0
  77. package/dist/cli/commands/init.js +155 -0
  78. package/dist/cli/commands/init.js.map +1 -0
  79. package/dist/cli/commands/run.d.ts +19 -0
  80. package/dist/cli/commands/run.d.ts.map +1 -0
  81. package/dist/cli/commands/run.js +89 -0
  82. package/dist/cli/commands/run.js.map +1 -0
  83. package/dist/cli/commands/setup.d.ts +19 -0
  84. package/dist/cli/commands/setup.d.ts.map +1 -0
  85. package/dist/cli/commands/setup.js +134 -0
  86. package/dist/cli/commands/setup.js.map +1 -0
  87. package/dist/cli/commands/start.d.ts +24 -0
  88. package/dist/cli/commands/start.d.ts.map +1 -0
  89. package/dist/cli/commands/start.js +1073 -0
  90. package/dist/cli/commands/start.js.map +1 -0
  91. package/dist/cli/commands/status.d.ts +10 -0
  92. package/dist/cli/commands/status.d.ts.map +1 -0
  93. package/dist/cli/commands/status.js +85 -0
  94. package/dist/cli/commands/status.js.map +1 -0
  95. package/dist/cli/commands/stop.d.ts +10 -0
  96. package/dist/cli/commands/stop.d.ts.map +1 -0
  97. package/dist/cli/commands/stop.js +65 -0
  98. package/dist/cli/commands/stop.js.map +1 -0
  99. package/dist/cli/config/config-manager.d.ts +51 -0
  100. package/dist/cli/config/config-manager.d.ts.map +1 -0
  101. package/dist/cli/config/config-manager.js +216 -0
  102. package/dist/cli/config/config-manager.js.map +1 -0
  103. package/dist/cli/config/types.d.ts +172 -0
  104. package/dist/cli/config/types.d.ts.map +1 -0
  105. package/dist/cli/config/types.js +48 -0
  106. package/dist/cli/config/types.js.map +1 -0
  107. package/dist/cli/index.d.ts +8 -0
  108. package/dist/cli/index.d.ts.map +1 -0
  109. package/dist/cli/index.js +92 -0
  110. package/dist/cli/index.js.map +1 -0
  111. package/dist/cli/utils/pid-manager.d.ts +66 -0
  112. package/dist/cli/utils/pid-manager.d.ts.map +1 -0
  113. package/dist/cli/utils/pid-manager.js +167 -0
  114. package/dist/cli/utils/pid-manager.js.map +1 -0
  115. package/dist/concurrency/index.d.ts +13 -0
  116. package/dist/concurrency/index.d.ts.map +1 -0
  117. package/dist/concurrency/index.js +22 -0
  118. package/dist/concurrency/index.js.map +1 -0
  119. package/dist/concurrency/lane-manager.d.ts +113 -0
  120. package/dist/concurrency/lane-manager.d.ts.map +1 -0
  121. package/dist/concurrency/lane-manager.js +245 -0
  122. package/dist/concurrency/lane-manager.js.map +1 -0
  123. package/dist/concurrency/session-key.d.ts +41 -0
  124. package/dist/concurrency/session-key.d.ts.map +1 -0
  125. package/dist/concurrency/session-key.js +61 -0
  126. package/dist/concurrency/session-key.js.map +1 -0
  127. package/dist/concurrency/types.d.ts +69 -0
  128. package/dist/concurrency/types.d.ts.map +1 -0
  129. package/dist/concurrency/types.js +16 -0
  130. package/dist/concurrency/types.js.map +1 -0
  131. package/dist/gateways/channel-history.d.ts +102 -0
  132. package/dist/gateways/channel-history.d.ts.map +1 -0
  133. package/dist/gateways/channel-history.js +181 -0
  134. package/dist/gateways/channel-history.js.map +1 -0
  135. package/dist/gateways/context-injector.d.ts +74 -0
  136. package/dist/gateways/context-injector.d.ts.map +1 -0
  137. package/dist/gateways/context-injector.js +121 -0
  138. package/dist/gateways/context-injector.js.map +1 -0
  139. package/dist/gateways/discord.d.ts +122 -0
  140. package/dist/gateways/discord.d.ts.map +1 -0
  141. package/dist/gateways/discord.js +602 -0
  142. package/dist/gateways/discord.js.map +1 -0
  143. package/dist/gateways/index.d.ts +30 -0
  144. package/dist/gateways/index.d.ts.map +1 -0
  145. package/dist/gateways/index.js +49 -0
  146. package/dist/gateways/index.js.map +1 -0
  147. package/dist/gateways/message-router.d.ts +116 -0
  148. package/dist/gateways/message-router.d.ts.map +1 -0
  149. package/dist/gateways/message-router.js +315 -0
  150. package/dist/gateways/message-router.js.map +1 -0
  151. package/dist/gateways/message-splitter.d.ts +54 -0
  152. package/dist/gateways/message-splitter.d.ts.map +1 -0
  153. package/dist/gateways/message-splitter.js +146 -0
  154. package/dist/gateways/message-splitter.js.map +1 -0
  155. package/dist/gateways/plugin-loader.d.ts +76 -0
  156. package/dist/gateways/plugin-loader.d.ts.map +1 -0
  157. package/dist/gateways/plugin-loader.js +221 -0
  158. package/dist/gateways/plugin-loader.js.map +1 -0
  159. package/dist/gateways/session-store.d.ts +77 -0
  160. package/dist/gateways/session-store.d.ts.map +1 -0
  161. package/dist/gateways/session-store.js +233 -0
  162. package/dist/gateways/session-store.js.map +1 -0
  163. package/dist/gateways/slack.d.ts +90 -0
  164. package/dist/gateways/slack.d.ts.map +1 -0
  165. package/dist/gateways/slack.js +281 -0
  166. package/dist/gateways/slack.js.map +1 -0
  167. package/dist/gateways/telegram.d.ts +79 -0
  168. package/dist/gateways/telegram.d.ts.map +1 -0
  169. package/dist/gateways/telegram.js +207 -0
  170. package/dist/gateways/telegram.js.map +1 -0
  171. package/dist/gateways/types.d.ts +340 -0
  172. package/dist/gateways/types.d.ts.map +1 -0
  173. package/dist/gateways/types.js +6 -0
  174. package/dist/gateways/types.js.map +1 -0
  175. package/dist/index.d.ts +7 -0
  176. package/dist/index.d.ts.map +1 -0
  177. package/dist/index.js +26 -0
  178. package/dist/index.js.map +1 -0
  179. package/dist/memory/memory-logger.d.ts +47 -0
  180. package/dist/memory/memory-logger.d.ts.map +1 -0
  181. package/dist/memory/memory-logger.js +126 -0
  182. package/dist/memory/memory-logger.js.map +1 -0
  183. package/dist/onboarding/all-tools.d.ts +18 -0
  184. package/dist/onboarding/all-tools.d.ts.map +1 -0
  185. package/dist/onboarding/all-tools.js +149 -0
  186. package/dist/onboarding/all-tools.js.map +1 -0
  187. package/dist/onboarding/autonomous-discovery-tools.d.ts +13 -0
  188. package/dist/onboarding/autonomous-discovery-tools.d.ts.map +1 -0
  189. package/dist/onboarding/autonomous-discovery-tools.js +268 -0
  190. package/dist/onboarding/autonomous-discovery-tools.js.map +1 -0
  191. package/dist/onboarding/bootstrap-template.d.ts +5 -0
  192. package/dist/onboarding/bootstrap-template.d.ts.map +1 -0
  193. package/dist/onboarding/bootstrap-template.js +142 -0
  194. package/dist/onboarding/bootstrap-template.js.map +1 -0
  195. package/dist/onboarding/complete-autonomous-prompt.d.ts +13 -0
  196. package/dist/onboarding/complete-autonomous-prompt.d.ts.map +1 -0
  197. package/dist/onboarding/complete-autonomous-prompt.js +1220 -0
  198. package/dist/onboarding/complete-autonomous-prompt.js.map +1 -0
  199. package/dist/onboarding/onboarding-state.d.ts +70 -0
  200. package/dist/onboarding/onboarding-state.d.ts.map +1 -0
  201. package/dist/onboarding/onboarding-state.js +184 -0
  202. package/dist/onboarding/onboarding-state.js.map +1 -0
  203. package/dist/onboarding/personality-quiz.d.ts +35 -0
  204. package/dist/onboarding/personality-quiz.d.ts.map +1 -0
  205. package/dist/onboarding/personality-quiz.js +219 -0
  206. package/dist/onboarding/personality-quiz.js.map +1 -0
  207. package/dist/onboarding/phase-5-summary.d.ts +22 -0
  208. package/dist/onboarding/phase-5-summary.d.ts.map +1 -0
  209. package/dist/onboarding/phase-5-summary.js +151 -0
  210. package/dist/onboarding/phase-5-summary.js.map +1 -0
  211. package/dist/onboarding/phase-6-security.d.ts +33 -0
  212. package/dist/onboarding/phase-6-security.d.ts.map +1 -0
  213. package/dist/onboarding/phase-6-security.js +473 -0
  214. package/dist/onboarding/phase-6-security.js.map +1 -0
  215. package/dist/onboarding/phase-7-integrations.d.ts +66 -0
  216. package/dist/onboarding/phase-7-integrations.d.ts.map +1 -0
  217. package/dist/onboarding/phase-7-integrations.js +619 -0
  218. package/dist/onboarding/phase-7-integrations.js.map +1 -0
  219. package/dist/onboarding/phase-8-demo.d.ts +43 -0
  220. package/dist/onboarding/phase-8-demo.d.ts.map +1 -0
  221. package/dist/onboarding/phase-8-demo.js +346 -0
  222. package/dist/onboarding/phase-8-demo.js.map +1 -0
  223. package/dist/onboarding/phase-9-finalization.d.ts +22 -0
  224. package/dist/onboarding/phase-9-finalization.d.ts.map +1 -0
  225. package/dist/onboarding/phase-9-finalization.js +375 -0
  226. package/dist/onboarding/phase-9-finalization.js.map +1 -0
  227. package/dist/onboarding/ritual-prompt.d.ts +2 -0
  228. package/dist/onboarding/ritual-prompt.d.ts.map +1 -0
  229. package/dist/onboarding/ritual-prompt.js +285 -0
  230. package/dist/onboarding/ritual-prompt.js.map +1 -0
  231. package/dist/onboarding/ritual-tools.d.ts +13 -0
  232. package/dist/onboarding/ritual-tools.d.ts.map +1 -0
  233. package/dist/onboarding/ritual-tools.js +93 -0
  234. package/dist/onboarding/ritual-tools.js.map +1 -0
  235. package/dist/runners/cli-runner.d.ts +59 -0
  236. package/dist/runners/cli-runner.d.ts.map +1 -0
  237. package/dist/runners/cli-runner.js +190 -0
  238. package/dist/runners/cli-runner.js.map +1 -0
  239. package/dist/runners/index.d.ts +11 -0
  240. package/dist/runners/index.d.ts.map +1 -0
  241. package/dist/runners/index.js +15 -0
  242. package/dist/runners/index.js.map +1 -0
  243. package/dist/runners/types.d.ts +81 -0
  244. package/dist/runners/types.d.ts.map +1 -0
  245. package/dist/runners/types.js +31 -0
  246. package/dist/runners/types.js.map +1 -0
  247. package/dist/scheduler/cron-scheduler.d.ts +115 -0
  248. package/dist/scheduler/cron-scheduler.d.ts.map +1 -0
  249. package/dist/scheduler/cron-scheduler.js +320 -0
  250. package/dist/scheduler/cron-scheduler.js.map +1 -0
  251. package/dist/scheduler/heartbeat.d.ts +53 -0
  252. package/dist/scheduler/heartbeat.d.ts.map +1 -0
  253. package/dist/scheduler/heartbeat.js +160 -0
  254. package/dist/scheduler/heartbeat.js.map +1 -0
  255. package/dist/scheduler/index.d.ts +22 -0
  256. package/dist/scheduler/index.d.ts.map +1 -0
  257. package/dist/scheduler/index.js +31 -0
  258. package/dist/scheduler/index.js.map +1 -0
  259. package/dist/scheduler/job-lock.d.ts +85 -0
  260. package/dist/scheduler/job-lock.d.ts.map +1 -0
  261. package/dist/scheduler/job-lock.js +137 -0
  262. package/dist/scheduler/job-lock.js.map +1 -0
  263. package/dist/scheduler/recovery.d.ts +78 -0
  264. package/dist/scheduler/recovery.d.ts.map +1 -0
  265. package/dist/scheduler/recovery.js +124 -0
  266. package/dist/scheduler/recovery.js.map +1 -0
  267. package/dist/scheduler/schedule-store.d.ts +112 -0
  268. package/dist/scheduler/schedule-store.d.ts.map +1 -0
  269. package/dist/scheduler/schedule-store.js +259 -0
  270. package/dist/scheduler/schedule-store.js.map +1 -0
  271. package/dist/scheduler/token-keep-alive.d.ts +49 -0
  272. package/dist/scheduler/token-keep-alive.d.ts.map +1 -0
  273. package/dist/scheduler/token-keep-alive.js +102 -0
  274. package/dist/scheduler/token-keep-alive.js.map +1 -0
  275. package/dist/scheduler/types.d.ts +96 -0
  276. package/dist/scheduler/types.d.ts.map +1 -0
  277. package/dist/scheduler/types.js +21 -0
  278. package/dist/scheduler/types.js.map +1 -0
  279. package/dist/setup/setup-prompt.d.ts +2 -0
  280. package/dist/setup/setup-prompt.d.ts.map +1 -0
  281. package/dist/setup/setup-prompt.js +138 -0
  282. package/dist/setup/setup-prompt.js.map +1 -0
  283. package/dist/setup/setup-server.d.ts +8 -0
  284. package/dist/setup/setup-server.d.ts.map +1 -0
  285. package/dist/setup/setup-server.js +71 -0
  286. package/dist/setup/setup-server.js.map +1 -0
  287. package/dist/setup/setup-tools.d.ts +13 -0
  288. package/dist/setup/setup-tools.d.ts.map +1 -0
  289. package/dist/setup/setup-tools.js +103 -0
  290. package/dist/setup/setup-tools.js.map +1 -0
  291. package/dist/setup/setup-websocket.d.ts +6 -0
  292. package/dist/setup/setup-websocket.d.ts.map +1 -0
  293. package/dist/setup/setup-websocket.js +312 -0
  294. package/dist/setup/setup-websocket.js.map +1 -0
  295. package/dist/skills/index.d.ts +10 -0
  296. package/dist/skills/index.d.ts.map +1 -0
  297. package/dist/skills/index.js +26 -0
  298. package/dist/skills/index.js.map +1 -0
  299. package/dist/skills/skill-executor.d.ts +48 -0
  300. package/dist/skills/skill-executor.d.ts.map +1 -0
  301. package/dist/skills/skill-executor.js +483 -0
  302. package/dist/skills/skill-executor.js.map +1 -0
  303. package/dist/skills/skill-loader.d.ts +40 -0
  304. package/dist/skills/skill-loader.d.ts.map +1 -0
  305. package/dist/skills/skill-loader.js +225 -0
  306. package/dist/skills/skill-loader.js.map +1 -0
  307. package/dist/skills/skill-matcher.d.ts +33 -0
  308. package/dist/skills/skill-matcher.d.ts.map +1 -0
  309. package/dist/skills/skill-matcher.js +190 -0
  310. package/dist/skills/skill-matcher.js.map +1 -0
  311. package/dist/skills/types.d.ts +123 -0
  312. package/dist/skills/types.d.ts.map +1 -0
  313. package/dist/skills/types.js +12 -0
  314. package/dist/skills/types.js.map +1 -0
  315. package/dist/tools/browser-tool.d.ts +149 -0
  316. package/dist/tools/browser-tool.d.ts.map +1 -0
  317. package/dist/tools/browser-tool.js +257 -0
  318. package/dist/tools/browser-tool.js.map +1 -0
  319. package/package.json +84 -0
  320. package/public/favicon.ico +0 -0
  321. package/public/setup.html +1026 -0
  322. package/public/viewer/icons/icon-192.png +0 -0
  323. package/public/viewer/icons/icon-512.png +0 -0
  324. package/public/viewer/js/modules/chat.js +1587 -0
  325. package/public/viewer/js/modules/dashboard.js +275 -0
  326. package/public/viewer/js/modules/graph.js +997 -0
  327. package/public/viewer/js/modules/memory.js +353 -0
  328. package/public/viewer/js/modules/settings.js +255 -0
  329. package/public/viewer/js/utils/api.js +169 -0
  330. package/public/viewer/js/utils/dom.js +92 -0
  331. package/public/viewer/js/utils/format.js +192 -0
  332. package/public/viewer/manifest.json +26 -0
  333. package/public/viewer/sw.js +131 -0
  334. package/public/viewer/viewer.css +500 -0
  335. package/public/viewer/viewer.html +1535 -0
  336. package/scripts/postinstall.js +118 -0
  337. package/templates/skills/document-analyze.md +63 -0
  338. package/templates/skills/heartbeat-report.md +75 -0
  339. package/templates/skills/image-translate.md +67 -0
  340. package/templates/workspace/skill-forge/DESIGN.md +115 -0
  341. package/templates/workspace/skill-forge/agents/architect.ts +295 -0
  342. package/templates/workspace/skill-forge/agents/developer.ts +364 -0
  343. package/templates/workspace/skill-forge/agents/qa.ts +313 -0
  344. package/templates/workspace/skill-forge/claude-api.ts +353 -0
  345. package/templates/workspace/skill-forge/discord-ui.ts +580 -0
  346. package/templates/workspace/skill-forge/error-handler.ts +354 -0
  347. package/templates/workspace/skill-forge/mama-integration.ts +357 -0
  348. package/templates/workspace/skill-forge/orchestrator.ts +495 -0
  349. package/templates/workspace/skill-forge/output/generated-skills/skills/hello-world/README.md +24 -0
  350. package/templates/workspace/skill-forge/output/generated-skills/skills/hello-world/index.ts +79 -0
  351. package/templates/workspace/skill-forge/output/generated-skills/skills/hello-world/types.ts +17 -0
  352. package/templates/workspace/skill-forge/package.json +21 -0
  353. package/templates/workspace/skill-forge/state/session.json +132 -0
  354. package/templates/workspace/skill-forge/test-e2e.ts +139 -0
  355. package/templates/workspace/skill-forge/tsconfig.json +20 -0
  356. package/templates/workspace/skill-forge/types.ts +159 -0
@@ -0,0 +1,1587 @@
1
+ /**
2
+ * Chat Module - Mobile Chat with Voice Input
3
+ * @module modules/chat
4
+ * @version 1.0.0
5
+ *
6
+ * Handles Chat tab functionality including:
7
+ * - WebSocket chat with Claude Code CLI
8
+ * - Voice input (Web Speech API)
9
+ * - Conversation history management
10
+ * - Real-time streaming responses
11
+ */
12
+
13
+ /* eslint-env browser */
14
+
15
+ import { escapeHtml, showToast, scrollToBottom, autoResizeTextarea } from '../utils/dom.js';
16
+ import { formatMessageTime, formatAssistantMessage } from '../utils/format.js';
17
+ import { API } from '../utils/api.js';
18
+
19
+ /**
20
+ * Chat Module Class
21
+ */
22
+ export class ChatModule {
23
+ constructor(memoryModule = null) {
24
+ // External dependencies
25
+ this.memoryModule = memoryModule;
26
+
27
+ // WebSocket state
28
+ this.ws = null;
29
+ this.sessionId = null;
30
+ this.reconnectAttempts = 0;
31
+ this.maxReconnectDelay = 30000; // 30 seconds
32
+
33
+ // Voice input state (STT)
34
+ this.speechRecognition = null;
35
+ this.isRecording = false;
36
+ this.silenceTimeout = null;
37
+ this.silenceDelay = 2500; // 2.5 seconds (increased for continuous mode)
38
+ this.accumulatedTranscript = ''; // Track accumulated final transcripts
39
+
40
+ // Voice output state (TTS)
41
+ this.speechSynthesis = window.speechSynthesis;
42
+ this.isSpeaking = false;
43
+ this.ttsEnabled = false; // Auto-play toggle
44
+ this.handsFreeMode = false; // Auto-listen after TTS
45
+ this.ttsVoice = null;
46
+ this.ttsRate = 1.8; // Speech rate (0.5 - 2.0), optimized for Korean
47
+ this.ttsPitch = 1.0; // Speech pitch (0.0 - 2.0)
48
+
49
+ // Streaming state
50
+ this.currentStreamEl = null;
51
+ this.currentStreamText = '';
52
+ this.streamBuffer = '';
53
+ this.rafPending = false;
54
+
55
+ // History state
56
+ this.history = [];
57
+ this.historyPrefix = 'mama_chat_history_';
58
+ this.maxHistoryMessages = 50;
59
+ this.historyExpiryMs = 24 * 60 * 60 * 1000; // 24 hours
60
+
61
+ // Idle auto-checkpoint state
62
+ this.idleTimer = null;
63
+ this.IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
64
+ this.checkpointCooldown = false;
65
+ this.COOLDOWN_MS = 60 * 1000; // 1 minute between checkpoints
66
+
67
+ // Initialize
68
+ this.initChatInput();
69
+ this.initLongPressCopy();
70
+ this.initSpeechRecognition();
71
+ this.initSpeechSynthesis();
72
+ }
73
+
74
+ // =============================================
75
+ // Idle Auto-Checkpoint
76
+ // =============================================
77
+
78
+ resetIdleTimer() {
79
+ if (this.idleTimer) {
80
+ clearTimeout(this.idleTimer);
81
+ }
82
+
83
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
84
+ this.idleTimer = setTimeout(() => {
85
+ this.autoCheckpoint();
86
+ }, this.IDLE_TIMEOUT);
87
+ }
88
+ }
89
+
90
+ async autoCheckpoint() {
91
+ // DISABLED: Auto-checkpoint was saving raw conversation history to MAMA memory.
92
+ // Checkpoints should only be saved manually via /checkpoint command with proper summaries.
93
+ // The viewer chat uses localStorage for session persistence instead.
94
+ console.log('[ChatModule] Auto-checkpoint disabled (use /checkpoint for manual saves)');
95
+ return;
96
+ }
97
+
98
+ // =============================================
99
+ // Session Management
100
+ // =============================================
101
+
102
+ /**
103
+ * Initialize chat session
104
+ */
105
+ async initSession() {
106
+ // Check for resumable session first
107
+ await this.checkForResumableSession();
108
+
109
+ // Try to get last active server session first
110
+ const lastActiveSession = await API.getLastActiveSession();
111
+ if (lastActiveSession && lastActiveSession.id && lastActiveSession.isAlive) {
112
+ console.log('[Chat] Resuming last active session:', lastActiveSession.id);
113
+ this.addSystemMessage('Resuming previous session...');
114
+ localStorage.setItem('mama_chat_session_id', lastActiveSession.id);
115
+ this.initWebSocket(lastActiveSession.id);
116
+ return;
117
+ }
118
+
119
+ const savedSessionId = localStorage.getItem('mama_chat_session_id');
120
+
121
+ if (savedSessionId) {
122
+ console.log('[Chat] Trying saved session:', savedSessionId);
123
+ this.addSystemMessage('Connecting to session...');
124
+ this.initWebSocket(savedSessionId);
125
+ } else {
126
+ try {
127
+ this.addSystemMessage('Creating new session...');
128
+ const data = await API.createSession('.');
129
+ const sessionId = data.sessionId;
130
+
131
+ console.log('[Chat] Created new session:', sessionId);
132
+ localStorage.setItem('mama_chat_session_id', sessionId);
133
+
134
+ this.initWebSocket(sessionId);
135
+ } catch (error) {
136
+ console.error('[Chat] Failed to create session:', error);
137
+ this.addSystemMessage(`Failed to create session: ${error.message}`, 'error');
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Connect to session (public method)
144
+ */
145
+ connectToSession(sessionId) {
146
+ this.initWebSocket(sessionId);
147
+ }
148
+
149
+ /**
150
+ * Disconnect from session (public method)
151
+ */
152
+ disconnect() {
153
+ if (this.ws) {
154
+ this.sessionId = null; // Prevent auto-reconnect
155
+ this.ws.close();
156
+ this.ws = null;
157
+ }
158
+ this.updateStatus('disconnected');
159
+ this.enableInput(false);
160
+ }
161
+
162
+ // =============================================
163
+ // WebSocket Management
164
+ // =============================================
165
+
166
+ /**
167
+ * Initialize WebSocket connection
168
+ */
169
+ initWebSocket(sessionId) {
170
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
171
+ console.log('[Chat] Already connected');
172
+ return;
173
+ }
174
+
175
+ this.sessionId = sessionId;
176
+ // Don't restore from localStorage - server will send authoritative history
177
+ // this.restoreHistory(sessionId);
178
+
179
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
180
+ const wsUrl = `${protocol}//${window.location.host}/ws?sessionId=${sessionId}`;
181
+
182
+ console.log('[Chat] Connecting to:', wsUrl);
183
+ this.ws = new WebSocket(wsUrl);
184
+
185
+ this.ws.onopen = () => {
186
+ console.log('[Chat] Connected');
187
+ this.reconnectAttempts = 0;
188
+ this.updateStatus('connected');
189
+ this.enableInput(true);
190
+
191
+ this.ws.send(
192
+ JSON.stringify({
193
+ type: 'attach',
194
+ sessionId: sessionId,
195
+ osAgentMode: true, // Enable OS Agent capabilities (Viewer-only)
196
+ language: navigator.language || 'en', // Browser language for greeting
197
+ })
198
+ );
199
+ };
200
+
201
+ this.ws.onmessage = (event) => {
202
+ try {
203
+ const data = JSON.parse(event.data);
204
+ this.handleMessage(data);
205
+ } catch (e) {
206
+ console.error('[Chat] Parse error:', e);
207
+ }
208
+ };
209
+
210
+ this.ws.onclose = (event) => {
211
+ console.log('[Chat] Disconnected:', event.code, event.reason);
212
+ this.updateStatus('disconnected');
213
+ this.enableInput(false);
214
+
215
+ if (this.sessionId) {
216
+ this.scheduleReconnect();
217
+ }
218
+ };
219
+
220
+ this.ws.onerror = (error) => {
221
+ console.error('[Chat] WebSocket error:', error);
222
+ this.updateStatus('disconnected');
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Handle incoming WebSocket message
228
+ */
229
+ handleMessage(data) {
230
+ switch (data.type) {
231
+ case 'attached':
232
+ console.log('[Chat] Attached to session:', data.sessionId);
233
+ this.addSystemMessage('Connected to session');
234
+ break;
235
+
236
+ case 'history':
237
+ // Display conversation history from server
238
+ if (data.messages && data.messages.length > 0) {
239
+ console.log('[Chat] Received history:', data.messages.length, 'messages');
240
+ this.displayHistory(data.messages);
241
+ }
242
+ break;
243
+
244
+ case 'output':
245
+ case 'stream':
246
+ if (data.content) {
247
+ this.enableSend(true);
248
+ this.appendStreamChunk(data.content);
249
+ }
250
+ break;
251
+
252
+ case 'stream_end':
253
+ this.finalizeStreamMessage();
254
+ break;
255
+
256
+ case 'error':
257
+ if (data.error === 'session_not_found') {
258
+ console.log('[Chat] Session not found, creating new one...');
259
+ localStorage.removeItem('mama_chat_session_id');
260
+ this.addSystemMessage('Session expired. Creating new session...');
261
+
262
+ if (this.ws) {
263
+ this.ws.close();
264
+ this.ws = null;
265
+ }
266
+
267
+ setTimeout(() => this.initSession(), 500);
268
+ } else {
269
+ this.addSystemMessage(`Error: ${data.message || data.error}`, 'error');
270
+ this.enableSend(true);
271
+ }
272
+ break;
273
+
274
+ case 'tool_use':
275
+ this.addToolCard(data.tool, data.toolId, data.input);
276
+ break;
277
+
278
+ case 'tool_complete':
279
+ this.completeToolCard(data.index);
280
+ break;
281
+
282
+ case 'pong':
283
+ break;
284
+
285
+ case 'connected':
286
+ console.log('[Chat] WebSocket connected:', data.clientId);
287
+ break;
288
+
289
+ default:
290
+ console.log('[Chat] Unknown message type:', data.type);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Schedule reconnection with exponential backoff
296
+ */
297
+ scheduleReconnect() {
298
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
299
+ this.reconnectAttempts++;
300
+
301
+ console.log(`[Chat] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
302
+ this.addSystemMessage(
303
+ `Connection lost. Reconnecting in ${Math.round(delay / 1000)}s...`,
304
+ 'warning'
305
+ );
306
+
307
+ setTimeout(() => {
308
+ if (this.sessionId) {
309
+ this.initWebSocket(this.sessionId);
310
+ }
311
+ }, delay);
312
+ }
313
+
314
+ // =============================================
315
+ // Message Handling
316
+ // =============================================
317
+
318
+ /**
319
+ * Send chat message
320
+ */
321
+ send() {
322
+ const input = document.getElementById('chat-input');
323
+ const message = input.value.trim();
324
+
325
+ if (!message) {
326
+ return;
327
+ }
328
+
329
+ // Handle slash commands
330
+ if (message.startsWith('/')) {
331
+ this.handleCommand(message);
332
+ input.value = '';
333
+ autoResizeTextarea(input);
334
+ return;
335
+ }
336
+
337
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
338
+ this.addSystemMessage('Not connected. Please connect to a session first.', 'error');
339
+ return;
340
+ }
341
+
342
+ this.addUserMessage(message);
343
+ this.enableSend(false);
344
+
345
+ this.ws.send(
346
+ JSON.stringify({
347
+ type: 'send',
348
+ sessionId: this.sessionId,
349
+ content: message,
350
+ })
351
+ );
352
+
353
+ // Search for related MAMA decisions
354
+ if (this.memoryModule) {
355
+ this.memoryModule.showRelatedForMessage(message);
356
+ }
357
+
358
+ input.value = '';
359
+ autoResizeTextarea(input);
360
+
361
+ console.log('[Chat] Sent:', message);
362
+ this.resetIdleTimer();
363
+ }
364
+
365
+ /**
366
+ * Send quiz choice (A, B, C, D)
367
+ * Called from quiz-choice-btn onclick
368
+ */
369
+ sendQuizChoice(choice) {
370
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
371
+ this.addSystemMessage('Not connected.', 'error');
372
+ return;
373
+ }
374
+
375
+ // Display choice as user message
376
+ this.addUserMessage(choice);
377
+ this.enableSend(false);
378
+
379
+ // Send to server
380
+ this.ws.send(
381
+ JSON.stringify({
382
+ type: 'send',
383
+ sessionId: this.sessionId,
384
+ content: choice,
385
+ })
386
+ );
387
+
388
+ console.log('[Chat] Quiz choice sent:', choice);
389
+ this.resetIdleTimer();
390
+ }
391
+
392
+ /**
393
+ * Handle slash commands
394
+ */
395
+ handleCommand(message) {
396
+ const parts = message.slice(1).split(' ');
397
+ const command = parts[0].toLowerCase();
398
+ const args = parts.slice(1).join(' ');
399
+
400
+ console.log('[Chat] Command:', command, 'Args:', args);
401
+
402
+ switch (command) {
403
+ case 'save':
404
+ this.commandSave(args);
405
+ break;
406
+ case 'search':
407
+ this.commandSearch(args);
408
+ break;
409
+ case 'checkpoint':
410
+ this.commandCheckpoint();
411
+ break;
412
+ case 'resume':
413
+ this.commandResume();
414
+ break;
415
+ case 'help':
416
+ this.commandHelp();
417
+ break;
418
+ default:
419
+ this.addSystemMessage(
420
+ `Unknown command: /${command}. Type /help for available commands.`,
421
+ 'error'
422
+ );
423
+ }
424
+ }
425
+
426
+ /**
427
+ * /save <text> - Open Memory form with text
428
+ */
429
+ commandSave(text) {
430
+ if (!this.memoryModule) {
431
+ this.addSystemMessage('Memory module not available', 'error');
432
+ return;
433
+ }
434
+
435
+ if (!text) {
436
+ this.addSystemMessage('Usage: /save <decision text>', 'error');
437
+ return;
438
+ }
439
+
440
+ // Switch to Memory tab and open form with text
441
+ window.switchTab('memory');
442
+ this.memoryModule.showSaveFormWithText(text);
443
+ this.addSystemMessage(`💾 Opening save form with: "${text.substring(0, 50)}..."`);
444
+ }
445
+
446
+ /**
447
+ * /search <query> - Search in Memory tab
448
+ */
449
+ commandSearch(query) {
450
+ if (!this.memoryModule) {
451
+ this.addSystemMessage('Memory module not available', 'error');
452
+ return;
453
+ }
454
+
455
+ if (!query) {
456
+ this.addSystemMessage('Usage: /search <query>', 'error');
457
+ return;
458
+ }
459
+
460
+ // Switch to Memory tab and execute search
461
+ window.switchTab('memory');
462
+ this.memoryModule.searchWithQuery(query);
463
+ this.addSystemMessage(`🔍 Searching for: "${query}"`);
464
+ }
465
+
466
+ /**
467
+ * /checkpoint - Save current session as checkpoint
468
+ */
469
+ async commandCheckpoint() {
470
+ try {
471
+ const summary = this.generateCheckpointSummary();
472
+ await this.saveCheckpoint(summary);
473
+ this.addSystemMessage('✅ Checkpoint saved successfully');
474
+ } catch (error) {
475
+ console.error('[Chat] Checkpoint save failed:', error);
476
+ this.addSystemMessage(`Failed to save checkpoint: ${error.message}`, 'error');
477
+ }
478
+ }
479
+
480
+ /**
481
+ * /resume - Load last checkpoint
482
+ */
483
+ async commandResume() {
484
+ try {
485
+ const checkpoint = await this.loadCheckpoint();
486
+ if (checkpoint) {
487
+ this.addSystemMessage(
488
+ `📖 Last checkpoint (${new Date(checkpoint.timestamp).toLocaleString()}):`
489
+ );
490
+ this.addSystemMessage(checkpoint.summary);
491
+ } else {
492
+ this.addSystemMessage('No checkpoint found', 'error');
493
+ }
494
+ } catch (error) {
495
+ console.error('[Chat] Checkpoint load failed:', error);
496
+ this.addSystemMessage(`Failed to load checkpoint: ${error.message}`, 'error');
497
+ }
498
+ }
499
+
500
+ /**
501
+ * /help - Show available commands
502
+ */
503
+ commandHelp() {
504
+ const helpText = `
505
+ **Available Commands:**
506
+
507
+ **/save <text>** - Save a decision to Memory
508
+ **/search <query>** - Search decisions in Memory
509
+ **/checkpoint** - Save current session
510
+ **/resume** - Load last checkpoint
511
+ **/help** - Show this help message
512
+
513
+ **Keyboard Shortcuts:**
514
+ - **Enter** - Send message
515
+ - **Shift+Enter** - New line
516
+ - **Long press message** - Copy to clipboard
517
+ `.trim();
518
+
519
+ this.addSystemMessage(helpText);
520
+ }
521
+
522
+ /**
523
+ * Add user message to chat
524
+ */
525
+ addUserMessage(text) {
526
+ const container = document.getElementById('chat-messages');
527
+ this.removePlaceholder();
528
+
529
+ const timestamp = new Date();
530
+ const msgEl = document.createElement('div');
531
+ msgEl.className = 'chat-message user';
532
+ msgEl.innerHTML = `
533
+ <div class="message-content">${escapeHtml(text)}</div>
534
+ <div class="message-time">${formatMessageTime(timestamp)}</div>
535
+ `;
536
+
537
+ container.appendChild(msgEl);
538
+ scrollToBottom(container);
539
+
540
+ this.saveToHistory('user', text, timestamp);
541
+ }
542
+
543
+ /**
544
+ * Add assistant message to chat
545
+ */
546
+ addAssistantMessage(text) {
547
+ const container = document.getElementById('chat-messages');
548
+ this.removePlaceholder();
549
+
550
+ this.enableSend(true);
551
+
552
+ const timestamp = new Date();
553
+ const msgEl = document.createElement('div');
554
+ msgEl.className = 'chat-message assistant';
555
+ msgEl.innerHTML = `
556
+ <div class="message-content">${formatAssistantMessage(text)}</div>
557
+ <div class="message-time">${formatMessageTime(timestamp)}</div>
558
+ `;
559
+
560
+ container.appendChild(msgEl);
561
+ scrollToBottom(container);
562
+
563
+ this.saveToHistory('assistant', text, timestamp);
564
+
565
+ // Auto-play TTS if enabled
566
+ if (this.ttsEnabled && text) {
567
+ console.log('[TTS] Auto-play enabled, speaking assistant message');
568
+ this.speak(text);
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Add system message to chat
574
+ */
575
+ addSystemMessage(text, type = 'info') {
576
+ const container = document.getElementById('chat-messages');
577
+ this.removePlaceholder();
578
+
579
+ const msgEl = document.createElement('div');
580
+ msgEl.className = `chat-message system ${type}`;
581
+ msgEl.innerHTML = `
582
+ <div class="message-content">${escapeHtml(text)}</div>
583
+ `;
584
+
585
+ container.appendChild(msgEl);
586
+ scrollToBottom(container);
587
+ }
588
+
589
+ /**
590
+ * Add tool usage card
591
+ */
592
+ addToolCard(toolName, toolId, input) {
593
+ const container = document.getElementById('chat-messages');
594
+ this.removePlaceholder();
595
+
596
+ // Tool icon mapping
597
+ const iconMap = {
598
+ Read: '📄',
599
+ Write: '✏️',
600
+ Bash: '💻',
601
+ Edit: '🔧',
602
+ Grep: '🔍',
603
+ Glob: '📂',
604
+ Task: '🤖',
605
+ WebFetch: '🌐',
606
+ WebSearch: '🔎',
607
+ };
608
+ const icon = iconMap[toolName] || '🔧';
609
+
610
+ // Extract file path for Read tool
611
+ let detail = '';
612
+ if (toolName === 'Read' && input && input.file_path) {
613
+ const fileName = input.file_path.split('/').pop();
614
+ detail = `<div class="tool-detail">${escapeHtml(fileName)}</div>`;
615
+ } else if (toolName === 'Bash' && input && input.command) {
616
+ detail = `<div class="tool-detail">${escapeHtml(input.command.substring(0, 50))}${input.command.length > 50 ? '...' : ''}</div>`;
617
+ }
618
+
619
+ const cardEl = document.createElement('div');
620
+ cardEl.className = 'tool-card loading';
621
+ cardEl.dataset.toolId = toolId;
622
+ cardEl.dataset.collapsed = 'true';
623
+ cardEl.innerHTML = `
624
+ <div class="tool-header" onclick="window.chatModule.toggleToolCard('${toolId}')">
625
+ <span class="tool-icon">${icon}</span>
626
+ <span class="tool-name">${escapeHtml(toolName)}</span>
627
+ <span class="tool-spinner">⏳</span>
628
+ </div>
629
+ ${detail}
630
+ `;
631
+
632
+ container.appendChild(cardEl);
633
+ scrollToBottom(container);
634
+ }
635
+
636
+ /**
637
+ * Complete tool card (mark as finished)
638
+ */
639
+ completeToolCard(_index) {
640
+ // Find the most recent loading tool card
641
+ const loadingCards = document.querySelectorAll('.tool-card.loading');
642
+ if (loadingCards.length > 0) {
643
+ const lastCard = loadingCards[loadingCards.length - 1];
644
+ lastCard.classList.remove('loading');
645
+ lastCard.classList.add('completed');
646
+
647
+ // Replace spinner with checkmark
648
+ const spinner = lastCard.querySelector('.tool-spinner');
649
+ if (spinner) {
650
+ spinner.textContent = '✓';
651
+ spinner.classList.add('checkmark');
652
+ }
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Toggle tool card collapsed/expanded state
658
+ */
659
+ toggleToolCard(toolId) {
660
+ const card = document.querySelector(`.tool-card[data-tool-id="${toolId}"]`);
661
+ if (card) {
662
+ const isCollapsed = card.dataset.collapsed === 'true';
663
+ card.dataset.collapsed = isCollapsed ? 'false' : 'true';
664
+ // Future: expand to show detailed results
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Remove placeholder
670
+ */
671
+ removePlaceholder() {
672
+ const placeholder = document.querySelector('.chat-placeholder');
673
+ if (placeholder) {
674
+ placeholder.remove();
675
+ }
676
+ }
677
+
678
+ // =============================================
679
+ // Streaming Message Handling
680
+ // =============================================
681
+
682
+ /**
683
+ * Append streaming chunk with RAF batching
684
+ */
685
+ appendStreamChunk(content) {
686
+ const container = document.getElementById('chat-messages');
687
+
688
+ if (!this.currentStreamEl) {
689
+ this.removePlaceholder();
690
+ this.currentStreamEl = document.createElement('div');
691
+ this.currentStreamEl.className = 'chat-message assistant streaming';
692
+ this.currentStreamEl.innerHTML = `
693
+ <div class="message-content"></div>
694
+ <div class="message-time">${formatMessageTime(new Date())}</div>
695
+ `;
696
+ container.appendChild(this.currentStreamEl);
697
+ this.currentStreamText = '';
698
+ this.streamBuffer = '';
699
+ }
700
+
701
+ this.streamBuffer += content;
702
+
703
+ if (!this.rafPending) {
704
+ this.rafPending = true;
705
+ requestAnimationFrame(() => {
706
+ if (this.streamBuffer) {
707
+ this.currentStreamText += this.streamBuffer;
708
+ this.streamBuffer = '';
709
+
710
+ const contentEl = this.currentStreamEl.querySelector('.message-content');
711
+ contentEl.innerHTML = formatAssistantMessage(this.currentStreamText);
712
+
713
+ container.scrollTo({
714
+ top: container.scrollHeight,
715
+ behavior: 'smooth',
716
+ });
717
+ }
718
+ this.rafPending = false;
719
+ });
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Finalize streaming message
725
+ */
726
+ finalizeStreamMessage() {
727
+ if (this.streamBuffer && this.currentStreamEl) {
728
+ this.currentStreamText += this.streamBuffer;
729
+ const contentEl = this.currentStreamEl.querySelector('.message-content');
730
+ contentEl.innerHTML = formatAssistantMessage(this.currentStreamText);
731
+ }
732
+
733
+ if (this.currentStreamText) {
734
+ this.saveToHistory('assistant', this.currentStreamText);
735
+ }
736
+
737
+ if (this.currentStreamEl) {
738
+ this.currentStreamEl.classList.remove('streaming');
739
+ this.currentStreamEl = null;
740
+ this.currentStreamText = '';
741
+ this.streamBuffer = '';
742
+ }
743
+ this.rafPending = false;
744
+ this.enableSend(true);
745
+ }
746
+
747
+ // =============================================
748
+ // UI Control
749
+ // =============================================
750
+
751
+ /**
752
+ * Update chat status
753
+ */
754
+ updateStatus(status) {
755
+ const statusEl = document.getElementById('chat-status');
756
+ if (!statusEl) {
757
+ console.warn('[Chat] Status element not found');
758
+ return;
759
+ }
760
+
761
+ const indicator = statusEl.querySelector('.status-indicator');
762
+ const text = statusEl.querySelector('span:not(.status-indicator)');
763
+
764
+ if (!indicator || !text) {
765
+ console.warn('[Chat] Status indicator or text not found');
766
+ return;
767
+ }
768
+
769
+ indicator.className = 'status-indicator ' + status;
770
+
771
+ switch (status) {
772
+ case 'connected':
773
+ text.textContent = 'Connected';
774
+ break;
775
+ case 'disconnected':
776
+ text.textContent = 'Disconnected';
777
+ break;
778
+ case 'connecting':
779
+ text.textContent = 'Connecting...';
780
+ break;
781
+ default:
782
+ text.textContent = status;
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Enable/disable chat input
788
+ */
789
+ enableInput(enabled) {
790
+ const input = document.getElementById('chat-input');
791
+ const sendBtn = document.getElementById('chat-send');
792
+
793
+ input.disabled = !enabled;
794
+ sendBtn.disabled = !enabled;
795
+
796
+ if (enabled) {
797
+ input.placeholder = 'Type your message...';
798
+ } else {
799
+ input.placeholder = 'Connect to a session to chat';
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Enable/disable send button
805
+ */
806
+ enableSend(enabled) {
807
+ const sendBtn = document.getElementById('chat-send');
808
+ sendBtn.disabled = !enabled;
809
+
810
+ if (enabled) {
811
+ sendBtn.textContent = 'Send';
812
+ sendBtn.classList.remove('loading');
813
+ } else {
814
+ sendBtn.textContent = 'Sending...';
815
+ sendBtn.classList.add('loading');
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Enable/disable mic button
821
+ */
822
+ enableMic(enabled) {
823
+ const micBtn = document.getElementById('chat-mic');
824
+ if (micBtn) {
825
+ micBtn.disabled = !enabled;
826
+ }
827
+ }
828
+
829
+ // =============================================
830
+ // Input Handlers
831
+ // =============================================
832
+
833
+ /**
834
+ * Handle chat input keydown
835
+ */
836
+ handleInputKeydown(event) {
837
+ if (event.key === 'Enter' && !event.shiftKey) {
838
+ event.preventDefault();
839
+ this.send();
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Initialize chat input handlers
845
+ */
846
+ initChatInput() {
847
+ const input = document.getElementById('chat-input');
848
+
849
+ input.addEventListener('input', () => {
850
+ autoResizeTextarea(input);
851
+ });
852
+
853
+ input.addEventListener('keydown', (event) => {
854
+ this.handleInputKeydown(event);
855
+ });
856
+ }
857
+
858
+ /**
859
+ * Initialize long press to copy message functionality
860
+ * Supports both touch (mobile) and mouse (desktop) events
861
+ */
862
+ initLongPressCopy() {
863
+ const messagesContainer = document.getElementById('chat-messages');
864
+ let pressTimer = null;
865
+ const PRESS_DURATION = 750; // milliseconds
866
+
867
+ // Touch events (mobile)
868
+ messagesContainer.addEventListener('touchstart', (e) => {
869
+ const message = e.target.closest('.message');
870
+ if (!message || message.classList.contains('system')) {
871
+ return;
872
+ }
873
+
874
+ pressTimer = setTimeout(() => {
875
+ copyMessageText(message);
876
+ }, PRESS_DURATION);
877
+ });
878
+
879
+ messagesContainer.addEventListener('touchend', () => {
880
+ if (pressTimer) {
881
+ clearTimeout(pressTimer);
882
+ pressTimer = null;
883
+ }
884
+ });
885
+
886
+ messagesContainer.addEventListener('touchmove', () => {
887
+ if (pressTimer) {
888
+ clearTimeout(pressTimer);
889
+ pressTimer = null;
890
+ }
891
+ });
892
+
893
+ // Mouse events (desktop)
894
+ messagesContainer.addEventListener('mousedown', (e) => {
895
+ const message = e.target.closest('.message');
896
+ if (!message || message.classList.contains('system')) {
897
+ return;
898
+ }
899
+
900
+ pressTimer = setTimeout(() => {
901
+ copyMessageText(message);
902
+ }, PRESS_DURATION);
903
+ });
904
+
905
+ messagesContainer.addEventListener('mouseup', () => {
906
+ if (pressTimer) {
907
+ clearTimeout(pressTimer);
908
+ pressTimer = null;
909
+ }
910
+ });
911
+
912
+ messagesContainer.addEventListener('mouseleave', () => {
913
+ if (pressTimer) {
914
+ clearTimeout(pressTimer);
915
+ pressTimer = null;
916
+ }
917
+ });
918
+
919
+ /**
920
+ * Copy message text to clipboard
921
+ */
922
+ async function copyMessageText(messageEl) {
923
+ const textContent = messageEl.querySelector('.message-text');
924
+ if (!textContent) {
925
+ return;
926
+ }
927
+
928
+ const text = textContent.textContent;
929
+
930
+ try {
931
+ await navigator.clipboard.writeText(text);
932
+ showToast('📋 Copied to clipboard');
933
+
934
+ // Visual feedback
935
+ messageEl.style.opacity = '0.5';
936
+ setTimeout(() => {
937
+ messageEl.style.opacity = '1';
938
+ }, 300);
939
+ } catch (err) {
940
+ console.error('[Chat] Copy failed:', err);
941
+ showToast('Failed to copy', 'error');
942
+ }
943
+ }
944
+ }
945
+
946
+ // =============================================
947
+ // Voice Input (Web Speech API)
948
+ // =============================================
949
+
950
+ /**
951
+ * Initialize speech recognition
952
+ */
953
+ initSpeechRecognition() {
954
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
955
+
956
+ if (!SpeechRecognition) {
957
+ console.warn('[Voice] SpeechRecognition not supported');
958
+ const micBtn = document.getElementById('chat-mic');
959
+ if (micBtn) {
960
+ micBtn.style.display = 'none';
961
+ }
962
+ return;
963
+ }
964
+
965
+ this.speechRecognition = new SpeechRecognition();
966
+ this.speechRecognition.lang = navigator.language || 'ko-KR';
967
+ this.speechRecognition.continuous = true; // Enable continuous recognition for longer phrases
968
+ this.speechRecognition.interimResults = true;
969
+ this.speechRecognition.maxAlternatives = 3; // Get multiple recognition candidates for better accuracy
970
+
971
+ this.speechRecognition.onresult = (event) => {
972
+ const input = document.getElementById('chat-input');
973
+ let interimTranscript = '';
974
+ let finalTranscript = '';
975
+
976
+ // Build transcript from NEW results only (use resultIndex)
977
+ console.log(
978
+ '[Voice] onresult fired, resultIndex:',
979
+ event.resultIndex,
980
+ 'total results:',
981
+ event.results.length
982
+ );
983
+
984
+ for (let i = event.resultIndex; i < event.results.length; i++) {
985
+ const result = event.results[i];
986
+ const transcript = result[0].transcript;
987
+
988
+ if (result.isFinal) {
989
+ finalTranscript += transcript;
990
+ console.log(
991
+ '[Voice] Final result [' + i + ']:',
992
+ transcript,
993
+ 'Confidence:',
994
+ result[0].confidence
995
+ );
996
+ } else {
997
+ interimTranscript += transcript;
998
+ console.log('[Voice] Interim result [' + i + ']:', transcript);
999
+ }
1000
+ }
1001
+
1002
+ // Handle final transcripts - accumulate them
1003
+ if (finalTranscript) {
1004
+ // Add space before appending if there's already text
1005
+ if (this.accumulatedTranscript) {
1006
+ this.accumulatedTranscript += ' ' + finalTranscript;
1007
+ } else {
1008
+ this.accumulatedTranscript = finalTranscript;
1009
+ }
1010
+ input.value = this.accumulatedTranscript;
1011
+ input.classList.remove('voice-active');
1012
+ console.log('[Voice] Accumulated transcript:', this.accumulatedTranscript);
1013
+ }
1014
+
1015
+ // Handle interim transcripts - show temporarily with accumulated text
1016
+ if (interimTranscript) {
1017
+ const displayText = this.accumulatedTranscript
1018
+ ? this.accumulatedTranscript + ' ' + interimTranscript
1019
+ : interimTranscript;
1020
+ input.value = displayText;
1021
+ input.classList.add('voice-active');
1022
+ console.log('[Voice] Showing interim (temp):', displayText);
1023
+ }
1024
+
1025
+ autoResizeTextarea(input);
1026
+
1027
+ // Reset silence timer on each result
1028
+ clearTimeout(this.silenceTimeout);
1029
+ this.silenceTimeout = setTimeout(() => {
1030
+ if (this.isRecording) {
1031
+ console.log('[Voice] Silence detected, stopping...');
1032
+ this.stopVoice();
1033
+ }
1034
+ }, this.silenceDelay);
1035
+ };
1036
+
1037
+ this.speechRecognition.onend = () => {
1038
+ console.log('[Voice] Recognition ended');
1039
+ this.stopVoice();
1040
+ };
1041
+
1042
+ this.speechRecognition.onerror = (event) => {
1043
+ console.error('[Voice] Error:', event.error);
1044
+ this.stopVoice();
1045
+
1046
+ let errorMessage = '';
1047
+ switch (event.error) {
1048
+ case 'not-allowed':
1049
+ errorMessage = '마이크 권한이 거부되었습니다. 브라우저 설정에서 마이크를 허용해주세요.';
1050
+ break;
1051
+ case 'no-speech':
1052
+ errorMessage = '음성이 감지되지 않았습니다. 다시 시도해주세요.';
1053
+ break;
1054
+ case 'network':
1055
+ errorMessage = '네트워크 오류가 발생했습니다.';
1056
+ break;
1057
+ default:
1058
+ errorMessage = `음성 인식 오류: ${event.error}`;
1059
+ }
1060
+
1061
+ this.addSystemMessage(errorMessage, 'error');
1062
+ };
1063
+
1064
+ console.log('[Voice] SpeechRecognition initialized (lang:', this.speechRecognition.lang + ')');
1065
+ }
1066
+
1067
+ /**
1068
+ * Toggle voice input
1069
+ */
1070
+ toggleVoice() {
1071
+ if (this.isRecording) {
1072
+ this.stopVoice();
1073
+ } else {
1074
+ this.startVoice();
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Start voice recording
1080
+ */
1081
+ startVoice() {
1082
+ if (!this.speechRecognition) {
1083
+ this.addSystemMessage('이 브라우저에서는 음성 인식이 지원되지 않습니다.', 'error');
1084
+ return;
1085
+ }
1086
+
1087
+ try {
1088
+ const micBtn = document.getElementById('chat-mic');
1089
+ const input = document.getElementById('chat-input');
1090
+
1091
+ // Clear input and accumulated transcript for new recording
1092
+ input.value = '';
1093
+ this.accumulatedTranscript = '';
1094
+
1095
+ this.speechRecognition.start();
1096
+ this.isRecording = true;
1097
+
1098
+ micBtn.classList.add('recording');
1099
+ input.classList.add('voice-active');
1100
+ input.placeholder = '말씀해주세요... (계속 말하면 이어서 인식됩니다)';
1101
+
1102
+ console.log('[Voice] Recording started (continuous mode)');
1103
+ console.log('[Voice] Settings:', {
1104
+ lang: this.speechRecognition.lang,
1105
+ continuous: this.speechRecognition.continuous,
1106
+ interimResults: this.speechRecognition.interimResults,
1107
+ maxAlternatives: this.speechRecognition.maxAlternatives,
1108
+ });
1109
+
1110
+ this.silenceTimeout = setTimeout(() => {
1111
+ if (this.isRecording) {
1112
+ this.stopVoice();
1113
+ }
1114
+ }, this.silenceDelay);
1115
+ } catch (err) {
1116
+ console.error('[Voice] Failed to start:', err);
1117
+ this.addSystemMessage('음성 인식을 시작할 수 없습니다.', 'error');
1118
+ }
1119
+ }
1120
+
1121
+ /**
1122
+ * Stop voice recording
1123
+ */
1124
+ stopVoice() {
1125
+ if (!this.isRecording) {
1126
+ return;
1127
+ }
1128
+
1129
+ clearTimeout(this.silenceTimeout);
1130
+
1131
+ try {
1132
+ this.speechRecognition.stop();
1133
+ } catch (e) {
1134
+ // Ignore errors
1135
+ }
1136
+
1137
+ this.isRecording = false;
1138
+
1139
+ const micBtn = document.getElementById('chat-mic');
1140
+ const input = document.getElementById('chat-input');
1141
+
1142
+ micBtn.classList.remove('recording');
1143
+ input.classList.remove('voice-active');
1144
+ input.placeholder = 'Type your message...';
1145
+
1146
+ console.log('[Voice] Recording stopped');
1147
+ this.resetIdleTimer();
1148
+ }
1149
+
1150
+ // =============================================
1151
+ // Text-to-Speech (TTS)
1152
+ // =============================================
1153
+
1154
+ /**
1155
+ * Initialize Speech Synthesis
1156
+ */
1157
+ initSpeechSynthesis() {
1158
+ if (!this.speechSynthesis) {
1159
+ console.warn('[TTS] SpeechSynthesis not supported');
1160
+ return;
1161
+ }
1162
+
1163
+ // Wait for voices to load
1164
+ const loadVoices = () => {
1165
+ const voices = this.speechSynthesis.getVoices();
1166
+ // Find Korean voice
1167
+ this.ttsVoice =
1168
+ voices.find((v) => v.lang === 'ko-KR') ||
1169
+ voices.find((v) => v.lang.startsWith('ko')) ||
1170
+ voices[0];
1171
+
1172
+ if (this.ttsVoice) {
1173
+ console.log('[TTS] Korean voice selected:', this.ttsVoice.name, this.ttsVoice.lang);
1174
+ } else {
1175
+ console.warn('[TTS] No Korean voice found, using default');
1176
+ }
1177
+ };
1178
+
1179
+ // Voices might not be loaded immediately
1180
+ if (this.speechSynthesis.getVoices().length > 0) {
1181
+ loadVoices();
1182
+ } else {
1183
+ this.speechSynthesis.onvoiceschanged = loadVoices;
1184
+ }
1185
+
1186
+ console.log('[TTS] SpeechSynthesis initialized');
1187
+ }
1188
+
1189
+ /**
1190
+ * Toggle TTS auto-play
1191
+ */
1192
+ toggleTTS() {
1193
+ this.ttsEnabled = !this.ttsEnabled;
1194
+ const btn = document.getElementById('chat-tts-toggle');
1195
+
1196
+ if (btn) {
1197
+ btn.classList.toggle('active', this.ttsEnabled);
1198
+ btn.title = this.ttsEnabled
1199
+ ? 'TTS 활성화됨 (클릭하여 끄기)'
1200
+ : 'TTS 비활성화됨 (클릭하여 켜기)';
1201
+ }
1202
+
1203
+ console.log('[TTS] Auto-play:', this.ttsEnabled ? 'ON' : 'OFF');
1204
+ showToast(this.ttsEnabled ? '🔊 TTS 활성화' : '🔇 TTS 비활성화');
1205
+ }
1206
+
1207
+ /**
1208
+ * Toggle hands-free mode
1209
+ */
1210
+ toggleHandsFree() {
1211
+ this.handsFreeMode = !this.handsFreeMode;
1212
+ const btn = document.getElementById('chat-handsfree-toggle');
1213
+
1214
+ if (btn) {
1215
+ btn.classList.toggle('active', this.handsFreeMode);
1216
+ btn.title = this.handsFreeMode ? '핸즈프리 활성화됨' : '핸즈프리 비활성화됨';
1217
+ }
1218
+
1219
+ console.log('[TTS] Hands-free mode:', this.handsFreeMode ? 'ON' : 'OFF');
1220
+ showToast(this.handsFreeMode ? '🎙️ 핸즈프리 모드 활성화' : '🎙️ 핸즈프리 모드 비활성화');
1221
+
1222
+ // Enable TTS automatically when hands-free is enabled
1223
+ if (this.handsFreeMode && !this.ttsEnabled) {
1224
+ this.toggleTTS();
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Speak text using TTS
1230
+ */
1231
+ speak(text) {
1232
+ if (!this.speechSynthesis || !text) {
1233
+ return;
1234
+ }
1235
+
1236
+ // Stop any ongoing speech
1237
+ this.stopSpeaking();
1238
+
1239
+ const utterance = new SpeechSynthesisUtterance(text);
1240
+ utterance.voice = this.ttsVoice;
1241
+ utterance.rate = this.ttsRate;
1242
+ utterance.pitch = this.ttsPitch;
1243
+ utterance.lang = this.ttsVoice?.lang || navigator.language || 'ko-KR';
1244
+
1245
+ utterance.onstart = () => {
1246
+ this.isSpeaking = true;
1247
+ console.log('[TTS] Speaking started');
1248
+ };
1249
+
1250
+ utterance.onend = () => {
1251
+ this.isSpeaking = false;
1252
+ console.log('[TTS] Speaking ended');
1253
+
1254
+ // If hands-free mode, start listening after TTS finishes
1255
+ if (this.handsFreeMode && !this.isRecording) {
1256
+ console.log('[TTS] Hands-free mode: auto-starting voice input');
1257
+ setTimeout(() => {
1258
+ this.startVoice();
1259
+ }, 500); // Small delay for smooth transition
1260
+ }
1261
+ };
1262
+
1263
+ utterance.onerror = (event) => {
1264
+ this.isSpeaking = false;
1265
+ console.error('[TTS] Error:', event.error);
1266
+ };
1267
+
1268
+ this.speechSynthesis.speak(utterance);
1269
+ console.log('[TTS] Speaking:', text.substring(0, 50) + '...');
1270
+ }
1271
+
1272
+ /**
1273
+ * Stop speaking
1274
+ */
1275
+ stopSpeaking() {
1276
+ if (this.speechSynthesis && this.isSpeaking) {
1277
+ this.speechSynthesis.cancel();
1278
+ this.isSpeaking = false;
1279
+ console.log('[TTS] Speaking stopped');
1280
+ }
1281
+ }
1282
+
1283
+ /**
1284
+ * Set TTS rate (0.5 - 2.0)
1285
+ */
1286
+ setTTSRate(rate) {
1287
+ this.ttsRate = Math.max(0.5, Math.min(2.0, rate));
1288
+ console.log('[TTS] Rate set to:', this.ttsRate);
1289
+ }
1290
+
1291
+ // =============================================
1292
+ // History Management
1293
+ // =============================================
1294
+
1295
+ /**
1296
+ * Save message to history
1297
+ */
1298
+ saveToHistory(role, content, timestamp = new Date()) {
1299
+ if (!this.sessionId) {
1300
+ return;
1301
+ }
1302
+
1303
+ this.history.push({
1304
+ role,
1305
+ content,
1306
+ timestamp: timestamp.toISOString(),
1307
+ });
1308
+
1309
+ if (this.history.length > this.maxHistoryMessages) {
1310
+ this.history = this.history.slice(-this.maxHistoryMessages);
1311
+ }
1312
+
1313
+ try {
1314
+ const storageKey = this.historyPrefix + this.sessionId;
1315
+ const storageData = {
1316
+ history: this.history,
1317
+ savedAt: Date.now(),
1318
+ };
1319
+ localStorage.setItem(storageKey, JSON.stringify(storageData));
1320
+ } catch (e) {
1321
+ console.warn('[Chat] Failed to save history:', e);
1322
+ }
1323
+ }
1324
+
1325
+ /**
1326
+ * Load history from localStorage
1327
+ */
1328
+ loadHistory(sessionId) {
1329
+ try {
1330
+ const storageKey = this.historyPrefix + sessionId;
1331
+ const stored = localStorage.getItem(storageKey);
1332
+
1333
+ if (!stored) {
1334
+ return null;
1335
+ }
1336
+
1337
+ const data = JSON.parse(stored);
1338
+
1339
+ if (Date.now() - data.savedAt > this.historyExpiryMs) {
1340
+ localStorage.removeItem(storageKey);
1341
+ return null;
1342
+ }
1343
+
1344
+ return data.history || [];
1345
+ } catch (e) {
1346
+ console.warn('[Chat] Failed to load history:', e);
1347
+ return null;
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * Restore chat history
1353
+ */
1354
+ restoreHistory(sessionId) {
1355
+ const history = this.loadHistory(sessionId);
1356
+
1357
+ if (!history || history.length === 0) {
1358
+ return false;
1359
+ }
1360
+
1361
+ this.history = history;
1362
+ const container = document.getElementById('chat-messages');
1363
+
1364
+ this.removePlaceholder();
1365
+
1366
+ history.forEach((msg) => {
1367
+ const msgEl = document.createElement('div');
1368
+ msgEl.className = `chat-message ${msg.role}`;
1369
+
1370
+ if (msg.role === 'user') {
1371
+ msgEl.innerHTML = `
1372
+ <div class="message-content">${escapeHtml(msg.content)}</div>
1373
+ <div class="message-time">${formatMessageTime(new Date(msg.timestamp))}</div>
1374
+ `;
1375
+ } else if (msg.role === 'assistant') {
1376
+ msgEl.innerHTML = `
1377
+ <div class="message-content">${formatAssistantMessage(msg.content)}</div>
1378
+ <div class="message-time">${formatMessageTime(new Date(msg.timestamp))}</div>
1379
+ `;
1380
+ } else if (msg.role === 'system') {
1381
+ msgEl.innerHTML = `
1382
+ <div class="message-content">${escapeHtml(msg.content)}</div>
1383
+ `;
1384
+ }
1385
+
1386
+ container.appendChild(msgEl);
1387
+ });
1388
+
1389
+ scrollToBottom(container);
1390
+ showToast('Previous conversation restored');
1391
+
1392
+ return true;
1393
+ }
1394
+
1395
+ /**
1396
+ * Display history received from server
1397
+ */
1398
+ displayHistory(messages) {
1399
+ const container = document.getElementById('chat-messages');
1400
+ if (!container) {
1401
+ return;
1402
+ }
1403
+
1404
+ // Clear existing messages first (server history is authoritative)
1405
+ container.innerHTML = '';
1406
+ this.history = [];
1407
+
1408
+ messages.forEach((msg) => {
1409
+ const msgEl = document.createElement('div');
1410
+ msgEl.className = `chat-message ${msg.role}`;
1411
+
1412
+ const timestamp = msg.timestamp ? new Date(msg.timestamp) : new Date();
1413
+
1414
+ if (msg.role === 'user') {
1415
+ msgEl.innerHTML = `
1416
+ <div class="message-content">${escapeHtml(msg.content)}</div>
1417
+ <div class="message-time">${formatMessageTime(timestamp)}</div>
1418
+ `;
1419
+ } else if (msg.role === 'assistant') {
1420
+ msgEl.innerHTML = `
1421
+ <div class="message-content">${formatAssistantMessage(msg.content)}</div>
1422
+ <div class="message-time">${formatMessageTime(timestamp)}</div>
1423
+ `;
1424
+ } else if (msg.role === 'system') {
1425
+ msgEl.innerHTML = `
1426
+ <div class="message-content">${escapeHtml(msg.content)}</div>
1427
+ `;
1428
+ }
1429
+
1430
+ container.appendChild(msgEl);
1431
+ });
1432
+
1433
+ scrollToBottom(container);
1434
+ console.log('[Chat] Displayed', messages.length, 'history messages');
1435
+ }
1436
+
1437
+ /**
1438
+ * Clear chat history
1439
+ */
1440
+ clearHistory(sessionId = null) {
1441
+ try {
1442
+ const storageKey = this.historyPrefix + (sessionId || this.sessionId);
1443
+ localStorage.removeItem(storageKey);
1444
+ this.history = [];
1445
+ } catch (e) {
1446
+ console.warn('[Chat] Failed to clear history:', e);
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * Clean up expired histories
1452
+ */
1453
+ cleanupExpiredHistories() {
1454
+ try {
1455
+ const keys = Object.keys(localStorage);
1456
+ const now = Date.now();
1457
+
1458
+ keys.forEach((key) => {
1459
+ if (key.startsWith(this.historyPrefix)) {
1460
+ try {
1461
+ const data = JSON.parse(localStorage.getItem(key));
1462
+ if (data && data.savedAt && now - data.savedAt > this.historyExpiryMs) {
1463
+ localStorage.removeItem(key);
1464
+ console.log('[Chat] Cleaned up expired history:', key);
1465
+ }
1466
+ } catch (e) {
1467
+ // Invalid data, remove it
1468
+ localStorage.removeItem(key);
1469
+ }
1470
+ }
1471
+ });
1472
+ } catch (e) {
1473
+ console.warn('[Chat] Failed to cleanup histories:', e);
1474
+ }
1475
+ }
1476
+
1477
+ // =============================================
1478
+ // Checkpoint Management
1479
+ // =============================================
1480
+
1481
+ /**
1482
+ * Generate checkpoint summary from current session (for manual /checkpoint command)
1483
+ */
1484
+ generateCheckpointSummary() {
1485
+ const summary = {
1486
+ sessionId: this.sessionId,
1487
+ messageCount: this.history.length,
1488
+ lastActivity: new Date().toISOString(),
1489
+ messages: this.history.slice(-10).map((msg) => ({
1490
+ role: msg.role,
1491
+ preview: msg.content.substring(0, 100),
1492
+ timestamp: msg.timestamp,
1493
+ })),
1494
+ };
1495
+
1496
+ return JSON.stringify(summary, null, 2);
1497
+ }
1498
+
1499
+ /**
1500
+ * Save checkpoint via API
1501
+ */
1502
+ async saveCheckpoint(summary) {
1503
+ const response = await fetch('/api/checkpoint/save', {
1504
+ method: 'POST',
1505
+ headers: { 'Content-Type': 'application/json' },
1506
+ body: JSON.stringify({ summary }),
1507
+ });
1508
+
1509
+ if (!response.ok) {
1510
+ throw new Error('Failed to save checkpoint');
1511
+ }
1512
+
1513
+ return await response.json();
1514
+ }
1515
+
1516
+ /**
1517
+ * Load last checkpoint via API
1518
+ */
1519
+ async loadCheckpoint() {
1520
+ const response = await fetch('/api/checkpoint/load');
1521
+
1522
+ if (!response.ok) {
1523
+ if (response.status === 404) {
1524
+ return null; // No checkpoint found
1525
+ }
1526
+ throw new Error('Failed to load checkpoint');
1527
+ }
1528
+
1529
+ return await response.json();
1530
+ }
1531
+
1532
+ /**
1533
+ * Check for resumable session on init
1534
+ */
1535
+ async checkForResumableSession() {
1536
+ try {
1537
+ const checkpoint = await this.loadCheckpoint();
1538
+ if (checkpoint) {
1539
+ // Show resume banner
1540
+ const banner = document.getElementById('session-resume-banner');
1541
+ if (banner) {
1542
+ banner.style.display = 'flex';
1543
+ console.log('[Chat] Resume banner shown');
1544
+ }
1545
+ }
1546
+ } catch (error) {
1547
+ // Silent fail - no checkpoint is okay
1548
+ console.log('[Chat] No resumable session');
1549
+ }
1550
+ }
1551
+
1552
+ /**
1553
+ * Cleanup resources when module is destroyed
1554
+ * Prevents memory leaks by cleaning up timers, connections, and APIs
1555
+ */
1556
+ cleanup() {
1557
+ // Clean up WebSocket
1558
+ if (this.ws) {
1559
+ this.ws.close();
1560
+ this.ws = null;
1561
+ }
1562
+
1563
+ // Clean up timers
1564
+ if (this.silenceTimeout) {
1565
+ clearTimeout(this.silenceTimeout);
1566
+ this.silenceTimeout = null;
1567
+ }
1568
+ if (this.idleTimer) {
1569
+ clearTimeout(this.idleTimer);
1570
+ this.idleTimer = null;
1571
+ }
1572
+
1573
+ // Clean up Speech Recognition
1574
+ if (this.speechRecognition) {
1575
+ this.speechRecognition.stop();
1576
+ this.speechRecognition = null;
1577
+ }
1578
+
1579
+ // Clean up Speech Synthesis
1580
+ if (this.isSpeaking) {
1581
+ this.speechSynthesis.cancel();
1582
+ this.isSpeaking = false;
1583
+ }
1584
+
1585
+ console.log('[Chat] Cleanup completed');
1586
+ }
1587
+ }