mirror of
https://github.com/willchen96/mike.git
synced 2026-07-02 22:01:00 +02:00
Add courtlistener intergration, liquid glass redesign, UI improvements, version control, various fixes
This commit is contained in:
parent
d39f5806e5
commit
44e868eb42
106 changed files with 16350 additions and 7753 deletions
|
|
@ -6,7 +6,12 @@ import {
|
|||
attachActiveVersionPaths,
|
||||
attachLatestVersionNumbers,
|
||||
} from "../lib/documentVersions";
|
||||
import { downloadFile, uploadFile, storageKey } from "../lib/storage";
|
||||
import {
|
||||
deleteFile,
|
||||
downloadFile,
|
||||
uploadFile,
|
||||
storageKey,
|
||||
} from "../lib/storage";
|
||||
import { docxToPdf, convertedPdfKey } from "../lib/convert";
|
||||
import { checkProjectAccess } from "../lib/access";
|
||||
import { singleFileUpload } from "../lib/upload";
|
||||
|
|
@ -367,6 +372,10 @@ projectsRouter.post(
|
|||
.single();
|
||||
if (!doc)
|
||||
return void res.status(404).json({ detail: "Document not found" });
|
||||
await attachActiveVersionPaths(
|
||||
db,
|
||||
[doc as { id: string; current_version_id?: string | null }],
|
||||
);
|
||||
|
||||
// Already in this project — idempotent
|
||||
if (doc.project_id === projectId) return void res.json(doc);
|
||||
|
|
@ -381,22 +390,49 @@ projectsRouter.post(
|
|||
.single();
|
||||
if (error || !updated)
|
||||
return void res.status(500).json({ detail: "Failed to update document" });
|
||||
await attachActiveVersionPaths(
|
||||
db,
|
||||
[updated as { id: string; current_version_id?: string | null }],
|
||||
);
|
||||
return void res.json(updated);
|
||||
} else {
|
||||
// Belongs to another project → duplicate record AND copy the
|
||||
// underlying storage objects so each project's copy is fully
|
||||
// independent (edits/version bumps on one don't leak into the
|
||||
// other).
|
||||
if (!doc.current_version_id) {
|
||||
return void res
|
||||
.status(404)
|
||||
.json({ detail: "Source document has no active version" });
|
||||
}
|
||||
|
||||
const { data: srcV } = await db
|
||||
.from("document_versions")
|
||||
.select(
|
||||
"storage_path, pdf_storage_path, version_number, filename, source, file_type, size_bytes, page_count",
|
||||
)
|
||||
.eq("id", doc.current_version_id)
|
||||
.single();
|
||||
if (!srcV?.storage_path) {
|
||||
return void res
|
||||
.status(404)
|
||||
.json({ detail: "Source document has no active version" });
|
||||
}
|
||||
|
||||
const activeVersionFilename =
|
||||
(srcV.filename as string | null)?.trim() || "Untitled document";
|
||||
const srcBytes = await downloadFile(srcV.storage_path);
|
||||
if (!srcBytes) {
|
||||
return void res
|
||||
.status(500)
|
||||
.json({ detail: "Failed to read source document bytes" });
|
||||
}
|
||||
|
||||
const { data: copy, error } = await db
|
||||
.from("documents")
|
||||
.insert({
|
||||
project_id: projectId,
|
||||
user_id: userId,
|
||||
filename: doc.filename,
|
||||
file_type: doc.file_type,
|
||||
size_bytes: doc.size_bytes,
|
||||
page_count: doc.page_count,
|
||||
structure_tree: doc.structure_tree,
|
||||
status: doc.status,
|
||||
})
|
||||
.select("*")
|
||||
|
|
@ -404,69 +440,90 @@ projectsRouter.post(
|
|||
if (error || !copy)
|
||||
return void res.status(500).json({ detail: "Failed to copy document" });
|
||||
|
||||
let copyVersionRowId: string | null = null;
|
||||
if (doc.current_version_id) {
|
||||
const { data: srcV } = await db
|
||||
.from("document_versions")
|
||||
.select(
|
||||
"storage_path, pdf_storage_path, version_number, display_name, source",
|
||||
)
|
||||
.eq("id", doc.current_version_id)
|
||||
.single();
|
||||
if (srcV?.storage_path) {
|
||||
const srcBytes = await downloadFile(srcV.storage_path);
|
||||
if (!srcBytes) {
|
||||
return void res
|
||||
.status(500)
|
||||
.json({ detail: "Failed to read source document bytes" });
|
||||
}
|
||||
const newKey = storageKey(userId, copy.id as string, doc.filename);
|
||||
const contentType =
|
||||
doc.file_type === "pdf"
|
||||
? "application/pdf"
|
||||
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
await uploadFile(newKey, srcBytes, contentType);
|
||||
const newKey = storageKey(
|
||||
userId,
|
||||
copy.id as string,
|
||||
activeVersionFilename,
|
||||
);
|
||||
let newPdfPath: string | null = null;
|
||||
try {
|
||||
const contentType =
|
||||
((srcV.file_type as string | null) ?? doc.file_type) === "pdf"
|
||||
? "application/pdf"
|
||||
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
await uploadFile(newKey, srcBytes, contentType);
|
||||
|
||||
// PDFs share one object for source + display rendition. DOCX
|
||||
// store the converted PDF at a separate `converted-pdfs/` key —
|
||||
// copy that too if it exists so the copy renders without going
|
||||
// back through libreoffice.
|
||||
let newPdfPath: string | null = null;
|
||||
if (srcV.pdf_storage_path) {
|
||||
if (srcV.pdf_storage_path === srcV.storage_path) {
|
||||
newPdfPath = newKey;
|
||||
} else {
|
||||
const pdfBytes = await downloadFile(srcV.pdf_storage_path);
|
||||
if (pdfBytes) {
|
||||
const newPdfKey = convertedPdfKey(userId, copy.id as string);
|
||||
await uploadFile(newPdfKey, pdfBytes, "application/pdf");
|
||||
newPdfPath = newPdfKey;
|
||||
}
|
||||
// PDFs share one object for source + display rendition. DOCX
|
||||
// store the converted PDF at a separate `converted-pdfs/` key —
|
||||
// copy that too if it exists so the copy renders without going
|
||||
// back through libreoffice.
|
||||
if (srcV.pdf_storage_path) {
|
||||
if (srcV.pdf_storage_path === srcV.storage_path) {
|
||||
newPdfPath = newKey;
|
||||
} else {
|
||||
const pdfBytes = await downloadFile(srcV.pdf_storage_path);
|
||||
if (pdfBytes) {
|
||||
const newPdfKey = convertedPdfKey(userId, copy.id as string);
|
||||
await uploadFile(newPdfKey, pdfBytes, "application/pdf");
|
||||
newPdfPath = newPdfKey;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: newV } = await db
|
||||
.from("document_versions")
|
||||
.insert({
|
||||
document_id: copy.id,
|
||||
storage_path: newKey,
|
||||
pdf_storage_path: newPdfPath,
|
||||
source: (srcV.source as string | null) ?? "upload",
|
||||
version_number: srcV.version_number ?? 1,
|
||||
display_name: srcV.display_name ?? doc.filename,
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
copyVersionRowId = (newV?.id as string | null) ?? null;
|
||||
if (copyVersionRowId) {
|
||||
await db
|
||||
.from("documents")
|
||||
.update({ current_version_id: copyVersionRowId })
|
||||
.eq("id", copy.id);
|
||||
}
|
||||
}
|
||||
|
||||
const { data: newV, error: newVError } = await db
|
||||
.from("document_versions")
|
||||
.insert({
|
||||
document_id: copy.id,
|
||||
storage_path: newKey,
|
||||
pdf_storage_path: newPdfPath,
|
||||
source: (srcV.source as string | null) ?? "upload",
|
||||
version_number: srcV.version_number ?? 1,
|
||||
filename: activeVersionFilename,
|
||||
file_type: (srcV.file_type as string | null) ?? doc.file_type,
|
||||
size_bytes:
|
||||
(srcV.size_bytes as number | null) ?? doc.size_bytes ?? null,
|
||||
page_count:
|
||||
(srcV.page_count as number | null) ?? doc.page_count ?? null,
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
const copyVersionRowId = (newV?.id as string | null) ?? null;
|
||||
if (newVError || !copyVersionRowId) {
|
||||
throw new Error(
|
||||
`Failed to create copied document version: ${newVError?.message ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { data: updatedCopy, error: updateCopyError } = await db
|
||||
.from("documents")
|
||||
.update({
|
||||
current_version_id: copyVersionRowId,
|
||||
})
|
||||
.eq("id", copy.id)
|
||||
.select("*")
|
||||
.single();
|
||||
if (updateCopyError || !updatedCopy) {
|
||||
throw new Error(
|
||||
`Failed to activate copied document version: ${updateCopyError?.message ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
await attachActiveVersionPaths(
|
||||
db,
|
||||
[updatedCopy as { id: string; current_version_id?: string | null }],
|
||||
);
|
||||
return void res.status(201).json(updatedCopy);
|
||||
} catch (err) {
|
||||
console.error("[projects/documents/copy] failed", err);
|
||||
await Promise.all([
|
||||
deleteFile(newKey).catch(() => {}),
|
||||
newPdfPath && newPdfPath !== newKey
|
||||
? deleteFile(newPdfPath).catch(() => {})
|
||||
: Promise.resolve(),
|
||||
db.from("documents").delete().eq("id", copy.id),
|
||||
]);
|
||||
return void res.status(500).json({ detail: "Failed to copy document" });
|
||||
}
|
||||
return void res.status(201).json(copy);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -484,20 +541,33 @@ projectsRouter.patch("/:projectId/documents/:documentId", requireAuth, async (re
|
|||
|
||||
const { data: doc } = await db
|
||||
.from("documents")
|
||||
.select("id, filename, current_version_id")
|
||||
.select("id, current_version_id")
|
||||
.eq("id", documentId)
|
||||
.eq("project_id", projectId)
|
||||
.single();
|
||||
if (!doc)
|
||||
return void res.status(404).json({ detail: "Document not found" });
|
||||
|
||||
const filename = normalizeDocumentFilename(req.body?.filename, doc.filename as string);
|
||||
const active = doc.current_version_id
|
||||
? await db
|
||||
.from("document_versions")
|
||||
.select("filename")
|
||||
.eq("id", doc.current_version_id)
|
||||
.eq("document_id", documentId)
|
||||
.single()
|
||||
: null;
|
||||
const currentName =
|
||||
typeof active?.data?.filename === "string" &&
|
||||
active.data.filename.trim()
|
||||
? active.data.filename.trim()
|
||||
: "Untitled document";
|
||||
const filename = normalizeDocumentFilename(req.body?.filename, currentName);
|
||||
if (!filename)
|
||||
return void res.status(400).json({ detail: "filename is required" });
|
||||
|
||||
const { data: updated, error } = await db
|
||||
.from("documents")
|
||||
.update({ filename, updated_at: new Date().toISOString() })
|
||||
.update({ updated_at: new Date().toISOString() })
|
||||
.eq("id", documentId)
|
||||
.eq("project_id", projectId)
|
||||
.select("*")
|
||||
|
|
@ -508,12 +578,15 @@ projectsRouter.patch("/:projectId/documents/:documentId", requireAuth, async (re
|
|||
if (doc.current_version_id) {
|
||||
await db
|
||||
.from("document_versions")
|
||||
.update({ display_name: filename })
|
||||
.update({ filename })
|
||||
.eq("id", doc.current_version_id)
|
||||
.eq("document_id", documentId);
|
||||
}
|
||||
|
||||
res.json(updated);
|
||||
res.json({
|
||||
...updated,
|
||||
filename,
|
||||
});
|
||||
});
|
||||
|
||||
// POST /projects/:projectId/documents
|
||||
|
|
@ -714,9 +787,6 @@ export async function handleDocumentUpload(
|
|||
.insert({
|
||||
project_id: projectId,
|
||||
user_id: userId,
|
||||
filename,
|
||||
file_type: suffix,
|
||||
size_bytes: content.byteLength,
|
||||
status: "processing",
|
||||
})
|
||||
.select("*")
|
||||
|
|
@ -747,7 +817,6 @@ export async function handleDocumentUpload(
|
|||
content.byteOffset,
|
||||
content.byteOffset + content.byteLength,
|
||||
) as ArrayBuffer;
|
||||
const tree = await extractStructureTree(rawBuf, suffix, filename);
|
||||
const pageCount = suffix === "pdf" ? await countPdfPages(rawBuf) : null;
|
||||
|
||||
// Convert DOCX/DOC → PDF for display. PDFs are their own rendition.
|
||||
|
|
@ -785,7 +854,10 @@ export async function handleDocumentUpload(
|
|||
pdf_storage_path: pdfStoragePath,
|
||||
source: "upload",
|
||||
version_number: 1,
|
||||
display_name: filename,
|
||||
filename,
|
||||
file_type: suffix,
|
||||
size_bytes: content.byteLength,
|
||||
page_count: pageCount,
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
|
|
@ -799,9 +871,6 @@ export async function handleDocumentUpload(
|
|||
.from("documents")
|
||||
.update({
|
||||
current_version_id: versionRow.id,
|
||||
size_bytes: content.byteLength,
|
||||
page_count: pageCount,
|
||||
structure_tree: tree ?? null,
|
||||
status: "ready",
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
|
|
@ -813,10 +882,15 @@ export async function handleDocumentUpload(
|
|||
.eq("id", docId)
|
||||
.single();
|
||||
const responseDoc = updated
|
||||
? {
|
||||
? {
|
||||
...updated,
|
||||
filename,
|
||||
storage_path: key,
|
||||
pdf_storage_path: pdfStoragePath,
|
||||
file_type: suffix,
|
||||
size_bytes: content.byteLength,
|
||||
page_count: pageCount,
|
||||
active_version_number: 1,
|
||||
}
|
||||
: updated;
|
||||
return void res.status(201).json(responseDoc);
|
||||
|
|
@ -843,63 +917,3 @@ async function countPdfPages(buf: ArrayBuffer): Promise<number | null> {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStructureTree(
|
||||
content: ArrayBuffer,
|
||||
fileType: string,
|
||||
filename: string,
|
||||
): Promise<unknown[] | null> {
|
||||
try {
|
||||
if (fileType === "pdf") {
|
||||
const pdfjsLib = await import(
|
||||
"pdfjs-dist/legacy/build/pdf.mjs" as string
|
||||
);
|
||||
const pdf = await (
|
||||
pdfjsLib as unknown as {
|
||||
getDocument: (opts: unknown) => {
|
||||
promise: Promise<{
|
||||
numPages: number;
|
||||
getOutline: () => Promise<{ title?: string }[]>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
).getDocument({ data: new Uint8Array(content) }).promise;
|
||||
if (pdf.numPages <= 5) return null;
|
||||
const outline = await pdf.getOutline();
|
||||
if (outline?.length) {
|
||||
return outline.map((item, i) => ({
|
||||
id: `h1-${i}`,
|
||||
title: item.title ?? `Item ${i + 1}`,
|
||||
level: 1,
|
||||
page_number: null,
|
||||
children: [],
|
||||
}));
|
||||
}
|
||||
return Array.from({ length: pdf.numPages }, (_, i) => ({
|
||||
id: `page-${i + 1}`,
|
||||
title: `Page ${i + 1}`,
|
||||
level: 1,
|
||||
page_number: i + 1,
|
||||
children: [],
|
||||
}));
|
||||
} else {
|
||||
const mammoth = await import("mammoth");
|
||||
const result = await mammoth.extractRawText({
|
||||
buffer: Buffer.from(content),
|
||||
});
|
||||
const lines = result.value.split("\n").filter((l) => l.trim());
|
||||
const nodes = lines
|
||||
.slice(0, 30)
|
||||
.map((line, i) => ({
|
||||
id: `h1-${i}`,
|
||||
title: line.slice(0, 100),
|
||||
level: 1,
|
||||
page_number: null,
|
||||
children: [],
|
||||
}));
|
||||
return nodes.length ? nodes : null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue