Update document UI, tabular reviews, and storage caching

This commit is contained in:
willchen96 2026-05-18 00:21:40 +08:00
parent 2bbb628891
commit 4f3384334a
26 changed files with 856 additions and 341 deletions

View file

@ -1,4 +1,3 @@
import { promisify } from "util";
import JSZip from "jszip";
let _convert:
@ -8,7 +7,26 @@ let _convert:
async function getConvert() {
if (!_convert) {
const libre = await import("libreoffice-convert");
_convert = promisify(libre.default.convert.bind(libre.default));
const convert = libre.default.convert.bind(libre.default) as (
buf: Buffer,
ext: string,
filter: undefined,
callback?: (err: Error | null, result: Buffer) => void,
) => Promise<Buffer> | void;
_convert = (buf, ext, filter) =>
new Promise<Buffer>((resolve, reject) => {
try {
const maybePromise = convert(buf, ext, filter, (err, result) => {
if (err) reject(err);
else resolve(result);
});
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.then(resolve, reject);
}
} catch (err) {
reject(err);
}
});
}
return _convert;
}

View file

@ -17,16 +17,21 @@ import {
} from "@aws-sdk/client-s3";
import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner";
let cachedClient: S3Client | undefined;
function getClient(): S3Client {
return new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT_URL!,
forcePathStyle: true,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
if (!cachedClient) {
cachedClient = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT_URL!,
forcePathStyle: true,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
}
return cachedClient;
}
const BUCKET = process.env.R2_BUCKET_NAME ?? "mike";
@ -37,6 +42,14 @@ export const storageEnabled = Boolean(
process.env.R2_SECRET_ACCESS_KEY,
);
function requireStorageConfig(): void {
if (!storageEnabled) {
throw new Error(
"R2_ENDPOINT_URL, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY must be set",
);
}
}
// ---------------------------------------------------------------------------
// Upload
// ---------------------------------------------------------------------------
@ -46,6 +59,7 @@ export async function uploadFile(
content: ArrayBuffer,
contentType: string,
): Promise<void> {
requireStorageConfig();
const client = getClient();
await client.send(
new PutObjectCommand({

View file

@ -141,6 +141,10 @@ async function getAccessibleChat(
chatRouter.get("/", requireAuth, async (req, res) => {
const userId = res.locals.userId as string;
const db = createServerSupabase();
const requestedLimit = Number.parseInt(String(req.query.limit ?? ""), 10);
const limit = Number.isFinite(requestedLimit)
? Math.min(Math.max(requestedLimit, 1), 100)
: null;
const { data: ownProjects, error: projErr } = await db
.from("projects")
@ -156,11 +160,15 @@ chatRouter.get("/", requireAuth, async (req, res) => {
? `user_id.eq.${userId},project_id.in.(${ownProjectIds.join(",")})`
: `user_id.eq.${userId}`;
const { data, error } = await db
let query = db
.from("chats")
.select("*")
.or(filter)
.order("created_at", { ascending: false });
if (limit) query = query.limit(limit);
const { data, error } = await query;
if (error) return void res.status(500).json({ detail: error.message });
res.json(data ?? []);
});

View file

@ -165,6 +165,15 @@ tabularRouter.get("/", requireAuth, async (req, res) => {
// Fetch distinct document counts per review
const reviewIds = reviews.map((r) => (r as { id: string }).id);
let docCounts: Record<string, number> = {};
const reviewsWithExplicitDocs = new Set<string>();
for (const review of reviews) {
const id = (review as { id: string }).id;
if (Array.isArray(review.document_ids)) {
const explicitDocIds = review.document_ids;
reviewsWithExplicitDocs.add(id);
docCounts[id] = new Set(explicitDocIds).size;
}
}
if (reviewIds.length > 0) {
const { data: cells } = await db
.from("tabular_cells")
@ -176,8 +185,10 @@ tabularRouter.get("/", requireAuth, async (req, res) => {
const key = `${cell.review_id}:${cell.document_id}`;
if (!seen.has(key)) {
seen.add(key);
docCounts[cell.review_id] =
(docCounts[cell.review_id] ?? 0) + 1;
if (!reviewsWithExplicitDocs.has(cell.review_id)) {
docCounts[cell.review_id] =
(docCounts[cell.review_id] ?? 0) + 1;
}
}
}
}
@ -229,6 +240,7 @@ tabularRouter.post("/", requireAuth, async (req, res) => {
user_id: userId,
title: title ?? null,
columns_config,
document_ids: allowedDocumentIds,
project_id: project_id ?? null,
workflow_id: workflow_id ?? null,
})
@ -345,17 +357,19 @@ tabularRouter.get("/:reviewId", requireAuth, async (req, res) => {
.from("tabular_cells")
.select("*")
.eq("review_id", reviewId);
const docIds = [...new Set((cells ?? []).map((c) => c.document_id))];
const cellDocIds = [...new Set((cells ?? []).map((c) => c.document_id))];
const hasExplicitDocIds = Array.isArray(review.document_ids);
const explicitDocIds = hasExplicitDocIds
? (review.document_ids as string[])
: [];
const docIds =
hasExplicitDocIds
? explicitDocIds
: cellDocIds;
const docsResult =
docIds.length > 0
? await db.from("documents").select("*").in("id", docIds)
: review.project_id
? await db
.from("documents")
.select("*")
.eq("project_id", review.project_id)
.order("created_at", { ascending: true })
: { data: [] as Record<string, unknown>[] };
: { data: [] as Record<string, unknown>[] };
res.json({
review: { ...review, is_owner: access.isOwner },
@ -517,6 +531,7 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
detail: updateError?.message ?? "Failed to update review",
});
let persistedDocumentIds: string[] | undefined;
if (
Array.isArray(req.body.columns_config) ||
Array.isArray(req.body.document_ids)
@ -577,13 +592,21 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
(existingCells ?? []).map((cell) => cell.document_id),
),
];
if (documentIds.length === 0 && existingReview.project_id) {
const { data: projectDocs } = await db
.from("documents")
.select("id")
.eq("project_id", existingReview.project_id);
documentIds = (projectDocs ?? []).map((doc) => doc.id);
}
}
if (Array.isArray(req.body.document_ids)) {
persistedDocumentIds = documentIds;
const { error: documentIdsError } = await db
.from("tabular_reviews")
.update({
document_ids: documentIds,
updated_at: new Date().toISOString(),
})
.eq("id", reviewId);
if (documentIdsError)
return void res.status(500).json({
detail: documentIdsError.message,
});
}
const activeColumns = Array.isArray(req.body.columns_config)
@ -614,7 +637,10 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => {
}
}
res.json(updatedReview);
res.json({
...updatedReview,
...(persistedDocumentIds ? { document_ids: persistedDocumentIds } : {}),
});
});
// DELETE /tabular-review/:reviewId