@anthusai/papyrus 5.43.0-next.3 → 5.43.0-next.30
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/data/resource.js +6 -0
- package/amplify/data/resource.ts +7 -0
- package/amplify/data/schema.js +6 -0
- package/amplify/data/schema.ts +7 -0
- package/amplify/functions/category-action/handler.ts +2 -1
- package/amplify/functions/content-actions/handler.py +8 -0
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/monitoring-view/schema.js +16 -0
- package/amplify/functions/monitoring-view/schema.ts +16 -0
- package/amplify/functions/procedure-action/handler.ts +2 -10
- package/amplify/functions/proposal-actions/proposals.py +20 -3
- package/amplify/plugins/reader-steering/function/handler.py +1 -3
- package/amplify/plugins/reader-steering/function/moderation.py +19 -0
- package/app/api/newsroom/source-frameable/route.ts +12 -0
- package/app/api/newsroom/source-icon/route.ts +20 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +2 -0
- package/app/newsroom/[section]/page.tsx +1 -0
- package/bin/papyrus-app.mjs +12 -1
- package/bin/papyrus-infra.mjs +21 -1
- package/components/dev-sandbox-editor-sign-in.tsx +2 -2
- package/components/markdown-context.tsx +412 -0
- package/components/monitoring-overview-view.tsx +143 -21
- package/components/monitoring-panel.tsx +64 -4
- package/components/needs-me-bodies.tsx +301 -0
- package/components/needs-me-detail.tsx +24 -0
- package/components/needs-me-feed.tsx +600 -0
- package/components/needs-me-reference-detail.tsx +75 -0
- package/components/needs-me-shared.tsx +48 -0
- package/components/news-desk-client-provider.tsx +24 -3
- package/components/news-desk-page.tsx +2 -1
- package/components/news-desk-taxonomy-client.ts +2 -1
- package/components/newsroom-access-gate.tsx +13 -20
- package/components/newsroom-client-shell.tsx +2 -1
- package/components/newsroom-ops-overview.tsx +88 -73
- package/components/newsroom-ops-shell.tsx +8 -28
- package/components/newsroom-organizer.css +92 -3
- package/components/newsroom-references-view.tsx +315 -156
- package/components/notification-cards.tsx +369 -0
- package/components/notification-center.tsx +376 -213
- package/components/papyrus-console-shell.tsx +2 -1
- package/components/proposals-panel.tsx +21 -24
- package/components/proposals-view.tsx +4 -2
- package/components/reader-auth-control.tsx +3 -1
- package/components/reference-capture-client.ts +110 -0
- package/components/reference-capture-panel.tsx +190 -0
- package/components/reference-feed.tsx +507 -0
- package/components/reference-review-card.tsx +142 -151
- package/components/reference-source-preview.tsx +27 -5
- package/components/references-page-loading.tsx +61 -0
- package/components/references-prefetch.ts +52 -0
- package/components/review-rating-grid.tsx +190 -0
- package/components/topic-steering-workspace.tsx +47 -437
- package/components/ui/auto-grow-textarea.tsx +18 -0
- package/components/use-media-query.ts +16 -0
- package/components/use-reference-preview-attachments.ts +101 -0
- package/components/use-reference-ratings.ts +188 -0
- package/infra/amplify-app-shell.js +18 -2
- package/infra/amplify-app-shell.ts +18 -2
- package/infra/chat-stack.js +222 -0
- package/infra/chat-stack.ts +250 -0
- package/infra/chat.test.js +142 -0
- package/infra/chat.test.ts +151 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +58 -1
- package/infra/site-config.ts +78 -1
- package/lib/category-repository.ts +6 -1
- package/lib/content-actions-client.ts +18 -0
- package/lib/local-preview-feed-fixture.ts +63 -0
- package/lib/local-preview-synthetic-rejects.ts +41 -0
- package/lib/monitoring-client.ts +29 -2
- package/lib/monitoring-overview.ts +52 -1
- package/lib/monitoring-paging.ts +58 -0
- package/lib/needs-me-items.ts +289 -0
- package/lib/needs-me-ranking.ts +315 -0
- package/lib/newsroom-chat-identity.ts +2 -1
- package/lib/newsroom-index-filters.ts +18 -0
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/newsroom-references.ts +25 -1
- package/lib/notification-feed.ts +212 -0
- package/lib/proposals-client.ts +29 -0
- package/lib/reader-subject.ts +3 -2
- package/lib/reference-capture.ts +294 -0
- package/lib/reference-feed-cache.ts +170 -0
- package/lib/reference-feed-ranking.ts +235 -0
- package/lib/reference-preview.ts +112 -0
- package/lib/review-client.ts +71 -5
- package/lib/review-drafts.ts +277 -0
- package/lib/review-fixture.ts +56 -2
- package/lib/review-requests.ts +89 -24
- package/lib/site-brand.ts +2 -0
- package/lib/source-frameable.ts +77 -0
- package/lib/source-icon.ts +104 -0
- package/lib/staff-groups.ts +23 -0
- package/lib/staging-gate.ts +2 -2
- package/package.json +35 -2
- package/routes.manifest.json +23 -1
- package/components/newsroom-overview-skeleton.tsx +0 -25
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { referenceDisplayTitle, referenceLineageId, formatReferenceListDate } from "../lib/newsroom-references";
|
|
4
|
+
import { referenceMetadataField } from "../lib/reference-display";
|
|
5
|
+
import { referenceDek, referencePreviewsByLineage } from "../lib/reference-preview";
|
|
6
|
+
import { DEFAULT_TEASER_WORDS } from "../lib/reference-capture";
|
|
7
|
+
import { referenceReasonCode, normalizeReferenceCurationStatus } from "../lib/reference-policy";
|
|
8
|
+
import type { ReferenceRecord } from "../lib/category-repository";
|
|
9
|
+
import type { NeedsMeEntry } from "../lib/needs-me-items";
|
|
10
|
+
import { DefaultOutcome, Heading, type DetailProps } from "./needs-me-bodies";
|
|
11
|
+
import { decidedChoiceOf } from "./needs-me-shared";
|
|
12
|
+
import { predictionFromDecision, ReviewCard, type CardPrediction } from "./reference-review-card";
|
|
13
|
+
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
14
|
+
import { useReferenceCapture } from "./reference-capture-client";
|
|
15
|
+
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
16
|
+
import { ReferenceSuggestionProvenance } from "./reference-cyclotron-suggestion";
|
|
17
|
+
import { useReferencePreviewAttachments } from "./use-reference-preview-attachments";
|
|
18
|
+
|
|
19
|
+
function cardPrediction(item: NeedsMeEntry, predictionLoading: boolean): CardPrediction {
|
|
20
|
+
if (item.payload.kind === "reference" && item.payload.decision) return predictionFromDecision(item.payload.decision);
|
|
21
|
+
return predictionLoading ? { state: "loading" } : { state: "none" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A Reference, or a Reference whose capture may be cut off: the References detail's own pieces, in the same order. What the item is
|
|
26
|
+
* comes first (title, blurb, link, badges, summary), then the rating, then what was captured and how well.
|
|
27
|
+
*/
|
|
28
|
+
export function ReferenceBody(props: DetailProps & { payload: Extract<NeedsMeEntry["payload"], { kind: "reference" }> }) {
|
|
29
|
+
const { item, payload, ratings, disabledReason, attachments, captureTeaserLine = DEFAULT_TEASER_WORDS, onCorrectCapture, predictionLoading } = props;
|
|
30
|
+
const reference: ReferenceRecord = payload.reference;
|
|
31
|
+
const lineageId = referenceLineageId(reference);
|
|
32
|
+
const status = normalizeReferenceCurationStatus(reference.curationStatus);
|
|
33
|
+
const previewAttachments = useReferencePreviewAttachments(reference, attachments);
|
|
34
|
+
const capture = useReferenceCapture(reference, previewAttachments, captureTeaserLine, onCorrectCapture);
|
|
35
|
+
const url = reference.sourceUri?.trim() && /^https?:\/\//i.test(reference.sourceUri.trim()) ? reference.sourceUri.trim() : null;
|
|
36
|
+
const summary = capture.summary ?? referenceMetadataField(null, reference.metadata, "summary");
|
|
37
|
+
const dek = referenceDek(reference, referencePreviewsByLineage(previewAttachments).get(lineageId));
|
|
38
|
+
const incomplete = item.type === "incomplete_capture";
|
|
39
|
+
return (
|
|
40
|
+
<div className="space-y-4" data-reference-curation-status={status} data-news-desk-reference-detail={lineageId}>
|
|
41
|
+
<ReviewCard
|
|
42
|
+
decided={decidedChoiceOf(item)}
|
|
43
|
+
disabledReason={disabledReason}
|
|
44
|
+
initialNote={status === "pending" ? "" : reference.curationStatusReason ?? ""}
|
|
45
|
+
initialReason={referenceReasonCode(reference) ?? null}
|
|
46
|
+
dek={dek}
|
|
47
|
+
itemId={item.id}
|
|
48
|
+
meta={<Heading item={item} />}
|
|
49
|
+
onDecide={async () => undefined}
|
|
50
|
+
prediction={cardPrediction(item, predictionLoading)}
|
|
51
|
+
ratings={ratings}
|
|
52
|
+
sourcePreview={<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />}
|
|
53
|
+
summary={summary}
|
|
54
|
+
title={referenceDisplayTitle(reference)}
|
|
55
|
+
titleHref={url}
|
|
56
|
+
>
|
|
57
|
+
{payload.decision ? <div data-reference-relevance-decision={lineageId}><ReferenceSuggestionProvenance decision={payload.decision} /></div> : null}
|
|
58
|
+
</ReviewCard>
|
|
59
|
+
<DefaultOutcome item={item} />
|
|
60
|
+
{incomplete ? <p className="text-sm text-muted-foreground" data-needs-me-capture-note>The captured text looks cut off or missing. Read it below. If it is incomplete, choose Paste or correct the text and say whether the capture was complete, or rate the Reference as it is.</p> : null}
|
|
61
|
+
<ReferenceCapturePanel capture={capture} showSummary={false} />
|
|
62
|
+
<dl className="grid grid-cols-2 gap-3 text-sm">
|
|
63
|
+
<div>
|
|
64
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Imported</dt>
|
|
65
|
+
<dd className="mt-1 text-foreground">{formatReferenceListDate(reference)}</dd>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Media</dt>
|
|
69
|
+
<dd className="mt-1 text-foreground">{reference.mediaType ?? "unknown"}</dd>
|
|
70
|
+
</div>
|
|
71
|
+
</dl>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckIcon, CircleCheckIcon, Loader2Icon } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import type { NeedsMeEntry } from "../lib/needs-me-items";
|
|
6
|
+
import type { ReviewChoice } from "./reference-review-card";
|
|
7
|
+
|
|
8
|
+
/** References and captures are rated like the References feed: a reason code is asked for on Reject. */
|
|
9
|
+
export function isReferenceLike(item: Pick<NeedsMeEntry, "type">): boolean {
|
|
10
|
+
return item.type === "reference" || item.type === "incomplete_capture";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function decidedChoiceOf(item: Pick<NeedsMeEntry, "decisionState">): ReviewChoice | null {
|
|
14
|
+
return item.decisionState === "accepted" ? "accept" : item.decisionState === "rejected" ? "reject" : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** What was recorded when a non-Reference item was decided: its Why, and whether it was rejected as "not this kind". Shown again whenever the item is reopened. */
|
|
18
|
+
export function recordedAnswer(item: NeedsMeEntry): { why: string | null; notThisKind: boolean } {
|
|
19
|
+
const { payload } = item;
|
|
20
|
+
if (payload.kind === "draft") return { why: payload.item.decision?.why ?? null, notThisKind: Boolean(payload.item.decision?.notThisKind) };
|
|
21
|
+
if (payload.kind === "request") return { why: payload.request.answer?.feedback ?? null, notThisKind: payload.request.answer?.steer === "not_this_kind" };
|
|
22
|
+
if (payload.kind === "email") return { why: payload.summary.quarantine?.decision?.reason || null, notThisKind: false };
|
|
23
|
+
return { why: null, notThisKind: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A flag is information, not a question: one Acknowledge button in the footer's own tone. */
|
|
27
|
+
export function FlagFooter({ busy, disabledReason, done, error, onAcknowledge }: { busy: boolean; disabledReason: string | null; done: boolean; error: string | null; onAcknowledge: () => void }) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-2" data-flag-footer>
|
|
30
|
+
{done ? (
|
|
31
|
+
<p className="inline-flex min-h-12 items-center gap-1.5 text-sm font-medium" data-flag-acknowledged><CircleCheckIcon aria-hidden="true" className="size-4" />Acknowledged</p>
|
|
32
|
+
) : (
|
|
33
|
+
<button
|
|
34
|
+
aria-disabled={busy || Boolean(disabledReason) || undefined}
|
|
35
|
+
className={cn("inline-flex min-h-12 w-full items-center justify-center gap-1.5 rounded-[16px] bg-[var(--feed-control)] px-3 text-sm font-semibold text-foreground outline-none transition-colors hover:bg-[var(--feed-control)]/70 focus-visible:ring-2 focus-visible:ring-ring", (busy || disabledReason) && "opacity-60")}
|
|
36
|
+
data-flag-acknowledge
|
|
37
|
+
onClick={() => { if (!busy && !disabledReason) onAcknowledge(); }}
|
|
38
|
+
type="button"
|
|
39
|
+
>
|
|
40
|
+
{busy ? <Loader2Icon aria-hidden="true" className="size-4 animate-spin" /> : <CheckIcon aria-hidden="true" className="size-4" />}
|
|
41
|
+
{busy ? "Saving…" : "Acknowledge"}
|
|
42
|
+
</button>
|
|
43
|
+
)}
|
|
44
|
+
{disabledReason && !done ? <p className="text-xs text-muted-foreground" data-review-disabled-reason>{disabledReason}</p> : null}
|
|
45
|
+
{error ? <p className="text-xs text-destructive" role="alert">{error}</p> : null}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -6,6 +6,9 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
|
|
6
6
|
import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
7
7
|
import type { CategorySteeringDashboard } from "../lib/category-repository";
|
|
8
8
|
import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
|
|
9
|
+
import { discardReferencesHeadStart, prefetchReferencesFeed, wantsReferencesHeadStart } from "./references-prefetch";
|
|
10
|
+
import { SITE_BRAND } from "../lib/site-brand";
|
|
11
|
+
import { clearSavedNewsroomDisplay, loadSessionDisplay, saveSessionDisplay } from "../lib/reference-feed-cache";
|
|
9
12
|
import {
|
|
10
13
|
loadEditorAssignmentsData,
|
|
11
14
|
loadEditorDoctrineRecordsData,
|
|
@@ -75,7 +78,12 @@ export function NewsDeskClientProvider({
|
|
|
75
78
|
));
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
|
-
|
|
81
|
+
// A recent confirmed session is shown (name, "loading" instead of "checking") while the real check runs again.
|
|
82
|
+
// Display only: the check below still decides access, and the backend authorizes every call.
|
|
83
|
+
const remembered = loadSessionDisplay(SITE_BRAND.id);
|
|
84
|
+
setShell((current) => (remembered && !current.dashboard
|
|
85
|
+
? { ...beginAccessCheck(current), phase: "loadingDesk" as const, auth: { status: "signedIn" as const, label: remembered.label } }
|
|
86
|
+
: beginAccessCheck(current)));
|
|
79
87
|
|
|
80
88
|
try {
|
|
81
89
|
await ensureDevSandboxEditorSession(devSandboxEditorAuth);
|
|
@@ -88,6 +96,8 @@ export function NewsDeskClientProvider({
|
|
|
88
96
|
if (bootstrapSequenceRef.current !== sequence) return;
|
|
89
97
|
|
|
90
98
|
if (access.status === "signedOut") {
|
|
99
|
+
clearSavedNewsroomDisplay(SITE_BRAND.id);
|
|
100
|
+
discardReferencesHeadStart();
|
|
91
101
|
setShell((current) => resolveSignedOut(current, access.auth));
|
|
92
102
|
return;
|
|
93
103
|
}
|
|
@@ -100,7 +110,10 @@ export function NewsDeskClientProvider({
|
|
|
100
110
|
return;
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
saveSessionDisplay(SITE_BRAND.id, { label: access.auth.label, isAdmin: access.isAdmin });
|
|
103
114
|
setShell((current) => beginDeskLoad(current, access.auth));
|
|
115
|
+
// The References reads do not depend on the summary dashboard: start them now, side by side with it.
|
|
116
|
+
if (typeof window !== "undefined" && wantsReferencesHeadStart(window.location.pathname)) prefetchReferencesFeed();
|
|
104
117
|
|
|
105
118
|
try {
|
|
106
119
|
const dashboard = await loadEditorNewsDeskDashboard({ isAdmin: access.isAdmin });
|
|
@@ -113,7 +126,8 @@ export function NewsDeskClientProvider({
|
|
|
113
126
|
return;
|
|
114
127
|
}
|
|
115
128
|
if (isAuthorizationError(error)) {
|
|
116
|
-
|
|
129
|
+
// The session already has editor rights, so the backend refusing a read is not a missing role. Say what was refused.
|
|
130
|
+
setShell((current) => resolveDashboardFailure(current, access.auth, refusedReadMessage(error)));
|
|
117
131
|
return;
|
|
118
132
|
}
|
|
119
133
|
setShell((current) => resolveDashboardFailure(
|
|
@@ -182,6 +196,8 @@ export function NewsDeskClientProvider({
|
|
|
182
196
|
const unsubscribe = Hub.listen("auth", ({ payload }) => {
|
|
183
197
|
if (payload.event === "signedOut") {
|
|
184
198
|
bootstrapSequenceRef.current += 1;
|
|
199
|
+
clearSavedNewsroomDisplay(SITE_BRAND.id);
|
|
200
|
+
discardReferencesHeadStart();
|
|
185
201
|
setShell((current) => resolveSignedOut(current, { status: "signedOut", label: "Signed out" }));
|
|
186
202
|
return;
|
|
187
203
|
}
|
|
@@ -215,9 +231,14 @@ function isAuthorizationError(error: unknown): boolean {
|
|
|
215
231
|
return /not authorized|unauthorized|access denied|forbidden/i.test(message);
|
|
216
232
|
}
|
|
217
233
|
|
|
234
|
+
function refusedReadMessage(error: unknown): string {
|
|
235
|
+
const detail = error instanceof Error ? error.message : String(error ?? "");
|
|
236
|
+
return `The Newsroom backend refused a read for this signed-in account (${detail}). The account has editor or admin rights, so this is a backend authorization rule, not a missing role.`;
|
|
237
|
+
}
|
|
238
|
+
|
|
218
239
|
function resolvePrivateReadFailure(state: NewsDeskShellState, auth: NewsDeskShellState["auth"], error: unknown, fallback: string): NewsDeskShellState {
|
|
219
240
|
if (isUnauthenticatedError(error)) return resolveSignedOut(state, { status: "signedOut", label: "Signed out" });
|
|
220
|
-
if (isAuthorizationError(error)) return
|
|
241
|
+
if (isAuthorizationError(error)) return resolveDashboardFailure(state, auth, refusedReadMessage(error));
|
|
221
242
|
return resolveDashboardFailure(state, auth, error instanceof Error ? error.message : fallback);
|
|
222
243
|
}
|
|
223
244
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withReferenceFeedFixture } from "../lib/local-preview-feed-fixture";
|
|
1
2
|
import { resolveNewsDeskTab } from "../lib/newsroom-section-routing";
|
|
2
3
|
import { NewsDeskWorkspace, type NewsDeskTab } from "./topic-steering-workspace";
|
|
3
4
|
import {
|
|
@@ -67,7 +68,7 @@ export async function NewsDeskPage({ section: routeSection, selectionPath, searc
|
|
|
67
68
|
const analysisProfiles = await loadAnalysisProfileSummaries(getBrandAnalysisProfilesPath(brand));
|
|
68
69
|
const configuredCorpora = await loadConfiguredCorpusSummaries(getBrandSteeringConfigPath(brand));
|
|
69
70
|
const dashboard = useDemoDashboard
|
|
70
|
-
? createDemoCategorySteeringDashboard(brand.id, getBrandNewsroomSectionsConfigPath(brand))
|
|
71
|
+
? withReferenceFeedFixture(createDemoCategorySteeringDashboard(brand.id, getBrandNewsroomSectionsConfigPath(brand)))
|
|
71
72
|
: await loadCategorySteeringDashboard();
|
|
72
73
|
return <NewsDeskWorkspace analysisProfiles={analysisProfiles} configuredCorpora={configuredCorpora} dashboard={dashboard} initialSelection={initialSelection} initialTab={initialTab} />;
|
|
73
74
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { loadReferenceReviewDetails } from "../lib/reference-review-details";
|
|
4
|
+
import { hasEditorRights } from "../lib/staff-groups";
|
|
4
5
|
|
|
5
6
|
import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
|
|
6
7
|
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
@@ -838,7 +839,7 @@ export async function loadEditorResolvedAccessState(): Promise<EditorAccessState
|
|
|
838
839
|
if (!refreshed.hasSession || refreshed.auth.status === "signedOut") {
|
|
839
840
|
return { status: "signedOut", isEditor: false, isAdmin: false, auth: refreshed.auth, error: null };
|
|
840
841
|
}
|
|
841
|
-
const isEditor = refreshed.groups
|
|
842
|
+
const isEditor = hasEditorRights(refreshed.groups);
|
|
842
843
|
if (!isEditor) {
|
|
843
844
|
return {
|
|
844
845
|
status: "forbidden",
|
|
@@ -1,44 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { newsroomHref } from "../lib/newsroom-base-path";
|
|
4
3
|
import Link from "next/link";
|
|
5
4
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
6
5
|
import type { NewsDeskShellState } from "../lib/news-desk-session";
|
|
7
6
|
import { cn } from "../lib/utils";
|
|
8
7
|
import { NewsroomOpsShell } from "./newsroom-ops-shell";
|
|
9
|
-
import {
|
|
8
|
+
import { NotificationSkeleton } from "./notification-cards";
|
|
10
9
|
import { ReaderAuthControl } from "./reader-auth-control";
|
|
11
10
|
import { buttonVariants } from "./ui/button";
|
|
11
|
+
import { ReferencesPageLoading } from "./references-page-loading";
|
|
12
12
|
|
|
13
|
-
export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
|
|
13
|
+
export function NewsDeskAccessGate({ initialTab, shell, showSectionTabs = false }: { initialTab?: string; shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
|
|
14
14
|
const accessPhase = shell?.phase ?? "checkingAccess";
|
|
15
15
|
const pending = accessPhase === "checkingAccess" || accessPhase === "loadingDesk";
|
|
16
|
+
const referencesPage = initialTab === "references";
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
19
|
<NewsroomOpsShell
|
|
19
|
-
activeTab="overview"
|
|
20
|
+
activeTab={referencesPage ? "references" : "overview"}
|
|
20
21
|
appTitle={SITE_BRAND.appTitle}
|
|
21
22
|
backHref="/"
|
|
22
23
|
backLabel={SITE_BRAND.backToHomeLabel}
|
|
23
24
|
headerActions={(
|
|
24
|
-
<Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
|
|
25
|
+
<Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "organizer-header-settings")} href="/settings">Settings</Link>
|
|
25
26
|
)}
|
|
26
|
-
pageTitle={showSectionTabs ? "Notification Center" : "Newsroom"}
|
|
27
|
-
showNavigation={showSectionTabs}
|
|
27
|
+
pageTitle={referencesPage ? "References" : showSectionTabs ? "Notification Center" : "Newsroom"}
|
|
28
|
+
showNavigation={showSectionTabs || referencesPage}
|
|
28
29
|
>
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
<div className="h-6 w-48 rounded bg-muted" />
|
|
32
|
-
<div className="h-4 w-3/4 rounded bg-muted" />
|
|
33
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
34
|
-
{Array.from({ length: 4 }, (_, index) => <div className="h-32 rounded-xl border bg-card p-4" key={index}>
|
|
35
|
-
<div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
|
|
36
|
-
</div>)}
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
)) : (
|
|
30
|
+
{/* The loading state is the feed's own skeleton, so what loads replaces it in place. */}
|
|
31
|
+
{pending && referencesPage ? <ReferencesPageLoading /> : pending ? <NotificationSkeleton bare={!showSectionTabs} count={4} /> : (
|
|
40
32
|
<section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
|
|
41
|
-
<div className="space-y-3 rounded-xl
|
|
33
|
+
<div className="space-y-3 rounded-xl bg-card p-6">
|
|
42
34
|
<p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
|
|
43
35
|
<h2 className="m-0 font-sans text-xl font-semibold tracking-tight">{formatAccessTitle(shell)}</h2>
|
|
44
36
|
<p className="m-0 text-sm text-muted-foreground">{formatAccessDetail(shell)}</p>
|
|
@@ -46,7 +38,8 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
|
|
|
46
38
|
<p className="m-0 text-sm text-foreground">{formatAccessActionDetail(shell)}</p>
|
|
47
39
|
{shell?.phase === "signedOut" || shell?.phase === "error" ? (
|
|
48
40
|
<div className="news-desk-access-panel__auth">
|
|
49
|
-
|
|
41
|
+
{/* No fixed return path: sign-in comes back to the page that asked, so a deep link survives it. */}
|
|
42
|
+
<ReaderAuthControl showIdentity />
|
|
50
43
|
</div>
|
|
51
44
|
) : null}
|
|
52
45
|
</div>
|
|
@@ -2,13 +2,14 @@ import { NewsDeskClientProvider } from "./news-desk-client-provider";
|
|
|
2
2
|
import { createDemoCategorySteeringDashboard } from "../lib/category-repository";
|
|
3
3
|
import { resolveDevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
4
4
|
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
5
|
+
import { withReferenceFeedFixture } from "../lib/local-preview-feed-fixture";
|
|
5
6
|
import { getBrandNewsroomSectionsConfigPath, resolveActiveSiteBrand } from "../lib/site-brand-tenant";
|
|
6
7
|
|
|
7
8
|
export function NewsroomClientShell({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
8
9
|
const devSandboxEditorAuth = resolveDevSandboxEditorAuth();
|
|
9
10
|
const activeBrand = resolveActiveSiteBrand();
|
|
10
11
|
const localPreviewDashboard = isLocalPreviewEnabled()
|
|
11
|
-
? createDemoCategorySteeringDashboard(activeBrand.id, getBrandNewsroomSectionsConfigPath(activeBrand))
|
|
12
|
+
? withReferenceFeedFixture(createDemoCategorySteeringDashboard(activeBrand.id, getBrandNewsroomSectionsConfigPath(activeBrand)))
|
|
12
13
|
: null;
|
|
13
14
|
|
|
14
15
|
return (
|
|
@@ -2,18 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
|
-
import { useMemo } from "react";
|
|
6
|
-
import { Badge } from "./ui/badge";
|
|
5
|
+
import { useCallback, useMemo } from "react";
|
|
7
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
8
|
-
import type { AssignmentRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
|
|
7
|
+
import type { AssignmentRecord, NewsroomSectionRecord, ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
|
|
9
8
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
10
|
-
import {
|
|
11
|
-
filterReferencesByStatus,
|
|
12
|
-
formatReferenceListDate,
|
|
13
|
-
referenceDisplayTitle,
|
|
14
|
-
referenceLineageId,
|
|
15
|
-
selectCanonicalReferenceRecords,
|
|
16
|
-
} from "../lib/newsroom-references";
|
|
9
|
+
import { referenceLineageId, selectCanonicalReferenceRecords, type ReferenceCurationAction } from "../lib/newsroom-references";
|
|
17
10
|
import {
|
|
18
11
|
buildNewsroomSectionHref,
|
|
19
12
|
displayNewsroomSectionShortTitle,
|
|
@@ -21,9 +14,15 @@ import {
|
|
|
21
14
|
newsroomDeskKind,
|
|
22
15
|
sortNewsroomSections,
|
|
23
16
|
} from "../lib/newsroom-sections";
|
|
24
|
-
import {
|
|
17
|
+
import { cn } from "../lib/utils";
|
|
18
|
+
import { FEED_SURFACE_PROPS } from "./reference-feed";
|
|
25
19
|
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
20
|
+
import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
21
|
+
import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
22
|
+
import type { NotificationFixture } from "./notification-center";
|
|
26
23
|
import { NotificationCenter } from "./notification-center";
|
|
24
|
+
import type { CorrectCaptureAction } from "./reference-capture-client";
|
|
25
|
+
import type { ReviewChoice } from "./reference-review-card";
|
|
27
26
|
|
|
28
27
|
type NewsroomOpsOverviewProps = {
|
|
29
28
|
assignments: AssignmentRecord[];
|
|
@@ -33,26 +32,57 @@ type NewsroomOpsOverviewProps = {
|
|
|
33
32
|
humanCuratedReferenceLineageIds?: ReadonlySet<string>;
|
|
34
33
|
newsroomSections: NewsroomSectionRecord[];
|
|
35
34
|
references: ReferenceRecord[];
|
|
35
|
+
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
36
|
+
/** True until the References have been read; a saved copy of the last view fills the page meanwhile. */
|
|
37
|
+
loading?: boolean;
|
|
38
|
+
savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
|
|
39
|
+
predictionState?: "loading" | "ready" | "error";
|
|
40
|
+
/** Words for why a rating cannot be saved right now, or null. */
|
|
41
|
+
disabledReason?: string | null;
|
|
42
|
+
onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; note?: string; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
|
|
43
|
+
/** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the capture panel has no edit action. */
|
|
44
|
+
onCorrectCapture?: CorrectCaptureAction;
|
|
45
|
+
/** Local real-data preview of the Notification Center (scripts/export-notification-center-fixture.py). */
|
|
46
|
+
notificationFixture?: NotificationFixture | null;
|
|
36
47
|
};
|
|
37
48
|
|
|
38
49
|
const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
|
|
39
|
-
const NO_IDS: ReadonlySet<string> = new Set();
|
|
40
50
|
|
|
41
51
|
export function NewsroomOpsOverview({
|
|
42
52
|
actorLabel = "",
|
|
43
53
|
assignments,
|
|
44
54
|
decisions = NO_DECISIONS,
|
|
45
55
|
demo = false,
|
|
46
|
-
humanCuratedReferenceLineageIds
|
|
56
|
+
humanCuratedReferenceLineageIds,
|
|
57
|
+
disabledReason = null,
|
|
58
|
+
loading = false,
|
|
47
59
|
newsroomSections,
|
|
60
|
+
notificationFixture = null,
|
|
61
|
+
onCorrectCapture,
|
|
62
|
+
onReview,
|
|
63
|
+
predictionState = "ready",
|
|
64
|
+
referenceAttachments = [],
|
|
48
65
|
references,
|
|
66
|
+
savedCopy = null,
|
|
49
67
|
}: NewsroomOpsOverviewProps) {
|
|
50
68
|
const pathname = usePathname();
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
69
|
+
const liveReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
70
|
+
// Until the live read arrives, a saved copy of the last view fills the page. It is display only.
|
|
71
|
+
const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
|
|
72
|
+
const canonicalReferences = showingSavedCopy ? savedCopy!.references : liveReferences;
|
|
73
|
+
const shownDecisions = useMemo(() => (showingSavedCopy && !decisions.size ? new Map(savedCopy!.decisions) : decisions), [decisions, savedCopy, showingSavedCopy]);
|
|
74
|
+
const previews = useMemo(() => {
|
|
75
|
+
const live = referencePreviewsByLineage(referenceAttachments);
|
|
76
|
+
return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
|
|
77
|
+
}, [referenceAttachments, savedCopy, showingSavedCopy]);
|
|
78
|
+
const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null, note?: string) => {
|
|
79
|
+
const decision = shownDecisions.get(referenceLineageId(reference));
|
|
80
|
+
await onReview?.(reference, choice, {
|
|
81
|
+
reasonCode: choice === "reject" ? reasonCode : null,
|
|
82
|
+
...(note ? { note } : {}),
|
|
83
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
84
|
+
});
|
|
85
|
+
}, [onReview, shownDecisions]);
|
|
56
86
|
const attentionAssignments = useMemo(
|
|
57
87
|
() => assignments
|
|
58
88
|
.filter((assignment) => {
|
|
@@ -68,58 +98,21 @@ export function NewsroomOpsOverview({
|
|
|
68
98
|
);
|
|
69
99
|
const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
|
|
70
100
|
const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
|
|
101
|
+
const rateDisabled = disabledReason ?? (showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null);
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
<div
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
>
|
|
78
|
-
<div className="space-y-8" data-newsroom-overview-feeds>
|
|
79
|
-
<div aria-label="Notification Center feed" className="newsroom-overview-feed-frame rounded-xl border bg-card p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" data-newsroom-overview-notifications role="region" tabIndex={0}>
|
|
80
|
-
<NotificationCenter
|
|
81
|
-
actorLabel={actorLabel}
|
|
82
|
-
decisions={decisions}
|
|
83
|
-
demo={demo}
|
|
84
|
-
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
85
|
-
references={canonicalReferences}
|
|
86
|
-
/>
|
|
87
|
-
</div>
|
|
88
|
-
<section aria-label="Needs attention" className="space-y-3">
|
|
89
|
-
<h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
|
|
90
|
-
{pendingReferences.length || attentionAssignments.length ? (
|
|
103
|
+
const below = (
|
|
104
|
+
<div className="space-y-12 pt-6" data-newsroom-overview-below {...FEED_SURFACE_PROPS}>
|
|
105
|
+
{attentionAssignments.length ? (
|
|
106
|
+
<section aria-label="Open assignments" className="space-y-3">
|
|
107
|
+
<h2 className="text-sm font-medium text-muted-foreground">Open assignments</h2>
|
|
91
108
|
<div className="space-y-2">
|
|
92
|
-
{pendingReferences.map((reference) => {
|
|
93
|
-
const lineageId = referenceLineageId(reference);
|
|
94
|
-
const href = getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
|
|
95
|
-
const active = pathname === href || pathname.startsWith(`${href}/`);
|
|
96
|
-
return (
|
|
97
|
-
<Link
|
|
98
|
-
aria-current={active ? "true" : undefined}
|
|
99
|
-
className={newsroomInboxRowClassName(active)}
|
|
100
|
-
data-newsroom-inbox-item="reference"
|
|
101
|
-
data-newsroom-list-row
|
|
102
|
-
data-selected={active || undefined}
|
|
103
|
-
data-newsroom-card
|
|
104
|
-
data-newsroom-card-id={lineageId}
|
|
105
|
-
href={href}
|
|
106
|
-
key={lineageId}
|
|
107
|
-
>
|
|
108
|
-
<div className="min-w-0 flex-1">
|
|
109
|
-
<p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
|
|
110
|
-
<p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
|
|
111
|
-
</div>
|
|
112
|
-
<Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
|
|
113
|
-
</Link>
|
|
114
|
-
);
|
|
115
|
-
})}
|
|
116
109
|
{attentionAssignments.map((assignment) => {
|
|
117
110
|
const href = getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignment.id)}`, demo);
|
|
118
111
|
const active = pathname === href || pathname.startsWith(`${href}/`);
|
|
119
112
|
return (
|
|
120
113
|
<Link
|
|
121
114
|
aria-current={active ? "true" : undefined}
|
|
122
|
-
className={
|
|
115
|
+
className={cn("flex items-start gap-3 rounded-[16px] bg-[var(--feed-card)] p-3 text-foreground outline-none transition-colors hover:bg-[var(--feed-control)] focus-visible:ring-2 focus-visible:ring-ring", active && "bg-[var(--feed-control)]")}
|
|
123
116
|
data-newsroom-inbox-item="assignment"
|
|
124
117
|
data-newsroom-list-row
|
|
125
118
|
data-selected={active || undefined}
|
|
@@ -130,19 +123,13 @@ export function NewsroomOpsOverview({
|
|
|
130
123
|
<p className="truncate text-sm font-medium text-foreground">{assignment.title}</p>
|
|
131
124
|
<p className="text-xs text-muted-foreground">Assignment · {assignment.assignmentTypeKey}</p>
|
|
132
125
|
</div>
|
|
133
|
-
<
|
|
126
|
+
<span className="mt-0.5 shrink-0 rounded-full bg-[var(--feed-control)] px-2 py-0.5 text-xs font-medium">{assignment.status}</span>
|
|
134
127
|
</Link>
|
|
135
128
|
);
|
|
136
129
|
})}
|
|
137
130
|
</div>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<CardContent className="p-4 text-sm text-muted-foreground">
|
|
141
|
-
No pending references or open assignments. Check those destinations for other work.
|
|
142
|
-
</CardContent>
|
|
143
|
-
</Card>
|
|
144
|
-
)}
|
|
145
|
-
</section>
|
|
131
|
+
</section>
|
|
132
|
+
) : null}
|
|
146
133
|
|
|
147
134
|
{desks.length ? (
|
|
148
135
|
<section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
|
|
@@ -156,7 +143,7 @@ export function NewsroomOpsOverview({
|
|
|
156
143
|
href={buildNewsroomSectionHref(section.id, demo)}
|
|
157
144
|
key={section.id}
|
|
158
145
|
>
|
|
159
|
-
<Card className="h-full transition-colors hover:
|
|
146
|
+
<Card className="h-full rounded-[20px] border-0 bg-[var(--feed-card)] shadow-none ring-0 transition-colors hover:bg-[var(--feed-control)]">
|
|
160
147
|
<CardHeader className="p-4">
|
|
161
148
|
<CardTitle className="text-base font-semibold">{section.title}</CardTitle>
|
|
162
149
|
<CardDescription className="uppercase tracking-wide text-[0.65rem]">
|
|
@@ -176,7 +163,7 @@ export function NewsroomOpsOverview({
|
|
|
176
163
|
href={buildNewsroomSectionHref(section.id, demo)}
|
|
177
164
|
key={section.id}
|
|
178
165
|
>
|
|
179
|
-
<Card className="h-full transition-colors hover:
|
|
166
|
+
<Card className="h-full rounded-[20px] border-0 bg-[var(--feed-card)] shadow-none ring-0 transition-colors hover:bg-[var(--feed-control)]">
|
|
180
167
|
<CardHeader className="p-4">
|
|
181
168
|
<CardTitle className="text-base font-semibold">{section.title}</CardTitle>
|
|
182
169
|
<CardDescription className="uppercase tracking-wide text-[0.65rem]">
|
|
@@ -189,6 +176,34 @@ export function NewsroomOpsOverview({
|
|
|
189
176
|
</div> : null}
|
|
190
177
|
</section>
|
|
191
178
|
) : null}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
className="h-full font-sans"
|
|
185
|
+
data-news-desk-section="overview"
|
|
186
|
+
data-newsroom-ops-home="true"
|
|
187
|
+
>
|
|
188
|
+
<div className="h-full" data-newsroom-overview-feeds>
|
|
189
|
+
<div className="h-full" data-newsroom-overview-notifications>
|
|
190
|
+
<NotificationCenter
|
|
191
|
+
below={below}
|
|
192
|
+
actorLabel={actorLabel}
|
|
193
|
+
decisions={shownDecisions}
|
|
194
|
+
demo={demo}
|
|
195
|
+
fixture={notificationFixture}
|
|
196
|
+
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
197
|
+
onCorrectCapture={onCorrectCapture}
|
|
198
|
+
onRateReference={rateReference}
|
|
199
|
+
referenceAttachments={referenceAttachments}
|
|
200
|
+
predictionLoading={predictionState === "loading"}
|
|
201
|
+
previews={previews}
|
|
202
|
+
referenceDisabledReason={rateDisabled}
|
|
203
|
+
references={canonicalReferences}
|
|
204
|
+
referencesLoading={loading && !canonicalReferences.length}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
192
207
|
</div>
|
|
193
208
|
</div>
|
|
194
209
|
);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { SearchIcon } from "lucide-react";
|
|
4
3
|
import { Button } from "./ui/button";
|
|
5
4
|
import { Separator } from "./ui/separator";
|
|
6
5
|
import { cn } from "../lib/utils";
|
|
@@ -143,6 +142,8 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
|
|
|
143
142
|
media.addEventListener("change", change);
|
|
144
143
|
return () => { document.removeEventListener("focusin", remember); media.removeEventListener("change", change); cancelAnimationFrame(frame); };
|
|
145
144
|
}, []);
|
|
145
|
+
// The mobile top bar prefers the brand's compact mark, then scales the full logo; the serif title is the no-logo fallback.
|
|
146
|
+
const MastheadLogo = SITE_BRAND.components?.NewsroomLogoCompact ?? SITE_BRAND.components?.NewsroomLogo;
|
|
146
147
|
const activePath = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
|
|
147
148
|
const sidebarToggleLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
148
149
|
const sidebarToggle = <button type="button" className="organizer-toggle" title={sidebarToggleLabel}
|
|
@@ -162,10 +163,10 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
|
|
|
162
163
|
<div className="organizer-workspace">
|
|
163
164
|
<header className="organizer-topbar" data-newsroom-ops-topbar>
|
|
164
165
|
{showNavigation && collapsed && sidebarToggle}
|
|
165
|
-
<Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
|
|
166
|
+
<Link className="organizer-masthead" href={backHref} title={backLabel}>{MastheadLogo ? <MastheadLogo className="organizer-masthead-logo" /> : appTitle}</Link>
|
|
166
167
|
<h1 data-newsroom-ops-title>{pageTitle}</h1><OrganizerSearch demo={demo} /><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
167
168
|
</header>
|
|
168
|
-
<main className="organizer-main">{demo && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
|
|
169
|
+
<main className="organizer-main">{demo && !session?.shell.dashboard?.realDataPreview && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
|
|
169
170
|
</div>
|
|
170
171
|
{showNavigation && <NewsroomMobileNavigation activeTab={activeTab} items={ORGANIZER_PRIMARY_NAV.map(item => ({ ...item, href: getNewsroomNavHref(item.path, demo) }))}
|
|
171
172
|
chat={consoleState?.shouldOfferConsole ? { open: consoleState.open, onOpen: consoleState.openConsole } : undefined}
|
|
@@ -177,27 +178,6 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
|
|
|
177
178
|
</div>;
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
export function NewsroomOpsSearchButton({
|
|
181
|
-
disabled,
|
|
182
|
-
onPress,
|
|
183
|
-
}: {
|
|
184
|
-
disabled?: boolean;
|
|
185
|
-
onPress?: () => void;
|
|
186
|
-
}) {
|
|
187
|
-
return (
|
|
188
|
-
<Button
|
|
189
|
-
aria-label="Search knowledge base"
|
|
190
|
-
disabled={disabled}
|
|
191
|
-
onClick={onPress}
|
|
192
|
-
size="icon-sm"
|
|
193
|
-
type="button"
|
|
194
|
-
variant="outline"
|
|
195
|
-
>
|
|
196
|
-
<SearchIcon className="size-4" />
|
|
197
|
-
</Button>
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
181
|
export function NewsroomOpsStatusBanner({
|
|
202
182
|
children,
|
|
203
183
|
tone = "info",
|
|
@@ -208,10 +188,10 @@ export function NewsroomOpsStatusBanner({
|
|
|
208
188
|
return (
|
|
209
189
|
<div
|
|
210
190
|
className={cn(
|
|
211
|
-
|
|
212
|
-
tone === "
|
|
213
|
-
tone === "
|
|
214
|
-
tone === "info" && "
|
|
191
|
+
// A success is announced to screen readers and never drawn: it would push the page down. Errors and notes are flat fills, no outlines.
|
|
192
|
+
tone === "ok" ? "sr-only" : "mb-4 rounded-lg px-3 py-2 text-sm",
|
|
193
|
+
tone === "error" && "bg-destructive/10 text-destructive",
|
|
194
|
+
tone === "info" && "bg-muted/60 text-foreground",
|
|
215
195
|
)}
|
|
216
196
|
role="status"
|
|
217
197
|
>
|