mirror of
https://github.com/vinta/awesome-python.git
synced 2026-06-28 03:42:13 +00:00
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:
parent
1a9210178b
commit
cac81d3a86
13 changed files with 1544 additions and 147 deletions
429
agrifine-extension/dist/background.js
vendored
429
agrifine-extension/dist/background.js
vendored
|
|
@ -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
21
agrifine-extension/dist/pdf.worker.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
agrifine-extension/dist/sidebar.css
vendored
4
agrifine-extension/dist/sidebar.css
vendored
|
|
@ -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));
|
||||
|
|
|
|||
9
agrifine-extension/dist/sidebar.html
vendored
9
agrifine-extension/dist/sidebar.html
vendored
|
|
@ -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 -->
|
||||
|
|
|
|||
795
agrifine-extension/dist/sidebar.js
vendored
795
agrifine-extension/dist/sidebar.js
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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-')) {
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
203
agrifine-extension/src/utils/agrefine-bridge.js
Normal file
203
agrifine-extension/src/utils/agrefine-bridge.js
Normal 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 };
|
||||
}
|
||||
101
agrifine-extension/tools/doc_server.py
Normal file
101
agrifine-extension/tools/doc_server.py
Normal 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)
|
||||
|
|
@ -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'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue