@citeark/agent 0.3.20

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 (347) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +128 -0
  3. package/data/dataset-source-registry.v1.json +300 -0
  4. package/dist/arkgraph/boot.js +6 -0
  5. package/dist/arkgraph/index.html +1 -0
  6. package/dist/arkgraph/viewer.css +1 -0
  7. package/dist/arkgraph/viewer.en.css +1 -0
  8. package/dist/arkgraph/viewer.en.js +49 -0
  9. package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
  10. package/dist/arkgraph/viewer.js +49 -0
  11. package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
  12. package/docker/claude-code/Dockerfile +97 -0
  13. package/docker/claude-code/codex-pro-relay.mjs +466 -0
  14. package/docker/claude-code/runtime-contract-check.mjs +79 -0
  15. package/docs/arkgraph-reading.md +79 -0
  16. package/docs/configuration.md +100 -0
  17. package/docs/integration.md +92 -0
  18. package/docs/maturity-plan.md +27 -0
  19. package/docs/npm-release.md +44 -0
  20. package/docs/paper-reading.md +40 -0
  21. package/docs/research-plan-granularity.md +27 -0
  22. package/docs/terminal.md +49 -0
  23. package/examples/toy-evaluation/compile-task.json +27 -0
  24. package/examples/toy-evaluation/paper.md +5 -0
  25. package/examples/toy-evaluation/repository/README.md +9 -0
  26. package/examples/toy-evaluation/repository/checkpoint.json +4 -0
  27. package/examples/toy-evaluation/repository/evaluate.py +17 -0
  28. package/examples/toy-evaluation/task.json +81 -0
  29. package/package.json +59 -0
  30. package/prompts/compile-research.md +58 -0
  31. package/prompts/execute-contract.md +72 -0
  32. package/prompts/execute-workspace-simple.md +51 -0
  33. package/prompts/execute-workspace.md +34 -0
  34. package/prompts/prepare-reproduction.md +82 -0
  35. package/prompts/repair-research.md +45 -0
  36. package/protocol/CAP.md +129 -0
  37. package/protocol/LICENSE +12 -0
  38. package/protocol/MAPPINGS.md +72 -0
  39. package/protocol/README.md +38 -0
  40. package/protocol/conformance-v2.0-alpha.1.json +36 -0
  41. package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
  42. package/protocol/examples/arkgraph/fixtures.mjs +49 -0
  43. package/protocol/examples/arkgraph/paper-free.json +291 -0
  44. package/protocol/examples/arkgraph/partial-failure.json +344 -0
  45. package/protocol/examples/arkgraph/training-evaluation.json +443 -0
  46. package/protocol/profiles/agent-trace.md +16 -0
  47. package/protocol/profiles/computational-run.md +16 -0
  48. package/protocol/profiles/core.md +15 -0
  49. package/protocol/profiles/public-bundle.md +18 -0
  50. package/protocol/profiles/reproduction.md +29 -0
  51. package/protocol/profiles/research-compilation.md +44 -0
  52. package/protocol/profiles/research-plan.md +39 -0
  53. package/protocol/profiles/restricted-evidence.md +15 -0
  54. package/runtime/bootstrap-autodl-runtime.sh +314 -0
  55. package/runtime/create-runtime-venv.sh +41 -0
  56. package/runtime/install-local-cpu-runtime.sh +23 -0
  57. package/runtime/install-scientific-runtime.sh +153 -0
  58. package/runtime/mineru/parse.py +62 -0
  59. package/runtime/mineru/requirements.txt +4 -0
  60. package/runtime/requirements-baseline.txt +38 -0
  61. package/schemas/cap/v2/activity.schema.json +47 -0
  62. package/schemas/cap/v2/agent.schema.json +32 -0
  63. package/schemas/cap/v2/assertion.schema.json +110 -0
  64. package/schemas/cap/v2/descriptor.schema.json +243 -0
  65. package/schemas/cap/v2/entity.schema.json +64 -0
  66. package/schemas/cap/v2/manifest.schema.json +67 -0
  67. package/schemas/cap/v2/relation.schema.json +82 -0
  68. package/schemas/compute-catalog.schema.json +63 -0
  69. package/schemas/compute-decision.schema.json +27 -0
  70. package/schemas/execution-contract.schema.json +1024 -0
  71. package/schemas/research-card.schema.json +30 -0
  72. package/schemas/research-inventory-draft.schema.json +366 -0
  73. package/schemas/research.schema.json +1044 -0
  74. package/schemas/result.schema.json +173 -0
  75. package/schemas/verification-policy.schema.json +47 -0
  76. package/schemas/verified-conclusion.schema.json +58 -0
  77. package/schemas/workspace-summary.schema.json +24 -0
  78. package/scripts/build-arkgraph-view.mjs +12 -0
  79. package/scripts/check-execution-feasibility.mjs +24 -0
  80. package/scripts/check-syntax.mjs +15 -0
  81. package/scripts/deterministic-asset-preparation.py +438 -0
  82. package/scripts/package-cap.mjs +23 -0
  83. package/scripts/package-local-agent.mjs +23 -0
  84. package/scripts/preview-arkgraph.mjs +25 -0
  85. package/scripts/replay-research-compiler-candidate.mjs +134 -0
  86. package/scripts/review-compiler-sources.mjs +44 -0
  87. package/scripts/run-asset-preparation.sh +17 -0
  88. package/scripts/run-research-plan.mjs +98 -0
  89. package/scripts/validate-asset-preparation.py +290 -0
  90. package/scripts/verify-local-runtime.mjs +57 -0
  91. package/scripts/verify-npm-package.mjs +57 -0
  92. package/src/adapters/paper2agent.mjs +107 -0
  93. package/src/assets/cache.mjs +159 -0
  94. package/src/assets/compute.mjs +98 -0
  95. package/src/assets/executor.mjs +145 -0
  96. package/src/assets/lifecycle.mjs +213 -0
  97. package/src/assets/manifest.mjs +242 -0
  98. package/src/assets/opportunistic-preparation.mjs +81 -0
  99. package/src/assets/plan.mjs +411 -0
  100. package/src/assets/prompts.mjs +29 -0
  101. package/src/assets/public-asset-probe.mjs +525 -0
  102. package/src/assets/qualification.mjs +119 -0
  103. package/src/assets/readiness.mjs +130 -0
  104. package/src/assets/reproduction-admission.mjs +355 -0
  105. package/src/assets/requirements.mjs +152 -0
  106. package/src/assets/source-grounding.mjs +341 -0
  107. package/src/assets/source-policy.mjs +118 -0
  108. package/src/autodl/client.mjs +260 -0
  109. package/src/autodl/ssh.mjs +380 -0
  110. package/src/autodl/tools.mjs +129 -0
  111. package/src/cap/redaction.mjs +38 -0
  112. package/src/cap/v2/archive.mjs +152 -0
  113. package/src/cap/v2/attestation.mjs +204 -0
  114. package/src/cap/v2/canonical-json.mjs +114 -0
  115. package/src/cap/v2/compilation-artifact.mjs +240 -0
  116. package/src/cap/v2/core.mjs +282 -0
  117. package/src/cap/v2/measurement-assessment-records.mjs +23 -0
  118. package/src/cap/v2/pipeline-artifact.mjs +922 -0
  119. package/src/cap/v2/read.mjs +41 -0
  120. package/src/cap/v2/reassessment-artifact.mjs +383 -0
  121. package/src/cap/v2/research-artifact.mjs +231 -0
  122. package/src/cap/v2/research-map-records.mjs +46 -0
  123. package/src/cap/v2/research-object-records.mjs +163 -0
  124. package/src/cap/v2/research-records.mjs +187 -0
  125. package/src/cap/v2/verify.mjs +642 -0
  126. package/src/cli.mjs +1146 -0
  127. package/src/compute/autodl-pro-compiler.mjs +347 -0
  128. package/src/compute/autodl-pro-executor.mjs +459 -0
  129. package/src/compute/autodl-pro-job.mjs +843 -0
  130. package/src/compute/autodl-pro-network.mjs +295 -0
  131. package/src/compute/autodl-pro-remote.mjs +810 -0
  132. package/src/compute/autodl-pro-staging.mjs +117 -0
  133. package/src/compute/campaign.mjs +110 -0
  134. package/src/compute/catalog.mjs +123 -0
  135. package/src/compute/checkpoint-protocol.mjs +154 -0
  136. package/src/compute/codex-account-lock.mjs +111 -0
  137. package/src/compute/codex-account-session.mjs +107 -0
  138. package/src/compute/compiler-profile.mjs +38 -0
  139. package/src/compute/compiler-router.mjs +23 -0
  140. package/src/compute/coordinator-recovery.mjs +210 -0
  141. package/src/compute/executor-router.mjs +29 -0
  142. package/src/compute/gcp-batch-compiler.mjs +685 -0
  143. package/src/compute/gcp-batch-executor.mjs +1215 -0
  144. package/src/compute/gcp-batch-failure.mjs +92 -0
  145. package/src/compute/gcp-batch-job.mjs +527 -0
  146. package/src/compute/gcp-batch-lifecycle.mjs +81 -0
  147. package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
  148. package/src/compute/local-codex-compiler.mjs +52 -0
  149. package/src/compute/measurement-hardware.mjs +128 -0
  150. package/src/compute/remote-attempt.mjs +226 -0
  151. package/src/compute/requirements.mjs +124 -0
  152. package/src/compute/research-phases.mjs +48 -0
  153. package/src/compute/scheduler.mjs +452 -0
  154. package/src/compute/shared-workloads.mjs +26 -0
  155. package/src/compute/stage-archive.mjs +79 -0
  156. package/src/contracts/campaign-contract.mjs +52 -0
  157. package/src/contracts/execution-contract.mjs +819 -0
  158. package/src/contracts/execution-mode.mjs +19 -0
  159. package/src/contracts/execution-timeouts.mjs +45 -0
  160. package/src/contracts/execution-workload.mjs +68 -0
  161. package/src/contracts/preflight-schema.mjs +25 -0
  162. package/src/contracts/public-contract.mjs +63 -0
  163. package/src/contracts/subject-tags.mjs +31 -0
  164. package/src/dashboard/data.mjs +898 -0
  165. package/src/dashboard/server.mjs +79 -0
  166. package/src/dashboard/static/dashboard.css +366 -0
  167. package/src/dashboard/static/dashboard.js +560 -0
  168. package/src/dashboard/static/index.html +85 -0
  169. package/src/deployment/community-policy.mjs +9 -0
  170. package/src/deployment/environment.mjs +112 -0
  171. package/src/deployment/guided.mjs +98 -0
  172. package/src/deployment/handoff.mjs +102 -0
  173. package/src/deployment/local-contract.mjs +31 -0
  174. package/src/deployment/local.mjs +100 -0
  175. package/src/deployment/prepare.mjs +46 -0
  176. package/src/deployment/recipe.mjs +108 -0
  177. package/src/deployment/supplement.mjs +51 -0
  178. package/src/deployment/terminal.mjs +43 -0
  179. package/src/diagnosis/renderer.mjs +75 -0
  180. package/src/diagnosis/target-failure.mjs +46 -0
  181. package/src/evidence/parser-registry.mjs +54 -0
  182. package/src/evidence/parsers/fasttext-classification.mjs +82 -0
  183. package/src/evidence/parsers/json-scalar.mjs +96 -0
  184. package/src/evidence/parsers/simcse-senteval.mjs +104 -0
  185. package/src/evidence/parsers/starspace-classification.mjs +78 -0
  186. package/src/evidence/registry.mjs +147 -0
  187. package/src/execution/runner-audit.mjs +473 -0
  188. package/src/gcp/auth.mjs +106 -0
  189. package/src/gcp/batch-client.mjs +120 -0
  190. package/src/gcp/resource-discovery.mjs +177 -0
  191. package/src/gcp/rest.mjs +82 -0
  192. package/src/gcp/secret-manager.mjs +34 -0
  193. package/src/gcp/signed-url.mjs +133 -0
  194. package/src/gcp/storage.mjs +220 -0
  195. package/src/graph/command.mjs +41 -0
  196. package/src/graph/execution.mjs +97 -0
  197. package/src/graph/model.mjs +37 -0
  198. package/src/graph/presentation.mjs +110 -0
  199. package/src/graph/query.mjs +159 -0
  200. package/src/graph/research-relations.mjs +69 -0
  201. package/src/graph/source-page.mjs +12 -0
  202. package/src/graph/source-preview.mjs +34 -0
  203. package/src/graph/validate.mjs +76 -0
  204. package/src/job.mjs +496 -0
  205. package/src/network/autodl-routing-proxy.mjs +462 -0
  206. package/src/network/egress-proxy.mjs +158 -0
  207. package/src/observability/event-contract.mjs +230 -0
  208. package/src/observability/pipeline-monitor.mjs +166 -0
  209. package/src/pipeline/orchestrator.mjs +1281 -0
  210. package/src/pipeline/recovery-error.mjs +11 -0
  211. package/src/pipeline/replay.mjs +304 -0
  212. package/src/pipeline/shared-execution.mjs +115 -0
  213. package/src/pipeline/stage-checkpoint.mjs +86 -0
  214. package/src/pipeline/stage-recovery.mjs +101 -0
  215. package/src/pipeline/targets.mjs +110 -0
  216. package/src/process.mjs +143 -0
  217. package/src/protocol.mjs +312 -0
  218. package/src/provider/codex-account.mjs +44 -0
  219. package/src/provider/codex-completion.mjs +49 -0
  220. package/src/provider/completion.mjs +292 -0
  221. package/src/provider/model-client.mjs +44 -0
  222. package/src/provider/model-route.mjs +29 -0
  223. package/src/provider/openrouter-readiness.mjs +189 -0
  224. package/src/provider/reader-bridge.mjs +35 -0
  225. package/src/provider/relay.mjs +263 -0
  226. package/src/provider/runtime-auth.mjs +40 -0
  227. package/src/public/cap.d.mts +90 -0
  228. package/src/public/cap.mjs +12 -0
  229. package/src/public/contracts.d.mts +2 -0
  230. package/src/public/host.mjs +171 -0
  231. package/src/public/operations.d.mts +11 -0
  232. package/src/public/presentation.d.mts +4 -0
  233. package/src/records/views.mjs +26 -0
  234. package/src/remote/command.mjs +178 -0
  235. package/src/remote/ssh.mjs +59 -0
  236. package/src/repository-origin.mjs +81 -0
  237. package/src/reproduction/evidence-feedback.mjs +96 -0
  238. package/src/reproduction/incomplete-initialization.mjs +25 -0
  239. package/src/reproduction/lifecycle.mjs +253 -0
  240. package/src/reproduction/plan.mjs +132 -0
  241. package/src/reproduction/prompts.mjs +70 -0
  242. package/src/reproduction/runner.mjs +188 -0
  243. package/src/reproduction/summary.mjs +130 -0
  244. package/src/reproduction/workspace-mode.mjs +7 -0
  245. package/src/research/automatic-admission.mjs +156 -0
  246. package/src/research/compiler-coverage.mjs +85 -0
  247. package/src/research/compiler-failure.mjs +24 -0
  248. package/src/research/compiler-normalization-guards.mjs +112 -0
  249. package/src/research/compiler-repair.mjs +3 -0
  250. package/src/research/compiler.mjs +853 -0
  251. package/src/research/continuation-selection.mjs +26 -0
  252. package/src/research/execution-graph-context.mjs +43 -0
  253. package/src/research/experiment-importance.mjs +15 -0
  254. package/src/research/inventory-handoff.mjs +104 -0
  255. package/src/research/inventory-revisions.mjs +32 -0
  256. package/src/research/mineru-local.mjs +73 -0
  257. package/src/research/paper-command.mjs +19 -0
  258. package/src/research/paper-markdown.mjs +180 -0
  259. package/src/research/paper-source-map.mjs +69 -0
  260. package/src/research/planning-policy.mjs +88 -0
  261. package/src/research/reference-materials.mjs +11 -0
  262. package/src/research/reproduction-scope.mjs +30 -0
  263. package/src/research/research-map.mjs +94 -0
  264. package/src/research/research-objects.mjs +88 -0
  265. package/src/research/source-discovery.mjs +646 -0
  266. package/src/research/source-observations.mjs +75 -0
  267. package/src/research/source-review-cli-mcp.mjs +26 -0
  268. package/src/research/source-review-input.mjs +209 -0
  269. package/src/research/source-review-local-codex.mjs +36 -0
  270. package/src/research/source-review-model.mjs +70 -0
  271. package/src/research/source-review.mjs +173 -0
  272. package/src/research/structure.mjs +3163 -0
  273. package/src/research-card/renderer.mjs +277 -0
  274. package/src/research-card/verified-conclusion.mjs +143 -0
  275. package/src/results/output-registry.mjs +183 -0
  276. package/src/runtime/claude-code.mjs +52 -0
  277. package/src/runtime/codex-capacity-retry.mjs +87 -0
  278. package/src/runtime/codex.mjs +64 -0
  279. package/src/runtime/config.mjs +157 -0
  280. package/src/runtime/final-output.mjs +40 -0
  281. package/src/runtime/index.mjs +21 -0
  282. package/src/runtime/local-codex.mjs +74 -0
  283. package/src/runtime/opencode.mjs +95 -0
  284. package/src/runtime/prompt.mjs +13 -0
  285. package/src/sandbox/docker.mjs +363 -0
  286. package/src/settings/command.mjs +297 -0
  287. package/src/settings/store.mjs +119 -0
  288. package/src/telemetry/pricing.mjs +68 -0
  289. package/src/telemetry/usage.mjs +265 -0
  290. package/src/terminal/events.mjs +97 -0
  291. package/src/terminal/input.mjs +40 -0
  292. package/src/terminal/plain.mjs +40 -0
  293. package/src/terminal/remote-stream.mjs +22 -0
  294. package/src/terminal/screen.mjs +214 -0
  295. package/src/terminal/transcript.mjs +69 -0
  296. package/src/util.mjs +107 -0
  297. package/src/verification/ai-assessor.mjs +534 -0
  298. package/src/verification/claim-evaluator.mjs +242 -0
  299. package/src/verification/evidence-context.mjs +165 -0
  300. package/src/verification/evidence-reader.mjs +95 -0
  301. package/src/verification/integrity.mjs +570 -0
  302. package/src/verification/tolerance.mjs +32 -0
  303. package/src/workloads/cpu-research-preparation.mjs +56 -0
  304. package/src/workloads/definition.mjs +74 -0
  305. package/src/workloads/phase-aware-reproduction.mjs +46 -0
  306. package/src/workloads/reproduction.mjs +85 -0
  307. package/src/workspace/command.mjs +242 -0
  308. package/src/workspace/control.mjs +49 -0
  309. package/src/workspace/entry.mjs +28 -0
  310. package/src/workspace/input.mjs +93 -0
  311. package/src/workspace/interactive.mjs +94 -0
  312. package/src/workspace/jobs.mjs +418 -0
  313. package/src/workspace/session.mjs +97 -0
  314. package/src/workspace/worker.mjs +137 -0
  315. package/ui/arkgraph/ambient-motion.mjs +10 -0
  316. package/ui/arkgraph/app.jsx +153 -0
  317. package/ui/arkgraph/boot.js +6 -0
  318. package/ui/arkgraph/camera-motion.mjs +20 -0
  319. package/ui/arkgraph/context-reveal.mjs +39 -0
  320. package/ui/arkgraph/details.css +3 -0
  321. package/ui/arkgraph/entry.jsx +28 -0
  322. package/ui/arkgraph/experiment-curves.mjs +17 -0
  323. package/ui/arkgraph/experiment-selection.mjs +15 -0
  324. package/ui/arkgraph/experiment-style.css +26 -0
  325. package/ui/arkgraph/experiment-ui.jsx +32 -0
  326. package/ui/arkgraph/frame.html +1 -0
  327. package/ui/arkgraph/graph-gestures.mjs +62 -0
  328. package/ui/arkgraph/label-layout.mjs +57 -0
  329. package/ui/arkgraph/locales/en.json +229 -0
  330. package/ui/arkgraph/locales/source-types.json +15 -0
  331. package/ui/arkgraph/localization-build.mjs +27 -0
  332. package/ui/arkgraph/material-build.mjs +23 -0
  333. package/ui/arkgraph/material-colors.mjs +39 -0
  334. package/ui/arkgraph/material-style.css +15 -0
  335. package/ui/arkgraph/open-graph.jsx +326 -0
  336. package/ui/arkgraph/outline.jsx +49 -0
  337. package/ui/arkgraph/package-lock.json +888 -0
  338. package/ui/arkgraph/package.json +17 -0
  339. package/ui/arkgraph/reading-layout.mjs +130 -0
  340. package/ui/arkgraph/reading-presentation.mjs +73 -0
  341. package/ui/arkgraph/record-detail.css +51 -0
  342. package/ui/arkgraph/record-details.jsx +29 -0
  343. package/ui/arkgraph/research-types.mjs +31 -0
  344. package/ui/arkgraph/selection-mark.jsx +6 -0
  345. package/ui/arkgraph/soft-spine.mjs +26 -0
  346. package/ui/arkgraph/steering-style.css +187 -0
  347. package/ui/arkgraph/style.css +272 -0
