@anthusai/papyrus 2.0.0-next.3 → 2.0.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 +22 -3
- package/amplify/data/resource.ts +24 -3
- package/amplify/data/schema.js +2029 -1967
- package/amplify/data/schema.ts +72 -2
- package/amplify/functions/assignment-action/data-access.js +88 -0
- package/amplify/functions/assignment-action/data-access.ts +87 -0
- package/amplify/functions/content-actions/handler.py +33 -0
- 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 +121 -0
- package/amplify/functions/editorial-checks/schema.ts +123 -0
- package/amplify/functions/editorial-checks/wire.js +53 -0
- package/amplify/functions/editorial-checks/wire.ts +61 -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 +16 -0
- package/amplify/functions/monitoring-view/schema.ts +18 -0
- package/amplify/functions/monitoring-view/wire.js +37 -0
- package/amplify/functions/monitoring-view/wire.ts +47 -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/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/publication/redirect-and-index.js +83 -0
- package/amplify/publication/redirect-rules.js +61 -0
- package/amplify/publication/redirect-rules.ts +79 -0
- package/amplify/publication/s3-projection.js +5 -1
- package/amplify/publication/s3-projection.ts +7 -1
- package/amplify/site-backend-config.ts +17 -0
- package/amplify/site-backend.js +36 -1
- package/amplify/site-backend.ts +44 -1
- 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/bin/papyrus-infra.mjs +20 -1
- 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/monitoring-overview-view.tsx +240 -0
- package/components/monitoring-panel.tsx +40 -0
- package/components/newsroom-article-editor.tsx +136 -11
- package/components/newsroom-board-view.tsx +6 -7
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-ops-shell.tsx +7 -3
- package/components/newsroom-organizer.css +74 -61
- package/components/newsroom-references-view.tsx +14 -14
- 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/publication-cyclotron-panel.tsx +10 -10
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +30 -21
- package/components/reference-cyclotron-priming.tsx +4 -4
- package/components/reference-cyclotron-suggestion.tsx +1 -1
- package/components/reference-priming.tsx +14 -10
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +27 -2
- package/components/use-editorial-checks.ts +133 -0
- package/lib/category-repository.ts +11 -9
- package/lib/curation-routes.ts +342 -0
- 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 +171 -0
- package/lib/editorial-preview.ts +228 -0
- package/lib/monitoring-client.ts +29 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +144 -0
- package/lib/newsroom-board.ts +27 -2
- package/lib/relevance-status.ts +2 -2
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +23 -3
- package/package.json +47 -3
- package/routes.manifest.json +34 -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,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { PrimingPanelProps } from "cyclotron/components/priming-panel";
|
|
5
|
+
import { ReferenceCyclotronPriming, type PrimingFormInput } from "./reference-cyclotron-priming";
|
|
6
|
+
import { ReferencePriming } from "./reference-priming";
|
|
7
|
+
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
8
|
+
import { Button } from "./ui/button";
|
|
9
|
+
|
|
10
|
+
export type ReferenceUrlOption = {
|
|
11
|
+
labels: readonly string[];
|
|
12
|
+
requests: PrimingRequestView[];
|
|
13
|
+
onRequest: (input: PrimingFormInput) => Promise<void> | void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Option = "url" | "file";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The one place an editor adds a reference. "Paste a URL" is always offered when
|
|
20
|
+
* the host can take one. "Import a file" appears only when a curation session
|
|
21
|
+
* (or the host) supplies the import transport, so without one it never renders.
|
|
22
|
+
*/
|
|
23
|
+
export function ReferenceAddEntry({
|
|
24
|
+
url,
|
|
25
|
+
importOptions,
|
|
26
|
+
onImported,
|
|
27
|
+
}: {
|
|
28
|
+
url?: ReferenceUrlOption | null;
|
|
29
|
+
importOptions?: PrimingPanelProps | null;
|
|
30
|
+
/** Called after an import is saved, so the host can reload what depends on it. */
|
|
31
|
+
onImported?: () => void;
|
|
32
|
+
}) {
|
|
33
|
+
const [chosen, setChosen] = useState<Option>("url");
|
|
34
|
+
const [fileOpened, setFileOpened] = useState(false);
|
|
35
|
+
const [saved, setSaved] = useState("");
|
|
36
|
+
if (!url && !importOptions) return null;
|
|
37
|
+
|
|
38
|
+
const option: Option = importOptions && (!url || chosen === "file") ? "file" : "url";
|
|
39
|
+
const choose = (next: Option) => {
|
|
40
|
+
setChosen(next);
|
|
41
|
+
if (next === "file") setFileOpened(true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<section aria-label="Add a reference" className="shrink-0 space-y-3" data-add-reference>
|
|
46
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
47
|
+
<h3 className="font-semibold">Add a reference</h3>
|
|
48
|
+
{url && importOptions ? (
|
|
49
|
+
<div aria-label="How to add a reference" className="flex gap-2" role="group">
|
|
50
|
+
<Button aria-pressed={option === "url"} onClick={() => choose("url")} size="sm" type="button" variant={option === "url" ? "secondary" : "outline"}>Paste a URL</Button>
|
|
51
|
+
<Button aria-pressed={option === "file"} onClick={() => choose("file")} size="sm" type="button" variant={option === "file" ? "secondary" : "outline"}>Import a file</Button>
|
|
52
|
+
</div>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
{url ? (
|
|
56
|
+
<div hidden={option !== "url"}>
|
|
57
|
+
<ReferenceCyclotronPriming labels={url.labels} onRequestPriming={url.onRequest} requests={url.requests} />
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
{importOptions && (fileOpened || !url) ? (
|
|
61
|
+
<div className="max-h-[55dvh] space-y-3 overflow-y-auto pr-1" hidden={option !== "file"}>
|
|
62
|
+
<ReferencePriming
|
|
63
|
+
{...importOptions}
|
|
64
|
+
onAccepted={(draft) => {
|
|
65
|
+
setSaved(`Added ${draft.rows.length} reference${draft.rows.length === 1 ? "" : "s"}.`);
|
|
66
|
+
importOptions.onAccepted?.(draft);
|
|
67
|
+
onImported?.();
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
{saved ? <p role="status">{saved}</p> : null}
|
|
71
|
+
</div>
|
|
72
|
+
) : null}
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -3,21 +3,31 @@
|
|
|
3
3
|
import {useCallback,useEffect,useRef,useState} from 'react';
|
|
4
4
|
import {useNewsroomCuration} from './newsroom-curation-provider';
|
|
5
5
|
import {PublicationCyclotronPanel} from './publication-cyclotron-panel';
|
|
6
|
-
import {
|
|
6
|
+
import type {PrimingPanelProps} from 'cyclotron/components/priming-panel';
|
|
7
7
|
import {Button} from './ui/button';
|
|
8
8
|
import {publicationSteeringPayload,type PublicationHistory,type PublicationStatus} from '../lib/publication-cyclotron';
|
|
9
9
|
import type {CurationSession} from '../lib/curation-client';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** `refreshSignal` changes when references were just added, so the panel reloads by itself. */
|
|
12
|
+
export function ReferenceCurationControls({disabled=false,refreshSignal=0}:{disabled?:boolean;refreshSignal?:number}){
|
|
12
13
|
const session=useNewsroomCuration();
|
|
13
|
-
return session&&!disabled?<Controls key={session.key} session={session} disabled={disabled}/>:null;
|
|
14
|
+
return session&&!disabled?<Controls key={session.key} session={session} disabled={disabled} refreshSignal={refreshSignal}/>:null;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The import option of the Add a reference area, or null. Fail-closed: it exists
|
|
19
|
+
* only when a curation session supplies priming and the person may review.
|
|
20
|
+
*/
|
|
21
|
+
export function useReferenceImport(disabled=false):{panel:PrimingPanelProps;sessionKey:string}|null{
|
|
22
|
+
const session=useNewsroomCuration();
|
|
23
|
+
if(!session||disabled||!session.bootstrap.priming||!session.bootstrap.capabilities.includes('review'))return null;
|
|
24
|
+
return {sessionKey:session.key,panel:{client:session.priming,listId:session.bootstrap.priming.listId,classifiers:session.bootstrap.priming.classifiers}};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Controls({session,disabled,refreshSignal}:{session:CurationSession;disabled:boolean;refreshSignal:number}){
|
|
17
28
|
const [snapshot,setSnapshot]=useState<{status:PublicationStatus;history:PublicationHistory}|null>(null);
|
|
18
29
|
const [error,setError]=useState('');
|
|
19
30
|
const [loading,setLoading]=useState(true);
|
|
20
|
-
const [primingMessage,setPrimingMessage]=useState('');
|
|
21
31
|
const [steering,setSteering]=useState<string|null>(null);
|
|
22
32
|
const [rubric,setRubric]=useState('');
|
|
23
33
|
const [explanation,setExplanation]=useState('');
|
|
@@ -30,14 +40,16 @@ function Controls({session,disabled}:{session:CurationSession;disabled:boolean})
|
|
|
30
40
|
const refresh=useCallback(async()=>{
|
|
31
41
|
setLoading(true);setError('');
|
|
32
42
|
try{setSnapshot(await session.snapshot());}
|
|
33
|
-
catch(failure){setSnapshot(null);setError(failure instanceof Error?failure.message:'
|
|
43
|
+
catch(failure){setSnapshot(null);setError(failure instanceof Error?failure.message:'Suggestions are unavailable right now.');}
|
|
34
44
|
finally{setLoading(false);}
|
|
35
45
|
},[session]);
|
|
36
46
|
useEffect(()=>{
|
|
37
47
|
let active=true;
|
|
38
|
-
void session.snapshot().then(value=>{if(active){setSnapshot(value);setError('');}}).catch(failure=>{if(active){setSnapshot(null);setError(failure instanceof Error?failure.message:'
|
|
48
|
+
void session.snapshot().then(value=>{if(active){setSnapshot(value);setError('');}}).catch(failure=>{if(active){setSnapshot(null);setError(failure instanceof Error?failure.message:'Suggestions are unavailable right now.');}}).finally(()=>{if(active)setLoading(false);});
|
|
39
49
|
return()=>{active=false;};
|
|
40
50
|
},[session]);
|
|
51
|
+
const lastSignal=useRef(refreshSignal);
|
|
52
|
+
useEffect(()=>{if(lastSignal.current!==refreshSignal){lastSignal.current=refreshSignal;void refresh();}},[refreshSignal,refresh]);
|
|
41
53
|
async function steer(action:'evaluate'|'promote'){
|
|
42
54
|
if(!snapshot||!steering||!session.bootstrap.bindingRef)return;
|
|
43
55
|
setBusy(true);setSteeringMessage('');
|
|
@@ -49,29 +61,26 @@ function Controls({session,disabled}:{session:CurationSession;disabled:boolean})
|
|
|
49
61
|
const receipt=await session.publication.steer(payload);
|
|
50
62
|
const result=(receipt.steering as {result?:{trial_fingerprint?:string;improved?:boolean;promoted?:boolean}}|undefined)?.result;
|
|
51
63
|
if(action==='evaluate')setTrial(result?.trial_fingerprint?{fingerprint:result.trial_fingerprint,improved:result.improved===true}:null);
|
|
52
|
-
setSteeringMessage(action==='promote'?'
|
|
64
|
+
setSteeringMessage(action==='promote'?'The tested change is now in use.':result?.improved?'Test finished: the change improved the results.':'Test finished: no improvement was found.');
|
|
53
65
|
await refresh();
|
|
54
|
-
}catch(failure){setSteeringMessage(failure instanceof Error?failure.message:'
|
|
66
|
+
}catch(failure){setSteeringMessage(failure instanceof Error?failure.message:'The result is unknown; check before trying again.');}
|
|
55
67
|
finally{setBusy(false);}
|
|
56
68
|
}
|
|
57
|
-
return <section aria-label="
|
|
58
|
-
<div className="flex items-center justify-between gap-3"><h3 className="font-semibold">
|
|
59
|
-
<p className="text-xs text-muted-foreground">{session.bootstrap.mode==='remote'?'
|
|
60
|
-
{loading?<p role="status">Loading
|
|
69
|
+
return <section aria-label="Suggestions for this publication" className="max-h-[55dvh] min-h-0 shrink-0 space-y-3 overflow-y-auto rounded-md border p-3" data-curation-mode={session.bootstrap.mode}>
|
|
70
|
+
<div className="flex items-center justify-between gap-3"><h3 className="font-semibold">Suggestions for this publication</h3><Button variant="outline" size="sm" disabled={loading||busy} onClick={()=>void refresh()}>Refresh</Button></div>
|
|
71
|
+
<p className="text-xs text-muted-foreground">{session.bootstrap.mode==='remote'?'Shared review service':'This site'} · {session.bootstrap.scope.publicationId}</p>
|
|
72
|
+
{loading?<p role="status">Loading suggestions…</p>:null}
|
|
61
73
|
{error?<p role="alert">{error}</p>:null}
|
|
62
74
|
{snapshot?<PublicationCyclotronPanel mode={session.bootstrap.mode} status={snapshot.status} history={snapshot.history}
|
|
63
75
|
onReview={canReview?async payload=>{await session.publication.review(payload);await refresh();}:undefined}
|
|
64
76
|
onSteer={canSteer&&snapshot.status.features?.steering&&session.bootstrap.bindingRef?classifier=>{setSteering(classifier);setRubric('');setExplanation('');setTrial(null);setSteeringMessage('');}:undefined}/>:null}
|
|
65
|
-
{steering?<form aria-label="
|
|
66
|
-
<h4 className="font-semibold">
|
|
67
|
-
<p className="text-sm">A
|
|
68
|
-
<label className="block text-sm">Proposed
|
|
77
|
+
{steering?<form aria-label="Rule change" className="space-y-3 rounded-md border p-3" onSubmit={event=>{event.preventDefault();void steer('evaluate');}}>
|
|
78
|
+
<h4 className="font-semibold">Test a rule change</h4>
|
|
79
|
+
<p className="text-sm">A change is tested before it is adopted. Opening this form does not change any existing suggestion.</p>
|
|
80
|
+
<label className="block text-sm">Proposed rule<textarea className="mt-1 block min-h-24 w-full rounded border bg-background p-2" required value={rubric} onChange={event=>{setRubric(event.target.value);setTrial(null);}}/></label>
|
|
69
81
|
<label className="block text-sm">Why make this change?<textarea className="mt-1 block min-h-20 w-full rounded border bg-background p-2" required value={explanation} onChange={event=>setExplanation(event.target.value)}/></label>
|
|
70
|
-
<div className="flex flex-wrap gap-2"><Button type="submit" disabled={busy||!canSteer||!rubric.trim()||!explanation.trim()}>
|
|
82
|
+
<div className="flex flex-wrap gap-2"><Button type="submit" disabled={busy||!canSteer||!rubric.trim()||!explanation.trim()}>Test change</Button><Button type="button" variant="outline" disabled={busy||!canSteer||!trial?.improved||!explanation.trim()} onClick={()=>void steer('promote')}>Adopt tested change</Button><Button type="button" variant="ghost" disabled={busy} onClick={()=>setSteering(null)}>Close</Button></div>
|
|
71
83
|
{steeringMessage?<p role="status">{steeringMessage}</p>:null}
|
|
72
84
|
</form>:null}
|
|
73
|
-
{session.bootstrap.priming&&canReview?<ReferencePriming client={session.priming} listId={session.bootstrap.priming.listId} classifiers={session.bootstrap.priming.classifiers}
|
|
74
|
-
onAccepted={draft=>{setPrimingMessage(`Accepted ${draft.rows.length} example item${draft.rows.length===1?'':'s'} for this pinned publication. Operation remains available without priming.`);void refresh();}}/>:null}
|
|
75
|
-
{primingMessage?<p role="status">{primingMessage}</p>:null}
|
|
76
85
|
</section>;
|
|
77
86
|
}
|
|
@@ -6,7 +6,7 @@ import { primingStateText, type PrimingRequestView } from "../lib/relevance-stat
|
|
|
6
6
|
export type PrimingFormInput = { url: string; label: string; explanation: string };
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* The "Paste a URL" option of the Add a reference area. The page is read in the
|
|
10
10
|
* background and the outcome is shown beside each added reference.
|
|
11
11
|
*/
|
|
12
12
|
export function ReferenceCyclotronPriming({
|
|
@@ -36,7 +36,7 @@ export function ReferenceCyclotronPriming({
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
|
-
<
|
|
39
|
+
<div className="space-y-2 text-sm" data-reference-cyclotron-priming>
|
|
40
40
|
<form
|
|
41
41
|
className="flex flex-wrap items-end gap-3"
|
|
42
42
|
onSubmit={(event) => {
|
|
@@ -45,7 +45,7 @@ export function ReferenceCyclotronPriming({
|
|
|
45
45
|
}}
|
|
46
46
|
>
|
|
47
47
|
<label className="flex min-w-64 flex-1 flex-col gap-1">
|
|
48
|
-
<span className="text-xs text-muted-foreground">
|
|
48
|
+
<span className="text-xs text-muted-foreground">Web page address</span>
|
|
49
49
|
<input
|
|
50
50
|
aria-label="Reference URL"
|
|
51
51
|
onChange={(event) => setUrl(event.target.value)}
|
|
@@ -79,6 +79,6 @@ export function ReferenceCyclotronPriming({
|
|
|
79
79
|
))}
|
|
80
80
|
</ul>
|
|
81
81
|
) : null}
|
|
82
|
-
</
|
|
82
|
+
</div>
|
|
83
83
|
);
|
|
84
84
|
}
|
|
@@ -60,7 +60,7 @@ export function ReferenceSuggestionBlock({
|
|
|
60
60
|
<section aria-label="Suggestion" className="space-y-2 border-y border-foreground py-3" data-reference-suggestion={referenceLineageId}>
|
|
61
61
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground">Suggestion</h3>
|
|
62
62
|
<p className="text-sm text-foreground">
|
|
63
|
-
|
|
63
|
+
Papyrus suggests <span className="font-semibold">{decision.label}</span> ({suggestionConfidenceWords(decision)}).
|
|
64
64
|
</p>
|
|
65
65
|
<p className="text-xs text-foreground" data-reference-suggestion-notice>
|
|
66
66
|
A suggestion never changes the status until an editor acts. This reference is {statusLabel.toLowerCase()}.
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
3
|
import { PrimingPanel, type PrimingPanelProps } from "cyclotron/components/priming-panel";
|
|
5
|
-
import { Button } from "./ui/button";
|
|
6
4
|
|
|
7
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The "Import a file" option of the Add a reference area. Papyrus supplies the
|
|
7
|
+
* transport and configuration; Cyclotron owns the preview and label controls.
|
|
8
|
+
* It is only rendered by `ReferenceAddEntry`, never as a section of its own.
|
|
9
|
+
*/
|
|
8
10
|
export function ReferencePriming(props: PrimingPanelProps) {
|
|
9
|
-
const [open, setOpen] = useState(false);
|
|
10
11
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
<PrimingPanel
|
|
13
|
+
{...props}
|
|
14
|
+
renderItem={props.renderItem ?? ((item) => (
|
|
15
|
+
<div className="space-y-2">
|
|
16
|
+
<p className="whitespace-pre-wrap text-sm">{String(item.values.abstract ?? item.values.text ?? "")}</p>
|
|
17
|
+
<p className="break-all text-xs text-muted-foreground">{String(item.values.source_uri ?? "")}</p>
|
|
18
|
+
</div>
|
|
19
|
+
))}
|
|
20
|
+
/>
|
|
17
21
|
);
|
|
18
22
|
}
|
|
@@ -40,7 +40,7 @@ export function SectionSuggestionBlock({
|
|
|
40
40
|
<section aria-label="Suggested Section" className="space-y-2 border-y border-foreground py-3" data-reference-section-suggestion={referenceLineageId}>
|
|
41
41
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground">Suggested Section</h3>
|
|
42
42
|
<p className="text-sm text-foreground">
|
|
43
|
-
|
|
43
|
+
Papyrus suggests <span className="font-semibold">{titleOf(sections, suggestion.sectionId)}</span>
|
|
44
44
|
{suggestion.confidence !== null ? ` (${formatPercentage(suggestion.confidence)})` : ""}.
|
|
45
45
|
</p>
|
|
46
46
|
{others.length ? (
|
|
@@ -13,6 +13,8 @@ import { usePathname, useRouter } from "next/navigation";
|
|
|
13
13
|
import type { ReactNode, RefObject } from "react";
|
|
14
14
|
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
|
|
15
15
|
import amplifyOutputs from "papyrus-amplify-outputs";
|
|
16
|
+
import { EditorialChecksAdminPanel } from "./editorial-checks-admin-panel";
|
|
17
|
+
import { MonitoringPanel } from "./monitoring-panel";
|
|
16
18
|
import { SignupEntriesPanel } from "./signup-entries-panel";
|
|
17
19
|
import { pluginSettings, type PluginOutputs } from "../lib/plugin-settings";
|
|
18
20
|
import { generateClient } from "aws-amplify/data";
|
|
@@ -88,6 +90,7 @@ import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
|
88
90
|
import type { ReaderAuthSnapshot } from "./reader-auth-state";
|
|
89
91
|
import { NewsDeskAccessGate } from "./newsroom-access-gate";
|
|
90
92
|
import { NewsroomOpsOverview } from "./newsroom-ops-overview";
|
|
93
|
+
import { NewsroomCurationBootstrap } from "./newsroom-curation-bootstrap";
|
|
91
94
|
import { NewsroomReferencesView } from "./newsroom-references-view";
|
|
92
95
|
import { NewsroomAssignmentsView } from "./newsroom-assignments-view";
|
|
93
96
|
import { NewsroomTopicsView } from "./newsroom-topics-view";
|
|
@@ -246,7 +249,7 @@ type AssignmentAction = "claim" | "release" | "complete" | "cancel" | "reopen" |
|
|
|
246
249
|
type ReportingPacketReviewDecision = "select" | "merge" | "brief" | "hold" | "kill";
|
|
247
250
|
type AssignmentDeskViewMode = "queue" | "budget";
|
|
248
251
|
type UserRoleAction = "grant" | "revoke";
|
|
249
|
-
type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "delegations" | "signups";
|
|
252
|
+
type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "delegations" | "signups" | "editorial-checks" | "monitoring";
|
|
250
253
|
export type NewsDeskTab = "overview" | "desks" | "topics" | "concepts" | "references" | "messages" | "assignments" | "administration" | "search";
|
|
251
254
|
type LexicalRuleScope = "publication" | "corpus" | "classifier" | "category";
|
|
252
255
|
type AnalysisReindexMode = "online-update" | "classifier-retrain" | "scoped-topic-rebuild" | "entity-graph-rebuild" | "generated-analysis-rebuild";
|
|
@@ -3114,6 +3117,7 @@ function NewsDeskDashboard({
|
|
|
3114
3117
|
/>
|
|
3115
3118
|
) : null}
|
|
3116
3119
|
{activeTab === "references" ? (
|
|
3120
|
+
<NewsroomCurationBootstrap enabled={!dashboard.isDemo}>
|
|
3117
3121
|
<NewsroomReferencesView
|
|
3118
3122
|
demo={Boolean(dashboard.isDemo)}
|
|
3119
3123
|
disabled={controlsDisabled}
|
|
@@ -3151,6 +3155,7 @@ function NewsDeskDashboard({
|
|
|
3151
3155
|
referenceAttachments={referenceAttachments}
|
|
3152
3156
|
references={references}
|
|
3153
3157
|
/>
|
|
3158
|
+
</NewsroomCurationBootstrap>
|
|
3154
3159
|
) : null}
|
|
3155
3160
|
{activeTab === "messages" ? (
|
|
3156
3161
|
<MessagesDeskView
|
|
@@ -4317,6 +4322,24 @@ function AdministrationDeskView({
|
|
|
4317
4322
|
<strong>Agent delegations</strong>
|
|
4318
4323
|
<span>Let an agent curate References for you</span>
|
|
4319
4324
|
</Link>
|
|
4325
|
+
<Link
|
|
4326
|
+
href={administrationPanelHref("editorial-checks", isDemo)}
|
|
4327
|
+
data-news-desk-admin-nav="editorial-checks"
|
|
4328
|
+
data-active={administrationPanel === "editorial-checks" || undefined}
|
|
4329
|
+
onClick={() => onPanelChange("editorial-checks")}
|
|
4330
|
+
>
|
|
4331
|
+
<strong>Editorial checks</strong>
|
|
4332
|
+
<span>Policies the article editor checks drafts against</span>
|
|
4333
|
+
</Link>
|
|
4334
|
+
<Link
|
|
4335
|
+
href={administrationPanelHref("monitoring", isDemo)}
|
|
4336
|
+
data-news-desk-admin-nav="monitoring"
|
|
4337
|
+
data-active={administrationPanel === "monitoring" || undefined}
|
|
4338
|
+
onClick={() => onPanelChange("monitoring")}
|
|
4339
|
+
>
|
|
4340
|
+
<strong>Monitoring</strong>
|
|
4341
|
+
<span>The automated research pipeline, read only</span>
|
|
4342
|
+
</Link>
|
|
4320
4343
|
{signupPluginEnabled() ? (
|
|
4321
4344
|
<Link
|
|
4322
4345
|
href={administrationPanelHref("signups", isDemo)}
|
|
@@ -4331,6 +4354,8 @@ function AdministrationDeskView({
|
|
|
4331
4354
|
</nav>}
|
|
4332
4355
|
<div className="news-desk-settings-panel">
|
|
4333
4356
|
{administrationPanel === "signups" ? <SignupEntriesPanel /> : null}
|
|
4357
|
+
{administrationPanel === "editorial-checks" ? <EditorialChecksAdminPanel /> : null}
|
|
4358
|
+
{administrationPanel === "monitoring" ? <MonitoringPanel isAdmin={canManageUsers} isDemo={isDemo} /> : null}
|
|
4334
4359
|
{administrationPanel === "delegations" ? <AdministrationDelegationsPanel disabled={disabled} /> : null}
|
|
4335
4360
|
{administrationPanel === "users" ? (
|
|
4336
4361
|
<AdministrationUsersPanel
|
|
@@ -12795,7 +12820,7 @@ function signupPluginEnabled(): boolean {
|
|
|
12795
12820
|
}
|
|
12796
12821
|
|
|
12797
12822
|
function normalizeAdministrationPanel(value: string | null | undefined): AdministrationPanel {
|
|
12798
|
-
if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations") return value;
|
|
12823
|
+
if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations" || value === "editorial-checks" || value === "monitoring") return value;
|
|
12799
12824
|
if (value === "signups" && signupPluginEnabled()) return value;
|
|
12800
12825
|
return "users";
|
|
12801
12826
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
feedbackFor,
|
|
6
|
+
publishWarnings,
|
|
7
|
+
type EditorialCheckResult,
|
|
8
|
+
type EditorialFinding,
|
|
9
|
+
type FeedbackDecision,
|
|
10
|
+
type RewriteResult,
|
|
11
|
+
type Span,
|
|
12
|
+
} from "../lib/editorial-checks";
|
|
13
|
+
import type { EditorialChecksBackend } from "../lib/editorial-checks-backend";
|
|
14
|
+
import { DEFAULT_EDITORIAL_SETTINGS, type EditorialSettings } from "../lib/editorial-checks-settings";
|
|
15
|
+
import type { CheckStatus } from "./editorial-findings-panel";
|
|
16
|
+
|
|
17
|
+
const CHECK_PAUSE_MS = 2000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Editorial checks for one article body. A result only applies to the exact body
|
|
21
|
+
* it was computed for: if the editor typed while the check ran, the result is
|
|
22
|
+
* dropped and the next pause checks again.
|
|
23
|
+
*/
|
|
24
|
+
export function useEditorialChecks({
|
|
25
|
+
backend,
|
|
26
|
+
body,
|
|
27
|
+
section,
|
|
28
|
+
itemId,
|
|
29
|
+
}: {
|
|
30
|
+
/** null when there is no backend to check against (a preview). Whether a publication runs checks is its stored setting. */
|
|
31
|
+
backend: EditorialChecksBackend | null;
|
|
32
|
+
body: string | null;
|
|
33
|
+
section: string;
|
|
34
|
+
itemId: string | null;
|
|
35
|
+
}) {
|
|
36
|
+
const available = backend !== null;
|
|
37
|
+
const [settings, setSettings] = useState<EditorialSettings>(DEFAULT_EDITORIAL_SETTINGS);
|
|
38
|
+
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
|
39
|
+
const enabled = available && settings.enabled;
|
|
40
|
+
const [result, setResult] = useState<EditorialCheckResult | null>(null);
|
|
41
|
+
const [checkedBody, setCheckedBody] = useState<string | null>(null);
|
|
42
|
+
const [status, setStatus] = useState<CheckStatus>({ state: "idle" });
|
|
43
|
+
const [dismissed, setDismissed] = useState<Set<string>>(() => new Set());
|
|
44
|
+
const [activeFindingId, setActiveFindingId] = useState<string | null>(null);
|
|
45
|
+
const [selection, setSelection] = useState<Span>({ start: 0, end: 0 });
|
|
46
|
+
const latestBody = useRef(body);
|
|
47
|
+
latestBody.current = body;
|
|
48
|
+
const sequence = useRef(0);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!available) return;
|
|
52
|
+
backend?.loadSettings().then(({ settings: loaded }) => setSettings(loaded)).catch(() => undefined).finally(() => setSettingsLoaded(true));
|
|
53
|
+
}, [available, backend]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled || !itemId) return;
|
|
57
|
+
backend?.dismissedFindingIds(itemId).then(setDismissed).catch(() => undefined);
|
|
58
|
+
}, [backend, enabled, itemId]);
|
|
59
|
+
|
|
60
|
+
const check = useCallback(async () => {
|
|
61
|
+
const text = latestBody.current;
|
|
62
|
+
if (!backend || !enabled || text === null || !text.trim()) return;
|
|
63
|
+
const run = ++sequence.current;
|
|
64
|
+
setStatus({ state: "checking" });
|
|
65
|
+
try {
|
|
66
|
+
const checked = await backend.check(text, section.trim() || null);
|
|
67
|
+
if (run !== sequence.current) return;
|
|
68
|
+
if (latestBody.current !== text) {
|
|
69
|
+
setStatus({ state: "idle" });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setResult(checked);
|
|
73
|
+
setCheckedBody(text);
|
|
74
|
+
setStatus({ state: "idle" });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (run === sequence.current) setStatus({ state: "error", message: error instanceof Error ? error.message : "The check failed." });
|
|
77
|
+
}
|
|
78
|
+
}, [backend, enabled, section]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!enabled || !settingsLoaded || !settings.checkWhileTyping || body === null || body === checkedBody) return;
|
|
82
|
+
const timer = window.setTimeout(() => void check(), CHECK_PAUSE_MS);
|
|
83
|
+
return () => window.clearTimeout(timer);
|
|
84
|
+
}, [body, check, checkedBody, enabled, settings.checkWhileTyping, settingsLoaded]);
|
|
85
|
+
|
|
86
|
+
const record = useCallback(
|
|
87
|
+
(finding: EditorialFinding, decision: FeedbackDecision, reason: string | null = null) => {
|
|
88
|
+
if (!result || checkedBody === null) return;
|
|
89
|
+
void backend?.recordFeedback(feedbackFor(result, checkedBody, finding, decision, itemId, reason)).catch(() => undefined);
|
|
90
|
+
},
|
|
91
|
+
[backend, checkedBody, itemId, result],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const dismiss = useCallback(
|
|
95
|
+
(finding: EditorialFinding, reason: string) => {
|
|
96
|
+
record(finding, "dismissed", reason);
|
|
97
|
+
setDismissed((current) => new Set(current).add(finding.id));
|
|
98
|
+
},
|
|
99
|
+
[record],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const suggest = useCallback(
|
|
103
|
+
(target: { finding: EditorialFinding } | { selection: Span }, note: string | null): Promise<RewriteResult> =>
|
|
104
|
+
backend
|
|
105
|
+
? backend.suggest(latestBody.current ?? "", section.trim() || null, target, note)
|
|
106
|
+
: Promise.reject(new Error("Editorial checks are off.")),
|
|
107
|
+
[backend, section],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const current = enabled && result && checkedBody === body;
|
|
111
|
+
return {
|
|
112
|
+
enabled,
|
|
113
|
+
settings,
|
|
114
|
+
result,
|
|
115
|
+
/** Findings whose spans still match the body on screen. */
|
|
116
|
+
inlineResult: current ? result : null,
|
|
117
|
+
status,
|
|
118
|
+
dismissed,
|
|
119
|
+
activeFindingId,
|
|
120
|
+
setActiveFindingId,
|
|
121
|
+
selection,
|
|
122
|
+
setSelection,
|
|
123
|
+
check,
|
|
124
|
+
suggest,
|
|
125
|
+
dismiss,
|
|
126
|
+
accept: (finding: EditorialFinding) => record(finding, "accepted"),
|
|
127
|
+
fixed: (finding: EditorialFinding) => {
|
|
128
|
+
record(finding, "fixed");
|
|
129
|
+
setDismissed((current) => new Set(current).add(finding.id));
|
|
130
|
+
},
|
|
131
|
+
openWarnings: enabled && settings.publishWarning && result ? publishWarnings(result.findings, dismissed) : [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -200,7 +200,6 @@ export type ProcedureRunRecord = {
|
|
|
200
200
|
newsroomFeedKey?: string | null;
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
const DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-newsroom-sections.yml");
|
|
204
203
|
const NEWSROOM_SECTION_TYPES = new Set<NewsroomSectionType>(["canonical", "floating", "rotating"]);
|
|
205
204
|
type NewsroomSectionSeedRow = Omit<NewsroomSectionRecord, "sortOrder" | "enabled" | "enabledStatus" | "createdAt" | "updatedAt"> & {
|
|
206
205
|
enabled: boolean;
|
|
@@ -690,10 +689,10 @@ export type CategorySteeringDashboard = {
|
|
|
690
689
|
loadError?: string | null;
|
|
691
690
|
};
|
|
692
691
|
|
|
693
|
-
const DEFAULT_ANALYSIS_PROFILES_PATH = path.join(process.cwd(), "corpora", "papyrus-analysis-profiles.yml");
|
|
694
|
-
const DEFAULT_STEERING_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-steering.yml");
|
|
695
692
|
|
|
696
|
-
|
|
693
|
+
/** Analysis profiles the site keeps in a file; a site that configures none (data-driven sites) has none. No default file is looked up. */
|
|
694
|
+
export async function loadAnalysisProfileSummaries(filepath?: string): Promise<AnalysisProfileSummary[]> {
|
|
695
|
+
if (!filepath) return [];
|
|
697
696
|
try {
|
|
698
697
|
const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
|
|
699
698
|
schemaVersion?: number;
|
|
@@ -722,7 +721,9 @@ export async function loadAnalysisProfileSummaries(filepath = DEFAULT_ANALYSIS_P
|
|
|
722
721
|
}
|
|
723
722
|
}
|
|
724
723
|
|
|
725
|
-
|
|
724
|
+
/** Corpora a site lists in a steering file; no configured file means none. No default file is looked up. */
|
|
725
|
+
export async function loadConfiguredCorpusSummaries(filepath?: string): Promise<CategorySteeringCorpus[]> {
|
|
726
|
+
if (!filepath) return [];
|
|
726
727
|
try {
|
|
727
728
|
const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
|
|
728
729
|
corpora?: Array<Record<string, unknown>>;
|
|
@@ -818,7 +819,8 @@ function sortCategorys(categorys: CategorySteeringCategory[]): CategorySteeringC
|
|
|
818
819
|
});
|
|
819
820
|
}
|
|
820
821
|
|
|
821
|
-
function defaultNewsroomSections(importedAt: string, sectionsConfigPath
|
|
822
|
+
function defaultNewsroomSections(importedAt: string, sectionsConfigPath?: string): NewsroomSectionRecord[] {
|
|
823
|
+
if (!sectionsConfigPath) return [];
|
|
822
824
|
return loadNewsroomSectionSeedRows(sectionsConfigPath).map((row, index) => ({
|
|
823
825
|
...row,
|
|
824
826
|
enabledStatus: "enabled",
|
|
@@ -828,7 +830,7 @@ function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAUL
|
|
|
828
830
|
}));
|
|
829
831
|
}
|
|
830
832
|
|
|
831
|
-
function loadNewsroomSectionSeedRows(sectionsConfigPath
|
|
833
|
+
function loadNewsroomSectionSeedRows(sectionsConfigPath: string) {
|
|
832
834
|
const cached = newsroomSectionSeedRowsCache.get(sectionsConfigPath);
|
|
833
835
|
if (cached) return cached;
|
|
834
836
|
const parsed = YAML.parse(fs.readFileSync(sectionsConfigPath, "utf8")) as {
|
|
@@ -883,7 +885,7 @@ function normalizeNewsroomSectionSeedRow(
|
|
|
883
885
|
|
|
884
886
|
export function createDemoCategorySteeringDashboard(
|
|
885
887
|
brandId: SiteBrandId = resolveSiteBrandId(),
|
|
886
|
-
newsroomSectionsConfigPath
|
|
888
|
+
newsroomSectionsConfigPath?: string,
|
|
887
889
|
): CategorySteeringDashboard {
|
|
888
890
|
const importedAt = "2026-05-16T12:00:00.000Z";
|
|
889
891
|
const profile = getNewsroomDemoProfile(brandId);
|
|
@@ -1698,7 +1700,7 @@ export function createDemoCategorySteeringDashboard(
|
|
|
1698
1700
|
question: "Should this candidate source become a reference for this publication?",
|
|
1699
1701
|
reviewControl: "thumbs",
|
|
1700
1702
|
reviewReason: "program",
|
|
1701
|
-
reviewDetail: "Every
|
|
1703
|
+
reviewDetail: "Every suggestion is reviewed while suggestions are getting started.",
|
|
1702
1704
|
version: 1,
|
|
1703
1705
|
}),
|
|
1704
1706
|
},
|