mirror of
https://github.com/willchen96/mike.git
synced 2026-06-26 21:39:39 +02:00
Sync CourtListener verification and document safety updates
- Refine CourtListener citation verification, bulk lookup logging, and API fallback behavior - Persist cancelled chat stream output and render cancellation as the final assistant message - Add document/version deletion safety fixes and shared warning/modal UI updates - Sync document panel, case law panel, and response UI styling refinements - Harden OSS sync script to preserve local env, dependency, and generated files
This commit is contained in:
parent
44e868eb42
commit
f32a194b33
24 changed files with 2494 additions and 1222 deletions
|
|
@ -44,7 +44,7 @@ caseLawRouter.post("/case-opinions", async (req, res) => {
|
|||
clusterId,
|
||||
});
|
||||
const db = createServerSupabase();
|
||||
const fetchKey = String(clusterId);
|
||||
const fetchKey = `${userId}:${clusterId}`;
|
||||
let fetchPromise = sidepanelOpinionFetches.get(fetchKey);
|
||||
if (fetchPromise) {
|
||||
devLog("[case-law/case-opinions] joining in-flight fetch", {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
enrichWithPriorEvents,
|
||||
buildWorkflowStore,
|
||||
AssistantStreamError,
|
||||
buildCancelledAssistantMessage,
|
||||
extractAnnotations,
|
||||
isAbortError,
|
||||
runLLMStream,
|
||||
|
|
@ -614,6 +615,28 @@ chatRouter.post("/", requireAuth, async (req, res) => {
|
|||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
devLog("[chat/stream] client aborted stream", { chatId });
|
||||
if (err instanceof AssistantStreamError) {
|
||||
const partial = buildCancelledAssistantMessage({
|
||||
fullText: err.fullText,
|
||||
events: err.events,
|
||||
buildAnnotations: (fullText, events) =>
|
||||
extractAnnotations(fullText, docIndex, events),
|
||||
});
|
||||
const { error: saveError } = await db.from("chat_messages").insert({
|
||||
chat_id: chatId,
|
||||
role: "assistant",
|
||||
content: partial.events.length ? partial.events : null,
|
||||
annotations: partial.annotations.length
|
||||
? partial.annotations
|
||||
: null,
|
||||
});
|
||||
if (saveError) {
|
||||
console.error(
|
||||
"[chat/stream] failed to save aborted stream",
|
||||
saveError,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.error("[chat/stream] error:", err);
|
||||
|
|
|
|||
|
|
@ -423,6 +423,15 @@ documentsRouter.post(
|
|||
const sourceAccess = await ensureDocAccess(sourceDoc, userId, userEmail, db);
|
||||
if (!sourceAccess.ok)
|
||||
return void res.status(404).json({ detail: "Source document not found" });
|
||||
const willDeleteSource =
|
||||
sourceDoc.project_id &&
|
||||
targetDoc.project_id &&
|
||||
sourceDoc.project_id === targetDoc.project_id;
|
||||
if (willDeleteSource && !sourceAccess.isOwner) {
|
||||
return void res.status(403).json({
|
||||
detail: "Only the source document owner can move it into a version.",
|
||||
});
|
||||
}
|
||||
|
||||
const targetActive = await loadActiveVersion(documentId, db);
|
||||
const targetType = targetActive?.file_type ?? "";
|
||||
|
|
@ -548,11 +557,7 @@ documentsRouter.post(
|
|||
.json({ detail: "Failed to update document current version." });
|
||||
}
|
||||
|
||||
if (
|
||||
sourceDoc.project_id &&
|
||||
targetDoc.project_id &&
|
||||
sourceDoc.project_id === targetDoc.project_id
|
||||
) {
|
||||
if (willDeleteSource) {
|
||||
const { error: deleteErr } = await deleteDocumentAndVersionFiles(
|
||||
db,
|
||||
sourceDocumentId,
|
||||
|
|
@ -721,12 +726,21 @@ documentsRouter.post(
|
|||
.json({ detail: "Failed to record new version." });
|
||||
}
|
||||
|
||||
await db
|
||||
const { error: updateDocErr } = await db
|
||||
.from("documents")
|
||||
.update({
|
||||
current_version_id: versionRow.id,
|
||||
})
|
||||
.eq("id", documentId);
|
||||
if (updateDocErr) {
|
||||
console.error(
|
||||
"[versions/upload] current version update failed",
|
||||
updateDocErr,
|
||||
);
|
||||
return void res
|
||||
.status(500)
|
||||
.json({ detail: "Failed to update document current version." });
|
||||
}
|
||||
|
||||
res.status(201).json(versionRow);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
buildWorkflowStore,
|
||||
enrichWithPriorEvents,
|
||||
AssistantStreamError,
|
||||
buildCancelledAssistantMessage,
|
||||
extractAnnotations,
|
||||
isAbortError,
|
||||
runLLMStream,
|
||||
|
|
@ -199,6 +200,28 @@ projectChatRouter.post("/", requireAuth, async (req, res) => {
|
|||
console.log("[project-chat/stream] client aborted stream", {
|
||||
chatId,
|
||||
});
|
||||
if (err instanceof AssistantStreamError) {
|
||||
const partial = buildCancelledAssistantMessage({
|
||||
fullText: err.fullText,
|
||||
events: err.events,
|
||||
buildAnnotations: (fullText, events) =>
|
||||
extractAnnotations(fullText, docIndex, events),
|
||||
});
|
||||
const { error: saveError } = await db.from("chat_messages").insert({
|
||||
chat_id: chatId,
|
||||
role: "assistant",
|
||||
content: partial.events.length ? partial.events : null,
|
||||
annotations: partial.annotations.length
|
||||
? partial.annotations
|
||||
: null,
|
||||
});
|
||||
if (saveError) {
|
||||
console.error(
|
||||
"[project-chat/stream] failed to save aborted stream",
|
||||
saveError,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.error("[project-chat/stream] error:", err);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,37 @@ function normalizeDocumentFilename(nextName: unknown, currentName: string) {
|
|||
return `${trimmed}${ext}`;
|
||||
}
|
||||
|
||||
async function deleteProjectDocumentsAndVersionFiles(
|
||||
db: ReturnType<typeof createServerSupabase>,
|
||||
projectId: string,
|
||||
documentIds: string[],
|
||||
) {
|
||||
if (documentIds.length === 0) return null;
|
||||
const { data: versions, error: versionsError } = await db
|
||||
.from("document_versions")
|
||||
.select("storage_path, pdf_storage_path")
|
||||
.in("document_id", documentIds);
|
||||
if (versionsError) return versionsError;
|
||||
|
||||
const paths = new Set<string>();
|
||||
for (const v of versions ?? []) {
|
||||
if (typeof v.storage_path === "string" && v.storage_path.length > 0) {
|
||||
paths.add(v.storage_path);
|
||||
}
|
||||
if (typeof v.pdf_storage_path === "string" && v.pdf_storage_path.length > 0) {
|
||||
paths.add(v.pdf_storage_path);
|
||||
}
|
||||
}
|
||||
await Promise.all([...paths].map((p) => deleteFile(p).catch(() => {})));
|
||||
|
||||
const { error } = await db
|
||||
.from("documents")
|
||||
.delete()
|
||||
.eq("project_id", projectId)
|
||||
.in("id", documentIds);
|
||||
return error ?? null;
|
||||
}
|
||||
|
||||
// GET /projects
|
||||
projectsRouter.get("/", requireAuth, async (req, res) => {
|
||||
const userId = res.locals.userId as string;
|
||||
|
|
@ -710,11 +741,48 @@ projectsRouter.delete("/:projectId/folders/:folderId", requireAuth, async (req,
|
|||
const access = await checkProjectAccess(projectId, userId, userEmail, db);
|
||||
if (!access.ok) return void res.status(404).json({ detail: "Project not found" });
|
||||
|
||||
const folder = await loadProjectFolder(db, projectId, folderId);
|
||||
if (!folder) return void res.status(404).json({ detail: "Folder not found" });
|
||||
const { data: allFolders, error: foldersError } = await db
|
||||
.from("project_subfolders")
|
||||
.select("id, parent_folder_id")
|
||||
.eq("project_id", projectId);
|
||||
if (foldersError)
|
||||
return void res.status(500).json({ detail: foldersError.message });
|
||||
if (!(allFolders ?? []).some((f) => f.id === folderId))
|
||||
return void res.status(404).json({ detail: "Folder not found" });
|
||||
|
||||
// Move direct documents to root before cascade-deleting subfolders
|
||||
await db.from("documents").update({ folder_id: null }).eq("folder_id", folderId).eq("project_id", projectId);
|
||||
const childrenByParent = new Map<string, string[]>();
|
||||
for (const f of allFolders ?? []) {
|
||||
const parentId = f.parent_folder_id as string | null;
|
||||
if (!parentId) continue;
|
||||
const children = childrenByParent.get(parentId) ?? [];
|
||||
children.push(f.id as string);
|
||||
childrenByParent.set(parentId, children);
|
||||
}
|
||||
|
||||
const folderIds = new Set<string>();
|
||||
const stack = [folderId];
|
||||
while (stack.length > 0) {
|
||||
const id = stack.pop()!;
|
||||
if (folderIds.has(id)) continue;
|
||||
folderIds.add(id);
|
||||
stack.push(...(childrenByParent.get(id) ?? []));
|
||||
}
|
||||
|
||||
const { data: docs, error: docsError } = await db
|
||||
.from("documents")
|
||||
.select("id")
|
||||
.eq("project_id", projectId)
|
||||
.in("folder_id", [...folderIds]);
|
||||
if (docsError) return void res.status(500).json({ detail: docsError.message });
|
||||
|
||||
const docIds = (docs ?? []).map((d) => d.id as string);
|
||||
const deleteDocsError = await deleteProjectDocumentsAndVersionFiles(
|
||||
db,
|
||||
projectId,
|
||||
docIds,
|
||||
);
|
||||
if (deleteDocsError)
|
||||
return void res.status(500).json({ detail: deleteDocsError.message });
|
||||
|
||||
const { error } = await db.from("project_subfolders")
|
||||
.delete().eq("id", folderId).eq("project_id", projectId);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { normalizeDocxZipPaths } from "../lib/convert";
|
||||
import {
|
||||
AssistantStreamError,
|
||||
buildCancelledAssistantMessage,
|
||||
isAbortError,
|
||||
runLLMStream,
|
||||
stripTransientAssistantEvents,
|
||||
|
|
@ -480,8 +481,6 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
|
|||
const { reviewId } = req.params;
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (req.body.title != null) updates.title = req.body.title;
|
||||
if (req.body.columns_config != null)
|
||||
updates.columns_config = req.body.columns_config;
|
||||
const projectIdUpdateProvided = req.body.project_id !== undefined;
|
||||
const projectIdUpdate =
|
||||
req.body.project_id === null
|
||||
|
|
@ -534,6 +533,14 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
|
|||
);
|
||||
if (!access.ok)
|
||||
return void res.status(404).json({ detail: "Review not found" });
|
||||
if (req.body.columns_config != null) {
|
||||
if (!access.isOwner) {
|
||||
return void res.status(403).json({
|
||||
detail: "Only the review owner can change columns",
|
||||
});
|
||||
}
|
||||
updates.columns_config = req.body.columns_config;
|
||||
}
|
||||
if (sharedWithUpdate !== undefined) {
|
||||
if (!access.isOwner)
|
||||
return void res
|
||||
|
|
@ -1365,8 +1372,9 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
messages.filter((m) => m.role === "user").length === 1;
|
||||
|
||||
if (chatId) {
|
||||
// Either chat owner OR any project member of the parent review can
|
||||
// continue the chat. We've already verified review access above.
|
||||
// The chat must belong to this exact review and to the requester.
|
||||
// Review access alone is not enough: otherwise a user could reuse one
|
||||
// of their chats from a different review in this route.
|
||||
const { data: existing } = await db
|
||||
.from("tabular_review_chats")
|
||||
.select("id, title, review_id, user_id")
|
||||
|
|
@ -1374,7 +1382,8 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
.single();
|
||||
const canUse =
|
||||
!!existing &&
|
||||
(existing.review_id === reviewId || existing.user_id === userId);
|
||||
existing.review_id === reviewId &&
|
||||
existing.user_id === userId;
|
||||
if (!canUse || !existing) chatId = null;
|
||||
else chatTitle = existing.title;
|
||||
}
|
||||
|
|
@ -1479,6 +1488,34 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
console.log("[tabular/chat] client aborted stream", { chatId });
|
||||
if (chatId && err instanceof AssistantStreamError) {
|
||||
const partial = buildCancelledAssistantMessage({
|
||||
fullText: err.fullText,
|
||||
events: err.events,
|
||||
buildAnnotations: (fullText) =>
|
||||
extractTabularAnnotations(fullText, tabularStore),
|
||||
});
|
||||
const { error: saveError } = await db
|
||||
.from("tabular_review_chat_messages")
|
||||
.insert({
|
||||
chat_id: chatId,
|
||||
role: "assistant",
|
||||
content: partial.events.length ? partial.events : null,
|
||||
annotations: partial.annotations.length
|
||||
? partial.annotations
|
||||
: null,
|
||||
});
|
||||
if (saveError) {
|
||||
console.error(
|
||||
"[tabular/chat] failed to save aborted stream",
|
||||
saveError,
|
||||
);
|
||||
}
|
||||
await db
|
||||
.from("tabular_review_chats")
|
||||
.update({ updated_at: new Date().toISOString() })
|
||||
.eq("id", chatId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.error("[tabular/chat] error", err);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue