@anthusai/papyrus 1.3.0 → 1.4.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/app/newsroom/[section]/page.tsx +6 -0
- package/app/newsroom/articles/page.tsx +3 -0
- package/components/news-desk-client-provider.tsx +17 -2
- package/components/news-desk-taxonomy-client.ts +74 -0
- package/components/newspaper.tsx +4 -4
- package/components/newsroom-board-view.tsx +59 -26
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-organizer-shell.tsx +12 -7
- package/components/newsroom-organizer.css +2 -1
- package/components/newsroom-references-view.tsx +18 -0
- package/components/reference-cyclotron-priming.tsx +85 -0
- package/components/topic-steering-workspace.tsx +35 -1
- package/lib/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/chrome-text.ts +504 -0
- package/lib/front-mosaic.ts +1132 -0
- package/lib/furniture.ts +385 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +37 -9
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/local-preview-seed.ts +97 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-board.ts +52 -45
- package/lib/newsroom-organizer-nav.ts +3 -4
- package/lib/pretext-layout.ts +22 -0
- package/lib/relevance-status.ts +73 -0
- package/lib/site-brand.ts +8 -1
- package/lib/text-columns.ts +251 -0
- package/package.json +13 -2
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/routes.manifest.json +1 -9
- package/app/newsroom/work/page.tsx +0 -10
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
|
|
2
2
|
import { notFound, redirect } from "next/navigation";
|
|
3
3
|
import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
|
|
4
|
+
import { NewsroomBoardView } from "../../../components/newsroom-board-view";
|
|
4
5
|
import { NewsDeskPage } from "../../../components/news-desk-page";
|
|
5
6
|
import type { NewsDeskTab } from "../../../components/topic-steering-workspace";
|
|
6
7
|
|
|
@@ -42,9 +43,14 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
|
|
|
42
43
|
|
|
43
44
|
export default async function NewsDeskSectionPage({ params, searchParams }: NewsDeskSectionPageProps) {
|
|
44
45
|
const { section } = await params;
|
|
46
|
+
if (section === "work") redirect(toPublicNewsroomPath("/newsroom/articles"));
|
|
45
47
|
if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
|
|
46
48
|
if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
|
|
47
49
|
if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") redirect(toPublicNewsroomPath("/newsroom/topics"));
|
|
48
50
|
if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
|
|
51
|
+
if (section === "assignments" && resolveActiveSiteBrand().newsroomLayout === "organizer") {
|
|
52
|
+
const resolved = await searchParams;
|
|
53
|
+
if (!resolved?.assignment && resolved?.demo !== "1") return <NewsroomBoardView subject="assignment" />;
|
|
54
|
+
}
|
|
49
55
|
return <NewsDeskPage section={section} searchParams={searchParams} />;
|
|
50
56
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { NewsroomBoardView } from "../../../components/newsroom-board-view";
|
|
2
|
+
import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
|
|
1
3
|
import { NewsroomArticlesView } from "../../../components/newsroom-articles-view";
|
|
2
4
|
|
|
3
5
|
export const dynamic = "force-dynamic";
|
|
@@ -9,5 +11,6 @@ type NewsroomArticlesPageProps = {
|
|
|
9
11
|
export default async function NewsroomArticlesPage({ searchParams }: NewsroomArticlesPageProps) {
|
|
10
12
|
const resolved = await searchParams;
|
|
11
13
|
const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
|
|
14
|
+
if (resolveActiveSiteBrand().newsroomLayout === "organizer" && demoValue !== "1") return <NewsroomBoardView subject="contentItem" />;
|
|
12
15
|
return <NewsroomArticlesView demo={demoValue === "1"} />;
|
|
13
16
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Hub } from "aws-amplify/utils";
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
6
|
+
import type { CategorySteeringDashboard } from "../lib/category-repository";
|
|
6
7
|
import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
|
|
7
8
|
import {
|
|
8
9
|
loadEditorAssignmentsData,
|
|
@@ -46,7 +47,12 @@ const NewsDeskClientContext = createContext<NewsDeskClientContextValue | null>(n
|
|
|
46
47
|
export function NewsDeskClientProvider({
|
|
47
48
|
children,
|
|
48
49
|
devSandboxEditorAuth = null,
|
|
49
|
-
|
|
50
|
+
localPreviewDashboard = null,
|
|
51
|
+
}: Readonly<{
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
devSandboxEditorAuth?: DevSandboxEditorAuth | null;
|
|
54
|
+
localPreviewDashboard?: CategorySteeringDashboard | null;
|
|
55
|
+
}>) {
|
|
50
56
|
const [shell, setShell] = useState<NewsDeskShellState>(() => createInitialNewsDeskShellState());
|
|
51
57
|
const [searchTransition, setSearchTransition] = useState<NewsDeskSearchTransition | null>(null);
|
|
52
58
|
const shellRef = useRef(shell);
|
|
@@ -58,6 +64,15 @@ export function NewsDeskClientProvider({
|
|
|
58
64
|
|
|
59
65
|
const refreshDashboard = useCallback(async () => {
|
|
60
66
|
const sequence = ++bootstrapSequenceRef.current;
|
|
67
|
+
if (localPreviewDashboard) {
|
|
68
|
+
setShell((current) => resolveDashboardReady(
|
|
69
|
+
current,
|
|
70
|
+
localPreviewDashboard,
|
|
71
|
+
{ status: "signedIn", label: "Local preview editor", email: null },
|
|
72
|
+
new Date().toISOString(),
|
|
73
|
+
));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
61
76
|
setShell((current) => beginAccessCheck(current));
|
|
62
77
|
|
|
63
78
|
try {
|
|
@@ -101,7 +116,7 @@ export function NewsDeskClientProvider({
|
|
|
101
116
|
error instanceof Error ? error.message : "Could not load Newsroom data.",
|
|
102
117
|
));
|
|
103
118
|
}
|
|
104
|
-
}, [devSandboxEditorAuth]);
|
|
119
|
+
}, [devSandboxEditorAuth, localPreviewDashboard]);
|
|
105
120
|
|
|
106
121
|
const refreshAssignments = useCallback(async () => {
|
|
107
122
|
try {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
4
|
+
import { createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
|
|
3
5
|
import { generateClient } from "aws-amplify/data";
|
|
4
6
|
import { testEditorOverrideAllowed } from "../lib/test-editor-override";
|
|
5
7
|
import { downloadData, getUrl } from "aws-amplify/storage";
|
|
@@ -1863,6 +1865,7 @@ export function normalizeNewsroomReferencePageOrder(value?: string | null): News
|
|
|
1863
1865
|
}
|
|
1864
1866
|
|
|
1865
1867
|
export async function loadNewsroomReferencePage(options: NewsroomReferencePageOptions = {}): Promise<NewsroomRecordPage<ReferenceRecord>> {
|
|
1868
|
+
if (isLocalPreviewEnabled()) return { items: createLocalPreviewReferences(), nextToken: null, hasMore: false };
|
|
1866
1869
|
configureAmplifyClient();
|
|
1867
1870
|
const order = normalizeNewsroomReferencePageOrder(options.order);
|
|
1868
1871
|
if (order === "imported") {
|
|
@@ -2146,6 +2149,7 @@ export async function loadEditorOverviewEditionData(): Promise<{
|
|
|
2146
2149
|
}
|
|
2147
2150
|
|
|
2148
2151
|
export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPageOptions = {}): Promise<NewsroomRecordPage<AssignmentRecord>> {
|
|
2152
|
+
if (isLocalPreviewEnabled()) return { items: createLocalPreviewAssignments(), nextToken: null, hasMore: false };
|
|
2149
2153
|
return loadNewsroomFeedPage<AssignmentRecord>({
|
|
2150
2154
|
query: NEWSROOM_ASSIGNMENT_FEED_QUERY,
|
|
2151
2155
|
field: "listAssignmentsByNewsroomFeedAndCreatedAt",
|
|
@@ -2169,6 +2173,7 @@ const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
|
2169
2173
|
`;
|
|
2170
2174
|
|
|
2171
2175
|
export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2176
|
+
if (isLocalPreviewEnabled()) return createLocalPreviewContentItemVersions();
|
|
2172
2177
|
configureAmplifyClient();
|
|
2173
2178
|
const client = generateClient<Schema>() as unknown as {
|
|
2174
2179
|
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
|
|
@@ -3826,3 +3831,72 @@ export async function requestCyclotronReviewRate(
|
|
|
3826
3831
|
content: JSON.stringify(request),
|
|
3827
3832
|
});
|
|
3828
3833
|
}
|
|
3834
|
+
|
|
3835
|
+
const LIST_PRIMING_REQUEST_PAYLOADS_QUERY = `
|
|
3836
|
+
query ListKnowledgeRawPayloadsByOwnerAndKind($ownerId: ID!, $payloadKind: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) {
|
|
3837
|
+
listKnowledgeRawPayloadsByOwnerAndKind(ownerId: $ownerId, payloadKind: $payloadKind, limit: $limit, nextToken: $nextToken) {
|
|
3838
|
+
items { id }
|
|
3839
|
+
nextToken
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
`;
|
|
3843
|
+
|
|
3844
|
+
/** Ask the next decide-relevance sweep to prime the running cyclotron with one pasted URL. */
|
|
3845
|
+
export async function requestCyclotronPriming(
|
|
3846
|
+
cyclotronId: string,
|
|
3847
|
+
request: import("../lib/relevance-status").PrimingRequest,
|
|
3848
|
+
): Promise<void> {
|
|
3849
|
+
const { primingRequestPayloadId, PRIMING_REQUEST_KIND, safeId } = await import("../lib/relevance-status");
|
|
3850
|
+
const id = primingRequestPayloadId(cyclotronId, request.requestId);
|
|
3851
|
+
await runGraphql(CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION, {
|
|
3852
|
+
input: {
|
|
3853
|
+
id,
|
|
3854
|
+
ownerType: "cyclotron",
|
|
3855
|
+
ownerId: safeId(cyclotronId),
|
|
3856
|
+
payloadKind: PRIMING_REQUEST_KIND,
|
|
3857
|
+
createdAt: request.requestedAt,
|
|
3858
|
+
updatedAt: request.requestedAt,
|
|
3859
|
+
},
|
|
3860
|
+
});
|
|
3861
|
+
await uploadModelPayloadForOwner({
|
|
3862
|
+
ownerKind: "knowledgeRawPayload",
|
|
3863
|
+
ownerId: id,
|
|
3864
|
+
ownerLineageId: id,
|
|
3865
|
+
role: "raw_payload",
|
|
3866
|
+
sortKey: "priming-request",
|
|
3867
|
+
filename: "priming-request.json",
|
|
3868
|
+
mediaType: "application/json",
|
|
3869
|
+
content: JSON.stringify(request),
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
/** The saved priming requests of a cyclotron, newest first, each with the sweep's outcome once there is one. */
|
|
3874
|
+
export async function loadCyclotronPrimingRequests(
|
|
3875
|
+
cyclotronId: string,
|
|
3876
|
+
): Promise<import("../lib/relevance-status").PrimingRequestView[]> {
|
|
3877
|
+
const { PRIMING_OUTCOME_ROLE, PRIMING_REQUEST_KIND, isPrimingOutcome, safeId } = await import("../lib/relevance-status");
|
|
3878
|
+
const ids: string[] = [];
|
|
3879
|
+
let nextToken: string | null = null;
|
|
3880
|
+
do {
|
|
3881
|
+
const page: { listKnowledgeRawPayloadsByOwnerAndKind?: { items?: Array<{ id: string } | null>; nextToken?: string | null } | null } =
|
|
3882
|
+
await runGraphql(LIST_PRIMING_REQUEST_PAYLOADS_QUERY, {
|
|
3883
|
+
ownerId: safeId(cyclotronId), payloadKind: { eq: PRIMING_REQUEST_KIND }, limit: 100, nextToken,
|
|
3884
|
+
});
|
|
3885
|
+
const connection = page.listKnowledgeRawPayloadsByOwnerAndKind;
|
|
3886
|
+
ids.push(...(connection?.items ?? []).filter((item): item is { id: string } => Boolean(item)).map((item) => item.id));
|
|
3887
|
+
nextToken = connection?.nextToken ?? null;
|
|
3888
|
+
} while (nextToken);
|
|
3889
|
+
const views = await Promise.all(ids.map(async (id) => {
|
|
3890
|
+
const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", id, ["raw_payload", PRIMING_OUTCOME_ROLE]);
|
|
3891
|
+
const request = payloads.find((payload) => payload.attachment.role === "raw_payload")?.json;
|
|
3892
|
+
const outcome = payloads.find((payload) => payload.attachment.role === PRIMING_OUTCOME_ROLE)?.json;
|
|
3893
|
+
if (!request || typeof request !== "object") return null;
|
|
3894
|
+
return {
|
|
3895
|
+
request: request as import("../lib/relevance-status").PrimingRequest,
|
|
3896
|
+
outcome: isPrimingOutcome(outcome) ? outcome : null,
|
|
3897
|
+
};
|
|
3898
|
+
}));
|
|
3899
|
+
return views
|
|
3900
|
+
.filter((view): view is import("../lib/relevance-status").PrimingRequestView => view !== null)
|
|
3901
|
+
.sort((left, right) => right.request.requestedAt.localeCompare(left.request.requestedAt));
|
|
3902
|
+
}
|
package/components/newspaper.tsx
CHANGED
|
@@ -1139,10 +1139,10 @@ function FrontStoryBlock({
|
|
|
1139
1139
|
<div className="jump-line">
|
|
1140
1140
|
{block.jumpTargetPage ? (
|
|
1141
1141
|
disableLinks ? (
|
|
1142
|
-
<span>{block.jumpLabel
|
|
1142
|
+
<span>{block.jumpLabel}</span>
|
|
1143
1143
|
) : (
|
|
1144
1144
|
<Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
|
|
1145
|
-
{block.jumpLabel
|
|
1145
|
+
{block.jumpLabel}
|
|
1146
1146
|
</Link>
|
|
1147
1147
|
)
|
|
1148
1148
|
) : (
|
|
@@ -1178,10 +1178,10 @@ function FrontStoryBlock({
|
|
|
1178
1178
|
<div className="jump-line">
|
|
1179
1179
|
{block.jumpTargetPage ? (
|
|
1180
1180
|
disableLinks ? (
|
|
1181
|
-
<span>{block.jumpLabel
|
|
1181
|
+
<span>{block.jumpLabel}</span>
|
|
1182
1182
|
) : (
|
|
1183
1183
|
<Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
|
|
1184
|
-
{block.jumpLabel
|
|
1184
|
+
{block.jumpLabel}
|
|
1185
1185
|
</Link>
|
|
1186
1186
|
)
|
|
1187
1187
|
) : (
|
|
@@ -2,10 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { IssueCollection, IssueDetailPanel
|
|
5
|
+
import { IssueCollection, IssueDetailPanel } from "@kanbus/ui";
|
|
6
6
|
import "@kanbus/ui/styles/kanban.css";
|
|
7
7
|
import "./newsroom-newsprint-board.css";
|
|
8
|
-
import {
|
|
8
|
+
import { PlusIcon } from "lucide-react";
|
|
9
|
+
import { buttonVariants } from "./ui/button";
|
|
10
|
+
import { cn } from "../lib/utils";
|
|
11
|
+
import { newsroomHref } from "../lib/newsroom-base-path";
|
|
12
|
+
import {
|
|
13
|
+
ASSIGNMENT_BOARD_TYPE,
|
|
14
|
+
assignmentBoardConfig,
|
|
15
|
+
canReadNewsroomBoard,
|
|
16
|
+
contentItemBoardConfig,
|
|
17
|
+
contentItemBoardType,
|
|
18
|
+
projectAssignmentBoard,
|
|
19
|
+
projectContentItemBoard,
|
|
20
|
+
type NativeBoardIssue,
|
|
21
|
+
} from "../lib/newsroom-board";
|
|
9
22
|
import type { AssignmentRecord } from "../lib/category-repository";
|
|
10
23
|
import { getContentItemName, SITE_BRAND } from "../lib/site-brand";
|
|
11
24
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
@@ -17,6 +30,7 @@ import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
|
17
30
|
import { readLocalReaderSettings, subscribeReaderSettingsChanges } from "./reader-settings";
|
|
18
31
|
|
|
19
32
|
const STATIC_MOTION = { mode: "static" } as const;
|
|
33
|
+
const BOARD_REFRESH_INTERVAL_MILLISECONDS = 30_000;
|
|
20
34
|
|
|
21
35
|
async function loadEveryNewsroomAssignment() {
|
|
22
36
|
const assignments: AssignmentRecord[] = [];
|
|
@@ -29,16 +43,16 @@ async function loadEveryNewsroomAssignment() {
|
|
|
29
43
|
return assignments;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
export type NewsroomBoardSubject = "contentItem" | "assignment";
|
|
47
|
+
|
|
48
|
+
/** Read-only board of one record kind over authenticated CMS records; existing workspaces own every mutation. */
|
|
49
|
+
export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }) {
|
|
34
50
|
const session = useOptionalNewsDeskClient();
|
|
35
51
|
const allowed = canReadNewsroomBoard(session?.shell.phase);
|
|
36
52
|
const theme = useResolvedPapyrusTheme();
|
|
37
53
|
const [lowMotion, setLowMotion] = useState(false);
|
|
38
54
|
const [issues, setIssues] = useState<NativeBoardIssue[]>([]);
|
|
39
|
-
const [filter, setFilter] = useState(contentItemBoardType());
|
|
40
55
|
const [view, setView] = useState<"board" | "list">("board");
|
|
41
|
-
const [revision, setRevision] = useState(0);
|
|
42
56
|
const [selected, setSelected] = useState<string | null>(null);
|
|
43
57
|
const [loading, setLoading] = useState(false);
|
|
44
58
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -51,35 +65,54 @@ export function NewsroomBoardView() {
|
|
|
51
65
|
const request = ++generation.current;
|
|
52
66
|
setIssues([]); setSelected(null); setError(null);
|
|
53
67
|
if (!allowed) { setLoading(false); return; }
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const loadIssues = () => subject === "contentItem"
|
|
69
|
+
? Promise.all([
|
|
70
|
+
Promise.all([loadNewsroomContentItemVersions(), loadNewsroomPublishedContentItemVersions()]).then(([workingVersions, publishedVersions]) => [...workingVersions, ...publishedVersions]),
|
|
71
|
+
loadEveryNewsroomAssignment(),
|
|
72
|
+
]).then(([contentItemVersions, assignments]) => projectContentItemBoard({ contentItemVersions, assignments }))
|
|
73
|
+
: loadEveryNewsroomAssignment().then(projectAssignmentBoard);
|
|
74
|
+
const refresh = (initial: boolean) => {
|
|
75
|
+
if (initial) setLoading(true);
|
|
76
|
+
loadIssues().then(loadedIssues => {
|
|
77
|
+
if (generation.current !== request) return;
|
|
78
|
+
setIssues(loadedIssues);
|
|
79
|
+
setError(null);
|
|
80
|
+
}).catch((reason: unknown) => {
|
|
81
|
+
if (generation.current === request && initial) setError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
|
|
82
|
+
}).finally(() => { if (generation.current === request && initial) setLoading(false); });
|
|
83
|
+
};
|
|
84
|
+
refresh(true);
|
|
85
|
+
const refreshWhenVisible = () => { if (document.visibilityState === "visible") refresh(false); };
|
|
86
|
+
const interval = window.setInterval(refreshWhenVisible, BOARD_REFRESH_INTERVAL_MILLISECONDS);
|
|
87
|
+
document.addEventListener("visibilitychange", refreshWhenVisible);
|
|
88
|
+
return () => { generation.current = request + 1; window.clearInterval(interval); document.removeEventListener("visibilitychange", refreshWhenVisible); };
|
|
89
|
+
}, [allowed, subject]);
|
|
90
|
+
const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
|
|
91
|
+
const boardType = subject === "contentItem" ? contentItemBoardType() : ASSIGNMENT_BOARD_TYPE;
|
|
92
|
+
const pageTitle = subject === "contentItem" ? getContentItemName().plural : "Assignments";
|
|
67
93
|
const issue = issues.find(item => item.id === selected);
|
|
68
94
|
if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
|
|
69
|
-
return <NewsroomOpsShell activeTab="
|
|
95
|
+
return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
|
|
96
|
+
headerActions={subject === "contentItem" ? (
|
|
97
|
+
<Link className={cn(buttonVariants({ size: "sm" }))} data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
|
|
98
|
+
<PlusIcon aria-hidden="true" />
|
|
99
|
+
{`New ${getContentItemName().singular.toLowerCase()}`}
|
|
100
|
+
</Link>
|
|
101
|
+
) : undefined}>
|
|
70
102
|
<div className="organizer-native-board papyrus-newsprint-board" data-color-mode={theme}>
|
|
71
103
|
<div className="organizer-board-toolbar">
|
|
72
|
-
<
|
|
73
|
-
<div role="group" aria-label="Record presentation"><button type="button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button><button type="button" disabled={loading} onClick={() => setRevision(current => current + 1)}>Refresh records</button></div>
|
|
104
|
+
<div role="group" aria-label="Record presentation"><button type="button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
|
|
74
105
|
</div>
|
|
75
|
-
<p className="organizer-board-note">{
|
|
106
|
+
<p className="organizer-board-note">{subject === "contentItem"
|
|
107
|
+
? `Each ${getContentItemName().singular.toLowerCase()} appears once, in the column for its stage. Open one to use its existing editor.`
|
|
108
|
+
: "Each assignment appears once, in the column for its status. Open one to use its existing workflow controls."}</p>
|
|
76
109
|
{error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
|
|
77
110
|
<p role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</p>
|
|
78
111
|
<div className="organizer-board-body" data-detail-open={Boolean(issue)}>
|
|
79
|
-
<IssueCollection issues={issues} config={config} query={{ type:
|
|
112
|
+
<IssueCollection issues={issues} config={config} query={{ type: boardType }} view={view}
|
|
80
113
|
motion={lowMotion ? STATIC_MOTION : undefined}
|
|
81
|
-
columnStateKey={`papyrus.cms.columns.v1.${SITE_BRAND.id}`} initialScroll="start"
|
|
82
|
-
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel=
|
|
114
|
+
columnStateKey={`papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}`} initialScroll="start"
|
|
115
|
+
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
|
|
83
116
|
emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
|
|
84
117
|
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[{ label: "CMS record ID", value: issue.nativeId }, { label: "Status", value: issue.nativeStatus }]}>
|
|
85
118
|
<Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}</Link>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { NewsDeskClientProvider } from "./news-desk-client-provider";
|
|
2
|
+
import { createDemoCategorySteeringDashboard } from "../lib/category-repository";
|
|
2
3
|
import { resolveDevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
4
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
5
|
+
import { getBrandNewsroomSectionsConfigPath, resolveActiveSiteBrand } from "../lib/site-brand-tenant";
|
|
3
6
|
|
|
4
7
|
export function NewsroomClientShell({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
5
8
|
const devSandboxEditorAuth = resolveDevSandboxEditorAuth();
|
|
9
|
+
const activeBrand = resolveActiveSiteBrand();
|
|
10
|
+
const localPreviewDashboard = isLocalPreviewEnabled()
|
|
11
|
+
? createDemoCategorySteeringDashboard(activeBrand.id, getBrandNewsroomSectionsConfigPath(activeBrand))
|
|
12
|
+
: null;
|
|
6
13
|
|
|
7
14
|
return (
|
|
8
|
-
<NewsDeskClientProvider devSandboxEditorAuth={devSandboxEditorAuth}>
|
|
15
|
+
<NewsDeskClientProvider devSandboxEditorAuth={devSandboxEditorAuth} localPreviewDashboard={localPreviewDashboard}>
|
|
9
16
|
{children}
|
|
10
17
|
</NewsDeskClientProvider>
|
|
11
18
|
);
|
|
@@ -15,7 +15,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGro
|
|
|
15
15
|
import type { NewsroomOpsShellProps } from "./newsroom-ops-shell";
|
|
16
16
|
import "./newsroom-organizer.css";
|
|
17
17
|
|
|
18
|
-
const ICONS = { overview: LayoutDashboard,
|
|
18
|
+
const ICONS = { overview: LayoutDashboard, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
19
19
|
|
|
20
20
|
/** Navigation is a presentation layer; existing route and server authorization still own access. */
|
|
21
21
|
function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
|
|
@@ -99,18 +99,23 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
|
|
|
99
99
|
return () => { document.removeEventListener("focusin", remember); media.removeEventListener("change", change); cancelAnimationFrame(frame); };
|
|
100
100
|
}, []);
|
|
101
101
|
const activePath = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
|
|
102
|
+
const sidebarToggleLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
103
|
+
const sidebarToggle = <button type="button" className="organizer-toggle" title={sidebarToggleLabel}
|
|
104
|
+
aria-label={sidebarToggleLabel} aria-expanded={!collapsed} aria-controls="organizer-sidebar" onClick={() => setCollapsed(value => !value)}>
|
|
105
|
+
{collapsed ? <PanelLeftOpen size={16} aria-hidden="true" /> : <PanelLeftClose size={16} aria-hidden="true" />}
|
|
106
|
+
</button>;
|
|
102
107
|
return <div className="newsroom-organizer" data-news-desk data-newsroom-ops-shell="true" data-newsroom-chrome="organizer" data-sidebar-collapsed={collapsed || !showNavigation ? "true" : "false"}>
|
|
103
108
|
{showNavigation && <aside className="organizer-sidebar" hidden={collapsed} id="organizer-sidebar">
|
|
104
|
-
<
|
|
109
|
+
<div className="organizer-sidebar-head">
|
|
110
|
+
<Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{appTitle}<small>The newsroom</small></Link>
|
|
111
|
+
{sidebarToggle}
|
|
112
|
+
</div>
|
|
105
113
|
<Navigation activePath={activePath} demo={demo} admin={admin} surface="sidebar" />
|
|
106
114
|
<Account placement="sidebar" />
|
|
107
115
|
</aside>}
|
|
108
116
|
<div className="organizer-workspace">
|
|
109
117
|
<header className="organizer-topbar" data-newsroom-ops-topbar>
|
|
110
|
-
{showNavigation &&
|
|
111
|
-
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"} aria-expanded={!collapsed} aria-controls="organizer-sidebar" onClick={() => setCollapsed(value => !value)}>
|
|
112
|
-
{collapsed ? <PanelLeftOpen size={16} aria-hidden="true" /> : <PanelLeftClose size={16} aria-hidden="true" />}<span>{collapsed ? "Expand sidebar" : "Collapse sidebar"}</span>
|
|
113
|
-
</button>}
|
|
118
|
+
{showNavigation && collapsed && sidebarToggle}
|
|
114
119
|
<Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
|
|
115
120
|
<h1 data-newsroom-ops-title>{pageTitle}</h1><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
116
121
|
</header>
|
|
@@ -118,7 +123,7 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
|
|
|
118
123
|
</div>
|
|
119
124
|
{showNavigation && <nav className="organizer-bottom-nav" aria-label="Primary newsroom navigation"><ul>
|
|
120
125
|
{ORGANIZER_PRIMARY_NAV.map(item => { const Icon = ICONS[item.key as keyof typeof ICONS]; return <li key={item.key}><Link
|
|
121
|
-
href={getNewsroomNavHref(item.path, demo)} title={item.description} aria-label={item.label} aria-current={activeTab === item.key
|
|
126
|
+
href={getNewsroomNavHref(item.path, demo)} title={item.description} aria-label={item.label} aria-current={activeTab === item.key ? "page" : undefined}>
|
|
122
127
|
<Icon size={18} aria-hidden="true" /><span>{item.label}</span></Link></li>; })}
|
|
123
128
|
<li><Sheet open={menuOpen} onOpenChange={setMenuOpen}><SheetTrigger className="organizer-more" aria-label="More"><Menu size={18} aria-hidden="true" /><span>More</span></SheetTrigger>
|
|
124
129
|
<SheetContent side="bottom" className="organizer-menu"><SheetHeader><SheetTitle>Newsroom</SheetTitle><SheetDescription>Workspaces and administration</SheetDescription></SheetHeader>
|
|
@@ -23,7 +23,8 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
23
23
|
.organizer-topbar{height:57px;min-height:57px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-ink)}
|
|
24
24
|
.organizer-masthead{font:900 28px Newsroom Display,Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
|
|
25
25
|
.organizer-topbar h1{font:400 12px Newsroom UI,system-ui;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}
|
|
26
|
-
.organizer-toggle{display:flex;align-items:center;
|
|
26
|
+
.organizer-toggle{display:flex;align-items:center;justify-content:center;width:36px;min-height:36px;padding:0;border:1px solid var(--org-line);flex-shrink:0}
|
|
27
|
+
.organizer-sidebar-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.organizer-sidebar-head .organizer-brand{flex:1;min-width:0}
|
|
27
28
|
.organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
|
|
28
29
|
.organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
|
|
29
30
|
.organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
|
|
@@ -49,7 +49,9 @@ import { normalizeReferenceCurationStatus } from "../lib/reference-policy";
|
|
|
49
49
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
50
50
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
51
51
|
import { ReferenceRelevanceReview } from "./reference-relevance-review";
|
|
52
|
+
import { ReferenceCyclotronPriming, type PrimingFormInput } from "./reference-cyclotron-priming";
|
|
52
53
|
import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyclotron-status";
|
|
54
|
+
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
53
55
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
54
56
|
import type { RelevanceCuration, RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
55
57
|
import {
|
|
@@ -62,6 +64,12 @@ type NewsroomReferencesViewProps = {
|
|
|
62
64
|
priming?: PrimingPanelProps;
|
|
63
65
|
/** The relevance cyclotron's status snapshot, shown above the list. */
|
|
64
66
|
cyclotronStatus?: CyclotronStatus | null;
|
|
67
|
+
/** Present for editors who may prime the running relevance cyclotron with a pasted URL. */
|
|
68
|
+
cyclotronPriming?: {
|
|
69
|
+
labels: readonly string[];
|
|
70
|
+
requests: PrimingRequestView[];
|
|
71
|
+
onRequest: (input: PrimingFormInput) => Promise<void> | void;
|
|
72
|
+
};
|
|
65
73
|
/** Present for editors who may ask for a manual review rate. */
|
|
66
74
|
onRequestReviewRate?: (choice: ReviewRateChoice) => Promise<void> | void;
|
|
67
75
|
demo?: boolean;
|
|
@@ -399,6 +407,7 @@ function ReferenceDetailPanel({
|
|
|
399
407
|
export function NewsroomReferencesView({
|
|
400
408
|
priming,
|
|
401
409
|
cyclotronStatus,
|
|
410
|
+
cyclotronPriming,
|
|
402
411
|
decisions,
|
|
403
412
|
onRequestReviewRate,
|
|
404
413
|
demo = false,
|
|
@@ -516,6 +525,15 @@ export function NewsroomReferencesView({
|
|
|
516
525
|
</div>
|
|
517
526
|
<ReferenceCurationControls disabled={disabled} />
|
|
518
527
|
{priming && !disabled ? <ReferencePriming {...priming} /> : null}
|
|
528
|
+
{cyclotronPriming && !disabled ? (
|
|
529
|
+
<div className="shrink-0">
|
|
530
|
+
<ReferenceCyclotronPriming
|
|
531
|
+
labels={cyclotronPriming.labels}
|
|
532
|
+
onRequestPriming={cyclotronPriming.onRequest}
|
|
533
|
+
requests={cyclotronPriming.requests}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
) : null}
|
|
519
537
|
{cyclotronStatus ? (
|
|
520
538
|
<div className="shrink-0">
|
|
521
539
|
<ReferenceCyclotronStatus onRequestRate={disabled ? undefined : onRequestReviewRate} status={cyclotronStatus} />
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { primingStateText, type PrimingRequestView } from "../lib/relevance-status";
|
|
5
|
+
|
|
6
|
+
export type PrimingFormInput = { url: string; label: string; explanation: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A compact form that primes the running relevance cyclotron with a pasted
|
|
10
|
+
* URL. The next sweep reads the page, adds it to the cyclotron's example list
|
|
11
|
+
* and records the outcome, which this list shows beside each saved request.
|
|
12
|
+
*/
|
|
13
|
+
export function ReferenceCyclotronPriming({
|
|
14
|
+
labels,
|
|
15
|
+
requests,
|
|
16
|
+
onRequestPriming,
|
|
17
|
+
}: {
|
|
18
|
+
labels: readonly string[];
|
|
19
|
+
requests: PrimingRequestView[];
|
|
20
|
+
onRequestPriming: (input: PrimingFormInput) => Promise<void> | void;
|
|
21
|
+
}) {
|
|
22
|
+
const [url, setUrl] = useState("");
|
|
23
|
+
const [label, setLabel] = useState(labels[0] ?? "");
|
|
24
|
+
const [explanation, setExplanation] = useState("");
|
|
25
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
async function submit() {
|
|
28
|
+
setMessage("Saving…");
|
|
29
|
+
try {
|
|
30
|
+
await onRequestPriming({ url, label, explanation });
|
|
31
|
+
setUrl("");
|
|
32
|
+
setExplanation("");
|
|
33
|
+
setMessage("Saved. The next sweep reads the page and adds it to the cyclotron.");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<section aria-label="Prime the relevance cyclotron" className="space-y-2 text-sm" data-reference-cyclotron-priming>
|
|
41
|
+
<form
|
|
42
|
+
className="flex flex-wrap items-end gap-3"
|
|
43
|
+
onSubmit={(event) => {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
void submit();
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<label className="flex min-w-64 flex-1 flex-col gap-1">
|
|
49
|
+
<span className="text-xs text-muted-foreground">Prime with a URL</span>
|
|
50
|
+
<input
|
|
51
|
+
aria-label="Priming URL"
|
|
52
|
+
onChange={(event) => setUrl(event.target.value)}
|
|
53
|
+
placeholder="https://"
|
|
54
|
+
required
|
|
55
|
+
type="url"
|
|
56
|
+
value={url}
|
|
57
|
+
/>
|
|
58
|
+
</label>
|
|
59
|
+
<label className="flex flex-col gap-1">
|
|
60
|
+
<span className="text-xs text-muted-foreground">Label</span>
|
|
61
|
+
<select aria-label="Priming label" onChange={(event) => setLabel(event.target.value)} value={label}>
|
|
62
|
+
{labels.map((choice) => (
|
|
63
|
+
<option key={choice} value={choice}>{choice}</option>
|
|
64
|
+
))}
|
|
65
|
+
</select>
|
|
66
|
+
</label>
|
|
67
|
+
<label className="flex min-w-48 flex-1 flex-col gap-1">
|
|
68
|
+
<span className="text-xs text-muted-foreground">Why (optional)</span>
|
|
69
|
+
<input aria-label="Priming explanation" onChange={(event) => setExplanation(event.target.value)} type="text" value={explanation} />
|
|
70
|
+
</label>
|
|
71
|
+
<button className="cyclotron-review-secondary" type="submit">Save</button>
|
|
72
|
+
{message ? <p className="basis-full text-xs" role="status">{message}</p> : null}
|
|
73
|
+
</form>
|
|
74
|
+
{requests.length ? (
|
|
75
|
+
<ul aria-label="Saved priming requests" className="space-y-1 text-xs text-muted-foreground">
|
|
76
|
+
{requests.slice(0, 10).map((view) => (
|
|
77
|
+
<li data-priming-request-state={view.outcome?.state ?? "waiting"} key={view.request.requestId}>
|
|
78
|
+
<span className="font-medium">{view.request.label}</span> {view.request.url} · {primingStateText(view)}
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
) : null}
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
type KnowledgeQueryResponse,
|
|
58
58
|
type NewsroomRecordPage,
|
|
59
59
|
loadCyclotronStatus,
|
|
60
|
+
requestCyclotronPriming,
|
|
61
|
+
loadCyclotronPrimingRequests,
|
|
60
62
|
requestCyclotronReviewRate,
|
|
61
63
|
} from "./news-desk-taxonomy-client";
|
|
62
64
|
import { listConsoleThreads } from "../lib/console-chat-client";
|
|
@@ -161,7 +163,13 @@ import {
|
|
|
161
163
|
referenceCurationStatusForAction,
|
|
162
164
|
} from "../lib/reference-policy";
|
|
163
165
|
import { currentRelevanceDecisions } from "../lib/relevance-decisions";
|
|
164
|
-
import {
|
|
166
|
+
import {
|
|
167
|
+
buildPrimingRequest,
|
|
168
|
+
buildReviewRateRequest,
|
|
169
|
+
DEFAULT_RELEVANCE_CYCLOTRON,
|
|
170
|
+
DEMO_CYCLOTRON_STATUS,
|
|
171
|
+
type PrimingRequestView,
|
|
172
|
+
} from "../lib/relevance-status";
|
|
165
173
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
166
174
|
import {
|
|
167
175
|
referenceDisplaySummary,
|
|
@@ -789,6 +797,23 @@ function NewsDeskDashboard({
|
|
|
789
797
|
() => [...relevanceDecisions.values()].find((decision) => decision.cyclotronId)?.cyclotronId ?? null,
|
|
790
798
|
[relevanceDecisions],
|
|
791
799
|
);
|
|
800
|
+
const primingCyclotronId = relevanceCyclotronId ?? DEFAULT_RELEVANCE_CYCLOTRON.id;
|
|
801
|
+
const primingLabels = useMemo(
|
|
802
|
+
() => [...relevanceDecisions.values()].find((decision) => decision.classes.length)?.classes
|
|
803
|
+
?? DEFAULT_RELEVANCE_CYCLOTRON.labels,
|
|
804
|
+
[relevanceDecisions],
|
|
805
|
+
);
|
|
806
|
+
const [primingRequests, setPrimingRequests] = useState<PrimingRequestView[]>([]);
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
if (dashboard.isDemo) return;
|
|
809
|
+
let cancelled = false;
|
|
810
|
+
const refresh = () => loadCyclotronPrimingRequests(primingCyclotronId)
|
|
811
|
+
.then((views) => { if (!cancelled) setPrimingRequests(views); })
|
|
812
|
+
.catch(() => { if (!cancelled) setPrimingRequests([]); });
|
|
813
|
+
void refresh();
|
|
814
|
+
const timer = window.setInterval(() => { void refresh(); }, 30_000);
|
|
815
|
+
return () => { cancelled = true; window.clearInterval(timer); };
|
|
816
|
+
}, [dashboard.isDemo, primingCyclotronId]);
|
|
792
817
|
const [cyclotronStatus, setCyclotronStatus] = useState<CyclotronStatus | null>(null);
|
|
793
818
|
useEffect(() => {
|
|
794
819
|
if (!relevanceCyclotronId) return;
|
|
@@ -3189,6 +3214,15 @@ function NewsDeskDashboard({
|
|
|
3189
3214
|
disabled={controlsDisabled}
|
|
3190
3215
|
initialReferenceLineageId={initialSelection.reference ?? pathnameReferenceLineageId}
|
|
3191
3216
|
cyclotronStatus={cyclotronStatus}
|
|
3217
|
+
cyclotronPriming={{
|
|
3218
|
+
labels: primingLabels,
|
|
3219
|
+
requests: primingRequests,
|
|
3220
|
+
onRequest: async (input) => {
|
|
3221
|
+
const request = buildPrimingRequest(input, authState.label, primingLabels);
|
|
3222
|
+
if (!dashboard.isDemo) await requestCyclotronPriming(primingCyclotronId, request);
|
|
3223
|
+
setPrimingRequests((current) => [{ request, outcome: null }, ...current]);
|
|
3224
|
+
},
|
|
3225
|
+
}}
|
|
3192
3226
|
decisions={relevanceDecisions}
|
|
3193
3227
|
onRequestReviewRate={relevanceCyclotronId ? async (choice) => {
|
|
3194
3228
|
const request = buildReviewRateRequest(choice, authState.label);
|