prts-terrarchive 0.2.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 (287) hide show
  1. package/GAME_ASSETS.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +274 -0
  4. package/README.md +423 -0
  5. package/THIRD_PARTY_NOTICES.md +61 -0
  6. package/bin/bake-rhine-details.mjs +34 -0
  7. package/bin/install.js +273 -0
  8. package/bin/pack-map-assets.mjs +110 -0
  9. package/bin/preview-rhine.mjs +71 -0
  10. package/bin/rhine-detail-baker.ts +84 -0
  11. package/contracts/cloud_tools_v1.schema.json +243 -0
  12. package/contracts/corpus_i18n_v1.schema.json +179 -0
  13. package/contracts/corpus_tools_v1.schema.json +1801 -0
  14. package/cordis.patch.yml +32 -0
  15. package/docs/endfield-official-i18n.md +103 -0
  16. package/lib/client.js +3912 -0
  17. package/lib/endfield-map/map.js.br +0 -0
  18. package/lib/endfield-map/map.js.gz +0 -0
  19. package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.br +0 -0
  20. package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.gz +0 -0
  21. package/lib/endfield-map/resources/map-01616f534aaaeca1.png +0 -0
  22. package/lib/endfield-map/resources/map-1562e0dff4be3d23.png +0 -0
  23. package/lib/endfield-map/resources/map-160fe040f080f6a3.png +0 -0
  24. package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.br +0 -0
  25. package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.gz +0 -0
  26. package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.br +0 -0
  27. package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.gz +0 -0
  28. package/lib/endfield-map/resources/map-1d30baaaef3e29cc.png +0 -0
  29. package/lib/endfield-map/resources/map-21a61386af79cd12.json.br +0 -0
  30. package/lib/endfield-map/resources/map-21a61386af79cd12.json.gz +0 -0
  31. package/lib/endfield-map/resources/map-24b920b8dab49b44.json.br +0 -0
  32. package/lib/endfield-map/resources/map-24b920b8dab49b44.json.gz +0 -0
  33. package/lib/endfield-map/resources/map-26caf8d71247abb7.png +0 -0
  34. package/lib/endfield-map/resources/map-2729942c89545e73.json.br +0 -0
  35. package/lib/endfield-map/resources/map-2729942c89545e73.json.gz +0 -0
  36. package/lib/endfield-map/resources/map-29063a9802f67014.json.br +0 -0
  37. package/lib/endfield-map/resources/map-29063a9802f67014.json.gz +0 -0
  38. package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.br +0 -0
  39. package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.gz +0 -0
  40. package/lib/endfield-map/resources/map-2f98a873da81425e.json.br +0 -0
  41. package/lib/endfield-map/resources/map-2f98a873da81425e.json.gz +0 -0
  42. package/lib/endfield-map/resources/map-3ab978ae44190f80.json.br +0 -0
  43. package/lib/endfield-map/resources/map-3ab978ae44190f80.json.gz +0 -0
  44. package/lib/endfield-map/resources/map-3b62c80d14208902.json.br +0 -0
  45. package/lib/endfield-map/resources/map-3b62c80d14208902.json.gz +0 -0
  46. package/lib/endfield-map/resources/map-3d486e887ae121ac.png +0 -0
  47. package/lib/endfield-map/resources/map-418dc4865608fce5.png +0 -0
  48. package/lib/endfield-map/resources/map-4366b84607335f76.json.br +0 -0
  49. package/lib/endfield-map/resources/map-4366b84607335f76.json.gz +0 -0
  50. package/lib/endfield-map/resources/map-4371259d3ab7aac7.png +0 -0
  51. package/lib/endfield-map/resources/map-48dcc0fd55e13f7f.png +0 -0
  52. package/lib/endfield-map/resources/map-4aabda0e41197570.json.br +0 -0
  53. package/lib/endfield-map/resources/map-4aabda0e41197570.json.gz +0 -0
  54. package/lib/endfield-map/resources/map-505f5687b287043c.json.br +0 -0
  55. package/lib/endfield-map/resources/map-505f5687b287043c.json.gz +0 -0
  56. package/lib/endfield-map/resources/map-5177e9d1d46f47e2.png +0 -0
  57. package/lib/endfield-map/resources/map-51baeb59b7f8c28d.png +0 -0
  58. package/lib/endfield-map/resources/map-549561b46e621874.png +0 -0
  59. package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.br +0 -0
  60. package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.gz +0 -0
  61. package/lib/endfield-map/resources/map-5a6963909423b674.png +0 -0
  62. package/lib/endfield-map/resources/map-5a9c533c0552d7b7.png +0 -0
  63. package/lib/endfield-map/resources/map-5d20ae9ffa9a02b0.png +0 -0
  64. package/lib/endfield-map/resources/map-626c8847393f60bf.png +0 -0
  65. package/lib/endfield-map/resources/map-63d43cbec3bed3b8.png +0 -0
  66. package/lib/endfield-map/resources/map-661b6ec56e722574.png +0 -0
  67. package/lib/endfield-map/resources/map-6ddd384eca190828.png +0 -0
  68. package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.br +0 -0
  69. package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.gz +0 -0
  70. package/lib/endfield-map/resources/map-738ebaa002c33cfc.png +0 -0
  71. package/lib/endfield-map/resources/map-758c13be6085ec07.json.br +0 -0
  72. package/lib/endfield-map/resources/map-758c13be6085ec07.json.gz +0 -0
  73. package/lib/endfield-map/resources/map-78929f26d6844509.png +0 -0
  74. package/lib/endfield-map/resources/map-7f09c142950a0f74.png +0 -0
  75. package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.br +0 -0
  76. package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.gz +0 -0
  77. package/lib/endfield-map/resources/map-881343926a568f27.json.br +0 -0
  78. package/lib/endfield-map/resources/map-881343926a568f27.json.gz +0 -0
  79. package/lib/endfield-map/resources/map-8d1ade3f01d952b0.png +0 -0
  80. package/lib/endfield-map/resources/map-8fef3495bb7ed60f.png +0 -0
  81. package/lib/endfield-map/resources/map-907a7ecf8a0ce352.png +0 -0
  82. package/lib/endfield-map/resources/map-9113d5951c4db903.json.br +0 -0
  83. package/lib/endfield-map/resources/map-9113d5951c4db903.json.gz +0 -0
  84. package/lib/endfield-map/resources/map-94ad6634436224e3.json.br +0 -0
  85. package/lib/endfield-map/resources/map-94ad6634436224e3.json.gz +0 -0
  86. package/lib/endfield-map/resources/map-99722e61da5f3e53.png +0 -0
  87. package/lib/endfield-map/resources/map-9be9596cc059abe3.png +0 -0
  88. package/lib/endfield-map/resources/map-a4db9a4763e47fbf.png +0 -0
  89. package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.br +0 -0
  90. package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.gz +0 -0
  91. package/lib/endfield-map/resources/map-ab35d06875ee418f.png +0 -0
  92. package/lib/endfield-map/resources/map-ac51bb3e7623cd8b.png +0 -0
  93. package/lib/endfield-map/resources/map-ac830a66296bc83a.png +0 -0
  94. package/lib/endfield-map/resources/map-b3fd04209718e55e.json.br +0 -0
  95. package/lib/endfield-map/resources/map-b3fd04209718e55e.json.gz +0 -0
  96. package/lib/endfield-map/resources/map-b8164f1af7c5d895.png +0 -0
  97. package/lib/endfield-map/resources/map-b8cde93b965384d8.json.br +0 -0
  98. package/lib/endfield-map/resources/map-b8cde93b965384d8.json.gz +0 -0
  99. package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.br +0 -0
  100. package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.gz +0 -0
  101. package/lib/endfield-map/resources/map-bfc58277d5ba649d.png +0 -0
  102. package/lib/endfield-map/resources/map-c013f82048c6891a.png +0 -0
  103. package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.br +0 -0
  104. package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.gz +0 -0
  105. package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.br +0 -0
  106. package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.gz +0 -0
  107. package/lib/endfield-map/resources/map-c607f8b4c8ad8967.png +0 -0
  108. package/lib/endfield-map/resources/map-c6f20276b8781326.json.br +0 -0
  109. package/lib/endfield-map/resources/map-c6f20276b8781326.json.gz +0 -0
  110. package/lib/endfield-map/resources/map-d51adf5952af1e8e.png +0 -0
  111. package/lib/endfield-map/resources/map-d695eface22f8b74.json.br +0 -0
  112. package/lib/endfield-map/resources/map-d695eface22f8b74.json.gz +0 -0
  113. package/lib/endfield-map/resources/map-db11d394456a7465.png +0 -0
  114. package/lib/endfield-map/resources/map-dcc4d218581e3dc6.png +0 -0
  115. package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.br +0 -0
  116. package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.gz +0 -0
  117. package/lib/endfield-map/resources/map-defe5db7dabf748a.png +0 -0
  118. package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.br +0 -0
  119. package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.gz +0 -0
  120. package/lib/endfield-map/resources/map-e25ee9af6e493500.png +0 -0
  121. package/lib/endfield-map/resources/map-e3824082cd6f747d.json.br +0 -0
  122. package/lib/endfield-map/resources/map-e3824082cd6f747d.json.gz +0 -0
  123. package/lib/endfield-map/resources/map-e694a91e630b1bd6.png +0 -0
  124. package/lib/endfield-map/resources/map-eaa93af79d12a87d.png +0 -0
  125. package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.br +0 -0
  126. package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.gz +0 -0
  127. package/lib/endfield-map/resources/map-ede421a329dc78a4.png +0 -0
  128. package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.br +0 -0
  129. package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.gz +0 -0
  130. package/lib/endfield-map/resources/map-fb5f4a59a181452c.png +0 -0
  131. package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.br +0 -0
  132. package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.gz +0 -0
  133. package/lib/rhine/assets/archive-assembly.glb +0 -0
  134. package/lib/rhine/assets/archive-cassette.glb +0 -0
  135. package/lib/rhine/fonts/MiSans-Bold.woff2 +0 -0
  136. package/lib/rhine/fonts/MiSans-Demibold.woff2 +0 -0
  137. package/lib/rhine/fonts/MiSans-Light.woff2 +0 -0
  138. package/lib/rhine/fonts/MiSans-Regular.woff2 +0 -0
  139. package/lib/rhine/fonts/MiSans-license.pdf +0 -0
  140. package/lib/rhine/fonts/NOTICE.txt +6 -0
  141. package/lib/rhine/licenses/RhineLabUI-MIT.txt +21 -0
  142. package/lib/rhine/licenses/entities-BSD-2-Clause.txt +11 -0
  143. package/lib/rhine/licenses/linkify-it-MIT.txt +22 -0
  144. package/lib/rhine/licenses/markdown-it-MIT.txt +22 -0
  145. package/lib/rhine/licenses/mdurl-MIT.txt +45 -0
  146. package/lib/rhine/licenses/punycode.js-MIT.txt +20 -0
  147. package/lib/rhine/licenses/rolling-number-MIT.txt +21 -0
  148. package/lib/rhine/licenses/three-MIT.txt +21 -0
  149. package/lib/rhine/licenses/uc.micro-MIT.txt +20 -0
  150. package/lib/rhine/rhine.css +1 -0
  151. package/lib/rhine/rhine.js +5106 -0
  152. package/lib/skins/common.css +31 -0
  153. package/lib/skins/endfield-aic.css +141 -0
  154. package/lib/skins/prts-agent.css +136 -0
  155. package/lib/skins/rhine-lab.css +70 -0
  156. package/package.json +112 -0
  157. package/presets/definition.js +26 -0
  158. package/presets/prts/agent.cordis.yml +25 -0
  159. package/presets/prts/preset.yml +3 -0
  160. package/presets/register.js +86 -0
  161. package/resources/entity-routing.json +21 -0
  162. package/skills/prts-investigation/SKILL.md +121 -0
  163. package/skills/prts-retrieval/SKILL.md +35 -0
  164. package/skills/prts-retrieval/references/cloud-tools.md +13 -0
  165. package/skills/prts-retrieval/references/diagnostics.md +15 -0
  166. package/skills/prts-retrieval/references/local-tools.md +24 -0
  167. package/skills/prts-retrieval/references/module-arknights.md +23 -0
  168. package/skills/prts-retrieval/references/module-dual.md +15 -0
  169. package/skills/prts-retrieval/references/module-endfield.md +21 -0
  170. package/skills/prts-retrieval/references/retrieval-process.md +44 -0
  171. package/skills/prts-retrieval/references/retrieval-recipes-dual.md +34 -0
  172. package/skills/prts-retrieval/references/retrieval-recipes-endfield.md +37 -0
  173. package/skills/prts-retrieval/references/retrieval-recipes.md +39 -0
  174. package/skills/prts-retrieval/references/tools-runtime.md +48 -0
  175. package/skills/prts-retrieval/references/wiki-schema.md +38 -0
  176. package/src/archive-presentation.js +287 -0
  177. package/src/cloud-projection.js +124 -0
  178. package/src/cloud.js +498 -0
  179. package/src/entity-recognizer.js +311 -0
  180. package/src/entity-routing.js +82 -0
  181. package/src/evidence-state.js +332 -0
  182. package/src/i18n.js +365 -0
  183. package/src/index.js +1261 -0
  184. package/src/installer.js +1362 -0
  185. package/src/investigation-review.js +87 -0
  186. package/src/investigation-store.js +579 -0
  187. package/src/investigation-tools.js +113 -0
  188. package/src/localization-format.js +23 -0
  189. package/src/manual-search.js +103 -0
  190. package/src/read.js +1097 -0
  191. package/src/release-compatibility.js +73 -0
  192. package/src/release-watch.js +40 -0
  193. package/src/search-projection.js +139 -0
  194. package/src/search.js +1522 -0
  195. package/src/skill.js +98 -0
  196. package/src/source-map.js +348 -0
  197. package/src/state.js +314 -0
  198. package/src/store.js +1569 -0
  199. package/src/timeline.js +386 -0
  200. package/src/ui.js +1353 -0
  201. package/src/wiki.js +111 -0
  202. package/tsconfig.rhine.json +15 -0
  203. package/ui/rhine/archive-navigation-limit.ts +31 -0
  204. package/ui/rhine/archive-rack.ts +83 -0
  205. package/ui/rhine/assets/evidence-observatory.webp +0 -0
  206. package/ui/rhine/board-plane-transform.ts +24 -0
  207. package/ui/rhine/catalogue.ts +72 -0
  208. package/ui/rhine/deferred-preparation.ts +146 -0
  209. package/ui/rhine/evidence-board-controls.css +22 -0
  210. package/ui/rhine/evidence-board-history.ts +73 -0
  211. package/ui/rhine/evidence-board-model.ts +189 -0
  212. package/ui/rhine/evidence-board-panel.ts +762 -0
  213. package/ui/rhine/evidence-board-scene.ts +548 -0
  214. package/ui/rhine/evidence-board.css +191 -0
  215. package/ui/rhine/evidence-card-art.ts +470 -0
  216. package/ui/rhine/evidence-card-style.ts +17 -0
  217. package/ui/rhine/evidence-inbox-scene.ts +43 -0
  218. package/ui/rhine/evidence-inbox.css +29 -0
  219. package/ui/rhine/evidence-inbox.ts +104 -0
  220. package/ui/rhine/index.ts +12 -0
  221. package/ui/rhine/interaction.css +20 -0
  222. package/ui/rhine/investigation-board-plane.ts +5 -0
  223. package/ui/rhine/investigation-board.css +34 -0
  224. package/ui/rhine/investigation-board.ts +348 -0
  225. package/ui/rhine/investigation-edits.ts +46 -0
  226. package/ui/rhine/investigation-layout.ts +29 -0
  227. package/ui/rhine/label-prefetch.ts +34 -0
  228. package/ui/rhine/looping-title.ts +40 -0
  229. package/ui/rhine/manual-search.css +41 -0
  230. package/ui/rhine/manual-search.ts +89 -0
  231. package/ui/rhine/original/appearance.ts +180 -0
  232. package/ui/rhine/original/archive-label.ts +84 -0
  233. package/ui/rhine/original/archive-lighting.ts +39 -0
  234. package/ui/rhine/original/archive-loop.ts +82 -0
  235. package/ui/rhine/original/archive-refill.ts +64 -0
  236. package/ui/rhine/original/array-detail-data.ts +2 -0
  237. package/ui/rhine/original/array-detail.ts +41 -0
  238. package/ui/rhine/original/array-interior.ts +411 -0
  239. package/ui/rhine/original/array-occlusion.ts +166 -0
  240. package/ui/rhine/original/array-shell.ts +38 -0
  241. package/ui/rhine/original/array-visibility.ts +309 -0
  242. package/ui/rhine/original/asset-url.ts +4 -0
  243. package/ui/rhine/original/brand.ts +12 -0
  244. package/ui/rhine/original/cassette-lod.ts +81 -0
  245. package/ui/rhine/original/data.ts +21 -0
  246. package/ui/rhine/original/decryption.ts +185 -0
  247. package/ui/rhine/original/detail-signature.ts +18 -0
  248. package/ui/rhine/original/glass-reveal.ts +45 -0
  249. package/ui/rhine/original/internal-optics.ts +70 -0
  250. package/ui/rhine/original/label-renderer.ts +119 -0
  251. package/ui/rhine/original/label-textures.ts +117 -0
  252. package/ui/rhine/original/model-viewer.ts +658 -0
  253. package/ui/rhine/original/motion.ts +116 -0
  254. package/ui/rhine/original/opaque-backfaces.ts +64 -0
  255. package/ui/rhine/original/program-preparation.ts +111 -0
  256. package/ui/rhine/original/quality-renderer.ts +97 -0
  257. package/ui/rhine/original/quality-settings.ts +111 -0
  258. package/ui/rhine/original/render-quality.ts +163 -0
  259. package/ui/rhine/original/scene.ts +2051 -0
  260. package/ui/rhine/original/shelf-interior.ts +142 -0
  261. package/ui/rhine/original/ui-transitions.ts +111 -0
  262. package/ui/rhine/original/viewer-camera.ts +92 -0
  263. package/ui/rhine/original/viewport-layout.ts +41 -0
  264. package/ui/rhine/original-ui.css +2334 -0
  265. package/ui/rhine/readability.css +46 -0
  266. package/ui/rhine/reading-object.ts +96 -0
  267. package/ui/rhine/reading-origin.ts +28 -0
  268. package/ui/rhine/reading-room.css +55 -0
  269. package/ui/rhine/report-markdown.ts +98 -0
  270. package/ui/rhine/scene-edge-navigation.ts +84 -0
  271. package/ui/rhine/scene.ts +1726 -0
  272. package/ui/rhine/shelf-layout.ts +38 -0
  273. package/ui/rhine/snapshot-update-gate.ts +22 -0
  274. package/ui/rhine/source-reading.ts +15 -0
  275. package/ui/rhine/source-selection.css +164 -0
  276. package/ui/rhine/temporary-performance-observers.ts +163 -0
  277. package/ui/rhine/temporary-performance-panel.ts +124 -0
  278. package/ui/rhine/temporary-performance.css +19 -0
  279. package/ui/rhine/temporary-performance.ts +552 -0
  280. package/ui/rhine/temporary-render-diagnostics.ts +105 -0
  281. package/ui/rhine/tool-activity.ts +69 -0
  282. package/ui/rhine/types.ts +219 -0
  283. package/ui/rhine/visible-scene-hit.ts +13 -0
  284. package/ui/rhine/workbench.css +818 -0
  285. package/ui/rhine/workbench.ts +2245 -0
  286. package/ui/rhine/workspace-travel.ts +32 -0
  287. package/vite.rhine.config.ts +19 -0
