Spaces:
Sleeping
Sleeping
Update src/App.jsx
Browse files- src/App.jsx +119 -6
src/App.jsx
CHANGED
|
@@ -28,12 +28,21 @@ const TABLE_ANCHORS = [
|
|
| 28 |
{ key: "qty", expected_text: "Qty" },
|
| 29 |
];
|
| 30 |
|
|
|
|
|
|
|
| 31 |
const TABLE_COLUMNS = [
|
| 32 |
{ key: "item_number", label: "Item Number" },
|
|
|
|
| 33 |
{ key: "description", label: "Description" },
|
| 34 |
{ key: "qty", label: "Qty" },
|
|
|
|
|
|
|
|
|
|
| 35 |
];
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
/** ---------- normalization helpers ---------- */
|
| 38 |
|
| 39 |
function normBBox(b, w, h) {
|
|
@@ -156,6 +165,10 @@ export default function App() {
|
|
| 156 |
|
| 157 |
// NEW: template id comes from URL OR manual input
|
| 158 |
const [templateId, setTemplateId] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
const [pageSize, setPageSize] = useState({ w: 0, h: 0 });
|
| 161 |
const [selectedFieldId, setSelectedFieldId] = useState(FIELDS[0].fieldId);
|
|
@@ -249,6 +262,10 @@ export default function App() {
|
|
| 249 |
const tid = sp.get("template_id");
|
| 250 |
setTemplateId((tid || "").trim());
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
if (!id) {
|
| 253 |
setPdfUrl(null);
|
| 254 |
setPdfName(null);
|
|
@@ -471,12 +488,13 @@ export default function App() {
|
|
| 471 |
return applyOffset(st.anchor, st.valueOffsetPx);
|
| 472 |
}
|
| 473 |
|
| 474 |
-
|
|
|
|
| 475 |
if (!pageSize.w || !pageSize.h) return null;
|
| 476 |
|
| 477 |
return {
|
| 478 |
form_id: pdfId ? `trainer_${pdfId}` : "trainer_form_v1",
|
| 479 |
-
|
| 480 |
page: 1,
|
| 481 |
scalar_value_region_mode: offsetMode ? "offset_from_anchor_v1" : "absolute_bbox_v1",
|
| 482 |
fields: FIELDS.map((f) => {
|
|
@@ -564,11 +582,55 @@ export default function App() {
|
|
| 564 |
};
|
| 565 |
}
|
| 566 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
const previewConfig = useMemo(
|
| 568 |
() => buildConfig(),
|
| 569 |
-
|
|
|
|
| 570 |
);
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
async function saveConfig() {
|
| 573 |
if (!pdfId) {
|
| 574 |
setSendStatus({ state: "err", msg: "Missing pdf_id in URL." });
|
|
@@ -652,7 +714,7 @@ export default function App() {
|
|
| 652 |
)}
|
| 653 |
</div>
|
| 654 |
|
| 655 |
-
{/*
|
| 656 |
<div style={{ marginBottom: 14 }}>
|
| 657 |
<div style={UI.label}>Template ID (required)</div>
|
| 658 |
<input
|
|
@@ -666,6 +728,44 @@ export default function App() {
|
|
| 666 |
</div>
|
| 667 |
</div>
|
| 668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
{/* Zoom / Pan controls */}
|
| 670 |
<div style={{ marginBottom: 14 }}>
|
| 671 |
<div style={UI.label}>View (Zoom / Pan)</div>
|
|
@@ -830,7 +930,7 @@ export default function App() {
|
|
| 830 |
))}
|
| 831 |
</select>
|
| 832 |
<div style={{ fontSize: 12, color: UI.subtle, marginTop: 6 }}>
|
| 833 |
-
Columns should be drawn inside the table region.
|
| 834 |
</div>
|
| 835 |
</div>
|
| 836 |
)}
|
|
@@ -881,6 +981,19 @@ export default function App() {
|
|
| 881 |
{sendStatus.state === "sending" ? "Saving..." : "Save Config"}
|
| 882 |
</button>
|
| 883 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
{sendStatus.state !== "idle" && (
|
| 885 |
<div
|
| 886 |
style={{
|
|
@@ -1088,7 +1201,7 @@ export default function App() {
|
|
| 1088 |
>
|
| 1089 |
<h3 style={{ margin: "0 0 10px", fontSize: 18 }}>Config Preview</h3>
|
| 1090 |
<div style={{ fontSize: 12, color: "#9aa4b2", marginBottom: 10 }}>
|
| 1091 |
-
Live preview of what will be saved.
|
| 1092 |
</div>
|
| 1093 |
|
| 1094 |
<pre
|
|
|
|
| 28 |
{ key: "qty", expected_text: "Qty" },
|
| 29 |
];
|
| 30 |
|
| 31 |
+
// Canonical line-item column set. Keep this in sync with the backend
|
| 32 |
+
// (prompts.py + generate_template_schema_skeletons.py).
|
| 33 |
const TABLE_COLUMNS = [
|
| 34 |
{ key: "item_number", label: "Item Number" },
|
| 35 |
+
{ key: "lot_number", label: "Lot Number" },
|
| 36 |
{ key: "description", label: "Description" },
|
| 37 |
{ key: "qty", label: "Qty" },
|
| 38 |
+
{ key: "price", label: "Price" },
|
| 39 |
+
{ key: "total", label: "Total" },
|
| 40 |
+
{ key: "bundle", label: "Bundle" }, // NEW: bundle column
|
| 41 |
];
|
| 42 |
|
| 43 |
+
// Schema version (matches T10 "version_schema")
|
| 44 |
+
const SCHEMA_VERSION = 3;
|
| 45 |
+
|
| 46 |
/** ---------- normalization helpers ---------- */
|
| 47 |
|
| 48 |
function normBBox(b, w, h) {
|
|
|
|
| 165 |
|
| 166 |
// NEW: template id comes from URL OR manual input
|
| 167 |
const [templateId, setTemplateId] = useState("");
|
| 168 |
+
// NEW: template metadata so the exported config matches the registry template format
|
| 169 |
+
const [templateName, setTemplateName] = useState("");
|
| 170 |
+
const [templateVersion, setTemplateVersion] = useState(1);
|
| 171 |
+
const [matchKeywordsAny, setMatchKeywordsAny] = useState("");
|
| 172 |
|
| 173 |
const [pageSize, setPageSize] = useState({ w: 0, h: 0 });
|
| 174 |
const [selectedFieldId, setSelectedFieldId] = useState(FIELDS[0].fieldId);
|
|
|
|
| 262 |
const tid = sp.get("template_id");
|
| 263 |
setTemplateId((tid || "").trim());
|
| 264 |
|
| 265 |
+
// Optional metadata via URL
|
| 266 |
+
const tname = sp.get("template_name");
|
| 267 |
+
if (tname) setTemplateName(tname.trim());
|
| 268 |
+
|
| 269 |
if (!id) {
|
| 270 |
setPdfUrl(null);
|
| 271 |
setPdfName(null);
|
|
|
|
| 488 |
return applyOffset(st.anchor, st.valueOffsetPx);
|
| 489 |
}
|
| 490 |
|
| 491 |
+
// Build ONLY the schema portion (flat) — fields + table mapping.
|
| 492 |
+
function buildSchema() {
|
| 493 |
if (!pageSize.w || !pageSize.h) return null;
|
| 494 |
|
| 495 |
return {
|
| 496 |
form_id: pdfId ? `trainer_${pdfId}` : "trainer_form_v1",
|
| 497 |
+
version_schema: SCHEMA_VERSION,
|
| 498 |
page: 1,
|
| 499 |
scalar_value_region_mode: offsetMode ? "offset_from_anchor_v1" : "absolute_bbox_v1",
|
| 500 |
fields: FIELDS.map((f) => {
|
|
|
|
| 582 |
};
|
| 583 |
}
|
| 584 |
|
| 585 |
+
// Build the FULL template object in the registry format (matches T10_*.json).
|
| 586 |
+
function buildConfig() {
|
| 587 |
+
const schema = buildSchema();
|
| 588 |
+
if (!schema) return null;
|
| 589 |
+
|
| 590 |
+
const tid = (templateId || "").trim();
|
| 591 |
+
const kwAny = (matchKeywordsAny || "")
|
| 592 |
+
.split(",")
|
| 593 |
+
.map((s) => s.trim())
|
| 594 |
+
.filter(Boolean);
|
| 595 |
+
|
| 596 |
+
return {
|
| 597 |
+
template_id: tid || "UNSET_TEMPLATE_ID",
|
| 598 |
+
name: (templateName || "").trim() || tid || "Untitled Template",
|
| 599 |
+
status: "active",
|
| 600 |
+
version: Number(templateVersion) || 1,
|
| 601 |
+
match: {
|
| 602 |
+
keywords_all: [],
|
| 603 |
+
keywords_any: kwAny,
|
| 604 |
+
},
|
| 605 |
+
// flat schema (same level as T10)
|
| 606 |
+
...schema,
|
| 607 |
+
};
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
const previewConfig = useMemo(
|
| 611 |
() => buildConfig(),
|
| 612 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 613 |
+
[pageSize.w, pageSize.h, boxes, offsetMode, rowHeightHintPx, pdfId, templateId, templateName, templateVersion, matchKeywordsAny]
|
| 614 |
);
|
| 615 |
|
| 616 |
+
function downloadTemplateJson() {
|
| 617 |
+
const cfg = buildConfig();
|
| 618 |
+
if (!cfg) {
|
| 619 |
+
setSendStatus({ state: "err", msg: "Config not ready. Load a PDF first." });
|
| 620 |
+
return;
|
| 621 |
+
}
|
| 622 |
+
const tid = (templateId || "").trim() || "template";
|
| 623 |
+
const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: "application/json" });
|
| 624 |
+
const url = URL.createObjectURL(blob);
|
| 625 |
+
const a = document.createElement("a");
|
| 626 |
+
a.href = url;
|
| 627 |
+
a.download = `${tid}.json`;
|
| 628 |
+
document.body.appendChild(a);
|
| 629 |
+
a.click();
|
| 630 |
+
document.body.removeChild(a);
|
| 631 |
+
URL.revokeObjectURL(url);
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
async function saveConfig() {
|
| 635 |
if (!pdfId) {
|
| 636 |
setSendStatus({ state: "err", msg: "Missing pdf_id in URL." });
|
|
|
|
| 714 |
)}
|
| 715 |
</div>
|
| 716 |
|
| 717 |
+
{/* Template ID */}
|
| 718 |
<div style={{ marginBottom: 14 }}>
|
| 719 |
<div style={UI.label}>Template ID (required)</div>
|
| 720 |
<input
|
|
|
|
| 728 |
</div>
|
| 729 |
</div>
|
| 730 |
|
| 731 |
+
{/* Template Name */}
|
| 732 |
+
<div style={{ marginBottom: 14 }}>
|
| 733 |
+
<div style={UI.label}>Template Name</div>
|
| 734 |
+
<input
|
| 735 |
+
style={UI.input}
|
| 736 |
+
value={templateName}
|
| 737 |
+
onChange={(e) => setTemplateName(e.target.value)}
|
| 738 |
+
placeholder="e.g. Zavation Sales Order Form"
|
| 739 |
+
/>
|
| 740 |
+
</div>
|
| 741 |
+
|
| 742 |
+
{/* Match keywords */}
|
| 743 |
+
<div style={{ marginBottom: 14 }}>
|
| 744 |
+
<div style={UI.label}>Match keywords (comma separated)</div>
|
| 745 |
+
<input
|
| 746 |
+
style={UI.input}
|
| 747 |
+
value={matchKeywordsAny}
|
| 748 |
+
onChange={(e) => setMatchKeywordsAny(e.target.value)}
|
| 749 |
+
placeholder="e.g. zavation, sales order, purchase order"
|
| 750 |
+
/>
|
| 751 |
+
<div style={{ fontSize: 12, color: UI.subtle, marginTop: 6 }}>
|
| 752 |
+
Stored as <code>match.keywords_any</code> for classification.
|
| 753 |
+
</div>
|
| 754 |
+
</div>
|
| 755 |
+
|
| 756 |
+
{/* Template version */}
|
| 757 |
+
<div style={{ marginBottom: 14 }}>
|
| 758 |
+
<div style={UI.label}>Template Version</div>
|
| 759 |
+
<input
|
| 760 |
+
style={UI.input}
|
| 761 |
+
type="number"
|
| 762 |
+
min="1"
|
| 763 |
+
step="1"
|
| 764 |
+
value={templateVersion}
|
| 765 |
+
onChange={(e) => setTemplateVersion(Number(e.target.value || 1))}
|
| 766 |
+
/>
|
| 767 |
+
</div>
|
| 768 |
+
|
| 769 |
{/* Zoom / Pan controls */}
|
| 770 |
<div style={{ marginBottom: 14 }}>
|
| 771 |
<div style={UI.label}>View (Zoom / Pan)</div>
|
|
|
|
| 930 |
))}
|
| 931 |
</select>
|
| 932 |
<div style={{ fontSize: 12, color: UI.subtle, marginTop: 6 }}>
|
| 933 |
+
Columns should be drawn inside the table region. (Includes <b>Bundle</b>.)
|
| 934 |
</div>
|
| 935 |
</div>
|
| 936 |
)}
|
|
|
|
| 981 |
{sendStatus.state === "sending" ? "Saving..." : "Save Config"}
|
| 982 |
</button>
|
| 983 |
|
| 984 |
+
{/* DOWNLOAD as <template_id>.json in registry format */}
|
| 985 |
+
<button
|
| 986 |
+
onClick={downloadTemplateJson}
|
| 987 |
+
disabled={!pdfUrl}
|
| 988 |
+
style={{
|
| 989 |
+
...UI.btnBase,
|
| 990 |
+
...(pdfUrl ? UI.blue : { background: "rgba(255,255,255,0.10)", color: "#aaa" }),
|
| 991 |
+
marginTop: 10,
|
| 992 |
+
}}
|
| 993 |
+
>
|
| 994 |
+
Download Template JSON
|
| 995 |
+
</button>
|
| 996 |
+
|
| 997 |
{sendStatus.state !== "idle" && (
|
| 998 |
<div
|
| 999 |
style={{
|
|
|
|
| 1201 |
>
|
| 1202 |
<h3 style={{ margin: "0 0 10px", fontSize: 18 }}>Config Preview</h3>
|
| 1203 |
<div style={{ fontSize: 12, color: "#9aa4b2", marginBottom: 10 }}>
|
| 1204 |
+
Live preview of what will be saved (registry template format).
|
| 1205 |
</div>
|
| 1206 |
|
| 1207 |
<pre
|