@anthusai/papyrus 1.2.0 → 1.4.0-next.1
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/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/app/newsroom/[section]/page.tsx +6 -0
- package/app/newsroom/articles/page.tsx +3 -0
- package/components/news-desk-client-provider.tsx +17 -2
- package/components/news-desk-taxonomy-client.ts +133 -2
- package/components/newspaper.tsx +4 -4
- package/components/newsroom-board-view.tsx +74 -50
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-organizer-shell.tsx +12 -7
- package/components/newsroom-organizer.css +4 -3
- package/components/newsroom-references-view.tsx +18 -0
- package/components/reference-cyclotron-priming.tsx +85 -0
- package/components/topic-steering-workspace.tsx +35 -1
- package/lib/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/chrome-text.ts +504 -0
- package/lib/content-item-lifecycle.ts +25 -26
- package/lib/front-mosaic.ts +1132 -0
- package/lib/furniture.ts +385 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +37 -9
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/local-preview-seed.ts +97 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-board.ts +63 -62
- package/lib/newsroom-organizer-nav.ts +3 -4
- package/lib/pretext-layout.ts +22 -0
- package/lib/relevance-status.ts +73 -0
- package/lib/site-brand.ts +15 -1
- package/lib/text-columns.ts +251 -0
- package/package.json +13 -2
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/routes.manifest.json +1 -9
- package/app/newsroom/work/page.tsx +0 -10
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { primingStateText, type PrimingRequestView } from "../lib/relevance-status";
|
|
5
|
+
|
|
6
|
+
export type PrimingFormInput = { url: string; label: string; explanation: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A compact form that primes the running relevance cyclotron with a pasted
|
|
10
|
+
* URL. The next sweep reads the page, adds it to the cyclotron's example list
|
|
11
|
+
* and records the outcome, which this list shows beside each saved request.
|
|
12
|
+
*/
|
|
13
|
+
export function ReferenceCyclotronPriming({
|
|
14
|
+
labels,
|
|
15
|
+
requests,
|
|
16
|
+
onRequestPriming,
|
|
17
|
+
}: {
|
|
18
|
+
labels: readonly string[];
|
|
19
|
+
requests: PrimingRequestView[];
|
|
20
|
+
onRequestPriming: (input: PrimingFormInput) => Promise<void> | void;
|
|
21
|
+
}) {
|
|
22
|
+
const [url, setUrl] = useState("");
|
|
23
|
+
const [label, setLabel] = useState(labels[0] ?? "");
|
|
24
|
+
const [explanation, setExplanation] = useState("");
|
|
25
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
async function submit() {
|
|
28
|
+
setMessage("Saving…");
|
|
29
|
+
try {
|
|
30
|
+
await onRequestPriming({ url, label, explanation });
|
|
31
|
+
setUrl("");
|
|
32
|
+
setExplanation("");
|
|
33
|
+
setMessage("Saved. The next sweep reads the page and adds it to the cyclotron.");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<section aria-label="Prime the relevance cyclotron" className="space-y-2 text-sm" data-reference-cyclotron-priming>
|
|
41
|
+
<form
|
|
42
|
+
className="flex flex-wrap items-end gap-3"
|
|
43
|
+
onSubmit={(event) => {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
void submit();
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<label className="flex min-w-64 flex-1 flex-col gap-1">
|
|
49
|
+
<span className="text-xs text-muted-foreground">Prime with a URL</span>
|
|
50
|
+
<input
|
|
51
|
+
aria-label="Priming URL"
|
|
52
|
+
onChange={(event) => setUrl(event.target.value)}
|
|
53
|
+
placeholder="https://"
|
|
54
|
+
required
|
|
55
|
+
type="url"
|
|
56
|
+
value={url}
|
|
57
|
+
/>
|
|
58
|
+
</label>
|
|
59
|
+
<label className="flex flex-col gap-1">
|
|
60
|
+
<span className="text-xs text-muted-foreground">Label</span>
|
|
61
|
+
<select aria-label="Priming label" onChange={(event) => setLabel(event.target.value)} value={label}>
|
|
62
|
+
{labels.map((choice) => (
|
|
63
|
+
<option key={choice} value={choice}>{choice}</option>
|
|
64
|
+
))}
|
|
65
|
+
</select>
|
|
66
|
+
</label>
|
|
67
|
+
<label className="flex min-w-48 flex-1 flex-col gap-1">
|
|
68
|
+
<span className="text-xs text-muted-foreground">Why (optional)</span>
|
|
69
|
+
<input aria-label="Priming explanation" onChange={(event) => setExplanation(event.target.value)} type="text" value={explanation} />
|
|
70
|
+
</label>
|
|
71
|
+
<button className="cyclotron-review-secondary" type="submit">Save</button>
|
|
72
|
+
{message ? <p className="basis-full text-xs" role="status">{message}</p> : null}
|
|
73
|
+
</form>
|
|
74
|
+
{requests.length ? (
|
|
75
|
+
<ul aria-label="Saved priming requests" className="space-y-1 text-xs text-muted-foreground">
|
|
76
|
+
{requests.slice(0, 10).map((view) => (
|
|
77
|
+
<li data-priming-request-state={view.outcome?.state ?? "waiting"} key={view.request.requestId}>
|
|
78
|
+
<span className="font-medium">{view.request.label}</span> {view.request.url} · {primingStateText(view)}
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
) : null}
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
type KnowledgeQueryResponse,
|
|
58
58
|
type NewsroomRecordPage,
|
|
59
59
|
loadCyclotronStatus,
|
|
60
|
+
requestCyclotronPriming,
|
|
61
|
+
loadCyclotronPrimingRequests,
|
|
60
62
|
requestCyclotronReviewRate,
|
|
61
63
|
} from "./news-desk-taxonomy-client";
|
|
62
64
|
import { listConsoleThreads } from "../lib/console-chat-client";
|
|
@@ -161,7 +163,13 @@ import {
|
|
|
161
163
|
referenceCurationStatusForAction,
|
|
162
164
|
} from "../lib/reference-policy";
|
|
163
165
|
import { currentRelevanceDecisions } from "../lib/relevance-decisions";
|
|
164
|
-
import {
|
|
166
|
+
import {
|
|
167
|
+
buildPrimingRequest,
|
|
168
|
+
buildReviewRateRequest,
|
|
169
|
+
DEFAULT_RELEVANCE_CYCLOTRON,
|
|
170
|
+
DEMO_CYCLOTRON_STATUS,
|
|
171
|
+
type PrimingRequestView,
|
|
172
|
+
} from "../lib/relevance-status";
|
|
165
173
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
166
174
|
import {
|
|
167
175
|
referenceDisplaySummary,
|
|
@@ -789,6 +797,23 @@ function NewsDeskDashboard({
|
|
|
789
797
|
() => [...relevanceDecisions.values()].find((decision) => decision.cyclotronId)?.cyclotronId ?? null,
|
|
790
798
|
[relevanceDecisions],
|
|
791
799
|
);
|
|
800
|
+
const primingCyclotronId = relevanceCyclotronId ?? DEFAULT_RELEVANCE_CYCLOTRON.id;
|
|
801
|
+
const primingLabels = useMemo(
|
|
802
|
+
() => [...relevanceDecisions.values()].find((decision) => decision.classes.length)?.classes
|
|
803
|
+
?? DEFAULT_RELEVANCE_CYCLOTRON.labels,
|
|
804
|
+
[relevanceDecisions],
|
|
805
|
+
);
|
|
806
|
+
const [primingRequests, setPrimingRequests] = useState<PrimingRequestView[]>([]);
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
if (dashboard.isDemo) return;
|
|
809
|
+
let cancelled = false;
|
|
810
|
+
const refresh = () => loadCyclotronPrimingRequests(primingCyclotronId)
|
|
811
|
+
.then((views) => { if (!cancelled) setPrimingRequests(views); })
|
|
812
|
+
.catch(() => { if (!cancelled) setPrimingRequests([]); });
|
|
813
|
+
void refresh();
|
|
814
|
+
const timer = window.setInterval(() => { void refresh(); }, 30_000);
|
|
815
|
+
return () => { cancelled = true; window.clearInterval(timer); };
|
|
816
|
+
}, [dashboard.isDemo, primingCyclotronId]);
|
|
792
817
|
const [cyclotronStatus, setCyclotronStatus] = useState<CyclotronStatus | null>(null);
|
|
793
818
|
useEffect(() => {
|
|
794
819
|
if (!relevanceCyclotronId) return;
|
|
@@ -3189,6 +3214,15 @@ function NewsDeskDashboard({
|
|
|
3189
3214
|
disabled={controlsDisabled}
|
|
3190
3215
|
initialReferenceLineageId={initialSelection.reference ?? pathnameReferenceLineageId}
|
|
3191
3216
|
cyclotronStatus={cyclotronStatus}
|
|
3217
|
+
cyclotronPriming={{
|
|
3218
|
+
labels: primingLabels,
|
|
3219
|
+
requests: primingRequests,
|
|
3220
|
+
onRequest: async (input) => {
|
|
3221
|
+
const request = buildPrimingRequest(input, authState.label, primingLabels);
|
|
3222
|
+
if (!dashboard.isDemo) await requestCyclotronPriming(primingCyclotronId, request);
|
|
3223
|
+
setPrimingRequests((current) => [{ request, outcome: null }, ...current]);
|
|
3224
|
+
},
|
|
3225
|
+
}}
|
|
3192
3226
|
decisions={relevanceDecisions}
|
|
3193
3227
|
onRequestReviewRate={relevanceCyclotronId ? async (choice) => {
|
|
3194
3228
|
const request = buildReviewRateRequest(choice, authState.label);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ArticlePublicationItem,
|
|
3
|
+
getArticlePublicationItems,
|
|
4
|
+
type PublicationItem,
|
|
5
|
+
} from "./publication-items";
|
|
6
|
+
import { type LayoutCursor } from "./pretext-layout";
|
|
7
|
+
import { type ArticleFlow, type PlacedTextRange, type SolvedFurniture } from "./layout-types";
|
|
8
|
+
import { EMPTY_CURSOR } from "./layout-config";
|
|
9
|
+
|
|
10
|
+
export function createArticleFlows(items: PublicationItem[]): Map<string, ArticleFlow> {
|
|
11
|
+
return new Map(
|
|
12
|
+
getArticlePublicationItems(items).map((article) => [
|
|
13
|
+
article.slug,
|
|
14
|
+
{
|
|
15
|
+
article,
|
|
16
|
+
currentCursor: { ...EMPTY_CURSOR },
|
|
17
|
+
placedRanges: [],
|
|
18
|
+
usedImageAssetIds: new Set<string>(),
|
|
19
|
+
},
|
|
20
|
+
]),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getOrCreateFlow(flows: Map<string, ArticleFlow>, flowKey: string, article: ArticlePublicationItem): ArticleFlow {
|
|
25
|
+
const existing = flows.get(flowKey);
|
|
26
|
+
if (existing) return existing;
|
|
27
|
+
const flow = {
|
|
28
|
+
article,
|
|
29
|
+
currentCursor: { ...EMPTY_CURSOR },
|
|
30
|
+
placedRanges: [],
|
|
31
|
+
usedImageAssetIds: new Set<string>(),
|
|
32
|
+
};
|
|
33
|
+
flows.set(flowKey, flow);
|
|
34
|
+
return flow;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resetArticleFlow(flow: ArticleFlow): void {
|
|
38
|
+
flow.currentCursor = { ...EMPTY_CURSOR };
|
|
39
|
+
flow.placedRanges = [];
|
|
40
|
+
flow.usedImageAssetIds.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function requireKnownItem(itemId: string, itemsBySlug: Map<string, PublicationItem>): PublicationItem {
|
|
44
|
+
const item = itemsBySlug.get(itemId);
|
|
45
|
+
if (!item) throw new Error(`Layout references missing item ${itemId}`);
|
|
46
|
+
return item;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requireArticleItem(itemId: string, itemsBySlug: Map<string, PublicationItem>): ArticlePublicationItem {
|
|
50
|
+
const item = requireKnownItem(itemId, itemsBySlug);
|
|
51
|
+
if (item.type !== "article") throw new Error(`Layout articleFrame requires article item ${itemId}`);
|
|
52
|
+
return item;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createTextRange({
|
|
56
|
+
flow,
|
|
57
|
+
pageId,
|
|
58
|
+
blockId,
|
|
59
|
+
startCursor,
|
|
60
|
+
endCursor,
|
|
61
|
+
exhausted,
|
|
62
|
+
}: {
|
|
63
|
+
flow: ArticleFlow;
|
|
64
|
+
pageId: string;
|
|
65
|
+
blockId: string;
|
|
66
|
+
startCursor: LayoutCursor;
|
|
67
|
+
endCursor: LayoutCursor;
|
|
68
|
+
exhausted: boolean;
|
|
69
|
+
}): PlacedTextRange {
|
|
70
|
+
return {
|
|
71
|
+
articleId: flow.article.slug,
|
|
72
|
+
pageId,
|
|
73
|
+
blockId,
|
|
74
|
+
startCursor,
|
|
75
|
+
endCursor,
|
|
76
|
+
exhausted,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function commitTextRange(flow: ArticleFlow, range: PlacedTextRange): void {
|
|
81
|
+
flow.currentCursor = { ...range.endCursor };
|
|
82
|
+
flow.placedRanges.push(range);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function commitUsedImageFurniture(flow: ArticleFlow, furniture: SolvedFurniture[]): void {
|
|
86
|
+
for (const item of furniture) {
|
|
87
|
+
if (item.kind === "image") {
|
|
88
|
+
flow.usedImageAssetIds.add(item.assetId);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (item.kind === "mediaCluster") {
|
|
92
|
+
for (const image of item.images) flow.usedImageAssetIds.add(image.assetId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|