@anthusai/papyrus 2.0.0-next.1 → 2.0.0-next.100
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/resource.js +16 -1
- package/amplify/auth/resource.ts +19 -1
- package/amplify/data/function-handler.js +20 -0
- package/amplify/data/function-handler.ts +40 -0
- package/amplify/data/resource.js +158 -61
- package/amplify/data/resource.ts +165 -61
- package/amplify/data/schema.js +2103 -1979
- package/amplify/data/schema.ts +206 -68
- package/amplify/functions/assignment-action/data-access.js +90 -0
- package/amplify/functions/assignment-action/data-access.ts +89 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/automation-view/handler.py +17 -0
- package/amplify/functions/automation-view/resource.js +24 -0
- package/amplify/functions/automation-view/resource.ts +34 -0
- package/amplify/functions/automation-view/schema.js +26 -0
- package/amplify/functions/automation-view/schema.ts +28 -0
- package/amplify/functions/automation-view/wire.js +56 -0
- package/amplify/functions/automation-view/wire.ts +76 -0
- package/amplify/functions/content-actions/handler.py +70 -2
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/editorial-checks/handler.py +14 -0
- package/amplify/functions/editorial-checks/requirements.txt +1 -0
- package/amplify/functions/editorial-checks/resource.js +43 -0
- package/amplify/functions/editorial-checks/resource.ts +60 -0
- package/amplify/functions/editorial-checks/schema.js +122 -0
- package/amplify/functions/editorial-checks/schema.ts +124 -0
- package/amplify/functions/editorial-checks/wire.js +53 -0
- package/amplify/functions/editorial-checks/wire.ts +61 -0
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +29 -0
- package/amplify/functions/monitoring-view/schema.ts +31 -0
- package/amplify/functions/monitoring-view/wire.js +64 -0
- package/amplify/functions/monitoring-view/wire.ts +81 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
- package/amplify/plugins/newsletter/schema.js +57 -0
- package/amplify/plugins/newsletter/schema.ts +62 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +182 -0
- package/amplify/plugins/reader-steering/config.ts +290 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1567 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/proposals.py +458 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
- package/amplify/plugins/reader-steering/schema.js +229 -0
- package/amplify/plugins/reader-steering/schema.ts +231 -0
- package/amplify/plugins/registry.js +4 -0
- package/amplify/plugins/registry.ts +4 -0
- package/amplify/plugins/signup/schema.js +2 -1
- package/amplify/plugins/signup/schema.ts +2 -1
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.js +19 -0
- package/amplify/site-backend-config.ts +45 -1
- package/amplify/site-backend-names.js +4 -2
- package/amplify/site-backend-names.ts +4 -2
- package/amplify/site-backend.js +51 -4
- package/amplify/site-backend.ts +60 -4
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/chat/[...path]/route.ts +17 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/app/layout.tsx +5 -1
- package/bin/papyrus-infra.mjs +20 -1
- package/components/automation-panel.tsx +49 -0
- package/components/automation-run-now.tsx +97 -0
- package/components/automation-view.tsx +195 -0
- package/components/editorial-checks-admin-panel.tsx +362 -0
- package/components/editorial-finding-card.tsx +162 -0
- package/components/editorial-findings-panel.tsx +91 -0
- package/components/editorial-preview-popover.tsx +103 -0
- package/components/markus-body-editor.tsx +213 -0
- package/components/markus-ir-preview.tsx +147 -6
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +414 -0
- package/components/monitoring-panel.tsx +60 -0
- package/components/news-desk-client-provider.tsx +18 -18
- package/components/news-desk-taxonomy-client.ts +28 -8
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-access-gate.tsx +16 -2
- package/components/newsroom-article-editor.tsx +145 -18
- package/components/newsroom-articles-view.tsx +6 -6
- package/components/newsroom-assignment-detail.tsx +6 -6
- package/components/newsroom-board-view.tsx +14 -10
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-ops-overview.tsx +26 -96
- package/components/newsroom-ops-shell.tsx +9 -5
- package/components/newsroom-organizer.css +90 -61
- package/components/newsroom-overview-skeleton.tsx +25 -0
- package/components/newsroom-references-view.tsx +192 -49
- package/components/newsroom-theme-fonts/broadsheet-green.css +10 -0
- package/components/newsroom-theme-fonts/broadsheet-green.tsx +6 -0
- package/components/newsroom-theme-fonts/newspaper-gothic.css +9 -0
- package/components/newsroom-theme-fonts/newspaper-gothic.tsx +6 -0
- package/components/newsroom-theme-fonts/night-desk.css +10 -0
- package/components/newsroom-theme-fonts/night-desk.tsx +6 -0
- package/components/newsroom-theme-fonts/refined-newsprint.css +11 -0
- package/components/newsroom-theme-fonts/refined-newsprint.tsx +6 -0
- package/components/newsroom-theme.css +39 -0
- package/components/newsroom-theme.tsx +33 -0
- package/components/notification-center.tsx +281 -0
- package/components/papyrus-console-conversation.css +14 -0
- package/components/papyrus-console-conversation.tsx +55 -0
- package/components/papyrus-console-shell.tsx +62 -420
- package/components/papyrus-private-chat-panel.tsx +134 -0
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reader-auth-state.ts +1 -1
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +63 -25
- package/components/reference-cyclotron-status.tsx +4 -4
- package/components/reference-cyclotron-suggestion.tsx +15 -11
- package/components/reference-live-agreement.tsx +10 -10
- package/components/reference-priming.tsx +14 -10
- package/components/reference-relevance-review.tsx +4 -4
- package/components/reference-section-suggestion.tsx +2 -2
- package/components/topic-steering-workspace.tsx +241 -144
- package/components/use-editorial-checks.ts +133 -0
- package/infra/amplify-app-shell.js +96 -16
- package/infra/amplify-app-shell.ts +108 -18
- package/infra/cloud-email-config.js +32 -0
- package/infra/cloud-email-config.ts +39 -0
- package/infra/cloud-email.js +57 -0
- package/infra/cloud-email.test.js +69 -0
- package/infra/cloud-email.test.ts +66 -0
- package/infra/cloud-email.ts +60 -0
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +92 -6
- package/infra/site-config.ts +138 -6
- package/lib/automation-client.ts +54 -0
- package/lib/automation-fixture.ts +93 -0
- package/lib/automation-overview.ts +252 -0
- package/lib/category-repository.ts +14 -12
- package/lib/console-web-context.ts +1 -1
- package/lib/curation-routes.ts +345 -0
- package/lib/cyclotron-suggestions.ts +11 -11
- package/lib/editorial-checks-backend.ts +16 -0
- package/lib/editorial-checks-client.ts +212 -0
- package/lib/editorial-checks-demo.ts +147 -0
- package/lib/editorial-checks-settings.ts +27 -0
- package/lib/editorial-checks.ts +175 -0
- package/lib/editorial-preview.ts +228 -0
- package/lib/monitoring-client.ts +83 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +188 -0
- package/lib/news-desk-session.ts +12 -21
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/newsroom-board.ts +27 -2
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/papyrus-chat-admission.ts +65 -0
- package/lib/papyrus-hosted-chat.ts +217 -0
- package/lib/papyrus-private-chat-client.ts +81 -0
- package/lib/prospectus-session.ts +30 -14
- package/lib/prospectus-sign-in-routes.ts +106 -31
- package/lib/prospectus-site-identity.ts +46 -6
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/lib/reference-review-details.ts +71 -0
- package/lib/reference-review-request.ts +14 -0
- package/lib/relevance-decisions.ts +42 -10
- package/lib/relevance-status.ts +6 -6
- package/lib/review-client.ts +120 -0
- package/lib/review-fixture.ts +45 -0
- package/lib/review-requests.ts +246 -0
- package/lib/semantic-graph.ts +2 -0
- package/lib/shared-chat-admission.ts +182 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +23 -3
- package/lib/site-origin-value.ts +50 -0
- package/lib/site-origin.ts +8 -0
- package/lib/use-reference-review-data.ts +44 -0
- package/middleware.ts +14 -3
- package/package.json +98 -4
- package/routes.manifest.json +227 -2
- package/src/with-papyrus.mjs +1 -1
- 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 +0 -93
- 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 +0 -93
- package/components/newsroom-newsprint-board.css +0 -7
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { Badge } from "./ui/badge";
|
|
6
|
+
import { Button } from "./ui/button";
|
|
7
|
+
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
8
|
+
import {
|
|
9
|
+
cyclotronAutoDecided, cyclotronHeldForReview, formatWhen, isUnread, modeLabel, modeMark, nextText, sectionReviews,
|
|
10
|
+
steeringText, subjectHref, subjectLabel, summarize, summaryText,
|
|
11
|
+
type AnswerInput, type CyclotronEntry, type ReviewRequest,
|
|
12
|
+
} from "../lib/review-requests";
|
|
13
|
+
import type { ReferenceRecord } from "../lib/category-repository";
|
|
14
|
+
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
15
|
+
import { FIXTURE_REVIEW_REQUESTS } from "../lib/review-fixture";
|
|
16
|
+
|
|
17
|
+
export type NotificationLoadState = "loading" | "ready" | "forbidden" | "unavailable";
|
|
18
|
+
|
|
19
|
+
type ViewProps = {
|
|
20
|
+
loadState: NotificationLoadState;
|
|
21
|
+
requests: ReviewRequest[];
|
|
22
|
+
readIds: ReadonlySet<string>;
|
|
23
|
+
held: CyclotronEntry[];
|
|
24
|
+
autoDecided: CyclotronEntry[];
|
|
25
|
+
storyTitles?: Readonly<Record<string, string>>;
|
|
26
|
+
deletedStories?: ReadonlySet<string>;
|
|
27
|
+
demo?: boolean;
|
|
28
|
+
busyId?: string | null;
|
|
29
|
+
error?: string | null;
|
|
30
|
+
onAnswer?: (request: ReviewRequest, input: AnswerInput) => void;
|
|
31
|
+
onMarkRead?: (requestId: string) => void;
|
|
32
|
+
onMarkAllRead?: () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function Entry({ request, unread, demo, busy, titles, deleted, onAnswer, onMarkRead }: {
|
|
36
|
+
request: ReviewRequest; unread: boolean; demo: boolean; busy: boolean; titles: Readonly<Record<string, string>>; deleted: ReadonlySet<string>;
|
|
37
|
+
onAnswer?: ViewProps["onAnswer"]; onMarkRead?: ViewProps["onMarkRead"];
|
|
38
|
+
}) {
|
|
39
|
+
const [note, setNote] = useState("");
|
|
40
|
+
const href = subjectHref(request.subject, demo);
|
|
41
|
+
const open = request.state === "open";
|
|
42
|
+
const steering = steeringText(request);
|
|
43
|
+
const act = (input: AnswerInput) => onAnswer?.(request, { ...input, feedback: input.feedback ?? (note.trim() || null) });
|
|
44
|
+
return (
|
|
45
|
+
<li className="rounded-lg border p-3 space-y-2" data-review-request={request.id} data-review-mode={request.mode} data-review-state={request.state} data-review-unread={unread || undefined}>
|
|
46
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
47
|
+
<Badge variant="outline"><span aria-hidden="true">{modeMark(request.mode)} </span>{modeLabel(request.mode)}</Badge>
|
|
48
|
+
{unread ? <Badge variant="default"><span aria-hidden="true">● </span>New</Badge> : <span className="text-xs text-muted-foreground">Read</span>}
|
|
49
|
+
<strong className="text-sm">{request.reason}</strong>
|
|
50
|
+
</div>
|
|
51
|
+
<p className="text-xs text-muted-foreground">
|
|
52
|
+
Raised by {request.raisedBy} on {formatWhen(request.raisedAt)}
|
|
53
|
+
{" · "}About: {href ? <Link href={href} onClick={() => onMarkRead?.(request.id)}>{subjectLabel(request.subject, titles, deleted)}</Link> : <span>{subjectLabel(request.subject, titles, deleted)}</span>}
|
|
54
|
+
</p>
|
|
55
|
+
{request.ask ? <p className="text-sm">{request.ask}</p> : null}
|
|
56
|
+
{request.links.length ? (
|
|
57
|
+
<ul className="text-xs space-y-0.5" aria-label="Items in this request">
|
|
58
|
+
{request.links.slice(0, 10).map((link, index) => {
|
|
59
|
+
const target = link.url ?? (link.type ? subjectHref({ type: link.type, id: link.id ?? "" }, demo) : null);
|
|
60
|
+
return <li key={`${link.id ?? link.url}-${index}`}>{target ? <Link href={target}>{link.label ?? link.id ?? link.url}</Link> : (link.label ?? link.id)}</li>;
|
|
61
|
+
})}
|
|
62
|
+
{request.links.length > 10 ? <li>and {request.links.length - 10} more</li> : null}
|
|
63
|
+
</ul>
|
|
64
|
+
) : null}
|
|
65
|
+
<p className="text-sm" data-review-next>{nextText(request)}</p>
|
|
66
|
+
{steering ? <p className="text-sm" data-review-steering>{steering}</p> : null}
|
|
67
|
+
{open ? (
|
|
68
|
+
<div className="space-y-2" data-review-controls>
|
|
69
|
+
<label className="block text-xs text-muted-foreground">
|
|
70
|
+
Steering note (optional)
|
|
71
|
+
<textarea
|
|
72
|
+
className="mt-1 block w-full rounded-md border bg-background p-2 text-sm"
|
|
73
|
+
disabled={busy}
|
|
74
|
+
onChange={(event) => setNote(event.target.value)}
|
|
75
|
+
placeholder={request.mode === "flag" ? "Tell the step what to do differently" : "What should change, or what did you want instead?"}
|
|
76
|
+
rows={2}
|
|
77
|
+
value={note}
|
|
78
|
+
/>
|
|
79
|
+
</label>
|
|
80
|
+
<div className="flex flex-wrap gap-2">
|
|
81
|
+
{request.mode === "flag" ? (
|
|
82
|
+
<Button disabled={busy} onClick={() => act({ decision: "acknowledge" })} size="sm" variant="outline">Acknowledge</Button>
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<Button disabled={busy} onClick={() => act({ decision: "approve" })} size="sm" variant="outline">Approve</Button>
|
|
86
|
+
<Button disabled={busy} onClick={() => act({ decision: "reject" })} size="sm" variant="outline">Reject</Button>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
<Button disabled={busy || !note.trim()} onClick={() => act({ decision: "feedback", steer: "adjust" })} size="sm" variant="outline">Steer with this note</Button>
|
|
90
|
+
<Button disabled={busy} onClick={() => act({ decision: "feedback", steer: "not_this_kind" })} size="sm" variant="outline">Not this kind of thing</Button>
|
|
91
|
+
{unread ? <Button disabled={busy} onClick={() => onMarkRead?.(request.id)} size="sm" variant="ghost">Mark read</Button> : null}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
) : unread ? (
|
|
95
|
+
<Button disabled={busy} onClick={() => onMarkRead?.(request.id)} size="sm" variant="ghost">Mark read</Button>
|
|
96
|
+
) : null}
|
|
97
|
+
</li>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function CyclotronLine({ entry, held }: { entry: CyclotronEntry; held: boolean }) {
|
|
102
|
+
const confidence = entry.confidence === null ? "confidence not recorded" : `confidence ${Math.round(entry.confidence * 100)}%`;
|
|
103
|
+
return (
|
|
104
|
+
<li className="rounded-lg border p-3 space-y-1" data-cyclotron-entry={entry.lineageId} data-cyclotron-held={held || undefined}>
|
|
105
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
106
|
+
<Badge variant="outline"><span aria-hidden="true">{held ? "◔" : "✓"} </span>{held ? "Held for review" : "Decided by the system"}</Badge>
|
|
107
|
+
<strong className="text-sm">{entry.title}</strong>
|
|
108
|
+
</div>
|
|
109
|
+
<p className="text-xs text-muted-foreground">
|
|
110
|
+
Classified as {entry.label} · {confidence}{entry.decidedAt ? ` · ${formatWhen(entry.decidedAt)}` : ""}
|
|
111
|
+
{entry.reviewedByPerson ? " · reviewed by a person" : ""}
|
|
112
|
+
</p>
|
|
113
|
+
{held ? <p className="text-sm">{entry.reviewDetail ? `${entry.reviewDetail}. ` : ""}Held for review: it stays pending until someone decides.</p> : null}
|
|
114
|
+
<Link className="text-sm" href={entry.href}>{held ? "Review it in References" : "Open it in References to correct it"}</Link>
|
|
115
|
+
</li>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Section({ id, title, count, children, empty }: { id: string; title: string; count: number; children: React.ReactNode; empty: string }) {
|
|
120
|
+
return (
|
|
121
|
+
<section aria-labelledby={`notification-${id}`} className="space-y-2" data-notification-section={id}>
|
|
122
|
+
<h2 className="text-sm font-medium" id={`notification-${id}`}>{title} ({count})</h2>
|
|
123
|
+
{count ? children : <p className="text-sm text-muted-foreground">{empty}</p>}
|
|
124
|
+
</section>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The CMS console home: what needs a person, what will proceed on its own, and what the system decided. Ignore-safe by design. */
|
|
129
|
+
export function NotificationCenterView({ loadState, requests, readIds, held, autoDecided, demo = false, storyTitles = {}, deletedStories = new Set<string>(), busyId = null, error = null, onAnswer, onMarkRead, onMarkAllRead }: ViewProps) {
|
|
130
|
+
const sections = useMemo(() => sectionReviews(requests), [requests]);
|
|
131
|
+
const unreadCount = requests.filter((request) => isUnread(request, readIds)).length;
|
|
132
|
+
const summary = summarize(sections, held, autoDecided, unreadCount);
|
|
133
|
+
const entry = (request: ReviewRequest) => (
|
|
134
|
+
<Entry busy={busyId === request.id} demo={demo} key={request.id} titles={storyTitles} deleted={deletedStories} onAnswer={onAnswer} onMarkRead={onMarkRead} request={request} unread={isUnread(request, readIds)} />
|
|
135
|
+
);
|
|
136
|
+
if (loadState === "loading") return (
|
|
137
|
+
<section aria-label="Loading Notification Center" className="space-y-4" data-notification-state="loading" role="status">
|
|
138
|
+
<div className="h-6 w-48 rounded bg-muted" />
|
|
139
|
+
<div className="h-4 w-3/4 max-w-xl rounded bg-muted" />
|
|
140
|
+
<div className="h-4 w-2/3 max-w-lg rounded bg-muted" />
|
|
141
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
142
|
+
{Array.from({ length: 4 }, (_, index) => <div className="h-28 rounded-xl border bg-card p-4" key={index}>
|
|
143
|
+
<div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
|
|
144
|
+
</div>)}
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
);
|
|
148
|
+
if (loadState === "forbidden") return <div role="alert" data-notification-state="forbidden"><strong>The Notification Center is for editors and admins</strong></div>;
|
|
149
|
+
if (loadState === "unavailable") return <div role="alert" data-notification-state="unavailable"><strong>Review requests could not be read right now</strong><p>Try again in a minute. The current request state is unknown.</p></div>;
|
|
150
|
+
return (
|
|
151
|
+
<div className="space-y-6" data-notification-center>
|
|
152
|
+
<header className="space-y-1">
|
|
153
|
+
<h1 className="text-lg font-semibold">Notification Center</h1>
|
|
154
|
+
<p className="text-sm" data-notification-summary>{summaryText(summary)}</p>
|
|
155
|
+
<p className="text-xs text-muted-foreground">
|
|
156
|
+
Look, or steer. Anything you ignore keeps going: soft reviews proceed at their deadline and flags never wait. Only a hard gate waits for you.
|
|
157
|
+
</p>
|
|
158
|
+
<p className="text-xs text-muted-foreground" data-notification-unread>
|
|
159
|
+
{unreadCount ? `${unreadCount} unread` : "Nothing unread"}
|
|
160
|
+
{unreadCount ? <Button className="ml-2" onClick={onMarkAllRead} size="sm" variant="ghost">Mark all read</Button> : null}
|
|
161
|
+
</p>
|
|
162
|
+
{error ? <p role="alert" className="text-sm" data-notification-error>{error}</p> : null}
|
|
163
|
+
</header>
|
|
164
|
+
|
|
165
|
+
<Section count={sections.hardGates.length} empty="No hard gates. Nothing is blocked on you." id="hard-gates" title="Waiting for you: hard gates">
|
|
166
|
+
<ul className="space-y-2">{sections.hardGates.map(entry)}</ul>
|
|
167
|
+
</Section>
|
|
168
|
+
<Section count={sections.softReviews.length} empty="No soft reviews are running." id="soft-reviews" title="Proceeding soon: soft reviews, soonest deadline first">
|
|
169
|
+
<ul className="space-y-2">{sections.softReviews.map(entry)}</ul>
|
|
170
|
+
</Section>
|
|
171
|
+
<Section count={held.length} empty="No Cyclotron items are held for review." id="cyclotron" title="Cyclotron items routed to a person (low confidence)">
|
|
172
|
+
<ul className="space-y-2">{held.slice(0, 10).map((item) => <CyclotronLine entry={item} held key={item.lineageId} />)}</ul>
|
|
173
|
+
{held.length > 10 ? <p className="text-sm">and {held.length - 10} more. <Link href={getNewsroomNavHref("/newsroom/references", demo)}>See all in References</Link></p> : null}
|
|
174
|
+
</Section>
|
|
175
|
+
<Section count={sections.flags.length} empty="No flags." id="flags" title="Flags: for your information">
|
|
176
|
+
<ul className="space-y-2">{sections.flags.map(entry)}</ul>
|
|
177
|
+
</Section>
|
|
178
|
+
<Section count={sections.decided.length} empty="Nothing answered or proceeded yet." id="decided" title="Answered, and what became of it">
|
|
179
|
+
<ul className="space-y-2">{sections.decided.slice(0, 20).map(entry)}</ul>
|
|
180
|
+
</Section>
|
|
181
|
+
<details data-notification-section="auto-decided">
|
|
182
|
+
<summary className="cursor-pointer text-sm font-medium">Recently decided by the system ({autoDecided.length})</summary>
|
|
183
|
+
<div className="mt-2 space-y-2">
|
|
184
|
+
<p className="text-xs text-muted-foreground">Read only. The Cyclotron was confident enough to decide these without you. To correct one, open it in References.</p>
|
|
185
|
+
{autoDecided.length ? <ul className="space-y-2">{autoDecided.map((item) => <CyclotronLine entry={item} held={false} key={item.lineageId} />)}</ul> : <p className="text-sm text-muted-foreground">No automatic decisions yet.</p>}
|
|
186
|
+
</div>
|
|
187
|
+
</details>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type ContainerProps = {
|
|
193
|
+
references: ReferenceRecord[];
|
|
194
|
+
decisions: ReadonlyMap<string, RelevanceDecisionView>;
|
|
195
|
+
humanCuratedReferenceLineageIds: ReadonlySet<string>;
|
|
196
|
+
actorLabel: string;
|
|
197
|
+
demo: boolean;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const REFRESH_MILLISECONDS = 60_000;
|
|
201
|
+
|
|
202
|
+
export function NotificationCenter({ references, decisions, humanCuratedReferenceLineageIds, actorLabel, demo }: ContainerProps) {
|
|
203
|
+
const [loadState, setLoadState] = useState<NotificationLoadState>("loading");
|
|
204
|
+
const [requests, setRequests] = useState<ReviewRequest[]>([]);
|
|
205
|
+
const [readIds, setReadIds] = useState<Set<string>>(new Set());
|
|
206
|
+
const [userKey, setUserKey] = useState<string | null>(null);
|
|
207
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
208
|
+
const [error, setError] = useState<string | null>(null);
|
|
209
|
+
const [storyTitles, setStoryTitles] = useState<Record<string, string>>({});
|
|
210
|
+
const [deletedStories, setDeletedStories] = useState<ReadonlySet<string>>(new Set());
|
|
211
|
+
|
|
212
|
+
const refresh = useCallback(async () => {
|
|
213
|
+
if (demo) {
|
|
214
|
+
setRequests(FIXTURE_REVIEW_REQUESTS);
|
|
215
|
+
setReadIds(new Set(["rr-flag-1"]));
|
|
216
|
+
setLoadState("ready");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const { loadReviews } = await import("../lib/review-client");
|
|
221
|
+
const loaded = await loadReviews();
|
|
222
|
+
if (loaded.state !== "ready") {
|
|
223
|
+
setLoadState(loaded.state);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
setRequests(loaded.requests);
|
|
227
|
+
setReadIds(new Set(loaded.readIds));
|
|
228
|
+
setUserKey(loaded.userKey);
|
|
229
|
+
setLoadState("ready");
|
|
230
|
+
const { loadStoryTitles } = await import("../lib/review-client");
|
|
231
|
+
const stories = await loadStoryTitles(loaded.requests.filter((request) => request.subject.type === "story").map((request) => request.subject.id)).catch(() => ({ titles: {}, deleted: [] as string[] }));
|
|
232
|
+
setStoryTitles(stories.titles);
|
|
233
|
+
setDeletedStories(new Set(stories.deleted));
|
|
234
|
+
} catch {
|
|
235
|
+
setLoadState("unavailable");
|
|
236
|
+
}
|
|
237
|
+
}, [demo]);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
void refresh();
|
|
241
|
+
const timer = window.setInterval(() => void refresh(), REFRESH_MILLISECONDS);
|
|
242
|
+
return () => window.clearInterval(timer);
|
|
243
|
+
}, [refresh]);
|
|
244
|
+
|
|
245
|
+
const held = useMemo(() => cyclotronHeldForReview(references, decisions, demo), [references, decisions, demo]);
|
|
246
|
+
const autoDecided = useMemo(() => cyclotronAutoDecided(references, decisions, humanCuratedReferenceLineageIds, demo), [references, decisions, humanCuratedReferenceLineageIds, demo]);
|
|
247
|
+
|
|
248
|
+
const markRead = useCallback(async (requestId: string) => {
|
|
249
|
+
setReadIds((current) => new Set(current).add(requestId));
|
|
250
|
+
if (demo || !userKey) return;
|
|
251
|
+
const { markReviewRead } = await import("../lib/review-client");
|
|
252
|
+
await markReviewRead(userKey, requestId).catch(() => undefined);
|
|
253
|
+
}, [demo, userKey]);
|
|
254
|
+
|
|
255
|
+
const answer = useCallback(async (request: ReviewRequest, input: AnswerInput) => {
|
|
256
|
+
setError(null);
|
|
257
|
+
setBusyId(request.id);
|
|
258
|
+
try {
|
|
259
|
+
if (demo) throw new Error("This is a demo: answers are not saved.");
|
|
260
|
+
const { answerReviewRequest } = await import("../lib/review-client");
|
|
261
|
+
const saved = await answerReviewRequest(request, input, actorLabel);
|
|
262
|
+
setRequests((current) => current.map((entry) => (entry.id === saved.id ? saved : entry)));
|
|
263
|
+
await markRead(request.id);
|
|
264
|
+
} catch (failure) {
|
|
265
|
+
setError(failure instanceof Error ? failure.message : "Could not save the answer.");
|
|
266
|
+
} finally {
|
|
267
|
+
setBusyId(null);
|
|
268
|
+
}
|
|
269
|
+
}, [actorLabel, demo, markRead]);
|
|
270
|
+
|
|
271
|
+
const markAllRead = useCallback(() => {
|
|
272
|
+
for (const request of requests) if (isUnread(request, readIds)) void markRead(request.id);
|
|
273
|
+
}, [markRead, readIds, requests]);
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<NotificationCenterView
|
|
277
|
+
autoDecided={autoDecided} busyId={busyId} storyTitles={storyTitles} deletedStories={deletedStories} demo={demo} error={error} held={held} loadState={loadState}
|
|
278
|
+
onAnswer={answer} onMarkAllRead={markAllRead} onMarkRead={markRead} readIds={readIds} requests={requests}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.papyrus-console-conversation {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex: 1;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
--chatticus-chat-surface: rgb(var(--paper-rgb));
|
|
7
|
+
--chatticus-chat-raised: color-mix(in srgb, rgb(var(--paper-rgb)) 94%, rgb(var(--ink-rgb)));
|
|
8
|
+
--chatticus-chat-foreground: var(--ink);
|
|
9
|
+
--chatticus-chat-accent: var(--ink);
|
|
10
|
+
--chatticus-chat-error: var(--ink);
|
|
11
|
+
--chatticus-chat-action: var(--ink);
|
|
12
|
+
--chatticus-chat-action-text: rgb(var(--paper-rgb));
|
|
13
|
+
}
|
|
14
|
+
.papyrus-console-conversation .papyrus-console__suggestions { padding: 12px 0; }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, type ReactNode } from "react";
|
|
4
|
+
import { ChatRuntime, type ChatMessage, type MessagePresentation } from "chatticus/chat-react";
|
|
5
|
+
|
|
6
|
+
/** Editorial console data adapted to the shared presentation without exposing GraphQL or auth. */
|
|
7
|
+
export type ConsoleConversationMessage = {
|
|
8
|
+
id: string;
|
|
9
|
+
role: string | null | undefined;
|
|
10
|
+
text: string;
|
|
11
|
+
createdAt?: string;
|
|
12
|
+
pending?: boolean;
|
|
13
|
+
failed?: boolean;
|
|
14
|
+
error?: string | null;
|
|
15
|
+
context?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Normalize host records; Papyrus keeps thread ownership, persistence and response state. */
|
|
19
|
+
export function consoleConversationMessages(messages: readonly ConsoleConversationMessage[]): ChatMessage[] {
|
|
20
|
+
return messages.map((message) => ({
|
|
21
|
+
id: message.id,
|
|
22
|
+
role: message.role === "USER" ? "user" : "assistant",
|
|
23
|
+
content: [{ type: "text", text: message.failed ? message.error || "Responder failed" : message.text }],
|
|
24
|
+
createdAt: message.createdAt ? new Date(message.createdAt) : undefined,
|
|
25
|
+
status: message.failed ? { type: "incomplete", reason: "error" } : message.pending ? { type: "running" } : undefined,
|
|
26
|
+
metadata: { custom: {
|
|
27
|
+
authorLabel: message.context ? "Page context" : undefined,
|
|
28
|
+
failed: message.failed,
|
|
29
|
+
isStreaming: message.pending,
|
|
30
|
+
isStreamingShell: message.pending && !message.text,
|
|
31
|
+
createdAt: message.createdAt,
|
|
32
|
+
} },
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Controlled newsroom adapter for the reusable Chatticus transcript and composer. */
|
|
37
|
+
export function PapyrusConsoleConversation({ messages, isSending, isRunning, onSendMessage, presentation, composerActions, emptyState, error }: {
|
|
38
|
+
messages: readonly ConsoleConversationMessage[];
|
|
39
|
+
isSending: boolean;
|
|
40
|
+
isRunning: boolean;
|
|
41
|
+
onSendMessage: (text: string) => Promise<void>;
|
|
42
|
+
presentation?: MessagePresentation;
|
|
43
|
+
composerActions?: ReactNode;
|
|
44
|
+
emptyState?: ReactNode;
|
|
45
|
+
error?: string | null;
|
|
46
|
+
}) {
|
|
47
|
+
const sharedMessages = useMemo(() => consoleConversationMessages(messages), [messages]);
|
|
48
|
+
return <div className="papyrus-console-conversation" data-reply-running={isRunning || undefined}>
|
|
49
|
+
{/* Papyrus routes can accept another editor message while the current reply runs.
|
|
50
|
+
The pending assistant message carries its own running status for display. */}
|
|
51
|
+
<ChatRuntime messages={sharedMessages} isRunning={false} isSendDisabled={isSending}
|
|
52
|
+
onSendMessage={onSendMessage} presentation={presentation}
|
|
53
|
+
threadProps={{ composerPlaceholder: "Write a message...", composerActions, emptyState, streamError: error }} />
|
|
54
|
+
</div>;
|
|
55
|
+
}
|