@@ -0,0 +1,326 @@
1
+ import {numberColor} from './reading-presentation.mjs';
2
+ import React,{useEffect,useLayoutEffect,useRef,useState} from 'react';
3
+ import ForceGraph from 'force-graph';
4
+ import {createGraphGestures} from './graph-gestures.mjs';
5
+ import {layoutLabels} from './label-layout.mjs';
6
+ import {wrapGraphLabel,planReadingLayout,readingNeighborhoodForce,labeledObjectCollision,readingObjectBody} from './reading-layout.mjs';
7
+ import {createCameraMotion} from './camera-motion.mjs';
8
+ import {spineControls,paintSpine} from './soft-spine.mjs';
9
+ import {ambientOffset,motionEnvelope,layoutPosition} from './ambient-motion.mjs';
10
+ import {createContextIndex,contextRecords,placeContext} from './context-reveal.mjs';
11
+ import {SelectionMark} from './selection-mark.jsx';
12
+ import {experimentCurve,curvePoint} from './experiment-curves.mjs';
13
+ import {MD,alpha,coreTypeOf,colorOf,shapeOf,semanticLabelOf} from './research-types.mjs';
14
+
15
+ // One fixed design coordinate system for nodes, labels, edges and collision geometry.
16
+ const DESIGN_SCALE=.25,UNIT=1/DESIGN_SCALE;
17
+ const baseRadius=n=>(n.main?(n.type==='claim'?14:12):5)*UNIT;
18
+ const radius=n=>baseRadius(n)+n.emphasis*1.5*UNIT;
19
+ const idOf=value=>typeof value==='object'?value.id:value;
20
+ const ease=value=>1-(1-value)**3;
21
+ const distanceOpacity=distance=>[1,.62,.4,.26][Math.min(3,distance)];
22
+ function distances(nodes,links,seeds){
23
+ const result=new Map(seeds.map(id=>[id,0])),adjacent=new Map(nodes.map(n=>[n.id,[]])),queue=[...seeds];
24
+ for(const link of links){const a=idOf(link.source),b=idOf(link.target);adjacent.get(a)?.push(b);adjacent.get(b)?.push(a);}
25
+ for(let i=0;i<queue.length;i++){const id=queue[i];for(const other of adjacent.get(id)||[])if(!result.has(other)){result.set(other,result.get(id)+1);queue.push(other);}}
26
+ return result;
27
+ }
28
+
29
+ export function createOpenGraph({D,byKey,spine,allPrimary,groups,shortLabels,recordLabel,predicateLabel,readingColorOf=colorOf}){
30
+ const contextIndex=createContextIndex(D,allPrimary.map(n=>n.key),spine);
31
+ const parents=new Map(groups.flatMap(group=>group.members.map(key=>[key,group.parent])));
32
+ function project(extras){
33
+ // A gentle horizontal reading direction; physics may displace every node.
34
+ const records=[...allPrimary];
35
+ extras.forEach(key=>{if(!records.some(n=>n.key===key))records.push(byKey.get(key));});
36
+ const nodes=records.map(record=>{
37
+ const mainIndex=spine.indexOf(record.key);
38
+ return {x:0,y:0,id:record.key,record,main:mainIndex>=0,step:mainIndex+1,phase:[...record.key].reduce((v,c)=>(v*31+c.charCodeAt(0))%997,0)/997*Math.PI*2,label:shortLabels[record.key]||recordLabel(record),type:coreTypeOf(record),shape:shapeOf(record),color:readingColorOf(record),opacity:1,entryAlpha:1,emphasis:0};
39
+ });
40
+ const links=spine.slice(1).map((key,index)=>({id:'reading-spine-'+index,source:spine[index],target:key,main:true,kind:'reading',step:index,curve:0,opacity:.48}));
41
+ for(const group of groups)for(const key of group.members)links.push({id:'branch:'+key,source:group.parent,target:key,kind:'reading',curve:.05,opacity:.6});
42
+ const keys=new Set(nodes.map(n=>n.id)),extraSet=new Set(extras);
43
+ for(const edge of D.edges)if((extraSet.has(edge.from)||extraSet.has(edge.to))&&keys.has(edge.from)&&keys.has(edge.to))links.push({id:edge.id,source:edge.from,target:edge.to,kind:'canonical',label:predicateLabel[edge.predicate]||edge.predicate,raw:edge,curve:.16,opacity:.6});
44
+ return {nodes,links};
45
+ }
46
+ return function OpenGraph(props){
47
+ const holder=useRef(null),api=useRef(null),hitEls=useRef(new Map()),experimentEls=useRef(new Map()),current=useRef(props),seenExtras=useRef(props.extras),seenSelection=useRef(props.selection?.id),seenReset=useRef(props.resetSignal),seenFit=useRef(props.fitSignal),seenFolded=useRef(props.folded),seenExperimentSelecting=useRef(props.experimentSelecting),[visible,setVisible]=useState([]),[error,setError]=useState('');
48
+ current.current=props;
49
+ useEffect(()=>{
50
+ const h=holder.current;let g,pan,camera,model={nodes:[],links:[]},resize,pauseTimer,entryFrame,appearanceFrame,ambientTimer,ambientAt=0,ambientTime=0,ambientFrames=0,pointerHeld=false,paused=false,busyUntil=0,hover=null,experimentHover=null,disposed=false,lastSize='',dragging=false,running=false,autoFit=false,ticks=0,savedCamera=null,overviewCamera=null,lastSelection=null;
51
+ let contextNodes=[],contextOffsets=new Map(),contextGeometry='',hoverOwner=null,contextTimer=null,contextFrame=0,contextKey='',settledOnce=false;
52
+ const clearHoverDelay=()=>{clearTimeout(contextTimer);contextTimer=null;};
53
+ const drawnNodes=()=>[...model.nodes,...contextNodes];
54
+ const contextLinks=()=>contextNodes.flatMap(n=>D.edges.filter(e=>(e.from===n.id&&e.to===n.owner)||(e.to===n.id&&e.from===n.owner)).map(e=>({id:e.id,source:drawnNodes().find(v=>v.id===e.from),target:drawnNodes().find(v=>v.id===e.to),raw:e,kind:'canonical'}))).filter(e=>e.source&&e.target);
55
+ const publishVisible=()=>{setVisible(drawnNodes());current.current.onVisibleCount?.(drawnNodes().length);h.dataset.contextNodes=contextNodes.map(n=>n.id).join(',');};
56
+ const preference=window.matchMedia('(prefers-reduced-motion: reduce)');
57
+ const view={width:0,height:0,top:100,bottom:70,left:20,right:20},cache=new Map();
58
+ const neighborhoodForce=readingNeighborhoodForce(UNIT);
59
+ const linkDistance=link=>link.restDistance??(link.kind==='canonical'?220:Math.hypot(link.source.layoutX-link.target.layoutX,link.source.layoutY-link.target.layoutY));
60
+ let labelOffsets=new Map(),repackLabels=false,lastLabelGeometry='',labelSolves=0,experimentGeometry='',experimentPaths=new Map();
61
+ const duration=ms=>preference.matches?0:ms;
62
+ const canDrift=()=>!disposed&&!document.hidden&&!preference.matches&&current.current.ambient!==false&&!current.current.motionBlocked&&!running&&!dragging&&!pointerHeld&&!pan?.active&&!camera?.active&&!hover&&!hoverOwner&&!contextNodes.length&&!experimentHover&&!current.current.selection&&!current.current.experimentSelecting&&performance.now()>=busyUntil;
63
+ const markMotion=()=>{h.dataset.motion=ambientTimer?'drifting':paused?'idle':running?'simulating':performance.now()<busyUntil?'moving':'active';h.dataset.physics=running?'running':'settled';};
64
+ const stopDrift=()=>{clearTimeout(ambientTimer);ambientTimer=null;ambientAt=0;};
65
+ // Bounded display motion around the freely settled layout. D3 stays stopped.
66
+ const driftFrame=()=>{
67
+ ambientTimer=null;if(!canDrift()){markMotion();return;}
68
+ const now=performance.now();ambientTime+=ambientAt?Math.min(100,now-ambientAt):50;ambientAt=now;
69
+ const t=ambientTime/1000;
70
+ for(const n of model.nodes){
71
+ const group=n.main?n.step:spine.indexOf(parents.get(n.id))+1,offset=ambientOffset(n,t,group);
72
+ if(n.restX==null){n.restX=n.x;n.restY=n.y;}
73
+ n.x=n.restX+offset.x*UNIT;n.y=n.restY+offset.y*UNIT;
74
+ }
75
+ paused=false;g.resumeAnimation();
76
+ // resumeAnimation draws once synchronously; cancel its next frame at rest.
77
+ if(canDrift()){g.pauseAnimation();paused=true;ambientFrames++;h.dataset.ambientFrames=String(ambientFrames);ambientTimer=setTimeout(driftFrame,50);}
78
+ markMotion();
79
+ };
80
+ const startDrift=()=>{if(canDrift()&&!ambientTimer){ambientAt=0;ambientTimer=setTimeout(driftFrame,50);}markMotion();};
81
+ const pause=()=>{if(disposed)return;const remaining=busyUntil-performance.now();if(running||dragging||remaining>0){pauseTimer=setTimeout(pause,Math.max(120,remaining+80));return;}paused=true;g?.pauseAnimation();startDrift();};
82
+ const wake=()=>{if(disposed||!g||document.hidden)return;stopDrift();if(paused){paused=false;g.resumeAnimation();}clearTimeout(pauseTimer);pauseTimer=setTimeout(pause,Math.max(220,busyUntil-performance.now()+80));markMotion();};
83
+ const hold=ms=>{busyUntil=Math.max(busyUntil,performance.now()+ms);wake();};
84
+ const bounds=()=>{
85
+ view.width=Math.max(1,h.clientWidth);view.height=Math.max(1,h.clientHeight);const origin=h.getBoundingClientRect();view.top=35;view.bottom=45;view.left=20;view.right=20;
86
+ for(const selector of ['.paper-card','.toolbar','.legend','.spine-nav','.projection-note','.experiment-dock']){
87
+ const el=h.closest('.reference-app').querySelector(selector);if(!el||!el.offsetHeight)continue;const b=el.getBoundingClientRect(),box={x:b.x-origin.x,y:b.y-origin.y,w:b.width,h:b.height};
88
+ if(selector==='.paper-card'||selector==='.toolbar')view.top=Math.max(view.top,box.y+box.h+20);
89
+ if(selector==='.legend'||selector==='.projection-note'||selector==='.experiment-dock')view.bottom=Math.max(view.bottom,view.height-box.y+18);
90
+ }
91
+ const app=h.closest('.reference-app'),panel=app.querySelector('.inspector');
92
+ if(panel){
93
+ // Final layout offsets ignore the panel's entrance transform. This is camera framing only.
94
+ if(view.width<=780)view.bottom=Math.max(view.bottom,view.height-panel.offsetTop+18);
95
+ else view.right=Math.max(view.right,view.width-panel.offsetLeft+24);
96
+ }
97
+
98
+ };
99
+ const metrics=(n,ctx)=>{
100
+ const keyClaim=n.main&&n.type==='claim',key=n.id;
101
+ if(cache.has(key))return cache.get(key);
102
+ const font=(keyClaim?'650 15px':n.main?'600 14px':'400 12px')+' -apple-system, "PingFang SC", sans-serif';ctx.font=font;
103
+ const lines=wrapGraphLabel(n.label,text=>ctx.measureText(text).width,n.main?165:148);
104
+ const titleWidth=Math.max(...lines.map(text=>ctx.measureText(text).width));const subtype=n.main?semanticLabelOf(n.record):null;ctx.font='400 11px -apple-system, "PingFang SC", sans-serif';
105
+ const value={lines,font,subtype,lineHeight:n.main?19:15,width:Math.max(titleWidth,subtype?ctx.measureText(subtype).width:0)+10,height:lines.length*(n.main?19:15)+4+(subtype?15:0)};cache.set(key,value);return value;
106
+ };
107
+ const positionContext=()=>{
108
+ const signature=lastLabelGeometry+'|'+contextNodes.map(n=>n.id+':'+n.owner).join(',');
109
+ if(contextGeometry===signature||!contextNodes.length)return;
110
+ const ctx=h.querySelector('canvas')?.getContext('2d');if(!ctx)return;
111
+ const obstacles=model.nodes.flatMap(n=>{const p=layoutPosition(n),o=labelOffsets.get(n.id),m=metrics(n,ctx),r=baseRadius(n)*DESIGN_SCALE+5;return[{x:p.x*DESIGN_SCALE-r,y:p.y*DESIGN_SCALE-r,w:2*r,h:2*r},...(o?[{x:p.x*DESIGN_SCALE+o.dx,y:p.y*DESIGN_SCALE+o.dy,w:m.width,h:m.height}]:[])];});
112
+ const placements=placeContext(contextNodes.map(n=>{const owner=layoutPosition(model.nodes.find(v=>v.id===n.owner)||model.nodes[0]),m=metrics(n,ctx);return{id:n.id,owner:{x:owner.x*DESIGN_SCALE,y:owner.y*DESIGN_SCALE},w:m.width,h:m.height};}),obstacles);
113
+ contextOffsets=new Map();for(const n of contextNodes){const p=placements.get(n.id);n.x=p.x*UNIT;n.y=p.y*UNIT;n.labelRect={x:p.label.x*UNIT,y:p.label.y*UNIT,w:p.label.w*UNIT,h:p.label.h*UNIT};contextOffsets.set(n.id,{dx:p.dx,dy:p.dy,side:p.side});}contextGeometry=signature;h.dataset.contextLayouts=String(Number(h.dataset.contextLayouts||0)+1);
114
+ };
115
+ const syncContext=()=>{
116
+ const selected=current.current.selection?.kind==='record'?current.current.selection.id:null,previousOwner=contextNodes.find(n=>n.id===selected)?.owner;
117
+ const wanted=contextRecords(contextIndex,{hoverOwner,selected,previousOwner});
118
+ const key=wanted.map(n=>n.id+':'+n.owner).join(',');if(contextKey===key)return;contextKey=key;
119
+ cancelAnimationFrame(contextFrame);const previous=new Map(contextNodes.map(n=>[n.id,n]));
120
+ const next=wanted.map(({id,owner})=>{const record=byKey.get(id);return Object.assign(previous.get(id)||{id,record,temporary:true,main:false,phase:0,label:recordLabel(record),type:coreTypeOf(record),shape:shapeOf(record),color:readingColorOf(record),opacity:1,entryAlpha:0,emphasis:0},{owner,leaving:false});});
121
+ const ids=new Set(next.map(n=>n.id));for(const n of contextNodes)if(!ids.has(n.id)){n.leaving=true;next.push(n);}contextNodes=next;contextGeometry='';positionContext();publishVisible();
122
+ const starts=new Map(contextNodes.map(n=>[n,n.entryAlpha])),start=performance.now(),time=duration(wanted.length?220:160);hold(time);
123
+ const tick=now=>{const t=time?Math.min(1,(now-start)/time):1;for(const n of contextNodes)n.entryAlpha=starts.get(n)+((n.leaving?0:1)-starts.get(n))*ease(t);wake();if(t<1)contextFrame=requestAnimationFrame(tick);else{contextFrame=0;contextNodes=contextNodes.filter(n=>!n.leaving);publishVisible();}};tick(start);
124
+ };
125
+ const setNodeHover=n=>{
126
+ const id=n?.id||null;if(hover===id&&(id!==null||!hoverOwner||contextTimer))return;hover=id;clearHoverDelay();
127
+ if(n?.temporary){hoverOwner=n.owner;syncContext();}
128
+ else if(n&&contextIndex.children.get(n.id)?.length){hoverOwner=n.id;syncContext();}
129
+ else contextTimer=setTimeout(()=>{contextTimer=null;hoverOwner=null;syncContext();appearance();},200);
130
+ appearance();h.style.cursor=n?'grab':'default';
131
+ };
132
+ const appearance=(ms=160)=>{
133
+ if(!g)return;cancelAnimationFrame(appearanceFrame);const chosen=hover||current.current.selection?.id,hidden=current.current.hidden;
134
+ const inspected=current.current.experiments?.find(e=>e.id===(experimentHover||current.current.selection?.id));
135
+ const choosing=current.current.experimentSelecting,selectedNodes=new Set(inspected?.contextNodeIds||current.current.experimentNodes||[]),hasExperimentSelection=!!inspected||(choosing&&selectedNodes.size>0);
136
+ const seeds=inspected?[inspected.source,inspected.target]:chosen?[chosen]:spine;
137
+ const depth=distances(drawnNodes(),[...model.links,...contextLinks()],seeds);
138
+ const starts=new Map(),targets=new Map(),emphasis=new Map();
139
+ for(const n of drawnNodes()){n.depth=depth.get(n.id)??3;starts.set(n,n.opacity);targets.set(n,hidden.has(n.type)?.08:hasExperimentSelection?(selectedNodes.has(n.id)?1:.22):distanceOpacity(n.depth));emphasis.set(n,{from:n.emphasis,to:n.id===chosen?1:0});}
140
+ for(const link of model.links){starts.set(link,link.opacity);const a=model.nodes.find(n=>n.id===idOf(link.source)),b=model.nodes.find(n=>n.id===idOf(link.target)),level=Math.max(a?.depth??3,b?.depth??3);targets.set(link,hidden.has(a?.type)||hidden.has(b?.type)?.06:hasExperimentSelection?(selectedNodes.has(a?.id)&&selectedNodes.has(b?.id)?.6:.1):distanceOpacity(level)*(link.main?.48:link.kind==='canonical'?.6:.55));}
141
+ const time=duration(ms),start=performance.now();hold(time);
142
+ const tick=now=>{const t=time?Math.min(1,(now-start)/time):1,k=ease(t);for(const [item,to] of targets)item.opacity=starts.get(item)+(to-starts.get(item))*k;for(const [n,value] of emphasis)n.emphasis=value.from+(value.to-value.from)*k;wake();if(t<1)appearanceFrame=requestAnimationFrame(tick);};tick(start);
143
+ };
144
+ const hoverExperiment=id=>{if(experimentHover===id)return;experimentHover=id;hover=null;hoverOwner=null;clearHoverDelay();syncContext();h.dataset.experimentHover=id||'';appearance();};
145
+ const linkColor=link=>{
146
+ const chosen=hover||current.current.selection?.id,active=chosen&&(idOf(link.source)===chosen||idOf(link.target)===chosen);
147
+ return alpha(active?(link.target.color||MD.grey700):link.kind==='canonical'?MD.grey700:link.main?MD.grey600:MD.grey400,(link.opacity??.6)*Math.min(link.source.entryAlpha??1,link.target.entryAlpha??1));
148
+ };
149
+ const softLink=(link,ctx)=>{
150
+ if(!link.main)return;
151
+ const points=spine.map(id=>model.nodes.find(n=>n.id===id)),controls=spineControls(points,link.step);
152
+ if(controls)paintSpine(ctx,controls,linkColor(link),UNIT);
153
+ };
154
+ const disc=(n,ctx,scale)=>{
155
+ ctx.save();ctx.globalAlpha=n.opacity*n.entryAlpha;const r=radius(n,scale);
156
+ const path=pad=>{const q=r+pad;ctx.beginPath();if(['method','plan','activity','observation'].includes(n.shape))ctx.roundRect(n.x-q,n.y-q,q*2,q*2,(n.shape==='observation'?2:5)*UNIT);else if(n.shape==='assessment'){ctx.moveTo(n.x,n.y-q);ctx.lineTo(n.x+q,n.y);ctx.lineTo(n.x,n.y+q);ctx.lineTo(n.x-q,n.y);ctx.closePath();}else ctx.arc(n.x,n.y,q,0,Math.PI*2);};
157
+ // Filled nodes have no decorative strokes; interaction adds only a soft fill.
158
+ if(n.emphasis>.02){path(4*UNIT);ctx.fillStyle=alpha(n.color,n.emphasis*.1);ctx.fill();}
159
+ const outline=!n.main&&['hypothesis','plan','observation'].includes(n.shape);path(0);ctx.fillStyle=outline?MD.white:n.color;ctx.fill();
160
+ if(outline){ctx.strokeStyle=n.color;ctx.lineWidth=1.4*UNIT;if(['hypothesis','plan'].includes(n.shape))ctx.setLineDash([3*UNIT,2*UNIT]);ctx.stroke();ctx.setLineDash([]);}
161
+ if(!n.main&&n.shape==='observation'){ctx.beginPath();ctx.moveTo(n.x-r*.45,n.y-r*.3);ctx.lineTo(n.x+r*.45,n.y-r*.3);ctx.moveTo(n.x-r*.45,n.y+r*.3);ctx.lineTo(n.x+r*.45,n.y+r*.3);ctx.strokeStyle=n.color;ctx.lineWidth=1.4*UNIT;ctx.stroke();}
162
+ if(n.main){ctx.font=`600 ${Math.min(12,Math.max(10,baseRadius(n)*DESIGN_SCALE*.8))*UNIT}px -apple-system, sans-serif`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillStyle=numberColor(n.color);ctx.fillText(String(n.step),n.x,n.y+.3*UNIT);}
163
+ ctx.restore();
164
+ };
165
+ const visibleExperiments=()=>current.current.experiments?.filter(e=>current.current.experimentSelecting||current.current.experimentSelected?.includes(e.id)||current.current.selection?.id===e.id)||[];
166
+ const experimentPathsFor=()=>{
167
+ const relevant=visibleExperiments(),signature=lastLabelGeometry+'|'+relevant.map(e=>{const el=experimentEls.current.get(e.id);return e.id+':'+el?.offsetWidth+':'+el?.offsetHeight;}).join(',');
168
+ if(signature!==experimentGeometry){
169
+ const rectangles=model.nodes.flatMap(n=>{const p=layoutPosition(n),o=labelOffsets.get(n.id),m=cache.get(n.id),r=baseRadius(n);return[{x:p.x-r,y:p.y-r,w:r*2,h:r*2},...(o&&m?[{x:p.x+o.dx*UNIT,y:p.y+o.dy*UNIT,w:m.width*UNIT,h:m.height*UNIT}]:[])];});
170
+ experimentPaths=new Map();
171
+ for(const edge of relevant){const a=model.nodes.find(n=>n.id===edge.source),b=model.nodes.find(n=>n.id===edge.target);if(!a||!b)continue;const el=experimentEls.current.get(edge.id),size={w:(el?.offsetWidth||210)+14,h:(el?.offsetHeight||32)+12},pa=layoutPosition(a),pb=layoutPosition(b),curve=experimentCurve(pa,pb,rectangles,edge.index,UNIT,size);experimentPaths.set(edge.id,{curve,a,b,size});const m=curvePoint(curve,.5);rectangles.push({x:m.x-size.w*UNIT/2,y:m.y-size.h*UNIT/2,w:size.w*UNIT,h:size.h*UNIT});}
172
+ experimentGeometry=signature;
173
+ }
174
+ return relevant.map(e=>{const p=experimentPaths.get(e.id);if(!p)return null;const a=p.a,b=p.b,da={x:a.x-p.curve[0].x,y:a.y-p.curve[0].y},db={x:b.x-p.curve[2].x,y:b.y-p.curve[2].y};return{...e,labelSize:p.size,curve:[{x:a.x,y:a.y},{x:p.curve[1].x+(da.x+db.x)/2,y:p.curve[1].y+(da.y+db.y)/2},{x:b.x,y:b.y}]};}).filter(Boolean);
175
+ };
176
+ const paintExperiments=(ctx,scale)=>{
177
+ for(const edge of experimentPathsFor()){
178
+ const [a,c,b]=edge.curve,selected=current.current.experimentSelected?.includes(edge.id),active=current.current.selection?.id===edge.id||experimentHover===edge.id;
179
+ ctx.save();ctx.lineWidth=(selected||active?1.8:1.2)*UNIT;ctx.strokeStyle=alpha(MD.blue700,active?.9:experimentHover?.18:selected?.8:.44);ctx.setLineDash(selected?[]:[4*UNIT,3*UNIT]);ctx.lineCap='round';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.quadraticCurveTo(c.x,c.y,b.x,b.y);ctx.stroke();ctx.restore();
180
+ const center=curvePoint(edge.curve,.5),point=g.graph2ScreenCoords(center.x,center.y),el=experimentEls.current.get(edge.id);
181
+ if(el){el.style.transform=`translate(${point.x}px,${point.y}px) translate(-50%,-50%) scale(${scale*UNIT})`;el.style.transformOrigin='center';el.dataset.hovered=String(experimentHover===edge.id);el.dataset.graphX=center.x;el.dataset.graphY=center.y;el.dataset.source=edge.source;el.dataset.target=edge.target;}
182
+ }
183
+ };
184
+ const pickExperiment=(x,y)=>{
185
+ let nearest=null,distance=10;
186
+ for(const edge of experimentPathsFor())for(let t=.06;t<.96;t+=.025){const p=curvePoint(edge.curve,t),q=g.graph2ScreenCoords(p.x,p.y),d=Math.hypot(q.x-x,q.y-y);if(d<distance){distance=d;nearest=edge;}}
187
+ return nearest;
188
+ };
189
+ const paintLabels=(ctx,scale)=>{
190
+ let nodes=model.nodes.filter(n=>Number.isFinite(n.x)&&Number.isFinite(n.y));
191
+ const coords=new Map(nodes.map(n=>[n.id,g.graph2ScreenCoords(n.x,n.y)]));
192
+ // Collision layout uses fixed design units, independent of BOTH zoom and pan.
193
+ const items=nodes.map(n=>{
194
+ const m=metrics(n,ctx),parent=model.nodes.find(p=>p.id===parents.get(n.id)),base=layoutPosition(n),parentBase=parent&&layoutPosition(parent);
195
+ const side=n.main?(n.step===1?'right':'left'):parentBase&&base.x<parentBase.x?'left':'right';
196
+ const length=parentBase?Math.hypot(base.x-parentBase.x,base.y-parentBase.y):0,outward=length?{x:(base.x-parentBase.x)/length,y:(base.y-parentBase.y)/length}:null;
197
+ return {id:n.id,outward,preferred:n.layoutLabel,motion:motionEnvelope(n),x:base.x*DESIGN_SCALE,y:base.y*DESIGN_SCALE,r:baseRadius(n)*DESIGN_SCALE+2,w:m.width,h:m.height,main:n.main,step:n.step,side};
198
+ });
199
+ const geometry=items.map(n=>[n.id,n.x,n.y,n.w,n.h].join(',')).join(';');
200
+ if(repackLabels||geometry!==lastLabelGeometry||labelOffsets.size!==items.length){labelOffsets=layoutLabels(items,repackLabels?new Map():labelOffsets);lastLabelGeometry=geometry;h.dataset.labelSolves=String(++labelSolves);}repackLabels=false;positionContext();nodes=[...nodes,...contextNodes];for(const n of contextNodes)coords.set(n.id,g.graph2ScreenCoords(n.x,n.y));const visualScale=scale*UNIT;
201
+ if(!current.current.selection)overviewCamera={...g.centerAt(),k:scale,width:g.width(),height:g.height()};
202
+ h.dataset.zoom=scale.toFixed(6);h.dataset.visualScale=visualScale.toFixed(6);h.dataset.mainLinkWidth=(1.45*visualScale).toFixed(3);
203
+ paintExperiments(ctx,scale);
204
+ for(const link of contextLinks()){const a=link.source,b=link.target,temp=a.temporary?a:b;ctx.save();ctx.globalAlpha=temp.entryAlpha*.4;ctx.strokeStyle=temp.color;ctx.lineWidth=.85*UNIT;ctx.setLineDash([3*UNIT,3*UNIT]);ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();ctx.restore();}
205
+ for(const n of [...nodes].sort((a,b)=>(b.depth??3)-(a.depth??3))){
206
+ const a=coords.get(n.id),m=metrics(n,ctx),offset=(n.temporary?contextOffsets:labelOffsets).get(n.id);
207
+ const x=a.x+offset.dx*visualScale,y=a.y+offset.dy*visualScale;n.labelSide=offset.side;n.labelRect=null;n.screenLabelRect=null;
208
+ if(!current.current.labels)continue;
209
+ const nearX=Math.max(x,Math.min(x+m.width*visualScale,a.x)),nearY=Math.max(y,Math.min(y+m.height*visualScale,a.y)),distance=Math.hypot(nearX-a.x,nearY-a.y),r=radius(n,scale)*scale;
210
+ n.labelGap=Math.max(0,distance-r)/visualScale;
211
+ if(distance-r>24*visualScale){
212
+ ctx.save();ctx.globalAlpha=n.opacity*n.entryAlpha*.48;ctx.strokeStyle=MD.grey500;ctx.lineWidth=.7*UNIT;
213
+ ctx.beginPath();ctx.moveTo(n.x+(nearX-a.x)*r/distance/scale,n.y+(nearY-a.y)*r/distance/scale);ctx.lineTo(n.x+(nearX-a.x)/scale,n.y+(nearY-a.y)/scale);ctx.stroke();ctx.restore();
214
+ }
215
+ if(n.temporary||n.depth===0)disc(n,ctx,scale);
216
+ ctx.save();ctx.translate(n.x+offset.dx*UNIT,n.y+offset.dy*UNIT);ctx.scale(UNIT,UNIT);ctx.globalAlpha=n.opacity*n.entryAlpha;
217
+ ctx.font=m.font;ctx.textBaseline='top';ctx.fillStyle=n.main?MD.grey900:MD.grey700;m.lines.forEach((text,index)=>ctx.fillText(text,5,2+index*m.lineHeight));
218
+ if(m.subtype){ctx.font='400 11px -apple-system, "PingFang SC", sans-serif';ctx.fillStyle=MD.grey700;ctx.fillText(m.subtype,5,4+m.lines.length*m.lineHeight);}ctx.restore();
219
+ n.labelRect={x:n.x+offset.dx*UNIT,y:n.y+offset.dy*UNIT,w:m.width*UNIT,h:m.height*UNIT};n.screenLabelRect={x,y,w:m.width*visualScale,h:m.height*visualScale};
220
+ }
221
+ for(const n of nodes){
222
+ const hit=hitEls.current.get(n.id),a=coords.get(n.id);if(!hit)continue;
223
+ const transform=`translate3d(${a.x-18}px,${a.y-18}px,0)`;if(hit.style.transform!==transform)hit.style.transform=transform;
224
+ hit.style.visibility=n.leaving||n.entryAlpha<.1?'hidden':'visible';hit.dataset.temporary=String(!!n.temporary);hit.dataset.owner=n.owner||'';hit.dataset.labelVisible=String(!!n.screenLabelRect);hit.dataset.labelSide=n.labelSide;hit.dataset.depth=String(n.depth);hit.dataset.opacity=n.opacity.toFixed(2);hit.dataset.positionPinned=String(n.fx!=null||n.fy!=null);hit.dataset.graphX=n.x.toFixed(3);hit.dataset.graphY=n.y.toFixed(3);hit.dataset.nodeRadius=(radius(n)*scale).toFixed(3);hit.dataset.labelFontSize=((n.main?(n.type==='claim'?15:14):12)*visualScale).toFixed(3);hit.tabIndex=current.current.hidden.has(n.type)?-1:0;
225
+ hit.dataset.nodeColor=n.color;hit.dataset.readingOwner=n.layoutOwner||n.owner||'';hit.dataset.labelGap=(n.labelGap||0).toFixed(2);hit.dataset.restX=layoutPosition(n).x.toFixed(3);hit.dataset.restY=layoutPosition(n).y.toFixed(3);
226
+ const label=hit.firstElementChild,p=n.screenLabelRect;if(label&&p){label.style.width=p.w+'px';label.style.height=p.h+'px';label.style.transform=`translate3d(${p.x-a.x+18}px,${p.y-a.y+18}px,0)`;}
227
+ }
228
+ };
229
+ const objectBounds=n=>{
230
+ const r=radius(n),label=n.labelRect;
231
+ return {x0:Math.min(n.x-r,label?.x??Infinity),y0:Math.min(n.y-r,label?.y??Infinity),x1:Math.max(n.x+r,label?label.x+label.w:-Infinity),y1:Math.max(n.y+r,label?label.y+label.h:-Infinity)};
232
+ };
233
+ const frameBounds=(box,k,ms)=>{
234
+ const screenY=(view.top+view.height-view.bottom)/2,screenX=(view.left+view.width-view.right)/2,time=duration(ms);hold(time);camera.move({k,x:(box.x0+box.x1)/2-(screenX-view.width/2)/k,y:(box.y0+box.y1)/2-(screenY-view.height/2)/k},time);
235
+ };
236
+ const fit=(ms=520)=>{
237
+ pan?.cancel();const nodes=drawnNodes().filter(n=>Number.isFinite(n.x)&&Number.isFinite(n.y));if(!nodes.length)return;bounds();
238
+ const boxes=nodes.map(objectBounds);
239
+ if(current.current.experimentSelecting)for(const e of experimentPathsFor()){for(let t=0;t<=1;t+=.05){const p=curvePoint(e.curve,t);boxes.push({x0:p.x-4*UNIT,y0:p.y-4*UNIT,x1:p.x+4*UNIT,y1:p.y+4*UNIT});}const m=curvePoint(e.curve,.5),w=e.labelSize.w*UNIT/2,h=e.labelSize.h*UNIT/2;boxes.push({x0:m.x-w,y0:m.y-h,x1:m.x+w,y1:m.y+h});}
240
+ const box={x0:Math.min(...boxes.map(b=>b.x0)),y0:Math.min(...boxes.map(b=>b.y0)),x1:Math.max(...boxes.map(b=>b.x1)),y1:Math.max(...boxes.map(b=>b.y1))};
241
+ const k=Math.max(.08,Math.min(1.15,(view.width-view.left-view.right)/Math.max(1,box.x1-box.x0),(view.height-view.top-view.bottom)/Math.max(1,box.y1-box.y0)));
242
+ frameBounds(box,k,ms);
243
+ };
244
+ const focus=(n,ms=320)=>{
245
+ pan?.cancel();if(current.current.detailExpanded||!Number.isFinite(n.x)||!Number.isFinite(n.y))return;bounds();const members=[n,...contextNodes.filter(v=>v.owner===(n.owner||n.id)&&!v.leaving)],boxes=members.map(objectBounds),box={x0:Math.min(...boxes.map(b=>b.x0)),y0:Math.min(...boxes.map(b=>b.y0)),x1:Math.max(...boxes.map(b=>b.x1)),y1:Math.max(...boxes.map(b=>b.y1))};
246
+ // Frame the chosen object and its label together; never move the label to fit a panel.
247
+ const k=Math.max(.08,Math.min(Math.max(g.zoom(),.4),.65,(view.width-view.left-view.right-20)/Math.max(1,box.x1-box.x0),(view.height-view.top-view.bottom)/Math.max(1,box.y1-box.y0)));
248
+ frameBounds(box,k,ms);
249
+ };
250
+ const restoreCamera=()=>{pan?.cancel();if(!savedCamera)return false;hold(duration(320));camera.move(savedCamera,duration(320));return true;};
251
+ const selectionChanged=()=>{
252
+ const id=current.current.selection?.id||null;hover=null;hoverOwner=null;clearHoverDelay();experimentHover=null;h.dataset.experimentHover='';syncContext();appearance();
253
+ if(id&&!lastSelection)savedCamera=overviewCamera||{...g.centerAt(),k:g.zoom(),width:g.width(),height:g.height()};
254
+ autoFit=false;lastSelection=id;if(current.current.detailExpanded)return;
255
+ const n=drawnNodes().find(n=>n.id===id),experiment=current.current.experiments?.find(e=>e.id===id);
256
+ if(experiment){const members=model.nodes.filter(n=>[experiment.source,experiment.target].includes(n.id));bounds();const boxes=members.map(objectBounds);if(boxes.length){const box={x0:Math.min(...boxes.map(b=>b.x0)),x1:Math.max(...boxes.map(b=>b.x1)),y0:Math.min(...boxes.map(b=>b.y0)),y1:Math.max(...boxes.map(b=>b.y1))},k=Math.max(.08,Math.min(.42,(view.width-view.left-view.right-20)/(box.x1-box.x0),(view.height-view.top-view.bottom-20)/(box.y1-box.y0)));frameBounds(box,k,320);}}
257
+ else if(n)focus(n);else if(id==='experiment-scope')fit();else if(!id&&!restoreCamera())fit();
258
+ };
259
+ const resetDrift=()=>{stopDrift();ambientTime=0;for(const n of model.nodes){delete n.restX;delete n.restY;}};
260
+ const reheat=()=>{resetDrift();running=true;ticks=0;g.cooldownTicks(preference.matches?0:240).d3ReheatSimulation();wake();};
261
+ const update=(extras,initial=false)=>{
262
+ pan?.cancel();resetDrift();labelOffsets.clear();cancelAnimationFrame(entryFrame);const next=project(extras),previous=new Map(model.nodes.map(n=>[n.id,n])),starts=new Map();
263
+ next.nodes=next.nodes.map(record=>{const node=previous.get(record.id)||record;delete node.fx;delete node.fy;node.entryAlpha=previous.has(node.id)?node.entryAlpha:0;starts.set(node,node.entryAlpha);return node;});
264
+ model=next;cache.clear();const ctx=h.querySelector('canvas').getContext('2d');
265
+ const items=model.nodes.map(n=>({id:n.id,main:n.main,r:baseRadius(n)*DESIGN_SCALE+2,w:metrics(n,ctx).width,h:metrics(n,ctx).height}));
266
+ const arrangement=planReadingLayout(items,spine,parents);
267
+ for(const [i,n] of model.nodes.entries()){
268
+ const p=arrangement.get(n.id);n.layoutOwner=p.owner;n.layoutX=p.x*UNIT;n.layoutY=p.y*UNIT;n.layoutDX=p.dx;n.layoutDY=p.dy;n.layoutLabel=p.label;n.layoutBody=readingObjectBody(items[i],p.label);
269
+ if(initial||!previous.has(n.id)){n.x=n.layoutX;n.y=n.layoutY;}
270
+ }
271
+ autoFit=true;running=true;ticks=0;g.warmupTicks(preference.matches?230:35).cooldownTicks(preference.matches?0:240).graphData(model);publishVisible();appearance(0);
272
+ const time=duration(initial?500:260),start=performance.now();hold(time);
273
+ const tick=now=>{const t=time?Math.min(1,(now-start)/time):1,k=ease(t);for(const n of model.nodes)n.entryAlpha=starts.get(n)+(1-starts.get(n))*k;wake();if(t<1)entryFrame=requestAnimationFrame(tick);};tick(start);
274
+ };
275
+ try{
276
+ // One scheduler owns redraws: the library must not skip our custom transitions.
277
+ bounds();g=new ForceGraph(h).width(view.width).height(view.height).backgroundColor('rgba(0,0,0,0)').cooldownTime(4500).d3AlphaMin(.002).d3AlphaDecay(.035).d3VelocityDecay(.3).enableNodeDrag(false).enablePanInteraction(false).nodeLabel('').autoPauseRedraw(false).minZoom(.08).maxZoom(3).nodeCanvasObject(disc)
278
+ .nodePointerAreaPaint((n,color,ctx,scale)=>{ctx.fillStyle=color;if(n.labelRect){const b=n.labelRect;ctx.fillRect(b.x,b.y,b.w,b.h);}ctx.beginPath();ctx.arc(n.x,n.y,radius(n,scale)+5/scale,0,Math.PI*2);ctx.fill();})
279
+ .linkColor(linkColor).linkWidth(link=>(link.main?1.45:link.kind==='canonical'?1.1:.75)*UNIT*g.zoom()).linkLineDash(link=>link.kind==='canonical'?[5*UNIT,3*UNIT]:null).linkCurvature(link=>link.curve)
280
+ .linkCanvasObjectMode(link=>link.main?'replace':'after').linkCanvasObject(softLink)
281
+ .linkDirectionalArrowLength(link=>link.kind==='canonical'?5*UNIT:0).linkDirectionalArrowRelPos(.8)
282
+ .onRenderFramePost(paintLabels).onZoom(wake).onNodeClick(n=>current.current.onPick(n.id)).onNodeHover(()=>{}).onBackgroundClick(()=>{setNodeHover(null);if(current.current.selection)current.current.onPick(null);}).onLinkClick(link=>{if(link.raw)current.current.onPickEdge(link.raw.id);})
283
+ .onEngineTick(()=>{stopDrift();running=true;ticks++;markMotion();if(autoFit&&!current.current.selection&&(ticks===1||ticks%24===0))fit(ticks===1?0:180);})
284
+ .onEngineStop(()=>{resetDrift();running=false;repackLabels=true;const frameInitialSelection=!settledOnce&&current.current.selection?.kind==='record';settledOnce=true;if(autoFit||frameInitialSelection){autoFit=false;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(disposed)return;const chosen=drawnNodes().find(n=>n.id===current.current.selection?.id);if(chosen)focus(chosen);else fit(380);}));}wake();});
285
+ camera=createCameraMotion({graph:g,element:h,reduced:()=>preference.matches,onActivity:wake});
286
+ // Our reading anchors own world placement; a second centroid force would
287
+ // continually shove asymmetric neighborhoods away from their own parent.
288
+ g.d3Force('center',null);
289
+ g.d3Force('charge').strength(n=>n.main?-850:-320).distanceMax(1000);
290
+ g.d3Force('link').distance(linkDistance).strength(link=>link.kind==='canonical'?.025:link.main?.09:.06);
291
+ g.d3Force('reading-neighborhood',neighborhoodForce);
292
+ g.d3Force('collide',labeledObjectCollision(UNIT));
293
+ pan=createGraphGestures({element:h,graph:g,reduced:()=>preference.matches,
294
+ findNode:(x,y)=>{
295
+ const ordered=drawnNodes().filter(n=>!n.leaving).sort((a,b)=>(a.depth??3)-(b.depth??3));
296
+ return ordered.find(n=>{const a=g.graph2ScreenCoords(n.x,n.y);return Math.hypot(x-a.x,y-a.y)<radius(n,g.zoom())*g.zoom()+5;})||ordered.find(n=>{const p=n.screenLabelRect;return p&&x>=p.x&&x<=p.x+p.w&&y>=p.y&&y<=p.y+p.h;});
297
+ },
298
+ onStart:()=>{camera.cancel();autoFit=false;pointerHeld=true;hover=null;experimentHover=null;appearance();wake();},onActivity:wake,
299
+ onRelease:()=>{pointerHeld=false;wake();},onTap:(n,point)=>{if(n){current.current.onPick(n.id);return;}const rect=h.getBoundingClientRect(),edge=pickExperiment(point.x-rect.left,point.y-rect.top);if(edge){current.current.onInspectExperiment(edge.id);return;}if(current.current.selection)current.current.onPick(null);},
300
+ onNodeStart:n=>{if(n.temporary){dragging=true;return;}resetDrift();dragging=true;running=true;n.entryAlpha=1;h.dataset.draggedNode=n.id;h.style.cursor='grabbing';g.cooldownTime(Infinity).cooldownTicks(Infinity).d3AlphaMin(0).d3ReheatSimulation();wake();},
301
+ onNodeEnd:n=>{if(n.temporary){dragging=false;delete n.fx;delete n.fy;wake();return;}neighborhoodForce.retarget(n);for(const link of model.links)if(link.source===n||link.target===n)link.restDistance=Math.hypot(link.source.x-link.target.x,link.source.y-link.target.y);g.d3Force('link').distance(linkDistance);dragging=false;h.style.cursor='grab';g.cooldownTime(4500).d3AlphaMin(.002);reheat();}
302
+ });
303
+ api.current={update,fit:()=>{savedCamera=null;autoFit=false;fit();},reset:()=>{pan?.cancel();labelOffsets.clear();savedCamera=null;autoFit=true;for(const n of model.nodes){delete n.fx;delete n.fy;}reheat();fit();},focus:selectionChanged,frameDetail:()=>{if(current.current.selection?.kind?.startsWith('experiment')){if(!current.current.detailExpanded)selectionChanged();return;}const n=drawnNodes().find(n=>n.id===current.current.selection?.id);if(n){camera?.cancel();if(!current.current.detailExpanded)focus(n);}},sync:()=>{if(experimentHover&&!visibleExperiments().some(e=>e.id===experimentHover))hoverExperiment(null);bounds();cache.clear();appearance();},hoverExperiment,hover:id=>setNodeHover(drawnNodes().find(n=>n.id===id))};
304
+ update(current.current.extras,true);
305
+ resize=new ResizeObserver(()=>{const signature=h.clientWidth+':'+h.clientHeight;if(signature===lastSize)return;lastSize=signature;bounds();cache.clear();g.width(view.width).height(view.height);const selected=model.nodes.find(n=>n.id===current.current.selection?.id);if(selected)focus(selected);else if(!restoreCamera())fit();});resize.observe(h);h.dataset.layout='content-aware-force';h.dataset.labels='graph-collision';h.dataset.opacityMode='graph-distance';h.dataset.zoomMode='world-geometry';h.dataset.spineStyle='smooth-cubic';h.dataset.spineBaseOpacity='0.48';
306
+ }catch(e){setError('节点图未能打开:'+e.message);}
307
+ const region=h.parentElement;const userZoom=()=>{camera?.cancel();autoFit=false;};region.addEventListener('wheel',userZoom,{capture:true,passive:true});
308
+ const events=['pointermove','pointerdown','pointerleave','wheel','keydown'];events.forEach(event=>region.addEventListener(event,wake,{passive:true,capture:true}));
309
+ const down=()=>{pointerHeld=true;wake();},up=()=>{pointerHeld=false;wake();};region.addEventListener('pointerdown',down);window.addEventListener('pointerup',up);window.addEventListener('pointercancel',up);
310
+ const leave=()=>{setNodeHover(null);hoverExperiment(null);};region.addEventListener('pointerleave',leave);
311
+ const hoverLine=event=>{if(event.target.tagName!=='CANVAS'||pan?.active||camera?.active||running)return;const r=h.getBoundingClientRect(),x=event.clientX-r.left,y=event.clientY-r.top,n=drawnNodes().filter(n=>!n.leaving).find(n=>{const a=g.graph2ScreenCoords(n.x,n.y),p=n.screenLabelRect;return Math.hypot(x-a.x,y-a.y)<radius(n)*g.zoom()+5||(p&&x>=p.x&&x<=p.x+p.w&&y>=p.y&&y<=p.y+p.h);});if(n){hoverExperiment(null);setNodeHover(n);}else{const bridge=contextNodes.filter(n=>!n.leaving&&n.owner===hoverOwner).some(n=>{const owner=model.nodes.find(o=>o.id===n.owner);if(!owner)return false;const a=g.graph2ScreenCoords(owner.x,owner.y),b=g.graph2ScreenCoords(n.x,n.y),dx=b.x-a.x,dy=b.y-a.y,t=Math.max(0,Math.min(1,((x-a.x)*dx+(y-a.y)*dy)/(dx*dx+dy*dy||1)));return Math.hypot(x-a.x-t*dx,y-a.y-t*dy)<14;});if(bridge){clearHoverDelay();return;}const edge=pickExperiment(x,y);if(edge){setNodeHover(null);hoverExperiment(edge.id);h.style.cursor='pointer';}else{hoverExperiment(null);setNodeHover(null);}}};h.addEventListener('pointermove',hoverLine);
312
+ const visibility=()=>{if(document.hidden){camera?.cancel();pan?.cancel();stopDrift();cancelAnimationFrame(entryFrame);cancelAnimationFrame(appearanceFrame);for(const n of model.nodes)n.entryAlpha=1;busyUntil=0;clearTimeout(pauseTimer);paused=true;g?.pauseAnimation();markMotion();}else{appearance(0);wake();}};document.addEventListener('visibilitychange',visibility);
313
+ const changePreference=()=>{camera?.cancel();pan?.cancel();stopDrift();if(preference.matches){cancelAnimationFrame(entryFrame);cancelAnimationFrame(appearanceFrame);for(const n of model.nodes)n.entryAlpha=1;busyUntil=0;g?.cooldownTicks(0);appearance(0);}else{g?.cooldownTicks(240);wake();}};preference.addEventListener('change',changePreference);
314
+ return()=>{disposed=true;clearHoverDelay();cancelAnimationFrame(contextFrame);region.removeEventListener('wheel',userZoom,true);pan?.dispose();camera?.dispose();stopDrift();region.removeEventListener('pointerdown',down);window.removeEventListener('pointerup',up);window.removeEventListener('pointercancel',up);resize?.disconnect();cancelAnimationFrame(entryFrame);cancelAnimationFrame(appearanceFrame);clearTimeout(pauseTimer);events.forEach(event=>region.removeEventListener(event,wake,true));region.removeEventListener('pointerleave',leave);h.removeEventListener('pointermove',hoverLine);document.removeEventListener('visibilitychange',visibility);preference.removeEventListener('change',changePreference);g?._destructor();api.current=null;};
315
+ },[]);
316
+ useEffect(()=>{if(seenExtras.current!==props.extras){seenExtras.current=props.extras;api.current?.update(props.extras);}},[props.extras]);
317
+ useLayoutEffect(()=>{if(seenSelection.current!==props.selection?.id){seenSelection.current=props.selection?.id;api.current?.sync();api.current?.focus();}},[props.selection?.id]);
318
+ useLayoutEffect(()=>{api.current?.frameDetail();},[props.detailExpanded]);
319
+ useEffect(()=>{api.current?.sync();},[props.labels,props.hidden,props.ambient,props.motionBlocked,props.experimentSelecting,props.experimentSelected]);
320
+ useLayoutEffect(()=>{if(seenExperimentSelecting.current!==props.experimentSelecting){seenExperimentSelecting.current=props.experimentSelecting;if(props.experimentSelecting)api.current?.fit();}},[props.experimentSelecting]);
321
+ useEffect(()=>{if(seenFolded.current!==props.folded){seenFolded.current=props.folded;api.current?.sync();api.current?.fit();}},[props.folded]);
322
+ useEffect(()=>{if(seenFit.current!==props.fitSignal){seenFit.current=props.fitSignal;api.current?.fit();}},[props.fitSignal]);
323
+ useEffect(()=>{if(seenReset.current!==props.resetSignal){seenReset.current=props.resetSignal;api.current?.reset();}},[props.resetSignal]);
324
+ return <div className="graph-region"><div className="graph-canvas" ref={holder}/><div className="node-overlay" aria-label="论文节点图">{visible.map(n=><button key={n.id} data-id={n.id} ref={el=>{if(el)hitEls.current.set(n.id,el);else hitEls.current.delete(n.id);}} className="graph-hit" aria-label={recordLabel(n.record)} title={semanticLabelOf(n.record)+' · '+recordLabel(n.record)} onFocus={()=>api.current?.hover(n.id)} onBlur={()=>api.current?.hover(null)} onClick={()=>props.onPick(n.id)}><span className="graph-label-hit" aria-hidden="true"/></button>)}</div><div className="experiment-overlay" aria-label="图上实验关系">{props.experiments?.filter(e=>props.experimentSelecting||props.experimentSelected?.includes(e.id)||props.selection?.id===e.id).map(e=><div key={e.id} ref={el=>{if(el)experimentEls.current.set(e.id,el);else experimentEls.current.delete(e.id)}} className="experiment-edge-group" data-experiment={e.id} data-selected={props.experimentSelected?.includes(e.id)||false} onMouseEnter={()=>api.current?.hoverExperiment(e.id)} onMouseLeave={()=>api.current?.hoverExperiment(null)} onFocusCapture={()=>api.current?.hoverExperiment(e.id)} onBlurCapture={event=>{if(!event.currentTarget.contains(event.relatedTarget))api.current?.hoverExperiment(null)}}><button className="experiment-edge-label" aria-label={'查看实验:'+e.question} title={e.question} onClick={()=>props.onInspectExperiment(e.id)}><span>{e.title||e.question}</span></button><button className="experiment-edge-pick" aria-label={(props.experimentSelecting?(props.experimentSelected?.includes(e.id)?'取消选择':'选择'):'查看')+e.title} disabled={props.experimentSelecting&&(props.experimentLocked||(props.experimentSelectable&&!props.experimentSelectable.includes(e.id)))} aria-pressed={props.experimentSelected?.includes(e.id)||false} onClick={()=>props.onExperiment(e.id)}><SelectionMark selected={props.experimentSelected?.includes(e.id)||false}/></button></div>)}</div>{error&&<div className="graph-error" role="alert">{error}</div>}</div>;
325
+ };
326
+ }
@@ -0,0 +1,49 @@
1
+ import React,{useState,useMemo,useDeferredValue,useEffect} from 'react';
2
+ import {contentGroups,buildOutlineReading} from './reading-presentation.mjs';
3
+
4
+ export function createOutline({D,byKey,spine,groups,recordLabel,typeLabel,coreLabels,coreHints,coreTypeOf,colorOf,Icon,TypeMark,useCompleteData,cachedSearch,predicateLabel}){
5
+ const {sections,appendices}=buildOutlineReading(D);
6
+ const categories=contentGroups.map(g=>({...g,count:D.nodes.filter(n=>g.types.includes(coreTypeOf(n))).length})).filter(g=>g.count);
7
+ return function Outline({onClose,openRecord,openEdge}){
8
+ const [dataUrl,setDataUrl]=useState('');
9
+ useEffect(()=>{const url=URL.createObjectURL(new Blob([JSON.stringify(D,null,2)],{type:'application/json'}));setDataUrl(url);return()=>URL.revokeObjectURL(url)},[]);
10
+ const [tab,setTab]=useState('reading'),[query,setQuery]=useState(''),[type,setType]=useState(''),[limit,setLimit]=useState(30);
11
+ const q=useDeferredValue(query).trim().toLowerCase(),{value:complete,error,retry}=useCompleteData(!!q);
12
+ const source=complete||D;
13
+ const activeGroup=contentGroups.find(g=>g.id===type);
14
+ const nodes=useMemo(()=>source.nodes.filter(n=>(!activeGroup||activeGroup.types.includes(coreTypeOf(n)))&&(!q||cachedSearch(n).includes(q))),[source,activeGroup,q]);
15
+ const edges=useMemo(()=>source.edges.filter(e=>!q||[cachedSearch(e),recordLabel(byKey.get(e.from)),recordLabel(byKey.get(e.to)),predicateLabel[e.predicate]].join(' ').toLowerCase().includes(q)),[source,q]);
16
+ const row=(n,main=false,index=0)=><button key={n.key} className={'outline-row '+(main?'outline-main':'')} data-record={n.key} onClick={()=>openRecord(n.key)}>
17
+ <span className={'outline-mark '+(main?'numbered':'')} style={{color:colorOf(n)}}>{main?String(index+1).padStart(2,'0'):<TypeMark type={coreTypeOf(n)}/>}</span>
18
+ <span className="outline-row-copy"><b>{recordLabel(n)}</b>{main&&<small>{typeLabel(n)}</small>}</span>
19
+ {!main&&<small className="outline-kind">{typeLabel(n)}</small>}<Icon name="arrow-right" size={13}/>
20
+ </button>;
21
+ const branches=items=>items.length?<div className="outline-children">{items.map(item=><div key={item.id}>{row(byKey.get(item.id))}{branches(item.children)}</div>)}</div>:null;
22
+ const fold=(id,label,nodes,children,note)=> <details key={id} className="outline-supplement"><summary><span>{label}</span><small>{nodes.length} 项</small><Icon name="chevron-down" size={14}/></summary>{note&&<p className="outline-type-note">{note}</p>}{children||nodes.map(n=>row(n))}</details>;
23
+ return <aside className="catalog outline-panel" aria-label="论文大纲">
24
+ <div className="outline-header"><div><h2>大纲</h2><p>{D.nodes.length} 个对象 <span>·</span> {D.edges.length} 条关系</p></div><button className="outline-close" aria-label="关闭大纲" onClick={onClose}><Icon name="close" size={18}/></button></div>
25
+ <div className="outline-search"><Icon name="search" size={16}/><input type="search" value={query} onChange={e=>{setQuery(e.target.value);setLimit(30)}} placeholder="搜索内容、数值或来源" aria-label="搜索大纲"/>{query&&<button aria-label="清空搜索" onClick={()=>setQuery('')}><Icon name="close" size={14}/></button>}</div>
26
+ <nav className="outline-tabs" aria-label="大纲视图">{[['reading','阅读脉络'],['objects','对象分类'],['relations','关系']].map(([id,label])=><button key={id} aria-pressed={tab===id} onClick={()=>{setTab(id);setType('');setLimit(30)}}>{label}</button>)}</nav>
27
+ {tab==='objects'&&<div className="outline-filters" aria-label="对象类型筛选"><button aria-pressed={!type} onClick={()=>setType('')}>全部 <small>{D.nodes.length}</small></button>{categories.map(group=><button key={group.id} aria-pressed={type===group.id} title={group.types.map(id=>coreHints[id]).join(';')} onClick={()=>setType(group.id)}>{group.label}<small>{group.count}</small></button>)}</div>}
28
+ <div className="outline-scroll" key={tab}>
29
+ {q&&<div className="outline-result-count" role="status">{tab==='relations'?edges.length:nodes.length} 项匹配{!complete&&!error?' · 正在检索完整记录…':''}</div>}
30
+ {error&&<p role="alert" className="outline-empty">{error} <button onClick={retry}>重试</button></p>}
31
+ {tab==='reading'&&!q?<>
32
+ <div className="outline-section-label">研究脉络 <span>{spine.length} 个主节点</span></div>
33
+ <div className="outline-sections">{sections.map(s=><section key={s.id} className="outline-section">{row(byKey.get(s.id),true,s.index)}{branches(s.children)}</section>)}</div>
34
+ {!!appendices.length&&<><div className="outline-section-label">按需查阅 <span>{appendices.reduce((total,g)=>total+g.nodes.length,0)} 个补充对象</span></div><div className="outline-appendices">{appendices.map(group=>fold(group.id,group.label,group.nodes,group.parts.length?<div className="outline-subgroups">{group.parts.map(part=>fold(part.id,part.label,part.nodes))}</div>:null,group.note))}</div></>}
35
+ </>:tab==='relations'?<>
36
+ {!q&&<div className="outline-section-label">对象之间的关联 <span>{edges.length} 条</span></div>}
37
+ {edges.slice(0,limit).map(e=><button key={e.id} className="outline-relation" data-relation={e.id} onClick={()=>openEdge(e.id)}><span className="outline-relation-from">{recordLabel(byKey.get(e.from))}</span><span className="outline-predicate"><Icon name="arrow-right" size={13}/>{predicateLabel[e.predicate]||e.predicate}</span><span className="outline-relation-to">{recordLabel(byKey.get(e.to))}</span></button>)}
38
+ {edges.length>limit&&<button className="outline-more" onClick={()=>setLimit(n=>n+30)}>再显示 {Math.min(30,edges.length-limit)} 条 <span>{limit} / {edges.length}</span></button>}
39
+ {!edges.length&&<p className="outline-empty">没有匹配的关系</p>}
40
+ </>:<>
41
+ {!q&&<div className="outline-section-label">{activeGroup?activeGroup.label:'全部研究对象'} <span>{nodes.length} 项</span></div>}
42
+ {activeGroup&&<p className="outline-type-note">{activeGroup.types.map(id=>coreLabels[id]).join(' · ')}</p>}{nodes.map(n=>row(n))}
43
+ {!nodes.length&&<p className="outline-empty">{q?'没有匹配内容,试试其他关键词。':'本篇尚未建立此类独立对象。'}</p>}
44
+ </>}
45
+ </div>
46
+ <footer className="outline-footer">{D.sourceDocument&&<a href={D.sourceDocument} target="_blank" rel="noopener"><Icon name="document" size={14}/>原文</a>}<a href={dataUrl} download="arkgraph.json">全部数据 ↓</a></footer>
47
+ </aside>;
48
+ };
49
+ }