@@ -0,0 +1,104 @@
1
+ import type { ArchiveSource } from './types';
2
+ import { SurfaceTransition } from './original/ui-transitions';
3
+ import './evidence-inbox.css';
4
+
5
+ export interface EvidenceInboxView { boardId:string; title:string; count:number; titles:string[] }
6
+ export interface EvidenceInboxAnchor { x:number; y:number; visible:boolean }
7
+ type Options = {
8
+ mutate(payload:Record<string,unknown>):Promise<unknown>;
9
+ read(source:ArchiveSource):void; notify(message:string):void;
10
+ scene(value:EvidenceInboxView,open:boolean):void;
11
+ ask?(boardId:string,title:string):Promise<void>;
12
+ };
13
+ const el=<K extends keyof HTMLElementTagNameMap>(tag:K,cls='',text='')=>{const n=document.createElement(tag);n.className=cls;n.textContent=text;return n;};
14
+ const button=(cls:string,text:string,fn:()=>void)=>{const n=el('button',cls,text);n.type='button';n.onclick=fn;return n;};
15
+
16
+ /** A board-owned staging queue. Opening a source and pinning a clue are separate actions. */
17
+ export function mountEvidenceInbox(host:HTMLElement,options:Options){
18
+ let board:any=null,sources:any[]=[],active=false,opened=false,busy=false,disposed=false,selected='',page=0,editing=false;
19
+ let anchor:EvidenceInboxAnchor={x:0,y:0,visible:false},signature='';
20
+ const motion=matchMedia('(prefers-reduced-motion: reduce)'),events=new AbortController();
21
+ const entry=button('rhine-inbox-entry','',()=>open());entry.setAttribute('aria-label','打开重点证据盒');entry.setAttribute('aria-expanded','false');entry.hidden=true;
22
+ const entryLabel=el('span','','重点证据盒'),count=el('b','','00'),entryHint=el('small','','待整理 ↗');entry.append(entryLabel,count,entryHint);
23
+ const panel=el('section','rhine-inbox-panel');panel.hidden=true;panel.setAttribute('role','dialog');panel.setAttribute('aria-label','重点证据盒');panel.tabIndex=-1;
24
+ const header=el('header'),kicker=el('span','','EVIDENCE / 待整理'),close=button('rhine-inbox-close','返回调查板 ↗',()=>hide());header.append(kicker,close);
25
+ const content=el('div','rhine-inbox-content'),title=el('h2','','重点证据盒'),subtitle=el('p','rhine-inbox-subtitle');
26
+ const tools=el('div','rhine-inbox-tools'),filter=el('input','rhine-inbox-filter');filter.type='search';filter.placeholder='按标题或待核对问题查找';filter.setAttribute('aria-label','筛选重点证据');
27
+ const ask=button('rhine-inbox-ask','交给 Agent 整理 ↗',()=>{if(!board||!options.ask||busy)return;const target=board;busy=true;paint();void options.ask(target.id,target.title).then(()=>options.notify('已请 Agent 继续整理这块调查板的证据盒。')).catch(fail).finally(()=>{busy=false;paint();});});ask.hidden=!options.ask;
28
+ tools.append(filter,ask);
29
+ const list=el('div','rhine-inbox-list');list.setAttribute('role','list');
30
+ const pager=el('div','rhine-inbox-pager'),prev=button('','← 上一页',()=>{page--;renderList();}),next=button('','下一页 →',()=>{page++;renderList();}),pageText=el('span');pager.append(prev,pageText,next);
31
+ const details=el('div','rhine-inbox-details'),detailTitle=el('h3'),meta=el('p','rhine-inbox-meta'),note=el('p','rhine-inbox-note');
32
+ const actions=el('div','rhine-inbox-actions'),read=button('rhine-inbox-read','阅读原文 ↗',()=>{const source=currentSource();if(source)options.read({...source,id:source.sourceId} as ArchiveSource);}),promote=button('rhine-inbox-promote','整理上板 +',()=>edit()),remove=button('rhine-inbox-remove','移出',()=>{if(selected)void mutate({action:'remove',source_id:selected});});actions.append(read,promote,remove);details.append(detailTitle,meta,note,actions);
33
+ const form=el('form','rhine-inbox-form');form.hidden=true;
34
+ const heading=el('input');heading.maxLength=120;heading.required=true;heading.setAttribute('aria-label','线索标题');
35
+ const body=el('textarea');body.maxLength=12000;body.required=true;body.rows=5;body.setAttribute('aria-label','线索摘记');
36
+ const kind=el('select');kind.setAttribute('aria-label','线索款式');for(const [value,label]of [['excerpt','原文摘录'],['finding','研究发现'],['question','未解问题'],['contrast','交叉对照'],['relation','关键关联'],['time','时间节点']]){const o=el('option','',label);o.value=value;kind.append(o);}
37
+ const interpretation=el('select');interpretation.setAttribute('aria-label','核验状态');for(const [value,label]of [['question','待核验'],['observation','观察记录'],['inference','推断']]){const o=el('option','',label);o.value=value;interpretation.append(o);}
38
+ const formOptions=el('div','rhine-inbox-form-options');formOptions.append(kind,interpretation);
39
+ const submit=el('button','','保存线索并上板 ↗');submit.type='submit';const cancel=button('','取消',()=>{editing=false;paint();});
40
+ const formActions=el('div','rhine-inbox-actions');formActions.append(cancel,submit);
41
+ form.append(el('small','','整理成一条线索 · 原始来源会保留'),heading,body,formOptions,formActions);
42
+ form.onsubmit=event=>{event.preventDefault();if(!busy&&heading.value.trim()&&body.value.trim())void mutate({action:'promote',source_id:selected,title:heading.value,detail:body.value,kind:kind.value,interpretation:interpretation.value});};
43
+ content.append(title,subtitle,tools,list,pager,details,form);panel.append(header,content);host.append(entry,panel);
44
+ const transition=new SurfaceTransition(panel,content,280,220);
45
+ const pending=()=>board?.evidenceInbox?.filter((item:any)=>item.status==='pending')||[];
46
+ const currentItem=()=>pending().find((item:any)=>item.source_id===selected);
47
+ const currentSource=()=>sources.find(s=>s.id===selected);
48
+ function scene(){const items=pending();options.scene({boardId:board?.id||'',title:board?.title||'',count:items.length,titles:items.slice(0,6).map((item:any)=>sources.find(s=>s.id===item.source_id)?.title||'待整理资料')},opened&&active);}
49
+ function fail(error:any){if(!disposed)options.notify(error?.message||'证据盒同步失败,材料仍保留。');}
50
+ async function mutate(payload:Record<string,unknown>){
51
+ if(!board||busy)return;
52
+ const target=board.id;busy=true;paint();
53
+ try{const result:any=await options.mutate({board_id:target,expected_inbox_revision:board.inboxRevision,...payload});if(board?.id===target){editing=false;paint();}options.notify(result?.clue_id?'线索已放上证据板,原始资料仍可回查。':'已移出待整理区。');}
54
+ catch(error){fail(error);}finally{busy=false;paint();}
55
+ }
56
+ function renderList(){
57
+ const query=filter.value.trim().toLowerCase();const items=pending().filter((item:any)=>{const source=sources.find(s=>s.id===item.source_id);return !query||`${source?.title||''} ${item.note}`.toLowerCase().includes(query);});
58
+ page=Math.max(0,Math.min(page,Math.ceil(items.length/6)-1));list.replaceChildren();
59
+ if(!items.length)list.append(el('p','rhine-inbox-empty',query?'没有匹配的重点材料。':board?'暂时没有待整理的证据。\n可从检索阵列、档案架或原文页放入;Agent 也会挑选值得核对的资料。':'先选择或创建一块调查板,再收集重点证据。'));
60
+ for(const item of items.slice(page*6,page*6+6)){
61
+ const source=sources.find(s=>s.id===item.source_id),row=el('div','rhine-inbox-row');row.setAttribute('role','listitem');
62
+ const select=button('rhine-inbox-select','',()=>{selected=item.source_id;editing=false;paint();});select.dataset.sourceId=item.source_id;select.setAttribute('aria-pressed',String(item.source_id===selected));select.disabled=busy;
63
+ select.append(el('small','',`${item.addedBy==='user'?'手动选入':'Agent 筛选'} · ${source?.agentRead?'Agent 已读':'待核对原文'}`),el('strong','',source?.title||'资料暂不可用'),el('span','',item.note||'等待进一步核对与整理'));row.append(select);list.append(row);
64
+ }
65
+ prev.disabled=page===0||busy;next.disabled=(page+1)*6>=items.length||busy;pageText.textContent=`${items.length? page*6+1:0}–${Math.min((page+1)*6,items.length)} / ${items.length}`;pager.hidden=items.length<=6;
66
+ }
67
+ function edit(){const source=currentSource();if(!source)return;editing=true;heading.value=source.title.slice(0,120);body.value=(source.excerpt||currentItem()?.note||'').slice(0,12000);kind.value='excerpt';interpretation.value='question';paint();heading.focus();}
68
+ function paint(){
69
+ if(disposed)return;const items=pending();if(!items.some((item:any)=>item.source_id===selected)){selected=items[0]?.source_id||'';editing=false;}
70
+ count.textContent=String(items.length).padStart(2,'0');entry.setAttribute('aria-label',`打开重点证据盒,${items.length} 份待整理材料`);entry.title=board?`${board.title} · ${items.length} 份待整理`:'重点证据盒';
71
+ subtitle.textContent=board?`${board.title} · ${items.length} 份待整理材料`:'材料先暂存,整理后再上板';
72
+ ask.disabled=busy||!board||!items.length;filter.disabled=busy;
73
+ content.classList.toggle('is-editing',editing);renderList();const item=currentItem(),source=currentSource();details.hidden=!item||editing;form.hidden=!editing;
74
+ if(item){detailTitle.textContent=source?.title||'资料暂不可用';meta.textContent=`${({source:'资料',web:'网页',story:'剧情原文',wiki:'Wiki',entity:'实体资料'} as Record<string,string>)[source?.kind]||source?.kind||'资料'} · ${source?.origin==='web'?'联网来源':source?.origin==='cloud'?'云端资料':'本地资料'} · ${source?.agentRead?'Agent 已读':'仅有返回摘要'}`;note.textContent=item.note||source?.excerpt||'等待核对原文。';}
75
+ read.disabled=busy||!source;promote.disabled=busy||!source;remove.disabled=busy||!item;submit.disabled=cancel.disabled=heading.disabled=body.disabled=kind.disabled=interpretation.disabled=busy;
76
+ syncEntry();scene();
77
+ }
78
+ function syncEntry(){
79
+ entry.hidden=!active||opened;entry.setAttribute('aria-expanded',String(opened));
80
+ entry.classList.toggle('is-anchored',anchor.visible);entry.style.left=anchor.visible?`${anchor.x}px`:'';entry.style.top=anchor.visible?`${anchor.y}px`:'';
81
+ }
82
+ function open(){if(!active)return;opened=true;host.classList.add('has-evidence-inbox');transition.show(motion.matches);paint();close.focus({preventScroll:true});}
83
+ function hide(immediate=false){
84
+ opened=false;scene();
85
+ const done=()=>{host.classList.remove('has-evidence-inbox');syncEntry();if(active&&!immediate)entry.focus({preventScroll:true});};
86
+ if(immediate){transition.dispose();panel.hidden=true;delete panel.dataset.transition;done();}else transition.hide(motion.matches,done);
87
+ }
88
+ function handleKeydown(event:KeyboardEvent){
89
+ if(panel.hidden||!active||event.isComposing)return false;
90
+ if(event.key==='Escape'){event.preventDefault();if(panel.dataset.transition!=='closing'){if(editing){editing=false;paint();promote.focus();}else hide();}return true;}
91
+ if(event.key==='Tab'){const focusable=[...panel.querySelectorAll<HTMLElement>('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled)')].filter(n=>n.getClientRects().length);const first=focusable[0],last=focusable.at(-1);if(first&&(!panel.contains(document.activeElement)||event.shiftKey&&document.activeElement===first||!event.shiftKey&&document.activeElement===last)){event.preventDefault();(event.shiftKey?last!:first).focus();}return true;}
92
+ return panel.contains(event.target as Node);
93
+ }
94
+ motion.addEventListener('change',()=>{if(motion.matches)transition.finish();},{signal:events.signal});
95
+ filter.oninput=()=>{page=0;renderList();};
96
+ return {open,hide,handleKeydown,isOpen:()=>opened,
97
+ setData(value:any,rows:any[]){const next=JSON.stringify([value?.id,value?.title,value?.inboxRevision,rows.map(s=>[s.id,s.title,s.agentRead])]);const changed=board?.id!==value?.id;board=value;sources=rows;if(changed){selected='';page=0;editing=false;filter.value='';hide(true);}if(signature!==next){signature=next;paint();}},
98
+ setActive(value:boolean){active=value;if(!active)hide(true);syncEntry();},
99
+ setAnchor(value:EvidenceInboxAnchor){anchor=value;syncEntry();},
100
+ state(){return{boardId:board?.id||'',title:board?.title||'',count:pending().length,titles:pending().slice(0,6).map((item:any)=>sources.find(s=>s.id===item.source_id)?.title||'待整理资料')};},
101
+ stats(){return{open:opened,count:pending().length,boardId:board?.id||'',selected,editing,busy};},
102
+ dispose(){disposed=true;transition.dispose();events.abort();host.classList.remove('has-evidence-inbox');entry.remove();panel.remove();}
103
+ };
104
+ }
@@ -0,0 +1,12 @@
1
+ import './workbench.css'
2
+ import './evidence-board.css'
3
+ import './evidence-board-controls.css'
4
+ import './readability.css'
5
+ import './reading-room.css'
6
+ import './source-selection.css'
7
+ import './manual-search.css'
8
+ import './interaction.css'
9
+ export { mountRhineWorkbench } from './workbench'
10
+ export { createRhineScene } from './scene'
11
+ export type * from './types'
12
+ export const RUNTIME_ABI = 1
@@ -0,0 +1,20 @@
1
+ /* Interaction controls share the transparent HUD surface. */
2
+ .rhine-workbench [data-layout] .rhine-follow-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:10px 0;color:#727963;font-size:11px;line-height:1.5}
3
+ .rhine-workbench [data-layout] .rhine-follow-toggle{color:#4b5c3c;font-size:12px;min-height:30px;white-space:nowrap}
4
+ .rhine-workbench [data-layout] .rhine-evidence-target{display:flex;align-items:center;gap:10px;min-width:0;margin:8px 0;font-size:11px;line-height:1.5;color:#737965;background:transparent;flex:none}
5
+ .rhine-workbench [data-layout] .rhine-evidence-target>span{flex:none}
6
+ .rhine-workbench [data-layout] .rhine-evidence-target select{min-width:0;max-width:100%;flex:1;background:transparent;color:#4f5c42;border:0;border-bottom:1px solid #7c81643d;border-radius:0;padding:5px 18px 5px 0;font:inherit;text-overflow:ellipsis;cursor:pointer}
7
+ .rhine-workbench [data-layout] .rhine-evidence-target select:focus-visible{outline:1px solid #7a845e;outline-offset:2px}
8
+ .rhine-workbench [data-layout] .rhine-evidence-target option{background:#eeece5;color:#30382d}
9
+ .rhine-workbench [data-layout] .rhine-evidence-target select:disabled{opacity:.55;cursor:wait}
10
+ .rhine-workbench [data-layout] .rhine-agent-board-context{font-size:10px;color:#7b806d;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-top:6px;line-height:1.5}
11
+ .rhine-workbench [data-layout] .rhine-shelf-current .rhine-evidence-target{margin-bottom:0}
12
+ .rhine-workbench [data-layout] .rhine-reader .rhine-evidence-target{margin:5px 0}
13
+ .rhine-workbench [data-layout] .rhine-index-modal>.rhine-evidence-target{margin:0 0 8px}
14
+ @media(max-width:650px){.rhine-workbench [data-layout] .rhine-follow-controls{gap:8px;margin:5px 0}.rhine-workbench [data-layout] .rhine-evidence-target{gap:6px;font-size:10px}}
15
+
16
+ .rhine-workbench [data-layout=compact] .rhine-desk-panel{top:120px}
17
+ .rhine-workbench [data-layout=compact] .rhine-desk-panel>.rhine-rack-intro{display:none}
18
+ .rhine-workbench [data-layout=compact] .rhine-desk-panel h2{margin:6px 0}
19
+ .rhine-workbench [data-layout=portrait] .rhine-desk-panel{top:210px}
20
+ .rhine-workbench [data-layout=portrait] .rhine-desk-panel>.rhine-rack-intro{display:none}
@@ -0,0 +1,5 @@
1
+ /** Shared board-local rectangle for the interactive print, at 100 CSS pixels per world unit. */
2
+ export const INVESTIGATION_BOARD_PLANE = {
3
+ left: -7.75, top: 4.45, width: 15.5, height: 8.7, z: 0.181,
4
+ pixelWidth: 1550, pixelHeight: 870,
5
+ } as const;
@@ -0,0 +1,34 @@
1
+ /* Coordinates are printed on the board at 100px per world unit; the camera supplies the perspective. */
2
+ .rhine-investigation-hud{position:absolute;left:0;top:0;z-index:8;transform-origin:0 0;transition:none;pointer-events:none;color:#343a2e;overflow:hidden}
3
+ .rhine-investigation-heading{position:absolute;left:30px;top:12px;width:1080px;height:56px;box-sizing:border-box;padding:6px 18px;display:flex;align-items:center;gap:16px;pointer-events:auto;font-size:18px;white-space:nowrap}
4
+ .rhine-investigation-index{display:flex;flex-direction:column;gap:3px;flex:none;width:172px;border-right:1px solid #96958055;color:#5f6155}
5
+ .rhine-investigation-index b{font-size:20px;font-weight:550;letter-spacing:.025em}.rhine-investigation-index small{font-size:11px;letter-spacing:.07em;color:#828371}
6
+ .rhine-investigation-picker{flex:1;min-width:0;border:0;border-bottom:1px solid #9b9a8766;background:transparent;color:#33392d;font:inherit;font-size:22px;padding:5px 6px;text-overflow:ellipsis}
7
+ .rhine-investigation-picker option{background:#eeece3;color:#33392d}
8
+ .rhine-investigation-heading button,.rhine-investigation-footer button{flex:none;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;padding:6px}
9
+ .rhine-investigation-heading button:hover,.rhine-investigation-footer button:hover{color:#8c764b}
10
+ .rhine-investigation-heading button:disabled,.rhine-investigation-footer button:disabled{opacity:.4;cursor:default}
11
+ .rhine-investigation-heading :focus-visible,.rhine-investigation-footer button:focus-visible{outline:1px solid #878f72;outline-offset:3px}
12
+ .rhine-investigation-status{margin-left:auto;color:#747b65;font-size:16px;max-width:260px;overflow:hidden;text-overflow:ellipsis}
13
+ .rhine-investigation-follow{color:#866b39!important;border-bottom:1px solid #ad9b75!important}
14
+ .rhine-investigation-footer{position:absolute;left:45px;right:45px;bottom:16px;height:24px;display:flex;align-items:center;gap:22px;font-size:16px;color:#727969;pointer-events:auto}
15
+ .rhine-investigation-footer button{padding:0 6px}
16
+ .rhine-investigation-hud [hidden]{display:none}
17
+ .rhine-investigation-hint{margin-right:auto;letter-spacing:.04em}
18
+ .rhine-investigation-empty{position:absolute;left:345px;top:320px;width:860px;pointer-events:none}
19
+ .rhine-investigation-empty small{font-size:15px;letter-spacing:.16em;color:#8c907e}
20
+ .rhine-investigation-empty h2{font-size:40px;font-weight:450;line-height:1.5;margin:22px 0}
21
+ .rhine-investigation-empty p{font-size:21px;line-height:2;color:#818675;max-width:790px;margin:0}
22
+ .rhine-investigation-drawer{position:absolute;right:3%;top:16%;bottom:17%;width:min(385px,40%);z-index:35;padding:24px;box-sizing:border-box;background:rgba(243,241,233,.94);backdrop-filter:blur(14px);border-left:1px solid #a9ab9855;box-shadow:-12px 0 40px #77715b0c;color:#333a2e;overflow:auto;scrollbar-width:thin;scrollbar-color:#979c8666 transparent}
23
+ .rhine-investigation-drawer-top{display:flex;justify-content:space-between;align-items:center;gap:12px;border-bottom:1px solid #b3b09c66;padding-bottom:18px}.rhine-investigation-drawer-top small{font-size:10px;letter-spacing:.08em}
24
+ .rhine-investigation-drawer button,.rhine-investigation-report button{cursor:pointer;background:transparent;border:0;color:#626f50;font:inherit;padding:8px 0;text-align:left}
25
+ .rhine-investigation-drawer h2{font-size:23px;line-height:1.6;font-weight:500}.rhine-investigation-drawer h3{font-size:12px;font-weight:500;margin-top:28px;letter-spacing:.05em}.rhine-investigation-clue-text{white-space:pre-wrap;font-size:14px;line-height:1.9;overflow-wrap:anywhere}
26
+ .rhine-investigation-provenance{font-size:10px;color:#899077;margin:12px 0 24px}.rhine-investigation-source{display:block;width:100%;padding:14px 0!important;border-bottom:1px solid #bcbba566!important;white-space:pre-line;line-height:1.7!important;font-size:12px!important;overflow-wrap:anywhere}.rhine-investigation-drawer blockquote{margin:14px 0;border-left:2px solid #969c7a;padding-left:14px;font-size:13px;line-height:1.8}
27
+ .rhine-investigation-drawer input,.rhine-investigation-drawer textarea{box-sizing:border-box;width:100%;margin-top:18px;padding:12px;border:1px solid #a9ad9988;background:#f8f7f166;color:inherit;font:inherit}.rhine-investigation-drawer textarea{min-height:250px;font-size:11px}
28
+ .rhine-investigation-report{position:absolute;inset:14% 10% 13% 24%;z-index:45;padding:30px 44px;box-sizing:border-box;background:rgba(246,244,237,.96);backdrop-filter:blur(18px);color:#2c3327;border-top:1px solid #a5ab9455;overflow:auto;scrollbar-width:thin;scrollbar-color:#969c8266 transparent;box-shadow:0 18px 65px #615f3b14}
29
+ .rhine-investigation-report header{display:flex;align-items:center;justify-content:space-between;gap:16px;font-size:11px;color:#838971;flex-wrap:wrap}.rhine-investigation-report select{border:0;background:transparent;color:inherit;max-width:50%;font:inherit}.rhine-investigation-report h2{font-size:28px;line-height:1.5;font-weight:550;margin:30px 0 15px}.rhine-investigation-report-notice{font-size:11px;color:#899078;padding-bottom:20px;border-bottom:1px solid #aeae9555}.rhine-investigation-report .rhine-report-body{display:block;max-height:none;height:auto;overflow:visible;padding:12px 0;font-size:15px;line-height:1.95}.rhine-investigation-report-evidence{border-top:1px solid #b1b29b55;margin-top:36px;padding-top:20px;font-size:12px}.rhine-investigation-report-evidence p{white-space:pre-line}
30
+ .rhine-investigation-hud[hidden],.rhine-investigation-drawer[hidden],.rhine-investigation-report[hidden]{display:none}
31
+ @media(max-width:900px){.rhine-investigation-drawer{right:4%;width:60%;top:24%;bottom:15%}.rhine-investigation-report{inset:18% 4% 12%;padding:22px}}
32
+ @media(prefers-reduced-motion:no-preference){.rhine-investigation-drawer,.rhine-investigation-report{animation:investigation-arrive .24s ease-out}@keyframes investigation-arrive{from{opacity:0;transform:translateY(-7px)}to{opacity:1;transform:none}}}
33
+
34
+ .rhine-workbench .rhine-evidence-panel>.rhine-evidence-kicker,.rhine-workbench .rhine-evidence-panel>.rhine-evidence-heading,.rhine-workbench .rhine-evidence-panel>.rhine-evidence-count{display:none}
@@ -0,0 +1,348 @@
1
+ import { mountEvidenceInbox, type EvidenceInboxView, type EvidenceInboxAnchor } from './evidence-inbox';
2
+ import { READING_ENTER_MS, READING_RETURN_MS, READING_FADE_EASING, readingFrameTime, type ReadingObject } from './reading-object';
3
+ import { SurfaceTransition } from './original/ui-transitions';
4
+ import { createPreviewCards, evidenceStorageKey, parseEvidenceCards, type EvidenceCard } from './evidence-board-model';
5
+ import { investigationPositions, investigationClueIndex } from './investigation-layout';
6
+ import { renderReportMarkdown } from './report-markdown';
7
+ import type { ArchiveSource, BoardAnchorFrame } from './types';
8
+ import { boardPlaneTransform } from './board-plane-transform';
9
+ import { INVESTIGATION_BOARD_PLANE } from './investigation-board-plane';
10
+ import { InvestigationEditRevisions } from './investigation-edits';
11
+ import { referenceSource } from './source-reading';
12
+ import './investigation-board.css';
13
+
14
+ type Panel = { closeTools():void; getCards(): EvidenceCard[]; remapIds(mapping:Record<string,string>,revisions?:Record<string,{content_revision:number;layout_revision:number}>):void; replaceCards(cards: EvidenceCard[], reset?: boolean): void; select(id: string | null, edit?: boolean): void };
15
+ export interface InvestigationContext {
16
+ sessionId: string;
17
+ boards: { id: string; title: string }[];
18
+ selectedId: string;
19
+ workingId: string;
20
+ ready: boolean;
21
+ }
22
+ type Options = { onContext?(value: InvestigationContext): void; onRackRevision?(revision:number):void; sessionId: string; api(endpoint: string, payload?: any, signal?: AbortSignal): Promise<any>;
23
+ panel: Panel; onInbox(value:EvidenceInboxView,open:boolean):void; askInbox?(boardId:string,title:string):Promise<void>; onReading(value: ReadingObject | null): void; onCards(cards: EvidenceCard[]): void; openSource(source: ArchiveSource, inbox?:boolean): void; notify(text: string): void };
24
+ const REPORT_ID = 'investigation-report';
25
+ const labels: Record<string, string> = { excerpt: '原文摘录', finding: '研究发现', time: '时间节点', relation: '关键关联', question: '未解问题', contrast: '交叉对照' };
26
+ const uid = () => crypto.randomUUID();
27
+ const clueSlot = investigationClueIndex;
28
+ const pageCount = (board:any) => Math.max(1,...activeClues(board).map((c:any)=>Math.floor(clueSlot(c)/12)+1));
29
+ const same = (a: any, b: any) => JSON.stringify(a) === JSON.stringify(b);
30
+ const activeClues = (board: any) => (board?.clues || []).filter((c: any) => !['superseded','retracted'].includes(c.status));
31
+ const el = <K extends keyof HTMLElementTagNameMap>(tag: K, className: string, text?: string) => { const n = document.createElement(tag); n.className = className; if (text !== undefined) n.textContent = text; return n; };
32
+
33
+ /** The viewed board is a browser preference; only investigation_open binds agent work. */
34
+ export function mountInvestigationBoard(host: HTMLElement, options: Options) {
35
+ let sessionId = options.sessionId, board: any = null, catalog: any[] = [], sourceRows: any[] = [];
36
+ let selectedId = '', workingId = '', followWorking = true, page = 0, commitSeq = 0, disposed = false, active = false;
37
+ let contextReady = false;
38
+ let syncing = false, saving = 0, refreshPending = false, loadEpoch = 0, loading = false;
39
+ let baseline: EvidenceCard[] = [], sceneCards: EvidenceCard[] = [];
40
+ let edits = new InvestigationEditRevisions(), pendingReset = false;
41
+ let editTarget = { boardId: '' };
42
+ let aborter = new AbortController(), queue = Promise.resolve(), inboxStageQueue = Promise.resolve();
43
+ const events = new AbortController();
44
+ const hud = el('section', 'rhine-investigation-hud'); hud.hidden = true;
45
+ hud.style.width = `${INVESTIGATION_BOARD_PLANE.pixelWidth}px`;
46
+ hud.style.height = `${INVESTIGATION_BOARD_PLANE.pixelHeight}px`;
47
+ let planeVisible = false;
48
+ function setBoardFrame(frame: BoardAnchorFrame) {
49
+ const transform = frame.visible && frame.surface.every(point => point.visible)
50
+ ? boardPlaneTransform(frame.surface, INVESTIGATION_BOARD_PLANE.pixelWidth, INVESTIGATION_BOARD_PLANE.pixelHeight) : null;
51
+ planeVisible = Boolean(transform);
52
+ if (transform) hud.style.transform = transform;
53
+ hud.hidden = !active || !planeVisible;
54
+ }
55
+ const header = el('div', 'rhine-investigation-heading');
56
+ const index = el('span','rhine-investigation-index');
57
+ index.append(el('b','','RHINE LAB'),el('small','','RESEARCH / 研究调查'));
58
+ const picker = el('select','rhine-investigation-picker'); picker.setAttribute('aria-label','切换调查板');
59
+ const status = el('span','rhine-investigation-status'); status.setAttribute('role','status');
60
+ const follow = el('button','rhine-investigation-follow','查看工作板 ↗'); follow.type='button'; follow.hidden=true;
61
+ const directory = el('button','rhine-investigation-directory','线索目录 ↗'); directory.type='button';
62
+ const add = el('button','rhine-investigation-add','+ 手动建板'); add.type='button';
63
+ header.append(index,picker,status,follow,directory,add); hud.append(header);
64
+ const footer=el('div','rhine-investigation-footer');
65
+ const previous=el('button','','←'), next=el('button','','→'), paging=el('span','','');
66
+ previous.type=next.type='button'; previous.setAttribute('aria-label','上一组线索'); next.setAttribute('aria-label','下一组线索');
67
+ const hint=el('span','rhine-investigation-hint','重要线索随调查加入 · 点击阅读,双击编辑'); footer.append(hint,previous,paging,next);hud.append(footer);
68
+ const empty=el('div','rhine-investigation-empty'); empty.append(el('small','','RESEARCH / READY'),el('h2','','从一个值得调查的问题开始'),el('p','','Agent 会在这里整理重要线索,并在中央留下报告。追问可以延续同一块板。'));hud.append(empty);
69
+ const drawer=el('aside','rhine-investigation-drawer');drawer.hidden=true;drawer.setAttribute('aria-label','线索详情');
70
+ const drawerTop=el('div','rhine-investigation-drawer-top'), close=el('button','','返回调查板 ↗');close.type='button';
71
+ const drawerLabel=el('small','','');drawerTop.append(drawerLabel,close);const drawerBody=el('div','rhine-investigation-drawer-body');drawer.append(drawerTop,drawerBody);
72
+ const report=el('section','rhine-investigation-report');report.hidden=true; report.setAttribute('role','dialog');report.setAttribute('aria-label','调查报告');report.tabIndex=-1;
73
+ const reportHeader=el('header',''), reportLabel=el('span','','RESEARCH REPORT / 调查报告'), versions=el('select','');versions.setAttribute('aria-label','报告版本');
74
+ const reportClose=el('button','','返回调查板 ↗');reportClose.setAttribute('aria-keyshortcuts','Escape');reportClose.type='button';reportHeader.append(reportLabel,versions,reportClose);
75
+ const reportTitle=el('h2','',''), reportNotice=el('p','rhine-investigation-report-notice'),reportArticle=el('article','rhine-report-body');
76
+ const reportIntro=el('div','rhine-investigation-report-intro');reportIntro.append(reportTitle,reportNotice);
77
+ const reportScroll=el('div','rhine-investigation-report-scroll');reportScroll.tabIndex=0;reportScroll.append(reportIntro,reportArticle);
78
+ report.append(reportHeader,reportScroll);host.append(hud,drawer,report);
79
+ const motion=window.matchMedia('(prefers-reduced-motion: reduce)');
80
+ const readingTransitions=new Map([[report,new SurfaceTransition(report,undefined,READING_ENTER_MS,READING_RETURN_MS,READING_FADE_EASING,READING_FADE_EASING,true)],
81
+ [drawer,new SurfaceTransition(drawer,undefined,READING_ENTER_MS,READING_RETURN_MS,READING_FADE_EASING,READING_FADE_EASING,true)]]);
82
+ let enterStartedAt:number|undefined,returnStartedAt:number|undefined;
83
+ const inbox=mountEvidenceInbox(host,{
84
+ scene:options.onInbox,notify:options.notify,ask:options.askInbox,
85
+ read:source=>options.openSource(source,true),
86
+ async mutate(payload){const currentSession=sessionId;try{const result=await request('inbox',{mutation_id:uid(),...payload});if(sessionId===currentSession)await refresh();return result;}catch(error){if(sessionId===currentSession)await refresh().catch(()=>{});throw error;}}
87
+ });
88
+
89
+ function syncReading(){
90
+ options.onReading(!active?null:!report.hidden?{title:reportTitle.textContent||'调查报告',code:`REPORT / V${versions.value}`,kind:'report',enterStartedAt,returnStartedAt}:!drawer.hidden?{title:drawerBody.querySelector('h2')?.textContent||drawerLabel.textContent||'线索记录',code:drawerLabel.textContent?.split(' / ')[0]||'EVIDENCE',kind:'clue',enterStartedAt,returnStartedAt}:null);
91
+ }
92
+ function showReading(layer:HTMLElement){
93
+ if(layer.hidden||!layer.dataset.transition){
94
+ enterStartedAt=readingFrameTime();readingTransitions.get(layer)!.show(motion.matches);
95
+ }
96
+ syncReading();
97
+ }
98
+ const visibility=new MutationObserver(()=>{
99
+ // Hiding a layer to open a source or leave the board must cancel its entrance,
100
+ // otherwise its completion callback would make the old report visible again.
101
+ for(const [layer,transition]of readingTransitions)if(layer.hidden){transition.dispose();delete layer.dataset.transition;}
102
+ syncReading();
103
+ });visibility.observe(report,{attributes:true,attributeFilter:['hidden']});visibility.observe(drawer,{attributes:true,attributeFilter:['hidden']});
104
+ function cancelReturn(){
105
+ if(returnStartedAt===undefined)return;
106
+ returnStartedAt=undefined;
107
+ for(const [layer,transition]of readingTransitions){transition.dispose();delete layer.dataset.transition;layer.removeAttribute('aria-busy');}
108
+ }
109
+ function closeReading(){
110
+ if(returnStartedAt!==undefined)return;
111
+ const layer=!report.hidden?report:!drawer.hidden?drawer:null;if(!layer)return;
112
+ returnStartedAt=readingFrameTime();layer.setAttribute('aria-busy','true');syncReading();
113
+ readingTransitions.get(layer)!.hide(motion.matches,()=>{
114
+ report.hidden=true;drawer.hidden=true;returnStartedAt=undefined;layer.removeAttribute('aria-busy');syncReading();
115
+ if(active&&!disposed)(picker.hidden?add:picker).focus({preventScroll:true});
116
+ });
117
+ }
118
+ motion.addEventListener('change',()=>{if(motion.matches)for(const transition of readingTransitions.values())transition.finish();},{signal:events.signal});
119
+ function handleKeydown(event:KeyboardEvent){
120
+ if(inbox.handleKeydown(event))return true;
121
+ const layer=!report.hidden?report:!drawer.hidden?drawer:null;if(!active||!layer||event.isComposing)return false;
122
+ if(returnStartedAt!==undefined){event.preventDefault();return true;}
123
+ if(event.key==='Escape'){event.preventDefault();closeReading();return true;}
124
+ if(event.key==='Tab'){
125
+ const focusable=[...layer.querySelectorAll<HTMLElement>('button:not(:disabled),a[href],input,textarea,select,summary,[tabindex="0"]')].filter(n=>n.getClientRects().length&&!n.closest('[hidden]'));
126
+ const first=focusable[0],last=focusable.at(-1);
127
+ if(first&&(!layer.contains(document.activeElement)||event.shiftKey&&document.activeElement===first||!event.shiftKey&&document.activeElement===last)){event.preventDefault();(event.shiftKey?last!:first).focus();}
128
+ }
129
+ return layer.contains(event.target as Node)||event.key==='Tab';
130
+ }
131
+ const preference=()=>`prts-investigation-view:${encodeURIComponent(sessionId)}`;
132
+ function request(endpoint: string, payload: any = {}, signal=aborter.signal) { return options.api(`investigation.${endpoint}`,{ session_id:sessionId,...payload },signal); }
133
+ function fail(error: any) { if (disposed || error?.name==='AbortError' || aborter.signal.aborted) return; loading=false;render();status.textContent='同步未完成'; options.notify(error?.message || '调查板暂时无法同步,请稍后重试'); }
134
+ function sourceFor(ref: any): ArchiveSource | null {
135
+ const s=sourceRows.find(s=>s.id===ref.source_id);if(!s)return null;
136
+ return referenceSource({ ...s,id:s.sourceId,excerpt:s.excerpt||'',state:s.state } as ArchiveSource,ref);
137
+ }
138
+ function toCards(): EvidenceCard[] {
139
+ const clues=activeClues(board), offset=page*12, positions=investigationPositions(board?.clues || []);
140
+ return clues.filter((c:any)=>clueSlot(c)>=offset&&clueSlot(c)<offset+12).map((c:any)=>{
141
+ const i=clueSlot(c)%12;
142
+ const source=sourceRows.find(s=>s.id===c.sources[0]?.source_id);
143
+ const position=positions.get(c.id)||{x:0,y:2.65};
144
+ return {id:c.id,contentRevision:c.contentRevision,layoutRevision:c.layoutRevision,title:c.title,body:c.detail||c.summary,summary:c.summary,clueKind:c.kind,variant:c.variant,
145
+ evidenceLabel:c.interpretation==='inference'?'推断':c.kind==='question'?'待核验':c.sources.some((r:any)=>!sourceRows.find(s=>s.id===r.source_id)?.agentRead)?'含检索摘要':'已核对来源',
146
+ kind:c.kind==='question'?'question':c.kind==='excerpt'?'source':'note',stage:c.status==='unresolved'?0:c.importance==='key'?2:1,
147
+ position:c.position||position,rotation:c.rotation??[-1.6,.9,-.7][c.variant ?? i%3],scale:c.scale||1,
148
+ sourceId:source?.sourceId,sourceTitle:source?.title,
149
+ sourceLabel:[...new Set(c.sources.map((r:any)=>r.source_id))].join(' · '),
150
+ relationKinds:Object.fromEntries(board.relations.filter((r:any)=>r.from===c.id).map((r:any)=>[r.to,r.type])),
151
+ links:board.relations.filter((r:any)=>r.from===c.id).map((r:any)=>r.to)};
152
+ });
153
+ }
154
+ function renderCards(reset=false) {
155
+ if(saving){refreshPending=true;return;}
156
+ baseline=toCards();edits.observe(board?.clues||[]);
157
+ syncing=true;options.panel.replaceCards(baseline,reset);syncing=false;
158
+ const latest=board?.reports.at(-1), running=catalog.find(b=>b.id===selectedId)?.run?.status==='running';
159
+ sceneCards=board?[{id:REPORT_ID,title:latest?.title||(running?'调查进行中':'等待调查报告'),body:latest?.summary||'重要线索正在汇入,报告将在调查完成后发布。',kind:'note',stage:2,clueKind:'report',
160
+ evidenceLabel:latest?`REPORT / V${String(latest.version).padStart(2,'0')}`:'RESEARCH / IN PROGRESS',sourceLabel:`${activeClues(board).length} 条线索 · ${latest?.sources?.length || 0} 份来源`,position:{x:0,y:0},rotation:0,scale:1},...baseline]:[];
161
+ options.onCards(sceneCards);
162
+ }
163
+ function getContext(): InvestigationContext {
164
+ return { sessionId, boards: catalog.map(({id,title})=>({id,title})), selectedId, workingId, ready: contextReady };
165
+ }
166
+ function holdReadingContext() {
167
+ // A source detour must return to the same board even if the agent starts another investigation.
168
+ followWorking=false;
169
+ try { if(selectedId)localStorage.setItem(preference(),selectedId); } catch {}
170
+ }
171
+ function render() {
172
+ options.onContext?.(getContext());
173
+ inbox.setData(board,sourceRows);
174
+ const value=selectedId;picker.replaceChildren();
175
+ for(const row of catalog){const option=el('option','',`${row.title} · ${row.clueCount} 条线索`);option.value=row.id;picker.append(option);}picker.value=value;
176
+ picker.disabled=!catalog.length||!!saving||loading;picker.hidden=!catalog.length;empty.hidden=!!board;directory.disabled=!board;
177
+ follow.disabled=add.disabled=!!saving||loading;
178
+ follow.hidden=!workingId||workingId===selectedId;
179
+ const row=catalog.find(b=>b.id===selectedId),latest=board?.reports.at(-1);
180
+ status.textContent=saving?'正在保存…':loading?'正在同步…':!board?'尚无调查':row?.run?.status==='running'?'● Agent 正在整理':row?.run?.status==='interrupted'?'已暂停 · 线索已保留':latest?`报告 V${latest.version}${row?.reportStale?' · 有新线索待更新':''}`:'线索已保存';
181
+ const pages=pageCount(board);previous.disabled=page===0||!!saving||loading;next.disabled=page>=pages-1||!!saving||loading;
182
+ paging.textContent=`${page+1} / ${pages} · ${activeClues(board).length} 条线索`;footer.hidden=!board;
183
+ }
184
+ async function refresh(reset=false) {
185
+ pendingReset ||= reset;
186
+ if(disposed||saving){refreshPending=true;return;}
187
+ const epoch=++loadEpoch;loading=true;render();
188
+ const directory=await request('get',{limit:128});if(disposed||epoch!==loadEpoch)return;
189
+ // Only the catalog snapshot may advance the watch cursor; a newer board read can race a catalog change.
190
+ catalog=directory.boards;workingId=directory.workingBoardId||'';commitSeq=directory.commitSeq;
191
+ options.onRackRevision?.(directory.rackRevision || 0);
192
+ const oldId=board?.id||'';
193
+ if(followWorking&&workingId)selectedId=workingId;
194
+ if(!catalog.some(b=>b.id===selectedId))selectedId=workingId||catalog[0]?.id||'';
195
+ let result:any=null;
196
+ if(selectedId){result=await request('get',{board_id:selectedId});if(disposed||epoch!==loadEpoch)return;}
197
+ if(oldId!==selectedId){
198
+ // Flush against the still-mounted board before replacing its editor.
199
+ options.panel.closeTools();
200
+ if(saving){pendingReset=true;refreshPending=true;loading=false;render();return;}
201
+ cancelReturn();page=0;drawer.hidden=true;report.hidden=true;edits=new InvestigationEditRevisions();editTarget={boardId:selectedId};
202
+ }
203
+ if(saving){refreshPending=true;loading=false;return;}
204
+ board=result?.board||null;sourceRows=result?.sources||[];
205
+ page=Math.min(page,pageCount(board)-1);loading=false;contextReady=true;
206
+ const resetCards=pendingReset||oldId!==selectedId;pendingReset=false;renderCards(resetCards);render();
207
+ }
208
+ async function loop(generation: AbortController) {
209
+ while(!disposed&&!generation.signal.aborted){
210
+ try{if(saving){await queue;continue;}const value=await request('watch',{after:commitSeq},generation.signal);if(generation!==aborter)return;if(value.commitSeq>commitSeq)await refresh();}
211
+ catch(error){if(generation.signal.aborted||disposed)return;status.textContent='连接暂断 · 保留当前内容';await new Promise<void>(resolve=>{const timer=setTimeout(resolve,3000);generation.signal.addEventListener('abort',()=>{clearTimeout(timer);resolve();},{once:true});});if(generation.signal.aborted||disposed)return;try{await refresh();}catch{}}
212
+ }
213
+ }
214
+ function showClue(id:string) {
215
+ cancelReturn();inbox.hide(true);
216
+ const clue=board?.clues.find((c:any)=>c.id===id);if(!clue)return;
217
+ options.panel.closeTools();options.panel.select(id,false);report.hidden=true;
218
+ drawerLabel.textContent=`${clue.id} / ${labels[clue.kind]}`;drawerBody.replaceChildren();
219
+ drawerBody.append(el('h2','',clue.title),el('p','rhine-investigation-provenance',`${clue.interpretation==='inference'?'推断':clue.interpretation==='question'?'待核验':'观察'} · ${clue.editedBy==='user'?'用户整理':'Agent 整理'}`));
220
+ const content=el('div','rhine-investigation-clue-text rhine-markdown');renderReportMarkdown(content,clue.detail||clue.summary);drawerBody.append(content);
221
+ const edit=el('button','','编辑这条线索 ↗');edit.type='button';edit.onclick=()=>{drawer.hidden=true;options.panel.select(id,true);};drawerBody.append(edit);
222
+ drawerBody.append(el('h3','','来源与核验'));
223
+ for(const ref of clue.sources){const source=sourceFor(ref);if(!source)continue;const button=el('button','rhine-investigation-source',`${source.agentRead?'已读':'检索摘要'} · ${source.title} ↗`);button.type='button';button.onclick=()=>options.openSource(source);drawerBody.append(button);if(ref.quote)drawerBody.append(el('blockquote','',ref.quote));}
224
+ if(!clue.sources.length)drawerBody.append(el('p','','尚无来源,不能作为已核实结论。'));
225
+ const relations=board.relations.filter((r:any)=>r.from===id||r.to===id);
226
+ if(relations.length){
227
+ drawerBody.append(el('h3','','关联线索'));
228
+ const names:Record<string,string>={supports:'支持',contradicts:'存在矛盾',precedes:'时间在先',relates:'相关'};
229
+ for(const relation of relations){
230
+ const outgoing=relation.from===id,otherId=outgoing?relation.to:relation.from;
231
+ const other=board.clues.find((c:any)=>c.id===otherId&&!['superseded','retracted'].includes(c.status));if(!other)continue;
232
+ const meaning=outgoing?names[relation.type]:relation.type==='supports'?'得到支持':relation.type==='precedes'?'时间在后':names[relation.type];
233
+ const button=el('button','rhine-investigation-source',`${meaning} · ${other.id}\n${other.title}${relation.label?'\n'+relation.label:''}`);button.type='button';
234
+ button.onclick=()=>{page=Math.floor(clueSlot(other)/12);renderCards();render();showClue(other.id);};drawerBody.append(button);
235
+ }
236
+ }
237
+ showReading(drawer);syncReading();
238
+ }
239
+ function showDirectory() {
240
+ cancelReturn();inbox.hide(true);
241
+ options.panel.closeTools();report.hidden=true;
242
+ drawerLabel.textContent='INVESTIGATION / 全部线索';drawerBody.replaceChildren();
243
+ for(const [i,c]of activeClues(board).entries()){const button=el('button','rhine-investigation-source',`${c.id} · ${labels[c.kind]}\n${c.title}`);button.type='button';button.onclick=()=>{page=Math.floor(clueSlot(c)/12);renderCards();render();showClue(c.id);};drawerBody.append(button);}showReading(drawer);syncReading();
244
+ }
245
+ function showReport(version?:number) {
246
+ cancelReturn();inbox.hide(true);
247
+ if(!board?.reports.length){options.notify('报告尚未发布,已保存的线索可以先阅读。');return;}
248
+ options.panel.closeTools();
249
+ versions.replaceChildren();for(const r of [...board.reports].reverse()){const o=el('option','',`V${r.version} · ${new Date(r.publishedAt).toLocaleString()}`);o.value=String(r.version);versions.append(o);}
250
+ const r=board.reports.find((r:any)=>r.version===version)||board.reports.at(-1);versions.value=String(r.version);reportTitle.textContent=r.title;
251
+ reportNotice.textContent=`${r.clues.length} 条依据 · ${r.sources.length} 份来源 · ${r.basisKnowledgeRevision!==board.knowledgeRevision?'发布后已有线索更新,以下保留当时版本':'已保存版本'}`;
252
+ renderReportMarkdown(reportArticle,r.markdown.replace(/\[(C\d+)\](?!\()/g, (_match:string,id:string) => r.clues.some((c:any)=>c.id===id) ? `[${id}](#investigation-clue-${id})` : `[${id}]`));
253
+ const details=el('details','rhine-investigation-report-evidence');details.append(el('summary','','查看此版本使用的线索与来源'));
254
+ for(const c of r.clues){const heading=el('h3','',`${c.id} · ${c.title}`);heading.id=`investigation-clue-${c.id}`;details.append(heading,el('p','',c.summary));for(const ref of c.sources){const s=r.sources.find((s:any)=>s.id===ref.source_id);if(s){const button=el('button','rhine-investigation-source',`${s.title}${ref.line_start?` · L${ref.line_start}–${ref.line_end}`:''} ↗`);button.type='button';button.onclick=()=>options.openSource(referenceSource({...s,id:s.sourceId} as ArchiveSource,ref));details.append(button);if(ref.quote)details.append(el('blockquote','',ref.quote));}}}reportArticle.append(details);
255
+ reportArticle.onclick=event=>{const anchor=(event.target as HTMLElement).closest<HTMLAnchorElement>('a[href^="#investigation-clue-"]');if(!anchor)return;event.preventDefault();details.open=true;reportArticle.querySelector(anchor.getAttribute('href')!)?.scrollIntoView({block:'nearest'});};
256
+ drawer.hidden=true;showReading(report);reportScroll.scrollTop=0;syncReading();report.focus({preventScroll:true});
257
+ }
258
+ function userChange(rawCards:EvidenceCard[]) {
259
+ if(syncing||disposed)return;
260
+ const cards=structuredClone(rawCards), before=structuredClone(baseline), sources=sourceRows;
261
+ const ledger=edits, context=editTarget, target=board?.id||context.boardId, currentSession=sessionId, generation=aborter;
262
+ const current=()=>!disposed&&generation===aborter;
263
+ // Build again when the queue reaches this edit: prior writes may assign IDs
264
+ // and advance revisions while the user keeps typing or dragging a slider.
265
+ function prepare() {
266
+ const changes:any[]=[],relations:any[]=[],live=new Set(cards.map(c=>ledger.id(c.id)));
267
+ for(const c of cards){
268
+ const id=ledger.id(c.id),old=before.find(o=>ledger.id(o.id)===id),existing=ledger.has(c.id);
269
+ const content={title:c.title,summary:!old?c.summary??c.body.slice(0,480):c.body.slice(0,480),detail:c.body,
270
+ kind:!old||c.kind===old.kind?c.clueKind||(c.kind==='question'?'question':c.kind==='source'?'excerpt':'finding'):c.kind==='question'?'question':c.kind==='source'?'excerpt':'finding',
271
+ importance:c.stage===2?'key':'supporting',status:c.stage===0?'unresolved':'active'};
272
+ if(!old&&!existing)changes.push({...content,client_key:c.id,position:c.position,scale:c.scale,rotation:c.rotation,
273
+ sources:sources.filter(s=>s.sourceId===c.sourceId).map(s=>({source_id:s.id})),interpretation:'question'});
274
+ else{
275
+ if(!old||c.title!==old.title||c.body!==old.body||c.kind!==old.kind||c.stage!==old.stage)
276
+ changes.push({...content,id,expected_content_revision:ledger.revision(c.id,c.contentRevision,'content_revision')});
277
+ if(!old||!same(c.position,old.position)||c.scale!==old.scale||c.rotation!==old.rotation)
278
+ changes.push({id,action:'layout',expected_layout_revision:ledger.revision(c.id,c.layoutRevision,'layout_revision'),position:c.position,scale:c.scale,rotation:c.rotation});
279
+ }
280
+ const oldLinks=(old?.links||[]).map(to=>ledger.id(to)),links=(c.links||[]).map(to=>ledger.id(to));
281
+ for(const to of c.links||[])if(!oldLinks.includes(ledger.id(to)))relations.push({from:id,to:ledger.id(to),type:c.relationKinds?.[to]||'relates'});
282
+ // Retraction hides endpoints; preserve those edges for undo. Only an
283
+ // explicit unlink between two visible cards removes a stored relation.
284
+ for(const to of oldLinks)if(live.has(to)&&!links.includes(to))relations.push({from:id,to,remove:true});
285
+ }
286
+ for(const c of before)if(!live.has(ledger.id(c.id)))changes.push({id:ledger.id(c.id),action:'retract',expected_content_revision:ledger.revision(c.id,c.contentRevision,'content_revision')});
287
+ return {changes,relations};
288
+ }
289
+ const {changes,relations}=prepare();
290
+ if(!changes.length&&!relations.length){options.onCards([...sceneCards.filter(c=>c.id===REPORT_ID),...cards]);return;}
291
+ baseline=cards;saving++;render();
292
+ options.onCards([...sceneCards.filter(c=>c.id===REPORT_ID),...cards]);
293
+ const send=(endpoint:string,payload:any)=>options.api(`investigation.${endpoint}`,{session_id:currentSession,...payload},generation.signal);
294
+ queue=queue.then(async()=>{
295
+ if(!current())throw new DOMException('编辑会话已切换','AbortError');
296
+ let boardId=target||context.boardId;
297
+ if(!boardId){const created=await send('create',{mutation_id:uid()});boardId=created.board_id;context.boardId=boardId;if(!current())throw new DOMException('编辑会话已切换','AbortError');selectedId=boardId;followWorking=false;}
298
+ const payload=prepare();
299
+ const result=await send('edit',{mutation_id:uid(),board_id:boardId,...payload});
300
+ if(!current())return;
301
+ ledger.acknowledge(payload.changes,result);
302
+ options.panel.remapIds(result.created_ids,result.clue_revisions);
303
+ }).catch(error=>{
304
+ const draft=JSON.stringify(cards,null,2);try{localStorage.setItem(`prts-investigation-draft:${currentSession}:${target}`,draft);}catch{}
305
+ if(!current())return;
306
+ fail(error);drawerLabel.textContent='编辑尚未保存';drawerBody.replaceChildren(el('p','','内容出现冲突或同步失败,草稿已保留。复制下面的内容,刷新后可重新编辑。'));const area=el('textarea','');area.value=draft;area.readOnly=true;drawerBody.append(area);showReading(drawer);
307
+ }).finally(async()=>{if(!current())return;saving--;if(!saving){refreshPending=false;try{await refresh();}catch(error){fail(error);}}});
308
+ }
309
+ async function migrate() {
310
+ let cards:EvidenceCard[]=[];try{const raw=localStorage.getItem(evidenceStorageKey(sessionId));if(raw){const parsed=JSON.parse(raw);cards=parseEvidenceCards(parsed.cards);const previews=new Map(createPreviewCards().map(c=>[c.id,c]));cards=cards.filter(c=>{const original=previews.get(c.id);return !original||c.title!==original.title||c.body!==original.body;});}}catch{}
311
+ if(cards.length)await request('import',{migration_id:`legacy:${sessionId}`,cards});
312
+ }
313
+ async function start() {
314
+ const generation=aborter;
315
+ try{const preferenceValue=localStorage.getItem(preference());selectedId=preferenceValue||'';followWorking=!preferenceValue;}catch{}
316
+ try{await migrate();if(generation!==aborter||disposed)return;await refresh(true);if(generation===aborter&&!disposed)void loop(generation);}catch(error){if(generation===aborter&&!disposed){fail(error);void loop(generation);}}
317
+ }
318
+ picker.onchange=()=>{selectedId=picker.value;followWorking=false;page=0;try{localStorage.setItem(preference(),selectedId);}catch{};void refresh(true).catch(fail);};
319
+ follow.onclick=()=>{followWorking=true;page=0;try{localStorage.removeItem(preference());}catch{};void refresh(true).catch(fail);};
320
+ previous.onclick=()=>{page=Math.max(0,page-1);renderCards(true);render();};next.onclick=()=>{page++;renderCards(true);render();};
321
+ directory.onclick=showDirectory;close.onclick=closeReading;reportClose.onclick=closeReading;versions.onchange=()=>showReport(Number(versions.value));
322
+ add.onclick=()=>{cancelReturn();options.panel.closeTools();report.hidden=true;drawerLabel.textContent='NEW / 新建手动调查';drawerBody.replaceChildren();const input=el('input','');input.placeholder='调查标题';input.maxLength=120;input.setAttribute('aria-label','新调查标题');const button=el('button','','创建调查板');button.type='button';button.onclick=async()=>{if(!input.value.trim())return;button.disabled=true;try{const r=await request('create',{mutation_id:uid(),title:input.value.trim()});selectedId=r.board_id;followWorking=false;drawer.hidden=true;await refresh(true);}catch(error){fail(error);button.disabled=false;}};drawerBody.append(input,button);showReading(drawer);input.focus();};
323
+ void start();
324
+ return { userChange, handleKeydown, getContext, holdReadingContext, getCards:()=>sceneCards, select(id:string|null,edit=false){cancelReturn();drawer.hidden=true;report.hidden=true;if(id===REPORT_ID){showReport();return;}if(id&&!edit)showClue(id);else options.panel.select(id,edit);},
325
+ resumeReading(kind:'report'|'clue'|'inbox'){if(!active)return;if(kind==='inbox'){inbox.open();return;}cancelReturn();if(kind==='report'){showReading(report);report.focus({preventScroll:true});}else showReading(drawer);syncReading();},
326
+ setBoardFrame, setInboxAnchor:(value:EvidenceInboxAnchor)=>inbox.setAnchor(value), getInboxState:()=>inbox.state(), openInbox(){options.panel.closeTools();cancelReturn();report.hidden=true;drawer.hidden=true;syncReading();inbox.open();},
327
+ setActive(value:boolean){active=value;inbox.setActive(value);hud.hidden=!value||!planeVisible;if(!value){cancelReturn();drawer.hidden=true;report.hidden=true;}syncReading();},
328
+ async setSession(id:string){if(id===sessionId)return;cancelReturn();aborter.abort();aborter=new AbortController();loadEpoch++;sessionId=id;board=null;selectedId='';workingId='';contextReady=false;catalog=[];sourceRows=[];baseline=[];edits=new InvestigationEditRevisions();editTarget={boardId:''};queue=Promise.resolve();saving=0;refreshPending=false;pendingReset=false;commitSeq=0;page=0;drawer.hidden=true;report.hidden=true;renderCards(true);render();await start();},
329
+ stageSource(source:ArchiveSource,targetId?:string){
330
+ const currentSession=sessionId,generation=aborter,targetAtClick=targetId ?? (workingId||selectedId);
331
+ const send=(endpoint:string,payload:Record<string,unknown>)=>options.api(`investigation.${endpoint}`,{session_id:currentSession,...payload},generation.signal);
332
+ const task=inboxStageQueue.then(async()=>{
333
+ if(disposed||sessionId!==currentSession||generation.signal.aborted)return;
334
+ if(loading||!catalog.length)await refresh();
335
+ if(disposed||sessionId!==currentSession||generation.signal.aborted)return;
336
+ let target=targetAtClick||workingId||selectedId;
337
+ if(!target){const created=await send('create',{mutation_id:uid()});if(sessionId!==currentSession||generation.signal.aborted)return;target=created.board_id;selectedId=target;followWorking=false;}
338
+ const current=await send('get',{board_id:target});
339
+ await send('inbox',{mutation_id:uid(),board_id:target,expected_inbox_revision:current.board.inboxRevision,action:'add',source_id:source.id,sources:[source],note:''});
340
+ if(sessionId===currentSession&&!disposed){await refresh();options.notify(`已放入「${current.board.title}」的重点证据盒。`);}
341
+ });
342
+ inboxStageQueue=task.catch(()=>{});return task;
343
+ },
344
+ async addSource(source:ArchiveSource){if(loading)await refresh();let target=selectedId;if(!target){const created=await request('create',{mutation_id:uid()});target=created.board_id;selectedId=target;followWorking=false;}await request('edit',{mutation_id:uid(),board_id:target,sources:[source],changes:[{title:source.title.slice(0,120),summary:source.excerpt.slice(0,480),detail:source.excerpt.slice(0,12000),kind:'excerpt',interpretation:'question',sources:[{source_id:source.id}]}]});await refresh();},
345
+ dispose(){disposed=true;inbox.dispose();cancelReturn();for(const transition of readingTransitions.values())transition.dispose();visibility.disconnect();options.onReading(null);aborter.abort();events.abort();hud.remove();drawer.remove();report.remove();},
346
+ stats(){return{inbox:inbox.stats(),selectedId,workingId,boards:catalog.length,clues:activeClues(board).length,reportVersions:board?.reports.length||0,reportOpen:!report.hidden,returning:returnStartedAt!==undefined,drawerOpen:!drawer.hidden,commitSeq,active,saving};},
347
+ };
348
+ }
@@ -0,0 +1,46 @@
1
+ type Revision = { content_revision: number; layout_revision: number };
2
+ type Axis = keyof Revision;
3
+
4
+ /** Advance only revisions acknowledged by this editor, never remote edits. */
5
+ export class InvestigationEditRevisions {
6
+ private aliases = new Map<string, string>();
7
+ private current = new Map<string, Revision>();
8
+ private created = new Map<string, Revision>();
9
+ private successors = new Map<string, Map<number, number>>();
10
+
11
+ id(value: string) { return this.aliases.get(value) || value; }
12
+ has(value: string) { return this.current.has(this.id(value)); }
13
+ observe(clues: { id: string; contentRevision: number; layoutRevision: number }[]) {
14
+ for (const clue of clues) this.current.set(clue.id, {
15
+ content_revision: clue.contentRevision, layout_revision: clue.layoutRevision,
16
+ });
17
+ }
18
+ revision(value: string, expected: number | undefined, axis: Axis) {
19
+ const id = this.id(value);
20
+ // A temporary card can inherit another card's revision when duplicated.
21
+ let revision = id !== value ? this.created.get(id)?.[axis] : expected;
22
+ revision ??= this.current.get(id)?.[axis];
23
+ const next = this.successors.get(`${id}:${axis}`);
24
+ while (revision !== undefined && next?.has(revision)) revision = next.get(revision);
25
+ return revision;
26
+ }
27
+ acknowledge(changes: { id?: string; expected_content_revision?: number; expected_layout_revision?: number }[],
28
+ result: { created_ids: Record<string, string>; clue_revisions: Record<string, Revision> }) {
29
+ for (const [from, to] of Object.entries(result.created_ids)) if (from !== to) {
30
+ this.aliases.set(from, to);
31
+ if (!this.created.has(to)) this.created.set(to, result.clue_revisions[to]);
32
+ }
33
+ for (const [id, revision] of Object.entries(result.clue_revisions)) this.current.set(id, revision);
34
+ for (const change of changes) {
35
+ if (!change.id) continue;
36
+ const revision = result.clue_revisions[change.id];
37
+ if (!revision) continue;
38
+ for (const axis of ['content_revision', 'layout_revision'] as const) {
39
+ const before = change[`expected_${axis}`], after = revision[axis];
40
+ if (before === undefined || after <= before) continue;
41
+ const key = `${change.id}:${axis}`, chain = this.successors.get(key) || new Map();
42
+ chain.set(before, after); this.successors.set(key, chain);
43
+ }
44
+ }
45
+ }
46
+ }