update
This commit is contained in:
139
web/app.js
139
web/app.js
@@ -16,27 +16,37 @@ const fieldDefs = [
|
||||
["Name / Company", "name_company"],
|
||||
["Prospective Buyer", "prospective_buyer"],
|
||||
["Company", "company"],
|
||||
null,
|
||||
["Phone", "phone"],
|
||||
["Cell", "cell"],
|
||||
["Email", "email"],
|
||||
null,
|
||||
["Address", "address"],
|
||||
["State", "state"],
|
||||
["How did you hear", "how_did_you_hear"],
|
||||
["Interested in updates", "interested_in_updates"],
|
||||
["Background experience", "background_experience"],
|
||||
["Types of businesses", "types_of_business_raw"],
|
||||
["Businesses from Notes", "notes_business_raw"],
|
||||
["Types of Businesses", "types_of_business_raw"],
|
||||
["Background Experience", "background_experience"],
|
||||
null,
|
||||
["How Did You Hear", "how_did_you_hear"],
|
||||
["Interested in Updates", "interested_in_updates"],
|
||||
["Down Payment", { key: "down_payment_raw", fallback: "down_payment" }],
|
||||
["Total Purchase Price", "total_purchase_price"],
|
||||
["Date of Introduction", "date_of_introduction"],
|
||||
["Down payment", "down_payment_raw"],
|
||||
["Total purchase price", "total_purchase_price"],
|
||||
null,
|
||||
["Notes Page", "_notes_page"],
|
||||
["Buyer Info Page", "_info_page"],
|
||||
["CA Page", "_ca_page"],
|
||||
];
|
||||
|
||||
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||||
})[char]);
|
||||
const esc = (value) =>
|
||||
String(value ?? "").replace(/[&<>"']/g, (char) =>
|
||||
({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[char]);
|
||||
|
||||
function buildGroups(docs) {
|
||||
const map = new Map();
|
||||
@@ -63,12 +73,15 @@ function buildGroups(docs) {
|
||||
|
||||
function visibleGroups() {
|
||||
const terms = search.value.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
||||
return groups.filter((group) => terms.every((term) => group.text.toLowerCase().includes(term)));
|
||||
return groups.filter((group) =>
|
||||
terms.every((term) => group.text.toLowerCase().includes(term))
|
||||
);
|
||||
}
|
||||
|
||||
function updateStatus(shownCount = visibleGroups().length) {
|
||||
const source = state.dataSource === "sample" ? "sample data" : "JSON file";
|
||||
status.textContent = `${shownCount} people / ${state.documents.length} documents · ${source}`;
|
||||
status.textContent =
|
||||
`${shownCount} people / ${state.documents.length} documents \u00b7 ${source}`;
|
||||
status.className = "";
|
||||
}
|
||||
|
||||
@@ -81,11 +94,17 @@ function renderList() {
|
||||
const shown = visibleGroups();
|
||||
people.innerHTML = shown.map((group) => `
|
||||
<div class="person">
|
||||
<div class="person-title">${esc(group.displayName)} <span class="muted">(${group.docs.length})</span></div>
|
||||
${group.docs.map((doc) => `
|
||||
<button class="doc ${doc.index === selectedIndex ? "active" : ""}" data-index="${doc.index}">
|
||||
<div class="person-title">${
|
||||
esc(group.displayName)
|
||||
} <span class="muted">(${group.docs.length})</span></div>
|
||||
${
|
||||
group.docs.map((doc) => `
|
||||
<button class="doc ${
|
||||
doc.index === selectedIndex ? "active" : ""
|
||||
}" data-index="${doc.index}">
|
||||
${esc(doc.file_name)}
|
||||
</button>`).join("")}
|
||||
</button>`).join("")
|
||||
}
|
||||
</div>
|
||||
`).join("");
|
||||
updateStatus(shown.length);
|
||||
@@ -99,13 +118,17 @@ async function loadPdf(index) {
|
||||
pdf.removeAttribute("src");
|
||||
viewer.classList.remove("loaded");
|
||||
pdfMessage.hidden = false;
|
||||
pdfMessage.textContent = "Loading PDF…";
|
||||
pdfMessage.textContent = "Loading PDF\u2026";
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/pdf?index=${index}`, { cache: "no-store" });
|
||||
const response = await fetch(`/api/pdf?index=${index}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw new Error(body.error || `PDF request failed with HTTP ${response.status}.`);
|
||||
throw new Error(
|
||||
body.error || `PDF request failed with HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
currentPdfUrl = URL.createObjectURL(blob);
|
||||
@@ -123,13 +146,33 @@ function select(index) {
|
||||
selectedIndex = index;
|
||||
const doc = state.documents[index];
|
||||
if (!doc) return;
|
||||
|
||||
let rowIdx = 0;
|
||||
fields.innerHTML = `
|
||||
<h2>${esc(doc.name_from_filename)}</h2>
|
||||
<p class="muted">${esc(doc.file_name)} · ${esc(doc._doc_type || "unknown")} · ${esc(doc._pages_total ?? "?")} pages</p>
|
||||
${doc._vision_error ? `<p class="error">Vision error: ${esc(doc._vision_error)}</p>` : ""}
|
||||
${fieldDefs.map(([label, key]) => `
|
||||
<div class="field"><b>${label}</b><div>${esc(doc[key] || "—")}</div></div>
|
||||
`).join("")}
|
||||
<p class="muted">${esc(doc.file_name)} \u00b7 ${
|
||||
esc(doc._doc_type || "unknown")
|
||||
} \u00b7 ${esc(doc._pages_total ?? "?")} pages</p>
|
||||
${
|
||||
doc._vision_error
|
||||
? `<p class="error">Vision error: ${esc(doc._vision_error)}</p>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
fieldDefs.map((def) => {
|
||||
if (!def) return '<hr class="field-sep">';
|
||||
const [label, keyOrObj] = def;
|
||||
let value;
|
||||
if (typeof keyOrObj === "object") {
|
||||
value = esc(doc[keyOrObj.key] || doc[keyOrObj.fallback] || "\u2014");
|
||||
} else {
|
||||
value = esc(doc[keyOrObj] || "\u2014");
|
||||
}
|
||||
const bgClass = rowIdx % 2 === 0 ? "row-even" : "row-odd";
|
||||
rowIdx++;
|
||||
return `<div class="field ${bgClass}"><b>${label}</b><div>${value}</div></div>`;
|
||||
}).join("")
|
||||
}
|
||||
`;
|
||||
void loadPdf(index);
|
||||
renderList();
|
||||
@@ -147,19 +190,29 @@ document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
const delta = event.key === "ArrowDown" ? 1 : -1;
|
||||
select(Math.max(0, Math.min(state.documents.length - 1, selectedIndex < 0 ? 0 : selectedIndex + delta)));
|
||||
select(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
state.documents.length - 1,
|
||||
selectedIndex < 0 ? 0 : selectedIndex + delta,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const dialog = document.querySelector("#settingsDialog");
|
||||
const jsonPath = document.querySelector("#jsonPath");
|
||||
const pdfBase = document.querySelector("#pdfBase");
|
||||
const useAnonData = document.querySelector("#useAnonData");
|
||||
const settingsError = document.querySelector("#settingsError");
|
||||
const saveSettings = document.querySelector("#saveSettings");
|
||||
|
||||
document.querySelector("#settings").onclick = () => {
|
||||
jsonPath.value = state.config.jsonPath;
|
||||
pdfBase.value = state.config.pdfBaseDirectory;
|
||||
jsonPath.value = state.config.jsonPath || "";
|
||||
pdfBase.value = state.config.pdfBaseDirectory || "";
|
||||
useAnonData.checked = !!state.config.useAnonymousData;
|
||||
settingsError.hidden = true;
|
||||
settingsError.textContent = "";
|
||||
dialog.showModal();
|
||||
@@ -177,6 +230,7 @@ saveSettings.onclick = async (event) => {
|
||||
body: JSON.stringify({
|
||||
jsonPath: jsonPath.value,
|
||||
pdfBaseDirectory: pdfBase.value,
|
||||
useAnonymousData: useAnonData.checked,
|
||||
}),
|
||||
});
|
||||
const body = await response.json();
|
||||
@@ -184,17 +238,46 @@ saveSettings.onclick = async (event) => {
|
||||
dialog.close();
|
||||
await load();
|
||||
} catch (error) {
|
||||
settingsError.textContent = error instanceof Error ? error.message : String(error);
|
||||
settingsError.textContent = error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
settingsError.hidden = false;
|
||||
} finally {
|
||||
saveSettings.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
let resizeTimer;
|
||||
globalThis.addEventListener("resize", () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
fetch("/api/window-size", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
width: globalThis.innerWidth,
|
||||
height: globalThis.innerHeight,
|
||||
}),
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
globalThis.addEventListener("beforeunload", () => {
|
||||
navigator.sendBeacon(
|
||||
"/api/window-size",
|
||||
JSON.stringify({
|
||||
width: globalThis.innerWidth,
|
||||
height: globalThis.innerHeight,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await fetch("/api/state", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`State request failed with HTTP ${response.status}.`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`State request failed with HTTP ${response.status}.`);
|
||||
}
|
||||
state = await response.json();
|
||||
groups = buildGroups(state.documents);
|
||||
showError(state.loadError || "");
|
||||
|
||||
Reference in New Issue
Block a user