@anthusai/papyrus 5.44.0 → 5.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/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/content-actions/handler.py +8 -0
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/monitoring-view/schema.js +8 -0
- package/amplify/functions/monitoring-view/schema.ts +8 -0
- package/bin/papyrus-app.mjs +12 -1
- package/components/monitoring-overview-view.tsx +52 -6
- package/components/monitoring-panel.tsx +61 -3
- package/components/news-desk-client-provider.tsx +16 -1
- package/components/news-desk-page.tsx +2 -1
- package/components/newsroom-access-gate.tsx +7 -5
- package/components/newsroom-client-shell.tsx +2 -1
- package/components/newsroom-ops-overview.tsx +63 -60
- package/components/newsroom-ops-shell.tsx +4 -2
- package/components/newsroom-organizer.css +49 -2
- package/components/newsroom-references-view.tsx +296 -23
- package/components/notification-cards.tsx +364 -0
- package/components/notification-center.tsx +319 -331
- package/components/reference-capture-client.ts +110 -0
- package/components/reference-capture-panel.tsx +180 -0
- package/components/reference-feed.tsx +502 -0
- package/components/reference-review-card.tsx +32 -80
- package/components/references-page-loading.tsx +61 -0
- package/components/references-prefetch.ts +52 -0
- package/components/review-rating-grid.tsx +177 -0
- package/components/topic-steering-workspace.tsx +43 -9
- package/components/use-reference-ratings.ts +132 -0
- 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 +16 -2
- package/lib/monitoring-overview.ts +20 -1
- package/lib/monitoring-paging.ts +58 -0
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +1 -1
- package/lib/notification-feed.ts +250 -0
- package/lib/reference-capture.ts +293 -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-drafts.ts +8 -3
- package/lib/site-brand.ts +2 -0
- package/package.json +17 -1
- package/routes.manifest.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import type { ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
|
|
5
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
6
|
+
import {
|
|
7
|
+
captureStatus, countWords, curationCardOf, grobidRows, primaryTextAttachment, structuredRows,
|
|
8
|
+
type CaptureAssessment as Assessment, type CaptureStatus, type CurationCard,
|
|
9
|
+
} from "../lib/reference-capture";
|
|
10
|
+
import { loadModelPayloadsForOwner, loadStoragePathText } from "./news-desk-taxonomy-client";
|
|
11
|
+
|
|
12
|
+
export type CaptureAssessment = Assessment;
|
|
13
|
+
|
|
14
|
+
export type CaptureView = {
|
|
15
|
+
status: CaptureStatus;
|
|
16
|
+
/** The stored text, once read. Null while loading, or when there is none. */
|
|
17
|
+
text: string | null;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
summary: string | null;
|
|
21
|
+
/** True when an editor replaced the text and the summary awaits the next curation run. */
|
|
22
|
+
summaryStale: boolean;
|
|
23
|
+
rows: Array<{ label: string; value: string }>;
|
|
24
|
+
/** Saves an editor's pasted or corrected text and their verdict on the capture (the `correctReferenceCapture` action). */
|
|
25
|
+
correct?: (input: { text?: string; assessment: CaptureAssessment; note?: string }) => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function previewText(attachment: ReferenceAttachmentRecord): string | null {
|
|
29
|
+
// Local preview only: a fixture can carry the captured text inline instead of in storage.
|
|
30
|
+
if (!isLocalPreviewEnabled()) return null;
|
|
31
|
+
const raw = attachment.metadata;
|
|
32
|
+
try {
|
|
33
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
34
|
+
const text = (parsed as { previewText?: unknown } | null)?.previewText;
|
|
35
|
+
return typeof text === "string" ? text : null;
|
|
36
|
+
} catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Payloads = { key: string; text: string | null; grobid: unknown; summary: string | null; loaded: boolean };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads what the newsroom stored for one Reference. The curation card comes first (status, word count, signals, summary, GROBID fields,
|
|
43
|
+
* and the `curation-text` body its `current` points at); the older `extracted_text` attachment is the fallback.
|
|
44
|
+
*/
|
|
45
|
+
export type CorrectCaptureAction = (input: { referenceId: string; text?: string; assessment: CaptureAssessment; note?: string }) => Promise<unknown>;
|
|
46
|
+
|
|
47
|
+
export function useReferenceCapture(reference: ReferenceRecord, attachments: readonly ReferenceAttachmentRecord[], teaserLine: number, correctAction?: CorrectCaptureAction): CaptureView {
|
|
48
|
+
const card: CurationCard | null = useMemo(() => curationCardOf(attachments), [attachments]);
|
|
49
|
+
const primary = useMemo(() => primaryTextAttachment(attachments), [attachments]);
|
|
50
|
+
const textSortKey = card?.current?.textSortKey ?? card?.capture.textSortKey ?? null;
|
|
51
|
+
const grobidSortKey = card?.current?.grobidSortKey ?? card?.grobid?.fieldsSortKey ?? null;
|
|
52
|
+
const [payloads, setPayloads] = useState<Payloads>({ key: "", text: null, grobid: null, summary: null, loaded: false });
|
|
53
|
+
const [fallback, setFallback] = useState<{ key: string; text: string | null; error: string | null; loading: boolean }>({ key: "", text: null, error: null, loading: false });
|
|
54
|
+
const [corrected, setCorrected] = useState<{ key: string; text: string; assessment: CaptureAssessment } | null>(null);
|
|
55
|
+
const referenceKey = `${reference.id}:${textSortKey ?? ""}:${grobidSortKey ?? ""}`;
|
|
56
|
+
|
|
57
|
+
// The card's bodies and the Reference's metadata.json, in one read of the Reference's ModelAttachments.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isLocalPreviewEnabled()) { setPayloads({ key: referenceKey, text: null, grobid: null, summary: null, loaded: true }); return; }
|
|
60
|
+
let active = true;
|
|
61
|
+
void loadModelPayloadsForOwner("reference", reference.id, ["metadata"])
|
|
62
|
+
.then((rows) => {
|
|
63
|
+
if (!active) return;
|
|
64
|
+
const bySortKey = (key: string | null) => (key ? rows.find((row) => row.attachment.sortKey === key) : undefined);
|
|
65
|
+
const metadata = rows.find((row) => row.attachment.sortKey === "metadata")?.json as { summary?: unknown } | null | undefined;
|
|
66
|
+
setPayloads({
|
|
67
|
+
key: referenceKey,
|
|
68
|
+
text: bySortKey(textSortKey)?.text ?? null,
|
|
69
|
+
grobid: bySortKey(grobidSortKey)?.json ?? null,
|
|
70
|
+
summary: typeof metadata?.summary === "string" && metadata.summary.trim() ? metadata.summary.trim() : null,
|
|
71
|
+
loaded: true,
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
.catch(() => { if (active) setPayloads({ key: referenceKey, text: null, grobid: null, summary: null, loaded: true }); });
|
|
75
|
+
return () => { active = false; };
|
|
76
|
+
}, [reference.id, referenceKey, textSortKey, grobidSortKey]);
|
|
77
|
+
|
|
78
|
+
// Fallback: the older extracted_text attachment, when the card has no text of its own.
|
|
79
|
+
const fallbackKey = primary ? `${primary.id}:${primary.storagePath ?? ""}` : "";
|
|
80
|
+
const useFallback = Boolean(primary) && !textSortKey;
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!useFallback || !primary) { setFallback({ key: "", text: null, error: null, loading: false }); return; }
|
|
83
|
+
const inline = previewText(primary);
|
|
84
|
+
if (inline !== null) { setFallback({ key: fallbackKey, text: inline, error: null, loading: false }); return; }
|
|
85
|
+
let active = true;
|
|
86
|
+
setFallback({ key: fallbackKey, text: null, error: null, loading: true });
|
|
87
|
+
void loadStoragePathText(primary.storagePath)
|
|
88
|
+
.then((result) => { if (active) setFallback({ key: fallbackKey, text: result.text, error: result.error, loading: false }); })
|
|
89
|
+
.catch((error) => { if (active) setFallback({ key: fallbackKey, text: null, error: error instanceof Error ? error.message : "Could not read the stored text.", loading: false }); });
|
|
90
|
+
return () => { active = false; };
|
|
91
|
+
}, [primary, fallbackKey, useFallback]);
|
|
92
|
+
|
|
93
|
+
const current = payloads.key === referenceKey ? payloads : null;
|
|
94
|
+
const mine = corrected && corrected.key === reference.id ? corrected : null;
|
|
95
|
+
const text = mine?.text ?? (textSortKey ? current?.text ?? null : fallback.key === fallbackKey ? fallback.text : null);
|
|
96
|
+
const loading = mine ? false : textSortKey ? !(current?.loaded) : useFallback && fallback.key === fallbackKey ? fallback.loading : useFallback;
|
|
97
|
+
const error = textSortKey || mine ? null : fallback.key === fallbackKey ? fallback.error : null;
|
|
98
|
+
const loadedWords = text !== null ? countWords(text) : null;
|
|
99
|
+
const base = captureStatus(attachments, teaserLine, loadedWords);
|
|
100
|
+
const status: CaptureStatus = mine ? { ...base, state: base.state === "none" || base.state === "running" ? (loadedWords !== null && loadedWords < teaserLine ? "thin" : "full") : base.state, words: loadedWords, estimated: false, assessment: mine.assessment, summaryStale: true, ...(loadedWords !== null && loadedWords < teaserLine ? { thinBy: "length" as const } : {}) } : base;
|
|
101
|
+
const summary = card?.summary?.status === "completed" && card.summary.text ? card.summary.text : current?.summary ?? null;
|
|
102
|
+
const rows = card ? grobidRows(current?.grobid, card) : structuredRows(attachments);
|
|
103
|
+
|
|
104
|
+
const correct = useCallback(async (input: { text?: string; assessment: CaptureAssessment; note?: string }) => {
|
|
105
|
+
if (correctAction) await correctAction({ referenceId: reference.id, ...(input.text !== undefined ? { text: input.text } : {}), assessment: input.assessment, ...(input.note ? { note: input.note } : {}) });
|
|
106
|
+
setCorrected({ key: reference.id, text: input.text ?? text ?? "", assessment: input.assessment });
|
|
107
|
+
}, [reference.id, text, correctAction]);
|
|
108
|
+
|
|
109
|
+
return { status, text, loading, error, summary, summaryStale: Boolean(mine) || status.summaryStale === true, rows, correct: correctAction || isLocalPreviewEnabled() ? correct : undefined };
|
|
110
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FileCheckIcon, FileClockIcon, FileWarningIcon, FileXIcon, Loader2Icon } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { captureLine, type CaptureAssessment, type CaptureState, type CaptureStatus } from "../lib/reference-capture";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import type { CaptureView } from "./reference-capture-client";
|
|
8
|
+
|
|
9
|
+
const ICON = { full: FileCheckIcon, thin: FileWarningIcon, none: FileXIcon, running: FileClockIcon } as const;
|
|
10
|
+
/** Colour never stands alone: each state has its own icon and words. */
|
|
11
|
+
const TONE: Record<CaptureState, string> = {
|
|
12
|
+
full: "bg-emerald-600/15 text-emerald-900 dark:bg-emerald-400/15 dark:text-emerald-100",
|
|
13
|
+
thin: "bg-amber-500/20 text-amber-900 dark:bg-amber-300/15 dark:text-amber-100",
|
|
14
|
+
none: "bg-red-600/15 text-red-900 dark:bg-red-400/15 dark:text-red-100",
|
|
15
|
+
running: "bg-[var(--feed-control)] text-foreground",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A small badge for a grid card, so a thin or missing capture shows before the item is opened. It follows the curation status when there is one. */
|
|
19
|
+
export function CaptureBadge({ status }: { status: CaptureStatus }) {
|
|
20
|
+
const Icon = ICON[status.state];
|
|
21
|
+
const text = status.state === "none" ? "No text" : status.state === "running" ? "Capturing" : status.state === "thin" ? (status.words === null || status.thinBy === "signals" ? "Maybe cut off" : `${status.words.toLocaleString("en-US")} words`) : "Full text";
|
|
22
|
+
return (
|
|
23
|
+
<span
|
|
24
|
+
className={cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[0.7rem] font-medium", TONE[status.state])}
|
|
25
|
+
data-capture-badge={status.state}
|
|
26
|
+
data-capture-curation={status.curation ?? undefined}
|
|
27
|
+
data-capture-source={status.source}
|
|
28
|
+
title={captureLine(status).label}
|
|
29
|
+
>
|
|
30
|
+
<Icon aria-hidden="true" className="size-3.5" />
|
|
31
|
+
{text}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PREVIEW_CHARACTERS = 1600;
|
|
37
|
+
const ASSESSMENTS: Array<{ key: CaptureAssessment; label: string; hint: string }> = [
|
|
38
|
+
{ key: "complete", label: "Complete", hint: "This is the whole item." },
|
|
39
|
+
{ key: "incomplete", label: "Incomplete", hint: "Something is missing or cut off." },
|
|
40
|
+
{ key: "incorrect", label: "Incorrect", hint: "This is not the right text." },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** "Paste or correct the text": an editor's own text and verdict on the capture, saved with `correctReferenceCapture`. */
|
|
44
|
+
function CorrectionForm({ capture, onDone }: { capture: CaptureView; onDone: () => void }) {
|
|
45
|
+
const [text, setText] = useState(capture.text ?? "");
|
|
46
|
+
const [assessment, setAssessment] = useState<CaptureAssessment | null>(null);
|
|
47
|
+
const [note, setNote] = useState("");
|
|
48
|
+
const [phase, setPhase] = useState<"idle" | "saving" | "error">("idle");
|
|
49
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
50
|
+
useEffect(() => { setText((current) => current || capture.text || ""); }, [capture.text]);
|
|
51
|
+
const changedText = text.trim() !== (capture.text ?? "").trim() && text.trim().length > 0;
|
|
52
|
+
const reason = !assessment ? "Say whether the capture was complete, incomplete or incorrect to save." : null;
|
|
53
|
+
async function save() {
|
|
54
|
+
if (!assessment || !capture.correct) return;
|
|
55
|
+
setPhase("saving");
|
|
56
|
+
setMessage(null);
|
|
57
|
+
try {
|
|
58
|
+
await capture.correct({ ...(changedText ? { text: text.trim() } : {}), assessment, ...(note.trim() ? { note: note.trim() } : {}) });
|
|
59
|
+
onDone();
|
|
60
|
+
} catch (failure) {
|
|
61
|
+
setPhase("error");
|
|
62
|
+
setMessage(failure instanceof Error ? failure.message : "The correction could not be saved.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-3 rounded-2xl bg-[var(--feed-control)] p-3" data-capture-correction>
|
|
67
|
+
<label className="block text-sm font-medium">
|
|
68
|
+
The text
|
|
69
|
+
<textarea
|
|
70
|
+
aria-label="Captured text"
|
|
71
|
+
className="mt-1 block min-h-40 w-full rounded-2xl bg-[var(--feed-card)] p-3 text-sm font-normal"
|
|
72
|
+
data-capture-correction-text
|
|
73
|
+
onChange={(event) => setText(event.target.value)}
|
|
74
|
+
placeholder="Paste the whole article here, or fix what was captured."
|
|
75
|
+
value={text}
|
|
76
|
+
/>
|
|
77
|
+
</label>
|
|
78
|
+
<div aria-label="Was the capture complete?" className="flex flex-wrap gap-2" role="group">
|
|
79
|
+
{ASSESSMENTS.map((option) => (
|
|
80
|
+
<button
|
|
81
|
+
aria-pressed={assessment === option.key}
|
|
82
|
+
className={cn("inline-flex min-h-9 items-center rounded-full px-3 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring", assessment === option.key ? "bg-[var(--primary)] text-[var(--primary-foreground)]" : "bg-[var(--feed-card)]")}
|
|
83
|
+
data-capture-assessment={option.key}
|
|
84
|
+
key={option.key}
|
|
85
|
+
onClick={() => setAssessment(option.key)}
|
|
86
|
+
title={option.hint}
|
|
87
|
+
type="button"
|
|
88
|
+
>
|
|
89
|
+
{option.label}
|
|
90
|
+
</button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
<label className="block text-sm font-medium">
|
|
94
|
+
Note (optional)
|
|
95
|
+
<textarea aria-label="Note about the capture (optional)" className="mt-1 block min-h-16 w-full rounded-2xl bg-[var(--feed-card)] p-3 text-sm font-normal" onChange={(event) => setNote(event.target.value)} value={note} />
|
|
96
|
+
</label>
|
|
97
|
+
{reason ? <p className="text-xs text-muted-foreground" data-capture-correction-reason>{reason}</p> : null}
|
|
98
|
+
{phase === "error" ? <p className="text-xs text-destructive" role="alert">{message}</p> : null}
|
|
99
|
+
<div className="flex gap-2">
|
|
100
|
+
<button
|
|
101
|
+
aria-disabled={reason ? true : undefined}
|
|
102
|
+
className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full bg-[var(--primary)] px-4 text-sm font-semibold text-[var(--primary-foreground)] outline-none focus-visible:ring-2 focus-visible:ring-ring", reason && "opacity-60")}
|
|
103
|
+
data-capture-correction-save
|
|
104
|
+
onClick={() => { if (!reason && phase !== "saving") void save(); }}
|
|
105
|
+
type="button"
|
|
106
|
+
>
|
|
107
|
+
{phase === "saving" ? <><Loader2Icon aria-hidden="true" className="size-4 animate-spin" />Saving…</> : changedText ? "Save the text and my verdict" : "Save my verdict"}
|
|
108
|
+
</button>
|
|
109
|
+
<button className="inline-flex min-h-9 items-center rounded-full bg-[var(--feed-card)] px-4 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={onDone} type="button">Cancel</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Our captured record of a Reference: what we stored of its content, said plainly, above the live source preview. */
|
|
116
|
+
export function ReferenceCapturePanel({ capture }: { capture: CaptureView }) {
|
|
117
|
+
const [expanded, setExpanded] = useState(false);
|
|
118
|
+
const [editing, setEditing] = useState(false);
|
|
119
|
+
const [savedNote, setSavedNote] = useState(false);
|
|
120
|
+
const line = captureLine(capture.status);
|
|
121
|
+
const Icon = ICON[line.state];
|
|
122
|
+
const text = capture.text ?? "";
|
|
123
|
+
const long = text.length > PREVIEW_CHARACTERS;
|
|
124
|
+
return (
|
|
125
|
+
<section aria-label="Our captured record" className="space-y-3" data-capture-panel={line.state}>
|
|
126
|
+
<h3 className="text-sm font-semibold text-foreground">Our captured record</h3>
|
|
127
|
+
<div className={cn("flex items-start gap-3 rounded-2xl p-3", TONE[line.state])} data-capture-status={line.state} data-capture-source={capture.status.source}>
|
|
128
|
+
<Icon aria-hidden="true" className="mt-0.5 size-5 shrink-0" />
|
|
129
|
+
<div className="min-w-0">
|
|
130
|
+
<p className="text-sm font-semibold" data-capture-label>{line.label}</p>
|
|
131
|
+
{line.state === "full" ? null : <p className="text-xs opacity-90" data-capture-detail>{line.detail}</p>}
|
|
132
|
+
{capture.status.words !== null ? <p className="text-xs font-medium" data-capture-words>{capture.status.estimated ? "about " : ""}{capture.status.words.toLocaleString("en-US")} words</p> : null}
|
|
133
|
+
{line.state === "full" && capture.status.flags?.length ? <p className="text-xs opacity-90" data-capture-flags>{capture.status.flags.join(" ")}</p> : null}
|
|
134
|
+
{capture.status.curation === "degraded" ? <p className="text-xs opacity-90" data-capture-curation-note>Curation finished all but one stage (usually the summary).</p> : null}
|
|
135
|
+
{capture.status.assessment ? <p className="text-xs font-medium" data-capture-assessment-note>An editor marked this capture {capture.status.assessment}.</p> : null}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
{capture.loading ? <p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground" role="status"><Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" />Reading the stored text…</p> : null}
|
|
139
|
+
{capture.error ? <p className="text-xs text-destructive" role="alert">The stored text could not be read. {capture.error}</p> : null}
|
|
140
|
+
{text ? (
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
<div className="max-h-80 overflow-y-auto whitespace-pre-wrap rounded-2xl bg-[var(--feed-control)] p-3 text-sm leading-relaxed" data-capture-text tabIndex={0}>
|
|
143
|
+
{expanded || !long ? text : `${text.slice(0, PREVIEW_CHARACTERS)}…`}
|
|
144
|
+
</div>
|
|
145
|
+
{long ? (
|
|
146
|
+
<button className="rounded-full bg-[var(--feed-control)] px-3 py-1.5 text-xs font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring" data-capture-toggle onClick={() => setExpanded((value) => !value)} type="button">
|
|
147
|
+
{expanded ? "Show less" : "Show all of the captured text"}
|
|
148
|
+
</button>
|
|
149
|
+
) : null}
|
|
150
|
+
</div>
|
|
151
|
+
) : null}
|
|
152
|
+
{capture.summary ? (
|
|
153
|
+
<div className="space-y-1" data-capture-summary>
|
|
154
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Summary{capture.summaryStale ? " (out of date: the text was replaced)" : ""}</h4>
|
|
155
|
+
<p className="text-sm">{capture.summary}</p>
|
|
156
|
+
</div>
|
|
157
|
+
) : capture.summaryStale ? <p className="text-xs text-muted-foreground" data-capture-summary-stale>The summary will be rewritten from the new text on the next curation run.</p> : null}
|
|
158
|
+
{capture.rows.length ? (
|
|
159
|
+
<dl className="grid gap-2 rounded-2xl bg-[var(--feed-control)] p-3 text-sm" data-capture-structured>
|
|
160
|
+
{capture.rows.map((row) => (
|
|
161
|
+
<div key={`${row.label}:${row.value}`}>
|
|
162
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{row.label}</dt>
|
|
163
|
+
<dd className="mt-0.5">{row.value}</dd>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</dl>
|
|
167
|
+
) : null}
|
|
168
|
+
{capture.correct ? (
|
|
169
|
+
editing ? <CorrectionForm capture={capture} onDone={() => { setEditing(false); setSavedNote(true); }} /> : (
|
|
170
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
171
|
+
<button className="inline-flex min-h-9 items-center rounded-full bg-[var(--feed-control)] px-4 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring" data-capture-correct onClick={() => { setSavedNote(false); setEditing(true); }} type="button">
|
|
172
|
+
Paste or correct the text
|
|
173
|
+
</button>
|
|
174
|
+
{savedNote ? <span className="text-xs text-muted-foreground" data-capture-correction-saved role="status">Saved. Your verdict trains the completeness check.</span> : null}
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
) : null}
|
|
178
|
+
</section>
|
|
179
|
+
);
|
|
180
|
+
}
|