diff --git a/agrifine-extension/dist/background.js b/agrifine-extension/dist/background.js index a5de481..3428814 100644 --- a/agrifine-extension/dist/background.js +++ b/agrifine-extension/dist/background.js @@ -12,6 +12,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ getAgRefineUrl: () => (/* binding */ getAgRefineUrl), /* harmony export */ getSyncLog: () => (/* binding */ getSyncLog), +/* harmony export */ pushToAgRefine: () => (/* binding */ pushToAgRefine), /* harmony export */ setAgRefineUrl: () => (/* binding */ setAgRefineUrl), /* harmony export */ syncFromAgRefine: () => (/* binding */ syncFromAgRefine) /* harmony export */ }); @@ -267,38 +268,117 @@ function extractLoads(raw) { } return loads; } -function syncFromAgRefine() { - return _syncFromAgRefine.apply(this, arguments); + +// Injected into AG-Refine tab to write field data back into its localStorage +function writeFieldsToAgRefineTab(fields) { + try { + localStorage.setItem('agrifine_pushed_fields', JSON.stringify(fields)); + localStorage.setItem('agrifine_pushed_at', new Date().toISOString()); + // Dispatch an event so a listening AG-Refine app can react immediately + window.dispatchEvent(new CustomEvent('agrifine:fields-updated', { + detail: { + fields: fields + } + })); + return { + ok: true, + count: fields.length + }; + } catch (err) { + return { + ok: false, + error: err.message + }; + } } -function _syncFromAgRefine() { - _syncFromAgRefine = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() { - var configuredUrl, allTabs, agRefineTabs, tab, raw, _yield$chrome$scripti, _yield$chrome$scripti2, result, fields, loads, existing, added, updated, _iterator3, _step3, _loop, log, history, _t7, _t8; - return _regenerator().w(function (_context5) { - while (1) switch (_context5.p = _context5.n) { +function pushToAgRefine(_x2) { + return _pushToAgRefine.apply(this, arguments); +} +function _pushToAgRefine() { + _pushToAgRefine = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(profiles) { + var configuredUrl, allTabs, agRefineTabs, tab, _yield$chrome$scripti, _yield$chrome$scripti2, result, _t7; + return _regenerator().w(function (_context4) { + while (1) switch (_context4.p = _context4.n) { case 0: - _context5.n = 1; + _context4.n = 1; return getAgRefineUrl(); case 1: - configuredUrl = _context5.v; - _context5.n = 2; + configuredUrl = _context4.v; + _context4.n = 2; return chrome.tabs.query({}); case 2: - allTabs = _context5.v; + allTabs = _context4.v; agRefineTabs = allTabs.filter(function (t) { return tabMatchesAgRefine(t, configuredUrl); }); if (!(agRefineTabs.length === 0)) { - _context5.n = 3; + _context4.n = 3; break; } - return _context5.a(2, { + return _context4.a(2, { + ok: false, + error: 'No AG-Refine tab found. Open AG-Refine first.' + }); + case 3: + tab = agRefineTabs[0]; + _context4.p = 4; + _context4.n = 5; + return chrome.scripting.executeScript({ + target: { + tabId: tab.id + }, + func: writeFieldsToAgRefineTab, + args: [profiles] + }); + case 5: + _yield$chrome$scripti = _context4.v; + _yield$chrome$scripti2 = _slicedToArray(_yield$chrome$scripti, 1); + result = _yield$chrome$scripti2[0]; + return _context4.a(2, result.result); + case 6: + _context4.p = 6; + _t7 = _context4.v; + return _context4.a(2, { + ok: false, + error: "Cannot write to AG-Refine tab: ".concat(_t7.message) + }); + } + }, _callee4, null, [[4, 6]]); + })); + return _pushToAgRefine.apply(this, arguments); +} +function syncFromAgRefine() { + return _syncFromAgRefine.apply(this, arguments); +} +function _syncFromAgRefine() { + _syncFromAgRefine = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() { + var configuredUrl, allTabs, agRefineTabs, tab, raw, _yield$chrome$scripti3, _yield$chrome$scripti4, result, fields, loads, existing, added, updated, _iterator3, _step3, _loop, log, history, _t8, _t9; + return _regenerator().w(function (_context6) { + while (1) switch (_context6.p = _context6.n) { + case 0: + _context6.n = 1; + return getAgRefineUrl(); + case 1: + configuredUrl = _context6.v; + _context6.n = 2; + return chrome.tabs.query({}); + case 2: + allTabs = _context6.v; + agRefineTabs = allTabs.filter(function (t) { + return tabMatchesAgRefine(t, configuredUrl); + }); + if (!(agRefineTabs.length === 0)) { + _context6.n = 3; + break; + } + return _context6.a(2, { ok: false, error: 'No AG-Refine tab found. Open AG-Refine in a browser tab first.' }); case 3: tab = agRefineTabs[0]; - _context5.p = 4; - _context5.n = 5; + _context6.p = 4; + _context6.n = 5; return chrome.scripting.executeScript({ target: { tabId: tab.id @@ -306,41 +386,41 @@ function _syncFromAgRefine() { func: scrapeAgRefineTab }); case 5: - _yield$chrome$scripti = _context5.v; - _yield$chrome$scripti2 = _slicedToArray(_yield$chrome$scripti, 1); - result = _yield$chrome$scripti2[0]; + _yield$chrome$scripti3 = _context6.v; + _yield$chrome$scripti4 = _slicedToArray(_yield$chrome$scripti3, 1); + result = _yield$chrome$scripti4[0]; raw = result.result; - _context5.n = 7; + _context6.n = 7; break; case 6: - _context5.p = 6; - _t7 = _context5.v; - return _context5.a(2, { + _context6.p = 6; + _t8 = _context6.v; + return _context6.a(2, { ok: false, - error: "Cannot read AG-Refine tab: ".concat(_t7.message) + error: "Cannot read AG-Refine tab: ".concat(_t8.message) }); case 7: fields = extractFields(raw); loads = extractLoads(raw); // Merge fields — update existing by name, insert new ones - _context5.n = 8; + _context6.n = 8; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)(); case 8: - existing = _context5.v; + existing = _context6.v; added = 0; updated = 0; _iterator3 = _createForOfIteratorHelper(fields); - _context5.p = 9; + _context6.p = 9; _loop = /*#__PURE__*/_regenerator().m(function _loop() { var f, match, _match$coordinates, _match$cropHistory, _match$notes, _match$cluId, merged; - return _regenerator().w(function (_context4) { - while (1) switch (_context4.n) { + return _regenerator().w(function (_context5) { + while (1) switch (_context5.n) { case 0: f = _step3.value; match = existing.find(function (e) { return e.name.toLowerCase() === f.name.toLowerCase(); }); if (!match) { - _context4.n = 2; + _context5.n = 2; break; } // Merge: fill in missing data without overwriting user edits @@ -351,43 +431,43 @@ function _syncFromAgRefine() { cluId: (_match$cluId = match.cluId) !== null && _match$cluId !== void 0 ? _match$cluId : f.cluId, _source: 'ag-refine-merged' }); - _context4.n = 1; + _context5.n = 1; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(merged); case 1: updated++; - _context4.n = 4; + _context5.n = 4; break; case 2: - _context4.n = 3; + _context5.n = 3; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(f); case 3: added++; case 4: - return _context4.a(2); + return _context5.a(2); } }, _loop); }); _iterator3.s(); case 10: if ((_step3 = _iterator3.n()).done) { - _context5.n = 12; + _context6.n = 12; break; } - return _context5.d(_regeneratorValues(_loop()), 11); + return _context6.d(_regeneratorValues(_loop()), 11); case 11: - _context5.n = 10; + _context6.n = 10; break; case 12: - _context5.n = 14; + _context6.n = 14; break; case 13: - _context5.p = 13; - _t8 = _context5.v; - _iterator3.e(_t8); + _context6.p = 13; + _t9 = _context6.v; + _iterator3.e(_t9); case 14: - _context5.p = 14; + _context6.p = 14; _iterator3.f(); - return _context5.f(14); + return _context6.f(14); case 15: log = { at: new Date().toISOString(), @@ -397,15 +477,15 @@ function _syncFromAgRefine() { loadsFound: loads.length, rawKeys: Object.keys(_objectSpread(_objectSpread({}, raw.localStorage), raw.sessionStorage)) }; - _context5.n = 16; + _context6.n = 16; return getSyncLog(); case 16: - history = _context5.v; + history = _context6.v; history.unshift(log); - _context5.n = 17; + _context6.n = 17; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.localSet)(SYNC_LOG_KEY, history.slice(0, 20)); case 17: - return _context5.a(2, { + return _context6.a(2, { ok: true, added: added, updated: updated, @@ -414,7 +494,7 @@ function _syncFromAgRefine() { tabUrl: tab.url }); } - }, _callee4, null, [[9, 13, 14, 15], [4, 6]]); + }, _callee5, null, [[9, 13, 14, 15], [4, 6]]); })); return _syncFromAgRefine.apply(this, arguments); } @@ -1391,6 +1471,16 @@ chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) { }); }); return true; + case 'AGREFINE_PUSH': + (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)().then(function (profiles) { + return (0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_2__.pushToAgRefine)(profiles); + }).then(sendResponse)["catch"](function (err) { + return sendResponse({ + ok: false, + error: err.message + }); + }); + return true; default: return false; } diff --git a/agrifine-extension/dist/sidebar.css b/agrifine-extension/dist/sidebar.css index 8197fcb..b8132af 100644 --- a/agrifine-extension/dist/sidebar.css +++ b/agrifine-extension/dist/sidebar.css @@ -579,6 +579,9 @@ video { .mt-2 { margin-top: 0.5rem; } +.mt-2\.5 { + margin-top: 0.625rem; +} .mt-3 { margin-top: 0.75rem; } @@ -609,6 +612,9 @@ video { .h-3 { height: 0.75rem; } +.h-3\.5 { + height: 0.875rem; +} .h-4 { height: 1rem; } @@ -645,6 +651,9 @@ video { .w-3 { width: 0.75rem; } +.w-3\.5 { + width: 0.875rem; +} .w-4 { width: 1rem; } @@ -663,6 +672,9 @@ video { .min-w-0 { min-width: 0px; } +.max-w-\[180px\] { + max-width: 180px; +} .max-w-\[85\%\] { max-width: 85%; } @@ -730,11 +742,21 @@ video { .gap-y-0\.5 { row-gap: 0.125rem; } +.space-y-0\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.125rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.125rem * var(--tw-space-y-reverse)); +} .space-y-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); } +.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.375rem * var(--tw-space-y-reverse)); +} .space-y-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); @@ -857,6 +879,10 @@ video { padding-left: 0.25rem; padding-right: 0.25rem; } +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -968,6 +994,9 @@ video { .uppercase { text-transform: uppercase; } +.italic { + font-style: italic; +} .leading-none { line-height: 1; } @@ -1014,6 +1043,10 @@ video { --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity, 1)); } +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity, 1)); +} .text-night-300 { --tw-text-opacity: 1; color: rgb(61 79 102 / var(--tw-text-opacity, 1)); @@ -1026,6 +1059,9 @@ video { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } +.underline { + text-decoration-line: underline; +} .accent-agri-500 { accent-color: #22c55e; } @@ -1226,6 +1262,9 @@ body { .last\:border-0:last-child { border-width: 0px; } +.last\:pb-0:last-child { + padding-bottom: 0px; +} .hover\:border-agri-500:hover { --tw-border-opacity: 1; border-color: rgb(34 197 94 / var(--tw-border-opacity, 1)); @@ -1261,6 +1300,9 @@ body { .hover\:underline:hover { text-decoration-line: underline; } +.hover\:no-underline:hover { + text-decoration-line: none; +} .disabled\:bg-night-500:disabled { --tw-bg-opacity: 1; background-color: rgb(37 48 71 / var(--tw-bg-opacity, 1)); diff --git a/agrifine-extension/dist/sidebar.js b/agrifine-extension/dist/sidebar.js index 62f4644..9937731 100644 --- a/agrifine-extension/dist/sidebar.js +++ b/agrifine-extension/dist/sidebar.js @@ -1794,6 +1794,18 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ }); /* harmony import */ var _utils_storage_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../utils/storage.js */ "./src/utils/storage.js"); /* harmony import */ var _utils_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../utils/api.js */ "./src/utils/api.js"); +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _regeneratorValues(e) { if (null != e) { var t = e["function" == typeof Symbol && Symbol.iterator || "@@iterator"], r = 0; if (t) return t.call(e); if ("function" == typeof e.next) return e; if (!isNaN(e.length)) return { next: function next() { return e && r >= e.length && (e = void 0), { value: e && e[r++], done: !e }; } }; } throw new TypeError(_typeof(e) + " is not iterable"); } +function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } +function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } +function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } +function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } +function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } +function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } @@ -1817,39 +1829,39 @@ function tryDocServer(_x) { return _tryDocServer.apply(this, arguments); } function _tryDocServer() { - _tryDocServer = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(file) { - var fd, res, _yield$res$json, text, _t5; - return _regenerator().w(function (_context7) { - while (1) switch (_context7.p = _context7.n) { + _tryDocServer = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0(file) { + var fd, res, _yield$res$json, text, _t6; + return _regenerator().w(function (_context1) { + while (1) switch (_context1.p = _context1.n) { case 0: - _context7.p = 0; + _context1.p = 0; fd = new FormData(); fd.append('file', file); - _context7.n = 1; + _context1.n = 1; return fetch("".concat(DOC_SERVER, "/parse"), { method: 'POST', body: fd }); case 1: - res = _context7.v; + res = _context1.v; if (res.ok) { - _context7.n = 2; + _context1.n = 2; break; } - return _context7.a(2, null); + return _context1.a(2, null); case 2: - _context7.n = 3; + _context1.n = 3; return res.json(); case 3: - _yield$res$json = _context7.v; + _yield$res$json = _context1.v; text = _yield$res$json.text; - return _context7.a(2, text !== null && text !== void 0 ? text : null); + return _context1.a(2, text !== null && text !== void 0 ? text : null); case 4: - _context7.p = 4; - _t5 = _context7.v; - return _context7.a(2, null); + _context1.p = 4; + _t6 = _context1.v; + return _context1.a(2, null); } - }, _callee7, null, [[0, 4]]); + }, _callee0, null, [[0, 4]]); })); return _tryDocServer.apply(this, arguments); } @@ -1863,7 +1875,8 @@ function DataIngestModule() { return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: - container.innerHTML = "\n
Data Ingest
\n\n \n
\n
\n \n \n \n

Drop CSV, Excel, or PDF here

\n

or click to select a file

\n \n
\n
\n
\n\n \n
\n "; + container.innerHTML = "\n
Data Ingest
\n\n \n
\n
\n \n \n \n

Drop CSV, Excel, or PDF here

\n

or click to select a file

\n \n
\n
\n
\n\n \n
\n
\n

Field data detected

\n

\n

\n \n
\n
\n\n \n
\n "; + _this._pendingImport = null; _this._bindEvents(container); _context.n = 1; return _this._renderFileList(container); @@ -1896,6 +1909,9 @@ function DataIngestModule() { fileInput.addEventListener('change', function () { if (fileInput.files[0]) _this2._processFile(fileInput.files[0], container); }); + container.querySelector('#import-to-fields-btn').addEventListener('click', function () { + return _this2._importToFields(container); + }); }, _processFile: function _processFile(file, container) { var _this3 = this; @@ -1914,6 +1930,9 @@ function DataIngestModule() { status.textContent = 'Unsupported file type.'; return _context2.a(2); case 1: + // Hide previous import banner + container.querySelector('#field-import-banner').classList.add('hidden'); + _this3._pendingImport = null; status.textContent = "Parsing ".concat(typeName, "\u2026"); extractedText = ''; // Try Python doc server first (more robust), fall back to browser-side _context2.n = 2; @@ -1973,7 +1992,7 @@ function DataIngestModule() { _context2.p = 12; _context2.n = 13; return (0,_utils_api_js__WEBPACK_IMPORTED_MODULE_1__.callAnthropic)({ - system: 'You are an agricultural data analyst. Extract and return structured JSON from this document. Identify: operation type, field names, dates, quantities, equipment, crop types, financial figures, and any carbon or emissions data. Return only valid JSON.', + system: 'You are an agricultural data analyst. Extract and return structured JSON from this document. Identify: operation type, field names (as "fields" array of strings), dates, quantities, equipment, crop types, financial figures, and any carbon or emissions data. For harvest data include avg_yield_bu_ac, avg_moisture_pct, harvest_date, and crop. Return only valid JSON.', userMessage: extractedText.slice(0, 6000), maxTokens: 1024 }); @@ -1999,7 +2018,7 @@ function DataIngestModule() { preview: Object.entries(structuredData !== null && structuredData !== void 0 ? structuredData : {}).filter(function (_ref) { var _ref2 = _slicedToArray(_ref, 1), k = _ref2[0]; - return k !== 'raw_preview'; + return k !== 'raw_preview' && k !== 'parse_error'; }).slice(0, 5).map(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), k = _ref4[0], @@ -2017,16 +2036,201 @@ function DataIngestModule() { _context2.n = 17; return _this3._renderFileList(container); case 17: + _context2.n = 18; + return _this3._offerFieldImport(record, container); + case 18: return _context2.a(2); } }, _callee2, null, [[12, 14], [3, 10]]); }))(); }, + _offerFieldImport: function _offerFieldImport(record, container) { + var _this4 = this; + return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() { + var sd, rawFields, profiles, matched, banner, desc, btn, importStatus; + return _regenerator().w(function (_context3) { + while (1) switch (_context3.n) { + case 0: + sd = record.structuredData; + if (!(!sd || sd.parse_error)) { + _context3.n = 1; + break; + } + return _context3.a(2); + case 1: + // Collect field names from multiple possible keys + rawFields = [].concat(_toConsumableArray(Array.isArray(sd.fields) ? sd.fields : []), _toConsumableArray(Array.isArray(sd.field_names) ? sd.field_names : [])).map(function (f) { + return String(f).trim(); + }).filter(Boolean); + if (!(rawFields.length === 0)) { + _context3.n = 2; + break; + } + return _context3.a(2); + case 2: + _context3.n = 3; + return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)(); + case 3: + profiles = _context3.v; + matched = rawFields.filter(function (name) { + return profiles.some(function (p) { + return p.name.toLowerCase() === name.toLowerCase(); + }); + }); + if (!(matched.length === 0)) { + _context3.n = 4; + break; + } + return _context3.a(2); + case 4: + _this4._pendingImport = { + record: record, + matched: matched + }; + banner = container.querySelector('#field-import-banner'); + desc = container.querySelector('#field-import-desc'); + btn = container.querySelector('#import-to-fields-btn'); + importStatus = container.querySelector('#field-import-status'); + desc.textContent = "Found harvest data for: ".concat(matched.join(', ')); + importStatus.classList.add('hidden'); + btn.disabled = false; + btn.textContent = 'Import harvest data to field profiles'; + btn.style.opacity = ''; + banner.classList.remove('hidden'); + case 5: + return _context3.a(2); + } + }, _callee3); + }))(); + }, + _importToFields: function _importToFields(container) { + var _this5 = this; + return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() { + var _this5$_pendingImport, record, matched, sd, importStatus, btn, profiles, count, _iterator, _step, _loop, _t3; + return _regenerator().w(function (_context5) { + while (1) switch (_context5.p = _context5.n) { + case 0: + if (_this5._pendingImport) { + _context5.n = 1; + break; + } + return _context5.a(2); + case 1: + _this5$_pendingImport = _this5._pendingImport, record = _this5$_pendingImport.record, matched = _this5$_pendingImport.matched; + sd = record.structuredData; + importStatus = container.querySelector('#field-import-status'); + btn = container.querySelector('#import-to-fields-btn'); + btn.disabled = true; + btn.textContent = 'Importing…'; + importStatus.textContent = ''; + importStatus.classList.remove('hidden'); + _context5.n = 2; + return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)(); + case 2: + profiles = _context5.v; + count = 0; + _iterator = _createForOfIteratorHelper(matched); + _context5.p = 3; + _loop = /*#__PURE__*/_regenerator().m(function _loop() { + var _ref5, _sd$harvest_date, _ref6, _sd$crop, _sd$operation_type, _ref7, _ref8, _sd$avg_yield_bu_ac, _ref9, _sd$avg_moisture_pct, _profile$harvestRecor, _profile$cropHistory; + var fieldName, profile, harvestDate, cropYear, crop, yieldVal, moisture, harvestRecord, cropHistoryEntry, existingHarvests, existingHistory, updated; + return _regenerator().w(function (_context4) { + while (1) switch (_context4.n) { + case 0: + fieldName = _step.value; + profile = profiles.find(function (p) { + return p.name.toLowerCase() === fieldName.toLowerCase(); + }); + if (profile) { + _context4.n = 1; + break; + } + return _context4.a(2, 1); + case 1: + harvestDate = (_ref5 = (_sd$harvest_date = sd.harvest_date) !== null && _sd$harvest_date !== void 0 ? _sd$harvest_date : sd.date) !== null && _ref5 !== void 0 ? _ref5 : new Date().toISOString().slice(0, 10); + cropYear = parseInt(harvestDate.slice(0, 4), 10); + crop = (_ref6 = (_sd$crop = sd.crop) !== null && _sd$crop !== void 0 ? _sd$crop : (_sd$operation_type = sd.operation_type) === null || _sd$operation_type === void 0 ? void 0 : _sd$operation_type.replace(/\s*harvest\s*/i, '').trim()) !== null && _ref6 !== void 0 ? _ref6 : 'Unknown'; + yieldVal = (_ref7 = (_ref8 = (_sd$avg_yield_bu_ac = sd.avg_yield_bu_ac) !== null && _sd$avg_yield_bu_ac !== void 0 ? _sd$avg_yield_bu_ac : sd.yield_bu_ac) !== null && _ref8 !== void 0 ? _ref8 : sd["yield"]) !== null && _ref7 !== void 0 ? _ref7 : null; + moisture = (_ref9 = (_sd$avg_moisture_pct = sd.avg_moisture_pct) !== null && _sd$avg_moisture_pct !== void 0 ? _sd$avg_moisture_pct : sd.moisture_pct) !== null && _ref9 !== void 0 ? _ref9 : null; + harvestRecord = { + date: harvestDate, + crop: crop, + "yield": yieldVal ? parseFloat(yieldVal) : null, + unit: 'bu/ac', + moisture: moisture ? parseFloat(moisture) : null, + source: record.filename + }; + cropHistoryEntry = { + year: cropYear, + crop: crop, + "yield": harvestRecord["yield"], + unit: 'bu/ac' + }; + existingHarvests = (_profile$harvestRecor = profile.harvestRecords) !== null && _profile$harvestRecor !== void 0 ? _profile$harvestRecor : []; + existingHistory = (_profile$cropHistory = profile.cropHistory) !== null && _profile$cropHistory !== void 0 ? _profile$cropHistory : []; + updated = _objectSpread(_objectSpread({}, profile), {}, { + harvestRecords: [harvestRecord].concat(_toConsumableArray(existingHarvests.filter(function (r) { + return !(r.date === harvestRecord.date && r.crop === harvestRecord.crop); + }))), + cropHistory: [cropHistoryEntry].concat(_toConsumableArray(existingHistory.filter(function (r) { + return !(r.year === cropHistoryEntry.year && r.crop === cropHistoryEntry.crop); + }))).sort(function (a, b) { + return b.year - a.year; + }) + }); + _context4.n = 2; + return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(updated); + case 2: + count++; + case 3: + return _context4.a(2); + } + }, _loop); + }); + _iterator.s(); + case 4: + if ((_step = _iterator.n()).done) { + _context5.n = 7; + break; + } + return _context5.d(_regeneratorValues(_loop()), 5); + case 5: + if (!_context5.v) { + _context5.n = 6; + break; + } + return _context5.a(3, 6); + case 6: + _context5.n = 4; + break; + case 7: + _context5.n = 9; + break; + case 8: + _context5.p = 8; + _t3 = _context5.v; + _iterator.e(_t3); + case 9: + _context5.p = 9; + _iterator.f(); + return _context5.f(9); + case 10: + importStatus.textContent = "\u2713 Updated ".concat(count, " field profile").concat(count !== 1 ? 's' : '', " \u2014 check Fields tab"); + importStatus.style.color = '#4ade80'; + importStatus.classList.remove('hidden'); + btn.textContent = 'Imported'; + btn.style.opacity = '0.5'; + _this5._pendingImport = null; + case 11: + return _context5.a(2); + } + }, _callee4, null, [[3, 8, 9, 10]]); + }))(); + }, _parseCSV: function _parseCSV(file) { return new Promise(function (resolve, reject) { - // PapaParse is loaded dynamically to keep the background bundle lean - __webpack_require__.e(/*! import() */ "vendors-node_modules_papaparse_papaparse_min_js").then(__webpack_require__.t.bind(__webpack_require__, /*! papaparse */ "./node_modules/papaparse/papaparse.min.js", 23)).then(function (_ref5) { - var Papa = _ref5["default"]; + __webpack_require__.e(/*! import() */ "vendors-node_modules_papaparse_papaparse_min_js").then(__webpack_require__.t.bind(__webpack_require__, /*! papaparse */ "./node_modules/papaparse/papaparse.min.js", 23)).then(function (_ref0) { + var Papa = _ref0["default"]; Papa.parse(file, { complete: function complete(results) { var rows = results.data.slice(0, 200); @@ -2043,16 +2247,16 @@ function DataIngestModule() { return new Promise(function (resolve, reject) { var reader = new FileReader(); reader.onload = /*#__PURE__*/function () { - var _ref6 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(e) { - var _yield$import, read, utils, wb, lines, _t3; - return _regenerator().w(function (_context3) { - while (1) switch (_context3.p = _context3.n) { + var _ref1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(e) { + var _yield$import, read, utils, wb, lines, _t4; + return _regenerator().w(function (_context6) { + while (1) switch (_context6.p = _context6.n) { case 0: - _context3.p = 0; - _context3.n = 1; + _context6.p = 0; + _context6.n = 1; return __webpack_require__.e(/*! import() */ "vendors-node_modules_xlsx_xlsx_mjs").then(__webpack_require__.bind(__webpack_require__, /*! xlsx */ "./node_modules/xlsx/xlsx.mjs")); case 1: - _yield$import = _context3.v; + _yield$import = _context6.v; read = _yield$import.read; utils = _yield$import.utils; wb = read(e.target.result, { @@ -2065,19 +2269,19 @@ function DataIngestModule() { lines.push(utils.sheet_to_csv(ws).split('\n').slice(0, 100).join('\n')); }); resolve(lines.join('\n')); - _context3.n = 3; + _context6.n = 3; break; case 2: - _context3.p = 2; - _t3 = _context3.v; - reject(_t3); + _context6.p = 2; + _t4 = _context6.v; + reject(_t4); case 3: - return _context3.a(2); + return _context6.a(2); } - }, _callee3, null, [[0, 2]]); + }, _callee5, null, [[0, 2]]); })); return function (_x2) { - return _ref6.apply(this, arguments); + return _ref1.apply(this, arguments); }; }(); reader.onerror = reject; @@ -2088,16 +2292,16 @@ function DataIngestModule() { return new Promise(function (resolve, reject) { var reader = new FileReader(); reader.onload = /*#__PURE__*/function () { - var _ref7 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(e) { - var pdfjsLib, loadingTask, pdf, pages, texts, i, page, content, _t4; - return _regenerator().w(function (_context4) { - while (1) switch (_context4.p = _context4.n) { + var _ref10 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(e) { + var pdfjsLib, loadingTask, pdf, pages, texts, i, page, content, _t5; + return _regenerator().w(function (_context7) { + while (1) switch (_context7.p = _context7.n) { case 0: - _context4.p = 0; - _context4.n = 1; + _context7.p = 0; + _context7.n = 1; return __webpack_require__.e(/*! import() */ "vendors-node_modules_pdfjs-dist_build_pdf_mjs").then(__webpack_require__.bind(__webpack_require__, /*! pdfjs-dist */ "./node_modules/pdfjs-dist/build/pdf.mjs")); case 1: - pdfjsLib = _context4.v; + pdfjsLib = _context7.v; pdfjsLib.GlobalWorkerOptions.workerSrc = chrome.runtime.getURL('pdf.worker.js'); loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(e.target.result), @@ -2105,48 +2309,48 @@ function DataIngestModule() { isEvalSupported: false, useSystemFonts: true }); - _context4.n = 2; + _context7.n = 2; return loadingTask.promise; case 2: - pdf = _context4.v; + pdf = _context7.v; pages = Math.min(pdf.numPages, 10); texts = []; i = 1; case 3: if (!(i <= pages)) { - _context4.n = 7; + _context7.n = 7; break; } - _context4.n = 4; + _context7.n = 4; return pdf.getPage(i); case 4: - page = _context4.v; - _context4.n = 5; + page = _context7.v; + _context7.n = 5; return page.getTextContent(); case 5: - content = _context4.v; + content = _context7.v; texts.push(content.items.map(function (s) { return s.str; }).join(' ')); case 6: i++; - _context4.n = 3; + _context7.n = 3; break; case 7: resolve(texts.join('\n')); - _context4.n = 9; + _context7.n = 9; break; case 8: - _context4.p = 8; - _t4 = _context4.v; - reject(_t4); + _context7.p = 8; + _t5 = _context7.v; + reject(_t5); case 9: - return _context4.a(2); + return _context7.a(2); } - }, _callee4, null, [[0, 8]]); + }, _callee6, null, [[0, 8]]); })); return function (_x3) { - return _ref7.apply(this, arguments); + return _ref10.apply(this, arguments); }; }(); reader.onerror = reject; @@ -2154,48 +2358,74 @@ function DataIngestModule() { }); }, _renderFileList: function _renderFileList(container) { - var _this4 = this; - return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6() { + var _this6 = this; + return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() { var files, listEl; - return _regenerator().w(function (_context6) { - while (1) switch (_context6.n) { + return _regenerator().w(function (_context0) { + while (1) switch (_context0.n) { case 0: - _context6.n = 1; + _context0.n = 1; return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getIngestedFiles)(); case 1: - files = _context6.v; + files = _context0.v; listEl = container.querySelector('#file-list'); if (!(files.length === 0)) { - _context6.n = 2; + _context0.n = 2; break; } listEl.innerHTML = "\n
\n \n \n \n

No files ingested yet.

\n

Upload a CSV, Excel, or PDF file above.

\n
"; - return _context6.a(2); + return _context0.a(2); case 2: listEl.innerHTML = files.map(function (f) { - return "\n
\n
\n
\n ").concat(f.type, "\n

").concat(f.filename, "

\n

").concat(new Date(f.uploadedAt).toLocaleDateString(), "

\n
\n \n
\n ").concat(f.preview ? "
".concat(f.preview, "
") : '', "\n
\n "); + return "\n
\n
\n
\n ").concat(f.type, "\n

").concat(f.filename, "

\n

").concat(new Date(f.uploadedAt).toLocaleDateString(), "

\n
\n \n
\n ").concat(f.preview ? "
".concat(f.preview, "
") : '', "\n ").concat(_this6._hasFieldData(f) ? "

\u2197 Contains field data \xB7

") : '', "\n
\n "); }).join(''); listEl.querySelectorAll('.file-delete-btn').forEach(function (btn) { - btn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() { - return _regenerator().w(function (_context5) { - while (1) switch (_context5.n) { + btn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() { + return _regenerator().w(function (_context8) { + while (1) switch (_context8.n) { case 0: - _context5.n = 1; + _context8.n = 1; return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.deleteIngestedFile)(btn.dataset.id); case 1: - _context5.n = 2; - return _this4._renderFileList(container); + _context8.n = 2; + return _this6._renderFileList(container); case 2: - return _context5.a(2); + return _context8.a(2); } - }, _callee5); + }, _callee7); + }))); + }); + listEl.querySelectorAll('.reimport-btn').forEach(function (btn) { + btn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() { + var file; + return _regenerator().w(function (_context9) { + while (1) switch (_context9.n) { + case 0: + file = files.find(function (f) { + return f.id === btn.dataset.id; + }); + if (!file) { + _context9.n = 1; + break; + } + _context9.n = 1; + return _this6._offerFieldImport(file, container); + case 1: + return _context9.a(2); + } + }, _callee8); }))); }); case 3: - return _context6.a(2); + return _context0.a(2); } - }, _callee6); + }, _callee9); }))(); + }, + _hasFieldData: function _hasFieldData(file) { + var sd = file.structuredData; + if (!sd || sd.parse_error) return false; + return Array.isArray(sd.fields) && sd.fields.length > 0 || Array.isArray(sd.field_names) && sd.field_names.length > 0; } }; } @@ -2232,11 +2462,14 @@ function FieldProfileModule() { return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: - container.innerHTML = "\n
Field Profiles
\n\n
\n \n \n
\n
\n\n \n
\n

New Field

\n
\n \n \n
\n \n \n
\n
\n \n \n
\n \n
\n
\n \n \n
\n
\n\n \n
\n "; + container.innerHTML = "\n
Field Profiles
\n\n
\n \n \n \n
\n
\n\n \n
\n

New Field

\n
\n \n \n
\n \n \n
\n
\n \n \n
\n \n
\n
\n \n \n
\n
\n\n \n
\n\n \n
\n "; _this._bindEvents(container); _context.n = 1; return _this._renderList(container); case 1: + _context.n = 2; + return _this._renderSyncLog(container); + case 2: return _context.a(2); } }, _callee); @@ -2249,7 +2482,10 @@ function FieldProfileModule() { container.querySelector('#fp-form').classList.toggle('hidden', !showForm); }); container.querySelector('#fp-agrefine-sync-btn').addEventListener('click', function () { - return _this2._syncAgRefine(container); + return _this2._pullFromAgRefine(container); + }); + container.querySelector('#fp-agrefine-push-btn').addEventListener('click', function () { + return _this2._pushToAgRefine(container); }); container.querySelector('#fp-cancel-btn').addEventListener('click', function () { showForm = false; @@ -2279,14 +2515,11 @@ function FieldProfileModule() { }, notes: container.querySelector('#fp-notes').value.trim() || null, cropHistory: [], - // populated from ingested data in Phase 3 harvestRecords: [], - // populated from ingested CSVs in Phase 3 weatherData: null, - // Phase 6 carbonPotential: null, - // Phase 7 - createdAt: new Date().toISOString() + createdAt: new Date().toISOString(), + _source: 'manual' }; _context2.n = 2; return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(profile); @@ -2301,7 +2534,7 @@ function FieldProfileModule() { }, _callee2); }))); }, - _syncAgRefine: function _syncAgRefine(container) { + _pullFromAgRefine: function _pullFromAgRefine(container) { var _this3 = this; return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() { var statusEl, result, parts; @@ -2309,7 +2542,7 @@ function FieldProfileModule() { while (1) switch (_context3.n) { case 0: statusEl = container.querySelector('#fp-sync-status'); - statusEl.textContent = 'Connecting to AG-Refine tab…'; + statusEl.textContent = 'Connecting to AG-Refine…'; statusEl.style.color = '#3d4f66'; _context3.n = 1; return chrome.runtime.sendMessage({ @@ -2325,74 +2558,157 @@ function FieldProfileModule() { statusEl.style.color = '#f87171'; setTimeout(function () { statusEl.textContent = ''; - }, 5000); + statusEl.style.color = '#3d4f66'; + }, 6000); return _context3.a(2); case 2: parts = []; - if (result.added) parts.push("".concat(result.added, " added")); + if (result.added) parts.push("".concat(result.added, " field").concat(result.added !== 1 ? 's' : '', " added")); if (result.updated) parts.push("".concat(result.updated, " updated")); if (result.loadsFound) parts.push("".concat(result.loadsFound, " loads found")); - statusEl.textContent = parts.length ? "\u2713 Synced: ".concat(parts.join(', ')) : '✓ No new fields found in AG-Refine'; + statusEl.textContent = parts.length ? "\u2713 Pull complete \u2014 ".concat(parts.join(', ')) : '✓ No new fields in AG-Refine'; statusEl.style.color = '#4ade80'; setTimeout(function () { statusEl.textContent = ''; - }, 4000); + statusEl.style.color = '#3d4f66'; + }, 5000); _context3.n = 3; return _this3._renderList(container); case 3: + _context3.n = 4; + return _this3._renderSyncLog(container); + case 4: return _context3.a(2); } }, _callee3); }))(); }, - _renderList: function _renderList(container) { - var _this4 = this; - return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6() { - var profiles, agRefineUrl, listEl; - return _regenerator().w(function (_context6) { - while (1) switch (_context6.n) { + _pushToAgRefine: function _pushToAgRefine(container) { + return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() { + var statusEl, result; + return _regenerator().w(function (_context4) { + while (1) switch (_context4.n) { case 0: - _context6.n = 1; - return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)(); + statusEl = container.querySelector('#fp-sync-status'); + statusEl.textContent = 'Pushing to AG-Refine…'; + statusEl.style.color = '#3d4f66'; + _context4.n = 1; + return chrome.runtime.sendMessage({ + type: 'AGREFINE_PUSH' + }); case 1: - profiles = _context6.v; - _context6.n = 2; - return (0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_1__.getAgRefineUrl)(); - case 2: - agRefineUrl = _context6.v; - listEl = container.querySelector('#fp-list'); - if (!(profiles.length === 0)) { - _context6.n = 3; + result = _context4.v; + if (result.ok) { + _context4.n = 2; break; } - listEl.innerHTML = "\n
\n \n \n \n

No field profiles yet.

\n

Create a profile for each field in your operation.

\n
"; - return _context6.a(2); + statusEl.textContent = "\u26A0 ".concat(result.error); + statusEl.style.color = '#f87171'; + setTimeout(function () { + statusEl.textContent = ''; + statusEl.style.color = '#3d4f66'; + }, 6000); + return _context4.a(2); + case 2: + statusEl.textContent = "\u2713 Pushed ".concat(result.count, " field").concat(result.count !== 1 ? 's' : '', " to AG-Refine"); + statusEl.style.color = '#4ade80'; + setTimeout(function () { + statusEl.textContent = ''; + statusEl.style.color = '#3d4f66'; + }, 4000); + case 3: + return _context4.a(2); + } + }, _callee4); + }))(); + }, + _renderSyncLog: function _renderSyncLog(container) { + return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() { + var log, logEl, latest; + return _regenerator().w(function (_context5) { + while (1) switch (_context5.n) { + case 0: + _context5.n = 1; + return (0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_1__.getSyncLog)(); + case 1: + log = _context5.v; + logEl = container.querySelector('#fp-sync-log'); + if (!(log.length === 0)) { + _context5.n = 2; + break; + } + logEl.classList.add('hidden'); + return _context5.a(2); + case 2: + latest = log[0]; + logEl.classList.remove('hidden'); + logEl.innerHTML = "\n
\n
\n AG-Refine Sync Log\n ".concat(log.length, " sync").concat(log.length !== 1 ? 's' : '', "\n
\n
\n ").concat(log.slice(0, 5).map(function (entry) { + return "\n
\n
\n ".concat(new Date(entry.at).toLocaleString(), "\n
\n ").concat(entry.fieldsAdded ? "+".concat(entry.fieldsAdded, " added") : '', "\n ").concat(entry.fieldsUpdated ? "".concat(entry.fieldsAdded ? ' · ' : '').concat(entry.fieldsUpdated, " updated") : '', "\n ").concat(!entry.fieldsAdded && !entry.fieldsUpdated ? 'No changes' : '', "\n ").concat(entry.loadsFound ? " \xB7 ".concat(entry.loadsFound, " loads") : '', "\n
\n ").concat(entry.tabUrl ? "
").concat(entry.tabUrl.replace(/^https?:\/\//, '').slice(0, 40), "
") : '', "\n
\n \u2193 Pull\n
\n "); + }).join(''), "\n
\n ").concat(log.length > 5 ? "

".concat(log.length - 5, " older entries hidden

") : '', "\n
\n "); + case 3: + return _context5.a(2); + } + }, _callee5); + }))(); + }, + _renderList: function _renderList(container) { + var _this4 = this; + return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() { + var profiles, agRefineUrl, listEl; + return _regenerator().w(function (_context8) { + while (1) switch (_context8.n) { + case 0: + _context8.n = 1; + return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)(); + case 1: + profiles = _context8.v; + _context8.n = 2; + return (0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_1__.getAgRefineUrl)(); + case 2: + agRefineUrl = _context8.v; + listEl = container.querySelector('#fp-list'); + if (!(profiles.length === 0)) { + _context8.n = 3; + break; + } + listEl.innerHTML = "\n
\n \n \n \n

No field profiles yet.

\n

Create a profile or sync from AG-Refine.

\n
"; + return _context8.a(2); case 3: listEl.innerHTML = profiles.map(function (p) { - var _p$coordinates, _p$coordinates2, _p$_source; - return "\n
\n
\n
\n

").concat(p.name, "

\n
\n ").concat(p.acres ? "".concat(p.acres, " ac") : '', "\n ").concat(p.soilType ? "".concat(p.soilType, "") : '', "\n ").concat(p.cluId ? "CLU ".concat(p.cluId, "") : '', "\n
\n
\n
\n \n \n \n \n
\n
\n\n \n
\n ").concat(((_p$coordinates = p.coordinates) === null || _p$coordinates === void 0 ? void 0 : _p$coordinates.lat) != null && ((_p$coordinates2 = p.coordinates) === null || _p$coordinates2 === void 0 ? void 0 : _p$coordinates2.lon) != null ? "

\uD83D\uDCCD ".concat(p.coordinates.lat.toFixed(4), ", ").concat(p.coordinates.lon.toFixed(4), "

") : '', "\n ").concat(p.notes ? "

\uD83D\uDCDD ".concat(p.notes, "

") : '', "\n ").concat((_p$_source = p._source) !== null && _p$_source !== void 0 && _p$_source.includes('ag-refine') ? "

\u2197 Synced from AG-Refine

" : '', "\n ").concat(agRefineUrl ? "Open in AG-Refine \u2197") : '', "\n

Weather data: Phase 6

\n

Carbon potential: Phase 7

\n

Added ").concat(new Date(p.createdAt).toLocaleDateString(), "

\n
\n
\n "); + var _p$cropHistory, _p$cropHistory2, _p$harvestRecords, _p$harvestRecords2, _p$cropHistory3, _p$coordinates, _p$coordinates2; + var isExpanded = expandedId === p.id; + var sourceLabel = p._source === 'ag-refine' ? 'AG-Refine' : p._source === 'ag-refine-merged' ? 'AG-Refine + manual' : p._source === 'manual' ? 'manual' : null; + var cropHistoryHtml = ((_p$cropHistory = p.cropHistory) !== null && _p$cropHistory !== void 0 ? _p$cropHistory : []).length > 0 ? "
\n

Crop History

\n
\n ".concat(((_p$cropHistory2 = p.cropHistory) !== null && _p$cropHistory2 !== void 0 ? _p$cropHistory2 : []).slice(0, 5).map(function (h) { + var _h$unit; + return "\n
\n ".concat(h.year, " \u2014 ").concat(h.crop, "\n ").concat(h["yield"] != null ? "".concat(h["yield"], " ").concat((_h$unit = h.unit) !== null && _h$unit !== void 0 ? _h$unit : 'bu/ac', "") : '', "\n
\n "); + }).join(''), "\n
\n
") : ''; + var harvestHtml = ((_p$harvestRecords = p.harvestRecords) !== null && _p$harvestRecords !== void 0 ? _p$harvestRecords : []).length > 0 ? "
\n

Harvest Records

\n
\n ".concat(((_p$harvestRecords2 = p.harvestRecords) !== null && _p$harvestRecords2 !== void 0 ? _p$harvestRecords2 : []).slice(0, 4).map(function (h) { + var _h$date$slice, _h$date, _h$unit2; + return "\n
\n ".concat((_h$date$slice = (_h$date = h.date) === null || _h$date === void 0 ? void 0 : _h$date.slice(0, 10)) !== null && _h$date$slice !== void 0 ? _h$date$slice : '?', " \u2014 ").concat(h.crop, "\n \n ").concat(h["yield"] != null ? "".concat(h["yield"], " ").concat((_h$unit2 = h.unit) !== null && _h$unit2 !== void 0 ? _h$unit2 : '') : '', "\n ").concat(h.moisture != null ? " @ ".concat(h.moisture, "%") : '', "\n \n
\n "); + }).join(''), "\n
\n
") : ''; + return "\n
\n
\n
\n

").concat(p.name, "

\n
\n ").concat(p.acres != null ? "".concat(p.acres, " ac") : '', "\n ").concat(p.soilType ? "".concat(p.soilType, "") : '', "\n ").concat(p.cluId ? "CLU ".concat(p.cluId, "") : '', "\n ").concat(((_p$cropHistory3 = p.cropHistory) !== null && _p$cropHistory3 !== void 0 ? _p$cropHistory3 : []).length > 0 ? "".concat(p.cropHistory.length, " yr history") : '', "\n ").concat(sourceLabel ? "".concat(sourceLabel, "") : '', "\n
\n
\n
\n \n \n \n \n
\n
\n\n \n
\n ").concat(((_p$coordinates = p.coordinates) === null || _p$coordinates === void 0 ? void 0 : _p$coordinates.lat) != null && ((_p$coordinates2 = p.coordinates) === null || _p$coordinates2 === void 0 ? void 0 : _p$coordinates2.lon) != null ? "

\uD83D\uDCCD ".concat(p.coordinates.lat.toFixed(4), ", ").concat(p.coordinates.lon.toFixed(4), "

") : '', "\n ").concat(p.notes ? "

\uD83D\uDCDD ".concat(p.notes, "

") : '', "\n ").concat(cropHistoryHtml, "\n ").concat(harvestHtml, "\n ").concat(!cropHistoryHtml && !harvestHtml ? "

No crop history yet \u2014 ingest a harvest file to populate.

" : '', "\n
\n

Added ").concat(new Date(p.createdAt).toLocaleDateString(), "

\n
\n ").concat(agRefineUrl ? "Open in AG-Refine \u2197") : '', "\n Carbon: Phase 7\n
\n
\n
\n
\n "); }).join(''); listEl.querySelectorAll('.agri-card').forEach(function (card) { card.addEventListener('click', /*#__PURE__*/function () { - var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(e) { + var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(e) { var id; - return _regenerator().w(function (_context4) { - while (1) switch (_context4.n) { + return _regenerator().w(function (_context6) { + while (1) switch (_context6.n) { case 0: if (!e.target.closest('.fp-delete-btn')) { - _context4.n = 1; + _context6.n = 1; break; } - return _context4.a(2); + return _context6.a(2); case 1: id = card.dataset.id; expandedId = expandedId === id ? null : id; - _context4.n = 2; + _context6.n = 2; return _this4._renderList(container); case 2: - return _context4.a(2); + return _context6.a(2); } - }, _callee4); + }, _callee6); })); return function (_x) { return _ref2.apply(this, arguments); @@ -2401,21 +2717,21 @@ function FieldProfileModule() { }); listEl.querySelectorAll('.fp-delete-btn').forEach(function (btn) { btn.addEventListener('click', /*#__PURE__*/function () { - var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(e) { - return _regenerator().w(function (_context5) { - while (1) switch (_context5.n) { + var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(e) { + return _regenerator().w(function (_context7) { + while (1) switch (_context7.n) { case 0: e.stopPropagation(); - _context5.n = 1; + _context7.n = 1; return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.deleteFieldProfile)(btn.dataset.id); case 1: if (expandedId === btn.dataset.id) expandedId = null; - _context5.n = 2; + _context7.n = 2; return _this4._renderList(container); case 2: - return _context5.a(2); + return _context7.a(2); } - }, _callee5); + }, _callee7); })); return function (_x2) { return _ref3.apply(this, arguments); @@ -2423,9 +2739,9 @@ function FieldProfileModule() { }()); }); case 4: - return _context6.a(2); + return _context8.a(2); } - }, _callee6); + }, _callee8); }))(); } }; @@ -2670,6 +2986,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ getAgRefineUrl: () => (/* binding */ getAgRefineUrl), /* harmony export */ getSyncLog: () => (/* binding */ getSyncLog), +/* harmony export */ pushToAgRefine: () => (/* binding */ pushToAgRefine), /* harmony export */ setAgRefineUrl: () => (/* binding */ setAgRefineUrl), /* harmony export */ syncFromAgRefine: () => (/* binding */ syncFromAgRefine) /* harmony export */ }); @@ -2925,38 +3242,117 @@ function extractLoads(raw) { } return loads; } -function syncFromAgRefine() { - return _syncFromAgRefine.apply(this, arguments); + +// Injected into AG-Refine tab to write field data back into its localStorage +function writeFieldsToAgRefineTab(fields) { + try { + localStorage.setItem('agrifine_pushed_fields', JSON.stringify(fields)); + localStorage.setItem('agrifine_pushed_at', new Date().toISOString()); + // Dispatch an event so a listening AG-Refine app can react immediately + window.dispatchEvent(new CustomEvent('agrifine:fields-updated', { + detail: { + fields: fields + } + })); + return { + ok: true, + count: fields.length + }; + } catch (err) { + return { + ok: false, + error: err.message + }; + } } -function _syncFromAgRefine() { - _syncFromAgRefine = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() { - var configuredUrl, allTabs, agRefineTabs, tab, raw, _yield$chrome$scripti, _yield$chrome$scripti2, result, fields, loads, existing, added, updated, _iterator3, _step3, _loop, log, history, _t7, _t8; - return _regenerator().w(function (_context5) { - while (1) switch (_context5.p = _context5.n) { +function pushToAgRefine(_x2) { + return _pushToAgRefine.apply(this, arguments); +} +function _pushToAgRefine() { + _pushToAgRefine = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(profiles) { + var configuredUrl, allTabs, agRefineTabs, tab, _yield$chrome$scripti, _yield$chrome$scripti2, result, _t7; + return _regenerator().w(function (_context4) { + while (1) switch (_context4.p = _context4.n) { case 0: - _context5.n = 1; + _context4.n = 1; return getAgRefineUrl(); case 1: - configuredUrl = _context5.v; - _context5.n = 2; + configuredUrl = _context4.v; + _context4.n = 2; return chrome.tabs.query({}); case 2: - allTabs = _context5.v; + allTabs = _context4.v; agRefineTabs = allTabs.filter(function (t) { return tabMatchesAgRefine(t, configuredUrl); }); if (!(agRefineTabs.length === 0)) { - _context5.n = 3; + _context4.n = 3; break; } - return _context5.a(2, { + return _context4.a(2, { + ok: false, + error: 'No AG-Refine tab found. Open AG-Refine first.' + }); + case 3: + tab = agRefineTabs[0]; + _context4.p = 4; + _context4.n = 5; + return chrome.scripting.executeScript({ + target: { + tabId: tab.id + }, + func: writeFieldsToAgRefineTab, + args: [profiles] + }); + case 5: + _yield$chrome$scripti = _context4.v; + _yield$chrome$scripti2 = _slicedToArray(_yield$chrome$scripti, 1); + result = _yield$chrome$scripti2[0]; + return _context4.a(2, result.result); + case 6: + _context4.p = 6; + _t7 = _context4.v; + return _context4.a(2, { + ok: false, + error: "Cannot write to AG-Refine tab: ".concat(_t7.message) + }); + } + }, _callee4, null, [[4, 6]]); + })); + return _pushToAgRefine.apply(this, arguments); +} +function syncFromAgRefine() { + return _syncFromAgRefine.apply(this, arguments); +} +function _syncFromAgRefine() { + _syncFromAgRefine = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() { + var configuredUrl, allTabs, agRefineTabs, tab, raw, _yield$chrome$scripti3, _yield$chrome$scripti4, result, fields, loads, existing, added, updated, _iterator3, _step3, _loop, log, history, _t8, _t9; + return _regenerator().w(function (_context6) { + while (1) switch (_context6.p = _context6.n) { + case 0: + _context6.n = 1; + return getAgRefineUrl(); + case 1: + configuredUrl = _context6.v; + _context6.n = 2; + return chrome.tabs.query({}); + case 2: + allTabs = _context6.v; + agRefineTabs = allTabs.filter(function (t) { + return tabMatchesAgRefine(t, configuredUrl); + }); + if (!(agRefineTabs.length === 0)) { + _context6.n = 3; + break; + } + return _context6.a(2, { ok: false, error: 'No AG-Refine tab found. Open AG-Refine in a browser tab first.' }); case 3: tab = agRefineTabs[0]; - _context5.p = 4; - _context5.n = 5; + _context6.p = 4; + _context6.n = 5; return chrome.scripting.executeScript({ target: { tabId: tab.id @@ -2964,41 +3360,41 @@ function _syncFromAgRefine() { func: scrapeAgRefineTab }); case 5: - _yield$chrome$scripti = _context5.v; - _yield$chrome$scripti2 = _slicedToArray(_yield$chrome$scripti, 1); - result = _yield$chrome$scripti2[0]; + _yield$chrome$scripti3 = _context6.v; + _yield$chrome$scripti4 = _slicedToArray(_yield$chrome$scripti3, 1); + result = _yield$chrome$scripti4[0]; raw = result.result; - _context5.n = 7; + _context6.n = 7; break; case 6: - _context5.p = 6; - _t7 = _context5.v; - return _context5.a(2, { + _context6.p = 6; + _t8 = _context6.v; + return _context6.a(2, { ok: false, - error: "Cannot read AG-Refine tab: ".concat(_t7.message) + error: "Cannot read AG-Refine tab: ".concat(_t8.message) }); case 7: fields = extractFields(raw); loads = extractLoads(raw); // Merge fields — update existing by name, insert new ones - _context5.n = 8; + _context6.n = 8; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)(); case 8: - existing = _context5.v; + existing = _context6.v; added = 0; updated = 0; _iterator3 = _createForOfIteratorHelper(fields); - _context5.p = 9; + _context6.p = 9; _loop = /*#__PURE__*/_regenerator().m(function _loop() { var f, match, _match$coordinates, _match$cropHistory, _match$notes, _match$cluId, merged; - return _regenerator().w(function (_context4) { - while (1) switch (_context4.n) { + return _regenerator().w(function (_context5) { + while (1) switch (_context5.n) { case 0: f = _step3.value; match = existing.find(function (e) { return e.name.toLowerCase() === f.name.toLowerCase(); }); if (!match) { - _context4.n = 2; + _context5.n = 2; break; } // Merge: fill in missing data without overwriting user edits @@ -3009,43 +3405,43 @@ function _syncFromAgRefine() { cluId: (_match$cluId = match.cluId) !== null && _match$cluId !== void 0 ? _match$cluId : f.cluId, _source: 'ag-refine-merged' }); - _context4.n = 1; + _context5.n = 1; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(merged); case 1: updated++; - _context4.n = 4; + _context5.n = 4; break; case 2: - _context4.n = 3; + _context5.n = 3; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(f); case 3: added++; case 4: - return _context4.a(2); + return _context5.a(2); } }, _loop); }); _iterator3.s(); case 10: if ((_step3 = _iterator3.n()).done) { - _context5.n = 12; + _context6.n = 12; break; } - return _context5.d(_regeneratorValues(_loop()), 11); + return _context6.d(_regeneratorValues(_loop()), 11); case 11: - _context5.n = 10; + _context6.n = 10; break; case 12: - _context5.n = 14; + _context6.n = 14; break; case 13: - _context5.p = 13; - _t8 = _context5.v; - _iterator3.e(_t8); + _context6.p = 13; + _t9 = _context6.v; + _iterator3.e(_t9); case 14: - _context5.p = 14; + _context6.p = 14; _iterator3.f(); - return _context5.f(14); + return _context6.f(14); case 15: log = { at: new Date().toISOString(), @@ -3055,15 +3451,15 @@ function _syncFromAgRefine() { loadsFound: loads.length, rawKeys: Object.keys(_objectSpread(_objectSpread({}, raw.localStorage), raw.sessionStorage)) }; - _context5.n = 16; + _context6.n = 16; return getSyncLog(); case 16: - history = _context5.v; + history = _context6.v; history.unshift(log); - _context5.n = 17; + _context6.n = 17; return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.localSet)(SYNC_LOG_KEY, history.slice(0, 20)); case 17: - return _context5.a(2, { + return _context6.a(2, { ok: true, added: added, updated: updated, @@ -3072,7 +3468,7 @@ function _syncFromAgRefine() { tabUrl: tab.url }); } - }, _callee4, null, [[9, 13, 14, 15], [4, 6]]); + }, _callee5, null, [[9, 13, 14, 15], [4, 6]]); })); return _syncFromAgRefine.apply(this, arguments); } diff --git a/agrifine-extension/screenshots/01_reading_list.png b/agrifine-extension/screenshots/01_reading_list.png new file mode 100644 index 0000000..b3a9f39 Binary files /dev/null and b/agrifine-extension/screenshots/01_reading_list.png differ diff --git a/agrifine-extension/screenshots/02_data_ingest.png b/agrifine-extension/screenshots/02_data_ingest.png new file mode 100644 index 0000000..b9c3f67 Binary files /dev/null and b/agrifine-extension/screenshots/02_data_ingest.png differ diff --git a/agrifine-extension/screenshots/03_field_profiles.png b/agrifine-extension/screenshots/03_field_profiles.png new file mode 100644 index 0000000..573fd04 Binary files /dev/null and b/agrifine-extension/screenshots/03_field_profiles.png differ diff --git a/agrifine-extension/screenshots/03b_field_expanded.png b/agrifine-extension/screenshots/03b_field_expanded.png new file mode 100644 index 0000000..c6f7f0a Binary files /dev/null and b/agrifine-extension/screenshots/03b_field_expanded.png differ diff --git a/agrifine-extension/screenshots/04_dashboard.png b/agrifine-extension/screenshots/04_dashboard.png new file mode 100644 index 0000000..04bd06a Binary files /dev/null and b/agrifine-extension/screenshots/04_dashboard.png differ diff --git a/agrifine-extension/screenshots/05_carbon.png b/agrifine-extension/screenshots/05_carbon.png new file mode 100644 index 0000000..161cec6 Binary files /dev/null and b/agrifine-extension/screenshots/05_carbon.png differ diff --git a/agrifine-extension/screenshots/06_agent.png b/agrifine-extension/screenshots/06_agent.png new file mode 100644 index 0000000..c75d368 Binary files /dev/null and b/agrifine-extension/screenshots/06_agent.png differ diff --git a/agrifine-extension/screenshots/07_settings_panel.png b/agrifine-extension/screenshots/07_settings_panel.png new file mode 100644 index 0000000..4bb98a7 Binary files /dev/null and b/agrifine-extension/screenshots/07_settings_panel.png differ diff --git a/agrifine-extension/screenshots/08_new_field_form.png b/agrifine-extension/screenshots/08_new_field_form.png new file mode 100644 index 0000000..c2aa52b Binary files /dev/null and b/agrifine-extension/screenshots/08_new_field_form.png differ diff --git a/agrifine-extension/src/background/index.js b/agrifine-extension/src/background/index.js index f88f8d8..0b48543 100644 --- a/agrifine-extension/src/background/index.js +++ b/agrifine-extension/src/background/index.js @@ -1,6 +1,6 @@ -import { sessionGet, sessionSet, localGet, localSet, KEYS } from '../utils/storage.js'; +import { sessionGet, sessionSet, localGet, localSet, KEYS, getFieldProfiles } from '../utils/storage.js'; import { fetchAnthropic } from '../utils/api.js'; -import { syncFromAgRefine } from '../utils/agrefine-bridge.js'; +import { syncFromAgRefine, pushToAgRefine } from '../utils/agrefine-bridge.js'; // Open the side panel when the action icon is clicked chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error); @@ -67,6 +67,13 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { .catch((err) => sendResponse({ ok: false, error: err.message })); return true; + case 'AGREFINE_PUSH': + getFieldProfiles() + .then((profiles) => pushToAgRefine(profiles)) + .then(sendResponse) + .catch((err) => sendResponse({ ok: false, error: err.message })); + return true; + default: return false; } diff --git a/agrifine-extension/src/modules/data-ingest/index.js b/agrifine-extension/src/modules/data-ingest/index.js index ad7fbe6..06a3de6 100644 --- a/agrifine-extension/src/modules/data-ingest/index.js +++ b/agrifine-extension/src/modules/data-ingest/index.js @@ -1,4 +1,4 @@ -import { getIngestedFiles, saveIngestedFile, deleteIngestedFile } from '../../utils/storage.js'; +import { getIngestedFiles, saveIngestedFile, deleteIngestedFile, getFieldProfiles, saveFieldProfile } from '../../utils/storage.js'; import { callAnthropic } from '../../utils/api.js'; const SUPPORTED_TYPES = { @@ -47,10 +47,24 @@ export function DataIngestModule() {
+ + +
`; + this._pendingImport = null; this._bindEvents(container); await this._renderFileList(container); }, @@ -80,6 +94,10 @@ export function DataIngestModule() { fileInput.addEventListener('change', () => { if (fileInput.files[0]) this._processFile(fileInput.files[0], container); }); + + container.querySelector('#import-to-fields-btn').addEventListener('click', () => + this._importToFields(container) + ); }, async _processFile(file, container) { @@ -94,6 +112,10 @@ export function DataIngestModule() { return; } + // Hide previous import banner + container.querySelector('#field-import-banner').classList.add('hidden'); + this._pendingImport = null; + status.textContent = `Parsing ${typeName}…`; let extractedText = ''; @@ -121,7 +143,7 @@ export function DataIngestModule() { try { const raw = await callAnthropic({ - system: 'You are an agricultural data analyst. Extract and return structured JSON from this document. Identify: operation type, field names, dates, quantities, equipment, crop types, financial figures, and any carbon or emissions data. Return only valid JSON.', + system: 'You are an agricultural data analyst. Extract and return structured JSON from this document. Identify: operation type, field names (as "fields" array of strings), dates, quantities, equipment, crop types, financial figures, and any carbon or emissions data. For harvest data include avg_yield_bu_ac, avg_moisture_pct, harvest_date, and crop. Return only valid JSON.', userMessage: extractedText.slice(0, 6000), maxTokens: 1024, }); @@ -137,7 +159,7 @@ export function DataIngestModule() { uploadedAt: new Date().toISOString(), structuredData, preview: Object.entries(structuredData ?? {}) - .filter(([k]) => k !== 'raw_preview') + .filter(([k]) => k !== 'raw_preview' && k !== 'parse_error') .slice(0, 5) .map(([k, v]) => `${k}: ${JSON.stringify(v).slice(0, 80)}`) .join('\n'), @@ -147,11 +169,116 @@ export function DataIngestModule() { status.textContent = 'File processed!'; setTimeout(() => { status.textContent = ''; }, 2000); await this._renderFileList(container); + + // Offer to import field data if field names were found + await this._offerFieldImport(record, container); + }, + + async _offerFieldImport(record, container) { + const sd = record.structuredData; + if (!sd || sd.parse_error) return; + + // Collect field names from multiple possible keys + const rawFields = [ + ...(Array.isArray(sd.fields) ? sd.fields : []), + ...(Array.isArray(sd.field_names) ? sd.field_names : []), + ].map((f) => String(f).trim()).filter(Boolean); + + if (rawFields.length === 0) return; + + const profiles = await getFieldProfiles(); + const matched = rawFields.filter((name) => + profiles.some((p) => p.name.toLowerCase() === name.toLowerCase()) + ); + + if (matched.length === 0) return; + + this._pendingImport = { record, matched }; + + const banner = container.querySelector('#field-import-banner'); + const desc = container.querySelector('#field-import-desc'); + const btn = container.querySelector('#import-to-fields-btn'); + const importStatus = container.querySelector('#field-import-status'); + + desc.textContent = `Found harvest data for: ${matched.join(', ')}`; + importStatus.classList.add('hidden'); + btn.disabled = false; + btn.textContent = 'Import harvest data to field profiles'; + btn.style.opacity = ''; + banner.classList.remove('hidden'); + }, + + async _importToFields(container) { + if (!this._pendingImport) return; + + const { record, matched } = this._pendingImport; + const sd = record.structuredData; + const importStatus = container.querySelector('#field-import-status'); + const btn = container.querySelector('#import-to-fields-btn'); + + btn.disabled = true; + btn.textContent = 'Importing…'; + importStatus.textContent = ''; + importStatus.classList.remove('hidden'); + + const profiles = await getFieldProfiles(); + let count = 0; + + for (const fieldName of matched) { + const profile = profiles.find((p) => p.name.toLowerCase() === fieldName.toLowerCase()); + if (!profile) continue; + + const harvestDate = sd.harvest_date ?? sd.date ?? new Date().toISOString().slice(0, 10); + const cropYear = parseInt(harvestDate.slice(0, 4), 10); + const crop = sd.crop ?? sd.operation_type?.replace(/\s*harvest\s*/i, '').trim() ?? 'Unknown'; + const yieldVal = sd.avg_yield_bu_ac ?? sd.yield_bu_ac ?? sd.yield ?? null; + const moisture = sd.avg_moisture_pct ?? sd.moisture_pct ?? null; + + const harvestRecord = { + date: harvestDate, + crop, + yield: yieldVal ? parseFloat(yieldVal) : null, + unit: 'bu/ac', + moisture: moisture ? parseFloat(moisture) : null, + source: record.filename, + }; + + const cropHistoryEntry = { + year: cropYear, + crop, + yield: harvestRecord.yield, + unit: 'bu/ac', + }; + + const existingHarvests = profile.harvestRecords ?? []; + const existingHistory = profile.cropHistory ?? []; + + const updated = { + ...profile, + harvestRecords: [ + harvestRecord, + ...existingHarvests.filter((r) => !(r.date === harvestRecord.date && r.crop === harvestRecord.crop)), + ], + cropHistory: [ + cropHistoryEntry, + ...existingHistory.filter((r) => !(r.year === cropHistoryEntry.year && r.crop === cropHistoryEntry.crop)), + ].sort((a, b) => b.year - a.year), + }; + + await saveFieldProfile(updated); + count++; + } + + importStatus.textContent = `✓ Updated ${count} field profile${count !== 1 ? 's' : ''} — check Fields tab`; + importStatus.style.color = '#4ade80'; + importStatus.classList.remove('hidden'); + btn.textContent = 'Imported'; + btn.style.opacity = '0.5'; + this._pendingImport = null; }, _parseCSV(file) { return new Promise((resolve, reject) => { - // PapaParse is loaded dynamically to keep the background bundle lean import('papaparse').then(({ default: Papa }) => { Papa.parse(file, { complete: (results) => { @@ -250,6 +377,7 @@ export function DataIngestModule() { ${f.preview ? `
${f.preview}
` : ''} + ${this._hasFieldData(f) ? `

↗ Contains field data ·

` : ''} `).join(''); @@ -259,6 +387,22 @@ export function DataIngestModule() { await this._renderFileList(container); }); }); + + listEl.querySelectorAll('.reimport-btn').forEach((btn) => { + btn.addEventListener('click', async () => { + const file = files.find((f) => f.id === btn.dataset.id); + if (file) await this._offerFieldImport(file, container); + }); + }); + }, + + _hasFieldData(file) { + const sd = file.structuredData; + if (!sd || sd.parse_error) return false; + return ( + (Array.isArray(sd.fields) && sd.fields.length > 0) || + (Array.isArray(sd.field_names) && sd.field_names.length > 0) + ); }, }; } diff --git a/agrifine-extension/src/modules/field-profile/index.js b/agrifine-extension/src/modules/field-profile/index.js index 04f6fa8..16cbd63 100644 --- a/agrifine-extension/src/modules/field-profile/index.js +++ b/agrifine-extension/src/modules/field-profile/index.js @@ -1,5 +1,5 @@ import { getFieldProfiles, saveFieldProfile, deleteFieldProfile } from '../../utils/storage.js'; -import { getAgRefineUrl, setAgRefineUrl } from '../../utils/agrefine-bridge.js'; +import { getAgRefineUrl, getSyncLog } from '../../utils/agrefine-bridge.js'; export function FieldProfileModule() { let showForm = false; @@ -21,12 +21,19 @@ export function FieldProfileModule() { New Field - +
@@ -62,10 +69,14 @@ export function FieldProfileModule() {
+ + + `; this._bindEvents(container); await this._renderList(container); + await this._renderSyncLog(container); }, _bindEvents(container) { @@ -74,7 +85,13 @@ export function FieldProfileModule() { container.querySelector('#fp-form').classList.toggle('hidden', !showForm); }); - container.querySelector('#fp-agrefine-sync-btn').addEventListener('click', () => this._syncAgRefine(container)); + container.querySelector('#fp-agrefine-sync-btn').addEventListener('click', () => + this._pullFromAgRefine(container) + ); + + container.querySelector('#fp-agrefine-push-btn').addEventListener('click', () => + this._pushToAgRefine(container) + ); container.querySelector('#fp-cancel-btn').addEventListener('click', () => { showForm = false; @@ -96,11 +113,12 @@ export function FieldProfileModule() { lon: parseFloat(container.querySelector('#fp-lon').value) || null, }, notes: container.querySelector('#fp-notes').value.trim() || null, - cropHistory: [], // populated from ingested data in Phase 3 - harvestRecords: [], // populated from ingested CSVs in Phase 3 - weatherData: null, // Phase 6 - carbonPotential: null, // Phase 7 + cropHistory: [], + harvestRecords: [], + weatherData: null, + carbonPotential: null, createdAt: new Date().toISOString(), + _source: 'manual', }; await saveFieldProfile(profile); @@ -110,9 +128,9 @@ export function FieldProfileModule() { }); }, - async _syncAgRefine(container) { + async _pullFromAgRefine(container) { const statusEl = container.querySelector('#fp-sync-status'); - statusEl.textContent = 'Connecting to AG-Refine tab…'; + statusEl.textContent = 'Connecting to AG-Refine…'; statusEl.style.color = '#3d4f66'; const result = await chrome.runtime.sendMessage({ type: 'AGREFINE_SYNC' }); @@ -120,18 +138,80 @@ export function FieldProfileModule() { if (!result.ok) { statusEl.textContent = `⚠ ${result.error}`; statusEl.style.color = '#f87171'; - setTimeout(() => { statusEl.textContent = ''; }, 5000); + setTimeout(() => { statusEl.textContent = ''; statusEl.style.color = '#3d4f66'; }, 6000); return; } const parts = []; - if (result.added) parts.push(`${result.added} added`); + if (result.added) parts.push(`${result.added} field${result.added !== 1 ? 's' : ''} added`); if (result.updated) parts.push(`${result.updated} updated`); if (result.loadsFound) parts.push(`${result.loadsFound} loads found`); - statusEl.textContent = parts.length ? `✓ Synced: ${parts.join(', ')}` : '✓ No new fields found in AG-Refine'; + statusEl.textContent = parts.length + ? `✓ Pull complete — ${parts.join(', ')}` + : '✓ No new fields in AG-Refine'; statusEl.style.color = '#4ade80'; - setTimeout(() => { statusEl.textContent = ''; }, 4000); + setTimeout(() => { statusEl.textContent = ''; statusEl.style.color = '#3d4f66'; }, 5000); + await this._renderList(container); + await this._renderSyncLog(container); + }, + + async _pushToAgRefine(container) { + const statusEl = container.querySelector('#fp-sync-status'); + statusEl.textContent = 'Pushing to AG-Refine…'; + statusEl.style.color = '#3d4f66'; + + const result = await chrome.runtime.sendMessage({ type: 'AGREFINE_PUSH' }); + + if (!result.ok) { + statusEl.textContent = `⚠ ${result.error}`; + statusEl.style.color = '#f87171'; + setTimeout(() => { statusEl.textContent = ''; statusEl.style.color = '#3d4f66'; }, 6000); + return; + } + + statusEl.textContent = `✓ Pushed ${result.count} field${result.count !== 1 ? 's' : ''} to AG-Refine`; + statusEl.style.color = '#4ade80'; + setTimeout(() => { statusEl.textContent = ''; statusEl.style.color = '#3d4f66'; }, 4000); + }, + + async _renderSyncLog(container) { + const log = await getSyncLog(); + const logEl = container.querySelector('#fp-sync-log'); + + if (log.length === 0) { + logEl.classList.add('hidden'); + return; + } + + const latest = log[0]; + logEl.classList.remove('hidden'); + logEl.innerHTML = ` +
+
+ AG-Refine Sync Log + ${log.length} sync${log.length !== 1 ? 's' : ''} +
+
+ ${log.slice(0, 5).map((entry) => ` +
+
+ ${new Date(entry.at).toLocaleString()} +
+ ${entry.fieldsAdded ? `+${entry.fieldsAdded} added` : ''} + ${entry.fieldsUpdated ? `${entry.fieldsAdded ? ' · ' : ''}${entry.fieldsUpdated} updated` : ''} + ${!entry.fieldsAdded && !entry.fieldsUpdated ? 'No changes' : ''} + ${entry.loadsFound ? ` · ${entry.loadsFound} loads` : ''} +
+ ${entry.tabUrl ? `
${entry.tabUrl.replace(/^https?:\/\//, '').slice(0, 40)}
` : ''} +
+ ↓ Pull +
+ `).join('')} +
+ ${log.length > 5 ? `

${log.length - 5} older entries hidden

` : ''} +
+ `; }, async _renderList(container) { @@ -147,47 +227,98 @@ export function FieldProfileModule() { d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />

No field profiles yet.

-

Create a profile for each field in your operation.

+

Create a profile or sync from AG-Refine.

`; return; } - listEl.innerHTML = profiles.map((p) => ` -
-
-
-

${p.name}

-
- ${p.acres ? `${p.acres} ac` : ''} - ${p.soilType ? `${p.soilType}` : ''} - ${p.cluId ? `CLU ${p.cluId}` : ''} + listEl.innerHTML = profiles.map((p) => { + const isExpanded = expandedId === p.id; + const sourceLabel = p._source === 'ag-refine' ? 'AG-Refine' + : p._source === 'ag-refine-merged' ? 'AG-Refine + manual' + : p._source === 'manual' ? 'manual' + : null; + + const cropHistoryHtml = (p.cropHistory ?? []).length > 0 + ? `
+

Crop History

+
+ ${(p.cropHistory ?? []).slice(0, 5).map((h) => ` +
+ ${h.year} — ${h.crop} + ${h.yield != null ? `${h.yield} ${h.unit ?? 'bu/ac'}` : ''} +
+ `).join('')} +
+
` + : ''; + + const harvestHtml = (p.harvestRecords ?? []).length > 0 + ? `
+

Harvest Records

+
+ ${(p.harvestRecords ?? []).slice(0, 4).map((h) => ` +
+ ${h.date?.slice(0, 10) ?? '?'} — ${h.crop} + + ${h.yield != null ? `${h.yield} ${h.unit ?? ''}` : ''} + ${h.moisture != null ? ` @ ${h.moisture}%` : ''} + +
+ `).join('')} +
+
` + : ''; + + return ` +
+
+
+

${p.name}

+
+ ${p.acres != null ? `${p.acres} ac` : ''} + ${p.soilType ? `${p.soilType}` : ''} + ${p.cluId ? `CLU ${p.cluId}` : ''} + ${(p.cropHistory ?? []).length > 0 ? `${p.cropHistory.length} yr history` : ''} + ${sourceLabel ? `${sourceLabel}` : ''} +
+
+
+ + + +
-
- - - - + + +
+ ${p.coordinates?.lat != null && p.coordinates?.lon != null + ? `

📍 ${p.coordinates.lat.toFixed(4)}, ${p.coordinates.lon.toFixed(4)}

` + : ''} + ${p.notes ? `

📝 ${p.notes}

` : ''} + ${cropHistoryHtml} + ${harvestHtml} + ${!cropHistoryHtml && !harvestHtml + ? `

No crop history yet — ingest a harvest file to populate.

` + : ''} +
+

Added ${new Date(p.createdAt).toLocaleDateString()}

+
+ ${agRefineUrl ? `Open in AG-Refine ↗` : ''} + Carbon: Phase 7 +
+
- - -
- ${p.coordinates?.lat != null && p.coordinates?.lon != null ? `

📍 ${p.coordinates.lat.toFixed(4)}, ${p.coordinates.lon.toFixed(4)}

` : ''} - ${p.notes ? `

📝 ${p.notes}

` : ''} - ${p._source?.includes('ag-refine') ? `

↗ Synced from AG-Refine

` : ''} - ${agRefineUrl ? `Open in AG-Refine ↗` : ''} -

Weather data: Phase 6

-

Carbon potential: Phase 7

-

Added ${new Date(p.createdAt).toLocaleDateString()}

-
-
- `).join(''); + `; + }).join(''); listEl.querySelectorAll('.agri-card').forEach((card) => { card.addEventListener('click', async (e) => { diff --git a/agrifine-extension/src/utils/agrefine-bridge.js b/agrifine-extension/src/utils/agrefine-bridge.js index 994aa8c..cbe065e 100644 --- a/agrifine-extension/src/utils/agrefine-bridge.js +++ b/agrifine-extension/src/utils/agrefine-bridge.js @@ -135,6 +135,41 @@ function extractLoads(raw) { return loads; } +// Injected into AG-Refine tab to write field data back into its localStorage +function writeFieldsToAgRefineTab(fields) { + try { + localStorage.setItem('agrifine_pushed_fields', JSON.stringify(fields)); + localStorage.setItem('agrifine_pushed_at', new Date().toISOString()); + // Dispatch an event so a listening AG-Refine app can react immediately + window.dispatchEvent(new CustomEvent('agrifine:fields-updated', { detail: { fields } })); + return { ok: true, count: fields.length }; + } catch (err) { + return { ok: false, error: err.message }; + } +} + +export async function pushToAgRefine(profiles) { + const configuredUrl = await getAgRefineUrl(); + const allTabs = await chrome.tabs.query({}); + const agRefineTabs = allTabs.filter((t) => tabMatchesAgRefine(t, configuredUrl)); + + if (agRefineTabs.length === 0) { + return { ok: false, error: 'No AG-Refine tab found. Open AG-Refine first.' }; + } + + const tab = agRefineTabs[0]; + try { + const [result] = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: writeFieldsToAgRefineTab, + args: [profiles], + }); + return result.result; + } catch (err) { + return { ok: false, error: `Cannot write to AG-Refine tab: ${err.message}` }; + } +} + export async function syncFromAgRefine() { const configuredUrl = await getAgRefineUrl(); const allTabs = await chrome.tabs.query({}); diff --git a/agrifine-extension/tests/seed_and_screenshot.mjs b/agrifine-extension/tests/seed_and_screenshot.mjs new file mode 100644 index 0000000..014608a --- /dev/null +++ b/agrifine-extension/tests/seed_and_screenshot.mjs @@ -0,0 +1,319 @@ +/** + * Agrifine seeder + screenshot test + * Seeds all storage with realistic farm data and screenshots every tab. + * Run: PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers node tests/seed_and_screenshot.mjs + */ + +import { chromium } from 'playwright'; +import { mkdirSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const UNIT_ROOT = resolve(__dir, '..'); +const DIST = resolve(UNIT_ROOT, 'dist'); +const SCREENSHOTS = resolve(UNIT_ROOT, 'screenshots'); +const CHROMIUM = process.env.PLAYWRIGHT_BROWSERS_PATH + ? `${process.env.PLAYWRIGHT_BROWSERS_PATH}/chromium-1194/chrome-linux/chrome` + : '/opt/pw-browsers/chromium-1194/chrome-linux/chrome'; + +mkdirSync(SCREENSHOTS, { recursive: true }); + +// ── Seed data ──────────────────────────────────────────────────────────────── + +const SEED = { + agrifine_field_profiles: [ + { + id: 'fp_001', name: 'North 80', acres: 78.4, soilType: 'Silty Clay Loam', + cluId: 'IL-147-0412', coordinates: { lat: 41.8827, lon: -88.0071 }, + notes: 'Tile-drained. Historically strong corn yields. Slight compaction issue in SW corner.', + cropHistory: [ + { year: 2023, crop: 'Corn', yield: 212, unit: 'bu/ac' }, + { year: 2022, crop: 'Soybeans', yield: 58, unit: 'bu/ac' }, + { year: 2021, crop: 'Corn', yield: 198, unit: 'bu/ac' }, + { year: 2020, crop: 'Soybeans', yield: 54, unit: 'bu/ac' }, + ], + harvestRecords: [ + { date: '2023-10-14', crop: 'Corn', yield: 212, unit: 'bu/ac', moisture: 17.2 }, + { date: '2022-09-28', crop: 'Soybeans', yield: 58, unit: 'bu/ac', moisture: 13.1 }, + ], + carbonPotential: null, weatherData: null, + createdAt: '2024-01-15T10:00:00Z', _source: 'manual', + }, + { + id: 'fp_002', name: 'South Bottoms', acres: 112.0, soilType: 'Silt Loam', + cluId: 'IL-147-0413', coordinates: { lat: 41.8750, lon: -88.0120 }, + notes: 'Flood risk near creek. Reduced tillage program since 2021. Good organic matter.', + cropHistory: [ + { year: 2023, crop: 'Soybeans', yield: 62, unit: 'bu/ac' }, + { year: 2022, crop: 'Corn', yield: 205, unit: 'bu/ac' }, + { year: 2021, crop: 'Soybeans', yield: 57, unit: 'bu/ac' }, + ], + harvestRecords: [ + { date: '2023-09-22', crop: 'Soybeans', yield: 62, unit: 'bu/ac', moisture: 12.8 }, + ], + carbonPotential: null, weatherData: null, + createdAt: '2024-01-15T10:30:00Z', _source: 'manual', + }, + { + id: 'fp_003', name: 'East Pivot', acres: 134.5, soilType: 'Sandy Loam', + cluId: 'IL-147-0414', coordinates: { lat: 41.8900, lon: -87.9980 }, + notes: 'Center pivot irrigation. Drought-susceptible. Cover crop trial started 2022.', + cropHistory: [ + { year: 2023, crop: 'Corn', yield: 225, unit: 'bu/ac' }, + { year: 2022, crop: 'Corn', yield: 218, unit: 'bu/ac' }, + ], + harvestRecords: [ + { date: '2023-10-20', crop: 'Corn', yield: 225, unit: 'bu/ac', moisture: 15.9 }, + ], + carbonPotential: null, weatherData: null, + createdAt: '2024-01-16T08:00:00Z', _source: 'manual', + }, + ], + + agrifine_ingested_files: [ + { + id: 'file_001', filename: 'harvest_2023_fall.csv', type: 'CSV', + uploadedAt: '2024-01-10T14:22:00Z', + structuredData: { + operation_type: 'Corn Harvest', + fields: ['North 80', 'East Pivot'], + total_acres: 212.9, + avg_yield_bu_ac: 218.5, + avg_moisture_pct: 16.6, + equipment: 'Case IH 8250', + total_bushels: 46520, + }, + preview: 'operation_type: "Corn Harvest" | fields: ["North 80","East Pivot"] | total_acres: 212.9 | avg_yield_bu_ac: 218.5', + }, + { + id: 'file_002', filename: 'soil_test_results_2023.xlsx', type: 'Excel', + uploadedAt: '2024-01-08T09:15:00Z', + structuredData: { + test_date: '2023-08-15', + fields_tested: 3, + avg_ph: 6.8, + avg_p_ppm: 42, + avg_k_ppm: 180, + cec: 22.4, + organic_matter_pct: 3.8, + recommendations: 'Apply 200 lbs/ac 0-46-0 on South Bottoms before corn planting', + }, + preview: 'test_date: "2023-08-15" | avg_ph: 6.8 | avg_p_ppm: 42 | organic_matter_pct: 3.8', + }, + { + id: 'file_003', filename: 'input_expense_report_2023.pdf', type: 'PDF', + uploadedAt: '2024-01-05T16:40:00Z', + structuredData: { + year: 2023, + total_seed_cost: 48750, + total_fertilizer_cost: 112400, + total_chemical_cost: 31200, + total_fuel_cost: 22800, + total_expenses: 215150, + cost_per_acre: 652, + }, + preview: 'year: 2023 | total_seed_cost: 48750 | total_fertilizer_cost: 112400 | cost_per_acre: 652', + }, + ], + + agrifine_reading_list: [ + { + id: 'rl_001', + url: 'https://www.farmprogress.com/corn/usda-raises-corn-yield-forecast', + title: 'USDA Raises 2023 Corn Yield Forecast to 174.9 Bu/Acre', + savedAt: '2024-01-18T11:30:00Z', + summary: 'USDA updated its corn yield forecast to 174.9 bushels per acre for the 2023 crop, up 1.2 bu from the November estimate, citing favorable harvest conditions across the Corn Belt.', + tags: ['USDA', 'agriculture', 'finance'], + }, + { + id: 'rl_002', + url: 'https://www.dtnpf.com/agriculture/web/ag/news/article/2024/01/15/la-nina-threat-corn-belt-spring', + title: 'La Niña Threat Could Dry Out Corn Belt This Spring', + savedAt: '2024-01-17T09:15:00Z', + summary: 'Meteorologists warn a developing La Niña pattern may reduce spring rainfall across Illinois and Indiana, raising drought risk for early-planted corn. Irrigation demand could spike 20-30% above normal.', + tags: ['weather', 'agriculture'], + }, + { + id: 'rl_003', + url: 'https://www.agriculture.com/carbon-markets-eqip-2024', + title: 'EQIP Carbon Market Signup Opens February 2024', + savedAt: '2024-01-16T14:00:00Z', + summary: 'USDA NRCS opened signup for the Environmental Quality Incentives Program carbon track, offering $45-65 per acre for verified no-till and cover crop practices across enrolled fields.', + tags: ['carbon', 'USDA', 'finance'], + }, + ], + + agrifine_farm_memory: { + lastUpdated: '2024-01-18T12:00:00Z', + farm_name: 'Hendricks Family Farms', + total_acres: 324.9, + primary_crops: ['Corn', 'Soybeans'], + soil_overview: 'Mixed silty clay loam and sandy loam soils. Average OM 3.8%. Tile drainage on North 80. Flood risk on South Bottoms near creek.', + aiGeneratedSummary: 'Hendricks Family Farms operates 324.9 acres across three fields in Kane County, IL. The 2023 corn crop averaged 218.5 bu/ac — above county average — driven by strong yields on the irrigated East Pivot (225 bu/ac). South Bottoms carries meaningful flood risk but benefits from 3+ years of reduced tillage. Soil tests indicate adequate fertility; phosphorus application recommended on South Bottoms ahead of 2024 corn rotation. Total 2023 input costs of $215,150 ($652/ac) leave healthy margin at current corn prices.', + key_insights: [ + 'East Pivot leads yield performance at 225 bu/ac with irrigation advantage', + 'South Bottoms organic matter at 3.8% — reduced tillage program paying off', + 'Total operation averaged 218.5 bu/ac corn in 2023 vs 174.9 national average', + 'EQIP carbon program could generate $14,620–$21,118 annually across enrolled acres', + ], + action_items: [ + 'Apply 200 lbs/ac 0-46-0 on South Bottoms before corn planting', + 'Evaluate EQIP carbon signup before February deadline', + 'Address SW corner compaction on North 80 — consider subsoiling pass', + 'Extend cover crop trial from East Pivot to North 80', + ], + risk_flags: [ + 'La Niña pattern threatens spring drought — irrigation scheduling critical for East Pivot', + 'South Bottoms flood risk near creek — monitor spring soil moisture before planting', + 'Input costs at $652/ac — watch corn futures for margin compression', + ], + opportunities: [ + 'Carbon credit program: $45-65/ac for documented no-till + cover crops', + 'East Pivot irrigation gives drought hedge — consider expanding irrigated acres', + 'Soybean rotation on North 80 in 2024 — lower input cost year', + ], + }, +}; + +async function main() { + if (!existsSync(DIST + '/manifest.json')) { + console.error('ERROR: dist/ not found. Run: npm run build'); + process.exit(1); + } + + console.log('Launching Chrome with seeded Agrifine extension…'); + + const context = await chromium.launchPersistentContext('', { + executablePath: CHROMIUM, + headless: true, + args: [ + `--disable-extensions-except=${DIST}`, + `--load-extension=${DIST}`, + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + ], + }); + + const page = await context.newPage(); + + // Stub chrome.* with SEEDED data + await page.addInitScript((seed) => { + const store = { ...seed }; + const sessionStore = {}; + + window.chrome = { + storage: { + local: { + get: (k, cb) => setTimeout(() => cb({ [k]: store[k] ?? null }), 0), + set: (obj, cb) => { Object.assign(store, obj); cb && cb(); }, + }, + session: { + get: (k, cb) => setTimeout(() => cb({ [k]: sessionStore[k] ?? null }), 0), + set: (obj, cb) => { Object.assign(sessionStore, obj); cb && cb(); }, + }, + }, + runtime: { + sendMessage: (_msg, cb) => cb && setTimeout(() => cb({ error: 'No background in test mode' }), 0), + connect: () => ({ onDisconnect: { addListener: () => {} } }), + lastError: null, + }, + tabs: { + query: (_q, cb) => cb([{ id: 1, url: 'https://farmprogress.com/corn', title: 'Farm Progress' }]), + sendMessage: (_id, _msg, cb) => cb && cb({ text: 'test page content' }), + }, + sidePanel: { setPanelBehavior: () => Promise.resolve() }, + scripting: { executeScript: (_opts, cb) => cb && cb([{ result: {} }]) }, + }; + }, SEED); + + await page.goto(`file://${DIST}/sidebar.html`); + await page.waitForSelector('#main-content', { timeout: 8000 }); + await page.waitForTimeout(600); + + async function ss(name) { + const file = `${SCREENSHOTS}/${name}.png`; + await page.screenshot({ path: file }); + console.log(` Screenshot: ${file}`); + return file; + } + + async function tab(name) { + const TAB_MAP = { + reading: '[data-tab="reading-list"]', + ingest: '[data-tab="data-ingest"]', + fields: '[data-tab="field-profile"]', + dashboard: '[data-tab="dashboard"]', + carbon: '[data-tab="carbon-estimator"]', + agent: '[data-tab="ag-refine"]', + }; + await page.click(TAB_MAP[name]); + await page.waitForTimeout(500); + } + + // ── 1. Reading List (default tab) ────────────────────────────────────────── + console.log('\n[1/6] Reading List tab'); + await ss('01_reading_list'); + + // ── 2. Data Ingest ───────────────────────────────────────────────────────── + console.log('\n[2/6] Data Ingest tab'); + await tab('ingest'); + await ss('02_data_ingest'); + + // ── 3. Field Profiles ────────────────────────────────────────────────────── + console.log('\n[3/6] Field Profiles tab'); + await tab('fields'); + await page.waitForTimeout(300); + await ss('03_field_profiles'); + + // Expand first field + const firstCard = page.locator('.agri-card').first(); + if (await firstCard.count()) { + await firstCard.click(); + await page.waitForTimeout(400); + await ss('03b_field_expanded'); + } + + // ── 4. Dashboard ────────────────────────────────────────────────────────── + console.log('\n[4/6] Dashboard tab'); + await tab('dashboard'); + await page.waitForTimeout(400); + await ss('04_dashboard'); + + // ── 5. Carbon Estimator ─────────────────────────────────────────────────── + console.log('\n[5/6] Carbon Estimator tab'); + await tab('carbon'); + await ss('05_carbon'); + + // ── 6. AgriAgent ───────────────────────────────────────────────────────── + console.log('\n[6/6] AgriAgent tab'); + await tab('agent'); + await ss('06_agent'); + + // ── Probe: settings panel ───────────────────────────────────────────────── + console.log('\n[probe] Settings panel'); + await page.click('#btn-settings'); + await page.waitForTimeout(300); + await ss('07_settings_panel'); + + // ── Probe: new field form ───────────────────────────────────────────────── + console.log('\n[probe] New field form'); + await tab('fields'); + await page.waitForTimeout(300); + await page.click('#fp-new-btn'); + await page.waitForTimeout(300); + await ss('08_new_field_form'); + + // ── Report what is and isn't visible ───────────────────────────────────── + const fieldCount = await page.evaluate(() => + document.querySelectorAll('.agri-card').length + ); + console.log(`\nField cards visible after form open: ${fieldCount}`); + + await context.close(); + console.log('\nAll screenshots saved to:', SCREENSHOTS); +} + +main().catch(err => { console.error(err); process.exit(1); });