@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.
- package/LICENSE +202 -0
- package/README.md +128 -0
- package/data/dataset-source-registry.v1.json +300 -0
- package/dist/arkgraph/boot.js +6 -0
- package/dist/arkgraph/index.html +1 -0
- package/dist/arkgraph/viewer.css +1 -0
- package/dist/arkgraph/viewer.en.css +1 -0
- package/dist/arkgraph/viewer.en.js +49 -0
- package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
- package/dist/arkgraph/viewer.js +49 -0
- package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
- package/docker/claude-code/Dockerfile +97 -0
- package/docker/claude-code/codex-pro-relay.mjs +466 -0
- package/docker/claude-code/runtime-contract-check.mjs +79 -0
- package/docs/arkgraph-reading.md +79 -0
- package/docs/configuration.md +100 -0
- package/docs/integration.md +92 -0
- package/docs/maturity-plan.md +27 -0
- package/docs/npm-release.md +44 -0
- package/docs/paper-reading.md +40 -0
- package/docs/research-plan-granularity.md +27 -0
- package/docs/terminal.md +49 -0
- package/examples/toy-evaluation/compile-task.json +27 -0
- package/examples/toy-evaluation/paper.md +5 -0
- package/examples/toy-evaluation/repository/README.md +9 -0
- package/examples/toy-evaluation/repository/checkpoint.json +4 -0
- package/examples/toy-evaluation/repository/evaluate.py +17 -0
- package/examples/toy-evaluation/task.json +81 -0
- package/package.json +59 -0
- package/prompts/compile-research.md +58 -0
- package/prompts/execute-contract.md +72 -0
- package/prompts/execute-workspace-simple.md +51 -0
- package/prompts/execute-workspace.md +34 -0
- package/prompts/prepare-reproduction.md +82 -0
- package/prompts/repair-research.md +45 -0
- package/protocol/CAP.md +129 -0
- package/protocol/LICENSE +12 -0
- package/protocol/MAPPINGS.md +72 -0
- package/protocol/README.md +38 -0
- package/protocol/conformance-v2.0-alpha.1.json +36 -0
- package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
- package/protocol/examples/arkgraph/fixtures.mjs +49 -0
- package/protocol/examples/arkgraph/paper-free.json +291 -0
- package/protocol/examples/arkgraph/partial-failure.json +344 -0
- package/protocol/examples/arkgraph/training-evaluation.json +443 -0
- package/protocol/profiles/agent-trace.md +16 -0
- package/protocol/profiles/computational-run.md +16 -0
- package/protocol/profiles/core.md +15 -0
- package/protocol/profiles/public-bundle.md +18 -0
- package/protocol/profiles/reproduction.md +29 -0
- package/protocol/profiles/research-compilation.md +44 -0
- package/protocol/profiles/research-plan.md +39 -0
- package/protocol/profiles/restricted-evidence.md +15 -0
- package/runtime/bootstrap-autodl-runtime.sh +314 -0
- package/runtime/create-runtime-venv.sh +41 -0
- package/runtime/install-local-cpu-runtime.sh +23 -0
- package/runtime/install-scientific-runtime.sh +153 -0
- package/runtime/mineru/parse.py +62 -0
- package/runtime/mineru/requirements.txt +4 -0
- package/runtime/requirements-baseline.txt +38 -0
- package/schemas/cap/v2/activity.schema.json +47 -0
- package/schemas/cap/v2/agent.schema.json +32 -0
- package/schemas/cap/v2/assertion.schema.json +110 -0
- package/schemas/cap/v2/descriptor.schema.json +243 -0
- package/schemas/cap/v2/entity.schema.json +64 -0
- package/schemas/cap/v2/manifest.schema.json +67 -0
- package/schemas/cap/v2/relation.schema.json +82 -0
- package/schemas/compute-catalog.schema.json +63 -0
- package/schemas/compute-decision.schema.json +27 -0
- package/schemas/execution-contract.schema.json +1024 -0
- package/schemas/research-card.schema.json +30 -0
- package/schemas/research-inventory-draft.schema.json +366 -0
- package/schemas/research.schema.json +1044 -0
- package/schemas/result.schema.json +173 -0
- package/schemas/verification-policy.schema.json +47 -0
- package/schemas/verified-conclusion.schema.json +58 -0
- package/schemas/workspace-summary.schema.json +24 -0
- package/scripts/build-arkgraph-view.mjs +12 -0
- package/scripts/check-execution-feasibility.mjs +24 -0
- package/scripts/check-syntax.mjs +15 -0
- package/scripts/deterministic-asset-preparation.py +438 -0
- package/scripts/package-cap.mjs +23 -0
- package/scripts/package-local-agent.mjs +23 -0
- package/scripts/preview-arkgraph.mjs +25 -0
- package/scripts/replay-research-compiler-candidate.mjs +134 -0
- package/scripts/review-compiler-sources.mjs +44 -0
- package/scripts/run-asset-preparation.sh +17 -0
- package/scripts/run-research-plan.mjs +98 -0
- package/scripts/validate-asset-preparation.py +290 -0
- package/scripts/verify-local-runtime.mjs +57 -0
- package/scripts/verify-npm-package.mjs +57 -0
- package/src/adapters/paper2agent.mjs +107 -0
- package/src/assets/cache.mjs +159 -0
- package/src/assets/compute.mjs +98 -0
- package/src/assets/executor.mjs +145 -0
- package/src/assets/lifecycle.mjs +213 -0
- package/src/assets/manifest.mjs +242 -0
- package/src/assets/opportunistic-preparation.mjs +81 -0
- package/src/assets/plan.mjs +411 -0
- package/src/assets/prompts.mjs +29 -0
- package/src/assets/public-asset-probe.mjs +525 -0
- package/src/assets/qualification.mjs +119 -0
- package/src/assets/readiness.mjs +130 -0
- package/src/assets/reproduction-admission.mjs +355 -0
- package/src/assets/requirements.mjs +152 -0
- package/src/assets/source-grounding.mjs +341 -0
- package/src/assets/source-policy.mjs +118 -0
- package/src/autodl/client.mjs +260 -0
- package/src/autodl/ssh.mjs +380 -0
- package/src/autodl/tools.mjs +129 -0
- package/src/cap/redaction.mjs +38 -0
- package/src/cap/v2/archive.mjs +152 -0
- package/src/cap/v2/attestation.mjs +204 -0
- package/src/cap/v2/canonical-json.mjs +114 -0
- package/src/cap/v2/compilation-artifact.mjs +240 -0
- package/src/cap/v2/core.mjs +282 -0
- package/src/cap/v2/measurement-assessment-records.mjs +23 -0
- package/src/cap/v2/pipeline-artifact.mjs +922 -0
- package/src/cap/v2/read.mjs +41 -0
- package/src/cap/v2/reassessment-artifact.mjs +383 -0
- package/src/cap/v2/research-artifact.mjs +231 -0
- package/src/cap/v2/research-map-records.mjs +46 -0
- package/src/cap/v2/research-object-records.mjs +163 -0
- package/src/cap/v2/research-records.mjs +187 -0
- package/src/cap/v2/verify.mjs +642 -0
- package/src/cli.mjs +1146 -0
- package/src/compute/autodl-pro-compiler.mjs +347 -0
- package/src/compute/autodl-pro-executor.mjs +459 -0
- package/src/compute/autodl-pro-job.mjs +843 -0
- package/src/compute/autodl-pro-network.mjs +295 -0
- package/src/compute/autodl-pro-remote.mjs +810 -0
- package/src/compute/autodl-pro-staging.mjs +117 -0
- package/src/compute/campaign.mjs +110 -0
- package/src/compute/catalog.mjs +123 -0
- package/src/compute/checkpoint-protocol.mjs +154 -0
- package/src/compute/codex-account-lock.mjs +111 -0
- package/src/compute/codex-account-session.mjs +107 -0
- package/src/compute/compiler-profile.mjs +38 -0
- package/src/compute/compiler-router.mjs +23 -0
- package/src/compute/coordinator-recovery.mjs +210 -0
- package/src/compute/executor-router.mjs +29 -0
- package/src/compute/gcp-batch-compiler.mjs +685 -0
- package/src/compute/gcp-batch-executor.mjs +1215 -0
- package/src/compute/gcp-batch-failure.mjs +92 -0
- package/src/compute/gcp-batch-job.mjs +527 -0
- package/src/compute/gcp-batch-lifecycle.mjs +81 -0
- package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
- package/src/compute/local-codex-compiler.mjs +52 -0
- package/src/compute/measurement-hardware.mjs +128 -0
- package/src/compute/remote-attempt.mjs +226 -0
- package/src/compute/requirements.mjs +124 -0
- package/src/compute/research-phases.mjs +48 -0
- package/src/compute/scheduler.mjs +452 -0
- package/src/compute/shared-workloads.mjs +26 -0
- package/src/compute/stage-archive.mjs +79 -0
- package/src/contracts/campaign-contract.mjs +52 -0
- package/src/contracts/execution-contract.mjs +819 -0
- package/src/contracts/execution-mode.mjs +19 -0
- package/src/contracts/execution-timeouts.mjs +45 -0
- package/src/contracts/execution-workload.mjs +68 -0
- package/src/contracts/preflight-schema.mjs +25 -0
- package/src/contracts/public-contract.mjs +63 -0
- package/src/contracts/subject-tags.mjs +31 -0
- package/src/dashboard/data.mjs +898 -0
- package/src/dashboard/server.mjs +79 -0
- package/src/dashboard/static/dashboard.css +366 -0
- package/src/dashboard/static/dashboard.js +560 -0
- package/src/dashboard/static/index.html +85 -0
- package/src/deployment/community-policy.mjs +9 -0
- package/src/deployment/environment.mjs +112 -0
- package/src/deployment/guided.mjs +98 -0
- package/src/deployment/handoff.mjs +102 -0
- package/src/deployment/local-contract.mjs +31 -0
- package/src/deployment/local.mjs +100 -0
- package/src/deployment/prepare.mjs +46 -0
- package/src/deployment/recipe.mjs +108 -0
- package/src/deployment/supplement.mjs +51 -0
- package/src/deployment/terminal.mjs +43 -0
- package/src/diagnosis/renderer.mjs +75 -0
- package/src/diagnosis/target-failure.mjs +46 -0
- package/src/evidence/parser-registry.mjs +54 -0
- package/src/evidence/parsers/fasttext-classification.mjs +82 -0
- package/src/evidence/parsers/json-scalar.mjs +96 -0
- package/src/evidence/parsers/simcse-senteval.mjs +104 -0
- package/src/evidence/parsers/starspace-classification.mjs +78 -0
- package/src/evidence/registry.mjs +147 -0
- package/src/execution/runner-audit.mjs +473 -0
- package/src/gcp/auth.mjs +106 -0
- package/src/gcp/batch-client.mjs +120 -0
- package/src/gcp/resource-discovery.mjs +177 -0
- package/src/gcp/rest.mjs +82 -0
- package/src/gcp/secret-manager.mjs +34 -0
- package/src/gcp/signed-url.mjs +133 -0
- package/src/gcp/storage.mjs +220 -0
- package/src/graph/command.mjs +41 -0
- package/src/graph/execution.mjs +97 -0
- package/src/graph/model.mjs +37 -0
- package/src/graph/presentation.mjs +110 -0
- package/src/graph/query.mjs +159 -0
- package/src/graph/research-relations.mjs +69 -0
- package/src/graph/source-page.mjs +12 -0
- package/src/graph/source-preview.mjs +34 -0
- package/src/graph/validate.mjs +76 -0
- package/src/job.mjs +496 -0
- package/src/network/autodl-routing-proxy.mjs +462 -0
- package/src/network/egress-proxy.mjs +158 -0
- package/src/observability/event-contract.mjs +230 -0
- package/src/observability/pipeline-monitor.mjs +166 -0
- package/src/pipeline/orchestrator.mjs +1281 -0
- package/src/pipeline/recovery-error.mjs +11 -0
- package/src/pipeline/replay.mjs +304 -0
- package/src/pipeline/shared-execution.mjs +115 -0
- package/src/pipeline/stage-checkpoint.mjs +86 -0
- package/src/pipeline/stage-recovery.mjs +101 -0
- package/src/pipeline/targets.mjs +110 -0
- package/src/process.mjs +143 -0
- package/src/protocol.mjs +312 -0
- package/src/provider/codex-account.mjs +44 -0
- package/src/provider/codex-completion.mjs +49 -0
- package/src/provider/completion.mjs +292 -0
- package/src/provider/model-client.mjs +44 -0
- package/src/provider/model-route.mjs +29 -0
- package/src/provider/openrouter-readiness.mjs +189 -0
- package/src/provider/reader-bridge.mjs +35 -0
- package/src/provider/relay.mjs +263 -0
- package/src/provider/runtime-auth.mjs +40 -0
- package/src/public/cap.d.mts +90 -0
- package/src/public/cap.mjs +12 -0
- package/src/public/contracts.d.mts +2 -0
- package/src/public/host.mjs +171 -0
- package/src/public/operations.d.mts +11 -0
- package/src/public/presentation.d.mts +4 -0
- package/src/records/views.mjs +26 -0
- package/src/remote/command.mjs +178 -0
- package/src/remote/ssh.mjs +59 -0
- package/src/repository-origin.mjs +81 -0
- package/src/reproduction/evidence-feedback.mjs +96 -0
- package/src/reproduction/incomplete-initialization.mjs +25 -0
- package/src/reproduction/lifecycle.mjs +253 -0
- package/src/reproduction/plan.mjs +132 -0
- package/src/reproduction/prompts.mjs +70 -0
- package/src/reproduction/runner.mjs +188 -0
- package/src/reproduction/summary.mjs +130 -0
- package/src/reproduction/workspace-mode.mjs +7 -0
- package/src/research/automatic-admission.mjs +156 -0
- package/src/research/compiler-coverage.mjs +85 -0
- package/src/research/compiler-failure.mjs +24 -0
- package/src/research/compiler-normalization-guards.mjs +112 -0
- package/src/research/compiler-repair.mjs +3 -0
- package/src/research/compiler.mjs +853 -0
- package/src/research/continuation-selection.mjs +26 -0
- package/src/research/execution-graph-context.mjs +43 -0
- package/src/research/experiment-importance.mjs +15 -0
- package/src/research/inventory-handoff.mjs +104 -0
- package/src/research/inventory-revisions.mjs +32 -0
- package/src/research/mineru-local.mjs +73 -0
- package/src/research/paper-command.mjs +19 -0
- package/src/research/paper-markdown.mjs +180 -0
- package/src/research/paper-source-map.mjs +69 -0
- package/src/research/planning-policy.mjs +88 -0
- package/src/research/reference-materials.mjs +11 -0
- package/src/research/reproduction-scope.mjs +30 -0
- package/src/research/research-map.mjs +94 -0
- package/src/research/research-objects.mjs +88 -0
- package/src/research/source-discovery.mjs +646 -0
- package/src/research/source-observations.mjs +75 -0
- package/src/research/source-review-cli-mcp.mjs +26 -0
- package/src/research/source-review-input.mjs +209 -0
- package/src/research/source-review-local-codex.mjs +36 -0
- package/src/research/source-review-model.mjs +70 -0
- package/src/research/source-review.mjs +173 -0
- package/src/research/structure.mjs +3163 -0
- package/src/research-card/renderer.mjs +277 -0
- package/src/research-card/verified-conclusion.mjs +143 -0
- package/src/results/output-registry.mjs +183 -0
- package/src/runtime/claude-code.mjs +52 -0
- package/src/runtime/codex-capacity-retry.mjs +87 -0
- package/src/runtime/codex.mjs +64 -0
- package/src/runtime/config.mjs +157 -0
- package/src/runtime/final-output.mjs +40 -0
- package/src/runtime/index.mjs +21 -0
- package/src/runtime/local-codex.mjs +74 -0
- package/src/runtime/opencode.mjs +95 -0
- package/src/runtime/prompt.mjs +13 -0
- package/src/sandbox/docker.mjs +363 -0
- package/src/settings/command.mjs +297 -0
- package/src/settings/store.mjs +119 -0
- package/src/telemetry/pricing.mjs +68 -0
- package/src/telemetry/usage.mjs +265 -0
- package/src/terminal/events.mjs +97 -0
- package/src/terminal/input.mjs +40 -0
- package/src/terminal/plain.mjs +40 -0
- package/src/terminal/remote-stream.mjs +22 -0
- package/src/terminal/screen.mjs +214 -0
- package/src/terminal/transcript.mjs +69 -0
- package/src/util.mjs +107 -0
- package/src/verification/ai-assessor.mjs +534 -0
- package/src/verification/claim-evaluator.mjs +242 -0
- package/src/verification/evidence-context.mjs +165 -0
- package/src/verification/evidence-reader.mjs +95 -0
- package/src/verification/integrity.mjs +570 -0
- package/src/verification/tolerance.mjs +32 -0
- package/src/workloads/cpu-research-preparation.mjs +56 -0
- package/src/workloads/definition.mjs +74 -0
- package/src/workloads/phase-aware-reproduction.mjs +46 -0
- package/src/workloads/reproduction.mjs +85 -0
- package/src/workspace/command.mjs +242 -0
- package/src/workspace/control.mjs +49 -0
- package/src/workspace/entry.mjs +28 -0
- package/src/workspace/input.mjs +93 -0
- package/src/workspace/interactive.mjs +94 -0
- package/src/workspace/jobs.mjs +418 -0
- package/src/workspace/session.mjs +97 -0
- package/src/workspace/worker.mjs +137 -0
- package/ui/arkgraph/ambient-motion.mjs +10 -0
- package/ui/arkgraph/app.jsx +153 -0
- package/ui/arkgraph/boot.js +6 -0
- package/ui/arkgraph/camera-motion.mjs +20 -0
- package/ui/arkgraph/context-reveal.mjs +39 -0
- package/ui/arkgraph/details.css +3 -0
- package/ui/arkgraph/entry.jsx +28 -0
- package/ui/arkgraph/experiment-curves.mjs +17 -0
- package/ui/arkgraph/experiment-selection.mjs +15 -0
- package/ui/arkgraph/experiment-style.css +26 -0
- package/ui/arkgraph/experiment-ui.jsx +32 -0
- package/ui/arkgraph/frame.html +1 -0
- package/ui/arkgraph/graph-gestures.mjs +62 -0
- package/ui/arkgraph/label-layout.mjs +57 -0
- package/ui/arkgraph/locales/en.json +229 -0
- package/ui/arkgraph/locales/source-types.json +15 -0
- package/ui/arkgraph/localization-build.mjs +27 -0
- package/ui/arkgraph/material-build.mjs +23 -0
- package/ui/arkgraph/material-colors.mjs +39 -0
- package/ui/arkgraph/material-style.css +15 -0
- package/ui/arkgraph/open-graph.jsx +326 -0
- package/ui/arkgraph/outline.jsx +49 -0
- package/ui/arkgraph/package-lock.json +888 -0
- package/ui/arkgraph/package.json +17 -0
- package/ui/arkgraph/reading-layout.mjs +130 -0
- package/ui/arkgraph/reading-presentation.mjs +73 -0
- package/ui/arkgraph/record-detail.css +51 -0
- package/ui/arkgraph/record-details.jsx +29 -0
- package/ui/arkgraph/research-types.mjs +31 -0
- package/ui/arkgraph/selection-mark.jsx +6 -0
- package/ui/arkgraph/soft-spine.mjs +26 -0
- package/ui/arkgraph/steering-style.css +187 -0
- 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&¤t.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&¤t.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
|
+
}
|