fix people
This commit is contained in:
103
main.ts
103
main.ts
@@ -461,9 +461,57 @@ async function servePdfBytes(
|
||||
|
||||
// ------- HTTP Server -------
|
||||
|
||||
// ---- Webview-reported window size (fallback tracking) ----
|
||||
// Both getSize() and (apparently) the native resize events are unreliable
|
||||
// in the experimental desktop backend, so the frontend reports its viewport
|
||||
// size via POST /api/window-metrics. The first report arrives ~800ms after
|
||||
// startup, i.e. after the enforced setSize, and is used to calibrate the
|
||||
// per-session offset between the outer window size and the webview viewport
|
||||
// (titlebar + borders). Later reports are translated back to outer-window
|
||||
// coordinates with that offset. Calibrating per session means any error
|
||||
// cancels out and the window cannot shrink a little on every restart.
|
||||
let webviewMetricsOffset: { dw: number; dh: number } | null = null;
|
||||
let sizeSeenFromWebview = false;
|
||||
|
||||
Deno.serve(async (request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/api/window-metrics" && request.method === "POST") {
|
||||
try {
|
||||
const body = await request.json() as {
|
||||
innerWidth?: number;
|
||||
innerHeight?: number;
|
||||
};
|
||||
const iw = Math.round(Number(body.innerWidth) || 0);
|
||||
const ih = Math.round(Number(body.innerHeight) || 0);
|
||||
if (iw > 200 && ih > 200) {
|
||||
if (webviewMetricsOffset === null) {
|
||||
// First report: window is still at the enforced startup size,
|
||||
// so the difference to the viewport is the decoration size.
|
||||
webviewMetricsOffset = {
|
||||
dw: Math.min(Math.max(clamped.windowWidth - iw, 0), 200),
|
||||
dh: Math.min(Math.max(clamped.windowHeight - ih, 0), 200),
|
||||
};
|
||||
console.log(
|
||||
`${PREFIX} Window metrics calibrated: viewport ${iw}x${ih}, decoration offset ${webviewMetricsOffset.dw}x${webviewMetricsOffset.dh}`,
|
||||
);
|
||||
} else {
|
||||
const w = iw + webviewMetricsOffset.dw;
|
||||
const h = ih + webviewMetricsOffset.dh;
|
||||
if (w >= 1100 && h >= 700) {
|
||||
settings.windowWidth = w;
|
||||
settings.windowHeight = h;
|
||||
sizeSeenFromWebview = true;
|
||||
console.log(`${PREFIX} Window size (from webview): ${w}x${h}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return json({ ok: true });
|
||||
} catch {
|
||||
return json({ ok: false }, 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/state" && request.method === "GET") {
|
||||
const configForClient = {
|
||||
jsonPath: settings.jsonPath,
|
||||
@@ -710,14 +758,45 @@ if (
|
||||
// Native resize tracking using Deno 2.9 BrowserWindow events
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let sizeSeenFromEvent = false;
|
||||
let resizeEventLogsLeft = 3;
|
||||
try {
|
||||
win.addEventListener(
|
||||
"resize",
|
||||
((e: CustomEvent) => {
|
||||
if (resizeTimer !== undefined) clearTimeout(resizeTimer);
|
||||
// Store immediately but debounce writes
|
||||
const w = e.detail?.width ?? 0;
|
||||
const h = e.detail?.height ?? 0;
|
||||
// The payload shape of the experimental backend is not settled;
|
||||
// accept detail.{width,height}, detail.{w,h}, detail as [w, h],
|
||||
// and width/height directly on the event object.
|
||||
const anyEvent = e as unknown as Record<string, unknown>;
|
||||
const detail = anyEvent.detail as
|
||||
| Record<string, unknown>
|
||||
| number[]
|
||||
| undefined;
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
if (Array.isArray(detail)) {
|
||||
w = Number(detail[0]) || 0;
|
||||
h = Number(detail[1]) || 0;
|
||||
} else if (detail && typeof detail === "object") {
|
||||
w = Number(detail.width ?? detail.w) || 0;
|
||||
h = Number(detail.height ?? detail.h) || 0;
|
||||
}
|
||||
if (!w) w = Number(anyEvent.width) || 0;
|
||||
if (!h) h = Number(anyEvent.height) || 0;
|
||||
|
||||
// Log the first few raw events so we can see what the backend
|
||||
// actually delivers (helps debug the unreliable size reporting).
|
||||
if (resizeEventLogsLeft > 0) {
|
||||
resizeEventLogsLeft--;
|
||||
try {
|
||||
console.log(
|
||||
`${PREFIX} Native resize event: parsed=${w}x${h}, detail=${
|
||||
JSON.stringify(detail)
|
||||
}, event.width/height=${anyEvent.width}/${anyEvent.height}`,
|
||||
);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
|
||||
if (w >= 1100 && h >= 700) {
|
||||
settings.windowWidth = w;
|
||||
settings.windowHeight = h;
|
||||
@@ -742,12 +821,11 @@ try {
|
||||
"close",
|
||||
(() => {
|
||||
if (resizeTimer !== undefined) clearTimeout(resizeTimer);
|
||||
// Prefer the size reported by native resize events. getSize() has
|
||||
// been observed to return a stale value (the last programmatically
|
||||
// requested size) instead of the actual user-resized window size,
|
||||
// which caused the same size to be saved on every close. Only fall
|
||||
// back to getSize() if no resize event was ever received.
|
||||
if (!sizeSeenFromEvent) {
|
||||
// Prefer sizes reported by native resize events or by the webview.
|
||||
// getSize() has been observed to return a stale value (the last
|
||||
// programmatically requested size) instead of the actual window
|
||||
// size, which caused the same size to be saved on every close.
|
||||
if (!sizeSeenFromEvent && !sizeSeenFromWebview) {
|
||||
try {
|
||||
const [fw, fh] = win.getSize();
|
||||
if (fw >= 1100 && fh >= 700) {
|
||||
@@ -760,10 +838,11 @@ try {
|
||||
const path = resolveSettingsPath();
|
||||
Deno.mkdirSync(dirname(path), { recursive: true });
|
||||
Deno.writeTextFileSync(path, JSON.stringify(settings, null, 2));
|
||||
const source = sizeSeenFromEvent
|
||||
? "resize events"
|
||||
: (sizeSeenFromWebview ? "webview" : "getSize()");
|
||||
console.log(
|
||||
`${PREFIX} Window size saved on close: ${settings.windowWidth}x${settings.windowHeight} (source: ${
|
||||
sizeSeenFromEvent ? "resize events" : "getSize()"
|
||||
})`,
|
||||
`${PREFIX} Window size saved on close: ${settings.windowWidth}x${settings.windowHeight} (source: ${source})`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
|
||||
31
src/data.ts
31
src/data.ts
@@ -24,6 +24,17 @@ export function validateDocuments(value: unknown): BuyerDocument[] {
|
||||
});
|
||||
}
|
||||
|
||||
// Normalized person key from the extracted buyer name: lowercase,
|
||||
// punctuation stripped, tokens sorted for order-insensitive comparison.
|
||||
function personKey(docs: BuyerDocument[]): string | null {
|
||||
const name = docs.map((d) => d.prospective_buyer).find((v) => text(v));
|
||||
if (!name) return null;
|
||||
const tokens = String(name).toLocaleLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, " ").trim().split(/\s+/).filter(Boolean)
|
||||
.sort();
|
||||
return tokens.length ? tokens.join(" ") : null;
|
||||
}
|
||||
|
||||
export function groupDocuments(documents: BuyerDocument[]): PersonGroup[] {
|
||||
const groups = new Map<string, BuyerDocument[]>();
|
||||
for (const doc of documents) {
|
||||
@@ -33,6 +44,24 @@ export function groupDocuments(documents: BuyerDocument[]): PersonGroup[] {
|
||||
groups.set(key, bucket);
|
||||
}
|
||||
|
||||
// Merge filename-based buckets that share the same prospective_buyer
|
||||
// (catches typos in scan filenames, e.g. "Zaboor" vs "Zahoor").
|
||||
const byPerson = new Map<string, string>(); // personKey -> bucket key
|
||||
for (const [key, docs] of [...groups.entries()]) {
|
||||
const pKey = personKey(docs);
|
||||
if (!pKey) continue;
|
||||
const existingKey = byPerson.get(pKey);
|
||||
if (existingKey === undefined) {
|
||||
byPerson.set(pKey, key);
|
||||
continue;
|
||||
}
|
||||
const target = groups.get(existingKey);
|
||||
if (target) {
|
||||
target.push(...docs);
|
||||
groups.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
return [...groups.entries()]
|
||||
.map(([key, docs]) => {
|
||||
docs.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||
@@ -66,4 +95,4 @@ export function filterGroups(
|
||||
return groups.filter((group) =>
|
||||
terms.every((term) => group.searchText.includes(term))
|
||||
);
|
||||
}
|
||||
}
|
||||
65
web/app.js
65
web/app.js
@@ -51,6 +51,19 @@ const esc = (value) =>
|
||||
"'": "'",
|
||||
})[char]);
|
||||
|
||||
// Normalized person key from the vision-extracted buyer name: lowercase,
|
||||
// punctuation stripped, name tokens sorted so "Zahoor Bilal" and
|
||||
// "Bilal Zahoor" compare equal.
|
||||
function personKey(group) {
|
||||
const name = group.docs.map((d) => d.prospective_buyer).find((v) =>
|
||||
typeof v === "string" && v.trim()
|
||||
);
|
||||
if (!name) return null;
|
||||
const tokens = name.toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ")
|
||||
.trim().split(/\s+/).filter(Boolean).sort();
|
||||
return tokens.length ? tokens.join(" ") : null;
|
||||
}
|
||||
|
||||
function buildGroups(docs) {
|
||||
const map = new Map();
|
||||
docs.forEach((doc, index) => {
|
||||
@@ -71,7 +84,33 @@ function buildGroups(docs) {
|
||||
].filter(Boolean).join(" ");
|
||||
map.set(key, group);
|
||||
});
|
||||
return [...map.values()].sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
// Second pass: merge filename-based groups that refer to the same person
|
||||
// according to the extracted prospective_buyer. This catches typos in the
|
||||
// scan filenames (e.g. "Zaboor, Bilal" vs "Zahoor, Bilal") which would
|
||||
// otherwise show the same buyer twice. Groups without a prospective_buyer
|
||||
// are never merged.
|
||||
const byPerson = new Map();
|
||||
const merged = [];
|
||||
for (const group of map.values()) {
|
||||
const pKey = personKey(group);
|
||||
const target = pKey ? byPerson.get(pKey) : undefined;
|
||||
if (target) {
|
||||
target.docs.push(...group.docs);
|
||||
target.text += " " + group.text;
|
||||
continue;
|
||||
}
|
||||
if (pKey) byPerson.set(pKey, group);
|
||||
merged.push(group);
|
||||
}
|
||||
for (const group of merged) {
|
||||
group.docs.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||
const preferred = group.docs.find((d) =>
|
||||
typeof d.prospective_buyer === "string" && d.prospective_buyer.trim()
|
||||
);
|
||||
if (preferred) group.displayName = preferred.prospective_buyer;
|
||||
}
|
||||
return merged.sort((a, b) => a.key.localeCompare(b.key));
|
||||
}
|
||||
|
||||
function visibleGroups() {
|
||||
@@ -298,4 +337,26 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
void load();
|
||||
void load();
|
||||
|
||||
// ---- Window size reporting ----
|
||||
// The backend's native size APIs are unreliable (see main.ts), so the
|
||||
// webview reports its own viewport size. The first report, sent shortly
|
||||
// after startup, calibrates the decoration offset on the backend; later
|
||||
// reports track user resizes.
|
||||
let metricsTimer = null;
|
||||
function reportWindowMetrics() {
|
||||
fetch("/api/window-metrics", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
innerWidth: globalThis.innerWidth,
|
||||
innerHeight: globalThis.innerHeight,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
globalThis.addEventListener("resize", () => {
|
||||
clearTimeout(metricsTimer);
|
||||
metricsTimer = setTimeout(reportWindowMetrics, 250);
|
||||
});
|
||||
setTimeout(reportWindowMetrics, 800); // calibration report
|
||||
Reference in New Issue
Block a user