@anthusai/papyrus 5.51.0 → 5.53.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/functions/content-actions/requirements.txt +2 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/components/reference-capture-client.ts +34 -6
- package/components/reference-capture-panel.tsx +104 -26
- package/lib/content-actions-client.ts +3 -1
- package/lib/reference-capture.ts +1 -1
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
10
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34"];
|
|
10
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34", "pypdf>=6.12.1,<7", "youtube-transcript-api>=1.2.4,<2"];
|
|
11
11
|
function installCheckoutRequirements(outputDir) {
|
|
12
12
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
13
13
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -10,7 +10,7 @@ import { pythonLambdaCode, type SiteBundleFile } from "../shared/python-bundle";
|
|
|
10
10
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
12
12
|
|
|
13
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34"];
|
|
13
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34", "pypdf>=6.12.1,<7", "youtube-transcript-api>=1.2.4,<2"];
|
|
14
14
|
|
|
15
15
|
function installCheckoutRequirements(outputDir: string): void {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
captureStatus, countWords, curationCardOf, grobidRows, primaryTextAttachment, structuredRows,
|
|
8
8
|
type CaptureAssessment as Assessment, type CaptureStatus, type CurationCard,
|
|
9
9
|
} from "../lib/reference-capture";
|
|
10
|
-
import { loadModelPayloadsForOwner, loadStoragePathText } from "./news-desk-taxonomy-client";
|
|
10
|
+
import { loadModelPayloadsForOwner, loadStoragePathText, uploadModelPayloadForOwner } from "./news-desk-taxonomy-client";
|
|
11
11
|
|
|
12
12
|
export type CaptureAssessment = Assessment;
|
|
13
13
|
|
|
@@ -23,8 +23,16 @@ export type CaptureView = {
|
|
|
23
23
|
rows: Array<{ label: string; value: string }>;
|
|
24
24
|
/** Saves an editor's pasted or corrected text and their verdict on the capture (the `correctReferenceCapture` action). */
|
|
25
25
|
correct?: (input: { text?: string; assessment: CaptureAssessment; note?: string }) => Promise<void>;
|
|
26
|
+
/** True when the current text was read from a PDF the editor uploaded. */
|
|
27
|
+
fromPdf: boolean;
|
|
28
|
+
/** Stores a PDF the editor downloaded as an attachment of the Reference, then has the server read its text (`correctReferenceCapture` with `pdfAttachmentId`). */
|
|
29
|
+
uploadPdf?: (input: { file: File; assessment: CaptureAssessment; note?: string }, onStep?: (step: "uploading" | "reading") => void) => Promise<void>;
|
|
26
30
|
};
|
|
27
31
|
|
|
32
|
+
/** The server reads PDFs up to this size (ideas.source_text.MAXIMUM_PDF_BYTES); say so before a slow upload that would be refused. */
|
|
33
|
+
export const MAXIMUM_PDF_UPLOAD_BYTES = 8_000_000;
|
|
34
|
+
export const PDF_UPLOAD_ROLE = "curation-upload-pdf";
|
|
35
|
+
|
|
28
36
|
function previewText(attachment: ReferenceAttachmentRecord): string | null {
|
|
29
37
|
// Local preview only: a fixture can carry the captured text inline instead of in storage.
|
|
30
38
|
if (!isLocalPreviewEnabled()) return null;
|
|
@@ -42,7 +50,7 @@ type Payloads = { key: string; text: string | null; grobid: unknown; summary: st
|
|
|
42
50
|
* Reads what the newsroom stored for one Reference. The curation card comes first (status, word count, signals, summary, GROBID fields,
|
|
43
51
|
* and the `curation-text` body its `current` points at); the older `extracted_text` attachment is the fallback.
|
|
44
52
|
*/
|
|
45
|
-
export type CorrectCaptureAction = (input: { referenceId: string; text?: string; assessment: CaptureAssessment; note?: string }) => Promise<unknown>;
|
|
53
|
+
export type CorrectCaptureAction = (input: { referenceId: string; text?: string; pdfAttachmentId?: string; assessment: CaptureAssessment; note?: string }) => Promise<unknown>;
|
|
46
54
|
|
|
47
55
|
export function useReferenceCapture(reference: ReferenceRecord, attachments: readonly ReferenceAttachmentRecord[], teaserLine: number, correctAction?: CorrectCaptureAction): CaptureView {
|
|
48
56
|
const card: CurationCard | null = useMemo(() => curationCardOf(attachments), [attachments]);
|
|
@@ -51,7 +59,7 @@ export function useReferenceCapture(reference: ReferenceRecord, attachments: rea
|
|
|
51
59
|
const grobidSortKey = card?.current?.grobidSortKey ?? card?.grobid?.fieldsSortKey ?? null;
|
|
52
60
|
const [payloads, setPayloads] = useState<Payloads>({ key: "", text: null, grobid: null, summary: null, loaded: false });
|
|
53
61
|
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);
|
|
62
|
+
const [corrected, setCorrected] = useState<{ key: string; text: string; assessment: CaptureAssessment; fromPdf?: boolean } | null>(null);
|
|
55
63
|
const referenceKey = `${reference.id}:${textSortKey ?? ""}:${grobidSortKey ?? ""}`;
|
|
56
64
|
|
|
57
65
|
// The card's bodies and the Reference's metadata.json, in one read of the Reference's ModelAttachments.
|
|
@@ -92,6 +100,7 @@ export function useReferenceCapture(reference: ReferenceRecord, attachments: rea
|
|
|
92
100
|
|
|
93
101
|
const current = payloads.key === referenceKey ? payloads : null;
|
|
94
102
|
const mine = corrected && corrected.key === reference.id ? corrected : null;
|
|
103
|
+
const fromPdf = mine ? mine.fromPdf === true : card?.current?.textVia === "editor_pdf";
|
|
95
104
|
const text = mine?.text ?? (textSortKey ? current?.text ?? null : fallback.key === fallbackKey ? fallback.text : null);
|
|
96
105
|
const loading = mine ? false : textSortKey ? !(current?.loaded) : useFallback && fallback.key === fallbackKey ? fallback.loading : useFallback;
|
|
97
106
|
const error = textSortKey || mine ? null : fallback.key === fallbackKey ? fallback.error : null;
|
|
@@ -103,8 +112,27 @@ export function useReferenceCapture(reference: ReferenceRecord, attachments: rea
|
|
|
103
112
|
|
|
104
113
|
const correct = useCallback(async (input: { text?: string; assessment: CaptureAssessment; note?: string }) => {
|
|
105
114
|
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]);
|
|
115
|
+
setCorrected({ key: reference.id, text: input.text ?? text ?? "", assessment: input.assessment, fromPdf: input.text === undefined && fromPdf });
|
|
116
|
+
}, [reference.id, text, fromPdf, correctAction]);
|
|
117
|
+
|
|
118
|
+
const uploadPdf = useCallback(async (input: { file: File; assessment: CaptureAssessment; note?: string }, onStep?: (step: "uploading" | "reading") => void) => {
|
|
119
|
+
if (!correctAction) throw new Error("Uploading is not available here.");
|
|
120
|
+
if (input.file.size > MAXIMUM_PDF_UPLOAD_BYTES) throw new Error(`That PDF is larger than we read (${MAXIMUM_PDF_UPLOAD_BYTES / 1_000_000} MB). Paste the text instead.`);
|
|
121
|
+
onStep?.("uploading");
|
|
122
|
+
const attachment = await uploadModelPayloadForOwner({
|
|
123
|
+
ownerKind: "reference", ownerId: reference.id, ownerLineageId: reference.lineageId ?? reference.id, ownerVersionNumber: reference.versionNumber ?? null,
|
|
124
|
+
ownerVersionKey: `reference#${reference.id}`, role: PDF_UPLOAD_ROLE, sortKey: `${PDF_UPLOAD_ROLE}-${Date.now().toString(36)}`,
|
|
125
|
+
filename: input.file.name || "upload.pdf", mediaType: "application/pdf", content: input.file,
|
|
126
|
+
});
|
|
127
|
+
onStep?.("reading");
|
|
128
|
+
const result = await correctAction({ referenceId: reference.id, pdfAttachmentId: attachment.id, assessment: input.assessment, ...(input.note ? { note: input.note } : {}) });
|
|
129
|
+
// The server read the text; fetch the new body it stored so the panel shows it.
|
|
130
|
+
const sortKey = (result as { edits?: Array<{ sortKey?: string; kind?: string }> } | null | undefined)?.edits?.find((edit) => edit.kind === "text")?.sortKey;
|
|
131
|
+
const rows = sortKey ? await loadModelPayloadsForOwner("reference", reference.id, ["metadata"]) : [];
|
|
132
|
+
const stored = rows.find((row) => row.attachment.sortKey === sortKey)?.text;
|
|
133
|
+
if (typeof stored !== "string") throw new Error("The PDF was read, but its text could not be loaded. Reload to see it.");
|
|
134
|
+
setCorrected({ key: reference.id, text: stored, assessment: input.assessment, fromPdf: true });
|
|
135
|
+
}, [reference.id, reference.lineageId, reference.versionNumber, correctAction]);
|
|
108
136
|
|
|
109
|
-
return { status, text, loading, error, summary, summaryStale: Boolean(mine) || status.summaryStale === true, rows, correct: correctAction || isLocalPreviewEnabled() ? correct : undefined };
|
|
137
|
+
return { status, text, loading, error, summary, summaryStale: Boolean(mine) || status.summaryStale === true, rows, fromPdf, correct: correctAction || isLocalPreviewEnabled() ? correct : undefined, uploadPdf: correctAction ? uploadPdf : undefined };
|
|
110
138
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { FileCheckIcon, FileClockIcon, FileWarningIcon, FileXIcon, Loader2Icon } from "lucide-react";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
3
|
+
import { FileCheckIcon, FileClockIcon, FileUpIcon, FileWarningIcon, FileXIcon, Loader2Icon } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { captureLine, type CaptureAssessment, type CaptureState, type CaptureStatus } from "../lib/reference-capture";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
7
|
import { AutoGrowTextarea } from "./ui/auto-grow-textarea";
|
|
@@ -42,6 +42,87 @@ const ASSESSMENTS: Array<{ key: CaptureAssessment; label: string; hint: string }
|
|
|
42
42
|
{ key: "incorrect", label: "Incorrect", hint: "This is not the right text." },
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
+
/** The editor's verdict on the machine capture; asked the same way whether they paste text or upload a PDF. */
|
|
46
|
+
function AssessmentChoice({ value, onChange }: { value: CaptureAssessment | null; onChange: (value: CaptureAssessment) => void }) {
|
|
47
|
+
return (
|
|
48
|
+
<div aria-label="Was the capture complete?" className="flex flex-wrap gap-2" role="group">
|
|
49
|
+
{ASSESSMENTS.map((option) => (
|
|
50
|
+
<button
|
|
51
|
+
aria-pressed={value === option.key}
|
|
52
|
+
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", value === option.key ? "bg-[var(--primary)] text-[var(--primary-foreground)]" : "bg-[var(--feed-card)]")}
|
|
53
|
+
data-capture-assessment={option.key}
|
|
54
|
+
key={option.key}
|
|
55
|
+
onClick={() => onChange(option.key)}
|
|
56
|
+
title={option.hint}
|
|
57
|
+
type="button"
|
|
58
|
+
>
|
|
59
|
+
{option.label}
|
|
60
|
+
</button>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** "Upload a PDF": the editor's verdict, then the PDF they downloaded themselves; the server reads its text. */
|
|
67
|
+
function PdfUploadForm({ capture, onDone, onCancel }: { capture: CaptureView; onDone: () => void; onCancel: () => void }) {
|
|
68
|
+
const [assessment, setAssessment] = useState<CaptureAssessment | null>(null);
|
|
69
|
+
const [note, setNote] = useState("");
|
|
70
|
+
const [phase, setPhase] = useState<"idle" | "uploading" | "reading" | "error">("idle");
|
|
71
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
72
|
+
const input = useRef<HTMLInputElement>(null);
|
|
73
|
+
const reason = !assessment ? "Say whether the capture was complete, incomplete or incorrect, then choose the PDF." : null;
|
|
74
|
+
const busy = phase === "uploading" || phase === "reading";
|
|
75
|
+
async function upload(file: File) {
|
|
76
|
+
if (!assessment || !capture.uploadPdf) return;
|
|
77
|
+
setMessage(null);
|
|
78
|
+
try {
|
|
79
|
+
await capture.uploadPdf({ file, assessment, ...(note.trim() ? { note: note.trim() } : {}) }, (step) => setPhase(step));
|
|
80
|
+
onDone();
|
|
81
|
+
} catch (failure) {
|
|
82
|
+
setPhase("error");
|
|
83
|
+
setMessage(failure instanceof Error ? failure.message : "The PDF could not be uploaded.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<div className="space-y-3 rounded-2xl bg-[var(--feed-control)] p-3" data-capture-upload-form>
|
|
88
|
+
<p className="text-sm font-medium">Upload the PDF you downloaded. Its text replaces the capture, and the capture is kept.</p>
|
|
89
|
+
<AssessmentChoice onChange={setAssessment} value={assessment} />
|
|
90
|
+
<label className="block text-sm font-medium">
|
|
91
|
+
Note (optional)
|
|
92
|
+
<AutoGrowTextarea 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} />
|
|
93
|
+
</label>
|
|
94
|
+
<input
|
|
95
|
+
accept="application/pdf"
|
|
96
|
+
aria-label="PDF file"
|
|
97
|
+
className="hidden"
|
|
98
|
+
data-capture-upload-input
|
|
99
|
+
onChange={(event) => {
|
|
100
|
+
const file = event.target.files?.[0];
|
|
101
|
+
event.target.value = "";
|
|
102
|
+
if (file) void upload(file);
|
|
103
|
+
}}
|
|
104
|
+
ref={input}
|
|
105
|
+
type="file"
|
|
106
|
+
/>
|
|
107
|
+
{reason && !busy ? <p className="text-xs text-muted-foreground" data-capture-upload-reason>{reason}</p> : null}
|
|
108
|
+
{busy ? <p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground" data-capture-upload-status role="status"><Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" />{phase === "uploading" ? "Uploading the PDF…" : "Reading the text from the PDF…"}</p> : null}
|
|
109
|
+
{phase === "error" ? <p className="text-xs text-destructive" data-capture-upload-error role="alert">{message}</p> : null}
|
|
110
|
+
<div className="flex gap-2">
|
|
111
|
+
<button
|
|
112
|
+
aria-disabled={reason || busy ? true : undefined}
|
|
113
|
+
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 || busy) && "opacity-60")}
|
|
114
|
+
data-capture-upload-choose
|
|
115
|
+
onClick={() => { if (!reason && !busy) input.current?.click(); }}
|
|
116
|
+
type="button"
|
|
117
|
+
>
|
|
118
|
+
<FileUpIcon aria-hidden="true" className="size-4" />Choose the PDF
|
|
119
|
+
</button>
|
|
120
|
+
<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" disabled={busy} onClick={onCancel} type="button">Cancel</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
45
126
|
/** "Paste or correct the text": an editor's own text and verdict on the capture, saved with `correctReferenceCapture`. */
|
|
46
127
|
function CorrectionForm({ capture, onDone }: { capture: CaptureView; onDone: () => void }) {
|
|
47
128
|
const [text, setText] = useState(capture.text ?? "");
|
|
@@ -77,21 +158,7 @@ function CorrectionForm({ capture, onDone }: { capture: CaptureView; onDone: ()
|
|
|
77
158
|
value={text}
|
|
78
159
|
/>
|
|
79
160
|
</label>
|
|
80
|
-
<
|
|
81
|
-
{ASSESSMENTS.map((option) => (
|
|
82
|
-
<button
|
|
83
|
-
aria-pressed={assessment === option.key}
|
|
84
|
-
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)]")}
|
|
85
|
-
data-capture-assessment={option.key}
|
|
86
|
-
key={option.key}
|
|
87
|
-
onClick={() => setAssessment(option.key)}
|
|
88
|
-
title={option.hint}
|
|
89
|
-
type="button"
|
|
90
|
-
>
|
|
91
|
-
{option.label}
|
|
92
|
-
</button>
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
161
|
+
<AssessmentChoice onChange={setAssessment} value={assessment} />
|
|
95
162
|
<label className="block text-sm font-medium">
|
|
96
163
|
Note (optional)
|
|
97
164
|
<AutoGrowTextarea 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} />
|
|
@@ -123,6 +190,7 @@ export function ReferenceCapturePanel({ capture, showSummary = true, startEditin
|
|
|
123
190
|
}) {
|
|
124
191
|
const [expanded, setExpanded] = useState(false);
|
|
125
192
|
const [editing, setEditing] = useState(startEditing && Boolean(capture.correct));
|
|
193
|
+
const [uploading, setUploading] = useState(false);
|
|
126
194
|
const [savedNote, setSavedNote] = useState(false);
|
|
127
195
|
const line = captureLine(capture.status);
|
|
128
196
|
const Icon = ICON[line.state];
|
|
@@ -145,6 +213,7 @@ export function ReferenceCapturePanel({ capture, showSummary = true, startEditin
|
|
|
145
213
|
{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}
|
|
146
214
|
{capture.error ? <p className="text-xs text-destructive" role="alert">The stored text could not be read. {capture.error}</p> : null}
|
|
147
215
|
{/* While the editor is open it holds the text, so the read-only copy steps aside: the text is never shown twice. */}
|
|
216
|
+
{text && capture.fromPdf ? <p className="text-xs font-medium" data-capture-from-pdf>Text from a PDF you uploaded</p> : null}
|
|
148
217
|
{text && !editing ? (
|
|
149
218
|
<div className="space-y-2">
|
|
150
219
|
{/* No scroll box of its own: on a phone a swipe here scrolls the page. "Show all" grows it in place. */}
|
|
@@ -175,15 +244,24 @@ export function ReferenceCapturePanel({ capture, showSummary = true, startEditin
|
|
|
175
244
|
))}
|
|
176
245
|
</dl>
|
|
177
246
|
) : null}
|
|
178
|
-
{capture.correct ? (
|
|
179
|
-
editing ? <CorrectionForm capture={capture} onDone={() => { setEditing(false); setSavedNote(true); }} />
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
247
|
+
{capture.correct || capture.uploadPdf ? (
|
|
248
|
+
editing && capture.correct ? <CorrectionForm capture={capture} onDone={() => { setEditing(false); setSavedNote(true); }} />
|
|
249
|
+
: uploading && capture.uploadPdf ? <PdfUploadForm capture={capture} onCancel={() => setUploading(false)} onDone={() => { setUploading(false); setSavedNote(true); }} />
|
|
250
|
+
: (
|
|
251
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
252
|
+
{capture.correct ? (
|
|
253
|
+
<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">
|
|
254
|
+
Paste or correct the text
|
|
255
|
+
</button>
|
|
256
|
+
) : null}
|
|
257
|
+
{capture.uploadPdf ? (
|
|
258
|
+
<button className="inline-flex min-h-9 items-center gap-1.5 rounded-full bg-[var(--feed-control)] px-4 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring" data-capture-upload onClick={() => { setSavedNote(false); setUploading(true); }} type="button">
|
|
259
|
+
<FileUpIcon aria-hidden="true" className="size-4" />Upload a PDF
|
|
260
|
+
</button>
|
|
261
|
+
) : null}
|
|
262
|
+
{savedNote ? <span className="text-xs text-muted-foreground" data-capture-correction-saved role="status">Saved. Your verdict trains the completeness check.</span> : null}
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
187
265
|
) : null}
|
|
188
266
|
</section>
|
|
189
267
|
);
|
|
@@ -35,12 +35,14 @@ export type CorrectReferenceCaptureInput = {
|
|
|
35
35
|
referenceId: string;
|
|
36
36
|
/** The full corrected or pasted text. */
|
|
37
37
|
text?: string;
|
|
38
|
+
/** The id of a PDF the editor uploaded to this Reference (a ModelAttachment); the server reads its text. Sent instead of `text`. */
|
|
39
|
+
pdfAttachmentId?: string;
|
|
38
40
|
grobid?: Record<string, unknown>;
|
|
39
41
|
/** Required with text; alone it is just a label on the capture. */
|
|
40
42
|
assessment?: CaptureAssessment;
|
|
41
43
|
note?: string;
|
|
42
44
|
};
|
|
43
|
-
export type CorrectReferenceCaptureResult = { ok: true; referenceId: string; status: string; edits:
|
|
45
|
+
export type CorrectReferenceCaptureResult = { ok: true; referenceId: string; status: string; edits: Array<{ sortKey?: string; via?: string }>; label: unknown; summaryStale: boolean };
|
|
44
46
|
|
|
45
47
|
export class ContentActionFailure extends Error {
|
|
46
48
|
readonly errors: ContentActionError[];
|
package/lib/reference-capture.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type CurationCard = {
|
|
|
23
23
|
editedSignals: Record<string, unknown> | null;
|
|
24
24
|
summary: { status?: string; text?: string; stale?: boolean; reason?: string } | null;
|
|
25
25
|
grobid: { status?: string; reason?: string; title?: string; authorCount?: number; hasAbstract?: boolean; sectionCount?: number; referenceCount?: number; fieldsSortKey?: string } | null;
|
|
26
|
-
current: { textSortKey?: string; grobidSortKey?: string } | null;
|
|
26
|
+
current: { textSortKey?: string; grobidSortKey?: string; textVia?: string } | null;
|
|
27
27
|
labels: Array<{ kind?: string; assessment?: string; labeledAt?: string }>;
|
|
28
28
|
};
|
|
29
29
|
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED