Fix document parsing and add AG-Refine sister-app sync

Document parsing fixes:
- Copy pdf.worker.min.mjs to dist/pdf.worker.js via webpack CopyPlugin
  (was missing, causing all PDF parsing to silently fail)
- Set webpack output.publicPath '/' so dynamic import chunks resolve
  correctly from Chrome extension chrome-extension:// URLs
- pdfjs: pass Uint8Array and set useWorkerFetch/isEvalSupported for
  extension CSP compatibility
- Add Python doc server (tools/doc_server.py) using flask + pandas +
  PyPDF2 at localhost:7432; extension tries it first then falls back
  to browser-side parsing for more robust CSV/Excel/PDF extraction

AG-Refine sister-app integration:
- New utils/agrefine-bridge.js: detects open AG-Refine tab by URL
  pattern, executes script to dump its localStorage, maps field/load
  data to Agrifine field profile schema, merges without overwriting
  user edits, logs sync history
- Background handler AGREFINE_SYNC calls syncFromAgRefine()
- Field Profiles module: "AG-Refine" sync button, status feedback,
  synced-field badge, "Open in AG-Refine ↗" link in expanded detail
- Settings panel: AG-Refine URL field so sync pins to a specific origin
- sidebar/index.js: loads and saves AG-Refine URL on settings open/save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
Claude 2026-06-27 06:54:41 +00:00
parent 1a9210178b
commit cac81d3a86
No known key found for this signature in database
13 changed files with 1544 additions and 147 deletions

View file

@ -2,6 +2,425 @@
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/utils/agrefine-bridge.js"
/*!**************************************!*\
!*** ./src/utils/agrefine-bridge.js ***!
\**************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ getAgRefineUrl: () => (/* binding */ getAgRefineUrl),
/* harmony export */ getSyncLog: () => (/* binding */ getSyncLog),
/* harmony export */ setAgRefineUrl: () => (/* binding */ setAgRefineUrl),
/* harmony export */ syncFromAgRefine: () => (/* binding */ syncFromAgRefine)
/* harmony export */ });
/* harmony import */ var _storage_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./storage.js */ "./src/utils/storage.js");
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 _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, 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 _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 _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; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
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 asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
/**
* AG-Refine Sister-App Bridge
*
* Detects an open AG-Refine tab, pulls field and output data from its
* localStorage/sessionStorage, and maps it into Agrifine field profiles.
*
* AG-Refine tab detection: any tab whose URL matches a configurable pattern
* (default: localhost:* OR any URL containing "ag-refine" or "agrefine").
* Set the URL in Settings > AG-Refine URL to pin it to a specific origin.
*/
var AGREFINE_KEY = 'agrifine_agrefine_url';
var SYNC_LOG_KEY = 'agrifine_agrefine_sync_log';
function getAgRefineUrl() {
return _getAgRefineUrl.apply(this, arguments);
}
function _getAgRefineUrl() {
_getAgRefineUrl = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
var _yield$localGet;
var _t, _t2, _t3;
return _regenerator().w(function (_context) {
while (1) switch (_context.n) {
case 0:
_context.n = 1;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.localGet)(AGREFINE_KEY);
case 1:
_t2 = _yield$localGet = _context.v;
_t = _t2 !== null;
if (!_t) {
_context.n = 2;
break;
}
_t = _yield$localGet !== void 0;
case 2:
if (!_t) {
_context.n = 3;
break;
}
_t3 = _yield$localGet;
_context.n = 4;
break;
case 3:
_t3 = '';
case 4:
return _context.a(2, _t3);
}
}, _callee);
}));
return _getAgRefineUrl.apply(this, arguments);
}
function setAgRefineUrl(_x) {
return _setAgRefineUrl.apply(this, arguments);
}
function _setAgRefineUrl() {
_setAgRefineUrl = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(url) {
return _regenerator().w(function (_context2) {
while (1) switch (_context2.n) {
case 0:
_context2.n = 1;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.localSet)(AGREFINE_KEY, url);
case 1:
return _context2.a(2);
}
}, _callee2);
}));
return _setAgRefineUrl.apply(this, arguments);
}
function getSyncLog() {
return _getSyncLog.apply(this, arguments);
}
function _getSyncLog() {
_getSyncLog = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
var _yield$localGet2;
var _t4, _t5, _t6;
return _regenerator().w(function (_context3) {
while (1) switch (_context3.n) {
case 0:
_context3.n = 1;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.localGet)(SYNC_LOG_KEY);
case 1:
_t5 = _yield$localGet2 = _context3.v;
_t4 = _t5 !== null;
if (!_t4) {
_context3.n = 2;
break;
}
_t4 = _yield$localGet2 !== void 0;
case 2:
if (!_t4) {
_context3.n = 3;
break;
}
_t6 = _yield$localGet2;
_context3.n = 4;
break;
case 3:
_t6 = [];
case 4:
return _context3.a(2, _t6);
}
}, _callee3);
}));
return _getSyncLog.apply(this, arguments);
}
function tabMatchesAgRefine(tab, configuredUrl) {
if (!tab.url) return false;
if (configuredUrl) {
try {
var origin = new URL(configuredUrl).origin;
return tab.url.startsWith(origin);
} catch (_) {}
}
var u = tab.url.toLowerCase();
return u.includes('ag-refine') || u.includes('agrefine') || u.startsWith('http://localhost') || u.startsWith('http://127.0.0.1');
}
// Injected into the AG-Refine tab — reads all storage and DOM hints
function scrapeAgRefineTab() {
var out = {
localStorage: {},
sessionStorage: {},
domHints: {}
};
for (var i = 0; i < localStorage.length; i++) {
var k = localStorage.key(i);
try {
out.localStorage[k] = JSON.parse(localStorage.getItem(k));
} catch (_) {
out.localStorage[k] = localStorage.getItem(k);
}
}
for (var _i = 0; _i < sessionStorage.length; _i++) {
var _k = sessionStorage.key(_i);
try {
out.sessionStorage[_k] = JSON.parse(sessionStorage.getItem(_k));
} catch (_) {
out.sessionStorage[_k] = sessionStorage.getItem(_k);
}
}
// Pull field-name-like text from the DOM as a fallback hint
var fieldEls = document.querySelectorAll('[data-field],[data-name],[data-id]');
fieldEls.forEach(function (el) {
var _ref, _el$dataset$field, _el$textContent;
var id = (_ref = (_el$dataset$field = el.dataset.field) !== null && _el$dataset$field !== void 0 ? _el$dataset$field : el.dataset.id) !== null && _ref !== void 0 ? _ref : el.dataset.name;
if (id) out.domHints[id] = ((_el$textContent = el.textContent) !== null && _el$textContent !== void 0 ? _el$textContent : '').trim().slice(0, 200);
});
return out;
}
/**
* Map raw AG-Refine storage dump to Agrifine field profile shape.
* Tries common key patterns used by React/Next.js ag apps.
*/
function extractFields(raw) {
var all = _objectSpread(_objectSpread({}, raw.localStorage), raw.sessionStorage);
var candidates = [];
for (var _i2 = 0, _Object$entries = Object.entries(all); _i2 < _Object$entries.length; _i2++) {
var _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2),
key = _Object$entries$_i[0],
val = _Object$entries$_i[1];
var k = key.toLowerCase();
if (!k.includes('field') && !k.includes('load') && !k.includes('farm') && !k.includes('plot')) continue;
var arr = Array.isArray(val) ? val : val && _typeof(val) === 'object' ? [val] : null;
if (!arr) continue;
var _iterator = _createForOfIteratorHelper(arr),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _ref2, _ref3, _ref4, _ref5, _item$name, _ref6, _item$id, _ref7, _ref8, _item$cluId, _ref9, _ref0, _item$acres, _ref1, _ref10, _item$soilType, _ref11, _item$lat, _item$coordinates, _ref12, _ref13, _ref14, _item$lon, _item$coordinates2, _item$coordinates3, _ref15, _ref16, _item$notes, _item$cropHistory, _item$cropHistory2, _item$harvests, _item$harvests2, _item$carbonPotential, _ref17, _item$createdAt;
var item = _step.value;
if (!item || _typeof(item) !== 'object') continue;
var name = (_ref2 = (_ref3 = (_ref4 = (_ref5 = (_item$name = item.name) !== null && _item$name !== void 0 ? _item$name : item.fieldName) !== null && _ref5 !== void 0 ? _ref5 : item.field_name) !== null && _ref4 !== void 0 ? _ref4 : item.title) !== null && _ref3 !== void 0 ? _ref3 : item.label) !== null && _ref2 !== void 0 ? _ref2 : null;
if (!name) continue;
candidates.push({
id: "agr_".concat((_ref6 = (_item$id = item.id) !== null && _item$id !== void 0 ? _item$id : item.fieldId) !== null && _ref6 !== void 0 ? _ref6 : Date.now(), "_").concat(Math.random().toString(36).slice(2, 6)),
name: String(name),
cluId: (_ref7 = (_ref8 = (_item$cluId = item.cluId) !== null && _item$cluId !== void 0 ? _item$cluId : item.clu_id) !== null && _ref8 !== void 0 ? _ref8 : item.clu) !== null && _ref7 !== void 0 ? _ref7 : null,
acres: parseFloat((_ref9 = (_ref0 = (_item$acres = item.acres) !== null && _item$acres !== void 0 ? _item$acres : item.area) !== null && _ref0 !== void 0 ? _ref0 : item.size) !== null && _ref9 !== void 0 ? _ref9 : item.acreage) || null,
soilType: (_ref1 = (_ref10 = (_item$soilType = item.soilType) !== null && _item$soilType !== void 0 ? _item$soilType : item.soil_type) !== null && _ref10 !== void 0 ? _ref10 : item.soil) !== null && _ref1 !== void 0 ? _ref1 : null,
coordinates: {
lat: parseFloat((_ref11 = (_item$lat = item.lat) !== null && _item$lat !== void 0 ? _item$lat : item.latitude) !== null && _ref11 !== void 0 ? _ref11 : (_item$coordinates = item.coordinates) === null || _item$coordinates === void 0 ? void 0 : _item$coordinates.lat) || null,
lon: parseFloat((_ref12 = (_ref13 = (_ref14 = (_item$lon = item.lon) !== null && _item$lon !== void 0 ? _item$lon : item.lng) !== null && _ref14 !== void 0 ? _ref14 : item.longitude) !== null && _ref13 !== void 0 ? _ref13 : (_item$coordinates2 = item.coordinates) === null || _item$coordinates2 === void 0 ? void 0 : _item$coordinates2.lon) !== null && _ref12 !== void 0 ? _ref12 : (_item$coordinates3 = item.coordinates) === null || _item$coordinates3 === void 0 ? void 0 : _item$coordinates3.lng) || null
},
notes: (_ref15 = (_ref16 = (_item$notes = item.notes) !== null && _item$notes !== void 0 ? _item$notes : item.description) !== null && _ref16 !== void 0 ? _ref16 : item.comments) !== null && _ref15 !== void 0 ? _ref15 : null,
cropHistory: Array.isArray((_item$cropHistory = item.cropHistory) !== null && _item$cropHistory !== void 0 ? _item$cropHistory : item.crop_history) ? (_item$cropHistory2 = item.cropHistory) !== null && _item$cropHistory2 !== void 0 ? _item$cropHistory2 : item.crop_history : [],
harvestRecords: Array.isArray((_item$harvests = item.harvests) !== null && _item$harvests !== void 0 ? _item$harvests : item.harvestRecords) ? (_item$harvests2 = item.harvests) !== null && _item$harvests2 !== void 0 ? _item$harvests2 : item.harvestRecords : [],
carbonPotential: (_item$carbonPotential = item.carbonPotential) !== null && _item$carbonPotential !== void 0 ? _item$carbonPotential : null,
weatherData: null,
createdAt: (_ref17 = (_item$createdAt = item.createdAt) !== null && _item$createdAt !== void 0 ? _item$createdAt : item.created_at) !== null && _ref17 !== void 0 ? _ref17 : new Date().toISOString(),
_source: 'ag-refine'
});
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
return candidates;
}
/**
* Loads also come over map to ingested file records for the dashboard.
*/
function extractLoads(raw) {
var all = _objectSpread(_objectSpread({}, raw.localStorage), raw.sessionStorage);
var loads = [];
for (var _i3 = 0, _Object$entries2 = Object.entries(all); _i3 < _Object$entries2.length; _i3++) {
var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i3], 2),
key = _Object$entries2$_i[0],
val = _Object$entries2$_i[1];
var k = key.toLowerCase();
if (!k.includes('load') && !k.includes('scale') && !k.includes('ticket') && !k.includes('delivery')) continue;
var arr = Array.isArray(val) ? val : null;
if (!arr) continue;
var _iterator2 = _createForOfIteratorHelper(arr),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var item = _step2.value;
if (!item || _typeof(item) !== 'object') continue;
loads.push(item);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
}
return loads;
}
function syncFromAgRefine() {
return _syncFromAgRefine.apply(this, arguments);
}
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) {
case 0:
_context5.n = 1;
return getAgRefineUrl();
case 1:
configuredUrl = _context5.v;
_context5.n = 2;
return chrome.tabs.query({});
case 2:
allTabs = _context5.v;
agRefineTabs = allTabs.filter(function (t) {
return tabMatchesAgRefine(t, configuredUrl);
});
if (!(agRefineTabs.length === 0)) {
_context5.n = 3;
break;
}
return _context5.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;
return chrome.scripting.executeScript({
target: {
tabId: tab.id
},
func: scrapeAgRefineTab
});
case 5:
_yield$chrome$scripti = _context5.v;
_yield$chrome$scripti2 = _slicedToArray(_yield$chrome$scripti, 1);
result = _yield$chrome$scripti2[0];
raw = result.result;
_context5.n = 7;
break;
case 6:
_context5.p = 6;
_t7 = _context5.v;
return _context5.a(2, {
ok: false,
error: "Cannot read AG-Refine tab: ".concat(_t7.message)
});
case 7:
fields = extractFields(raw);
loads = extractLoads(raw); // Merge fields — update existing by name, insert new ones
_context5.n = 8;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.getFieldProfiles)();
case 8:
existing = _context5.v;
added = 0;
updated = 0;
_iterator3 = _createForOfIteratorHelper(fields);
_context5.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) {
case 0:
f = _step3.value;
match = existing.find(function (e) {
return e.name.toLowerCase() === f.name.toLowerCase();
});
if (!match) {
_context4.n = 2;
break;
}
// Merge: fill in missing data without overwriting user edits
merged = _objectSpread(_objectSpread(_objectSpread({}, f), match), {}, {
coordinates: ((_match$coordinates = match.coordinates) === null || _match$coordinates === void 0 ? void 0 : _match$coordinates.lat) != null ? match.coordinates : f.coordinates,
cropHistory: (_match$cropHistory = match.cropHistory) !== null && _match$cropHistory !== void 0 && _match$cropHistory.length ? match.cropHistory : f.cropHistory,
notes: (_match$notes = match.notes) !== null && _match$notes !== void 0 ? _match$notes : f.notes,
cluId: (_match$cluId = match.cluId) !== null && _match$cluId !== void 0 ? _match$cluId : f.cluId,
_source: 'ag-refine-merged'
});
_context4.n = 1;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(merged);
case 1:
updated++;
_context4.n = 4;
break;
case 2:
_context4.n = 3;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveFieldProfile)(f);
case 3:
added++;
case 4:
return _context4.a(2);
}
}, _loop);
});
_iterator3.s();
case 10:
if ((_step3 = _iterator3.n()).done) {
_context5.n = 12;
break;
}
return _context5.d(_regeneratorValues(_loop()), 11);
case 11:
_context5.n = 10;
break;
case 12:
_context5.n = 14;
break;
case 13:
_context5.p = 13;
_t8 = _context5.v;
_iterator3.e(_t8);
case 14:
_context5.p = 14;
_iterator3.f();
return _context5.f(14);
case 15:
log = {
at: new Date().toISOString(),
tabUrl: tab.url,
fieldsAdded: added,
fieldsUpdated: updated,
loadsFound: loads.length,
rawKeys: Object.keys(_objectSpread(_objectSpread({}, raw.localStorage), raw.sessionStorage))
};
_context5.n = 16;
return getSyncLog();
case 16:
history = _context5.v;
history.unshift(log);
_context5.n = 17;
return (0,_storage_js__WEBPACK_IMPORTED_MODULE_0__.localSet)(SYNC_LOG_KEY, history.slice(0, 20));
case 17:
return _context5.a(2, {
ok: true,
added: added,
updated: updated,
loadsFound: loads.length,
loads: loads,
tabUrl: tab.url
});
}
}, _callee4, null, [[9, 13, 14, 15], [4, 6]]);
}));
return _syncFromAgRefine.apply(this, arguments);
}
/***/ },
/***/ "./src/utils/api.js"
/*!**************************!*\
!*** ./src/utils/api.js ***!
@ -869,6 +1288,7 @@ let __webpack_exports__ = {};
__webpack_require__.r(__webpack_exports__);
/* 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");
/* harmony import */ var _utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../utils/agrefine-bridge.js */ "./src/utils/agrefine-bridge.js");
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; } }
@ -882,6 +1302,7 @@ function _asyncToGenerator(n) { return function () { var t = this, e = arguments
// Open the side panel when the action icon is clicked
chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
@ -944,6 +1365,14 @@ chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
});
});
return true;
case 'AGREFINE_SYNC':
(0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_2__.syncFromAgRefine)().then(sendResponse)["catch"](function (err) {
return sendResponse({
ok: false,
error: err.message
});
});
return true;
default:
return false;
}

21
agrifine-extension/dist/pdf.worker.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1232,6 +1232,10 @@ body {
--tw-bg-opacity: 1;
background-color: rgb(19 28 43 / var(--tw-bg-opacity, 1));
}
.hover\:text-agri-400:hover {
--tw-text-opacity: 1;
color: rgb(74 222 128 / var(--tw-text-opacity, 1));
}
.hover\:text-gray-300:hover {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));

View file

@ -36,6 +36,15 @@
class="text-xs bg-agri-600 hover:bg-agri-700 text-white px-3 py-1.5 rounded-lg transition font-medium">Save</button>
</div>
<p id="api-key-status" class="text-xs mt-1.5" style="color:#3d4f66;"></p>
<label class="block text-[10px] uppercase tracking-widest font-semibold mb-2 mt-3" style="color:#3d4f66;">AG-Refine App URL</label>
<div class="flex gap-2">
<input id="agrefine-url-input" type="url" placeholder="http://localhost:3000"
class="ag-input flex-1" />
<button id="btn-save-agrefine-url"
class="text-xs bg-agri-600 hover:bg-agri-700 text-white px-3 py-1.5 rounded-lg transition font-medium">Save</button>
</div>
<p id="agrefine-url-status" class="text-xs mt-1" style="color:#3d4f66;">Used to sync fields and outputs from your AG-Refine app.</p>
</div>
<!-- Main content -->

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import { sessionGet, sessionSet, KEYS } from '../utils/storage.js';
import { fetchAnthropic } from '../utils/api.js';
import { syncFromAgRefine } from '../utils/agrefine-bridge.js';
// Open the side panel when the action icon is clicked
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error);
@ -47,6 +48,12 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
.catch((err) => sendResponse({ error: err.message }));
return true;
case 'AGREFINE_SYNC':
syncFromAgRefine()
.then(sendResponse)
.catch((err) => sendResponse({ ok: false, error: err.message }));
return true;
default:
return false;
}

View file

@ -8,6 +8,21 @@ const SUPPORTED_TYPES = {
'application/pdf': 'PDF',
};
const DOC_SERVER = 'http://localhost:7432';
async function tryDocServer(file) {
try {
const fd = new FormData();
fd.append('file', file);
const res = await fetch(`${DOC_SERVER}/parse`, { method: 'POST', body: fd });
if (!res.ok) return null;
const { text } = await res.json();
return text ?? null;
} catch (_) {
return null;
}
}
export function DataIngestModule() {
return {
id: 'data-ingest',
@ -82,17 +97,23 @@ export function DataIngestModule() {
status.textContent = `Parsing ${typeName}`;
let extractedText = '';
try {
if (typeName === 'CSV') {
extractedText = await this._parseCSV(file);
} else if (typeName === 'Excel') {
extractedText = await this._parseExcel(file);
} else if (typeName === 'PDF') {
extractedText = await this._parsePDF(file);
// Try Python doc server first (more robust), fall back to browser-side
extractedText = await tryDocServer(file);
if (extractedText) {
status.textContent = `Parsed via Python server…`;
} else {
try {
if (typeName === 'CSV') {
extractedText = await this._parseCSV(file);
} else if (typeName === 'Excel') {
extractedText = await this._parseExcel(file);
} else if (typeName === 'PDF') {
extractedText = await this._parsePDF(file);
}
} catch (err) {
status.textContent = `Parse error: ${err.message}`;
return;
}
} catch (err) {
status.textContent = `Parse error: ${err.message}`;
return;
}
status.textContent = 'Extracting structured data with AI…';
@ -173,7 +194,13 @@ export function DataIngestModule() {
try {
const pdfjsLib = await import('pdfjs-dist');
pdfjsLib.GlobalWorkerOptions.workerSrc = chrome.runtime.getURL('pdf.worker.js');
const pdf = await pdfjsLib.getDocument({ data: e.target.result }).promise;
const loadingTask = pdfjsLib.getDocument({
data: new Uint8Array(e.target.result),
useWorkerFetch: false,
isEvalSupported: false,
useSystemFonts: true,
});
const pdf = await loadingTask.promise;
const pages = Math.min(pdf.numPages, 10);
const texts = [];
for (let i = 1; i <= pages; i++) {

View file

@ -1,4 +1,5 @@
import { getFieldProfiles, saveFieldProfile, deleteFieldProfile } from '../../utils/storage.js';
import { getAgRefineUrl, setAgRefineUrl } from '../../utils/agrefine-bridge.js';
export function FieldProfileModule() {
let showForm = false;
@ -12,15 +13,23 @@ export function FieldProfileModule() {
container.innerHTML = `
<div class="section-heading">Field Profiles</div>
<div class="px-4 mb-3">
<div class="px-4 mb-3 flex gap-2">
<button id="fp-new-btn"
class="w-full flex items-center justify-center gap-2 bg-agri-600 hover:bg-agri-700 text-white text-sm font-medium py-2.5 rounded-xl transition">
class="flex-1 flex items-center justify-center gap-2 bg-agri-600 hover:bg-agri-700 text-white text-sm font-medium py-2.5 rounded-xl transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
New Field Profile
New Field
</button>
<button id="fp-agrefine-sync-btn" title="Sync fields from AG-Refine"
class="flex items-center justify-center gap-1.5 border border-night-500 text-gray-300 hover:border-agri-500 hover:text-agri-400 text-xs font-medium px-3 py-2.5 rounded-xl transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
AG-Refine
</button>
</div>
<div id="fp-sync-status" class="px-4 text-xs min-h-[1rem] mb-2" style="color:#3d4f66;"></div>
<!-- Create form -->
<div id="fp-form" class="hidden px-4 mb-4 bg-night-700 border border-night-600 rounded-xl mx-4 p-4">
@ -65,6 +74,8 @@ 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-cancel-btn').addEventListener('click', () => {
showForm = false;
container.querySelector('#fp-form').classList.add('hidden');
@ -99,8 +110,33 @@ export function FieldProfileModule() {
});
},
async _syncAgRefine(container) {
const statusEl = container.querySelector('#fp-sync-status');
statusEl.textContent = 'Connecting to AG-Refine tab…';
statusEl.style.color = '#3d4f66';
const result = await chrome.runtime.sendMessage({ type: 'AGREFINE_SYNC' });
if (!result.ok) {
statusEl.textContent = `${result.error}`;
statusEl.style.color = '#f87171';
setTimeout(() => { statusEl.textContent = ''; }, 5000);
return;
}
const parts = [];
if (result.added) parts.push(`${result.added} 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.style.color = '#4ade80';
setTimeout(() => { statusEl.textContent = ''; }, 4000);
await this._renderList(container);
},
async _renderList(container) {
const profiles = await getFieldProfiles();
const agRefineUrl = await getAgRefineUrl();
const listEl = container.querySelector('#fp-list');
if (profiles.length === 0) {
@ -144,6 +180,8 @@ export function FieldProfileModule() {
<div class="fp-detail ${expandedId === p.id ? '' : 'hidden'} mt-3 pt-3 border-t border-night-600 text-xs text-gray-400 space-y-1">
${p.coordinates?.lat != null && p.coordinates?.lon != null ? `<p>📍 ${p.coordinates.lat.toFixed(4)}, ${p.coordinates.lon.toFixed(4)}</p>` : ''}
${p.notes ? `<p>📝 ${p.notes}</p>` : ''}
${p._source?.includes('ag-refine') ? `<p class="text-agri-400">↗ Synced from AG-Refine</p>` : ''}
${agRefineUrl ? `<a href="${agRefineUrl}" target="_blank" rel="noopener noreferrer" class="text-agri-400 hover:underline">Open in AG-Refine ↗</a>` : ''}
<p class="text-gray-500">Weather data: <span class="coming-soon">Phase 6</span></p>
<p class="text-gray-500">Carbon potential: <span class="coming-soon">Phase 7</span></p>
<p class="text-gray-500">Added ${new Date(p.createdAt).toLocaleDateString()}</p>

View file

@ -6,6 +6,7 @@ import { DashboardModule } from '../modules/dashboard/index.js';
import { CarbonEstimatorModule } from '../modules/carbon-estimator/index.js';
import { AgRefineModule } from '../ag-refine/index.js';
import { sessionSet, sessionGet, KEYS } from '../utils/storage.js';
import { getAgRefineUrl, setAgRefineUrl } from '../utils/agrefine-bridge.js';
// ── Module registry ───────────────────────────────────────────────────────────
const MODULES = [
@ -50,6 +51,10 @@ function setupSettings() {
const input = document.getElementById('api-key-input');
const status = document.getElementById('api-key-status');
const agRefineInput = document.getElementById('agrefine-url-input');
const agRefineStatus = document.getElementById('agrefine-url-status');
const agRefineSaveBtn = document.getElementById('btn-save-agrefine-url');
btn.addEventListener('click', async () => {
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')) {
@ -59,9 +64,19 @@ function setupSettings() {
input.placeholder = 'Key set — enter new key to replace';
status.textContent = '✓ API key is active this session';
}
const agUrl = await getAgRefineUrl();
if (agUrl) agRefineInput.value = agUrl;
}
});
agRefineSaveBtn.addEventListener('click', async () => {
const url = agRefineInput.value.trim();
await setAgRefineUrl(url);
agRefineStatus.textContent = url ? `✓ AG-Refine URL saved` : '✓ Cleared';
agRefineStatus.style.color = '#4ade80';
setTimeout(() => { agRefineStatus.style.color = '#3d4f66'; agRefineStatus.textContent = 'Used to sync fields and outputs from your AG-Refine app.'; }, 2500);
});
saveBtn.addEventListener('click', async () => {
const key = input.value.trim();
if (!key.startsWith('sk-ant-')) {

View file

@ -36,6 +36,15 @@
class="text-xs bg-agri-600 hover:bg-agri-700 text-white px-3 py-1.5 rounded-lg transition font-medium">Save</button>
</div>
<p id="api-key-status" class="text-xs mt-1.5" style="color:#3d4f66;"></p>
<label class="block text-[10px] uppercase tracking-widest font-semibold mb-2 mt-3" style="color:#3d4f66;">AG-Refine App URL</label>
<div class="flex gap-2">
<input id="agrefine-url-input" type="url" placeholder="http://localhost:3000"
class="ag-input flex-1" />
<button id="btn-save-agrefine-url"
class="text-xs bg-agri-600 hover:bg-agri-700 text-white px-3 py-1.5 rounded-lg transition font-medium">Save</button>
</div>
<p id="agrefine-url-status" class="text-xs mt-1" style="color:#3d4f66;">Used to sync fields and outputs from your AG-Refine app.</p>
</div>
<!-- Main content -->

View file

@ -0,0 +1,203 @@
/**
* AG-Refine Sister-App Bridge
*
* Detects an open AG-Refine tab, pulls field and output data from its
* localStorage/sessionStorage, and maps it into Agrifine field profiles.
*
* AG-Refine tab detection: any tab whose URL matches a configurable pattern
* (default: localhost:* OR any URL containing "ag-refine" or "agrefine").
* Set the URL in Settings > AG-Refine URL to pin it to a specific origin.
*/
import { getFieldProfiles, saveFieldProfile, localGet, localSet } from './storage.js';
const AGREFINE_KEY = 'agrifine_agrefine_url';
const SYNC_LOG_KEY = 'agrifine_agrefine_sync_log';
export async function getAgRefineUrl() {
return (await localGet(AGREFINE_KEY)) ?? '';
}
export async function setAgRefineUrl(url) {
await localSet(AGREFINE_KEY, url);
}
export async function getSyncLog() {
return (await localGet(SYNC_LOG_KEY)) ?? [];
}
function tabMatchesAgRefine(tab, configuredUrl) {
if (!tab.url) return false;
if (configuredUrl) {
try {
const origin = new URL(configuredUrl).origin;
return tab.url.startsWith(origin);
} catch (_) {}
}
const u = tab.url.toLowerCase();
return (
u.includes('ag-refine') ||
u.includes('agrefine') ||
u.startsWith('http://localhost') ||
u.startsWith('http://127.0.0.1')
);
}
// Injected into the AG-Refine tab — reads all storage and DOM hints
function scrapeAgRefineTab() {
const out = { localStorage: {}, sessionStorage: {}, domHints: {} };
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
try { out.localStorage[k] = JSON.parse(localStorage.getItem(k)); }
catch (_) { out.localStorage[k] = localStorage.getItem(k); }
}
for (let i = 0; i < sessionStorage.length; i++) {
const k = sessionStorage.key(i);
try { out.sessionStorage[k] = JSON.parse(sessionStorage.getItem(k)); }
catch (_) { out.sessionStorage[k] = sessionStorage.getItem(k); }
}
// Pull field-name-like text from the DOM as a fallback hint
const fieldEls = document.querySelectorAll('[data-field],[data-name],[data-id]');
fieldEls.forEach((el) => {
const id = el.dataset.field ?? el.dataset.id ?? el.dataset.name;
if (id) out.domHints[id] = (el.textContent ?? '').trim().slice(0, 200);
});
return out;
}
/**
* Map raw AG-Refine storage dump to Agrifine field profile shape.
* Tries common key patterns used by React/Next.js ag apps.
*/
function extractFields(raw) {
const all = { ...raw.localStorage, ...raw.sessionStorage };
const candidates = [];
for (const [key, val] of Object.entries(all)) {
const k = key.toLowerCase();
if (!k.includes('field') && !k.includes('load') && !k.includes('farm') && !k.includes('plot')) continue;
const arr = Array.isArray(val) ? val : (val && typeof val === 'object' ? [val] : null);
if (!arr) continue;
for (const item of arr) {
if (!item || typeof item !== 'object') continue;
const name = item.name ?? item.fieldName ?? item.field_name ?? item.title ?? item.label ?? null;
if (!name) continue;
candidates.push({
id: `agr_${item.id ?? item.fieldId ?? Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
name: String(name),
cluId: item.cluId ?? item.clu_id ?? item.clu ?? null,
acres: parseFloat(item.acres ?? item.area ?? item.size ?? item.acreage) || null,
soilType: item.soilType ?? item.soil_type ?? item.soil ?? null,
coordinates: {
lat: parseFloat(item.lat ?? item.latitude ?? item.coordinates?.lat) || null,
lon: parseFloat(item.lon ?? item.lng ?? item.longitude ?? item.coordinates?.lon ?? item.coordinates?.lng) || null,
},
notes: item.notes ?? item.description ?? item.comments ?? null,
cropHistory: Array.isArray(item.cropHistory ?? item.crop_history) ? (item.cropHistory ?? item.crop_history) : [],
harvestRecords: Array.isArray(item.harvests ?? item.harvestRecords) ? (item.harvests ?? item.harvestRecords) : [],
carbonPotential: item.carbonPotential ?? null,
weatherData: null,
createdAt: item.createdAt ?? item.created_at ?? new Date().toISOString(),
_source: 'ag-refine',
});
}
}
return candidates;
}
/**
* Loads also come over map to ingested file records for the dashboard.
*/
function extractLoads(raw) {
const all = { ...raw.localStorage, ...raw.sessionStorage };
const loads = [];
for (const [key, val] of Object.entries(all)) {
const k = key.toLowerCase();
if (!k.includes('load') && !k.includes('scale') && !k.includes('ticket') && !k.includes('delivery')) continue;
const arr = Array.isArray(val) ? val : null;
if (!arr) continue;
for (const item of arr) {
if (!item || typeof item !== 'object') continue;
loads.push(item);
}
}
return loads;
}
export async function syncFromAgRefine() {
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 in a browser tab first.' };
}
const tab = agRefineTabs[0];
let raw;
try {
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: scrapeAgRefineTab,
});
raw = result.result;
} catch (err) {
return { ok: false, error: `Cannot read AG-Refine tab: ${err.message}` };
}
const fields = extractFields(raw);
const loads = extractLoads(raw);
// Merge fields — update existing by name, insert new ones
const existing = await getFieldProfiles();
let added = 0;
let updated = 0;
for (const f of fields) {
const match = existing.find((e) => e.name.toLowerCase() === f.name.toLowerCase());
if (match) {
// Merge: fill in missing data without overwriting user edits
const merged = {
...f,
...match,
coordinates: match.coordinates?.lat != null ? match.coordinates : f.coordinates,
cropHistory: match.cropHistory?.length ? match.cropHistory : f.cropHistory,
notes: match.notes ?? f.notes,
cluId: match.cluId ?? f.cluId,
_source: 'ag-refine-merged',
};
await saveFieldProfile(merged);
updated++;
} else {
await saveFieldProfile(f);
added++;
}
}
const log = {
at: new Date().toISOString(),
tabUrl: tab.url,
fieldsAdded: added,
fieldsUpdated: updated,
loadsFound: loads.length,
rawKeys: Object.keys({ ...raw.localStorage, ...raw.sessionStorage }),
};
const history = await getSyncLog();
history.unshift(log);
await localSet(SYNC_LOG_KEY, history.slice(0, 20));
return { ok: true, added, updated, loadsFound: loads.length, loads, tabUrl: tab.url };
}

View file

@ -0,0 +1,101 @@
"""
Agrifine Document Processing Server
Runs locally at http://localhost:7432 and gives the extension robust
document parsing for CSV, Excel, and PDF files via Python libraries.
Install deps: pip install flask flask-cors pandas openpyxl pypdf2
Run: python tools/doc_server.py
"""
import io
import json
import sys
import traceback
try:
from flask import Flask, request, jsonify
from flask_cors import CORS
import pandas as pd
import PyPDF2
except ImportError as e:
print(f"Missing dependency: {e}")
print("Run: pip install flask flask-cors pandas openpyxl pypdf2")
sys.exit(1)
app = Flask(__name__)
CORS(app, origins=["chrome-extension://*"])
PORT = 7432
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "service": "agrifine-doc-server"})
@app.route("/parse", methods=["POST"])
def parse_document():
if "file" not in request.files:
return jsonify({"error": "No file provided"}), 400
f = request.files["file"]
filename = f.filename.lower()
data = f.read()
try:
if filename.endswith(".csv"):
text, preview = _parse_csv(data)
elif filename.endswith((".xlsx", ".xls")):
text, preview = _parse_excel(data, filename)
elif filename.endswith(".pdf"):
text, preview = _parse_pdf(data)
else:
return jsonify({"error": f"Unsupported file type: {filename}"}), 400
return jsonify({"text": text, "preview": preview, "filename": f.filename})
except Exception:
return jsonify({"error": traceback.format_exc()}), 500
def _parse_csv(data: bytes):
df = pd.read_csv(io.BytesIO(data), nrows=500)
text = df.to_csv(index=False)
preview = _df_preview(df)
return text, preview
def _parse_excel(data: bytes, filename: str):
engine = "openpyxl" if filename.endswith(".xlsx") else "xlrd"
xl = pd.ExcelFile(io.BytesIO(data), engine=engine)
parts = []
previews = {}
for sheet in xl.sheet_names[:4]:
df = xl.parse(sheet, nrows=200)
parts.append(f"Sheet: {sheet}\n{df.to_csv(index=False)}")
previews[sheet] = _df_preview(df)
return "\n\n".join(parts), json.dumps(previews)
def _parse_pdf(data: bytes):
reader = PyPDF2.PdfReader(io.BytesIO(data))
pages = min(len(reader.pages), 15)
texts = []
for i in range(pages):
texts.append(reader.pages[i].extract_text() or "")
text = "\n".join(texts)
preview = text[:600].replace("\n", " ").strip()
return text, preview
def _df_preview(df: "pd.DataFrame") -> str:
rows, cols = df.shape
col_names = ", ".join(str(c) for c in df.columns[:10])
sample = df.head(3).to_dict(orient="records")
return f"{rows} rows × {cols} cols | columns: {col_names} | sample: {json.dumps(sample[:2], default=str)[:300]}"
if __name__ == "__main__":
print(f"Agrifine doc server running at http://localhost:{PORT}")
print("The extension will auto-detect this server and use it for document parsing.")
app.run(host="127.0.0.1", port=PORT, debug=False)

View file

@ -19,6 +19,7 @@ module.exports = {
path: distDir,
filename: '[name].js',
clean: true,
publicPath: '/',
},
module: {
rules: [
@ -45,6 +46,10 @@ module.exports = {
{ from: path.join(publicDir, 'icons'), to: path.join(distDir, 'icons') },
{ from: path.join(rootDir, 'manifest.json'), to: distDir },
{ from: path.join(srcDir, 'sidebar', 'sidebar.html'), to: path.join(distDir, 'sidebar.html') },
{
from: path.join(rootDir, 'node_modules/pdfjs-dist/build/pdf.worker.min.mjs'),
to: path.join(distDir, 'pdf.worker.js'),
},
],
}),
],