@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.
Files changed (37) hide show
  1. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  2. package/app/newsroom/[section]/page.tsx +6 -0
  3. package/app/newsroom/articles/page.tsx +3 -0
  4. package/components/news-desk-client-provider.tsx +17 -2
  5. package/components/news-desk-taxonomy-client.ts +133 -2
  6. package/components/newspaper.tsx +4 -4
  7. package/components/newsroom-board-view.tsx +74 -50
  8. package/components/newsroom-client-shell.tsx +8 -1
  9. package/components/newsroom-organizer-shell.tsx +12 -7
  10. package/components/newsroom-organizer.css +4 -3
  11. package/components/newsroom-references-view.tsx +18 -0
  12. package/components/reference-cyclotron-priming.tsx +85 -0
  13. package/components/topic-steering-workspace.tsx +35 -1
  14. package/lib/article-flow.ts +95 -0
  15. package/lib/article-frame-solver.ts +519 -0
  16. package/lib/chrome-text.ts +504 -0
  17. package/lib/content-item-lifecycle.ts +25 -26
  18. package/lib/front-mosaic.ts +1132 -0
  19. package/lib/furniture.ts +385 -0
  20. package/lib/labels.ts +37 -0
  21. package/lib/layout-config.ts +237 -0
  22. package/lib/layout-plan.ts +37 -9
  23. package/lib/layout-scenarios.ts +21 -11
  24. package/lib/layout-types.ts +457 -0
  25. package/lib/local-preview-seed.ts +97 -0
  26. package/lib/local-preview.ts +3 -0
  27. package/lib/newspaper-layout.ts +17 -3410
  28. package/lib/newsroom-board.ts +63 -62
  29. package/lib/newsroom-organizer-nav.ts +3 -4
  30. package/lib/pretext-layout.ts +22 -0
  31. package/lib/relevance-status.ts +73 -0
  32. package/lib/site-brand.ts +15 -1
  33. package/lib/text-columns.ts +251 -0
  34. package/package.json +13 -2
  35. package/renderers/pretext/presentation-shell.tsx +1 -1
  36. package/routes.manifest.json +1 -9
  37. 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 { buildReviewRateRequest, DEMO_CYCLOTRON_STATUS } from "../lib/relevance-status";
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
+ }