mirror of
https://github.com/willchen96/mike.git
synced 2026-07-04 22:02:12 +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
|
|
@ -2,10 +2,16 @@ import { Router } from "express";
|
|||
import { requireAuth } from "../middleware/auth";
|
||||
import { createServerSupabase } from "../lib/supabase";
|
||||
import { downloadFile } from "../lib/storage";
|
||||
import { loadActiveVersion } from "../lib/documentVersions";
|
||||
import {
|
||||
attachActiveVersionPaths,
|
||||
loadActiveVersion,
|
||||
} from "../lib/documentVersions";
|
||||
import { normalizeDocxZipPaths } from "../lib/convert";
|
||||
import {
|
||||
AssistantStreamError,
|
||||
isAbortError,
|
||||
runLLMStream,
|
||||
stripTransientAssistantEvents,
|
||||
TABULAR_TOOLS,
|
||||
type ChatMessage,
|
||||
type TabularCellStore,
|
||||
|
|
@ -370,6 +376,11 @@ tabularRouter.get("/:reviewId", requireAuth, async (req, res) => {
|
|||
docIds.length > 0
|
||||
? await db.from("documents").select("*").in("id", docIds)
|
||||
: { data: [] as Record<string, unknown>[] };
|
||||
const docs = (docsResult.data ?? []) as unknown as {
|
||||
id: string;
|
||||
current_version_id?: string | null;
|
||||
}[];
|
||||
await attachActiveVersionPaths(db, docs);
|
||||
|
||||
res.json({
|
||||
review: { ...review, is_owner: access.isOwner },
|
||||
|
|
@ -377,7 +388,7 @@ tabularRouter.get("/:reviewId", requireAuth, async (req, res) => {
|
|||
...cell,
|
||||
content: parseCellContent(cell.content),
|
||||
})),
|
||||
documents: docsResult.data ?? [],
|
||||
documents: docs,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -471,8 +482,19 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
|
|||
if (req.body.title != null) updates.title = req.body.title;
|
||||
if (req.body.columns_config != null)
|
||||
updates.columns_config = req.body.columns_config;
|
||||
if (req.body.project_id !== undefined)
|
||||
updates.project_id = req.body.project_id;
|
||||
const projectIdUpdateProvided = req.body.project_id !== undefined;
|
||||
const projectIdUpdate =
|
||||
req.body.project_id === null
|
||||
? null
|
||||
: typeof req.body.project_id === "string" &&
|
||||
req.body.project_id.trim()
|
||||
? req.body.project_id.trim()
|
||||
: undefined;
|
||||
if (projectIdUpdateProvided && projectIdUpdate === undefined) {
|
||||
return void res.status(400).json({
|
||||
detail: "project_id must be a non-empty string or null",
|
||||
});
|
||||
}
|
||||
// shared_with edits are owner-only — gated below after we know who's
|
||||
// making the call. Normalize lowercase + dedupe + drop empties.
|
||||
let sharedWithUpdate: string[] | undefined;
|
||||
|
|
@ -519,6 +541,27 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
|
|||
.json({ detail: "Only the review owner can change sharing" });
|
||||
updates.shared_with = sharedWithUpdate;
|
||||
}
|
||||
if (projectIdUpdateProvided) {
|
||||
if (!access.isOwner) {
|
||||
return void res.status(403).json({
|
||||
detail: "Only the review owner can move a review",
|
||||
});
|
||||
}
|
||||
if (projectIdUpdate) {
|
||||
const projectAccess = await checkProjectAccess(
|
||||
projectIdUpdate,
|
||||
userId,
|
||||
userEmail,
|
||||
db,
|
||||
);
|
||||
if (!projectAccess.ok) {
|
||||
return void res
|
||||
.status(404)
|
||||
.json({ detail: "Target project not found" });
|
||||
}
|
||||
}
|
||||
updates.project_id = projectIdUpdate;
|
||||
}
|
||||
|
||||
const { data: updatedReview, error: updateError } = await db
|
||||
.from("tabular_reviews")
|
||||
|
|
@ -744,7 +787,7 @@ tabularRouter.post(
|
|||
return void res.status(404).json({ detail: "Document not found" });
|
||||
const { data: doc } = await db
|
||||
.from("documents")
|
||||
.select("id, filename, file_type")
|
||||
.select("id, current_version_id")
|
||||
.eq("id", document_id)
|
||||
.single();
|
||||
if (!doc)
|
||||
|
|
@ -776,7 +819,7 @@ tabularRouter.post(
|
|||
if (buf) {
|
||||
try {
|
||||
markdown =
|
||||
(doc.file_type as string) === "pdf"
|
||||
docActive.file_type === "pdf"
|
||||
? await extractPdfMarkdown(buf)
|
||||
: await extractDocxMarkdown(buf);
|
||||
} catch (err) {
|
||||
|
|
@ -790,7 +833,7 @@ tabularRouter.post(
|
|||
|
||||
const result = await queryTabularCell(
|
||||
tabular_model,
|
||||
doc.filename as string,
|
||||
docActive?.filename?.trim() || "Untitled document",
|
||||
markdown,
|
||||
column.prompt,
|
||||
column.format,
|
||||
|
|
@ -866,18 +909,25 @@ tabularRouter.post("/:reviewId/generate", requireAuth, async (req, res) => {
|
|||
filteredIds.length > 0
|
||||
? await db
|
||||
.from("documents")
|
||||
.select("id, filename, file_type, page_count")
|
||||
.select("id, current_version_id")
|
||||
.in("id", filteredIds)
|
||||
: { data: [] as Record<string, unknown>[] };
|
||||
docs = data ?? [];
|
||||
} else if (review.project_id) {
|
||||
const { data } = await db
|
||||
.from("documents")
|
||||
.select("id, filename, file_type, page_count")
|
||||
.select("id, current_version_id")
|
||||
.eq("project_id", review.project_id)
|
||||
.order("created_at", { ascending: true });
|
||||
docs = data ?? [];
|
||||
}
|
||||
await attachActiveVersionPaths(
|
||||
db,
|
||||
docs as {
|
||||
id: string;
|
||||
current_version_id?: string | null;
|
||||
}[],
|
||||
);
|
||||
|
||||
const { tabular_model, api_keys } = await getUserModelSettings(userId, db);
|
||||
const missingKey = missingModelApiKey(tabular_model, api_keys);
|
||||
|
|
@ -900,16 +950,22 @@ tabularRouter.post("/:reviewId/generate", requireAuth, async (req, res) => {
|
|||
await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
const docId = doc.id as string;
|
||||
const filename = doc.filename as string;
|
||||
let markdown = "";
|
||||
|
||||
const active = await loadActiveVersion(docId, db);
|
||||
if (active) {
|
||||
const buf = await downloadFile(active.storage_path);
|
||||
const filename =
|
||||
(typeof doc.filename === "string" && doc.filename.trim()
|
||||
? doc.filename.trim()
|
||||
: "Untitled document");
|
||||
const storagePath =
|
||||
typeof doc.storage_path === "string" ? doc.storage_path : "";
|
||||
const fileType =
|
||||
typeof doc.file_type === "string" ? doc.file_type : "";
|
||||
if (storagePath) {
|
||||
const buf = await downloadFile(storagePath);
|
||||
if (buf) {
|
||||
try {
|
||||
markdown =
|
||||
(doc.file_type as string) === "pdf"
|
||||
fileType === "pdf"
|
||||
? await extractPdfMarkdown(buf)
|
||||
: await extractDocxMarkdown(buf);
|
||||
} catch (err) {
|
||||
|
|
@ -1253,14 +1309,29 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
const docIds = [
|
||||
...new Set((cells ?? []).map((c: any) => c.document_id as string)),
|
||||
];
|
||||
let docs: { id: string; filename: string }[] = [];
|
||||
let docs: {
|
||||
id: string;
|
||||
filename: string;
|
||||
current_version_id?: string | null;
|
||||
}[] = [];
|
||||
if (docIds.length > 0) {
|
||||
const { data } = await db
|
||||
.from("documents")
|
||||
.select("id, filename")
|
||||
.select("id, current_version_id")
|
||||
.in("id", docIds)
|
||||
.order("created_at", { ascending: true });
|
||||
docs = (data ?? []) as { id: string; filename: string }[];
|
||||
const attachedDocs = (data ?? []) as {
|
||||
id: string;
|
||||
current_version_id?: string | null;
|
||||
filename?: string | null;
|
||||
}[];
|
||||
await attachActiveVersionPaths(db, attachedDocs);
|
||||
docs = attachedDocs.map((doc) => ({
|
||||
...doc,
|
||||
filename:
|
||||
(typeof doc.filename === "string" && doc.filename.trim()) ||
|
||||
"Untitled document",
|
||||
}));
|
||||
}
|
||||
|
||||
const sortedColumns = (
|
||||
|
|
@ -1339,6 +1410,11 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.flushHeaders();
|
||||
const write = (line: string) => res.write(line);
|
||||
const streamAbort = new AbortController();
|
||||
let streamFinished = false;
|
||||
res.on("close", () => {
|
||||
if (!streamFinished) streamAbort.abort();
|
||||
});
|
||||
|
||||
if (chatId) {
|
||||
write(`data: ${JSON.stringify({ type: "chat_id", chatId })}\n\n`);
|
||||
|
|
@ -1353,20 +1429,23 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
db,
|
||||
write,
|
||||
extraTools: TABULAR_TOOLS,
|
||||
includeResearchTools: false,
|
||||
tabularStore,
|
||||
buildCitations: (text) =>
|
||||
extractTabularAnnotations(text, tabularStore),
|
||||
model: tabular_model,
|
||||
apiKeys: api_keys,
|
||||
signal: streamAbort.signal,
|
||||
});
|
||||
|
||||
const persistedEvents = stripTransientAssistantEvents(events);
|
||||
const annotations = extractTabularAnnotations(fullText, tabularStore);
|
||||
|
||||
if (chatId) {
|
||||
await db.from("tabular_review_chat_messages").insert({
|
||||
chat_id: chatId,
|
||||
role: "assistant",
|
||||
content: events.length ? events : null,
|
||||
content: persistedEvents.length ? persistedEvents : null,
|
||||
annotations: annotations.length ? annotations : null,
|
||||
});
|
||||
await db
|
||||
|
|
@ -1398,16 +1477,48 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
console.log("[tabular/chat] client aborted stream", { chatId });
|
||||
return;
|
||||
}
|
||||
console.error("[tabular/chat] error", err);
|
||||
const message =
|
||||
err instanceof Error && err.message ? err.message : "Stream error";
|
||||
const errorEvents = err instanceof AssistantStreamError
|
||||
? stripTransientAssistantEvents(err.events)
|
||||
: [{ type: "error" as const, message }];
|
||||
const errorFullText =
|
||||
err instanceof AssistantStreamError ? err.fullText : "";
|
||||
if (chatId) {
|
||||
try {
|
||||
const annotations = extractTabularAnnotations(
|
||||
errorFullText,
|
||||
tabularStore,
|
||||
);
|
||||
const { error: saveError } = await db
|
||||
.from("tabular_review_chat_messages")
|
||||
.insert({
|
||||
chat_id: chatId,
|
||||
role: "assistant",
|
||||
content: errorEvents.length ? errorEvents : null,
|
||||
annotations: annotations.length ? annotations : null,
|
||||
});
|
||||
if (saveError)
|
||||
console.error("[tabular/chat] failed to save error", saveError);
|
||||
} catch (saveErr) {
|
||||
console.error("[tabular/chat] failed to save error", saveErr);
|
||||
}
|
||||
}
|
||||
try {
|
||||
write(
|
||||
`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`,
|
||||
`data: ${JSON.stringify({ type: "error", message })}\n\n`,
|
||||
);
|
||||
write("data: [DONE]\n\n");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} finally {
|
||||
streamFinished = true;
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue