Wire HtmlViewerAdapter into ViewerShell for HTML/Markdown documents
UploadDropzone accepts HTML/MD files; ViewerShell routes by representationType; PendingSelection uses SelectionCapture union.
This commit is contained in:
parent
0cd2ca5d04
commit
589b8356b4
5 changed files with 208 additions and 95 deletions
|
|
@ -1,37 +1,23 @@
|
|||
/**
|
||||
* UploadDropzone — drag-drop + file-picker for uploading PDFs into the
|
||||
* active session.
|
||||
*
|
||||
* On every successful drop:
|
||||
* 1. read each File as bytes,
|
||||
* 2. run the source-layer `ingestPdfFromFile` (mints the blob URL
|
||||
* via the session's `PdfByteStore`),
|
||||
* 3. register the resulting `{document, representation}` with the
|
||||
* engine,
|
||||
* 4. activate the most-recently-uploaded document.
|
||||
*
|
||||
* Failures (non-PDFs, ingest errors) are surfaced inline above the
|
||||
* dropzone; the caller doesn't need a separate toast for them.
|
||||
*
|
||||
* ── Extraction decision (CWORK-WP-0001 T03) ──────────────────────────
|
||||
* This component lives in `citation-work` rather than the umbrella app.
|
||||
* Its only dependencies are `@source` (PDF ingestion) and the local
|
||||
* engine/session hooks — both inside the review-workspace boundary — so
|
||||
* hosting it here lets the package render a complete review shell,
|
||||
* including the upload affordance, without any umbrella-local imports.
|
||||
* `ReviewShell` still exposes an `upload` slot so a consumer may swap in
|
||||
* its own affordance; when the slot is omitted, this default is used.
|
||||
* UploadDropzone — drag-drop + file-picker for uploading documents into the
|
||||
* active session (PDF, HTML, Markdown).
|
||||
*/
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { ingestPdfFromFile } from "@source/index";
|
||||
import {
|
||||
ingestHtmlFromFile,
|
||||
ingestMarkdownFromFile,
|
||||
ingestPdfFromFile,
|
||||
} from "@source/index";
|
||||
import {
|
||||
useActiveDocumentId,
|
||||
useEngine,
|
||||
usePdfByteStore,
|
||||
} from "./EngineContext";
|
||||
|
||||
type UploadFormat = "pdf" | "html" | "markdown" | "unsupported";
|
||||
|
||||
interface UploadEntry {
|
||||
readonly file: File;
|
||||
status: "queued" | "uploading" | "done" | "error";
|
||||
|
|
@ -43,6 +29,22 @@ export interface UploadDropzoneProps {
|
|||
readonly onUploaded?: (documentId: import("@shared/ids").DocumentId) => void;
|
||||
}
|
||||
|
||||
function detectFormat(file: File): UploadFormat {
|
||||
const name = file.name.toLowerCase();
|
||||
if (file.type === "application/pdf" || name.endsWith(".pdf")) return "pdf";
|
||||
if (file.type === "text/html" || name.endsWith(".html") || name.endsWith(".htm")) {
|
||||
return "html";
|
||||
}
|
||||
if (
|
||||
file.type === "text/markdown"
|
||||
|| name.endsWith(".md")
|
||||
|| name.endsWith(".markdown")
|
||||
) {
|
||||
return "markdown";
|
||||
}
|
||||
return "unsupported";
|
||||
}
|
||||
|
||||
export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||
const engine = useEngine();
|
||||
const byteStore = usePdfByteStore();
|
||||
|
|
@ -55,14 +57,15 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
async (files: readonly File[]) => {
|
||||
if (files.length === 0) return;
|
||||
const initial: UploadEntry[] = files.map((file) => {
|
||||
const isPdf =
|
||||
file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
|
||||
if (isPdf) return { file, status: "queued" };
|
||||
return {
|
||||
file,
|
||||
status: "error",
|
||||
error: "Not a PDF (only application/pdf accepted)",
|
||||
};
|
||||
const format = detectFormat(file);
|
||||
if (format === "unsupported") {
|
||||
return {
|
||||
file,
|
||||
status: "error",
|
||||
error: "Unsupported format (PDF, HTML, or Markdown only)",
|
||||
};
|
||||
}
|
||||
return { file, status: "queued" };
|
||||
});
|
||||
setEntries((prev) => [...prev, ...initial]);
|
||||
|
||||
|
|
@ -72,14 +75,16 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
entry.status = "uploading";
|
||||
setEntries((prev) => [...prev]);
|
||||
try {
|
||||
const { document, representation } = await ingestPdfFromFile(
|
||||
entry.file,
|
||||
byteStore,
|
||||
);
|
||||
engine.documents.register({ document, representation });
|
||||
const format = detectFormat(entry.file);
|
||||
const ingested = await (format === "html"
|
||||
? ingestHtmlFromFile(entry.file, byteStore)
|
||||
: format === "markdown"
|
||||
? ingestMarkdownFromFile(entry.file, byteStore)
|
||||
: ingestPdfFromFile(entry.file, byteStore));
|
||||
engine.documents.register(ingested);
|
||||
entry.status = "done";
|
||||
lastDocumentId = document.id;
|
||||
onUploaded?.(document.id);
|
||||
lastDocumentId = ingested.document.id;
|
||||
onUploaded?.(ingested.document.id);
|
||||
} catch (err) {
|
||||
entry.status = "error";
|
||||
entry.error = err instanceof Error ? err.message : String(err);
|
||||
|
|
@ -118,7 +123,6 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files ? Array.from(e.target.files) : [];
|
||||
void processFiles(files);
|
||||
// Reset so the same filename can be picked again.
|
||||
e.target.value = "";
|
||||
},
|
||||
[processFiles],
|
||||
|
|
@ -131,7 +135,7 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
role="region"
|
||||
aria-label="PDF upload"
|
||||
aria-label="Document upload"
|
||||
style={{
|
||||
border: `2px dashed ${isOver ? "#0050b3" : "#bbb"}`,
|
||||
background: isOver ? "#e8f0ff" : "#fafafa",
|
||||
|
|
@ -142,7 +146,7 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<div>Drop PDF files here</div>
|
||||
<div>Drop PDF, HTML, or Markdown files here</div>
|
||||
<div style={{ margin: "6px 0", color: "#888" }}>or</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -156,12 +160,12 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Choose PDF…
|
||||
Choose file…
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/pdf,.pdf"
|
||||
accept="application/pdf,.pdf,text/html,.html,.htm,text/markdown,.md,.markdown"
|
||||
multiple
|
||||
onChange={onPicked}
|
||||
style={{ display: "none" }}
|
||||
|
|
@ -195,4 +199,4 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue