agentworkshop 0.7.40 → 0.7.44

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 (327) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B8uy9iRW.js +1 -0
  3. package/.output/public/_nuxt/BGTYYQH-.js +1 -0
  4. package/.output/public/_nuxt/BUmHEJ77.js +2 -0
  5. package/.output/public/_nuxt/BY7YrV_D.js +1 -0
  6. package/.output/public/_nuxt/Bqcgz95s.js +18 -0
  7. package/.output/public/_nuxt/C7UlqPeD.js +1 -0
  8. package/.output/public/_nuxt/CTmDn-YP.js +1 -0
  9. package/.output/public/_nuxt/Cs8pVeVG.js +1 -0
  10. package/.output/public/_nuxt/{DcsCcxp4.js → D98NH8kG.js} +1 -1
  11. package/.output/public/_nuxt/DKySmNPI.js +14 -0
  12. package/.output/public/_nuxt/Di0NY_ht.js +1 -0
  13. package/.output/public/_nuxt/Du9v6qGB.js +1 -0
  14. package/.output/public/_nuxt/{C_iYD-EB.js → EHlEMRii.js} +1 -1
  15. package/.output/public/_nuxt/Eke1fs-U.js +1 -0
  16. package/.output/public/_nuxt/{DeJykBFf.js → H0BcqZtT.js} +1 -1
  17. package/.output/public/_nuxt/{C4oW1HsE.js → HSYEkkce.js} +1 -1
  18. package/.output/public/_nuxt/{DZshyNLa.js → IREZ1eC2.js} +1 -1
  19. package/.output/public/_nuxt/{D_OTvQza.js → J9oU00D1.js} +1 -1
  20. package/.output/public/_nuxt/OmpTerminalPanel.BxR5-lvi.css +1 -0
  21. package/.output/public/_nuxt/SSyAPaHc.js +1 -0
  22. package/.output/public/_nuxt/TownView.li4EMQxK.css +1 -0
  23. package/.output/public/_nuxt/VdLk4jLE2.js +1 -0
  24. package/.output/public/_nuxt/_id_.BhsXoEuk.css +1 -0
  25. package/.output/public/_nuxt/_id_.DQyj-WKN.css +1 -0
  26. package/.output/public/_nuxt/{_wsId_.B4WiqkIS.css → _wsId_.CTx-BlfF.css} +1 -1
  27. package/.output/public/_nuxt/aShsv_rG.js +19 -0
  28. package/.output/public/_nuxt/{yF0aYMF1.js → aesgXKSr.js} +3 -3
  29. package/.output/public/_nuxt/agents.B21nUEUX.css +1 -0
  30. package/.output/public/_nuxt/aml.ekTjxFkf.css +1 -0
  31. package/.output/public/_nuxt/builds/latest.json +1 -1
  32. package/.output/public/_nuxt/builds/meta/b5506ef0-f48e-4f14-a6c0-d65e505d0b09.json +1 -0
  33. package/.output/public/_nuxt/ca4TJ3Op.js +1 -0
  34. package/.output/public/_nuxt/channel-templates.DplM1pJ3.css +1 -0
  35. package/.output/public/_nuxt/daq.BdVmoIfe.css +1 -0
  36. package/.output/public/_nuxt/dcw.DCgyzpWV.css +1 -0
  37. package/.output/public/_nuxt/{default.J32SgqFY.css → default.Bh3L2HU8.css} +1 -1
  38. package/.output/public/_nuxt/lHAxdSsj.js +1 -0
  39. package/.output/public/_nuxt/pages.j9rzRocr.css +1 -0
  40. package/.output/public/_nuxt/plugins.DhiZbHU8.css +1 -0
  41. package/.output/public/_nuxt/settings.BRx9rIDg.css +1 -0
  42. package/.output/public/_nuxt/teams.BLlOPB_Z.css +1 -0
  43. package/.output/public/_nuxt/tokens.De8xzszn.css +1 -0
  44. package/.output/public/_nuxt/workshop.D1jFNkrn.css +1 -0
  45. package/.output/server/chunks/_/nitro.mjs +1058 -830
  46. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  47. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs → OmpTerminalPanel-CVvxpsF7.mjs} +15 -15
  48. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs.map → OmpTerminalPanel-CVvxpsF7.mjs.map} +1 -1
  49. package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs → TownView-CJ6IsDCc.mjs} +229 -228
  50. package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs.map → TownView-CJ6IsDCc.mjs.map} +1 -1
  51. package/.output/server/chunks/build/{_id_-BU1wfOID.mjs → _id_-CVD_H0ir.mjs} +149 -147
  52. package/.output/server/chunks/build/_id_-CVD_H0ir.mjs.map +1 -0
  53. package/.output/server/chunks/build/{_id_-BYsEua57.mjs → _id_-CtxMb3qp.mjs} +47 -46
  54. package/.output/server/chunks/build/_id_-CtxMb3qp.mjs.map +1 -0
  55. package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs → _wsId_-DpY3T-md.mjs} +516 -446
  56. package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs.map → _wsId_-DpY3T-md.mjs.map} +1 -1
  57. package/.output/server/chunks/build/{agents-CLQqJF2y.mjs → agents-DsCkuXDV.mjs} +49 -46
  58. package/.output/server/chunks/build/agents-DsCkuXDV.mjs.map +1 -0
  59. package/.output/server/chunks/build/{aml-_7FdFL2p.mjs → aml-CT0xKGKI.mjs} +122 -105
  60. package/.output/server/chunks/build/aml-CT0xKGKI.mjs.map +1 -0
  61. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs +117 -0
  62. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs.map +1 -0
  63. package/.output/server/chunks/build/{channel-templates-p6SZNjlM.mjs → channel-templates-Dla9FbYa.mjs} +49 -37
  64. package/.output/server/chunks/build/channel-templates-Dla9FbYa.mjs.map +1 -0
  65. package/.output/server/chunks/build/{daq-cgyjKmJu.mjs → daq-DWWSKGAV.mjs} +95 -93
  66. package/.output/server/chunks/build/daq-DWWSKGAV.mjs.map +1 -0
  67. package/.output/server/chunks/build/{dcw-CoTxBlAZ.mjs → dcw-CT8k17_E.mjs} +46 -45
  68. package/.output/server/chunks/build/dcw-CT8k17_E.mjs.map +1 -0
  69. package/.output/server/chunks/build/{default-BE4r0FZN.mjs → default-D3525UFX.mjs} +16 -15
  70. package/.output/server/chunks/build/{default-BE4r0FZN.mjs.map → default-D3525UFX.mjs.map} +1 -1
  71. package/.output/server/chunks/build/{logs-BSOeZwwI.mjs → logs--dByNyhw.mjs} +2 -2
  72. package/.output/server/chunks/build/{logs-BSOeZwwI.mjs.map → logs--dByNyhw.mjs.map} +1 -1
  73. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs → monitor-Dh2Kvaq5.mjs} +2 -2
  74. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs.map → monitor-Dh2Kvaq5.mjs.map} +1 -1
  75. package/.output/server/chunks/build/{pages-DFxjn9Xa.mjs → pages-BHcSBM2D.mjs} +26 -20
  76. package/.output/server/chunks/build/pages-BHcSBM2D.mjs.map +1 -0
  77. package/.output/server/chunks/build/{plugins-lGlUr1lj.mjs → plugins-BNgIi61W.mjs} +23 -17
  78. package/.output/server/chunks/build/plugins-BNgIi61W.mjs.map +1 -0
  79. package/.output/server/chunks/build/{settings-Ddd1qx2Z.mjs → settings-ipYBDWdn.mjs} +45 -44
  80. package/.output/server/chunks/build/settings-ipYBDWdn.mjs.map +1 -0
  81. package/.output/server/chunks/build/{teams-MEDLZdMn.mjs → teams-5lx-CwFa.mjs} +47 -44
  82. package/.output/server/chunks/build/teams-5lx-CwFa.mjs.map +1 -0
  83. package/.output/server/chunks/build/{tokens-C7fTM5C2.mjs → tokens-q-IDT8Io.mjs} +73 -53
  84. package/.output/server/chunks/build/tokens-q-IDT8Io.mjs.map +1 -0
  85. package/.output/server/chunks/build/{town-qMPql9Se.mjs → town-QJWB5wng.mjs} +5 -4
  86. package/.output/server/chunks/build/town-QJWB5wng.mjs.map +1 -0
  87. package/.output/server/chunks/build/{useDaqStream-CIBaiGAl.mjs → useDaqStream-DfCFpb3p.mjs} +9 -3
  88. package/.output/server/chunks/build/useDaqStream-DfCFpb3p.mjs.map +1 -0
  89. package/.output/server/chunks/build/{useDcwStream-CBbiYnWe.mjs → useDcwStream-UPiX-3iO.mjs} +11 -2
  90. package/.output/server/chunks/build/useDcwStream-UPiX-3iO.mjs.map +1 -0
  91. package/.output/server/chunks/build/{workshop-BL5bhfs0.mjs → workshop-BIlCj_ZP.mjs} +45 -45
  92. package/.output/server/chunks/build/workshop-BIlCj_ZP.mjs.map +1 -0
  93. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  94. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +3 -3
  95. package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +1 -1
  96. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
  97. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
  98. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +1 -1
  99. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +1 -1
  100. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
  101. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
  102. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  103. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
  104. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
  105. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
  106. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  107. package/.output/server/chunks/routes/api/workshop/index5.get.mjs +3 -1
  108. package/.output/server/chunks/routes/api/workshop/index5.get.mjs.map +1 -1
  109. package/.output/server/chunks/routes/api/workshop/index6.get.mjs +3 -1
  110. package/.output/server/chunks/routes/api/workshop/index6.get.mjs.map +1 -1
  111. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  112. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  113. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  114. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  115. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  116. package/.output/server/chunks/routes/renderer.mjs +2 -2
  117. package/.output/server/chunks/virtual/entry.mjs +639 -88
  118. package/.output/server/chunks/virtual/entry.mjs.map +1 -1
  119. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  120. package/.output/server/package.json +1 -1
  121. package/README-zh.md +16 -6
  122. package/README.md +17 -7
  123. package/app/components/AppHeader.vue +961 -959
  124. package/app/components/workshop/A2aRpcDebugger.vue +237 -237
  125. package/app/components/workshop/AgentInspectorDrawer.vue +424 -424
  126. package/app/components/workshop/AgentLanesView.vue +998 -998
  127. package/app/components/workshop/ArtifactCard.vue +120 -120
  128. package/app/components/workshop/AssetLibrary.vue +452 -452
  129. package/app/components/workshop/ChannelSessionList.vue +812 -804
  130. package/app/components/workshop/CommandPalette.vue +269 -269
  131. package/app/components/workshop/Composer.vue +827 -827
  132. package/app/components/workshop/DeviceTwinPanel.vue +643 -643
  133. package/app/components/workshop/EventBlock.vue +602 -602
  134. package/app/components/workshop/FileSelectorModal.vue +188 -188
  135. package/app/components/workshop/InspectorPanel.vue +301 -301
  136. package/app/components/workshop/MemoryPanel.vue +364 -364
  137. package/app/components/workshop/MultiChannelView.vue +189 -189
  138. package/app/components/workshop/TaskBoardView.vue +686 -686
  139. package/app/components/workshop/TaskInspectorDrawer.vue +390 -390
  140. package/app/components/workshop/TranscriptTimeline.vue +601 -601
  141. package/app/components/workshop/blocks/ClusterArtifact.vue +61 -61
  142. package/app/components/workshop/blocks/ClusterError.vue +85 -85
  143. package/app/components/workshop/blocks/ClusterLife.vue +87 -87
  144. package/app/components/workshop/blocks/ClusterMember.vue +95 -93
  145. package/app/components/workshop/blocks/ClusterMemory.vue +84 -84
  146. package/app/components/workshop/blocks/ClusterOther.vue +67 -67
  147. package/app/components/workshop/blocks/ClusterStatus.vue +149 -147
  148. package/app/components/workshop/blocks/ClusterTool.vue +158 -158
  149. package/app/components/workshop/terminal/OmpTerminalPanel.vue +1011 -1011
  150. package/app/components/workshop/town/TownView.vue +56 -56
  151. package/app/composables/workshop/useDaqStream.ts +7 -4
  152. package/app/composables/workshop/useDcwStream.ts +8 -2
  153. package/app/middleware/locale-cookie.global.ts +22 -0
  154. package/app/pages/aml/index.vue +24 -2
  155. package/app/pages/daq/[id].vue +20 -16
  156. package/app/pages/daq/index.vue +2310 -2307
  157. package/app/pages/dcw/[id].vue +26 -23
  158. package/app/pages/dcw/index.vue +3 -3
  159. package/app/pages/index.vue +987 -981
  160. package/app/pages/plugins/index.vue +317 -311
  161. package/app/pages/settings.vue +1564 -1562
  162. package/app/pages/tokens.vue +15 -15
  163. package/app/pages/workshop/agents.vue +612 -609
  164. package/app/pages/workshop/channel-templates.vue +18 -11
  165. package/app/pages/workshop/index.vue +668 -668
  166. package/app/pages/workshop/teams.vue +743 -740
  167. package/app/pages/workshop/w/[wsId].vue +704 -704
  168. package/app/plugins/locale-restore.client.ts +16 -1
  169. package/app/utils/builtin-catalog.ts +104 -0
  170. package/config.yml +170 -170
  171. package/docs/cli.en.md +2 -2
  172. package/docs/cli.md +2 -2
  173. package/docs/full-test-plan.md +2 -3
  174. package/docs/plugins.en.md +33 -1
  175. package/docs/plugins.md +29 -1
  176. package/docs/sdk.en.md +1 -1
  177. package/docs/sdk.md +1 -1
  178. package/i18n/locales/en.ts +293 -35
  179. package/i18n/locales/zh-CN.ts +265 -7
  180. package/package.json +4 -3
  181. package/scripts/_dbg-perms-e2e.mjs +132 -109
  182. package/scripts/_demo-agentteam-capture.mjs +278 -0
  183. package/scripts/_e2e-ui-functional.mjs +371 -0
  184. package/scripts/_i18n-en-cjk-scan.mjs +28 -0
  185. package/scripts/_i18n-en-verify.mjs +107 -0
  186. package/scripts/_i18n-extra-checks.mjs +53 -0
  187. package/scripts/_i18n-scan-hardcoded.mjs +180 -0
  188. package/scripts/_i18n-verify-keys.mjs +50 -0
  189. package/scripts/_loop-biax-campaign.mjs +130 -0
  190. package/scripts/_sweep-button-contrast.mjs +87 -0
  191. package/scripts/test-serial-bridge.mjs +418 -0
  192. package/scripts/ui/e2e-interactive.mjs +328 -0
  193. package/scripts/ui/record-demo.mjs +557 -0
  194. package/scripts/ui/seed-industrial-demo.mjs +171 -0
  195. package/server/api/workshop/daq/index.get.ts +3 -1
  196. package/server/api/workshop/dcw/index.get.ts +3 -0
  197. package/server/plugins/aw-plugins.ts +9 -7
  198. package/server/plugins-builtin/diag-bridge/index.mjs +608 -661
  199. package/server/plugins-builtin/rag-bridge/index.mjs +57 -133
  200. package/server/plugins-builtin/serial-bridge/client.mjs +210 -0
  201. package/server/plugins-builtin/serial-bridge/drivers.mjs +409 -0
  202. package/server/plugins-builtin/serial-bridge/i18n.json +62 -0
  203. package/server/plugins-builtin/serial-bridge/index.mjs +247 -0
  204. package/server/plugins-builtin/serial-bridge/lib.mjs +208 -0
  205. package/server/plugins-builtin/serial-bridge/rtu.mjs +168 -0
  206. package/server/services/workshop/daq/daq-controller.ts +7 -2
  207. package/server/services/workshop/daq/drivers.ts +89 -3
  208. package/server/services/workshop/dcw/dcw-controller.ts +5 -1
  209. package/server/services/workshop/dcw/drivers.ts +85 -2
  210. package/server/services/workshop/dcw/plugin-bridge.ts +45 -0
  211. package/server/services/workshop/plugins/host.mjs +30 -0
  212. package/shared/daq-protocol.ts +15 -0
  213. package/.output/public/_nuxt/88GOowgO2.js +0 -1
  214. package/.output/public/_nuxt/BOt2SP4z.js +0 -1
  215. package/.output/public/_nuxt/BP-NyKVG.js +0 -18
  216. package/.output/public/_nuxt/BXxH0UQE.js +0 -1
  217. package/.output/public/_nuxt/BsROmY89.js +0 -1
  218. package/.output/public/_nuxt/ClILjvWH.js +0 -1
  219. package/.output/public/_nuxt/CzOCSGLe.js +0 -1
  220. package/.output/public/_nuxt/D8qvkfyY.js +0 -1
  221. package/.output/public/_nuxt/DCn7QZ5N.js +0 -2
  222. package/.output/public/_nuxt/DS8qj8EL.js +0 -1
  223. package/.output/public/_nuxt/DUjW5Yq5.js +0 -1
  224. package/.output/public/_nuxt/Dihnimlk.js +0 -19
  225. package/.output/public/_nuxt/Dqw1lpr-.js +0 -1
  226. package/.output/public/_nuxt/Dr9v6C0f.js +0 -14
  227. package/.output/public/_nuxt/Du9ZJtnj.js +0 -1
  228. package/.output/public/_nuxt/OmpTerminalPanel.B_Pl5Ynm.css +0 -1
  229. package/.output/public/_nuxt/TownView.f-YOdXRs.css +0 -1
  230. package/.output/public/_nuxt/_id_.CM4bIgKY.css +0 -1
  231. package/.output/public/_nuxt/_id_.bNV69Dtf.css +0 -1
  232. package/.output/public/_nuxt/agents.ZD0rVdHE.css +0 -1
  233. package/.output/public/_nuxt/aml.BN8ZEW4h.css +0 -1
  234. package/.output/public/_nuxt/builds/meta/f4707884-7cf4-4a31-80d2-e3d72b73745f.json +0 -1
  235. package/.output/public/_nuxt/channel-templates.BOtahzPi.css +0 -1
  236. package/.output/public/_nuxt/daq.B0D3dPAc.css +0 -1
  237. package/.output/public/_nuxt/dcw.8Y7FqCVN.css +0 -1
  238. package/.output/public/_nuxt/pages.D7aSDlUI.css +0 -1
  239. package/.output/public/_nuxt/plugins.sb4nDGqr.css +0 -1
  240. package/.output/public/_nuxt/qr6R6Ha1.js +0 -1
  241. package/.output/public/_nuxt/settings.BTgR-NiE.css +0 -1
  242. package/.output/public/_nuxt/teams.CxnxKVVs.css +0 -1
  243. package/.output/public/_nuxt/tokens.BFY7qv4f.css +0 -1
  244. package/.output/public/_nuxt/workshop.CR_QzeAN.css +0 -1
  245. package/.output/server/chunks/build/_id_-BU1wfOID.mjs.map +0 -1
  246. package/.output/server/chunks/build/_id_-BYsEua57.mjs.map +0 -1
  247. package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +0 -1
  248. package/.output/server/chunks/build/aml-_7FdFL2p.mjs.map +0 -1
  249. package/.output/server/chunks/build/channel-templates-p6SZNjlM.mjs.map +0 -1
  250. package/.output/server/chunks/build/daq-cgyjKmJu.mjs.map +0 -1
  251. package/.output/server/chunks/build/dcw-CoTxBlAZ.mjs.map +0 -1
  252. package/.output/server/chunks/build/pages-DFxjn9Xa.mjs.map +0 -1
  253. package/.output/server/chunks/build/plugins-lGlUr1lj.mjs.map +0 -1
  254. package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +0 -1
  255. package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +0 -1
  256. package/.output/server/chunks/build/tokens-C7fTM5C2.mjs.map +0 -1
  257. package/.output/server/chunks/build/town-qMPql9Se.mjs.map +0 -1
  258. package/.output/server/chunks/build/useDaqStream-CIBaiGAl.mjs.map +0 -1
  259. package/.output/server/chunks/build/useDcwStream-CBbiYnWe.mjs.map +0 -1
  260. package/.output/server/chunks/build/workshop-BL5bhfs0.mjs.map +0 -1
  261. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +0 -1
  262. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +0 -1
  263. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +0 -153
  264. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +0 -153
  265. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +0 -431
  266. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +0 -180
  267. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +0 -112
  268. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +0 -227
  269. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +0 -131
  270. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +0 -160
  271. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +0 -157
  272. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +0 -348
  273. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +0 -296
  274. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +0 -285
  275. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +0 -217
  276. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +0 -301
  277. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +0 -151
  278. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +0 -135
  279. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +0 -156
  280. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +0 -72
  281. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +0 -469
  282. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +0 -259
  283. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +0 -97
  284. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +0 -277
  285. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +0 -171
  286. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +0 -425
  287. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +0 -99
  288. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +0 -325
  289. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +0 -332
  290. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +0 -43
  291. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +0 -154
  292. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +0 -531
  293. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +0 -439
  294. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +0 -323
  295. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +0 -433
  296. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +0 -280
  297. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +0 -117
  298. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +0 -160
  299. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +0 -101
  300. package/scripts/.mimosa/hook-status/sess_aeaecb58-8833-4b83-b93c-050467d21e03-d944d71bb3.json +0 -14
  301. package/scripts/_dbg-audit-all-pages.mjs +0 -43
  302. package/scripts/_dbg-daq-frames-e2e.mjs +0 -118
  303. package/scripts/_dbg-daq-rowstyle-verify.mjs +0 -112
  304. package/scripts/_dbg-hitl-timeout-e2e.mjs +0 -93
  305. package/scripts/_dbg-live-line-e2e.mjs +0 -279
  306. package/scripts/_dbg-minimal-omp.mjs +0 -77
  307. package/scripts/_dbg-mode-nav.mjs +0 -173
  308. package/scripts/_dbg-real-scenario-timeline.log +0 -55
  309. package/scripts/_dbg-team-repro.mjs +0 -76
  310. package/scripts/_dbg-town-agents-probe.mjs +0 -60
  311. package/scripts/_dbg-visual-acceptance.mjs +0 -63
  312. package/scripts/_townview_style.css +0 -1185
  313. package/scripts/_townview_template.html +0 -1215
  314. package/server/data/agent-node-bindings.json +0 -554
  315. package/server/data/daq-templates.json +0 -16
  316. package/server/data/daqs.json +0 -7464
  317. package/server/data/dcw-lines.json +0 -1787
  318. package/server/data/dcw-products.json +0 -1626
  319. package/server/data/dcw-recipes.json +0 -4672
  320. package/server/data/dcw-rollback.json +0 -20754
  321. package/server/data/dcw-runs.json +0 -6354
  322. package/server/data/dcw-templates.json +0 -146
  323. package/server/data/dcw-writes.json +0 -15650
  324. package/server/data/dcws.json +0 -9033
  325. package/server/data/device-twins.json +0 -133
  326. package/server/data/line-runs.json +0 -322
  327. package/server/data/scene-layouts.json +0 -277
@@ -1,601 +1,601 @@
1
- <script setup lang="ts">
2
- /**
3
- * Transcript 时间线:过滤条(全部/消息/任务/错误)+ cluster 块流 + 自动吸底。
4
- * 事件经 useClusteredBlocks 增量聚类(turn block,内容智能去重),
5
- * 流式更新只命中变化的块组件;新帧到达保持吸底。
6
- * 历史窗口:默认最近 200 帧(loadHistory);顶部"加载更早"按 beforeSeq 向上翻页。
7
- */
8
- import { useEventsStore, type EventFilter } from '@/app/stores/workshop/events'
9
- import { useClusteredBlocks } from '@/app/composables/workshop/useClusteredBlocks'
10
- import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
11
- import { useCodeCopy } from '@/app/composables/useCodeCopy'
12
-
13
- const { t } = useI18n()
14
-
15
- const props = defineProps<{ channelId: string }>()
16
- const events = useEventsStore()
17
- const { conn } = useWorkshopWs()
18
- // 代码块复制事件委托(文档级单例;时间线内所有 .code-copy 通用)
19
- useCodeCopy()
20
- const scroller = ref<HTMLElement | null>(null)
21
- const stickBottom = ref(true)
22
-
23
- /** 连接中骨架:WS 连接中且尚无块 → 按 block 行形态的 shimmer(open-tag skel 声部) */
24
- const connecting = computed(() =>
25
- conn.state === 'connecting' && events.ring(props.channelId).items.length === 0)
26
-
27
- const filter = computed({
28
- get: () => events.filters[props.channelId] ?? 'all',
29
- set: (v: EventFilter) => events.setFilter(props.channelId, v),
30
- })
31
-
32
- const { blocks, totalEvents } = useClusteredBlocks(() => props.channelId)
33
-
34
- // ===== 向上翻页历史 =====
35
- const loadingEarlier = ref(false)
36
- const earlierExhausted = ref(false)
37
- /** 已加载事件数 vs 持久化总量 → 是否可能还有更早(粗判,点击时由 loadEarlier 探底) */
38
- const loadedCount = computed(() => events.ring(props.channelId).items.length)
39
- const maybeMore = computed(() => !earlierExhausted.value && loadedCount.value > 0)
40
- const loadEarlier = async (): Promise<void> => {
41
- if (loadingEarlier.value || earlierExhausted.value) return
42
- loadingEarlier.value = true
43
- // 记住滚动位置:头部插入后保持视口锚定(不吸底跳变)
44
- const el = scroller.value
45
- const anchor = el ? { top: el.scrollTop, height: el.scrollHeight } : null
46
- try {
47
- const hasMore = await events.loadEarlier(props.channelId)
48
- if (!hasMore) earlierExhausted.value = true
49
- if (el && anchor) {
50
- await nextTick()
51
- el.scrollTop = el.scrollHeight - anchor.height + anchor.top
52
- }
53
- }
54
- catch {
55
- /* 历史拉取失败:保持按钮可重试,不打断时间线 */
56
- }
57
- finally {
58
- loadingEarlier.value = false
59
- }
60
- }
61
-
62
- const onScroll = (): void => {
63
- const el = scroller.value
64
- if (!el) return
65
- stickBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < 120
66
- }
67
-
68
- // ===== 回底动画(open-tag jump-bottom:800ms ease-out,动画期抑制按钮闪烁) =====
69
- const scrollingDown = ref(false)
70
- /** 结构化滚动容器(绕开 vue-tsc 双 lib.dom 的 Element 类型不兼容) */
71
- type ScrollBox = { scrollTop: number, scrollHeight: number, clientHeight: number }
72
- const animateScroll = (el: ScrollBox, ms: number): Promise<void> =>
73
- new Promise((resolve) => {
74
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
75
- el.scrollTop = el.scrollHeight
76
- resolve()
77
- return
78
- }
79
- const from = el.scrollTop
80
- const delta = el.scrollHeight - el.clientHeight - from
81
- const t0 = performance.now()
82
- const step = (t: number): void => {
83
- const k = Math.min(1, (t - t0) / ms)
84
- const eased = 1 - (1 - k) ** 3 // ease-out cubic
85
- el.scrollTop = from + delta * eased
86
- if (k < 1) requestAnimationFrame(step)
87
- else resolve()
88
- }
89
- requestAnimationFrame(step)
90
- })
91
-
92
- /** 跳回最新:滚底并恢复吸底(用户离开底部后新内容不再自动跟随) */
93
- const jumpToLatest = async (): Promise<void> => {
94
- stickBottom.value = true
95
- const el = scroller.value
96
- if (!el) return
97
- scrollingDown.value = true
98
- try {
99
- await animateScroll(el, 800)
100
- }
101
- finally {
102
- scrollingDown.value = false
103
- }
104
- }
105
-
106
- // ===== 新块进场编排(open-tag motion charter 移植) =====
107
- /**
108
- * 只有实时新到的尾部块做进场动画(60ms stagger,600ms burst 窗口,上限 8 条);
109
- * 整体重建(过滤切换/聚焦/历史回填——一次出现大量新 id)与组件首载直接显示,
110
- * 不动画不延迟。映射:blockId → 进场延迟 ms;-1 = 不动画。
111
- */
112
- type Stage = { enter: boolean, delay: number }
113
- const staged = ref(new Map<string, Stage>())
114
- let burstAt = 0
115
- let burstN = 0
116
- const mountedAt = Date.now()
117
- watch(blocks, (list, prev) => {
118
- const prevIds = new Set((prev ?? []).map(b => b.id))
119
- const fresh = list.filter(b => !prevIds.has(b.id))
120
- const map = new Map<string, Stage>()
121
- const wholesale = fresh.length === 0
122
- || fresh.length > Math.max(8, Math.ceil(list.length * 0.4))
123
- || Date.now() - mountedAt < 1500
124
- if (!wholesale) {
125
- const now = Date.now()
126
- if (now - burstAt > 600) {
127
- burstAt = now
128
- burstN = 0
129
- }
130
- }
131
- for (const b of list) {
132
- const isFresh = !prevIds.has(b.id)
133
- const delay = isFresh && !wholesale ? Math.min(burstN++, 7) * 60 : -1
134
- map.set(b.id, { enter: delay >= 0, delay: Math.max(0, delay) })
135
- }
136
- staged.value = map
137
- }, { immediate: true })
138
-
139
- /** 吸底:新块出现 / seq 增长 / 块内容尺寸变化(可能高增)后滚到底 */
140
- const scrollToBottom = async (): Promise<void> => {
141
- if (!stickBottom.value) return
142
- await nextTick()
143
- const el = scroller.value
144
- if (el) el.scrollTop = el.scrollHeight
145
- }
146
- watch(() => blocks.value.length, () => {
147
- void scrollToBottom()
148
- })
149
- watch(() => events.lastSeq(props.channelId), () => {
150
- void scrollToBottom()
151
- })
152
-
153
- // 内容高度增长吸底:同一流块 delta 打字机让块变高(块数与 seq 不变或合并帧不推 seq),
154
- // ResizeObserver 观测内容列高度变化补一次滚底 —— 长输出不再"卡"在中途。
155
- // (观察器以结构化类型声明:绕开 vue-tsc 双 lib.dom 下 Element 类型不兼容问题)
156
- const columnEl = ref<HTMLElement | null>(null)
157
- let contentObserver: { disconnect(): void } | null = null
158
- onMounted(() => {
159
- const el = scroller.value
160
- const column = columnEl.value
161
- if (!el || !column || typeof ResizeObserver === 'undefined') return
162
- const Observer = ResizeObserver as unknown as
163
- new (cb: () => void) => { observe(target: unknown): void, disconnect(): void }
164
- const observer = new Observer(() => {
165
- if (stickBottom.value) el.scrollTop = el.scrollHeight
166
- })
167
- observer.observe(column)
168
- contentObserver = observer
169
- })
170
- onBeforeUnmount(() => {
171
- contentObserver?.disconnect()
172
- contentObserver = null
173
- })
174
-
175
- /** 断线待对齐(诚实连接态:open-tag 规范——不假装在线,提示同步中) */
176
- const syncing = computed(() => conn.pendingReplay || conn.state === 'connecting')
177
- /** 最后数据时间(x 秒前;无数据返回空) */
178
- const lastDataAgo = computed(() => {
179
- if (!conn.lastDataAt) return ''
180
- const s = Math.max(0, Math.round((Date.now() - conn.lastDataAt) / 1000))
181
- return s < 5 ? t('transcriptTimeline.k3wwxl026') : t('transcriptTimeline.k2gyl6l027', { p0: s })
182
- })
183
-
184
- const filterOptions: Array<{ value: EventFilter, label: string }> = [
185
- { value: 'all', label: t('transcriptTimeline.k3x4t1012') },
186
- { value: 'key', label: t('transcriptTimeline.k3x5xi013') },
187
- { value: 'messages', label: t('transcriptTimeline.k41ykc014') },
188
- { value: 'tasks', label: t('transcriptTimeline.k3wcox015') },
189
- { value: 'team', label: t('transcriptTimeline.k3y5ja016') },
190
- { value: 'errors', label: t('transcriptTimeline.k49d2l017') },
191
- ]
192
-
193
- // ===== 日期分隔线(open-tag date-divider 移植):块 firstAt 跨日 → 插入 hairline 分隔 =====
194
- const startOfDay = (iso: string): number => {
195
- const d = new Date(iso)
196
- return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
197
- }
198
- const dayLabel = (iso: string): string => {
199
- const d = new Date(iso)
200
- const today = new Date()
201
- const yesterday = new Date()
202
- yesterday.setDate(today.getDate() - 1)
203
- const sameDay = (a: Date, b: Date) =>
204
- a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
205
- if (sameDay(d, today)) return t('transcriptTimeline.k3wcqg018')
206
- if (sameDay(d, yesterday)) return t('transcriptTimeline.k40l1y019')
207
- const y = d.getFullYear() !== today.getFullYear() ? t('transcriptTimeline.k2hda35028', { p0: d.getFullYear() }) : ''
208
- return t('transcriptTimeline.kbs29zx029', { p0: y, p1: d.getMonth() + 1, p2: d.getDate() })
209
- }
210
- /** 每个块是否需要前置日界分隔(与上一块不同日,或首块) */
211
- const blockDayFlags = computed(() => {
212
- const flags: Array<{ divider: string | null }> = []
213
- let prevDay: number | null = null
214
- for (const b of blocks.value) {
215
- const day = startOfDay(b.firstAt)
216
- flags.push({ divider: prevDay === null || day !== prevDay ? dayLabel(b.firstAt) : null })
217
- prevDay = day
218
- }
219
- return flags
220
- })
221
- </script>
222
-
223
- <template>
224
- <div class="transcript">
225
- <div class="filter-bar">
226
- <a-segmented
227
- v-model:value="filter"
228
- size="small"
229
- :options="filterOptions"
230
- />
231
- <span class="count">{{ totalEvents }} {{ $t('transcriptTimeline.k1atjpx1005') }} {{ blocks.length }} {{ $t('transcriptTimeline.k4a9o007') }}</span>
232
- <!-- 连接诚实态:断线待对齐 → 同步中脉搏;在线 → 最后数据时间(open-tag 规范) -->
233
- <span
234
- class="sync-chip"
235
- :data-state="syncing ? 'syncing' : conn.state"
236
- :title="syncing ? '连接中断,重连后将自动对齐缺失事件' : $t('transcriptTimeline.kglov3b025', { p0: lastDataAgo || '无' })"
237
- >
238
- <span
239
- v-if="syncing"
240
- class="sync-pulse"
241
- />
242
- <span
243
- v-else-if="conn.state === 'open'"
244
- class="i-tabler-point-filled sync-dot"
245
- />
246
- {{ syncing ? $t('transcriptTimeline.k3lmtk3008') : conn.state === 'open' ? lastDataAgo || $t('transcriptTimeline.k3y2p8020') : $t('transcriptTimeline.k44c2n024') }}
247
- </span>
248
- </div>
249
- <div
250
- ref="scroller"
251
- class="scroller"
252
- @scroll="onScroll"
253
- >
254
- <!-- 离底时的跳转最新悬浮按钮(流式新内容到达不强制跟随,点击回底;动画期抑制闪烁) -->
255
- <button
256
- v-if="!stickBottom && !scrollingDown"
257
- class="jump-latest"
258
- :title="$t('transcriptTimeline.kqu93z8001')"
259
- @click="jumpToLatest"
260
- >
261
- <span class="i-tabler-arrow-down" />
262
- {{ $t('transcriptTimeline.k40t11002') }}
263
- </button>
264
- <div
265
- ref="columnEl"
266
- class="column"
267
- >
268
- <button
269
- v-if="maybeMore"
270
- class="earlier-btn"
271
- :disabled="loadingEarlier"
272
- @click="loadEarlier"
273
- >
274
- {{ loadingEarlier ? $t('transcriptTimeline.k1br0ij9009') : $t('transcriptTimeline.kywgd38021') }}
275
- </button>
276
- <div
277
- v-if="earlierExhausted && loadedCount > 0"
278
- class="earlier-done"
279
- >
280
- {{ $t('transcriptTimeline.k1kbaden003') }}
281
- </div>
282
- <!-- 连接中骨架:头像圆 + 双行占位,布局对齐最终块形态(免跳变) -->
283
- <div
284
- v-if="connecting"
285
- class="skel-stack"
286
- >
287
- <div
288
- v-for="n in 3"
289
- :key="n"
290
- class="skel-row"
291
- :style="{ '--d': `${(n - 1) * 0.12}s` }"
292
- >
293
- <span class="aw-skel skel-ava" />
294
- <div class="skel-lines">
295
- <span class="aw-skel skel-line name" />
296
- <span class="aw-skel skel-line w70" />
297
- <span class="aw-skel skel-line w45" />
298
- </div>
299
- </div>
300
- </div>
301
- <div
302
- v-else-if="blocks.length === 0 && totalEvents === 0"
303
- class="empty"
304
- >
305
- <!-- 同步中 ≠ 空:诚实区分(快照未到时不断言"无事件") -->
306
- <span
307
- v-if="syncing || conn.state !== 'open'"
308
- class="i-tabler-refresh empty-icon"
309
- />
310
- <span
311
- v-else
312
- class="i-tabler-message-dots empty-icon"
313
- />
314
- <p class="empty-title">
315
- {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.klnne6o010') : $t('transcriptTimeline.k126izdm022') }}
316
- </p>
317
- <p class="empty-hint">
318
- {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.k1iuqpxj011') : $t('transcriptTimeline.kuyjpmi023') }}
319
- </p>
320
- </div>
321
- <!-- 过滤空态:有事件但当前过滤无匹配(区别于真空) -->
322
- <div
323
- v-else-if="blocks.length === 0"
324
- class="empty filtered"
325
- >
326
- <span class="i-tabler-filter-off empty-icon" />
327
- <p class="empty-title">
328
- {{ $t('transcriptTimeline.ktuo1zg004') }}
329
- </p>
330
- <p class="empty-hint">
331
- 「{{ filterOptions.find(o => o.value === filter)?.label ?? filter }}」{{ $t('transcriptTimeline.kapxgq4006') }}
332
- </p>
333
- </div>
334
- <template
335
- v-for="(b, i) in blocks"
336
- :key="b.id"
337
- >
338
- <div
339
- v-if="blockDayFlags[i]?.divider"
340
- class="date-divider"
341
- >
342
- <span class="date-divider-label">{{ blockDayFlags[i]!.divider }}</span>
343
- </div>
344
- <workshop-event-block
345
- :block="b"
346
- :turn-start="i > 0 && blocks[i - 1]!.agentId !== b.agentId"
347
- :compact="i > 0 && blocks[i - 1]!.agentId === b.agentId"
348
- :enter-stage="staged.get(b.id)"
349
- />
350
- </template>
351
- </div>
352
- </div>
353
- </div>
354
- </template>
355
-
356
- <style scoped>
357
- .transcript {
358
- display: flex;
359
- flex-direction: column;
360
- height: 100%;
361
- min-height: 0;
362
- background: var(--paper); /* 灰画布:消息气泡白卡在此浮出(Slack 声部) */
363
- }
364
- .filter-bar {
365
- display: flex;
366
- gap: 10px;
367
- align-items: center;
368
- padding: 6px 16px;
369
- background: var(--paper-raised);
370
- border-bottom: 1px solid var(--line);
371
- }
372
- .count {
373
- font-size: 11px;
374
- font-family: var(--font-mono);
375
- font-variant-numeric: tabular-nums;
376
- color: var(--ink-faint);
377
- }
378
- /* 连接诚实态 chip:同步中琥珀脉搏 / 在线绿点+最后数据时间 / 离线灰 */
379
- .sync-chip {
380
- display: inline-flex;
381
- gap: 5px;
382
- align-items: center;
383
- margin-left: auto;
384
- padding: 1px 8px;
385
- font-family: var(--font-mono);
386
- font-size: 9.5px;
387
- font-variant-numeric: tabular-nums;
388
- color: var(--ink-faint);
389
- background: var(--paper-deep);
390
- border-radius: var(--radius-pill);
391
- }
392
- .sync-chip[data-state='syncing'] {
393
- color: var(--tone-warning-dot);
394
- }
395
- .sync-chip[data-state='open'] .sync-dot {
396
- font-size: 7px;
397
- color: var(--tone-success-dot);
398
- }
399
- .sync-pulse {
400
- width: 6px;
401
- height: 6px;
402
- background: var(--tone-warning-dot);
403
- border-radius: 50%;
404
- animation: sync-breath 1.2s ease-in-out infinite;
405
- }
406
- @keyframes sync-breath {
407
- 0%, 100% { opacity: 0.35; transform: scale(0.85); }
408
- 50% { opacity: 1; transform: scale(1); }
409
- }
410
- @media (prefers-reduced-motion: reduce) {
411
- .sync-pulse { animation: none; opacity: 0.8; }
412
- }
413
- .jump-latest {
414
- position: sticky;
415
- bottom: 12px;
416
- z-index: 5;
417
- display: inline-flex;
418
- gap: 5px;
419
- align-items: center;
420
- float: right;
421
- margin-right: 14px;
422
- padding: 5px 12px;
423
- font-family: var(--font-body);
424
- font-size: 11px;
425
- color: var(--on-accent);
426
- cursor: pointer;
427
- background: var(--accent);
428
- border: none;
429
- border-radius: var(--radius-pill);
430
- box-shadow: var(--shadow-float);
431
- transition: opacity var(--transition-fast), transform var(--transition-fast);
432
- }
433
-
434
- .jump-latest:hover { opacity: 0.92; }
435
- .jump-latest:active { transform: translateY(1px); }
436
-
437
- .scroller {
438
- overscroll-behavior: contain;
439
- flex: 1 1 auto;
440
- min-height: 0;
441
- overflow-y: auto;
442
- }
443
- /* Codex 式阅读列:宽屏下内容居中,阅读行宽 ~900px;首载轻淡入 */
444
- .column {
445
- max-width: 900px;
446
- padding: 10px 4px 24px;
447
- margin: 0 auto;
448
- animation: column-in 0.28s ease-out;
449
- }
450
- @keyframes column-in {
451
- from { opacity: 0; }
452
- to { opacity: 1; }
453
- }
454
- .earlier-btn {
455
- display: block;
456
- margin: 0 auto 10px;
457
- padding: 4px 14px;
458
- font-family: var(--font-body);
459
- font-size: 11px;
460
- color: var(--ink-faint);
461
- cursor: pointer;
462
- background: var(--paper-raised);
463
- border: 1px solid var(--line-strong);
464
- border-radius: var(--radius-pill);
465
- transition: background var(--transition-fast), color var(--transition-fast);
466
- }
467
- .earlier-btn:hover:not(:disabled) {
468
- color: var(--ink);
469
- background: var(--paper-deep);
470
- }
471
- .earlier-btn:disabled { opacity: 0.5; cursor: default; }
472
- .earlier-done {
473
- margin-bottom: 8px;
474
- font-family: var(--font-mono);
475
- font-size: 9.5px;
476
- text-align: center;
477
- color: var(--ink-faint);
478
- }
479
- /* 空态:编辑部式 serif 标题(pane-empty 声部) */
480
- .empty {
481
- display: flex;
482
- flex-direction: column;
483
- gap: 10px;
484
- align-items: center;
485
- padding: 72px 16px;
486
- text-align: center;
487
- }
488
- .empty-icon {
489
- font-size: 28px;
490
- color: var(--ink-faint);
491
- }
492
- .empty-title {
493
- margin: 0;
494
- font-family: var(--font-display);
495
- font-size: 22px;
496
- font-weight: 400;
497
- letter-spacing: -0.01em;
498
- color: var(--ink-soft);
499
- }
500
- .empty-hint {
501
- margin: 0;
502
- font-size: 12px;
503
- color: var(--ink-faint);
504
- }
505
-
506
- /* 连接中骨架:对齐块行网格(26px 头像列 + 内容列),行间延迟入场 */
507
- .skel-stack {
508
- display: flex;
509
- flex-direction: column;
510
- gap: 18px;
511
- padding: 26px 6px;
512
- }
513
-
514
- .skel-row {
515
- display: grid;
516
- grid-template-columns: 26px minmax(0, 1fr);
517
- gap: 10px;
518
- animation: aw-rise 0.4s cubic-bezier(0.22, 0.68, 0.36, 1) backwards;
519
- animation-delay: var(--d, 0s);
520
- }
521
-
522
- .skel-ava {
523
- width: 26px;
524
- height: 26px;
525
- border-radius: 50%;
526
- }
527
-
528
- .skel-lines {
529
- display: flex;
530
- flex-direction: column;
531
- gap: 6px;
532
- }
533
-
534
- .skel-line {
535
- height: 11px;
536
- }
537
-
538
- .skel-line.name { width: 120px; height: 13px; }
539
- .skel-line.w70 { width: 70%; }
540
- .skel-line.w45 { width: 45%; }
541
-
542
- @media (prefers-reduced-motion: reduce) {
543
- .skel-row { animation: none; }
544
- }
545
-
546
- /* ── 窄屏(≤1023):阅读列收边 + 富文本/代码/终端块横向滚动而非顶破视口 ──
547
- 消息体由 workshop-event-block(及其内部的 prose/code-block)渲染,
548
- 这里用 :deep 给它们一条自己的卷轴:代码不折行(折行会毁掉缩进语义),
549
- 改为在自己块内横扫;图片/画布一律不超列宽。 */
550
- @media (max-width: 1023.98px) {
551
- .filter-bar {
552
- flex-wrap: wrap;
553
- gap: 8px;
554
- padding: 6px 10px;
555
- }
556
-
557
- .filter-bar :deep(.ant-segmented) {
558
- flex: 1 1 100%;
559
- }
560
-
561
- .count,
562
- .sync-chip,
563
- .earlier-done {
564
- font-size: 11.5px;
565
- }
566
-
567
- .column {
568
- padding: 8px 8px 22px;
569
- }
570
-
571
- .column :deep(pre),
572
- .column :deep(table),
573
- .column :deep(.code-block),
574
- .column :deep(.st-text) {
575
- max-width: 100%;
576
- overflow-x: auto;
577
- -webkit-overflow-scrolling: touch;
578
- }
579
-
580
- .column :deep(img),
581
- .column :deep(video),
582
- .column :deep(canvas) {
583
- max-width: 100%;
584
- height: auto;
585
- }
586
-
587
- .empty-hint {
588
- font-size: 13px;
589
- }
590
-
591
- .earlier-btn,
592
- .jump-latest {
593
- min-height: 40px;
594
- }
595
-
596
- .jump-latest {
597
- padding: 8px 14px;
598
- font-size: 12px;
599
- }
600
- }
601
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Transcript 时间线:过滤条(全部/消息/任务/错误)+ cluster 块流 + 自动吸底。
4
+ * 事件经 useClusteredBlocks 增量聚类(turn block,内容智能去重),
5
+ * 流式更新只命中变化的块组件;新帧到达保持吸底。
6
+ * 历史窗口:默认最近 200 帧(loadHistory);顶部"加载更早"按 beforeSeq 向上翻页。
7
+ */
8
+ import { useEventsStore, type EventFilter } from '@/app/stores/workshop/events'
9
+ import { useClusteredBlocks } from '@/app/composables/workshop/useClusteredBlocks'
10
+ import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
11
+ import { useCodeCopy } from '@/app/composables/useCodeCopy'
12
+
13
+ const { t } = useI18n()
14
+
15
+ const props = defineProps<{ channelId: string }>()
16
+ const events = useEventsStore()
17
+ const { conn } = useWorkshopWs()
18
+ // 代码块复制事件委托(文档级单例;时间线内所有 .code-copy 通用)
19
+ useCodeCopy()
20
+ const scroller = ref<HTMLElement | null>(null)
21
+ const stickBottom = ref(true)
22
+
23
+ /** 连接中骨架:WS 连接中且尚无块 → 按 block 行形态的 shimmer(open-tag skel 声部) */
24
+ const connecting = computed(() =>
25
+ conn.state === 'connecting' && events.ring(props.channelId).items.length === 0)
26
+
27
+ const filter = computed({
28
+ get: () => events.filters[props.channelId] ?? 'all',
29
+ set: (v: EventFilter) => events.setFilter(props.channelId, v),
30
+ })
31
+
32
+ const { blocks, totalEvents } = useClusteredBlocks(() => props.channelId)
33
+
34
+ // ===== 向上翻页历史 =====
35
+ const loadingEarlier = ref(false)
36
+ const earlierExhausted = ref(false)
37
+ /** 已加载事件数 vs 持久化总量 → 是否可能还有更早(粗判,点击时由 loadEarlier 探底) */
38
+ const loadedCount = computed(() => events.ring(props.channelId).items.length)
39
+ const maybeMore = computed(() => !earlierExhausted.value && loadedCount.value > 0)
40
+ const loadEarlier = async (): Promise<void> => {
41
+ if (loadingEarlier.value || earlierExhausted.value) return
42
+ loadingEarlier.value = true
43
+ // 记住滚动位置:头部插入后保持视口锚定(不吸底跳变)
44
+ const el = scroller.value
45
+ const anchor = el ? { top: el.scrollTop, height: el.scrollHeight } : null
46
+ try {
47
+ const hasMore = await events.loadEarlier(props.channelId)
48
+ if (!hasMore) earlierExhausted.value = true
49
+ if (el && anchor) {
50
+ await nextTick()
51
+ el.scrollTop = el.scrollHeight - anchor.height + anchor.top
52
+ }
53
+ }
54
+ catch {
55
+ /* 历史拉取失败:保持按钮可重试,不打断时间线 */
56
+ }
57
+ finally {
58
+ loadingEarlier.value = false
59
+ }
60
+ }
61
+
62
+ const onScroll = (): void => {
63
+ const el = scroller.value
64
+ if (!el) return
65
+ stickBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < 120
66
+ }
67
+
68
+ // ===== 回底动画(open-tag jump-bottom:800ms ease-out,动画期抑制按钮闪烁) =====
69
+ const scrollingDown = ref(false)
70
+ /** 结构化滚动容器(绕开 vue-tsc 双 lib.dom 的 Element 类型不兼容) */
71
+ type ScrollBox = { scrollTop: number, scrollHeight: number, clientHeight: number }
72
+ const animateScroll = (el: ScrollBox, ms: number): Promise<void> =>
73
+ new Promise((resolve) => {
74
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
75
+ el.scrollTop = el.scrollHeight
76
+ resolve()
77
+ return
78
+ }
79
+ const from = el.scrollTop
80
+ const delta = el.scrollHeight - el.clientHeight - from
81
+ const t0 = performance.now()
82
+ const step = (t: number): void => {
83
+ const k = Math.min(1, (t - t0) / ms)
84
+ const eased = 1 - (1 - k) ** 3 // ease-out cubic
85
+ el.scrollTop = from + delta * eased
86
+ if (k < 1) requestAnimationFrame(step)
87
+ else resolve()
88
+ }
89
+ requestAnimationFrame(step)
90
+ })
91
+
92
+ /** 跳回最新:滚底并恢复吸底(用户离开底部后新内容不再自动跟随) */
93
+ const jumpToLatest = async (): Promise<void> => {
94
+ stickBottom.value = true
95
+ const el = scroller.value
96
+ if (!el) return
97
+ scrollingDown.value = true
98
+ try {
99
+ await animateScroll(el, 800)
100
+ }
101
+ finally {
102
+ scrollingDown.value = false
103
+ }
104
+ }
105
+
106
+ // ===== 新块进场编排(open-tag motion charter 移植) =====
107
+ /**
108
+ * 只有实时新到的尾部块做进场动画(60ms stagger,600ms burst 窗口,上限 8 条);
109
+ * 整体重建(过滤切换/聚焦/历史回填——一次出现大量新 id)与组件首载直接显示,
110
+ * 不动画不延迟。映射:blockId → 进场延迟 ms;-1 = 不动画。
111
+ */
112
+ type Stage = { enter: boolean, delay: number }
113
+ const staged = ref(new Map<string, Stage>())
114
+ let burstAt = 0
115
+ let burstN = 0
116
+ const mountedAt = Date.now()
117
+ watch(blocks, (list, prev) => {
118
+ const prevIds = new Set((prev ?? []).map(b => b.id))
119
+ const fresh = list.filter(b => !prevIds.has(b.id))
120
+ const map = new Map<string, Stage>()
121
+ const wholesale = fresh.length === 0
122
+ || fresh.length > Math.max(8, Math.ceil(list.length * 0.4))
123
+ || Date.now() - mountedAt < 1500
124
+ if (!wholesale) {
125
+ const now = Date.now()
126
+ if (now - burstAt > 600) {
127
+ burstAt = now
128
+ burstN = 0
129
+ }
130
+ }
131
+ for (const b of list) {
132
+ const isFresh = !prevIds.has(b.id)
133
+ const delay = isFresh && !wholesale ? Math.min(burstN++, 7) * 60 : -1
134
+ map.set(b.id, { enter: delay >= 0, delay: Math.max(0, delay) })
135
+ }
136
+ staged.value = map
137
+ }, { immediate: true })
138
+
139
+ /** 吸底:新块出现 / seq 增长 / 块内容尺寸变化(可能高增)后滚到底 */
140
+ const scrollToBottom = async (): Promise<void> => {
141
+ if (!stickBottom.value) return
142
+ await nextTick()
143
+ const el = scroller.value
144
+ if (el) el.scrollTop = el.scrollHeight
145
+ }
146
+ watch(() => blocks.value.length, () => {
147
+ void scrollToBottom()
148
+ })
149
+ watch(() => events.lastSeq(props.channelId), () => {
150
+ void scrollToBottom()
151
+ })
152
+
153
+ // 内容高度增长吸底:同一流块 delta 打字机让块变高(块数与 seq 不变或合并帧不推 seq),
154
+ // ResizeObserver 观测内容列高度变化补一次滚底 —— 长输出不再"卡"在中途。
155
+ // (观察器以结构化类型声明:绕开 vue-tsc 双 lib.dom 下 Element 类型不兼容问题)
156
+ const columnEl = ref<HTMLElement | null>(null)
157
+ let contentObserver: { disconnect(): void } | null = null
158
+ onMounted(() => {
159
+ const el = scroller.value
160
+ const column = columnEl.value
161
+ if (!el || !column || typeof ResizeObserver === 'undefined') return
162
+ const Observer = ResizeObserver as unknown as
163
+ new (cb: () => void) => { observe(target: unknown): void, disconnect(): void }
164
+ const observer = new Observer(() => {
165
+ if (stickBottom.value) el.scrollTop = el.scrollHeight
166
+ })
167
+ observer.observe(column)
168
+ contentObserver = observer
169
+ })
170
+ onBeforeUnmount(() => {
171
+ contentObserver?.disconnect()
172
+ contentObserver = null
173
+ })
174
+
175
+ /** 断线待对齐(诚实连接态:open-tag 规范——不假装在线,提示同步中) */
176
+ const syncing = computed(() => conn.pendingReplay || conn.state === 'connecting')
177
+ /** 最后数据时间(x 秒前;无数据返回空) */
178
+ const lastDataAgo = computed(() => {
179
+ if (!conn.lastDataAt) return ''
180
+ const s = Math.max(0, Math.round((Date.now() - conn.lastDataAt) / 1000))
181
+ return s < 5 ? t('transcriptTimeline.k3wwxl026') : t('transcriptTimeline.k2gyl6l027', { p0: s })
182
+ })
183
+
184
+ const filterOptions: Array<{ value: EventFilter, label: string }> = [
185
+ { value: 'all', label: t('transcriptTimeline.k3x4t1012') },
186
+ { value: 'key', label: t('transcriptTimeline.k3x5xi013') },
187
+ { value: 'messages', label: t('transcriptTimeline.k41ykc014') },
188
+ { value: 'tasks', label: t('transcriptTimeline.k3wcox015') },
189
+ { value: 'team', label: t('transcriptTimeline.k3y5ja016') },
190
+ { value: 'errors', label: t('transcriptTimeline.k49d2l017') },
191
+ ]
192
+
193
+ // ===== 日期分隔线(open-tag date-divider 移植):块 firstAt 跨日 → 插入 hairline 分隔 =====
194
+ const startOfDay = (iso: string): number => {
195
+ const d = new Date(iso)
196
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
197
+ }
198
+ const dayLabel = (iso: string): string => {
199
+ const d = new Date(iso)
200
+ const today = new Date()
201
+ const yesterday = new Date()
202
+ yesterday.setDate(today.getDate() - 1)
203
+ const sameDay = (a: Date, b: Date) =>
204
+ a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
205
+ if (sameDay(d, today)) return t('transcriptTimeline.k3wcqg018')
206
+ if (sameDay(d, yesterday)) return t('transcriptTimeline.k40l1y019')
207
+ const y = d.getFullYear() !== today.getFullYear() ? t('transcriptTimeline.k2hda35028', { p0: d.getFullYear() }) : ''
208
+ return t('transcriptTimeline.kbs29zx029', { p0: y, p1: d.getMonth() + 1, p2: d.getDate() })
209
+ }
210
+ /** 每个块是否需要前置日界分隔(与上一块不同日,或首块) */
211
+ const blockDayFlags = computed(() => {
212
+ const flags: Array<{ divider: string | null }> = []
213
+ let prevDay: number | null = null
214
+ for (const b of blocks.value) {
215
+ const day = startOfDay(b.firstAt)
216
+ flags.push({ divider: prevDay === null || day !== prevDay ? dayLabel(b.firstAt) : null })
217
+ prevDay = day
218
+ }
219
+ return flags
220
+ })
221
+ </script>
222
+
223
+ <template>
224
+ <div class="transcript">
225
+ <div class="filter-bar">
226
+ <a-segmented
227
+ v-model:value="filter"
228
+ size="small"
229
+ :options="filterOptions"
230
+ />
231
+ <span class="count">{{ totalEvents }} {{ $t('transcriptTimeline.k1atjpx1005') }} {{ blocks.length }} {{ $t('transcriptTimeline.k4a9o007') }}</span>
232
+ <!-- 连接诚实态:断线待对齐 → 同步中脉搏;在线 → 最后数据时间(open-tag 规范) -->
233
+ <span
234
+ class="sync-chip"
235
+ :data-state="syncing ? 'syncing' : conn.state"
236
+ :title="syncing ? $t('transcriptTimeline.syncLag') : $t('transcriptTimeline.kglov3b025', { p0: lastDataAgo || $t('transcriptTimeline.none') })"
237
+ >
238
+ <span
239
+ v-if="syncing"
240
+ class="sync-pulse"
241
+ />
242
+ <span
243
+ v-else-if="conn.state === 'open'"
244
+ class="i-tabler-point-filled sync-dot"
245
+ />
246
+ {{ syncing ? $t('transcriptTimeline.k3lmtk3008') : conn.state === 'open' ? lastDataAgo || $t('transcriptTimeline.k3y2p8020') : $t('transcriptTimeline.k44c2n024') }}
247
+ </span>
248
+ </div>
249
+ <div
250
+ ref="scroller"
251
+ class="scroller"
252
+ @scroll="onScroll"
253
+ >
254
+ <!-- 离底时的跳转最新悬浮按钮(流式新内容到达不强制跟随,点击回底;动画期抑制闪烁) -->
255
+ <button
256
+ v-if="!stickBottom && !scrollingDown"
257
+ class="jump-latest"
258
+ :title="$t('transcriptTimeline.kqu93z8001')"
259
+ @click="jumpToLatest"
260
+ >
261
+ <span class="i-tabler-arrow-down" />
262
+ {{ $t('transcriptTimeline.k40t11002') }}
263
+ </button>
264
+ <div
265
+ ref="columnEl"
266
+ class="column"
267
+ >
268
+ <button
269
+ v-if="maybeMore"
270
+ class="earlier-btn"
271
+ :disabled="loadingEarlier"
272
+ @click="loadEarlier"
273
+ >
274
+ {{ loadingEarlier ? $t('transcriptTimeline.k1br0ij9009') : $t('transcriptTimeline.kywgd38021') }}
275
+ </button>
276
+ <div
277
+ v-if="earlierExhausted && loadedCount > 0"
278
+ class="earlier-done"
279
+ >
280
+ {{ $t('transcriptTimeline.k1kbaden003') }}
281
+ </div>
282
+ <!-- 连接中骨架:头像圆 + 双行占位,布局对齐最终块形态(免跳变) -->
283
+ <div
284
+ v-if="connecting"
285
+ class="skel-stack"
286
+ >
287
+ <div
288
+ v-for="n in 3"
289
+ :key="n"
290
+ class="skel-row"
291
+ :style="{ '--d': `${(n - 1) * 0.12}s` }"
292
+ >
293
+ <span class="aw-skel skel-ava" />
294
+ <div class="skel-lines">
295
+ <span class="aw-skel skel-line name" />
296
+ <span class="aw-skel skel-line w70" />
297
+ <span class="aw-skel skel-line w45" />
298
+ </div>
299
+ </div>
300
+ </div>
301
+ <div
302
+ v-else-if="blocks.length === 0 && totalEvents === 0"
303
+ class="empty"
304
+ >
305
+ <!-- 同步中 ≠ 空:诚实区分(快照未到时不断言"无事件") -->
306
+ <span
307
+ v-if="syncing || conn.state !== 'open'"
308
+ class="i-tabler-refresh empty-icon"
309
+ />
310
+ <span
311
+ v-else
312
+ class="i-tabler-message-dots empty-icon"
313
+ />
314
+ <p class="empty-title">
315
+ {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.klnne6o010') : $t('transcriptTimeline.k126izdm022') }}
316
+ </p>
317
+ <p class="empty-hint">
318
+ {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.k1iuqpxj011') : $t('transcriptTimeline.kuyjpmi023') }}
319
+ </p>
320
+ </div>
321
+ <!-- 过滤空态:有事件但当前过滤无匹配(区别于真空) -->
322
+ <div
323
+ v-else-if="blocks.length === 0"
324
+ class="empty filtered"
325
+ >
326
+ <span class="i-tabler-filter-off empty-icon" />
327
+ <p class="empty-title">
328
+ {{ $t('transcriptTimeline.ktuo1zg004') }}
329
+ </p>
330
+ <p class="empty-hint">
331
+ 「{{ filterOptions.find(o => o.value === filter)?.label ?? filter }}」{{ $t('transcriptTimeline.kapxgq4006') }}
332
+ </p>
333
+ </div>
334
+ <template
335
+ v-for="(b, i) in blocks"
336
+ :key="b.id"
337
+ >
338
+ <div
339
+ v-if="blockDayFlags[i]?.divider"
340
+ class="date-divider"
341
+ >
342
+ <span class="date-divider-label">{{ blockDayFlags[i]!.divider }}</span>
343
+ </div>
344
+ <workshop-event-block
345
+ :block="b"
346
+ :turn-start="i > 0 && blocks[i - 1]!.agentId !== b.agentId"
347
+ :compact="i > 0 && blocks[i - 1]!.agentId === b.agentId"
348
+ :enter-stage="staged.get(b.id)"
349
+ />
350
+ </template>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ </template>
355
+
356
+ <style scoped>
357
+ .transcript {
358
+ display: flex;
359
+ flex-direction: column;
360
+ height: 100%;
361
+ min-height: 0;
362
+ background: var(--paper); /* 灰画布:消息气泡白卡在此浮出(Slack 声部) */
363
+ }
364
+ .filter-bar {
365
+ display: flex;
366
+ gap: 10px;
367
+ align-items: center;
368
+ padding: 6px 16px;
369
+ background: var(--paper-raised);
370
+ border-bottom: 1px solid var(--line);
371
+ }
372
+ .count {
373
+ font-size: 11px;
374
+ font-family: var(--font-mono);
375
+ font-variant-numeric: tabular-nums;
376
+ color: var(--ink-faint);
377
+ }
378
+ /* 连接诚实态 chip:同步中琥珀脉搏 / 在线绿点+最后数据时间 / 离线灰 */
379
+ .sync-chip {
380
+ display: inline-flex;
381
+ gap: 5px;
382
+ align-items: center;
383
+ margin-left: auto;
384
+ padding: 1px 8px;
385
+ font-family: var(--font-mono);
386
+ font-size: 9.5px;
387
+ font-variant-numeric: tabular-nums;
388
+ color: var(--ink-faint);
389
+ background: var(--paper-deep);
390
+ border-radius: var(--radius-pill);
391
+ }
392
+ .sync-chip[data-state='syncing'] {
393
+ color: var(--tone-warning-dot);
394
+ }
395
+ .sync-chip[data-state='open'] .sync-dot {
396
+ font-size: 7px;
397
+ color: var(--tone-success-dot);
398
+ }
399
+ .sync-pulse {
400
+ width: 6px;
401
+ height: 6px;
402
+ background: var(--tone-warning-dot);
403
+ border-radius: 50%;
404
+ animation: sync-breath 1.2s ease-in-out infinite;
405
+ }
406
+ @keyframes sync-breath {
407
+ 0%, 100% { opacity: 0.35; transform: scale(0.85); }
408
+ 50% { opacity: 1; transform: scale(1); }
409
+ }
410
+ @media (prefers-reduced-motion: reduce) {
411
+ .sync-pulse { animation: none; opacity: 0.8; }
412
+ }
413
+ .jump-latest {
414
+ position: sticky;
415
+ bottom: 12px;
416
+ z-index: 5;
417
+ display: inline-flex;
418
+ gap: 5px;
419
+ align-items: center;
420
+ float: right;
421
+ margin-right: 14px;
422
+ padding: 5px 12px;
423
+ font-family: var(--font-body);
424
+ font-size: 11px;
425
+ color: var(--on-accent);
426
+ cursor: pointer;
427
+ background: var(--accent);
428
+ border: none;
429
+ border-radius: var(--radius-pill);
430
+ box-shadow: var(--shadow-float);
431
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
432
+ }
433
+
434
+ .jump-latest:hover { opacity: 0.92; }
435
+ .jump-latest:active { transform: translateY(1px); }
436
+
437
+ .scroller {
438
+ overscroll-behavior: contain;
439
+ flex: 1 1 auto;
440
+ min-height: 0;
441
+ overflow-y: auto;
442
+ }
443
+ /* Codex 式阅读列:宽屏下内容居中,阅读行宽 ~900px;首载轻淡入 */
444
+ .column {
445
+ max-width: 900px;
446
+ padding: 10px 4px 24px;
447
+ margin: 0 auto;
448
+ animation: column-in 0.28s ease-out;
449
+ }
450
+ @keyframes column-in {
451
+ from { opacity: 0; }
452
+ to { opacity: 1; }
453
+ }
454
+ .earlier-btn {
455
+ display: block;
456
+ margin: 0 auto 10px;
457
+ padding: 4px 14px;
458
+ font-family: var(--font-body);
459
+ font-size: 11px;
460
+ color: var(--ink-faint);
461
+ cursor: pointer;
462
+ background: var(--paper-raised);
463
+ border: 1px solid var(--line-strong);
464
+ border-radius: var(--radius-pill);
465
+ transition: background var(--transition-fast), color var(--transition-fast);
466
+ }
467
+ .earlier-btn:hover:not(:disabled) {
468
+ color: var(--ink);
469
+ background: var(--paper-deep);
470
+ }
471
+ .earlier-btn:disabled { opacity: 0.5; cursor: default; }
472
+ .earlier-done {
473
+ margin-bottom: 8px;
474
+ font-family: var(--font-mono);
475
+ font-size: 9.5px;
476
+ text-align: center;
477
+ color: var(--ink-faint);
478
+ }
479
+ /* 空态:编辑部式 serif 标题(pane-empty 声部) */
480
+ .empty {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 10px;
484
+ align-items: center;
485
+ padding: 72px 16px;
486
+ text-align: center;
487
+ }
488
+ .empty-icon {
489
+ font-size: 28px;
490
+ color: var(--ink-faint);
491
+ }
492
+ .empty-title {
493
+ margin: 0;
494
+ font-family: var(--font-display);
495
+ font-size: 22px;
496
+ font-weight: 400;
497
+ letter-spacing: -0.01em;
498
+ color: var(--ink-soft);
499
+ }
500
+ .empty-hint {
501
+ margin: 0;
502
+ font-size: 12px;
503
+ color: var(--ink-faint);
504
+ }
505
+
506
+ /* 连接中骨架:对齐块行网格(26px 头像列 + 内容列),行间延迟入场 */
507
+ .skel-stack {
508
+ display: flex;
509
+ flex-direction: column;
510
+ gap: 18px;
511
+ padding: 26px 6px;
512
+ }
513
+
514
+ .skel-row {
515
+ display: grid;
516
+ grid-template-columns: 26px minmax(0, 1fr);
517
+ gap: 10px;
518
+ animation: aw-rise 0.4s cubic-bezier(0.22, 0.68, 0.36, 1) backwards;
519
+ animation-delay: var(--d, 0s);
520
+ }
521
+
522
+ .skel-ava {
523
+ width: 26px;
524
+ height: 26px;
525
+ border-radius: 50%;
526
+ }
527
+
528
+ .skel-lines {
529
+ display: flex;
530
+ flex-direction: column;
531
+ gap: 6px;
532
+ }
533
+
534
+ .skel-line {
535
+ height: 11px;
536
+ }
537
+
538
+ .skel-line.name { width: 120px; height: 13px; }
539
+ .skel-line.w70 { width: 70%; }
540
+ .skel-line.w45 { width: 45%; }
541
+
542
+ @media (prefers-reduced-motion: reduce) {
543
+ .skel-row { animation: none; }
544
+ }
545
+
546
+ /* ── 窄屏(≤1023):阅读列收边 + 富文本/代码/终端块横向滚动而非顶破视口 ──
547
+ 消息体由 workshop-event-block(及其内部的 prose/code-block)渲染,
548
+ 这里用 :deep 给它们一条自己的卷轴:代码不折行(折行会毁掉缩进语义),
549
+ 改为在自己块内横扫;图片/画布一律不超列宽。 */
550
+ @media (max-width: 1023.98px) {
551
+ .filter-bar {
552
+ flex-wrap: wrap;
553
+ gap: 8px;
554
+ padding: 6px 10px;
555
+ }
556
+
557
+ .filter-bar :deep(.ant-segmented) {
558
+ flex: 1 1 100%;
559
+ }
560
+
561
+ .count,
562
+ .sync-chip,
563
+ .earlier-done {
564
+ font-size: 11.5px;
565
+ }
566
+
567
+ .column {
568
+ padding: 8px 8px 22px;
569
+ }
570
+
571
+ .column :deep(pre),
572
+ .column :deep(table),
573
+ .column :deep(.code-block),
574
+ .column :deep(.st-text) {
575
+ max-width: 100%;
576
+ overflow-x: auto;
577
+ -webkit-overflow-scrolling: touch;
578
+ }
579
+
580
+ .column :deep(img),
581
+ .column :deep(video),
582
+ .column :deep(canvas) {
583
+ max-width: 100%;
584
+ height: auto;
585
+ }
586
+
587
+ .empty-hint {
588
+ font-size: 13px;
589
+ }
590
+
591
+ .earlier-btn,
592
+ .jump-latest {
593
+ min-height: 40px;
594
+ }
595
+
596
+ .jump-latest {
597
+ padding: 8px 14px;
598
+ font-size: 12px;
599
+ }
600
+ }
601
+ </style>