mike/backend/src/routes/caseLaw.ts
willchen96 f32a194b33 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
2026-06-09 01:46:58 +08:00

84 lines
2.9 KiB
TypeScript

import { Router } from "express";
import { requireAuth } from "../middleware/auth";
import { getCourtlistenerCaseOpinions } from "../lib/courtlistener";
import { createServerSupabase } from "../lib/supabase";
import { getUserModelSettings } from "../lib/userSettings";
export const caseLawRouter = Router();
caseLawRouter.use(requireAuth);
const isDev = process.env.NODE_ENV !== "production";
const devLog = (...args: Parameters<typeof console.log>) => {
if (isDev) console.log(...args);
};
const sidepanelOpinionFetches = new Map<string, Promise<unknown>>();
function cleanClusterId(value: unknown): number | null {
const numeric =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseInt(value, 10)
: NaN;
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : null;
}
caseLawRouter.post("/case-opinions", async (req, res) => {
const body =
req.body && typeof req.body === "object" && !Array.isArray(req.body)
? (req.body as Record<string, unknown>)
: {};
const clusterId = cleanClusterId(body.clusterId ?? body.cluster_id);
if (!clusterId) {
return res.status(400).json({
detail: "cluster_id is required",
});
}
try {
const userId = String(res.locals.userId ?? "");
const settings = await getUserModelSettings(userId);
devLog("[case-law/case-opinions] loading sidepanel opinions", {
clusterId,
});
const db = createServerSupabase();
const fetchKey = `${userId}:${clusterId}`;
let fetchPromise = sidepanelOpinionFetches.get(fetchKey);
if (fetchPromise) {
devLog("[case-law/case-opinions] joining in-flight fetch", {
clusterId,
});
} else {
fetchPromise = getCourtlistenerCaseOpinions({
clusterId,
db,
includeFullText: true,
maxChars: 50000,
apiToken: settings.api_keys.courtlistener,
}).finally(() => {
sidepanelOpinionFetches.delete(fetchKey);
});
sidepanelOpinionFetches.set(fetchKey, fetchPromise);
}
const fetched = await fetchPromise;
const fetchedRecord =
fetched && typeof fetched === "object" && !Array.isArray(fetched)
? (fetched as Record<string, unknown>)
: {};
const opinions = Array.isArray(fetchedRecord.opinions)
? fetchedRecord.opinions
: [];
devLog("[case-law/case-opinions] returning sidepanel opinions", {
clusterId,
opinionCount: opinions.length,
});
return res.json({ opinions });
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to fetch case opinions";
return res.status(502).json({ detail: message });
}
});