@anthusai/papyrus 1.0.0-next.9 → 1.0.0
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 +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -60
- package/amplify/data/schema.ts +47 -60
- 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 +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- 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/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +143 -176
- package/amplify/site-backend.ts +181 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -0
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -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/priming-proof-page.tsx +92 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -6
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +148 -27
- package/infra/amplify-app-shell.ts +160 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- 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 +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +89 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- 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-prewarm.ts +53 -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/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +64 -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 +68 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- 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
package/app/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { getSiteRenderer } from "../lib/site-renderer";
|
|
|
2
2
|
import { contentRepository, getScenarioIdParam } from "../lib/content-repository";
|
|
3
3
|
import type { EditionContent } from "../lib/content-types";
|
|
4
4
|
import { createEditionSectionPlan } from "../lib/edition-sections";
|
|
5
|
+
import { getCachedLatestPublishedEdition, loadCachedEditionContent } from "../lib/cached-content-repository";
|
|
5
6
|
import { getEditionDatePath } from "../lib/edition-routes";
|
|
6
7
|
import {
|
|
7
8
|
EMPTY_EDITION_PLACEHOLDER_SLUG,
|
|
@@ -15,7 +16,10 @@ import {
|
|
|
15
16
|
} from "../lib/public-placeholder-config";
|
|
16
17
|
import type { PublicationItem } from "../lib/publication-items";
|
|
17
18
|
import { SITE_BRAND, rootRoute } from "../lib/site-brand";
|
|
18
|
-
import { redirect } from "next/navigation";
|
|
19
|
+
import { notFound, redirect } from "next/navigation";
|
|
20
|
+
import { getReaderBasePath } from "../lib/reader-base-path";
|
|
21
|
+
import { buildRootRedirectTarget } from "../lib/root-route-redirect";
|
|
22
|
+
import { NewsroomClientShell } from "../components/newsroom-client-shell";
|
|
19
23
|
import NewsDeskRootPage, { type NewsDeskRootSearchParams } from "./newsroom/page";
|
|
20
24
|
|
|
21
25
|
export const dynamic = "force-dynamic";
|
|
@@ -37,12 +41,22 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|
|
37
41
|
// co-hosts reader + CMS uses the default "reader" (render the publication
|
|
38
42
|
// home page here). A CMS-only site points the root at the newsroom or
|
|
39
43
|
// anywhere else. Preserve the scenario and OAuth-callback paths.
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const publicationOwnsRoot = getReaderBasePath() !== "";
|
|
45
|
+
|
|
46
|
+
if (!publicationOwnsRoot && rootRoute.kind === "newsroom" && !scenarioId) {
|
|
47
|
+
return (
|
|
48
|
+
<NewsroomClientShell>
|
|
49
|
+
<NewsDeskRootPage searchParams={searchParams} />
|
|
50
|
+
</NewsroomClientShell>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!publicationOwnsRoot && rootRoute.kind === "redirect") {
|
|
55
|
+
redirect(buildRootRedirectTarget(rootRoute.destination, resolvedSearchParams));
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
58
|
+
if (SITE_BRAND.renderer.kind === "markus") {
|
|
59
|
+
notFound();
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
// Resolved only on the reader path: Markus brands (renderer "markus") have
|
|
@@ -64,7 +78,7 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|
|
64
78
|
);
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
const latestEdition = await
|
|
81
|
+
const latestEdition = await getCachedLatestPublishedEdition();
|
|
68
82
|
if (latestEdition) redirect(getEditionDatePath(latestEdition.editionDate));
|
|
69
83
|
return <siteRenderer.renderEdition content={createEmptyGraphQLEdition()} />;
|
|
70
84
|
}
|
|
@@ -75,9 +89,9 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
async function loadLatestGraphQLEdition(): Promise<EditionContent | null> {
|
|
78
|
-
const latestEdition = await
|
|
92
|
+
const latestEdition = await getCachedLatestPublishedEdition();
|
|
79
93
|
if (!latestEdition) return null;
|
|
80
|
-
return
|
|
94
|
+
return loadCachedEditionContent({ editionDate: latestEdition.editionDate });
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
async function loadFirstPublishedEditionPath(): Promise<string | undefined> {
|
package/app/robots.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next";
|
|
2
|
-
import {
|
|
2
|
+
import { buildRobots } from "../lib/robots-policy";
|
|
3
3
|
|
|
4
4
|
export const dynamic = "force-dynamic";
|
|
5
5
|
|
|
6
6
|
export default function robots(): MetadataRoute.Robots {
|
|
7
|
-
|
|
8
|
-
return { rules: { userAgent: "*", allow: "/" } };
|
|
9
|
-
}
|
|
10
|
-
return { rules: { userAgent: "*", disallow: "/" } };
|
|
7
|
+
return buildRobots();
|
|
11
8
|
}
|
package/app/tailwind.css
CHANGED
|
@@ -8,11 +8,14 @@ import { CookieStorage, Hub } from "aws-amplify/utils";
|
|
|
8
8
|
import { useEffect } from "react";
|
|
9
9
|
import { assertSandboxAmplifyOutputsForDev } from "../lib/amplify-outputs-guard";
|
|
10
10
|
import amplifyOutputs from "papyrus-amplify-outputs";
|
|
11
|
+
import { getPublicNewsroomBasePath } from "../lib/newsroom-base-path";
|
|
12
|
+
import { prioritizeRedirectUrlsForOrigin } from "../lib/oauth-redirect-priority";
|
|
11
13
|
|
|
12
14
|
let configured = false;
|
|
13
15
|
|
|
14
16
|
export function configureAmplifyClient() {
|
|
15
17
|
if (configured) return;
|
|
18
|
+
if (typeof window === "undefined") return;
|
|
16
19
|
assertSandboxAmplifyOutputsForDev(amplifyOutputs as Record<string, unknown>);
|
|
17
20
|
Amplify.configure(prioritizeCurrentOrigin(amplifyOutputs as ResourcesConfig), { ssr: true });
|
|
18
21
|
applyClientTokenStorageForCurrentProtocol();
|
|
@@ -72,14 +75,10 @@ function prioritizeCurrentOrigin(config: ResourcesConfig): ResourcesConfig {
|
|
|
72
75
|
const auth = (config as { auth?: { oauth?: Record<string, unknown> } }).auth;
|
|
73
76
|
const oauth = auth?.oauth;
|
|
74
77
|
if (!auth || !oauth) return config;
|
|
75
|
-
const origin = normalizeUrlOrigin(window.location.origin);
|
|
76
78
|
const reorder = (urls: unknown) => {
|
|
77
79
|
if (!Array.isArray(urls)) return urls;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (!head.length) return normalized;
|
|
81
|
-
const tail = normalized.filter((value) => normalizeUrlOrigin(value) !== origin);
|
|
82
|
-
return [...head, ...tail];
|
|
80
|
+
const strings = urls.filter((value): value is string => typeof value === "string");
|
|
81
|
+
return prioritizeRedirectUrlsForOrigin(strings, window.location.origin, getPublicNewsroomBasePath());
|
|
83
82
|
};
|
|
84
83
|
return {
|
|
85
84
|
...config,
|
|
@@ -111,22 +110,6 @@ function redirectLoopbackToLocalhost(): void {
|
|
|
111
110
|
window.location.replace(target);
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
function normalizeUrlOrigin(value: string): string {
|
|
115
|
-
const trimmed = value.replace(/\/+$/, "");
|
|
116
|
-
try {
|
|
117
|
-
const parsed = new URL(trimmed);
|
|
118
|
-
const hostname = normalizeLoopbackHostname(parsed.hostname);
|
|
119
|
-
return `${parsed.protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
120
|
-
} catch {
|
|
121
|
-
return trimmed;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizeLoopbackHostname(hostname: string): string {
|
|
126
|
-
if (hostname === "127.0.0.1" || hostname === "::1") return "localhost";
|
|
127
|
-
return hostname;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
113
|
function extractAuthFailureDetail(error: unknown): string | null {
|
|
131
114
|
if (!error) return null;
|
|
132
115
|
if (typeof error === "string") return error;
|
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useRhythmOverlay } from "./use-rhythm-overlay";
|
|
5
5
|
|
|
6
6
|
type ArchiveShellProps = {
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export function ArchiveShell({ children }: ArchiveShellProps) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
15
|
-
if (isEditableEventTarget(event.target)) return;
|
|
16
|
-
if ((event.key === "=" || event.code === "Equal") && event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
17
|
-
event.preventDefault();
|
|
18
|
-
setShowRhythmOverlay((current) => !current);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
23
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
24
|
-
}, []);
|
|
11
|
+
const showRhythmOverlay = useRhythmOverlay();
|
|
25
12
|
|
|
26
13
|
return (
|
|
27
14
|
<main
|
|
@@ -32,10 +19,3 @@ export function ArchiveShell({ children }: ArchiveShellProps) {
|
|
|
32
19
|
</main>
|
|
33
20
|
);
|
|
34
21
|
}
|
|
35
|
-
|
|
36
|
-
function isEditableEventTarget(target: EventTarget | null): boolean {
|
|
37
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
38
|
-
const tagName = target.tagName.toLowerCase();
|
|
39
|
-
if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
|
|
40
|
-
return target.isContentEditable || target.closest("[contenteditable='true']") !== null;
|
|
41
|
-
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
|
|
4
|
+
import { Pause, Play } from "lucide-react";
|
|
4
5
|
import type { ArticleVideoAsset } from "../lib/articles";
|
|
5
6
|
import { resolveThemedVideoSrc } from "../lib/themed-image";
|
|
6
7
|
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
7
8
|
import { getSiteBrand } from "../lib/site-brand";
|
|
8
9
|
import { normalizeDevPreviewDsl, type VideoScriptRef } from "../lib/video-script";
|
|
10
|
+
import { useNearViewport } from "./use-near-viewport";
|
|
11
|
+
import { planVideoLoading, videoElementKey } from "../lib/video-loading";
|
|
9
12
|
import { resolveVideoMode, resolveVideoModeFromEnv, type VideoMode } from "../lib/video-mode";
|
|
10
13
|
|
|
11
14
|
type ArticleVideoFigureProps = {
|
|
@@ -17,6 +20,7 @@ type ArticleVideoFigureProps = {
|
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
const PREVIEW_STORAGE_PREFIX = "papyrus-video-preview:";
|
|
23
|
+
const VIDEO_LOAD_ROOT_MARGIN = "200px 0px";
|
|
20
24
|
|
|
21
25
|
export function ArticleVideoFigure({
|
|
22
26
|
video,
|
|
@@ -32,6 +36,10 @@ export function ArticleVideoFigure({
|
|
|
32
36
|
const [duration, setDuration] = useState(0);
|
|
33
37
|
const [previewReady, setPreviewReady] = useState(false);
|
|
34
38
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
39
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
40
|
+
const figureRef = useRef<HTMLElement>(null);
|
|
41
|
+
const isNearViewport = useNearViewport(figureRef, VIDEO_LOAD_ROOT_MARGIN);
|
|
42
|
+
const isFramed = getSiteBrand().videoPlayer === "framed";
|
|
35
43
|
|
|
36
44
|
useEffect(() => {
|
|
37
45
|
setHasHydrated(true);
|
|
@@ -41,6 +49,12 @@ export function ArticleVideoFigure({
|
|
|
41
49
|
// Match SSR to the default dark MP4 in `video.src` until after hydration.
|
|
42
50
|
const resolvedTheme = hasHydrated ? theme : "dark";
|
|
43
51
|
const src = resolveThemedVideoSrc(video.src, video.themeVariants, resolvedTheme);
|
|
52
|
+
const loadingPlan = planVideoLoading({
|
|
53
|
+
hasHydrated,
|
|
54
|
+
isNearViewport,
|
|
55
|
+
hasPoster: Boolean(video.posterSrc),
|
|
56
|
+
});
|
|
57
|
+
const videoKey = videoElementKey(loadingPlan.attachSource, src);
|
|
44
58
|
const usePreview = videoMode === "preview" && Boolean(videoScript?.dsl);
|
|
45
59
|
const previewDsl = videoScript?.dsl
|
|
46
60
|
? normalizeDevPreviewDsl(videoScript.dsl, getSiteBrand().video?.sceneComponents)
|
|
@@ -91,6 +105,58 @@ export function ArticleVideoFigure({
|
|
|
91
105
|
setCurrentTime(clamped);
|
|
92
106
|
}, []);
|
|
93
107
|
|
|
108
|
+
const toggleNativePlayback = useCallback(() => {
|
|
109
|
+
const element = videoRef.current;
|
|
110
|
+
if (!element) return;
|
|
111
|
+
if (element.paused) void element.play().catch(() => setIsPlaying(false));
|
|
112
|
+
else element.pause();
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const seekNative = useCallback((nextTime: number) => {
|
|
116
|
+
const element = videoRef.current;
|
|
117
|
+
if (!element) return;
|
|
118
|
+
element.currentTime = Math.max(0, Number.isFinite(nextTime) ? nextTime : 0);
|
|
119
|
+
setCurrentTime(element.currentTime);
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const framedControls = (
|
|
123
|
+
<FramedVideoControls
|
|
124
|
+
currentTime={currentTime}
|
|
125
|
+
duration={duration}
|
|
126
|
+
isPlaying={isPlaying}
|
|
127
|
+
onSeek={usePreview ? seekPreview : seekNative}
|
|
128
|
+
onToggle={usePreview ? togglePreviewPlayback : toggleNativePlayback}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (isFramed && usePreview) {
|
|
133
|
+
return (
|
|
134
|
+
<figure
|
|
135
|
+
className={`${figureClassName} article-video--framed article-video--preview`}
|
|
136
|
+
data-media-type="videoml-preview"
|
|
137
|
+
data-video-theme={resolvedTheme}
|
|
138
|
+
data-video-mode={videoMode}
|
|
139
|
+
>
|
|
140
|
+
<div className="article-video__media">
|
|
141
|
+
{hasHydrated ? (
|
|
142
|
+
<iframe
|
|
143
|
+
key={`${slug}-${resolvedTheme}-${src}`}
|
|
144
|
+
ref={iframeRef}
|
|
145
|
+
className="article-video__preview-frame"
|
|
146
|
+
src={previewSrc}
|
|
147
|
+
title={video.alt}
|
|
148
|
+
onLoad={() => setPreviewReady(true)}
|
|
149
|
+
/>
|
|
150
|
+
) : null}
|
|
151
|
+
</div>
|
|
152
|
+
{framedControls}
|
|
153
|
+
<span className="sr-only" data-video-slug={slug}>
|
|
154
|
+
{video.alt}
|
|
155
|
+
</span>
|
|
156
|
+
</figure>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
94
160
|
if (usePreview) {
|
|
95
161
|
return (
|
|
96
162
|
<figure
|
|
@@ -133,18 +199,56 @@ export function ArticleVideoFigure({
|
|
|
133
199
|
);
|
|
134
200
|
}
|
|
135
201
|
|
|
202
|
+
if (isFramed) {
|
|
203
|
+
return (
|
|
204
|
+
<figure
|
|
205
|
+
ref={figureRef}
|
|
206
|
+
className={`${figureClassName} article-video--framed`}
|
|
207
|
+
data-media-type="video"
|
|
208
|
+
data-video-theme={resolvedTheme}
|
|
209
|
+
data-video-mode={videoMode}
|
|
210
|
+
>
|
|
211
|
+
<div className="article-video__media">
|
|
212
|
+
<video
|
|
213
|
+
ref={videoRef}
|
|
214
|
+
playsInline
|
|
215
|
+
preload={loadingPlan.preload}
|
|
216
|
+
poster={video.posterSrc}
|
|
217
|
+
aria-label={video.alt}
|
|
218
|
+
className="article-video__player"
|
|
219
|
+
key={videoKey}
|
|
220
|
+
onPlay={() => setIsPlaying(true)}
|
|
221
|
+
onPause={() => setIsPlaying(false)}
|
|
222
|
+
onEnded={() => setIsPlaying(false)}
|
|
223
|
+
onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)}
|
|
224
|
+
onDurationChange={(event) => {
|
|
225
|
+
const nextDuration = event.currentTarget.duration;
|
|
226
|
+
if (Number.isFinite(nextDuration) && nextDuration > 0) setDuration(nextDuration);
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
{loadingPlan.attachSource ? <source src={src} type="video/mp4" /> : null}
|
|
230
|
+
</video>
|
|
231
|
+
</div>
|
|
232
|
+
{framedControls}
|
|
233
|
+
<span className="sr-only" data-video-slug={slug}>
|
|
234
|
+
{video.alt}
|
|
235
|
+
</span>
|
|
236
|
+
</figure>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
136
240
|
return (
|
|
137
|
-
<figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
|
|
241
|
+
<figure ref={figureRef} className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
|
|
138
242
|
<video
|
|
139
243
|
controls
|
|
140
244
|
playsInline
|
|
141
|
-
preload=
|
|
245
|
+
preload={loadingPlan.preload}
|
|
142
246
|
poster={video.posterSrc}
|
|
143
247
|
aria-label={video.alt}
|
|
144
248
|
className="article-video__player"
|
|
145
|
-
key={
|
|
249
|
+
key={videoKey}
|
|
146
250
|
>
|
|
147
|
-
<source src={src} type="video/mp4" />
|
|
251
|
+
{loadingPlan.attachSource ? <source src={src} type="video/mp4" /> : null}
|
|
148
252
|
</video>
|
|
149
253
|
{video.caption ? <figcaption>{video.caption}</figcaption> : null}
|
|
150
254
|
{video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
|
|
@@ -154,3 +258,46 @@ export function ArticleVideoFigure({
|
|
|
154
258
|
</figure>
|
|
155
259
|
);
|
|
156
260
|
}
|
|
261
|
+
|
|
262
|
+
function FramedVideoControls({
|
|
263
|
+
currentTime,
|
|
264
|
+
duration,
|
|
265
|
+
isPlaying,
|
|
266
|
+
onSeek,
|
|
267
|
+
onToggle,
|
|
268
|
+
}: {
|
|
269
|
+
currentTime: number;
|
|
270
|
+
duration: number;
|
|
271
|
+
isPlaying: boolean;
|
|
272
|
+
onSeek: (nextTime: number) => void;
|
|
273
|
+
onToggle: () => void;
|
|
274
|
+
}) {
|
|
275
|
+
const seekProgress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
276
|
+
return (
|
|
277
|
+
<div className="article-video__cta" data-playing={isPlaying ? "true" : "false"}>
|
|
278
|
+
<button type="button" className="article-video__cta-toggle" onClick={onToggle} aria-pressed={isPlaying}>
|
|
279
|
+
<span className="article-video__cta-label">
|
|
280
|
+
<span className="article-video__cta-label-sizer" aria-hidden="true">
|
|
281
|
+
Pause Video
|
|
282
|
+
</span>
|
|
283
|
+
<span className="article-video__cta-label-text">{isPlaying ? "Pause Video" : "Play Video"}</span>
|
|
284
|
+
</span>
|
|
285
|
+
<span className="article-video__cta-icon" aria-hidden="true">
|
|
286
|
+
{isPlaying ? <Pause /> : <Play />}
|
|
287
|
+
</span>
|
|
288
|
+
</button>
|
|
289
|
+
<input
|
|
290
|
+
className="article-video__cta-seek"
|
|
291
|
+
type="range"
|
|
292
|
+
min={0}
|
|
293
|
+
max={duration || 0}
|
|
294
|
+
step={0.1}
|
|
295
|
+
value={Math.min(currentTime, duration || 0)}
|
|
296
|
+
onChange={(event) => onSeek(Number(event.target.value))}
|
|
297
|
+
aria-label="Seek video"
|
|
298
|
+
disabled={!duration}
|
|
299
|
+
style={{ "--seek-progress": `${seekProgress}%` } as CSSProperties}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
@@ -2,18 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
import type { RefObject } from "react";
|
|
4
4
|
|
|
5
|
+
/** A rectangle, in the background layer's own coordinates, that header text occupies. */
|
|
6
|
+
export type BlogHeaderObstacle = {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Vertical rhythm grid the rhythm blog layout is built on. */
|
|
14
|
+
export type BlogBackgroundRhythm = {
|
|
15
|
+
rowHeight: number;
|
|
16
|
+
paintBuffer: number;
|
|
17
|
+
paintHeight: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
5
20
|
export type BlogPageBackgroundProps = {
|
|
6
21
|
pageRef: RefObject<HTMLElement | null>;
|
|
22
|
+
/** Rhythm blog layout only: header text boxes the artwork should keep clear of. */
|
|
23
|
+
headerObstacles?: BlogHeaderObstacle[];
|
|
24
|
+
/** Rhythm blog layout only: the rhythm grid, so artwork can align to it. */
|
|
25
|
+
rhythm?: BlogBackgroundRhythm;
|
|
7
26
|
};
|
|
8
27
|
|
|
9
28
|
/**
|
|
10
29
|
* Default (no-op) blog page background for publications without a bespoke
|
|
11
|
-
* backdrop.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `renderers/pretext` falls back to
|
|
15
|
-
* Intelligence, so the blog layout loads cleanly without depending on TI's
|
|
16
|
-
* package.
|
|
30
|
+
* backdrop. A publication supplies header artwork through the
|
|
31
|
+
* `components.BlogPageBackground` brand slot; with the rhythm blog layout the
|
|
32
|
+
* slot also receives `headerObstacles` and `rhythm`. This is what
|
|
33
|
+
* `renderers/pretext` falls back to when no slot component is registered.
|
|
17
34
|
*/
|
|
18
35
|
export function BlogPageBackground(_props: BlogPageBackgroundProps) {
|
|
19
36
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveNewsDeskTab } from "../lib/newsroom-section-routing";
|
|
1
2
|
import { NewsDeskWorkspace, type NewsDeskTab } from "./topic-steering-workspace";
|
|
2
3
|
import {
|
|
3
4
|
createDemoCategorySteeringDashboard,
|
|
@@ -41,7 +42,8 @@ export type NewsDeskPageProps = {
|
|
|
41
42
|
|
|
42
43
|
export async function NewsDeskPage({ section: routeSection, sectionPageId, selectionPath, searchParams }: NewsDeskPageProps) {
|
|
43
44
|
const resolvedSearchParams = await searchParams;
|
|
44
|
-
const
|
|
45
|
+
const brand = resolveActiveSiteBrand();
|
|
46
|
+
const initialTab = resolveNewsDeskTab(routeSection ?? getSearchParam(resolvedSearchParams, "section"), getSearchParam(resolvedSearchParams, "tab"), brand.newsroomLayout === "organizer");
|
|
45
47
|
const routeSelection = parseRouteSelection(initialTab, selectionPath);
|
|
46
48
|
const initialSelection = {
|
|
47
49
|
assignment: routeSelection.assignment ?? getFirstSearchParam(resolvedSearchParams, "assignment"),
|
|
@@ -63,7 +65,6 @@ export async function NewsDeskPage({ section: routeSection, sectionPageId, selec
|
|
|
63
65
|
proposal: getFirstSearchParam(resolvedSearchParams, "proposal"),
|
|
64
66
|
};
|
|
65
67
|
const useDemoDashboard = getFirstSearchParam(resolvedSearchParams, "demo") === "1";
|
|
66
|
-
const brand = resolveActiveSiteBrand();
|
|
67
68
|
const analysisProfiles = await loadAnalysisProfileSummaries(getBrandAnalysisProfilesPath(brand));
|
|
68
69
|
const configuredCorpora = await loadConfiguredCorpusSummaries(getBrandSteeringConfigPath(brand));
|
|
69
70
|
const dashboard = useDemoDashboard
|
|
@@ -77,6 +78,7 @@ function parseRouteSelection(tab: NewsDeskTab, selectionPath: string[] | null |
|
|
|
77
78
|
if (!segments.length) return {};
|
|
78
79
|
if (segments[0] === "forum" && segments[1]) return { forumThread: segments[1] };
|
|
79
80
|
if (tab === "administration") return { panel: segments[0] ?? null };
|
|
81
|
+
if (tab === "desks") return { category: segments[0] ?? null };
|
|
80
82
|
if (tab === "topics") return { category: segments[1] ?? segments[0] ?? null };
|
|
81
83
|
if (tab === "references") return { reference: segments[0] ?? null };
|
|
82
84
|
if (tab === "concepts") return { category: segments[0] ?? null };
|
|
@@ -101,16 +103,3 @@ function getFirstSearchParam(searchParams: unknown, key: string): string | null
|
|
|
101
103
|
const first = Array.isArray(value) ? value[0] : value;
|
|
102
104
|
return typeof first === "string" && first.trim() ? first.trim() : null;
|
|
103
105
|
}
|
|
104
|
-
|
|
105
|
-
function parseNewsDeskTab(sectionValue: string | string[] | null | undefined, legacyTabValue: string | string[] | null | undefined): NewsDeskTab {
|
|
106
|
-
const section = Array.isArray(sectionValue) ? sectionValue[0] : sectionValue;
|
|
107
|
-
const legacyTab = Array.isArray(legacyTabValue) ? legacyTabValue[0] : legacyTabValue;
|
|
108
|
-
const rawValue = section ?? (legacyTab === "categories" ? "topics" : legacyTab);
|
|
109
|
-
const value = rawValue === "users" || rawValue === "doctrine"
|
|
110
|
-
? "administration"
|
|
111
|
-
: rawValue === "desks"
|
|
112
|
-
? "topics"
|
|
113
|
-
: rawValue;
|
|
114
|
-
if (value === "administration" || value === "topics" || value === "concepts" || value === "references" || value === "insights" || value === "messages" || value === "assignments" || value === "search") return value;
|
|
115
|
-
return "overview";
|
|
116
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import { testEditorOverrideAllowed } from "../lib/test-editor-override";
|
|
4
5
|
import { downloadData, getUrl } from "aws-amplify/storage";
|
|
5
6
|
import type { Schema } from "../amplify/data/resource";
|
|
6
7
|
import type { NewsDeskAppendix, NewsDeskCategoryTreeNode } from "../lib/content-types";
|
|
@@ -38,6 +39,7 @@ import type {
|
|
|
38
39
|
UserDirectoryEntry,
|
|
39
40
|
} from "../lib/category-repository";
|
|
40
41
|
import { relationTypeKey } from "../lib/semantic-graph";
|
|
42
|
+
import { CONTENT_ITEM_STAGES, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
41
43
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
42
44
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
43
45
|
import { isUnauthenticatedError, loadReaderSessionSnapshot, type ReaderAuthSnapshot } from "./reader-auth-state";
|
|
@@ -252,7 +254,7 @@ const UPDATE_MESSAGE_MUTATION = `
|
|
|
252
254
|
const NEWSROOM_ASSIGNMENT_FEED_QUERY = `
|
|
253
255
|
query ListAssignmentsByNewsroomFeedAndCreatedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelAssignmentFilterInput) {
|
|
254
256
|
listAssignmentsByNewsroomFeedAndCreatedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
255
|
-
items { id assignmentTypeKey queueKey queueStatusKey status priority title summary assigneeType assigneeId assigneeKey claimedAt claimExpiresAt completedAt canceledAt corpusId categorySetId classifierId sectionId sectionKey sectionType sectionStatusKey sectionQueueStatusKey primaryFocusCategoryKey topicScopeCategoryKeys sourceSnapshotId importRunId createdBy createdAt updatedAt newsroomFeedKey }
|
|
257
|
+
items { id assignmentTypeKey queueKey queueStatusKey status priority title summary assigneeType assigneeId assigneeKey claimedAt claimExpiresAt completedAt canceledAt corpusId categorySetId classifierId sectionId sectionKey sectionType sectionStatusKey sectionQueueStatusKey primaryFocusCategoryKey topicScopeCategoryKeys sourceSnapshotId importRunId createdBy createdAt updatedAt newsroomFeedKey itemLineageId }
|
|
256
258
|
nextToken
|
|
257
259
|
}
|
|
258
260
|
}
|
|
@@ -2156,6 +2158,41 @@ export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPage
|
|
|
2156
2158
|
});
|
|
2157
2159
|
}
|
|
2158
2160
|
|
|
2161
|
+
const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
2162
|
+
query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
2163
|
+
listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
2164
|
+
items { id lineageId versionNumber stage type status title headline slug section updatedAt }
|
|
2165
|
+
nextToken
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
`;
|
|
2169
|
+
|
|
2170
|
+
export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2171
|
+
configureAmplifyClient();
|
|
2172
|
+
const client = generateClient<Schema>() as unknown as {
|
|
2173
|
+
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
|
|
2174
|
+
};
|
|
2175
|
+
const field = "listItemsByStageAndUpdatedAt";
|
|
2176
|
+
const versionsPerStage = await Promise.all(CONTENT_ITEM_STAGES.map(async (stage) => {
|
|
2177
|
+
const versions: ContentItemVersionRecord[] = [];
|
|
2178
|
+
let nextToken: string | null = null;
|
|
2179
|
+
do {
|
|
2180
|
+
const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
|
|
2181
|
+
query: CONTENT_ITEMS_BY_STAGE_QUERY,
|
|
2182
|
+
variables: { stage, sortDirection: "DESC", limit: NEWSROOM_PAGE_LIMIT, nextToken },
|
|
2183
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
2184
|
+
});
|
|
2185
|
+
assertNoGraphQLErrors(response.errors);
|
|
2186
|
+
const connection = response.data?.[field];
|
|
2187
|
+
if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
|
|
2188
|
+
versions.push(...((connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[]));
|
|
2189
|
+
nextToken = connection.nextToken ?? null;
|
|
2190
|
+
} while (nextToken);
|
|
2191
|
+
return versions;
|
|
2192
|
+
}));
|
|
2193
|
+
return versionsPerStage.flat();
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2159
2196
|
export async function loadModelPayloadsForOwner(
|
|
2160
2197
|
ownerKind: string,
|
|
2161
2198
|
ownerId: string,
|
|
@@ -2569,8 +2606,7 @@ export async function runNewsroomKnowledgeQuery(input: Record<string, unknown>):
|
|
|
2569
2606
|
|
|
2570
2607
|
export function hasTestEditorOverride(): boolean {
|
|
2571
2608
|
if (typeof window === "undefined") return false;
|
|
2572
|
-
const allowOverride = process.env.NODE_ENV
|
|
2573
|
-
|| process.env.NEXT_PUBLIC_ENABLE_TEST_EDITOR_OVERRIDE === "true";
|
|
2609
|
+
const allowOverride = testEditorOverrideAllowed(process.env.NODE_ENV, process.env.NEXT_PUBLIC_ENABLE_TEST_EDITOR_OVERRIDE);
|
|
2574
2610
|
if (!allowOverride) return false;
|
|
2575
2611
|
return window.localStorage.getItem(TEST_EDITOR_STORAGE_KEY) === "true";
|
|
2576
2612
|
}
|
|
@@ -3682,3 +3718,54 @@ function normalizeUnknownErrorMessage(error: unknown, fallback: string): string
|
|
|
3682
3718
|
return fallback;
|
|
3683
3719
|
}
|
|
3684
3720
|
}
|
|
3721
|
+
|
|
3722
|
+
const CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION = `
|
|
3723
|
+
mutation CreateKnowledgeRawPayload($input: CreateKnowledgeRawPayloadInput!) {
|
|
3724
|
+
createKnowledgeRawPayload(input: $input) { id }
|
|
3725
|
+
}
|
|
3726
|
+
`;
|
|
3727
|
+
|
|
3728
|
+
const GET_KNOWLEDGE_RAW_PAYLOAD_QUERY = `
|
|
3729
|
+
query GetKnowledgeRawPayload($id: ID!) {
|
|
3730
|
+
getKnowledgeRawPayload(id: $id) { id }
|
|
3731
|
+
}
|
|
3732
|
+
`;
|
|
3733
|
+
|
|
3734
|
+
/** The relevance cyclotron's latest cyclotron-status/v1 snapshot, as the sweep recorded it. */
|
|
3735
|
+
export async function loadCyclotronStatus(cyclotronId: string): Promise<import("cyclotron/cyclotron-status").CyclotronStatus | null> {
|
|
3736
|
+
const { cyclotronStatusPayloadId, latestCyclotronStatus } = await import("../lib/relevance-status");
|
|
3737
|
+
const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", cyclotronStatusPayloadId(cyclotronId), ["raw_payload"]);
|
|
3738
|
+
return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
/** Ask the next decide-relevance sweep to set or clear a manual review rate. */
|
|
3742
|
+
export async function requestCyclotronReviewRate(
|
|
3743
|
+
cyclotronId: string,
|
|
3744
|
+
request: import("../lib/relevance-status").ReviewRateRequest,
|
|
3745
|
+
): Promise<void> {
|
|
3746
|
+
const { reviewRateRequestPayloadId, safeId } = await import("../lib/relevance-status");
|
|
3747
|
+
const id = reviewRateRequestPayloadId(cyclotronId);
|
|
3748
|
+
const existing = await runGraphql<{ getKnowledgeRawPayload?: { id: string } | null }>(GET_KNOWLEDGE_RAW_PAYLOAD_QUERY, { id });
|
|
3749
|
+
if (!existing.getKnowledgeRawPayload) {
|
|
3750
|
+
await runGraphql(CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION, {
|
|
3751
|
+
input: {
|
|
3752
|
+
id,
|
|
3753
|
+
ownerType: "cyclotron",
|
|
3754
|
+
ownerId: safeId(cyclotronId),
|
|
3755
|
+
payloadKind: "cyclotron-review-rate-request",
|
|
3756
|
+
createdAt: request.requestedAt,
|
|
3757
|
+
updatedAt: request.requestedAt,
|
|
3758
|
+
},
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
await uploadModelPayloadForOwner({
|
|
3762
|
+
ownerKind: "knowledgeRawPayload",
|
|
3763
|
+
ownerId: id,
|
|
3764
|
+
ownerLineageId: id,
|
|
3765
|
+
role: "raw_payload",
|
|
3766
|
+
sortKey: `review-rate-request-${request.requestedAt}`,
|
|
3767
|
+
filename: "review-rate-request.json",
|
|
3768
|
+
mediaType: "application/json",
|
|
3769
|
+
content: JSON.stringify(request),
|
|
3770
|
+
});
|
|
3771
|
+
}
|
package/components/newspaper.tsx
CHANGED
|
@@ -12,6 +12,8 @@ import { ARCHIVE_PREVIEW_HEIGHT, ARCHIVE_PREVIEW_WIDTH } from "../lib/archive-ty
|
|
|
12
12
|
import type { EditionContent, NewsDeskAppendix, NewsDeskCategoryTreeNode } from "../lib/content-types";
|
|
13
13
|
import { shouldBypassImageOptimization } from "../lib/image-url";
|
|
14
14
|
import type { PublicationItem } from "../lib/publication-items";
|
|
15
|
+
import { clearPretextMeasurementCache } from "../lib/pretext-layout";
|
|
16
|
+
import { toPublicReaderPath } from "../lib/reader-base-path";
|
|
15
17
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
16
18
|
import { resolveThemedImageSrc, type ResolvedTheme } from "../lib/themed-image";
|
|
17
19
|
import { loadEditorCategoryTreeState } from "./news-desk-taxonomy-client";
|
|
@@ -130,7 +132,9 @@ export function Newspaper({
|
|
|
130
132
|
if (!fonts) return;
|
|
131
133
|
let cancelled = false;
|
|
132
134
|
const refreshAfterFontLoad = () => {
|
|
133
|
-
if (
|
|
135
|
+
if (cancelled) return;
|
|
136
|
+
clearPretextMeasurementCache();
|
|
137
|
+
setFontRevision((revision) => revision + 1);
|
|
134
138
|
};
|
|
135
139
|
fonts.ready.then(refreshAfterFontLoad).catch(() => undefined);
|
|
136
140
|
fonts.addEventListener("loadingdone", refreshAfterFontLoad);
|
|
@@ -1877,7 +1881,7 @@ function getPageHref(pageNumber: number, editionBasePath?: string): string {
|
|
|
1877
1881
|
}
|
|
1878
1882
|
|
|
1879
1883
|
function getArticleHref(articleSlug: string, editionBasePath?: string): string {
|
|
1880
|
-
return editionBasePath ? `${normalizePath(editionBasePath)}/${encodeURIComponent(articleSlug)}` : `/articles/${articleSlug}
|
|
1884
|
+
return editionBasePath ? `${normalizePath(editionBasePath)}/${encodeURIComponent(articleSlug)}` : toPublicReaderPath(`/articles/${articleSlug}`);
|
|
1881
1885
|
}
|
|
1882
1886
|
|
|
1883
1887
|
function getItemAnchorHref(articleSlug: string, pageNumber: number, editionBasePath?: string): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { newsroomHref } from "../lib/newsroom-base-path";
|
|
3
4
|
import Link from "next/link";
|
|
4
5
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
5
6
|
import type { NewsDeskShellState } from "../lib/news-desk-session";
|
|
@@ -32,7 +33,7 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
|
|
|
32
33
|
<p className="m-0 text-sm text-foreground">{formatAccessActionDetail(shell)}</p>
|
|
33
34
|
{shell?.phase === "signedOut" || shell?.phase === "error" ? (
|
|
34
35
|
<div className="news-desk-access-panel__auth">
|
|
35
|
-
<ReaderAuthControl postAuthPath=
|
|
36
|
+
<ReaderAuthControl postAuthPath={newsroomHref()} showIdentity />
|
|
36
37
|
</div>
|
|
37
38
|
) : null}
|
|
38
39
|
</div>
|