Export template source from be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9:examples/local-first/packages/language-reader-presentation

This commit is contained in:
Quixos Template Publisher
2000-01-01 00:00:00 +00:00
commit 19b330933d
61 changed files with 9997 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
import { useEffect, useRef, useState } from "react";
import { referenceOf, useInterface } from "@quixos/camino-react";
import { interfaces } from "./gen/client.js";
import { sameCacheConfiguration } from "./cache.js";
import { accepted, errorMessage, type DocNode, type FeatureNode } from "./model.js";
import { preparationQueue, processorMatches, sameScope } from "./work.js";
import type { Range } from "./canvas/types.js";
import type { ReadingWindow } from "./canvas/pages.js";
import type { AnnotationRender } from "./render-hooks.js";
type Planned = { feature: FeatureNode; range: Range; context: string; planning?: "click" | "hover" };
/** One admission/cache path for proximity preparation, clicks, hover and retry.
* A changed window replaces unsent plans. Admitted jobs retain their results.
* Model preparation has two slots, shared by independent scopes. */
export function useProcessing({
document: doc,
annotationsReady,
features,
revisions,
revisionAliases = {},
computations,
renders,
window,
error,
}: {
document: DocNode;
annotationsReady: boolean;
features: FeatureNode[];
revisions: Record<string, string>;
revisionAliases?: Record<string, readonly string[]>;
computations: Record<string, string>;
renders: Record<string, AnnotationRender[] | undefined>;
window: ReadingWindow;
error: (message: string) => void;
}) {
const ops = useInterface(
{ plan: interfaces.ReadingDocument.plan, generate: interfaces.ReadingDocument.generate },
referenceOf(doc),
);
const jobs = doc.jobs.entries.map(({ node }) => node);
const [planning, setPlanning] = useState<{ context: string; items: Planned[] }>({ context: "", items: [] }),
[tick, setTick] = useState(0);
const [clickScopes, setClickScopes] = useState<Record<string, Range[]>>({});
const [intent, setIntent] = useState<Planned[]>([]);
const submitted = useRef(new Set<string>()),
admitting = useRef(new Map<string, Planned>()),
focusSerial = useRef({ click: 0, hover: 0 });
const featureKey = features
.map(
(f) =>
`${referenceOf(f).object}:${computations[referenceOf(f).object]}:${f.data.enabled}:${revisions[f.data.processor]}`,
)
.join("|");
const context = `${referenceOf(doc).object}:${doc.data.sourceRevision}:${featureKey}`;
const currentContext = useRef(context);
currentContext.current = context;
const planningContext = [
context,
window.nearby.start,
window.nearby.end,
window.visible.start,
window.visible.end,
].join(":");
const plans = planning.context === planningContext ? planning.items : [];
const matches = (job: (typeof jobs)[number], { feature, range }: Planned) =>
processorMatches(job.data, revisions[feature.data.processor], revisionAliases[feature.data.processor]) &&
sameScope(
{ ...job.data, configuration: job.configuration },
doc.data.sourceRevision,
computations[referenceOf(feature).object],
range,
);
const keyFor = ({ feature, range }: Planned) =>
[
doc.data.sourceRevision,
computations[referenceOf(feature).object],
revisions[feature.data.processor],
range.start,
range.end,
].join(":");
function request(feature: FeatureNode, range: Range, retry = false): boolean {
if (!annotationsReady || ops.status !== "ready" || range.end <= range.start || !revisions[feature.data.processor])
return false;
const item = { feature, range, context },
key = keyFor(item);
if (!retry && (submitted.current.has(key) || jobs.some((job) => matches(job, item)))) return false;
submitted.current.add(key);
setIntent((previous) => [...previous.filter((item) => keyFor(item) !== key), item]);
admitting.current.set(key, item);
void accepted(
ops.value.generate.call({
feature: { ...referenceOf(feature), expectation: { kind: "atom", id: "atom:reading:feature" } },
...range,
retry,
createdAt: new Date().toISOString(),
}),
).catch((cause) => {
admitting.current.delete(key);
setIntent((previous) => previous.filter((item) => item.context !== context || keyFor(item) !== key));
error(errorMessage(cause));
setTick((value) => value + 1);
});
return true;
}
useEffect(() => {
let closed = false;
setPlanning({ context: planningContext, items: [] });
if (!annotationsReady || ops.status !== "ready" || window.nearby.end <= window.nearby.start) return;
const timer = setTimeout(() => {
void Promise.all(
features
.filter((f) => f.data.enabled && f.data.generation !== "focus")
.map(async (feature) => {
const ranges = await ops.value.plan.call({
feature: { ...referenceOf(feature), expectation: { kind: "atom", id: "atom:reading:feature" } },
...window.nearby,
visibleStart: window.visible.start,
visibleEnd: window.visible.end,
kind: "nearby",
});
return ranges.map((range) => ({ feature, range, context }));
}),
)
.then((all) => {
if (!closed) setPlanning({ context: planningContext, items: all.flat() });
})
.catch((cause) => {
if (!closed) error(errorMessage(cause));
});
}, 180);
return () => {
closed = true;
clearTimeout(timer);
};
}, [planningContext, ops.status, annotationsReady]);
useEffect(() => {
for (const [key, item] of admitting.current)
if (jobs.some((job) => matches(job, item))) admitting.current.delete(key);
setIntent((previous) => {
const next = previous.filter((item) => item.context === context && !jobs.some((job) => matches(job, item)));
return next.length === previous.length ? previous : next;
});
const queue = nearbyQueue();
for (const { feature, range } of queue.admit) request(feature, range);
}, [planning, doc.jobs.entries, context, tick, planningContext]);
function nearbyQueue() {
const pending = jobs.filter(
(job) =>
job.data.kind === "annotation" &&
job.data.status === "queued" &&
job.data.sourceRevision === doc.data.sourceRevision &&
features.some(
(f) =>
f.data.enabled &&
f.data.generation !== "focus" &&
sameCacheConfiguration(computations[referenceOf(f).object], job.configuration) &&
processorMatches(job.data, revisions[f.data.processor], revisionAliases[f.data.processor]),
),
);
const inAdmission = [...admitting.current.values()].filter(
(item) =>
item.context === context &&
item.feature.data.generation === "section" &&
!jobs.some((job) => matches(job, item)),
);
return preparationQueue(
annotationsReady && ops.status === "ready"
? plans
.filter(
(item) => item.context === context && item.feature.data.enabled && revisions[item.feature.data.processor],
)
.map((item) => ({
...item,
key: keyFor(item),
configuration: computations[referenceOf(item.feature).object],
local: item.feature.data.generation === "local",
}))
: [],
window.visible,
pending.length + inAdmission.length,
submitted.current,
new Set(plans.filter((item) => jobs.some((job) => matches(job, item))).map(keyFor)),
);
}
async function focus(range: Range, kind: "click" | "hover") {
const serial = ++focusSerial.current[kind];
if (kind === "click") setClickScopes({});
const selected = features.filter(
(f) =>
f.data.enabled &&
f.data.generation === "focus" &&
(renders[f.data.processor] ?? [{ reveal: f.data.reveal }]).some((render) =>
kind === "hover" ? render.reveal === "hover" : render.reveal !== "hover",
),
);
// Covering the clicked word does not imply a cache hit for the newly
// planned sentence/passage. Show intent now; exact scope identity decides
// reuse after planning, even while operation-input admission is pending.
setIntent((previous) => [
...previous.filter((item) => item.planning !== kind),
...selected.map((feature) => ({ feature, range, context, planning: kind })),
]);
if (!annotationsReady || ops.status !== "ready") return;
await Promise.all(
selected.map(async (feature) => {
try {
const ranges = await ops.value.plan.call({
feature: { ...referenceOf(feature), expectation: { kind: "atom", id: "atom:reading:feature" } },
...range,
visibleStart: range.start,
visibleEnd: range.end,
kind: "focus",
});
if (serial === focusSerial.current[kind] && currentContext.current === context) {
setIntent((previous) =>
previous.filter(
(item) => !(item.planning === kind && referenceOf(item.feature).object === referenceOf(feature).object),
),
);
if (kind === "click")
setClickScopes((previous) => ({ ...previous, [referenceOf(feature).object]: [...ranges] }));
for (const scope of ranges) request(feature, scope);
}
} catch (cause) {
if (serial === focusSerial.current[kind] && currentContext.current === context) {
setIntent((previous) =>
previous.filter(
(item) => !(item.planning === kind && referenceOf(item.feature).object === referenceOf(feature).object),
),
);
error(errorMessage(cause));
}
}
}),
);
}
useEffect(
() => () => {
focusSerial.current.click++;
focusSerial.current.hover++;
},
[],
);
return {
ready:
annotationsReady &&
ops.status === "ready" &&
features.filter((feature) => feature.data.enabled).every((feature) => !!revisions[feature.data.processor]),
request,
focus,
clickScopes,
pending: [
...nearbyQueue().waiting,
...intent.filter(
(item) =>
item.context === context &&
features.some(
(feature) => referenceOf(feature).object === referenceOf(item.feature).object && feature.data.enabled,
) &&
!jobs.some((job) => matches(job, item)),
),
],
cancelHover: () => {
focusSerial.current.hover++;
setIntent((previous) => previous.filter((item) => item.planning !== "hover"));
},
};
}