@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.40
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/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +45 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +212 -50
- package/infra/amplify-app-shell.ts +240 -53
- package/infra/build-specs.js +14 -5
- package/infra/build-specs.ts +17 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +167 -10
- package/infra/site-config.ts +196 -14
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +13 -0
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -1
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +45 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +19 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type PresentationRhythmHRuleProps = {
|
|
2
|
+
/** Hide in wide 2-column secondary pairs (items 1+2 side-by-side). */
|
|
3
|
+
hideInSecondaryPair?: boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function PresentationRhythmHRule({ hideInSecondaryPair = false }: PresentationRhythmHRuleProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
className="presentation-rhythm-hrule"
|
|
11
|
+
data-hide-in-secondary-pair={hideInSecondaryPair ? "true" : undefined}
|
|
12
|
+
role="presentation"
|
|
13
|
+
>
|
|
14
|
+
<div className="presentation-rhythm-hrule__line" />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { CyclotronStatusView } from "cyclotron/components/cyclotron-status";
|
|
5
|
+
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
6
|
+
|
|
7
|
+
const RATE_CHOICES = [1, 0.5, 0.25, 0.1, 0];
|
|
8
|
+
const DAY_CHOICES = [1, 7, 14, 30];
|
|
9
|
+
|
|
10
|
+
export type ReviewRateChoice = { rate: number; days: number } | { clear: true };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The relevance cyclotron's status above the References list: a strip, the
|
|
14
|
+
* detail card on demand, and, for editors who may change it, a form that asks
|
|
15
|
+
* the next sweep for a manual review rate.
|
|
16
|
+
*/
|
|
17
|
+
export function ReferenceCyclotronStatus({
|
|
18
|
+
onRequestRate,
|
|
19
|
+
status,
|
|
20
|
+
staleAfterHours = 24,
|
|
21
|
+
now = new Date(),
|
|
22
|
+
}: {
|
|
23
|
+
onRequestRate?: (choice: ReviewRateChoice) => Promise<void> | void;
|
|
24
|
+
status: CyclotronStatus;
|
|
25
|
+
staleAfterHours?: number;
|
|
26
|
+
now?: Date;
|
|
27
|
+
}) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [rate, setRate] = useState(0.5);
|
|
30
|
+
const [days, setDays] = useState(7);
|
|
31
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
32
|
+
const ageHours = Math.max(0, (now.getTime() - Date.parse(status.asOf)) / 3_600_000);
|
|
33
|
+
const stale = Number.isFinite(ageHours) && ageHours > staleAfterHours;
|
|
34
|
+
const override = status.reviewRate.override;
|
|
35
|
+
|
|
36
|
+
async function submit(choice: ReviewRateChoice) {
|
|
37
|
+
if (!onRequestRate) return;
|
|
38
|
+
setMessage("Sending…");
|
|
39
|
+
try {
|
|
40
|
+
await onRequestRate(choice);
|
|
41
|
+
setMessage("The next sweep applies it and the status will show who set it.");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<section aria-label="Relevance cyclotron" className="space-y-2" data-reference-cyclotron-status>
|
|
49
|
+
<CyclotronStatusView status={status} />
|
|
50
|
+
{stale ? (
|
|
51
|
+
<p className="text-xs text-muted-foreground" data-reference-cyclotron-status-stale>
|
|
52
|
+
<span aria-hidden="true">⚠ </span>
|
|
53
|
+
Snapshot from {Math.round(ageHours)} hours ago; the next sweep refreshes it.
|
|
54
|
+
</p>
|
|
55
|
+
) : null}
|
|
56
|
+
<button
|
|
57
|
+
aria-expanded={open}
|
|
58
|
+
className="text-xs text-muted-foreground underline underline-offset-4"
|
|
59
|
+
onClick={() => setOpen((value) => !value)}
|
|
60
|
+
type="button"
|
|
61
|
+
>
|
|
62
|
+
{open ? "Hide what the cyclotron is doing" : "What the cyclotron is doing"}
|
|
63
|
+
</button>
|
|
64
|
+
{open ? (
|
|
65
|
+
<div className="space-y-3">
|
|
66
|
+
<CyclotronStatusView classifierName="Relevant to this publication" status={status} variant="card" />
|
|
67
|
+
{onRequestRate ? (
|
|
68
|
+
<form
|
|
69
|
+
aria-label="Manual review rate"
|
|
70
|
+
className="flex flex-wrap items-end gap-3 text-sm"
|
|
71
|
+
data-reference-review-rate-form
|
|
72
|
+
onSubmit={(event) => {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
void submit({ rate, days });
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<label className="flex flex-col gap-1">
|
|
78
|
+
<span className="text-xs text-muted-foreground">Review rate for confident decisions</span>
|
|
79
|
+
<select aria-label="Manual review rate" onChange={(event) => setRate(Number(event.target.value))} value={rate}>
|
|
80
|
+
{RATE_CHOICES.map((choice) => (
|
|
81
|
+
<option key={choice} value={choice}>{Math.round(choice * 100)}%</option>
|
|
82
|
+
))}
|
|
83
|
+
</select>
|
|
84
|
+
</label>
|
|
85
|
+
<label className="flex flex-col gap-1">
|
|
86
|
+
<span className="text-xs text-muted-foreground">For</span>
|
|
87
|
+
<select aria-label="How long the manual rate lasts" onChange={(event) => setDays(Number(event.target.value))} value={days}>
|
|
88
|
+
{DAY_CHOICES.map((choice) => (
|
|
89
|
+
<option key={choice} value={choice}>{choice} day{choice === 1 ? "" : "s"}</option>
|
|
90
|
+
))}
|
|
91
|
+
</select>
|
|
92
|
+
</label>
|
|
93
|
+
<button className="cyclotron-review-secondary" type="submit">Set manual rate</button>
|
|
94
|
+
{override ? (
|
|
95
|
+
<button className="cyclotron-review-secondary" onClick={() => void submit({ clear: true })} type="button">
|
|
96
|
+
Clear manual rate
|
|
97
|
+
</button>
|
|
98
|
+
) : null}
|
|
99
|
+
<p className="basis-full text-xs text-muted-foreground">
|
|
100
|
+
Low-confidence decisions and the random audit share are always reviewed.
|
|
101
|
+
</p>
|
|
102
|
+
{message ? <p className="basis-full text-xs" role="status">{message}</p> : null}
|
|
103
|
+
</form>
|
|
104
|
+
) : null}
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
</section>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ReviewControl, type ReviewReason } from "cyclotron/components/review-control";
|
|
5
|
+
import { isScopeTrainingNegativeReason, REFERENCE_REJECTION_REASON_CODES } from "../lib/reference-policy";
|
|
6
|
+
import {
|
|
7
|
+
curationForRelevanceReview,
|
|
8
|
+
type RelevanceCuration,
|
|
9
|
+
type RelevanceDecisionView,
|
|
10
|
+
} from "../lib/relevance-decisions";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Papyrus's existing rejection reasons, unchanged. Reasons that are not about
|
|
14
|
+
* relevance (a duplicate, an unavailable source) close the review without
|
|
15
|
+
* teaching the cyclotron.
|
|
16
|
+
*/
|
|
17
|
+
export const RELEVANCE_REVIEW_REASONS: ReviewReason[] = REFERENCE_REJECTION_REASON_CODES.map((code) => ({
|
|
18
|
+
code,
|
|
19
|
+
text: code.charAt(0).toUpperCase() + code.slice(1).replaceAll("_", " "),
|
|
20
|
+
...(isScopeTrainingNegativeReason(code) ? {} : { noLabel: true }),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
export function ReferenceRelevanceReview({
|
|
24
|
+
disabled,
|
|
25
|
+
decision,
|
|
26
|
+
onCurate,
|
|
27
|
+
referenceLineageId,
|
|
28
|
+
}: {
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
decision: RelevanceDecisionView;
|
|
31
|
+
onCurate: (curation: RelevanceCuration) => void;
|
|
32
|
+
referenceLineageId: string;
|
|
33
|
+
}) {
|
|
34
|
+
const [shareable, setShareable] = useState(false);
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
const classes = decision.classes.length ? decision.classes : [decision.label];
|
|
37
|
+
return (
|
|
38
|
+
<section aria-label="Relevance review" className="space-y-2" data-reference-relevance-review={referenceLineageId}>
|
|
39
|
+
<ReviewControl
|
|
40
|
+
decision={{
|
|
41
|
+
decisionId: decision.decisionId,
|
|
42
|
+
label: decision.label,
|
|
43
|
+
confidence: decision.confidence,
|
|
44
|
+
classes,
|
|
45
|
+
version: decision.version ?? undefined,
|
|
46
|
+
}}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
itemId={referenceLineageId}
|
|
49
|
+
mode={decision.reviewControl === "thumbs" && classes.length === 2 ? "thumbs" : "labels"}
|
|
50
|
+
onReview={(review) => {
|
|
51
|
+
try {
|
|
52
|
+
setError(null);
|
|
53
|
+
onCurate(curationForRelevanceReview(decision, { ...review, shareable }));
|
|
54
|
+
} catch (failure) {
|
|
55
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
positiveLabel={decision.positiveLabel ?? undefined}
|
|
59
|
+
question={decision.question ?? undefined}
|
|
60
|
+
reasons={RELEVANCE_REVIEW_REASONS}
|
|
61
|
+
reviewReason={decision.reviewRecommended ? decision.reviewDetail ?? undefined : undefined}
|
|
62
|
+
/>
|
|
63
|
+
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
64
|
+
<input
|
|
65
|
+
checked={shareable}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
onChange={(event) => setShareable(event.target.checked)}
|
|
68
|
+
type="checkbox"
|
|
69
|
+
/>
|
|
70
|
+
My explanation may be quoted publicly (for the Cyclotron example page)
|
|
71
|
+
</label>
|
|
72
|
+
{error ? <p className="text-xs text-destructive" role="alert">{error}</p> : null}
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { toPublicReaderPath } from "../lib/reader-base-path";
|
|
3
4
|
import { signOut } from "aws-amplify/auth";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -114,7 +115,7 @@ export function SettingsPage() {
|
|
|
114
115
|
return (
|
|
115
116
|
<main className="settings-page">
|
|
116
117
|
<nav className="edition-progress edition-progress--newsroom settings-progress" aria-label="Settings navigation">
|
|
117
|
-
<Link className="edition-progress__button edition-progress__button--previous" href="/">
|
|
118
|
+
<Link className="edition-progress__button edition-progress__button--previous" href={toPublicReaderPath("/")}>
|
|
118
119
|
<svg aria-hidden="true" className="edition-progress__icon" focusable="false" viewBox="0 0 10 10">
|
|
119
120
|
<path d="M7.5 1 2.5 5 7.5 9Z" fill="currentColor" />
|
|
120
121
|
</svg>
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
type ForumThreadWithMessages,
|
|
54
54
|
type KnowledgeQueryResponse,
|
|
55
55
|
type NewsroomRecordPage,
|
|
56
|
+
loadCyclotronStatus,
|
|
57
|
+
requestCyclotronReviewRate,
|
|
56
58
|
} from "./news-desk-taxonomy-client";
|
|
57
59
|
import { listConsoleThreads } from "../lib/console-chat-client";
|
|
58
60
|
import {
|
|
@@ -76,12 +78,13 @@ import { buildNewsroomKnowledgeQueryInput, type NewsroomKnowledgeQueryAnchor as
|
|
|
76
78
|
import { NewsroomConsoleProgressToggle, PapyrusConsoleChatIcon, usePapyrusConsole } from "./papyrus-console-shell";
|
|
77
79
|
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
78
80
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
81
|
+
import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "../lib/newsroom-base-path";
|
|
79
82
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
80
83
|
import { cn } from "../lib/utils";
|
|
81
84
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
82
85
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
83
86
|
import type { ReaderAuthSnapshot } from "./reader-auth-state";
|
|
84
|
-
import {
|
|
87
|
+
import { NewsDeskAccessGate } from "./newsroom-access-gate";
|
|
85
88
|
import { NewsroomOpsOverview } from "./newsroom-ops-overview";
|
|
86
89
|
import { NewsroomReferencesView } from "./newsroom-references-view";
|
|
87
90
|
import { NewsroomAssignmentsView } from "./newsroom-assignments-view";
|
|
@@ -150,9 +153,13 @@ import {
|
|
|
150
153
|
type SemanticObjectSummary,
|
|
151
154
|
} from "../lib/semantic-graph";
|
|
152
155
|
import {
|
|
156
|
+
normalizeReferenceRejectionReasonCode,
|
|
153
157
|
REFERENCE_REJECTION_REASON_CODES,
|
|
154
158
|
referenceCurationStatusForAction,
|
|
155
159
|
} from "../lib/reference-policy";
|
|
160
|
+
import { currentRelevanceDecisions } from "../lib/relevance-decisions";
|
|
161
|
+
import { buildReviewRateRequest, DEMO_CYCLOTRON_STATUS } from "../lib/relevance-status";
|
|
162
|
+
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
156
163
|
import {
|
|
157
164
|
referenceDisplaySummary,
|
|
158
165
|
referenceMetadataField,
|
|
@@ -169,7 +176,6 @@ import {
|
|
|
169
176
|
uniqueNeighborGroupsForCategoryContext,
|
|
170
177
|
type CategoryDrilldownContext,
|
|
171
178
|
} from "../lib/newsroom-category-drilldown";
|
|
172
|
-
import type { NewsDeskShellState } from "../lib/news-desk-session";
|
|
173
179
|
import {
|
|
174
180
|
resolveNewsroomCardTemplate,
|
|
175
181
|
type NewsroomCardSpan,
|
|
@@ -776,6 +782,24 @@ function NewsDeskDashboard({
|
|
|
776
782
|
const [referenceAttachments, setReferenceAttachments] = useState(dashboard.referenceAttachments);
|
|
777
783
|
const [messages, setMessages] = useState(dashboard.messages);
|
|
778
784
|
const [semanticRelations, setSemanticRelations] = useState(dashboard.semanticRelations);
|
|
785
|
+
const relevanceDecisions = useMemo(() => currentRelevanceDecisions(semanticRelations), [semanticRelations]);
|
|
786
|
+
const relevanceCyclotronId = useMemo(
|
|
787
|
+
() => [...relevanceDecisions.values()].find((decision) => decision.cyclotronId)?.cyclotronId ?? null,
|
|
788
|
+
[relevanceDecisions],
|
|
789
|
+
);
|
|
790
|
+
const [cyclotronStatus, setCyclotronStatus] = useState<CyclotronStatus | null>(null);
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
if (!relevanceCyclotronId) return;
|
|
793
|
+
if (dashboard.isDemo) {
|
|
794
|
+
setCyclotronStatus(DEMO_CYCLOTRON_STATUS);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
let cancelled = false;
|
|
798
|
+
loadCyclotronStatus(relevanceCyclotronId)
|
|
799
|
+
.then((status) => { if (!cancelled) setCyclotronStatus(status); })
|
|
800
|
+
.catch(() => { if (!cancelled) setCyclotronStatus(null); });
|
|
801
|
+
return () => { cancelled = true; };
|
|
802
|
+
}, [dashboard.isDemo, relevanceCyclotronId]);
|
|
779
803
|
const [semanticNodes, setSemanticNodes] = useState(dashboard.semanticNodes);
|
|
780
804
|
const [assignments, setAssignments] = useState(dashboard.assignments);
|
|
781
805
|
const [assignmentEvents, setAssignmentEvents] = useState(dashboard.assignmentEvents);
|
|
@@ -1507,7 +1531,13 @@ function NewsDeskDashboard({
|
|
|
1507
1531
|
});
|
|
1508
1532
|
}
|
|
1509
1533
|
|
|
1510
|
-
function runReferenceCurationAction(
|
|
1534
|
+
function runReferenceCurationAction(
|
|
1535
|
+
reference: ReferenceRecord,
|
|
1536
|
+
action: ReferenceCurationAction,
|
|
1537
|
+
note?: string,
|
|
1538
|
+
reasonCode?: ReferenceRejectionReasonCode | null,
|
|
1539
|
+
relevance?: { decisionRelationId: string; shareable: boolean },
|
|
1540
|
+
) {
|
|
1511
1541
|
const nextStatus = referenceCurationStatusForAction(action);
|
|
1512
1542
|
setActionState({ id: reference.id, message: `${action} pending`, tone: "pending" });
|
|
1513
1543
|
if (dashboard.isDemo) {
|
|
@@ -1535,6 +1565,7 @@ function NewsDeskDashboard({
|
|
|
1535
1565
|
action,
|
|
1536
1566
|
curationStatus: nextStatus,
|
|
1537
1567
|
reasonCode: action === "reject" ? reasonCode ?? null : null,
|
|
1568
|
+
...(relevance ? { decisionRelationId: relevance.decisionRelationId, shareable: relevance.shareable } : {}),
|
|
1538
1569
|
},
|
|
1539
1570
|
}, ...current]);
|
|
1540
1571
|
setSemanticRelations((current) => [{
|
|
@@ -1575,6 +1606,8 @@ function NewsDeskDashboard({
|
|
|
1575
1606
|
actorLabel: authState.label,
|
|
1576
1607
|
note: note?.trim() || undefined,
|
|
1577
1608
|
reasonCode: action === "reject" ? reasonCode ?? undefined : undefined,
|
|
1609
|
+
decisionRelationId: relevance?.decisionRelationId,
|
|
1610
|
+
shareable: relevance ? relevance.shareable : undefined,
|
|
1578
1611
|
},
|
|
1579
1612
|
{ authMode: USER_POOL_AUTH_MODE },
|
|
1580
1613
|
);
|
|
@@ -3153,6 +3186,20 @@ function NewsDeskDashboard({
|
|
|
3153
3186
|
demo={Boolean(dashboard.isDemo)}
|
|
3154
3187
|
disabled={controlsDisabled}
|
|
3155
3188
|
initialReferenceLineageId={initialSelection.reference ?? pathnameReferenceLineageId}
|
|
3189
|
+
cyclotronStatus={cyclotronStatus}
|
|
3190
|
+
decisions={relevanceDecisions}
|
|
3191
|
+
onRequestReviewRate={relevanceCyclotronId ? async (choice) => {
|
|
3192
|
+
const request = buildReviewRateRequest(choice, authState.label);
|
|
3193
|
+
if (dashboard.isDemo) return;
|
|
3194
|
+
await requestCyclotronReviewRate(relevanceCyclotronId, request);
|
|
3195
|
+
} : undefined}
|
|
3196
|
+
onRelevanceReview={(reference, curation) => runReferenceCurationAction(
|
|
3197
|
+
reference,
|
|
3198
|
+
curation.action,
|
|
3199
|
+
curation.note ?? undefined,
|
|
3200
|
+
normalizeReferenceRejectionReasonCode(curation.reasonCode),
|
|
3201
|
+
{ decisionRelationId: curation.decisionRelationId, shareable: curation.shareable },
|
|
3202
|
+
)}
|
|
3156
3203
|
onReview={(reference, action) => runReferenceCurationAction(reference, action)}
|
|
3157
3204
|
referenceAttachments={referenceAttachments}
|
|
3158
3205
|
references={references}
|
|
@@ -15188,14 +15235,14 @@ function normalizeKnowledgeQueryAnchorKind(value: string | null | undefined): Kn
|
|
|
15188
15235
|
|
|
15189
15236
|
function normalizeNewsroomFromHref(value: string | null | undefined): string | null {
|
|
15190
15237
|
if (!value) return null;
|
|
15191
|
-
const trimmed = value.trim();
|
|
15238
|
+
const trimmed = browserPathToInternalNewsroomPath(value.trim());
|
|
15192
15239
|
if (!trimmed.startsWith("/newsroom")) return null;
|
|
15193
|
-
return trimmed;
|
|
15240
|
+
return toPublicNewsroomPath(trimmed);
|
|
15194
15241
|
}
|
|
15195
15242
|
|
|
15196
15243
|
function currentNewsroomOriginHref(fallback: string | null): string | null {
|
|
15197
15244
|
if (typeof window === "undefined") return fallback;
|
|
15198
|
-
const current = `${window.location.pathname}${window.location.search}
|
|
15245
|
+
const current = browserPathToInternalNewsroomPath(`${window.location.pathname}${window.location.search}`);
|
|
15199
15246
|
return current.startsWith("/newsroom/search") ? fallback : normalizeNewsroomFromHref(current);
|
|
15200
15247
|
}
|
|
15201
15248
|
|
|
@@ -15229,7 +15276,7 @@ function buildNewsroomSearchHref(request: NewsroomSearchRequest, demo?: boolean)
|
|
|
15229
15276
|
if (request.from) params.set("from", request.from);
|
|
15230
15277
|
if (demo) params.set("demo", "1");
|
|
15231
15278
|
const queryString = params.toString();
|
|
15232
|
-
return queryString ? `/newsroom/search?${queryString}` : "/newsroom/search";
|
|
15279
|
+
return toPublicNewsroomPath(queryString ? `/newsroom/search?${queryString}` : "/newsroom/search");
|
|
15233
15280
|
}
|
|
15234
15281
|
|
|
15235
15282
|
function focusNewsroomSearchForm() {
|
|
@@ -15399,63 +15446,6 @@ function readTextClaim(value: unknown): string | null {
|
|
|
15399
15446
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
15400
15447
|
}
|
|
15401
15448
|
|
|
15402
|
-
function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
|
|
15403
|
-
const accessPhase = shell?.phase ?? "checkingAccess";
|
|
15404
|
-
|
|
15405
|
-
return (
|
|
15406
|
-
<NewsroomOpsShell
|
|
15407
|
-
activeTab="overview"
|
|
15408
|
-
appTitle={SITE_BRAND.appTitle}
|
|
15409
|
-
backHref="/"
|
|
15410
|
-
backLabel={SITE_BRAND.backToHomeLabel}
|
|
15411
|
-
headerActions={(
|
|
15412
|
-
<Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
|
|
15413
|
-
)}
|
|
15414
|
-
pageTitle="Newsroom"
|
|
15415
|
-
showNavigation={showSectionTabs}
|
|
15416
|
-
>
|
|
15417
|
-
<section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
|
|
15418
|
-
<div className="space-y-3 rounded-xl border border-border bg-card p-6">
|
|
15419
|
-
<p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
|
|
15420
|
-
<h2 className="m-0 font-sans text-xl font-semibold tracking-tight">{formatAccessTitle(shell)}</h2>
|
|
15421
|
-
<p className="m-0 text-sm text-muted-foreground">{formatAccessDetail(shell)}</p>
|
|
15422
|
-
{shell?.error ? <p className="text-sm text-destructive">{shell.error}</p> : null}
|
|
15423
|
-
<p className="m-0 text-sm text-foreground">{formatAccessActionDetail(shell)}</p>
|
|
15424
|
-
{shell?.phase === "signedOut" || shell?.phase === "error" ? (
|
|
15425
|
-
<div className="news-desk-access-panel__auth">
|
|
15426
|
-
<ReaderAuthControl postAuthPath="/newsroom" showIdentity />
|
|
15427
|
-
</div>
|
|
15428
|
-
) : null}
|
|
15429
|
-
</div>
|
|
15430
|
-
</section>
|
|
15431
|
-
</NewsroomOpsShell>
|
|
15432
|
-
);
|
|
15433
|
-
}
|
|
15434
|
-
|
|
15435
|
-
function formatAccessTitle(state: NewsDeskShellState | null): string {
|
|
15436
|
-
if (!state || state.phase === "checkingAccess") return "Checking Desk Credentials";
|
|
15437
|
-
if (state.phase === "loadingDesk") return "Loading Newsroom Records";
|
|
15438
|
-
if (state.phase === "forbidden") return "Editor Role Required";
|
|
15439
|
-
if (state.phase === "error") return "Newsroom Unavailable";
|
|
15440
|
-
return "Editor Sign-In Required";
|
|
15441
|
-
}
|
|
15442
|
-
|
|
15443
|
-
function formatAccessDetail(state: NewsDeskShellState | null): string {
|
|
15444
|
-
if (!state || state.phase === "checkingAccess") return `${SITE_BRAND.appTitle} is checking the current browser session before loading steering state.`;
|
|
15445
|
-
if (state.phase === "loadingDesk") return `${SITE_BRAND.appTitle} verified the browser session and is loading private Newsroom records.`;
|
|
15446
|
-
if (state.phase === "forbidden") return "This account is signed in, but the Cognito session does not include the editor or admin group.";
|
|
15447
|
-
if (state.phase === "error") return `${SITE_BRAND.appTitle} could not verify this editor session or load the private Newsroom data.`;
|
|
15448
|
-
return "Sign in with an editor or admin account to inspect category, category tree, ontology, and graph steering.";
|
|
15449
|
-
}
|
|
15450
|
-
|
|
15451
|
-
function formatAccessActionDetail(state: NewsDeskShellState | null): string {
|
|
15452
|
-
if (!state || state.phase === "checkingAccess") return "This should only take a moment. If it hangs, reload the page.";
|
|
15453
|
-
if (state.phase === "loadingDesk") return "Private records are loading from GraphQL. Large reference queues may take several seconds.";
|
|
15454
|
-
if (state.phase === "forbidden") return "Ask an admin to add this account to the editor or admin group, then sign out and back in.";
|
|
15455
|
-
if (state.phase === "error") return "Reload the page or check the local development console for the GraphQL error.";
|
|
15456
|
-
return "Use the login button above to authenticate with an editor or admin account.";
|
|
15457
|
-
}
|
|
15458
|
-
|
|
15459
15449
|
function SectionHeader({ title, detail }: { title: string; detail: string }) {
|
|
15460
15450
|
const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
15461
15451
|
return (
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type RefObject } from "react";
|
|
4
|
+
import type { BlogHeaderObstacle } from "./blog-page-background";
|
|
5
|
+
|
|
6
|
+
const HEADER_OBSTACLE_SELECTORS = [
|
|
7
|
+
".presentation-header__eyebrow",
|
|
8
|
+
".presentation-header__word",
|
|
9
|
+
".presentation-header__meta",
|
|
10
|
+
".presentation-header__subtitle",
|
|
11
|
+
".presentation-header__date",
|
|
12
|
+
".presentation-header__tagline",
|
|
13
|
+
".presentation-section-nav a",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function measureObstacleRect(element: HTMLElement): DOMRect {
|
|
17
|
+
const range = document.createRange();
|
|
18
|
+
range.selectNodeContents(element);
|
|
19
|
+
const contentRect = range.getBoundingClientRect();
|
|
20
|
+
range.detach();
|
|
21
|
+
if (contentRect.width > 0 && contentRect.height > 0) return contentRect;
|
|
22
|
+
return element.getBoundingClientRect();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Measures the rendered header text (eyebrow, masthead words, meta, tagline,
|
|
27
|
+
* section links) as rectangles in the coordinates of `.blog-page-background`,
|
|
28
|
+
* so a brand's background artwork can keep clear of them. Block-level header
|
|
29
|
+
* elements span the full page width even when their text ends mid-line, so
|
|
30
|
+
* the text range is measured instead of the element box.
|
|
31
|
+
*/
|
|
32
|
+
export function useBlogHeaderObstacles(
|
|
33
|
+
pageRef: RefObject<HTMLElement | null>,
|
|
34
|
+
paintBuffer: number,
|
|
35
|
+
): BlogHeaderObstacle[] {
|
|
36
|
+
const [obstacles, setObstacles] = useState<BlogHeaderObstacle[]>([]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const page = pageRef.current;
|
|
40
|
+
if (!page) return;
|
|
41
|
+
|
|
42
|
+
const update = () => {
|
|
43
|
+
const background = page.querySelector<HTMLElement>(".blog-page-background");
|
|
44
|
+
const backgroundRect = background?.getBoundingClientRect();
|
|
45
|
+
if (!backgroundRect) return;
|
|
46
|
+
|
|
47
|
+
const next: BlogHeaderObstacle[] = [];
|
|
48
|
+
for (const selector of HEADER_OBSTACLE_SELECTORS) {
|
|
49
|
+
for (const element of Array.from(page.querySelectorAll<HTMLElement>(selector))) {
|
|
50
|
+
const rect = measureObstacleRect(element);
|
|
51
|
+
const intersectionTop = Math.max(rect.top, backgroundRect.top);
|
|
52
|
+
const intersectionBottom = Math.min(rect.bottom, backgroundRect.bottom);
|
|
53
|
+
const intersectionLeft = Math.max(rect.left, backgroundRect.left);
|
|
54
|
+
const intersectionRight = Math.min(rect.right, backgroundRect.right);
|
|
55
|
+
const width = intersectionRight - intersectionLeft;
|
|
56
|
+
const height = intersectionBottom - intersectionTop;
|
|
57
|
+
if (width <= 0 || height <= 0) continue;
|
|
58
|
+
next.push({
|
|
59
|
+
x: intersectionLeft - backgroundRect.left,
|
|
60
|
+
y: intersectionTop - backgroundRect.top,
|
|
61
|
+
width,
|
|
62
|
+
height: height + paintBuffer * 2,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
setObstacles(next);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
update();
|
|
70
|
+
const observer = new ResizeObserver(update);
|
|
71
|
+
observer.observe(page);
|
|
72
|
+
window.addEventListener("resize", update);
|
|
73
|
+
void document.fonts?.ready.then(update);
|
|
74
|
+
return () => {
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
window.removeEventListener("resize", update);
|
|
77
|
+
};
|
|
78
|
+
}, [pageRef, paintBuffer]);
|
|
79
|
+
|
|
80
|
+
return obstacles;
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { listBrandFontFamilies } from "../lib/brand-fonts";
|
|
5
|
+
import { watchBrandFontsLoaded } from "../lib/brand-fonts-watch";
|
|
6
|
+
import { clearPretextMeasurementCache } from "../lib/pretext-layout";
|
|
7
|
+
import { SITE_BRAND } from "../lib/site-brand";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns a number that increments each time the brand's declared font
|
|
11
|
+
* families finish loading. Pretext's width cache is cleared first, so
|
|
12
|
+
* measurements taken with fallback metrics are re-run with the real glyph
|
|
13
|
+
* widths. Stays 0 for brands without a font slot, and on the server.
|
|
14
|
+
*/
|
|
15
|
+
export function useBrandFontsReadyVersion(): number {
|
|
16
|
+
const [version, setVersion] = useState(0);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
return watchBrandFontsLoaded({
|
|
20
|
+
fontFaceSet: typeof document === "undefined" ? undefined : document.fonts,
|
|
21
|
+
families: listBrandFontFamilies(SITE_BRAND.fonts),
|
|
22
|
+
clearMeasurementCaches: clearPretextMeasurementCache,
|
|
23
|
+
onFontsSettled: () => setVersion((current) => current + 1),
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return version;
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export function useRhythmOverlay(): boolean {
|
|
6
|
+
const [showRhythmOverlay, setShowRhythmOverlay] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
10
|
+
if (isEditableEventTarget(event.target)) return;
|
|
11
|
+
if ((event.key === "=" || event.code === "Equal") && event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
setShowRhythmOverlay((current) => !current);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
18
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return showRhythmOverlay;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isEditableEventTarget(target: EventTarget | null): boolean {
|
|
25
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
26
|
+
const tagName = target.tagName.toLowerCase();
|
|
27
|
+
if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
|
|
28
|
+
return target.isContentEditable || target.closest("[contenteditable='true']") !== null;
|
|
29
|
+
}
|