@fraziym/axoniz 0.1.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 (512) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/agents/definition.d.ts +15 -0
  4. package/dist/agents/definition.d.ts.map +1 -0
  5. package/dist/agents/definition.js +41 -0
  6. package/dist/agents/definition.js.map +1 -0
  7. package/dist/agents/index.d.ts +12 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/index.js +11 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/registry.d.ts +17 -0
  12. package/dist/agents/registry.d.ts.map +1 -0
  13. package/dist/agents/registry.js +23 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/specialized.d.ts +31 -0
  16. package/dist/agents/specialized.d.ts.map +1 -0
  17. package/dist/agents/specialized.js +75 -0
  18. package/dist/agents/specialized.js.map +1 -0
  19. package/dist/awareness/index.d.ts +14 -0
  20. package/dist/awareness/index.d.ts.map +1 -0
  21. package/dist/awareness/index.js +22 -0
  22. package/dist/awareness/index.js.map +1 -0
  23. package/dist/awareness/service.d.ts +180 -0
  24. package/dist/awareness/service.d.ts.map +1 -0
  25. package/dist/awareness/service.js +789 -0
  26. package/dist/awareness/service.js.map +1 -0
  27. package/dist/cli/entry.d.ts +13 -0
  28. package/dist/cli/entry.d.ts.map +1 -0
  29. package/dist/cli/entry.js +35 -0
  30. package/dist/cli/entry.js.map +1 -0
  31. package/dist/comms/telegram.d.ts +41 -0
  32. package/dist/comms/telegram.d.ts.map +1 -0
  33. package/dist/comms/telegram.js +332 -0
  34. package/dist/comms/telegram.js.map +1 -0
  35. package/dist/core/agent.d.ts +252 -0
  36. package/dist/core/agent.d.ts.map +1 -0
  37. package/dist/core/agent.js +1399 -0
  38. package/dist/core/agent.js.map +1 -0
  39. package/dist/core/auth.d.ts +23 -0
  40. package/dist/core/auth.d.ts.map +1 -0
  41. package/dist/core/auth.js +113 -0
  42. package/dist/core/auth.js.map +1 -0
  43. package/dist/core/authority.d.ts +118 -0
  44. package/dist/core/authority.d.ts.map +1 -0
  45. package/dist/core/authority.js +402 -0
  46. package/dist/core/authority.js.map +1 -0
  47. package/dist/core/backend/async_queue.d.ts +17 -0
  48. package/dist/core/backend/async_queue.d.ts.map +1 -0
  49. package/dist/core/backend/async_queue.js +66 -0
  50. package/dist/core/backend/async_queue.js.map +1 -0
  51. package/dist/core/backend/index.d.ts +24 -0
  52. package/dist/core/backend/index.d.ts.map +1 -0
  53. package/dist/core/backend/index.js +128 -0
  54. package/dist/core/backend/index.js.map +1 -0
  55. package/dist/core/backend/llama_server.d.ts +39 -0
  56. package/dist/core/backend/llama_server.d.ts.map +1 -0
  57. package/dist/core/backend/llama_server.js +182 -0
  58. package/dist/core/backend/llama_server.js.map +1 -0
  59. package/dist/core/backend/llamacpp.d.ts +40 -0
  60. package/dist/core/backend/llamacpp.d.ts.map +1 -0
  61. package/dist/core/backend/llamacpp.js +200 -0
  62. package/dist/core/backend/llamacpp.js.map +1 -0
  63. package/dist/core/backend/lmstudio.d.ts +53 -0
  64. package/dist/core/backend/lmstudio.d.ts.map +1 -0
  65. package/dist/core/backend/lmstudio.js +274 -0
  66. package/dist/core/backend/lmstudio.js.map +1 -0
  67. package/dist/core/backend/ollama.d.ts +25 -0
  68. package/dist/core/backend/ollama.d.ts.map +1 -0
  69. package/dist/core/backend/ollama.js +86 -0
  70. package/dist/core/backend/ollama.js.map +1 -0
  71. package/dist/core/backend/openai.d.ts +27 -0
  72. package/dist/core/backend/openai.d.ts.map +1 -0
  73. package/dist/core/backend/openai.js +84 -0
  74. package/dist/core/backend/openai.js.map +1 -0
  75. package/dist/core/backend/types.d.ts +73 -0
  76. package/dist/core/backend/types.d.ts.map +1 -0
  77. package/dist/core/backend/types.js +137 -0
  78. package/dist/core/backend/types.js.map +1 -0
  79. package/dist/core/chat_store.d.ts +77 -0
  80. package/dist/core/chat_store.d.ts.map +1 -0
  81. package/dist/core/chat_store.js +332 -0
  82. package/dist/core/chat_store.js.map +1 -0
  83. package/dist/core/cli.d.ts +82 -0
  84. package/dist/core/cli.d.ts.map +1 -0
  85. package/dist/core/cli.js +602 -0
  86. package/dist/core/cli.js.map +1 -0
  87. package/dist/core/config.d.ts +127 -0
  88. package/dist/core/config.d.ts.map +1 -0
  89. package/dist/core/config.js +295 -0
  90. package/dist/core/config.js.map +1 -0
  91. package/dist/core/debug.d.ts +8 -0
  92. package/dist/core/debug.d.ts.map +1 -0
  93. package/dist/core/debug.js +8 -0
  94. package/dist/core/debug.js.map +1 -0
  95. package/dist/core/downloader.d.ts +31 -0
  96. package/dist/core/downloader.d.ts.map +1 -0
  97. package/dist/core/downloader.js +180 -0
  98. package/dist/core/downloader.js.map +1 -0
  99. package/dist/core/errors.d.ts +80 -0
  100. package/dist/core/errors.d.ts.map +1 -0
  101. package/dist/core/errors.js +134 -0
  102. package/dist/core/errors.js.map +1 -0
  103. package/dist/core/extras.d.ts +160 -0
  104. package/dist/core/extras.d.ts.map +1 -0
  105. package/dist/core/extras.js +553 -0
  106. package/dist/core/extras.js.map +1 -0
  107. package/dist/core/health.d.ts +27 -0
  108. package/dist/core/health.d.ts.map +1 -0
  109. package/dist/core/health.js +96 -0
  110. package/dist/core/health.js.map +1 -0
  111. package/dist/core/history.d.ts +50 -0
  112. package/dist/core/history.d.ts.map +1 -0
  113. package/dist/core/history.js +276 -0
  114. package/dist/core/history.js.map +1 -0
  115. package/dist/core/index.d.ts +6 -0
  116. package/dist/core/index.d.ts.map +1 -0
  117. package/dist/core/index.js +6 -0
  118. package/dist/core/index.js.map +1 -0
  119. package/dist/core/intelligence/ast_index.d.ts +255 -0
  120. package/dist/core/intelligence/ast_index.d.ts.map +1 -0
  121. package/dist/core/intelligence/ast_index.js +1369 -0
  122. package/dist/core/intelligence/ast_index.js.map +1 -0
  123. package/dist/core/intelligence/confidence.d.ts +113 -0
  124. package/dist/core/intelligence/confidence.d.ts.map +1 -0
  125. package/dist/core/intelligence/confidence.js +298 -0
  126. package/dist/core/intelligence/confidence.js.map +1 -0
  127. package/dist/core/intelligence/context_compressor.d.ts +80 -0
  128. package/dist/core/intelligence/context_compressor.d.ts.map +1 -0
  129. package/dist/core/intelligence/context_compressor.js +245 -0
  130. package/dist/core/intelligence/context_compressor.js.map +1 -0
  131. package/dist/core/intelligence/daemon.d.ts +247 -0
  132. package/dist/core/intelligence/daemon.d.ts.map +1 -0
  133. package/dist/core/intelligence/daemon.js +842 -0
  134. package/dist/core/intelligence/daemon.js.map +1 -0
  135. package/dist/core/intelligence/index.d.ts +60 -0
  136. package/dist/core/intelligence/index.d.ts.map +1 -0
  137. package/dist/core/intelligence/index.js +101 -0
  138. package/dist/core/intelligence/index.js.map +1 -0
  139. package/dist/core/intelligence/predictor.d.ts +246 -0
  140. package/dist/core/intelligence/predictor.d.ts.map +1 -0
  141. package/dist/core/intelligence/predictor.js +590 -0
  142. package/dist/core/intelligence/predictor.js.map +1 -0
  143. package/dist/core/intelligence/reflex.d.ts +84 -0
  144. package/dist/core/intelligence/reflex.d.ts.map +1 -0
  145. package/dist/core/intelligence/reflex.js +344 -0
  146. package/dist/core/intelligence/reflex.js.map +1 -0
  147. package/dist/core/intelligence/self_correction.d.ts +121 -0
  148. package/dist/core/intelligence/self_correction.d.ts.map +1 -0
  149. package/dist/core/intelligence/self_correction.js +327 -0
  150. package/dist/core/intelligence/self_correction.js.map +1 -0
  151. package/dist/core/intelligence/skill_distiller.d.ts +100 -0
  152. package/dist/core/intelligence/skill_distiller.d.ts.map +1 -0
  153. package/dist/core/intelligence/skill_distiller.js +246 -0
  154. package/dist/core/intelligence/skill_distiller.js.map +1 -0
  155. package/dist/core/intelligence/swarm.d.ts +330 -0
  156. package/dist/core/intelligence/swarm.d.ts.map +1 -0
  157. package/dist/core/intelligence/swarm.js +1064 -0
  158. package/dist/core/intelligence/swarm.js.map +1 -0
  159. package/dist/core/intelligence/trajectory.d.ts +107 -0
  160. package/dist/core/intelligence/trajectory.d.ts.map +1 -0
  161. package/dist/core/intelligence/trajectory.js +330 -0
  162. package/dist/core/intelligence/trajectory.js.map +1 -0
  163. package/dist/core/logger.d.ts +111 -0
  164. package/dist/core/logger.d.ts.map +1 -0
  165. package/dist/core/logger.js +640 -0
  166. package/dist/core/logger.js.map +1 -0
  167. package/dist/core/loop.d.ts +123 -0
  168. package/dist/core/loop.d.ts.map +1 -0
  169. package/dist/core/loop.js +423 -0
  170. package/dist/core/loop.js.map +1 -0
  171. package/dist/core/memory.d.ts +81 -0
  172. package/dist/core/memory.d.ts.map +1 -0
  173. package/dist/core/memory.js +328 -0
  174. package/dist/core/memory.js.map +1 -0
  175. package/dist/core/metrics.d.ts +29 -0
  176. package/dist/core/metrics.d.ts.map +1 -0
  177. package/dist/core/metrics.js +109 -0
  178. package/dist/core/metrics.js.map +1 -0
  179. package/dist/core/models.d.ts +51 -0
  180. package/dist/core/models.d.ts.map +1 -0
  181. package/dist/core/models.js +146 -0
  182. package/dist/core/models.js.map +1 -0
  183. package/dist/core/optimizer.d.ts +34 -0
  184. package/dist/core/optimizer.d.ts.map +1 -0
  185. package/dist/core/optimizer.js +131 -0
  186. package/dist/core/optimizer.js.map +1 -0
  187. package/dist/core/persona.d.ts +35 -0
  188. package/dist/core/persona.d.ts.map +1 -0
  189. package/dist/core/persona.js +126 -0
  190. package/dist/core/persona.js.map +1 -0
  191. package/dist/core/rate_limit.d.ts +23 -0
  192. package/dist/core/rate_limit.d.ts.map +1 -0
  193. package/dist/core/rate_limit.js +62 -0
  194. package/dist/core/rate_limit.js.map +1 -0
  195. package/dist/core/runner.d.ts +63 -0
  196. package/dist/core/runner.d.ts.map +1 -0
  197. package/dist/core/runner.js +671 -0
  198. package/dist/core/runner.js.map +1 -0
  199. package/dist/core/stream_parser.d.ts +43 -0
  200. package/dist/core/stream_parser.d.ts.map +1 -0
  201. package/dist/core/stream_parser.js +140 -0
  202. package/dist/core/stream_parser.js.map +1 -0
  203. package/dist/core/tool_schemas.d.ts +26 -0
  204. package/dist/core/tool_schemas.d.ts.map +1 -0
  205. package/dist/core/tool_schemas.js +124 -0
  206. package/dist/core/tool_schemas.js.map +1 -0
  207. package/dist/core/which.d.ts +2 -0
  208. package/dist/core/which.d.ts.map +1 -0
  209. package/dist/core/which.js +35 -0
  210. package/dist/core/which.js.map +1 -0
  211. package/dist/goals/index.d.ts +9 -0
  212. package/dist/goals/index.d.ts.map +1 -0
  213. package/dist/goals/index.js +17 -0
  214. package/dist/goals/index.js.map +1 -0
  215. package/dist/goals/service.d.ts +115 -0
  216. package/dist/goals/service.d.ts.map +1 -0
  217. package/dist/goals/service.js +415 -0
  218. package/dist/goals/service.js.map +1 -0
  219. package/dist/goals/types.d.ts +47 -0
  220. package/dist/goals/types.d.ts.map +1 -0
  221. package/dist/goals/types.js +66 -0
  222. package/dist/goals/types.js.map +1 -0
  223. package/dist/index.d.ts +22 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +22 -0
  226. package/dist/index.js.map +1 -0
  227. package/dist/integrations/autonomous_startup.d.ts +51 -0
  228. package/dist/integrations/autonomous_startup.d.ts.map +1 -0
  229. package/dist/integrations/autonomous_startup.js +419 -0
  230. package/dist/integrations/autonomous_startup.js.map +1 -0
  231. package/dist/integrations/goose.d.ts +105 -0
  232. package/dist/integrations/goose.d.ts.map +1 -0
  233. package/dist/integrations/goose.js +408 -0
  234. package/dist/integrations/goose.js.map +1 -0
  235. package/dist/integrations/index.d.ts +21 -0
  236. package/dist/integrations/index.d.ts.map +1 -0
  237. package/dist/integrations/index.js +17 -0
  238. package/dist/integrations/index.js.map +1 -0
  239. package/dist/integrations/mcp.d.ts +73 -0
  240. package/dist/integrations/mcp.d.ts.map +1 -0
  241. package/dist/integrations/mcp.js +307 -0
  242. package/dist/integrations/mcp.js.map +1 -0
  243. package/dist/integrations/mempalace.d.ts +18 -0
  244. package/dist/integrations/mempalace.d.ts.map +1 -0
  245. package/dist/integrations/mempalace.js +25 -0
  246. package/dist/integrations/mempalace.js.map +1 -0
  247. package/dist/integrations/multi_agent.d.ts +82 -0
  248. package/dist/integrations/multi_agent.d.ts.map +1 -0
  249. package/dist/integrations/multi_agent.js +280 -0
  250. package/dist/integrations/multi_agent.js.map +1 -0
  251. package/dist/integrations/unified_memory.d.ts +415 -0
  252. package/dist/integrations/unified_memory.d.ts.map +1 -0
  253. package/dist/integrations/unified_memory.js +1489 -0
  254. package/dist/integrations/unified_memory.js.map +1 -0
  255. package/dist/roles/beru.yaml +48 -0
  256. package/dist/sidecar/client.d.ts +63 -0
  257. package/dist/sidecar/client.d.ts.map +1 -0
  258. package/dist/sidecar/client.js +222 -0
  259. package/dist/sidecar/client.js.map +1 -0
  260. package/dist/sidecar/index.d.ts +7 -0
  261. package/dist/sidecar/index.d.ts.map +1 -0
  262. package/dist/sidecar/index.js +6 -0
  263. package/dist/sidecar/index.js.map +1 -0
  264. package/dist/startup.d.ts +22 -0
  265. package/dist/startup.d.ts.map +1 -0
  266. package/dist/startup.js +194 -0
  267. package/dist/startup.js.map +1 -0
  268. package/dist/tools/_internal.d.ts +90 -0
  269. package/dist/tools/_internal.d.ts.map +1 -0
  270. package/dist/tools/_internal.js +375 -0
  271. package/dist/tools/_internal.js.map +1 -0
  272. package/dist/tools/_smoke.d.ts +2 -0
  273. package/dist/tools/_smoke.d.ts.map +1 -0
  274. package/dist/tools/_smoke.js +203 -0
  275. package/dist/tools/_smoke.js.map +1 -0
  276. package/dist/tools/axodex_tools.d.ts +51 -0
  277. package/dist/tools/axodex_tools.d.ts.map +1 -0
  278. package/dist/tools/axodex_tools.js +181 -0
  279. package/dist/tools/axodex_tools.js.map +1 -0
  280. package/dist/tools/code_tools.d.ts +33 -0
  281. package/dist/tools/code_tools.d.ts.map +1 -0
  282. package/dist/tools/code_tools.js +191 -0
  283. package/dist/tools/code_tools.js.map +1 -0
  284. package/dist/tools/computer_tools.d.ts +59 -0
  285. package/dist/tools/computer_tools.d.ts.map +1 -0
  286. package/dist/tools/computer_tools.js +348 -0
  287. package/dist/tools/computer_tools.js.map +1 -0
  288. package/dist/tools/file_tools.d.ts +51 -0
  289. package/dist/tools/file_tools.d.ts.map +1 -0
  290. package/dist/tools/file_tools.js +424 -0
  291. package/dist/tools/file_tools.js.map +1 -0
  292. package/dist/tools/index.d.ts +13 -0
  293. package/dist/tools/index.d.ts.map +1 -0
  294. package/dist/tools/index.js +13 -0
  295. package/dist/tools/index.js.map +1 -0
  296. package/dist/tools/shell_tools.d.ts +39 -0
  297. package/dist/tools/shell_tools.d.ts.map +1 -0
  298. package/dist/tools/shell_tools.js +163 -0
  299. package/dist/tools/shell_tools.js.map +1 -0
  300. package/dist/tools/web_tools.d.ts +31 -0
  301. package/dist/tools/web_tools.d.ts.map +1 -0
  302. package/dist/tools/web_tools.js +117 -0
  303. package/dist/tools/web_tools.js.map +1 -0
  304. package/dist/voice/index.d.ts +41 -0
  305. package/dist/voice/index.d.ts.map +1 -0
  306. package/dist/voice/index.js +58 -0
  307. package/dist/voice/index.js.map +1 -0
  308. package/dist/voice/light_daemon.d.ts +197 -0
  309. package/dist/voice/light_daemon.d.ts.map +1 -0
  310. package/dist/voice/light_daemon.js +746 -0
  311. package/dist/voice/light_daemon.js.map +1 -0
  312. package/dist/voice/setup.d.ts +63 -0
  313. package/dist/voice/setup.d.ts.map +1 -0
  314. package/dist/voice/setup.js +443 -0
  315. package/dist/voice/setup.js.map +1 -0
  316. package/dist/voice/stt.d.ts +148 -0
  317. package/dist/voice/stt.d.ts.map +1 -0
  318. package/dist/voice/stt.js +361 -0
  319. package/dist/voice/stt.js.map +1 -0
  320. package/dist/voice/tts.d.ts +168 -0
  321. package/dist/voice/tts.d.ts.map +1 -0
  322. package/dist/voice/tts.js +698 -0
  323. package/dist/voice/tts.js.map +1 -0
  324. package/dist/voice/voice_loop.d.ts +101 -0
  325. package/dist/voice/voice_loop.d.ts.map +1 -0
  326. package/dist/voice/voice_loop.js +463 -0
  327. package/dist/voice/voice_loop.js.map +1 -0
  328. package/dist/voice/wake_word.d.ts +153 -0
  329. package/dist/voice/wake_word.d.ts.map +1 -0
  330. package/dist/voice/wake_word.js +458 -0
  331. package/dist/voice/wake_word.js.map +1 -0
  332. package/dist/voice/wav.d.ts +99 -0
  333. package/dist/voice/wav.d.ts.map +1 -0
  334. package/dist/voice/wav.js +306 -0
  335. package/dist/voice/wav.js.map +1 -0
  336. package/dist/web/broker.d.ts +33 -0
  337. package/dist/web/broker.d.ts.map +1 -0
  338. package/dist/web/broker.js +58 -0
  339. package/dist/web/broker.js.map +1 -0
  340. package/dist/web/open_browser.d.ts +2 -0
  341. package/dist/web/open_browser.d.ts.map +1 -0
  342. package/dist/web/open_browser.js +42 -0
  343. package/dist/web/open_browser.js.map +1 -0
  344. package/dist/web/server.d.ts +54 -0
  345. package/dist/web/server.d.ts.map +1 -0
  346. package/dist/web/server.js +831 -0
  347. package/dist/web/server.js.map +1 -0
  348. package/dist/web/static/app.js +4121 -0
  349. package/dist/web/static/index.html +2281 -0
  350. package/dist/web/static/js/MODULE_SUMMARY.md +124 -0
  351. package/dist/web/static/js/a11y.js +165 -0
  352. package/dist/web/static/js/admin.js +2145 -0
  353. package/dist/web/static/js/assistant.js +475 -0
  354. package/dist/web/static/js/calendar/reminders.js +114 -0
  355. package/dist/web/static/js/calendar/utils.js +166 -0
  356. package/dist/web/static/js/calendar.js +3348 -0
  357. package/dist/web/static/js/censor.js +356 -0
  358. package/dist/web/static/js/chat.js +4546 -0
  359. package/dist/web/static/js/chatRenderer.js +2294 -0
  360. package/dist/web/static/js/chatStream.js +276 -0
  361. package/dist/web/static/js/codeRunner.js +398 -0
  362. package/dist/web/static/js/color/hex.js +14 -0
  363. package/dist/web/static/js/colorPicker.js +453 -0
  364. package/dist/web/static/js/compare/icons.js +77 -0
  365. package/dist/web/static/js/compare/index.js +1482 -0
  366. package/dist/web/static/js/compare/models.js +104 -0
  367. package/dist/web/static/js/compare/panes.js +817 -0
  368. package/dist/web/static/js/compare/probe.js +78 -0
  369. package/dist/web/static/js/compare/scoreboard.js +223 -0
  370. package/dist/web/static/js/compare/selector.js +1335 -0
  371. package/dist/web/static/js/compare/state.js +58 -0
  372. package/dist/web/static/js/compare/stream.js +695 -0
  373. package/dist/web/static/js/compare/vote.js +254 -0
  374. package/dist/web/static/js/cookbook-diagnosis.js +694 -0
  375. package/dist/web/static/js/cookbook-hwfit.js +1888 -0
  376. package/dist/web/static/js/cookbook.js +2093 -0
  377. package/dist/web/static/js/cookbookDownload.js +633 -0
  378. package/dist/web/static/js/cookbookProgressSignal.js +29 -0
  379. package/dist/web/static/js/cookbookRunning.js +3506 -0
  380. package/dist/web/static/js/cookbookServe.js +2128 -0
  381. package/dist/web/static/js/document.js +9731 -0
  382. package/dist/web/static/js/documentLibrary.js +3376 -0
  383. package/dist/web/static/js/dragSort.js +265 -0
  384. package/dist/web/static/js/editor/ai-inpaint.js +374 -0
  385. package/dist/web/static/js/editor/ai-models.js +273 -0
  386. package/dist/web/static/js/editor/ai-rembg.js +230 -0
  387. package/dist/web/static/js/editor/ai-tool-runner.js +147 -0
  388. package/dist/web/static/js/editor/ai-tools-misc.js +216 -0
  389. package/dist/web/static/js/editor/build/controls.js +366 -0
  390. package/dist/web/static/js/editor/build/popups.js +112 -0
  391. package/dist/web/static/js/editor/build/right-panel.js +200 -0
  392. package/dist/web/static/js/editor/build/toolbar.js +73 -0
  393. package/dist/web/static/js/editor/build/topbar.js +131 -0
  394. package/dist/web/static/js/editor/build/transform-popup.js +109 -0
  395. package/dist/web/static/js/editor/canvas-coords.js +21 -0
  396. package/dist/web/static/js/editor/canvas-events.js +197 -0
  397. package/dist/web/static/js/editor/canvas-transforms.js +132 -0
  398. package/dist/web/static/js/editor/checkerboard.js +24 -0
  399. package/dist/web/static/js/editor/clipboard-and-drop.js +133 -0
  400. package/dist/web/static/js/editor/composite-helpers.js +83 -0
  401. package/dist/web/static/js/editor/filters/blur.js +118 -0
  402. package/dist/web/static/js/editor/filters/edge-feather.js +70 -0
  403. package/dist/web/static/js/editor/fx/adj-popup.js +677 -0
  404. package/dist/web/static/js/editor/fx/filter-string.js +38 -0
  405. package/dist/web/static/js/editor/fx/histogram.js +67 -0
  406. package/dist/web/static/js/editor/fx/pixel-pass.js +249 -0
  407. package/dist/web/static/js/editor/harmonize-masks.js +116 -0
  408. package/dist/web/static/js/editor/history-panel.js +176 -0
  409. package/dist/web/static/js/editor/keyboard-shortcuts.js +271 -0
  410. package/dist/web/static/js/editor/layer-helpers.js +131 -0
  411. package/dist/web/static/js/editor/layer-panel.js +600 -0
  412. package/dist/web/static/js/editor/mask-utils.js +91 -0
  413. package/dist/web/static/js/editor/shortcuts-popover.js +149 -0
  414. package/dist/web/static/js/editor/slider-ux.js +191 -0
  415. package/dist/web/static/js/editor/snap.js +107 -0
  416. package/dist/web/static/js/editor/state.js +257 -0
  417. package/dist/web/static/js/editor/stroke-pipeline.js +162 -0
  418. package/dist/web/static/js/editor/stroke-tool-sliders.js +64 -0
  419. package/dist/web/static/js/editor/tools/clone.js +81 -0
  420. package/dist/web/static/js/editor/tools/crop.js +137 -0
  421. package/dist/web/static/js/editor/tools/flood-fill.js +72 -0
  422. package/dist/web/static/js/editor/tools/lasso-mask.js +171 -0
  423. package/dist/web/static/js/editor/tools/lasso.js +65 -0
  424. package/dist/web/static/js/editor/tools/move.js +79 -0
  425. package/dist/web/static/js/editor/tools/stroke.js +123 -0
  426. package/dist/web/static/js/editor/tools/transform-drag.js +174 -0
  427. package/dist/web/static/js/editor/tools/transform-handles.js +271 -0
  428. package/dist/web/static/js/editor/tools/transform-session.js +381 -0
  429. package/dist/web/static/js/editor/tools/wand.js +46 -0
  430. package/dist/web/static/js/editor/wire-import.js +148 -0
  431. package/dist/web/static/js/editor/wire-inpaint-controls.js +164 -0
  432. package/dist/web/static/js/editor/wire-merge-buttons.js +103 -0
  433. package/dist/web/static/js/editor/wire-selection-controls.js +170 -0
  434. package/dist/web/static/js/editor/wire-topbar-menus.js +174 -0
  435. package/dist/web/static/js/editor/wire-topbar-overflow.js +49 -0
  436. package/dist/web/static/js/editor/wire-topbar.js +188 -0
  437. package/dist/web/static/js/emailInbox.js +1245 -0
  438. package/dist/web/static/js/emailLibrary/replyRecipients.js +27 -0
  439. package/dist/web/static/js/emailLibrary/signatureFold.js +339 -0
  440. package/dist/web/static/js/emailLibrary/state.js +34 -0
  441. package/dist/web/static/js/emailLibrary/utils.js +202 -0
  442. package/dist/web/static/js/emailLibrary.js +5217 -0
  443. package/dist/web/static/js/emojiPicker.js +311 -0
  444. package/dist/web/static/js/escMenuStack.js +102 -0
  445. package/dist/web/static/js/fileHandler.js +295 -0
  446. package/dist/web/static/js/gallery.js +2835 -0
  447. package/dist/web/static/js/galleryEditor.js +3798 -0
  448. package/dist/web/static/js/group.js +955 -0
  449. package/dist/web/static/js/init.js +405 -0
  450. package/dist/web/static/js/keyboard-shortcuts.js +292 -0
  451. package/dist/web/static/js/langIcons.js +187 -0
  452. package/dist/web/static/js/markdown/tableRow.js +19 -0
  453. package/dist/web/static/js/markdown.js +806 -0
  454. package/dist/web/static/js/memory.js +1418 -0
  455. package/dist/web/static/js/modalManager.js +1552 -0
  456. package/dist/web/static/js/modalSnap.js +782 -0
  457. package/dist/web/static/js/model/matchKey.js +19 -0
  458. package/dist/web/static/js/modelPicker.js +695 -0
  459. package/dist/web/static/js/modelSort.js +33 -0
  460. package/dist/web/static/js/models.js +642 -0
  461. package/dist/web/static/js/notes.js +5054 -0
  462. package/dist/web/static/js/package.json +1 -0
  463. package/dist/web/static/js/platform.js +47 -0
  464. package/dist/web/static/js/presets.js +1110 -0
  465. package/dist/web/static/js/providers.js +93 -0
  466. package/dist/web/static/js/rag.js +177 -0
  467. package/dist/web/static/js/research/jobs.js +335 -0
  468. package/dist/web/static/js/research/panel.js +1233 -0
  469. package/dist/web/static/js/researchSynapse.js +225 -0
  470. package/dist/web/static/js/search-chat.js +201 -0
  471. package/dist/web/static/js/search.js +52 -0
  472. package/dist/web/static/js/section-management.js +260 -0
  473. package/dist/web/static/js/sessions.js +3108 -0
  474. package/dist/web/static/js/settings.js +4991 -0
  475. package/dist/web/static/js/sidebar-layout.js +573 -0
  476. package/dist/web/static/js/signature.js +504 -0
  477. package/dist/web/static/js/skills.js +1908 -0
  478. package/dist/web/static/js/slashAutocomplete.js +267 -0
  479. package/dist/web/static/js/slashCommands.js +6099 -0
  480. package/dist/web/static/js/spinner.js +406 -0
  481. package/dist/web/static/js/storage.js +123 -0
  482. package/dist/web/static/js/tasks.js +2699 -0
  483. package/dist/web/static/js/theme.js +2086 -0
  484. package/dist/web/static/js/tileManager.js +386 -0
  485. package/dist/web/static/js/tourAutoplay.js +133 -0
  486. package/dist/web/static/js/tourHints.js +179 -0
  487. package/dist/web/static/js/tts-ai.js +521 -0
  488. package/dist/web/static/js/ui.js +1288 -0
  489. package/dist/web/static/js/util/ordinal.js +13 -0
  490. package/dist/web/static/js/voiceRecorder.js +283 -0
  491. package/dist/web/static/js/windowDrag.js +326 -0
  492. package/dist/web/static/js/windowResize.js +233 -0
  493. package/dist/web/static/landing.html +824 -0
  494. package/dist/web/static/lib/docx.umd.min.js +20116 -0
  495. package/dist/web/static/lib/highlight.min.js +1213 -0
  496. package/dist/web/static/lib/html2pdf.bundle.min.js +3 -0
  497. package/dist/web/static/lib/mammoth.browser.min.js +21 -0
  498. package/dist/web/static/lib/qrcode.min.js +8 -0
  499. package/dist/web/static/lib/xlsx.full.min.js +24 -0
  500. package/dist/web/static/login.html +582 -0
  501. package/dist/web/static/manifest.json +15 -0
  502. package/dist/web/static/style.css +35733 -0
  503. package/dist/web/static/sw.js +144 -0
  504. package/dist/workflows/engine.d.ts +162 -0
  505. package/dist/workflows/engine.d.ts.map +1 -0
  506. package/dist/workflows/engine.js +625 -0
  507. package/dist/workflows/engine.js.map +1 -0
  508. package/dist/workflows/index.d.ts +8 -0
  509. package/dist/workflows/index.d.ts.map +1 -0
  510. package/dist/workflows/index.js +14 -0
  511. package/dist/workflows/index.js.map +1 -0
  512. package/package.json +112 -0
@@ -0,0 +1,1482 @@
1
+ // compare/index.js — orchestrator module (public API)
2
+ /**
3
+ * Model A/B Comparison module.
4
+ * Builds its own multi-pane grid layout (up to 8 models).
5
+ * Sends same prompt to all models in parallel, lets user vote.
6
+ *
7
+ * Uses show/hide on the original container children instead of
8
+ * innerHTML replacement, so event listeners on the input bar,
9
+ * compare button, mode toggle, etc. are preserved.
10
+ */
11
+
12
+ // ── Submodule imports ──
13
+ import state from './state.js';
14
+ import { EVAL_PROMPTS, WAVE_FRAMES,
15
+ ICON_DICE, ICON_EXPAND, ICON_COLLAPSE, ICON_CLOSE,
16
+ ICON_REROLL, ICON_COPY, ICON_PLAY, ICON_CODE,
17
+ ICON_PARALLEL, ICON_SEQUENTIAL,
18
+ EYE_OPEN, EYE_CLOSED, SAVE_ICON, CHAT_ICON,
19
+ SEND_SVG, VOTES_STORAGE_KEY,
20
+ } from './icons.js';
21
+ import { fetchModels, _persistSelections, _modelDisplayNames, getExcludedModels, setExcludedModels } from './models.js';
22
+ import { showModelSelector, disableToolToggles, restoreToolToggles, _syncToolbarIndicator } from './selector.js';
23
+ import { _checkUnprobed, _clearProbeWaves } from './probe.js';
24
+ import { streamToPane, _renderSearchResults, _runSynthForPane, _formatMs, registerStreamActions } from './stream.js';
25
+ import {
26
+ stopAll, stopPane, rerollPane, shufflePanePositions, resetCompare,
27
+ _addPane, _removePane, toggleExpandPane, togglePanePreview, copyPaneResponse,
28
+ _showModelSwapDropdown, _createAndAppendPane, _autoPreviewHtml,
29
+ registerPaneActions,
30
+ } from './panes.js';
31
+ import { handleVote, buildVoteBar, addFinishBadge, spawnConfetti, _saveVote, registerCompareActions } from './vote.js';
32
+ import { showScoreboard } from './scoreboard.js';
33
+
34
+ // ── External dependency imports ──
35
+ import Storage from '../storage.js';
36
+ import uiModule from '../ui.js';
37
+ import sessionModule from '../sessions.js';
38
+ import spinnerModule from '../spinner.js';
39
+ import themeModule from '../theme.js';
40
+ import presetsModule from '../presets.js';
41
+ import markdownModule from '../markdown.js';
42
+
43
+ var escapeHtml = uiModule.esc;
44
+
45
+ /** Slot label: letters (A, B) in parallel, numbers (1, 2) in sequential */
46
+ function _slotChar(i) { return state._parallel ? String.fromCharCode(65 + i) : String(i + 1); }
47
+
48
+ // ────────────────────────────────────────────────────────────────────────────
49
+ // ── Toolbar indicator sync ──
50
+ // ────────────────────────────────────────────────────────────────────────────
51
+ // ── init ──
52
+ // ────────────────────────────────────────────────────────────────────────────
53
+
54
+ function init(apiBase) {
55
+ state.API_BASE = apiBase;
56
+ // Clean up unsaved compare sessions on page close/refresh
57
+ window.addEventListener('beforeunload', () => {
58
+ if (!state._saveOnClose && state._paneSessionIds.length > 0) {
59
+ // sendBeacon uses POST — use the bulk delete endpoint
60
+ navigator.sendBeacon(
61
+ `${state.API_BASE}/api/sessions/bulk-delete`,
62
+ new Blob([JSON.stringify({ ids: state._paneSessionIds })], { type: 'application/json' })
63
+ );
64
+ }
65
+ });
66
+ }
67
+
68
+ // ────────────────────────────────────────────────────────────────────────────
69
+ // ── isCompareActive ──
70
+ // ────────────────────────────────────────────────────────────────────────────
71
+
72
+ function isCompareActive() {
73
+ return state.isActive;
74
+ }
75
+
76
+ // ────────────────────────────────────────────────────────────────────────────
77
+ // ── closeCompare ──
78
+ // ────────────────────────────────────────────────────────────────────────────
79
+
80
+ /** Close compare mode (public API for toolbar indicator). */
81
+ function closeCompare() {
82
+ if (state.isActive) deactivate(true);
83
+ }
84
+
85
+ // ────────────────────────────────────────────────────────────────────────────
86
+ // ── toggleMode ──
87
+ // ────────────────────────────────────────────────────────────────────────────
88
+
89
+ /** Toggle compare mode — shows model selector, then builds UI. */
90
+ async function toggleMode() {
91
+ if (state.isActive) {
92
+ deactivate(true);
93
+ return false;
94
+ }
95
+ if (state._openingSelector) return false;
96
+
97
+ state._openingSelector = true;
98
+ try {
99
+ const confirmed = await showModelSelector();
100
+ if (!confirmed) return false;
101
+
102
+ state.isActive = true;
103
+ _syncToolbarIndicator(true);
104
+ await _buildCompareUI();
105
+ return true;
106
+ } catch (err) {
107
+ console.error('Compare toggleMode error:', err);
108
+ return false;
109
+ } finally {
110
+ state._openingSelector = false;
111
+ }
112
+ }
113
+
114
+ // ────────────────────────────────────────────────────────────────────────────
115
+ // ── deactivate ──
116
+ // ────────────────────────────────────────────────────────────────────────────
117
+
118
+ async function deactivate(teardown) {
119
+ // Abort any in-flight streams
120
+ state._abortControllers.forEach(ac => { if (ac) ac.abort(); });
121
+ state._abortControllers = [];
122
+
123
+ // Move sessions to compare folder if saving
124
+ if (state._saveOnClose && state._paneSessionIds.length > 0) {
125
+ const modelShorts = _modelDisplayNames(state._selectedModels);
126
+ const folderName = 'Compare: ' + modelShorts.join(' vs ');
127
+ await Promise.all(state._paneSessionIds.map(sid =>
128
+ fetch(`${state.API_BASE}/api/session/${sid}`, {
129
+ method: 'PATCH', body: new URLSearchParams({ folder: folderName })
130
+ }).catch(() => {})
131
+ ));
132
+ }
133
+
134
+ // Capture session IDs to delete before resetting state
135
+ const sessionIdsToDelete = (!state._saveOnClose && teardown && state._paneSessionIds.length > 0)
136
+ ? [...state._paneSessionIds] : [];
137
+
138
+ removeOverlays();
139
+ state.isActive = false;
140
+ state._streaming = false;
141
+ state._paneSessionIds = [];
142
+ state._paneMetrics = [];
143
+ state._finishOrder = 0;
144
+ state._paneElapsed = [];
145
+ state._saveOnClose = false;
146
+ state._continueChat = false;
147
+ state._probed.clear();
148
+ state._expectedAnswer = '';
149
+ _syncToolbarIndicator(false);
150
+
151
+ // Restore main textarea placeholder
152
+ const msgTA = document.getElementById('message');
153
+ if (msgTA) msgTA.placeholder = '';
154
+
155
+ // Restore toolbar indicator display states and pointer events
156
+ Object.entries(state._savedIndicatorDisplay).forEach(([id, display]) => {
157
+ const el = document.getElementById(id);
158
+ if (el) { el.style.display = display; el.style.pointerEvents = ''; }
159
+ });
160
+ state._savedIndicatorDisplay = {};
161
+
162
+ // Unlock mode toggle
163
+ const _modeToggleR = document.querySelector('.mode-toggle');
164
+ if (_modeToggleR) { _modeToggleR.style.pointerEvents = ''; _modeToggleR.style.opacity = ''; }
165
+
166
+ // Restore tool toggle pointer events
167
+ ['overflow-plus-btn', 'web-toggle-btn', 'bash-toggle-btn'].forEach(id => {
168
+ const el = document.getElementById(id);
169
+ if (el) el.style.pointerEvents = '';
170
+ });
171
+
172
+ // Restore agent/chat mode to what it was before compare
173
+ const _ts = Storage.loadToggleState();
174
+ _ts.mode = state._savedMode;
175
+ Storage.saveToggleState(_ts);
176
+ const _ab2 = document.getElementById('mode-agent-btn'), _cb2 = document.getElementById('mode-chat-btn');
177
+ if (_ab2 && _cb2) { _ab2.classList.toggle('active', state._savedMode === 'agent'); _cb2.classList.toggle('active', state._savedMode === 'chat'); }
178
+ document.querySelectorAll('[data-mode-tool]').forEach(b => { b.style.display = state._savedMode === 'agent' ? '' : 'none'; });
179
+
180
+ // Delete unsaved sessions, then reload
181
+ if (teardown) {
182
+ if (sessionIdsToDelete.length > 0) {
183
+ // keepalive ensures requests complete even during page navigation
184
+ await Promise.all(sessionIdsToDelete.map(sid =>
185
+ fetch(`${state.API_BASE}/api/session/${sid}`, { method: 'DELETE', keepalive: true }).catch(() => {})
186
+ ));
187
+ }
188
+ location.href = location.pathname;
189
+ }
190
+ }
191
+
192
+ // ────────────────────────────────────────────────────────────────────────────
193
+ // ── _buildCompareUI ──
194
+ // ────────────────────────────────────────────────────────────────────────────
195
+
196
+ /** Build the compare UI: sessions, header bar, grid of panes, vote bar, eval dropdown. */
197
+ async function _buildCompareUI() {
198
+ if (state._selectedModels.length < 1) {
199
+ if (uiModule) uiModule.showError('Select at least 1 model');
200
+ return;
201
+ }
202
+
203
+ const n = state._selectedModels.length;
204
+ const modelShorts = _modelDisplayNames(state._selectedModels);
205
+ _persistSelections();
206
+
207
+ // 1. Create sessions (skip for search mode — no LLM sessions needed)
208
+ if (state._compareMode !== 'search') {
209
+ const sessionIds = [];
210
+ for (let i = 0; i < n; i++) {
211
+ const m = state._selectedModels[i];
212
+ const fd = new FormData();
213
+ // Blind mode: name the session by its neutral slot so the sidebar /
214
+ // GET /api/sessions can't de-anonymize the comparison (issue #1285).
215
+ fd.append('name', '[CMP] ' + (state._blindMode ? 'Model ' + _slotChar(i) : modelShorts[i]));
216
+ fd.append('endpoint_url', m.endpoint || '');
217
+ fd.append('model', m.model || '');
218
+ if (m.endpointId) {
219
+ fd.append('endpoint_id', m.endpointId);
220
+ fd.append('skip_validation', 'true');
221
+ }
222
+ const res = await fetch(`${state.API_BASE}/api/session`, { method: 'POST', body: fd });
223
+ if (!res.ok) throw new Error('Failed to create session for ' + modelShorts[i]);
224
+ const data = await res.json();
225
+ sessionIds.push(data.id);
226
+ }
227
+ state._paneSessionIds = sessionIds;
228
+ } else {
229
+ state._paneSessionIds = [];
230
+ }
231
+ state._paneMetrics = state._selectedModels.map(() => null);
232
+ state._abortControllers = state._selectedModels.map(() => null);
233
+
234
+ // 2. Auto-collapse sidebar if many panes
235
+ if (n > 3) {
236
+ const sidebar = document.getElementById('sidebar');
237
+ if (sidebar && !sidebar.classList.contains('hidden')) {
238
+ sidebar.classList.add('hidden');
239
+ state._sidebarWasHidden = true;
240
+ const iconRail = document.getElementById('icon-rail');
241
+ if (iconRail) iconRail.classList.remove('rail-hidden');
242
+ if (typeof window.syncRailSide === 'function') window.syncRailSide();
243
+ }
244
+ }
245
+
246
+ // 3. Hide mobile new-chat button during compare
247
+ const _mobileNewBtn = document.getElementById('mobile-new-chat-btn');
248
+ if (_mobileNewBtn) {
249
+ _mobileNewBtn.dataset.cmpWasDisplay = _mobileNewBtn.style.display;
250
+ _mobileNewBtn.style.display = 'none';
251
+ }
252
+
253
+ // 4. Save toolbar indicator display states before hiding
254
+ const indicatorIds = ['overflow-tts-btn', 'overflow-attach-btn', 'overflow-rag-btn', 'overflow-research-btn', 'overflow-doc-btn', 'rag-indicator-btn', 'research-toggle-btn'];
255
+ state._savedIndicatorDisplay = {};
256
+ indicatorIds.forEach(id => {
257
+ const el = document.getElementById(id);
258
+ if (el) state._savedIndicatorDisplay[id] = el.style.display;
259
+ });
260
+
261
+ // 5. Save current mode and lock to the right one for this compare type
262
+ const _toggleState = Storage.loadToggleState();
263
+ state._savedMode = _toggleState.mode || 'chat';
264
+ const _targetMode = (state._compareMode === 'agent') ? 'agent' : 'chat';
265
+ _toggleState.mode = _targetMode;
266
+ Storage.saveToggleState(_toggleState);
267
+ const _ab = document.getElementById('mode-agent-btn'), _cb = document.getElementById('mode-chat-btn');
268
+ if (_ab && _cb) {
269
+ _ab.classList.toggle('active', _targetMode === 'agent');
270
+ _cb.classList.toggle('active', _targetMode === 'chat');
271
+ }
272
+ const _modeToggle = document.querySelector('.mode-toggle');
273
+ if (_modeToggle) { _modeToggle.style.pointerEvents = 'none'; _modeToggle.style.opacity = '0.4'; }
274
+
275
+ // 6. Force tool toggles per compare mode
276
+ disableToolToggles();
277
+ if (state._compareMode === 'search') {
278
+ const webChk = document.getElementById('web-toggle');
279
+ if (webChk && !webChk.checked) { webChk.checked = true; webChk.dispatchEvent(new Event('change')); }
280
+ const webBtn = document.getElementById('web-toggle-btn');
281
+ if (webBtn) webBtn.classList.add('active');
282
+ } else if (state._compareMode === 'research') {
283
+ const resChk = document.getElementById('research-toggle');
284
+ if (resChk && !resChk.checked) { resChk.checked = true; resChk.dispatchEvent(new Event('change')); }
285
+ const resBtn = document.getElementById('research-toggle-btn');
286
+ if (resBtn) { resBtn.style.display = ''; resBtn.classList.add('active'); }
287
+ }
288
+
289
+ // 7. Hide existing chat container children (preserves event listeners)
290
+ const container = document.getElementById('chat-container');
291
+ state._compareElements = [];
292
+ Array.from(container.children).forEach(child => {
293
+ if (child.style.display === 'none') return;
294
+ child.dataset.cmpHidden = '1';
295
+ child.style.display = 'none';
296
+ });
297
+ container.classList.add('compare-active');
298
+
299
+ // 8. Header bar
300
+ const cols = Math.min(n, 4);
301
+ const headerBar = document.createElement('div');
302
+ headerBar.className = 'compare-header-bar';
303
+ headerBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:6px 10px;flex-shrink:0;';
304
+ const headerLabel = document.createElement('span');
305
+ headerLabel.style.cssText = 'font-size:10px;font-weight:400;color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;';
306
+ const _modeLabel = ({ search: ' search providers', agent: ' agents', research: ' research models' }[state._compareMode] || ' models');
307
+ headerLabel.textContent = 'Comparing' + _modeLabel + (state._blindMode ? ' (blind)' : '') + ' · ' + state._timeout + 's timeout';
308
+ // Left side: the Compare tool icon (two side-by-side panes, matching the
309
+ // rail/sidebar icon) + the label. Other tool headers carry their icon; this
310
+ // one was missing it.
311
+ const headerLeft = document.createElement('div');
312
+ headerLeft.style.cssText = 'display:flex;align-items:center;min-width:0;';
313
+ const headerIcon = document.createElement('span');
314
+ headerIcon.style.cssText = 'display:inline-flex;flex-shrink:0;margin-right:6px;opacity:0.85;';
315
+ headerIcon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="8" height="18" rx="1"/><rect x="14" y="3" width="8" height="18" rx="1"/></svg>';
316
+ headerLeft.appendChild(headerIcon);
317
+ headerLeft.appendChild(headerLabel);
318
+ headerBar.appendChild(headerLeft);
319
+
320
+ const headerActions = document.createElement('div');
321
+ headerActions.style.cssText = 'display:flex;align-items:center;gap:2px;';
322
+
323
+ const _btnCSS = 'background:none;border:1px solid var(--border);color:var(--fg);cursor:pointer;padding:3px 10px;font-size:11px;font-weight:600;opacity:0.7;transition:all 0.15s;line-height:1;border-radius:4px;display:inline-flex;align-items:center;font-family:inherit;';
324
+
325
+ const checkBtn = document.createElement('button');
326
+ checkBtn.id = 'compare-check-btn';
327
+ checkBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg><span style="font-size:11px;margin-left:3px;">Probe</span>';
328
+ checkBtn.title = 'Probe unverified models with a small test request';
329
+ checkBtn.style.cssText = _btnCSS;
330
+ checkBtn.addEventListener('click', () => _checkUnprobed());
331
+ headerActions.appendChild(checkBtn);
332
+
333
+ // Check button is dynamic: only visible when at least one selected model
334
+ // hasn't been probed yet. Show right after add/change, hide after success.
335
+ window._updateCheckBtnState = function() {
336
+ const btn = document.getElementById('compare-check-btn');
337
+ if (!btn) return;
338
+ const hasUnprobed = state._selectedModels.some(m => !state._probed.has(m.model));
339
+ btn.style.display = hasUnprobed ? '' : 'none';
340
+ };
341
+
342
+ // (Scoreboard button moved into the vote bar, next to Tie — see vote.js.)
343
+
344
+ const exportWrap = document.createElement('div');
345
+ exportWrap.style.cssText = 'position:relative;display:inline-flex;';
346
+ const exportBtn = document.createElement('button');
347
+ exportBtn.id = 'compare-export-btn';
348
+ exportBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span style="font-size:11px;margin-left:3px;">Export</span>';
349
+ exportBtn.title = 'Export options';
350
+ exportBtn.style.cssText = _btnCSS;
351
+ exportBtn.addEventListener('click', (e) => {
352
+ e.stopPropagation();
353
+ _toggleExportMenu(exportBtn);
354
+ });
355
+ exportWrap.appendChild(exportBtn);
356
+ headerActions.appendChild(exportWrap);
357
+
358
+ const shuffleBtn = document.createElement('button');
359
+ shuffleBtn.id = 'compare-shuffle-btn';
360
+ shuffleBtn.innerHTML = ICON_DICE + '<span style="font-size:11px;margin-left:3px;">Shuffle</span>';
361
+ shuffleBtn.title = 'Shuffle pane positions';
362
+ shuffleBtn.style.cssText = _btnCSS;
363
+ shuffleBtn.addEventListener('click', () => shufflePanePositions());
364
+ headerActions.appendChild(shuffleBtn);
365
+
366
+ const addBtn = document.createElement('button');
367
+ addBtn.id = 'compare-add-btn';
368
+ addBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span style="font-size:11px;margin-left:3px;">Add</span>';
369
+ addBtn.title = 'Add model pane';
370
+ addBtn.style.cssText = _btnCSS;
371
+ addBtn.addEventListener('click', () => _addPane(addBtn));
372
+ headerActions.appendChild(addBtn);
373
+
374
+ const closeBtn = document.createElement('button');
375
+ closeBtn.className = 'compare-close-btn';
376
+ closeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
377
+ closeBtn.title = 'Close compare mode';
378
+ // Match Export/Score/Shuffle/Model styling so the X sits flush with
379
+ // the rest of the toolbar instead of being a 24×24 bordered square.
380
+ closeBtn.style.cssText = _btnCSS;
381
+ closeBtn.addEventListener('click', () => deactivate(true));
382
+ headerActions.appendChild(closeBtn);
383
+
384
+ // Move Export to the far left of the action cluster (per user preference).
385
+ headerActions.insertBefore(exportWrap, headerActions.firstChild);
386
+
387
+ headerBar.appendChild(headerActions);
388
+ container.appendChild(headerBar);
389
+ state._compareElements.push(headerBar);
390
+
391
+ // Initial visibility — hidden if all current models are already probed
392
+ window._updateCheckBtnState();
393
+
394
+ // 9. Grid of panes
395
+ const grid = document.createElement('div');
396
+ grid.className = 'compare-grid';
397
+ grid.dataset.cols = String(cols);
398
+ for (let i = 0; i < n; i++) {
399
+ const label = state._blindMode ? 'Model ' + _slotChar(i) : modelShorts[i];
400
+ const pane = document.createElement('div');
401
+ pane.className = 'compare-pane';
402
+ pane.dataset.pane = String(i);
403
+ pane.innerHTML =
404
+ '<div class="pane-header">' +
405
+ '<button class="pane-title pane-title-btn" id="cmp-title-' + i + '" data-pane="' + i + '" type="button">' + escapeHtml(label) + ' <span class="pane-title-caret">&#x25BE;</span></button>' +
406
+ '<span class="pane-timer" id="cmp-timer-' + i + '"></span>' +
407
+ '<span class="pane-finish-badge" id="cmp-badge-' + i + '"></span>' +
408
+ '<div class="pane-actions">' +
409
+ '<button class="pane-action-btn pane-stop-btn" data-action="stop" data-pane="' + i + '" title="Stop" style="display:none;"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg></button>' +
410
+ '<button class="pane-action-btn pane-preview-btn" data-action="preview" data-pane="' + i + '" id="cmp-preview-' + i + '" title="Run preview" style="display:none;">' + ICON_PLAY + '</button>' +
411
+ '<button class="pane-action-btn" data-action="reroll" data-pane="' + i + '" title="Re-roll">' + ICON_REROLL + '</button>' +
412
+ '<button class="pane-action-btn" data-action="copy" data-pane="' + i + '" title="Copy">' + ICON_COPY + '</button>' +
413
+ '<button class="pane-action-btn" data-action="expand" data-pane="' + i + '" title="Expand">' + ICON_EXPAND + '</button>' +
414
+ '<button class="pane-action-btn pane-close-btn" data-action="close" data-pane="' + i + '" title="Remove pane">' + ICON_CLOSE + '</button>' +
415
+ '</div>' +
416
+ '</div>' +
417
+ '<div class="chat-history" id="cmp-history-' + i + '"></div>' +
418
+ '<iframe class="compare-pane-iframe" id="cmp-iframe-' + i + '" sandbox="allow-scripts" style="display:none;"></iframe>' +
419
+ '<div class="pane-vote-footer">' +
420
+ '<button class="pane-vote-btn" data-pane="' + i + '" type="button" disabled style="opacity:0.4;">' +
421
+ '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;vertical-align:-2px;"><polyline points="20 6 9 17 4 12"/></svg>' +
422
+ '<span class="pane-vote-label">Vote ' + escapeHtml(label) + '</span>' +
423
+ '</button>' +
424
+ '</div>';
425
+ grid.appendChild(pane);
426
+ }
427
+ grid.addEventListener('click', (e) => {
428
+ const voteBtn = e.target.closest('.pane-vote-btn');
429
+ if (voteBtn) {
430
+ e.stopPropagation();
431
+ if (voteBtn.disabled) return;
432
+ const idx = parseInt(voteBtn.dataset.pane);
433
+ handleVote(idx);
434
+ return;
435
+ }
436
+ const actionBtn = e.target.closest('.pane-action-btn');
437
+ if (actionBtn) {
438
+ e.stopPropagation();
439
+ const action = actionBtn.dataset.action;
440
+ const idx = parseInt(actionBtn.dataset.pane);
441
+ if (action === 'stop') stopPane(idx);
442
+ else if (action === 'copy') copyPaneResponse(idx);
443
+ else if (action === 'reroll') rerollPane(idx);
444
+ else if (action === 'expand') toggleExpandPane(idx, actionBtn);
445
+ else if (action === 'preview') togglePanePreview(idx);
446
+ else if (action === 'close') _removePane(idx);
447
+ return;
448
+ }
449
+ const titleBtn = e.target.closest('.pane-title-btn');
450
+ if (titleBtn) {
451
+ e.stopPropagation();
452
+ const idx = parseInt(titleBtn.dataset.pane);
453
+ _showModelSwapDropdown(idx, titleBtn);
454
+ }
455
+ });
456
+ container.appendChild(grid);
457
+ state._compareElements.push(grid);
458
+
459
+ // 10. Vote bar placeholder
460
+ const voteBar = document.createElement('div');
461
+ voteBar.id = 'compare-vote-bar';
462
+ voteBar.className = 'compare-vote-bar';
463
+ container.appendChild(voteBar);
464
+ state._compareElements.push(voteBar);
465
+ buildVoteBar(n);
466
+
467
+ if (state._blindMode && n > 1) shufflePanePositions();
468
+
469
+ // 11. Move chat input bar to the bottom of the container
470
+ const inputBar = document.querySelector('.chat-input-bar');
471
+ if (inputBar) {
472
+ inputBar.style.display = '';
473
+ if (inputBar.dataset.cmpHidden) delete inputBar.dataset.cmpHidden;
474
+ container.appendChild(inputBar);
475
+ }
476
+ const msgTA = document.getElementById('message');
477
+ if (msgTA) {
478
+ msgTA.placeholder = 'Enter prompt for all models...';
479
+ requestAnimationFrame(() => msgTA.focus());
480
+ }
481
+
482
+ // Eval-prompts picker — sits inside the message box at top-right (where
483
+ // model-picker normally lives). Model-picker is irrelevant during compare,
484
+ // so hide it and restore on deactivate via the wrap's _cleanup.
485
+ _setupEvalPicker();
486
+
487
+ // 12. Hide tool buttons that don't apply during compare
488
+ ['overflow-tts-btn', 'overflow-attach-btn', 'overflow-rag-btn', 'overflow-research-btn', 'overflow-doc-btn', 'rag-indicator-btn', 'web-toggle-btn', 'bash-toggle-btn', 'overflow-plus-btn'].forEach(id => {
489
+ const el = document.getElementById(id);
490
+ if (el) { el.style.display = 'none'; el.style.pointerEvents = 'none'; }
491
+ });
492
+ if (state._compareMode !== 'research') {
493
+ const resBtn = document.getElementById('research-toggle-btn');
494
+ if (resBtn) { resBtn.style.display = 'none'; resBtn.style.pointerEvents = 'none'; }
495
+ }
496
+ document.querySelectorAll('[data-mode-tool]').forEach(b => { b.style.display = 'none'; });
497
+
498
+ _setSendBtn('send');
499
+ }
500
+
501
+ // ────────────────────────────────────────────────────────────────────────────
502
+ // ── _setSendBtn ──
503
+ // ────────────────────────────────────────────────────────────────────────────
504
+
505
+ function _setSendBtn(mode) {
506
+ const btn = document.querySelector('.send-btn');
507
+ if (!btn) return;
508
+ if (mode === 'stop') {
509
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
510
+ btn.title = 'Stop all models';
511
+ btn.dataset.mode = 'streaming';
512
+ btn.classList.remove('mic-mode', 'newchat-mode');
513
+ } else {
514
+ btn.dataset.mode = '';
515
+ btn.innerHTML = SEND_SVG;
516
+ btn.style.color = '';
517
+ btn.title = 'Send to all models';
518
+ btn.classList.remove('mic-mode', 'newchat-mode', 'newchat-expanded');
519
+ }
520
+ }
521
+
522
+ // ────────────────────────────────────────────────────────────────────────────
523
+ // ── handleCompareSubmit ──
524
+ // ────────────────────────────────────────────────────────────────────────────
525
+
526
+ /**
527
+ * Handle submit from the main chat input while compare is active.
528
+ * Called by app.js submit guard.
529
+ */
530
+ function handleCompareSubmit(e) {
531
+ // If streaming, act as stop button
532
+ if (state._streaming) {
533
+ stopAll();
534
+ return;
535
+ }
536
+ const input = document.getElementById('message');
537
+ const message = input ? input.value.trim() : '';
538
+ if (!message) return;
539
+ input.value = '';
540
+ // Reset textarea height
541
+ input.style.height = '';
542
+ // Notify input listeners (eval-picker visibility, autosize, etc.) that the
543
+ // textarea is empty again — programmatic clears don't fire `input` natively.
544
+ input.dispatchEvent(new Event('input', { bubbles: true }));
545
+ // Mobile: dismiss the on-screen keyboard after the prompt is sent so the
546
+ // user sees the streaming output instead of the typing area. A plain blur()
547
+ // is often ignored on Firefox mobile, so toggle readonly around it (and blur
548
+ // the active element too) to reliably collapse the keyboard.
549
+ // Mobile keyboard dismiss — use the SAME proven logic as the main chat send
550
+ // (chat.js handleChatSubmit). Compare returns early in that flow, so it never
551
+ // reached this code; replicating it here is what actually works on Firefox
552
+ // mobile (readonly + blur, then drop readonly only once the blur is confirmed
553
+ // or the user taps to type again — avoids the keyboard bouncing back up).
554
+ if (window.innerWidth <= 768) {
555
+ try {
556
+ input.setAttribute('readonly', 'readonly');
557
+ input.blur();
558
+ // Setting readonly on an ALREADY-FOCUSED textarea doesn't dismiss the
559
+ // keyboard on Firefox, and blur() is often ignored — so the readonly-only
560
+ // approach works only when the input happened not to be focused at send
561
+ // time (inconsistent between 1st/2nd prompt). Deterministically pull focus
562
+ // off the textarea by focusing a throwaway readonly input, then drop it.
563
+ const tmp = document.createElement('input');
564
+ tmp.setAttribute('readonly', 'readonly');
565
+ tmp.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;border:0;padding:0;';
566
+ document.body.appendChild(tmp);
567
+ tmp.focus();
568
+ setTimeout(() => { try { tmp.blur(); tmp.remove(); } catch {} }, 50);
569
+ const _dropReadonly = () => { try { input.removeAttribute('readonly'); } catch {} };
570
+ setTimeout(() => {
571
+ if (document.activeElement === input) {
572
+ input.addEventListener('pointerdown', _dropReadonly, { once: true });
573
+ input.addEventListener('focus', _dropReadonly, { once: true });
574
+ } else {
575
+ _dropReadonly();
576
+ }
577
+ }, 120);
578
+ } catch {}
579
+ }
580
+ _executeCompare(message);
581
+ }
582
+
583
+ // ────────────────────────────────────────────────────────────────────────────
584
+ // ── _executeCompare ──
585
+ // ────────────────────────────────────────────────────────────────────────────
586
+
587
+ /**
588
+ * Send prompt to all panes, stream responses.
589
+ * Works for both first and follow-up messages.
590
+ */
591
+ async function _executeCompare(message) {
592
+ if (state._streaming) return;
593
+ if (state._selectedModels.length < 1) return;
594
+
595
+ // New round — allow voting again and clear the previous round's win/lose/tie
596
+ // styling (pane highlight + the Winner!/= title decorations), otherwise the
597
+ // old result stays stuck on the panes through the next prompt.
598
+ state._voted = false;
599
+ for (let i = 0; i < state._selectedModels.length; i++) {
600
+ const pane = document.querySelector('.compare-pane[data-pane="' + i + '"]');
601
+ if (pane) {
602
+ pane.classList.remove('winner', 'loser');
603
+ // Clear the previous round's Failed/Timeout badge and the eval ✓/✗ grade.
604
+ pane.querySelector('.pane-grade-badge')?.remove();
605
+ }
606
+ const fb = document.getElementById('cmp-badge-' + i);
607
+ if (fb) { fb.textContent = ''; fb.style.color = ''; }
608
+ const titleEl = document.getElementById('cmp-title-' + i);
609
+ if (titleEl) {
610
+ const label = state._blindMode
611
+ ? 'Model ' + _slotChar(i)
612
+ : ((state._selectedModels[i] && state._selectedModels[i].name) || 'Model ' + _slotChar(i));
613
+ titleEl.innerHTML = escapeHtml(label) + ' <span class="pane-title-caret">&#x25BE;</span>';
614
+ }
615
+ }
616
+
617
+ state._streaming = true;
618
+ state._lastPrompt = message;
619
+ _setSendBtn('stop');
620
+ // Disable header buttons during streaming
621
+ document.querySelectorAll('#compare-shuffle-btn, #compare-check-btn, #compare-add-btn').forEach(b => {
622
+ b.disabled = true; b.style.opacity = '0.25'; b.style.pointerEvents = 'none';
623
+ });
624
+
625
+ // ── Search mode: direct API calls, no SSE streaming ──
626
+ if (state._compareMode === 'search') {
627
+ try {
628
+ const n = state._selectedModels.length;
629
+
630
+ // Clear previous vote buttons on follow-up
631
+ const voteBar = document.getElementById('compare-vote-bar');
632
+ if (voteBar) voteBar.innerHTML = '';
633
+
634
+ // Add user query + spinner to each pane
635
+ for (let i = 0; i < n; i++) {
636
+ const hist = document.getElementById('cmp-history-' + i);
637
+ if (!hist) continue;
638
+ const userMsg = document.createElement('div');
639
+ userMsg.className = 'msg msg-user';
640
+ userMsg.innerHTML = '<div class="role">You</div><div class="body"></div>';
641
+ userMsg.querySelector('.body').textContent = message;
642
+ hist.appendChild(userMsg);
643
+
644
+ const aiMsg = document.createElement('div');
645
+ aiMsg.className = 'msg msg-ai';
646
+ aiMsg.innerHTML = '<div class="role">Search</div><div class="body"></div>';
647
+ const aiBody = aiMsg.querySelector('.body');
648
+ if (spinnerModule) {
649
+ const spinner = spinnerModule.create('Searching...', 'right');
650
+ aiBody.appendChild(spinner.createElement());
651
+ spinner.start();
652
+ }
653
+ hist.appendChild(aiMsg);
654
+ hist.scrollTop = hist.scrollHeight;
655
+ }
656
+
657
+ // Fire searches — parallel or sequential based on _parallel setting
658
+ const t0 = performance.now();
659
+ state._abortControllers = state._selectedModels.map(() => new AbortController());
660
+
661
+ async function _searchOne(m, i) {
662
+ const fd = new FormData();
663
+ fd.append('query', message);
664
+ fd.append('provider', m.model);
665
+ fd.append('count', '10');
666
+ try {
667
+ const res = await fetch(`${state.API_BASE}/api/search/query`, { method: 'POST', body: fd, signal: state._abortControllers[i].signal });
668
+ const data = await res.json();
669
+ return { idx: i, data };
670
+ } catch (err) {
671
+ return { idx: i, data: { results: [], error: err.name === 'AbortError' ? 'Stopped' : err.message } };
672
+ }
673
+ }
674
+
675
+ let results;
676
+ const _seqSynthDone = new Set();
677
+ if (state._parallel) {
678
+ results = await Promise.all(state._selectedModels.map((m, i) => _searchOne(m, i)));
679
+ } else {
680
+ // Sequential — run one at a time, dim waiting panes
681
+ results = [];
682
+ const panes = document.querySelectorAll('.compare-pane');
683
+ panes.forEach((p, i) => { if (i > 0) p.style.opacity = '0.4'; });
684
+ for (let i = 0; i < state._selectedModels.length; i++) {
685
+ const pane = panes[i];
686
+ if (pane) pane.style.opacity = '1';
687
+ results.push(await _searchOne(state._selectedModels[i], i));
688
+ // Render this result immediately
689
+ const { idx, data } = results[results.length - 1];
690
+ const hist = document.getElementById('cmp-history-' + idx);
691
+ if (hist) {
692
+ const aiMsg = hist.querySelector('.msg-ai:last-child');
693
+ if (aiMsg) {
694
+ const aiBody = aiMsg.querySelector('.body');
695
+ aiBody.innerHTML = '';
696
+ if (data.error) {
697
+ aiBody.innerHTML = '<div style="color:var(--color-error);font-size:0.85em;">Error: ' + escapeHtml(data.error) + '</div>';
698
+ } else if (!data.results || data.results.length === 0) {
699
+ aiBody.innerHTML = '<div style="color:color-mix(in srgb, var(--fg) 50%, transparent);font-size:0.85em;font-style:italic;">No results found</div>';
700
+ } else {
701
+ aiBody.appendChild(_renderSearchResults(data));
702
+ }
703
+ const footer = document.createElement('div'); footer.className = 'msg-footer';
704
+ const span = document.createElement('span'); span.className = 'response-metrics';
705
+ const parts = [];
706
+ if (data.results) parts.push(data.results.length + ' results');
707
+ if (data.time) parts.push(data.time + 's');
708
+ span.textContent = parts.join(' | '); footer.appendChild(span); aiMsg.appendChild(footer);
709
+ hist.scrollTop = hist.scrollHeight;
710
+ const _pe = document.querySelector(`.compare-pane[data-pane="${idx}"]`);
711
+ if (_pe) _pe.querySelectorAll('.pane-needs-response').forEach(b => b.style.display = '');
712
+ }
713
+ }
714
+ // Sequential: run synthesis for this pane immediately before moving to next
715
+ _seqSynthDone.add(idx);
716
+ if (!data.error && data.results && data.results.length > 0) {
717
+ const modelToUse = state._searchSynthModels?.[idx] || null;
718
+ if (modelToUse) {
719
+ const seqHist = document.getElementById('cmp-history-' + idx);
720
+ if (seqHist) {
721
+ const synthMsg = document.createElement('div');
722
+ synthMsg.className = 'msg msg-ai';
723
+ synthMsg.innerHTML = '<div class="role">Analysis</div><div class="body"></div>';
724
+ const synthBody = synthMsg.querySelector('.body');
725
+ let spinner = null;
726
+ if (spinnerModule) { spinner = spinnerModule.create('Analyzing...', 'right'); synthBody.appendChild(spinner.createElement()); spinner.start(); }
727
+ seqHist.appendChild(synthMsg);
728
+ seqHist.scrollTop = seqHist.scrollHeight;
729
+ const resultsText = data.results.map((r, ri) => `[${ri + 1}] ${r.title}\n${r.snippet || ''}\nURL: ${r.url}`).join('\n\n');
730
+ const synthPrompt = `Analyze these search results for the query "${message}". Summarize the key findings, note any consensus or conflicting information, and provide a brief synthesis.\n\nSearch Results:\n${resultsText}`;
731
+ await _runSynthForPane(modelToUse, synthPrompt, synthBody, spinner, seqHist);
732
+ }
733
+ }
734
+ }
735
+ }
736
+ // Reset opacity
737
+ panes.forEach(p => { p.style.opacity = ''; });
738
+ }
739
+ // Render results into each pane
740
+ for (const { idx, data } of results) {
741
+ const hist = document.getElementById('cmp-history-' + idx);
742
+ if (!hist) continue;
743
+ const aiMsg = hist.querySelector('.msg-ai:last-child');
744
+ if (!aiMsg) continue;
745
+ const aiBody = aiMsg.querySelector('.body');
746
+ aiBody.innerHTML = '';
747
+
748
+ if (data.error) {
749
+ aiBody.innerHTML = '<div style="color:var(--color-error);font-size:0.85em;">Error: ' + escapeHtml(data.error) + '</div>';
750
+ } else if (!data.results || data.results.length === 0) {
751
+ aiBody.innerHTML = '<div style="color:color-mix(in srgb, var(--fg) 50%, transparent);font-size:0.85em;font-style:italic;">No results found</div>';
752
+ } else {
753
+ aiBody.appendChild(_renderSearchResults(data));
754
+ }
755
+
756
+ // Footer metrics
757
+ const footer = document.createElement('div');
758
+ footer.className = 'msg-footer';
759
+ const span = document.createElement('span');
760
+ span.className = 'response-metrics';
761
+ const parts = [];
762
+ if (data.results) parts.push(data.results.length + ' results');
763
+ if (data.time) parts.push(data.time + 's');
764
+ span.textContent = parts.join(' | ');
765
+ footer.appendChild(span);
766
+ aiMsg.appendChild(footer);
767
+
768
+ hist.scrollTop = hist.scrollHeight;
769
+ // Show reroll/copy buttons for search results
770
+ const _paneEl = document.querySelector(`.compare-pane[data-pane="${idx}"]`);
771
+ if (_paneEl) _paneEl.querySelectorAll('.pane-needs-response').forEach(b => b.style.display = '');
772
+ }
773
+
774
+ // ── Synthesis: send results to LLM for analysis (respects _parallel setting) ──
775
+ if (state._searchSynthModels) {
776
+ // Build list of synthesis tasks
777
+ const synthTasks = [];
778
+ for (let i = 0; i < results.length; i++) {
779
+ const { idx, data } = results[i];
780
+ // Skip panes already synthesized in sequential mode
781
+ if (_seqSynthDone.has(idx)) continue;
782
+ if (data.error || !data.results || data.results.length === 0) continue;
783
+
784
+ const modelToUse = state._searchSynthModels?.[idx] || null;
785
+ if (!modelToUse) continue;
786
+
787
+ const hist = document.getElementById('cmp-history-' + idx);
788
+ if (!hist) continue;
789
+
790
+ // Add synthesis message with spinner
791
+ const synthMsg = document.createElement('div');
792
+ synthMsg.className = 'msg msg-ai';
793
+ synthMsg.innerHTML = '<div class="role">Analysis</div><div class="body"></div>';
794
+ const synthBody = synthMsg.querySelector('.body');
795
+ let spinner = null;
796
+ if (spinnerModule) {
797
+ spinner = spinnerModule.create('Analyzing...', 'right');
798
+ synthBody.appendChild(spinner.createElement());
799
+ spinner.start();
800
+ }
801
+ hist.appendChild(synthMsg);
802
+ // Auto-scroll to show the Analysis message
803
+ hist.scrollTop = hist.scrollHeight;
804
+
805
+ // Build synthesis prompt
806
+ const resultsText = data.results.map((r, ri) =>
807
+ `[${ri + 1}] ${r.title}\n${r.snippet || ''}\nURL: ${r.url}`
808
+ ).join('\n\n');
809
+
810
+ const synthPrompt = `Analyze these search results for the query "${message}". Summarize the key findings, note any consensus or conflicting information, and provide a brief synthesis.\n\nSearch Results:\n${resultsText}`;
811
+
812
+ synthTasks.push({ idx, modelToUse, synthBody, synthMsg, spinner, hist, synthPrompt });
813
+ }
814
+
815
+ // Run synthesis streams (parallel or sequential based on _parallel flag)
816
+ const runSynthesis = async (task) => _runSynthForPane(task.modelToUse, task.synthPrompt, task.synthBody, task.spinner, task.hist);
817
+
818
+ if (state._parallel) {
819
+ await Promise.all(synthTasks.map(runSynthesis));
820
+ } else {
821
+ for (const task of synthTasks) {
822
+ await runSynthesis(task);
823
+ }
824
+ }
825
+ }
826
+
827
+ buildVoteBar(n);
828
+ } catch (err) {
829
+ console.error('Search compare error:', err);
830
+ if (uiModule) uiModule.showError('Search compare failed: ' + err.message);
831
+ } finally {
832
+ state._streaming = false;
833
+ _setSendBtn('send');
834
+ }
835
+ return;
836
+ }
837
+
838
+ // ── Chat / Image mode ──
839
+ const isFollowUp = document.getElementById('cmp-history-0')?.querySelector('.msg-ai');
840
+
841
+ try {
842
+ const n = state._selectedModels.length;
843
+
844
+ if (isFollowUp) {
845
+ const voteBar = document.getElementById('compare-vote-bar');
846
+ if (voteBar) {
847
+ voteBar.innerHTML = '';
848
+ voteBar.classList.add('hidden');
849
+ }
850
+ }
851
+
852
+ // ── Add user + AI bubbles to each pane ──
853
+ const aiElements = [];
854
+ for (let i = 0; i < n; i++) {
855
+ const hist = document.getElementById('cmp-history-' + i);
856
+ if (!hist) { aiElements.push(null); continue; }
857
+
858
+ const userMsg = document.createElement('div');
859
+ userMsg.className = 'msg msg-user';
860
+ userMsg.innerHTML = '<div class="role">You</div><div class="body"></div>';
861
+ userMsg.querySelector('.body').textContent = message;
862
+ hist.appendChild(userMsg);
863
+
864
+ const aiMsg = document.createElement('div');
865
+ aiMsg.className = 'msg msg-ai';
866
+ aiMsg.innerHTML = '<div class="role">AI</div><div class="body"></div>';
867
+ const aiBody = aiMsg.querySelector('.body');
868
+ if (spinnerModule) {
869
+ // In sequential mode, only first pane says "Processing", rest say "Waiting"
870
+ const label = (!state._parallel && i > 0)
871
+ ? 'Waiting for Model ' + _slotChar(i - 1) + '...'
872
+ : 'Processing...';
873
+ const spinner = spinnerModule.create(label, 'right');
874
+ aiBody.appendChild(spinner.createElement());
875
+ spinner.start();
876
+ aiMsg._spinner = spinner;
877
+ }
878
+ hist.appendChild(aiMsg);
879
+ hist.scrollTop = hist.scrollHeight;
880
+ aiElements.push(aiMsg);
881
+ }
882
+
883
+ // ── Auto-extend timeout ──
884
+ const researchChk = document.getElementById('research-toggle');
885
+ const webChkT = document.getElementById('web-toggle');
886
+ const noTimeLimit = state._compareMode === 'research' || (researchChk && researchChk.checked);
887
+ const needsLongTimeout = state._compareMode === 'agent' || (webChkT && webChkT.checked);
888
+ const runTimeout = noTimeLimit ? 999999 : needsLongTimeout ? Math.max(state._timeout, 300) : state._timeout;
889
+
890
+ // ── Pre-search if web toggle is on (share same results across all panes) ──
891
+ let sharedSearchContext = null;
892
+ let sharedSearchSources = null;
893
+ const webChk = document.getElementById('web-toggle');
894
+ const toggleState = Storage.loadToggleState();
895
+ const isAgentMode = (toggleState.mode || 'chat') === 'agent';
896
+ const webOn = webChk && webChk.checked;
897
+ // In agent mode, web_search is a tool (handled per-pane); in chat mode, pre-search and share
898
+ if (webOn && !isAgentMode) {
899
+ try {
900
+ const fd = new FormData();
901
+ fd.append('query', message);
902
+ const searchRes = await fetch(`${state.API_BASE}/api/search`, { method: 'POST', body: fd });
903
+ if (searchRes.ok) {
904
+ const searchData = await searchRes.json();
905
+ if (searchData.context) sharedSearchContext = searchData.context;
906
+ if (searchData.sources) sharedSearchSources = searchData.sources;
907
+ }
908
+ } catch (err) {
909
+ console.warn('Compare pre-search failed, panes will search individually:', err);
910
+ }
911
+ }
912
+
913
+ // ── Show vote bar immediately so user can vote anytime ──
914
+ buildVoteBar(n);
915
+
916
+ // ── Stream all panes (parallel or sequential based on _parallel flag) ──
917
+ state._finishOrder = 0;
918
+ state._paneElapsed = new Array(n).fill(null);
919
+ state._paneMetrics = new Array(n).fill(null);
920
+ state._abortControllers = new Array(n).fill(null);
921
+
922
+ if (state._parallel) {
923
+ // Run all panes at once
924
+ await Promise.all(state._paneSessionIds.map((sid, i) =>
925
+ streamToPane(i, sid, message, aiElements[i], { searchContext: sharedSearchContext, timeout: runTimeout })
926
+ ));
927
+ } else {
928
+ // Run one pane at a time (sequential) — active pane full opacity, others dimmed
929
+ const allPanes = document.querySelectorAll('.compare-pane');
930
+ allPanes.forEach(p => { p.style.transition = 'opacity 0.4s ease'; });
931
+ // Dim all except first
932
+ allPanes.forEach((p, idx) => { p.style.opacity = idx === 0 ? '1' : '0.35'; });
933
+
934
+ for (let i = 0; i < state._paneSessionIds.length; i++) {
935
+ // Update spinner
936
+ if (aiElements[i] && aiElements[i]._spinner) {
937
+ aiElements[i]._spinner.updateLabel('Processing...');
938
+ }
939
+
940
+ await streamToPane(i, state._paneSessionIds[i], message, aiElements[i], { searchContext: sharedSearchContext, timeout: runTimeout });
941
+
942
+ // Swap opacity: dim current, brighten next
943
+ if (allPanes[i]) allPanes[i].style.opacity = '0.35';
944
+ if (i + 1 < allPanes.length && allPanes[i + 1]) {
945
+ allPanes[i + 1].style.opacity = '1';
946
+ }
947
+ }
948
+
949
+ // Restore all pane opacities when done
950
+ allPanes.forEach(p => { p.style.opacity = ''; p.style.transition = ''; });
951
+ }
952
+
953
+ // Re-focus main input for follow-up
954
+ if (state._continueChat) {
955
+ const ta = document.getElementById('message');
956
+ if (ta) ta.focus();
957
+ }
958
+
959
+ } catch (err) {
960
+ console.error('Compare error:', err);
961
+ if (uiModule) uiModule.showError('Compare failed: ' + err.message);
962
+ } finally {
963
+ state._streaming = false;
964
+ _setSendBtn('send');
965
+ // Re-enable header buttons
966
+ document.querySelectorAll('#compare-shuffle-btn, #compare-check-btn, #compare-add-btn').forEach(b => {
967
+ b.disabled = false; b.style.opacity = '0.7'; b.style.pointerEvents = '';
968
+ });
969
+ }
970
+ }
971
+
972
+ // showModelSelector imported from ./selector.js
973
+
974
+ // ────────────────────────────────────────────────────────────────────────────
975
+ // ── cleanupResults / removeOverlays ──
976
+ // ────────────────────────────────────────────────────────────────────────────
977
+
978
+ /**
979
+ * Build a markdown comparison of the current panes (prompt + each model's
980
+ * response + metrics + grade) and copy it to the clipboard. Lets users save
981
+ * or share a side-by-side at a glance.
982
+ */
983
+ // Build the comparison markdown string. Shared by all export paths.
984
+ function _buildComparisonMarkdown() {
985
+ const grid = document.querySelector('.compare-grid');
986
+ if (!grid) return null;
987
+ const panes = grid.querySelectorAll('.compare-pane');
988
+ if (!panes.length) return null;
989
+ const prompt = state._lastPrompt || '(no prompt yet — run a comparison first)';
990
+ const expected = state._expectedAnswer || '';
991
+ const date = new Date().toISOString().slice(0, 19).replace('T', ' ');
992
+ let md = '# Compare\n\n';
993
+ md += '**When:** ' + date + '\n';
994
+ md += '**Type:** ' + (state._compareMode || 'chat') + (state._blindMode ? ' (blind)' : '') + '\n';
995
+ md += '**Prompt:**\n\n```\n' + prompt + '\n```\n\n';
996
+ if (expected) md += '**Expected answer:** `' + expected + '`\n\n';
997
+ panes.forEach((pane, i) => {
998
+ const m = state._selectedModels[i];
999
+ const name = m ? (m.name || m.model) + (m.endpointName ? ' (' + m.endpointName + ')' : '') : 'Model ' + (i + 1);
1000
+ const body = pane.querySelector('.compare-text-content, .msg-body, .body');
1001
+ const text = body ? (body.innerText || body.textContent || '').trim() : '';
1002
+ const metrics = state._paneMetrics[i];
1003
+ const grade = pane.querySelector('.pane-grade-badge');
1004
+ const gradeMark = grade ? (grade.classList.contains('pass') ? ' ✓' : ' ✗') : '';
1005
+ md += '## ' + name + gradeMark + '\n\n';
1006
+ if (metrics) {
1007
+ const bits = [];
1008
+ if (metrics.output_tokens != null) bits.push(metrics.output_tokens + ' tokens');
1009
+ if (metrics.tokens_per_second != null) bits.push(metrics.tokens_per_second + ' tok/s');
1010
+ if (metrics.response_time != null) bits.push(metrics.response_time + 's');
1011
+ if (bits.length) md += '_' + bits.join(' · ') + '_\n\n';
1012
+ }
1013
+ md += text ? text + '\n\n' : '_(no response)_\n\n';
1014
+ md += '---\n\n';
1015
+ });
1016
+ return md;
1017
+ }
1018
+
1019
+ let _exportMenuEl = null;
1020
+ function _toggleExportMenu(btn) {
1021
+ if (_exportMenuEl) { _closeExportMenu(); return; }
1022
+ const r = btn.getBoundingClientRect();
1023
+ const m = document.createElement('div');
1024
+ m.className = 'compare-export-menu';
1025
+ m.style.cssText = 'position:fixed;z-index:10001;top:' + (r.bottom + 4) + 'px;left:' + r.left + 'px;background:var(--panel,var(--bg));border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);padding:4px;font-size:12px;display:flex;flex-direction:column;min-width:170px;';
1026
+ const opts = [
1027
+ { label: 'Copy as Markdown', fn: () => _exportCopyMarkdown(btn) },
1028
+ { label: 'Download .md', fn: () => _exportDownloadMarkdown() },
1029
+ { label: 'Print / Save PDF', fn: () => _exportPrint() },
1030
+ ];
1031
+ for (const o of opts) {
1032
+ const item = document.createElement('button');
1033
+ item.type = 'button';
1034
+ item.textContent = o.label;
1035
+ item.style.cssText = 'background:none;border:none;color:var(--fg);text-align:left;padding:8px 12px;border-radius:6px;cursor:pointer;font:inherit;font-size:12px;';
1036
+ item.addEventListener('mouseenter', () => { item.style.background = 'color-mix(in srgb, var(--fg) 8%, transparent)'; });
1037
+ item.addEventListener('mouseleave', () => { item.style.background = 'none'; });
1038
+ item.addEventListener('click', () => { _closeExportMenu(); o.fn(); });
1039
+ m.appendChild(item);
1040
+ }
1041
+ document.body.appendChild(m);
1042
+ _exportMenuEl = m;
1043
+ setTimeout(() => document.addEventListener('click', _closeExportMenu, { once: true }), 0);
1044
+ }
1045
+ function _closeExportMenu() {
1046
+ if (_exportMenuEl) { _exportMenuEl.remove(); _exportMenuEl = null; }
1047
+ }
1048
+
1049
+ async function _exportCopyMarkdown(_btn) {
1050
+ const md = _buildComparisonMarkdown();
1051
+ if (!md) return;
1052
+ try {
1053
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1054
+ await navigator.clipboard.writeText(md);
1055
+ } else {
1056
+ // Avoid the focus-stealing textarea fallback when the modern API
1057
+ // is available — that path briefly flashes the page as the
1058
+ // textarea is added/focused/removed.
1059
+ const ta = document.createElement('textarea');
1060
+ ta.value = md;
1061
+ ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
1062
+ document.body.appendChild(ta);
1063
+ ta.select(); document.execCommand('copy'); ta.remove();
1064
+ }
1065
+ try { window.uiModule?.showToast?.('Copied comparison to clipboard'); } catch {}
1066
+ } catch (e) {
1067
+ try { window.uiModule?.showToast?.('Copy failed'); } catch {}
1068
+ }
1069
+ }
1070
+
1071
+ function _exportDownloadMarkdown() {
1072
+ const md = _buildComparisonMarkdown();
1073
+ if (!md) return;
1074
+ const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
1075
+ const blob = new Blob([md], { type: 'text/markdown' });
1076
+ const url = URL.createObjectURL(blob);
1077
+ const a = document.createElement('a');
1078
+ a.href = url;
1079
+ a.download = 'compare-' + ts + '.md';
1080
+ document.body.appendChild(a);
1081
+ a.click();
1082
+ a.remove();
1083
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1084
+ }
1085
+
1086
+ function _exportPrint() {
1087
+ const md = _buildComparisonMarkdown();
1088
+ if (!md) return;
1089
+ // Render the markdown as a quick HTML view in a new window and trigger
1090
+ // the system print dialog — user can pick "Save as PDF" from there.
1091
+ const w = window.open('', '_blank');
1092
+ if (!w) return;
1093
+ const escape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1094
+ const html = '<!doctype html><meta charset="utf-8"><title>Compare export</title>' +
1095
+ '<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:780px;margin:32px auto;padding:0 24px;line-height:1.55;color:#222}' +
1096
+ 'pre{background:#f5f5f5;border-radius:6px;padding:10px;white-space:pre-wrap}' +
1097
+ 'h1{margin-top:0}h2{border-bottom:1px solid #ddd;padding-bottom:4px;margin-top:32px}' +
1098
+ 'hr{border:none;border-top:1px solid #ccc;margin:24px 0}' +
1099
+ '</style><body><pre style="background:none;padding:0">' + escape(md) + '</pre>' +
1100
+ '<script>window.onload=()=>setTimeout(()=>window.print(),100)<\/script>';
1101
+ w.document.write(html);
1102
+ w.document.close();
1103
+ }
1104
+
1105
+ async function _exportComparison(btn) {
1106
+ const grid = document.querySelector('.compare-grid');
1107
+ if (!grid) return;
1108
+ const panes = grid.querySelectorAll('.compare-pane');
1109
+ if (!panes.length) return;
1110
+
1111
+ const prompt = state._lastPrompt || '(no prompt yet — run a comparison first)';
1112
+ const expected = state._expectedAnswer || '';
1113
+ const date = new Date().toISOString().slice(0, 19).replace('T', ' ');
1114
+
1115
+ let md = '# Compare\n\n';
1116
+ md += '**When:** ' + date + '\n';
1117
+ md += '**Type:** ' + (state._compareMode || 'chat') + (state._blindMode ? ' (blind)' : '') + '\n';
1118
+ md += '**Prompt:**\n\n```\n' + prompt + '\n```\n\n';
1119
+ if (expected) md += '**Expected answer:** `' + expected + '`\n\n';
1120
+
1121
+ panes.forEach((pane, i) => {
1122
+ const m = state._selectedModels[i];
1123
+ const name = m ? (m.name || m.model) + (m.endpointName ? ' (' + m.endpointName + ')' : '') : 'Model ' + (i + 1);
1124
+ const body = pane.querySelector('.compare-text-content, .msg-body, .body');
1125
+ const text = body ? (body.innerText || body.textContent || '').trim() : '';
1126
+ const metrics = state._paneMetrics[i];
1127
+ const grade = pane.querySelector('.pane-grade-badge');
1128
+ const gradeMark = grade ? (grade.classList.contains('pass') ? ' ✓' : ' ✗') : '';
1129
+
1130
+ md += '## ' + name + gradeMark + '\n\n';
1131
+ if (metrics) {
1132
+ const bits = [];
1133
+ if (metrics.output_tokens != null) bits.push(metrics.output_tokens + ' tokens');
1134
+ if (metrics.tokens_per_second != null) bits.push(metrics.tokens_per_second + ' tok/s');
1135
+ if (metrics.response_time != null) bits.push(metrics.response_time + 's');
1136
+ if (bits.length) md += '_' + bits.join(' · ') + '_\n\n';
1137
+ }
1138
+ md += text ? text + '\n\n' : '_(no response)_\n\n';
1139
+ md += '---\n\n';
1140
+ });
1141
+
1142
+ // Copy to clipboard
1143
+ const origLabel = btn ? btn.innerHTML : '';
1144
+ try {
1145
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1146
+ await navigator.clipboard.writeText(md);
1147
+ } else {
1148
+ const ta = document.createElement('textarea');
1149
+ ta.value = md; document.body.appendChild(ta);
1150
+ ta.select(); document.execCommand('copy'); ta.remove();
1151
+ }
1152
+ if (btn) {
1153
+ btn.innerHTML = '<span style="font-size:11px;">Copied!</span>';
1154
+ setTimeout(() => { btn.innerHTML = origLabel; }, 1500);
1155
+ }
1156
+ } catch (e) {
1157
+ if (btn) {
1158
+ btn.innerHTML = '<span style="font-size:11px;color:var(--color-error);">Failed</span>';
1159
+ setTimeout(() => { btn.innerHTML = origLabel; }, 2000);
1160
+ }
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Build the eval-prompts picker — shown only during compare. Mirrors the
1166
+ * absolute-positioned model-picker location (top-right of .chat-input-top)
1167
+ * and is auto-cleaned up by the standard _compareElements teardown.
1168
+ */
1169
+ function _setupEvalPicker() {
1170
+ const inputTop = document.querySelector('.chat-input-top');
1171
+ if (!inputTop) return;
1172
+
1173
+ const escapeHtml = uiModule.esc;
1174
+
1175
+ // Hide the model-picker so eval-prompts can occupy the same slot
1176
+ const modelWrap = document.getElementById('model-picker-wrap');
1177
+ const prevModelDisplay = modelWrap ? modelWrap.style.display : '';
1178
+ if (modelWrap) modelWrap.style.display = 'none';
1179
+
1180
+ const wrap = document.createElement('div');
1181
+ wrap.className = 'cmp-eval-wrap';
1182
+ wrap.id = 'cmp-eval-wrap';
1183
+
1184
+ const btn = document.createElement('button');
1185
+ btn.type = 'button';
1186
+ btn.id = 'cmp-eval-btn';
1187
+ btn.className = 'cmp-eval-btn';
1188
+ btn.title = 'Insert an evaluation prompt';
1189
+ btn.innerHTML =
1190
+ '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'
1191
+ + '<span class="cmp-eval-label">Eval prompts</span>'
1192
+ + '<svg class="cmp-eval-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
1193
+
1194
+ const menu = document.createElement('div');
1195
+ menu.className = 'cmp-eval-menu hidden';
1196
+ menu.id = 'cmp-eval-menu';
1197
+
1198
+ function _renderItems() {
1199
+ const mode = state._compareMode || 'chat';
1200
+ // research/html aren't first-class compare types — fall back gracefully
1201
+ const key = EVAL_PROMPTS[mode] ? mode
1202
+ : (mode === 'research' ? 'search' : 'chat');
1203
+ const list = EVAL_PROMPTS[key] || [];
1204
+
1205
+ if (!list.length) {
1206
+ menu.innerHTML = '<div class="cmp-eval-empty">No prompts for this type</div>';
1207
+ return;
1208
+ }
1209
+ // Group by sub-category in original order
1210
+ const order = [];
1211
+ const groups = {};
1212
+ for (const p of list) {
1213
+ const sub = p.sub || 'Other';
1214
+ if (!groups[sub]) { groups[sub] = []; order.push(sub); }
1215
+ groups[sub].push(p);
1216
+ }
1217
+ let html = '';
1218
+ for (const sub of order) {
1219
+ html += '<div class="cmp-eval-group-label">' + escapeHtml(sub) + '</div>';
1220
+ for (const p of groups[sub]) {
1221
+ const data = encodeURIComponent(p.prompt);
1222
+ const ans = p.answer ? ' data-answer="' + encodeURIComponent(p.answer) + '"' : '';
1223
+ const checkMark = p.answer ? '<span class="cmp-eval-item-tick" title="Has expected answer">✓</span>' : '';
1224
+ html += '<button type="button" class="cmp-eval-item" data-prompt="' + data + '"' + ans + '>'
1225
+ + escapeHtml(p.label) + checkMark + '</button>';
1226
+ }
1227
+ }
1228
+ menu.innerHTML = html;
1229
+ menu.querySelectorAll('.cmp-eval-item').forEach(item => {
1230
+ item.addEventListener('click', (e) => {
1231
+ e.stopPropagation();
1232
+ const ta = document.getElementById('message');
1233
+ if (ta) {
1234
+ ta.value = decodeURIComponent(item.dataset.prompt);
1235
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
1236
+ ta.focus();
1237
+ }
1238
+ const ans = item.dataset.answer ? decodeURIComponent(item.dataset.answer) : '';
1239
+ _showExpectedAnswer(ans);
1240
+ menu.classList.add('hidden');
1241
+ });
1242
+ });
1243
+ }
1244
+
1245
+ btn.addEventListener('click', (e) => {
1246
+ e.stopPropagation();
1247
+ if (menu.classList.contains('hidden')) {
1248
+ _renderItems();
1249
+ menu.classList.remove('hidden');
1250
+ } else {
1251
+ menu.classList.add('hidden');
1252
+ }
1253
+ });
1254
+
1255
+ const _onDocClick = (e) => {
1256
+ if (!wrap.contains(e.target)) menu.classList.add('hidden');
1257
+ };
1258
+ document.addEventListener('click', _onDocClick);
1259
+
1260
+ wrap.appendChild(btn);
1261
+ wrap.appendChild(menu);
1262
+ inputTop.appendChild(wrap);
1263
+
1264
+ // Expected-answer chip — placed above the chat-input-bar (outside it), so
1265
+ // it floats over the compare grid right before the message box. Shows when
1266
+ // a graded prompt is picked so the eval-runner can verify model output.
1267
+ const hintChip = document.createElement('div');
1268
+ hintChip.className = 'cmp-eval-expected hidden';
1269
+ hintChip.id = 'cmp-eval-expected';
1270
+ hintChip.innerHTML =
1271
+ '<span class="cmp-eval-expected-label">Expected:</span>'
1272
+ + ' <strong class="cmp-eval-expected-value"></strong>'
1273
+ + ' <button type="button" class="cmp-eval-expected-close" title="Dismiss">×</button>';
1274
+ // Anchor the floating panel against the input bar (needs position:relative
1275
+ // — added via CSS rule on .chat-input-bar:has(.cmp-eval-expected) below).
1276
+ const inputBar = document.querySelector('.chat-input-bar');
1277
+ if (inputBar) {
1278
+ inputBar.appendChild(hintChip);
1279
+ } else {
1280
+ inputTop.appendChild(hintChip);
1281
+ }
1282
+ hintChip.querySelector('.cmp-eval-expected-close').addEventListener('click', (e) => {
1283
+ e.stopPropagation();
1284
+ hintChip.classList.add('hidden');
1285
+ state._expectedAnswer = '';
1286
+ });
1287
+
1288
+ function _showExpectedAnswer(answer) {
1289
+ state._expectedAnswer = answer || '';
1290
+ if (!answer) {
1291
+ hintChip.classList.add('hidden');
1292
+ return;
1293
+ }
1294
+ hintChip.querySelector('.cmp-eval-expected-value').textContent = answer;
1295
+ hintChip.classList.remove('hidden');
1296
+ }
1297
+
1298
+ // Hide the picker when the textarea has any user text (it's only useful
1299
+ // when starting fresh). Reappears when cleared. The expected-answer
1300
+ // chip stays put across sends — clearing it on every empty-textarea
1301
+ // tick wiped state._expectedAnswer before grading could read it, so
1302
+ // pane ✓/✗ badges never appeared. The chip is only cleared via its
1303
+ // own dismiss button (or when the user picks a new eval).
1304
+ const ta = document.getElementById('message');
1305
+ const _syncEvalVisibility = () => {
1306
+ const hasText = ta && ta.value.trim().length > 0;
1307
+ wrap.style.display = hasText ? 'none' : '';
1308
+ if (hasText) menu.classList.add('hidden');
1309
+ };
1310
+ if (ta) ta.addEventListener('input', _syncEvalVisibility);
1311
+ _syncEvalVisibility();
1312
+
1313
+ // Stash cleanup so cleanupResults() can detach the doc listener and
1314
+ // restore the model-picker when compare deactivates.
1315
+ wrap._cleanup = () => {
1316
+ document.removeEventListener('click', _onDocClick);
1317
+ if (ta) ta.removeEventListener('input', _syncEvalVisibility);
1318
+ if (modelWrap) modelWrap.style.display = prevModelDisplay || '';
1319
+ if (hintChip.parentNode) hintChip.remove();
1320
+ };
1321
+ state._compareElements.push(wrap);
1322
+ }
1323
+
1324
+ /** Remove compare UI elements and restore original view. */
1325
+ function cleanupResults() {
1326
+ // Remove all compare elements
1327
+ state._compareElements.forEach(el => {
1328
+ if (el._cleanup) el._cleanup();
1329
+ if (el._cleanupInput) el._cleanupInput();
1330
+ if (el.parentNode) el.remove();
1331
+ });
1332
+ state._compareElements = [];
1333
+
1334
+ // Remove any stray compare/probe overlays
1335
+ document.querySelectorAll('.compare-probe-overlay').forEach(el => el.remove());
1336
+
1337
+ // Restore sidebar
1338
+ if (state._sidebarWasHidden) {
1339
+ const sidebar = document.getElementById('sidebar');
1340
+ if (sidebar) sidebar.classList.remove('hidden');
1341
+ state._sidebarWasHidden = false;
1342
+ }
1343
+ const _mobileNewRestore = document.getElementById('mobile-new-chat-btn');
1344
+ if (_mobileNewRestore && _mobileNewRestore.dataset.cmpWasDisplay !== undefined) {
1345
+ _mobileNewRestore.style.display = _mobileNewRestore.dataset.cmpWasDisplay;
1346
+ delete _mobileNewRestore.dataset.cmpWasDisplay;
1347
+ }
1348
+ state._hasVisibleResults = false;
1349
+
1350
+ // Hard reload the page to cleanly restore all UI state
1351
+ window.location.reload();
1352
+ }
1353
+
1354
+ function removeOverlays() {
1355
+ const bar = document.getElementById('compare-vote-bar');
1356
+ if (bar) bar.remove();
1357
+ const modal = document.getElementById('compare-model-overlay');
1358
+ if (modal) modal.remove();
1359
+ const probe = document.querySelector('.compare-probe-overlay');
1360
+ if (probe) probe.remove();
1361
+ }
1362
+
1363
+ // ────────────────────────────────────────────────────────────────────────────
1364
+ // ── showShufflePoolEditor ──
1365
+ // ────────────────────────────────────────────────────────────────────────────
1366
+
1367
+ /** Shuffle pool editor — lets users exclude broken models from the dice. */
1368
+ async function showShufflePoolEditor() {
1369
+ let models;
1370
+ try { models = await fetchModels(); } catch (e) {
1371
+ if (uiModule) uiModule.showError('Failed to load models');
1372
+ return;
1373
+ }
1374
+
1375
+ const overlay = document.createElement('div');
1376
+ overlay.className = 'modal';
1377
+ overlay.id = 'shuffle-pool-overlay';
1378
+
1379
+ const content = document.createElement('div');
1380
+ content.className = 'modal-content';
1381
+ content.style.width = '420px';
1382
+
1383
+ const header = document.createElement('div');
1384
+ header.className = 'modal-header';
1385
+ header.innerHTML = '<h4>Shuffle Pool</h4>';
1386
+ const closeBtn = document.createElement('button');
1387
+ closeBtn.className = 'close-btn';
1388
+ closeBtn.innerHTML = '&#x2716;';
1389
+ closeBtn.addEventListener('click', () => overlay.remove());
1390
+ header.appendChild(closeBtn);
1391
+ content.appendChild(header);
1392
+
1393
+ const body = document.createElement('div');
1394
+ body.className = 'modal-body';
1395
+ body.style.padding = '12px 16px';
1396
+
1397
+ const desc = document.createElement('p');
1398
+ desc.style.cssText = 'color:color-mix(in srgb, var(--fg) 55%, transparent);font-size:0.85em;margin:0 0 12px;';
1399
+ desc.textContent = 'Uncheck models to exclude them from random shuffle. They can still be picked manually.';
1400
+ body.appendChild(desc);
1401
+
1402
+ const list = document.createElement('div');
1403
+ list.style.cssText = 'max-height:400px;overflow-y:auto;';
1404
+
1405
+ const excluded = getExcludedModels();
1406
+
1407
+ // Group by type
1408
+ const groups = { chat: [], image: [] };
1409
+ models.forEach(m => { if (groups[m.type]) groups[m.type].push(m); });
1410
+
1411
+ Object.entries(groups).forEach(([type, items]) => {
1412
+ if (items.length === 0) return;
1413
+ const heading = document.createElement('div');
1414
+ heading.style.cssText = 'font-size:0.78em;font-weight:600;color:color-mix(in srgb, var(--fg) 50%, transparent);text-transform:uppercase;letter-spacing:0.5px;padding:8px 4px 4px;';
1415
+ heading.textContent = type === 'chat' ? 'Chat Models' : 'Image Models';
1416
+ list.appendChild(heading);
1417
+
1418
+ items.forEach(m => {
1419
+ const row = document.createElement('label');
1420
+ row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 4px;cursor:pointer;font-size:0.85em;color:var(--fg);border-radius:4px;';
1421
+ row.addEventListener('mouseenter', () => { row.style.background = 'color-mix(in srgb, var(--fg) 4%, transparent)'; });
1422
+ row.addEventListener('mouseleave', () => { row.style.background = ''; });
1423
+ const chk = document.createElement('input');
1424
+ chk.type = 'checkbox';
1425
+ chk.checked = !excluded.includes(m.id);
1426
+ chk.addEventListener('change', () => {
1427
+ const exc = getExcludedModels();
1428
+ if (chk.checked) {
1429
+ const idx = exc.indexOf(m.id);
1430
+ if (idx >= 0) exc.splice(idx, 1);
1431
+ } else {
1432
+ if (!exc.includes(m.id)) exc.push(m.id);
1433
+ }
1434
+ setExcludedModels(exc);
1435
+ });
1436
+ const label = document.createElement('span');
1437
+ label.textContent = m.endpointName ? m.name + ' (' + m.endpointName + ')' : m.name;
1438
+ row.appendChild(chk);
1439
+ row.appendChild(label);
1440
+ list.appendChild(row);
1441
+ });
1442
+ });
1443
+
1444
+ body.appendChild(list);
1445
+ content.appendChild(body);
1446
+ overlay.appendChild(content);
1447
+ document.body.appendChild(overlay);
1448
+
1449
+ if (themeModule && themeModule.makeDraggable) {
1450
+ themeModule.makeDraggable(content, header);
1451
+ }
1452
+ }
1453
+
1454
+ // ────────────────────────────────────────────────────────────────────────────
1455
+ // ── Register cross-module callbacks ──
1456
+ // ────────────────────────────────────────────────────────────────────────────
1457
+
1458
+ registerCompareActions({ stopAll, resetCompare });
1459
+ registerStreamActions({ rerollPane, autoPreviewHtml: _autoPreviewHtml });
1460
+ registerPaneActions({ setSendBtn: _setSendBtn, deactivate, streamToPane, renderSearchResults: _renderSearchResults, fetchModels });
1461
+
1462
+ // ────────────────────────────────────────────────────────────────────────────
1463
+ // ── Public API ──
1464
+ // ────────────────────────────────────────────────────────────────────────────
1465
+
1466
+ export { EVAL_PROMPTS, showScoreboard, handleCompareSubmit };
1467
+
1468
+ const compareModule = {
1469
+ init,
1470
+ toggleMode,
1471
+ handleCompareSubmit,
1472
+ isActive: isCompareActive,
1473
+ hasVisibleResults: () => state._hasVisibleResults,
1474
+ deactivate,
1475
+ closeCompare,
1476
+ cleanupResults,
1477
+ showShufflePoolEditor,
1478
+ showScoreboard,
1479
+ };
1480
+
1481
+ export default compareModule;
1482
+ window.compareModule = compareModule;