diff --git a/agrifine-extension/dist/background.js b/agrifine-extension/dist/background.js
index 5994aef..80339c0 100644
--- a/agrifine-extension/dist/background.js
+++ b/agrifine-extension/dist/background.js
@@ -1547,7 +1547,7 @@ function _buildContextBundle() {
var preview = f.structuredData ? Object.entries(f.structuredData).filter(function (_ref2) {
var _ref3 = _slicedToArray(_ref2, 1),
k = _ref3[0];
- return k !== 'raw_preview' && k !== 'parse_error';
+ return k !== 'raw_preview' && k !== 'raw_text' && k !== 'parse_error';
}).slice(0, 5).map(function (_ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
k = _ref5[0],
diff --git a/agrifine-extension/dist/sidebar.css b/agrifine-extension/dist/sidebar.css
index 7aced9c..86cd221 100644
--- a/agrifine-extension/dist/sidebar.css
+++ b/agrifine-extension/dist/sidebar.css
@@ -1049,6 +1049,10 @@ video {
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
}
+.text-amber-400 {
+ --tw-text-opacity: 1;
+ color: rgb(251 191 36 / var(--tw-text-opacity, 1));
+}
.text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity, 1));
@@ -1307,6 +1311,10 @@ body {
--tw-bg-opacity: 1;
background-color: rgb(19 28 43 / var(--tw-bg-opacity, 1));
}
+.hover\:text-agri-300:hover {
+ --tw-text-opacity: 1;
+ color: rgb(134 239 172 / var(--tw-text-opacity, 1));
+}
.hover\:text-agri-400:hover {
--tw-text-opacity: 1;
color: rgb(74 222 128 / var(--tw-text-opacity, 1));
diff --git a/agrifine-extension/dist/sidebar.js b/agrifine-extension/dist/sidebar.js
index 145f268..25f1320 100644
--- a/agrifine-extension/dist/sidebar.js
+++ b/agrifine-extension/dist/sidebar.js
@@ -2285,39 +2285,39 @@ function tryDocServer(_x) {
return _tryDocServer.apply(this, arguments);
}
function _tryDocServer() {
- _tryDocServer = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0(file) {
- var fd, res, _yield$res$json, text, _t6;
- return _regenerator().w(function (_context1) {
- while (1) switch (_context1.p = _context1.n) {
+ _tryDocServer = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee10(file) {
+ var fd, res, _yield$res$json, text, _t7;
+ return _regenerator().w(function (_context11) {
+ while (1) switch (_context11.p = _context11.n) {
case 0:
- _context1.p = 0;
+ _context11.p = 0;
fd = new FormData();
fd.append('file', file);
- _context1.n = 1;
+ _context11.n = 1;
return fetch("".concat(DOC_SERVER, "/parse"), {
method: 'POST',
body: fd
});
case 1:
- res = _context1.v;
+ res = _context11.v;
if (res.ok) {
- _context1.n = 2;
+ _context11.n = 2;
break;
}
- return _context1.a(2, null);
+ return _context11.a(2, null);
case 2:
- _context1.n = 3;
+ _context11.n = 3;
return res.json();
case 3:
- _yield$res$json = _context1.v;
+ _yield$res$json = _context11.v;
text = _yield$res$json.text;
- return _context1.a(2, text !== null && text !== void 0 ? text : null);
+ return _context11.a(2, text !== null && text !== void 0 ? text : null);
case 4:
- _context1.p = 4;
- _t6 = _context1.v;
- return _context1.a(2, null);
+ _context11.p = 4;
+ _t7 = _context11.v;
+ return _context11.a(2, null);
}
- }, _callee0, null, [[0, 4]]);
+ }, _callee10, null, [[0, 4]]);
}));
return _tryDocServer.apply(this, arguments);
}
@@ -2373,7 +2373,7 @@ function DataIngestModule() {
var _this3 = this;
return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
var _SUPPORTED_TYPES$file;
- var status, typeName, extractedText, structuredData, raw, record, _t, _t2;
+ var status, typeName, extractedText, structuredData, raw, _err$message, _err$message2, isNoKey, record, _t, _t2;
return _regenerator().w(function (_context2) {
while (1) switch (_context2.p = _context2.n) {
case 0:
@@ -2460,10 +2460,15 @@ function DataIngestModule() {
case 14:
_context2.p = 14;
_t2 = _context2.v;
+ isNoKey = ((_err$message = _t2.message) === null || _err$message === void 0 ? void 0 : _err$message.toLowerCase().includes('no api key')) || ((_err$message2 = _t2.message) === null || _err$message2 === void 0 ? void 0 : _err$message2.toLowerCase().includes('api key set'));
structuredData = {
- raw_preview: extractedText.slice(0, 500),
- parse_error: 'AI extraction unavailable'
+ raw_text: extractedText.slice(0, 6000),
+ // preserved for re-extraction
+ parse_error: isNoKey ? 'no_api_key' : 'ai_error'
};
+ if (isNoKey) {
+ status.textContent = '⚙ Set API key in Settings to extract data';
+ }
case 15:
record = {
id: "file_".concat(Date.now()),
@@ -2474,7 +2479,7 @@ function DataIngestModule() {
preview: Object.entries(structuredData !== null && structuredData !== void 0 ? structuredData : {}).filter(function (_ref) {
var _ref2 = _slicedToArray(_ref, 1),
k = _ref2[0];
- return k !== 'raw_preview' && k !== 'parse_error';
+ return k !== 'raw_preview' && k !== 'raw_text' && k !== 'parse_error';
}).slice(0, 5).map(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
k = _ref4[0],
@@ -2815,25 +2820,36 @@ function DataIngestModule() {
},
_renderFileList: function _renderFileList(container) {
var _this6 = this;
- return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
var files, listEl;
- return _regenerator().w(function (_context0) {
- while (1) switch (_context0.n) {
+ return _regenerator().w(function (_context1) {
+ while (1) switch (_context1.n) {
case 0:
- _context0.n = 1;
+ _context1.n = 1;
return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.getIngestedFiles)();
case 1:
- files = _context0.v;
+ files = _context1.v;
listEl = container.querySelector('#file-list');
if (!(files.length === 0)) {
- _context0.n = 2;
+ _context1.n = 2;
break;
}
listEl.innerHTML = "\n
\n
\n
No files ingested yet.
\n
Upload a CSV, Excel, or PDF file above.
\n
";
- return _context0.a(2);
+ return _context1.a(2);
case 2:
listEl.innerHTML = files.map(function (f) {
- return "\n \n
\n
\n
").concat(f.type, "\n
").concat(f.filename, "
\n
").concat(new Date(f.uploadedAt).toLocaleDateString(), "
\n
\n
\n
\n ").concat(f.preview ? "
".concat(f.preview, "
") : '', "\n ").concat(_this6._hasFieldData(f) ? "
\u2197 Contains field data \xB7
") : '', "\n
\n ");
+ var _f$structuredData;
+ var parseError = (_f$structuredData = f.structuredData) === null || _f$structuredData === void 0 ? void 0 : _f$structuredData.parse_error;
+ var cardFooter = '';
+ if (parseError === 'no_api_key') {
+ cardFooter = "\n \n \u2699 Add API key in Settings to extract\n \n
");
+ } else if (parseError === 'ai_error') {
+ cardFooter = "\n \n AI extraction failed\n \n
");
+ } else if (f.preview) {
+ cardFooter = "".concat(f.preview, "
");
+ }
+ var fieldLink = _this6._hasFieldData(f) ? "\u2197 Contains field data \xB7
") : '';
+ return "\n \n
\n
\n
").concat(f.type, "\n
").concat(f.filename, "
\n
").concat(new Date(f.uploadedAt).toLocaleDateString(), "
\n
\n
\n
\n ").concat(cardFooter, "\n ").concat(fieldLink, "\n
");
}).join('');
listEl.querySelectorAll('.file-delete-btn').forEach(function (btn) {
btn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
@@ -2872,10 +2888,110 @@ function DataIngestModule() {
}, _callee8);
})));
});
+ listEl.querySelectorAll('.reextract-btn').forEach(function (btn) {
+ btn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
+ var file;
+ return _regenerator().w(function (_context0) {
+ while (1) switch (_context0.n) {
+ case 0:
+ file = files.find(function (f) {
+ return f.id === btn.dataset.id;
+ });
+ if (!file) {
+ _context0.n = 1;
+ break;
+ }
+ _context0.n = 1;
+ return _this6._reExtractFile(file, container);
+ case 1:
+ return _context0.a(2);
+ }
+ }, _callee9);
+ })));
+ });
case 3:
- return _context0.a(2);
+ return _context1.a(2);
}
- }, _callee9);
+ }, _callee0);
+ }))();
+ },
+ _reExtractFile: function _reExtractFile(file, container) {
+ var _this7 = this;
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
+ var _ref14, _file$structuredData$, _file$structuredData, _file$structuredData2;
+ var status, rawText, structuredData, raw, _err$message3, _err$message4, isNoKey, updated, _t6;
+ return _regenerator().w(function (_context10) {
+ while (1) switch (_context10.p = _context10.n) {
+ case 0:
+ status = container.querySelector('#ingest-status');
+ rawText = (_ref14 = (_file$structuredData$ = (_file$structuredData = file.structuredData) === null || _file$structuredData === void 0 ? void 0 : _file$structuredData.raw_text) !== null && _file$structuredData$ !== void 0 ? _file$structuredData$ : (_file$structuredData2 = file.structuredData) === null || _file$structuredData2 === void 0 ? void 0 : _file$structuredData2.raw_preview) !== null && _ref14 !== void 0 ? _ref14 : '';
+ if (rawText) {
+ _context10.n = 1;
+ break;
+ }
+ status.textContent = 'No cached text — please re-upload the file.';
+ setTimeout(function () {
+ status.textContent = '';
+ }, 4000);
+ return _context10.a(2);
+ case 1:
+ status.textContent = "Re-extracting ".concat(file.filename, "\u2026");
+ structuredData = null;
+ _context10.p = 2;
+ _context10.n = 3;
+ return (0,_utils_api_js__WEBPACK_IMPORTED_MODULE_1__.callAnthropic)({
+ system: 'You are an agricultural data analyst. Extract and return structured JSON from this document. Identify: operation type, field names (as "fields" array of strings), dates, quantities, equipment, crop types, financial figures, and any carbon or emissions data. For harvest data include avg_yield_bu_ac, avg_moisture_pct, harvest_date, and crop. Return only valid JSON.',
+ userMessage: rawText.slice(0, 6000),
+ maxTokens: 1024
+ });
+ case 3:
+ raw = _context10.v;
+ structuredData = JSON.parse(raw);
+ _context10.n = 5;
+ break;
+ case 4:
+ _context10.p = 4;
+ _t6 = _context10.v;
+ isNoKey = ((_err$message3 = _t6.message) === null || _err$message3 === void 0 ? void 0 : _err$message3.toLowerCase().includes('no api key')) || ((_err$message4 = _t6.message) === null || _err$message4 === void 0 ? void 0 : _err$message4.toLowerCase().includes('api key set'));
+ status.textContent = isNoKey ? '⚙ API key required — open Settings (gear icon) to add your Anthropic key.' : "Extraction failed: ".concat(_t6.message.slice(0, 80));
+ status.style.color = '#f87171';
+ setTimeout(function () {
+ status.textContent = '';
+ status.style.color = '';
+ }, 6000);
+ return _context10.a(2);
+ case 5:
+ updated = _objectSpread(_objectSpread({}, file), {}, {
+ structuredData: structuredData,
+ preview: Object.entries(structuredData).filter(function (_ref15) {
+ var _ref16 = _slicedToArray(_ref15, 1),
+ k = _ref16[0];
+ return k !== 'raw_text' && k !== 'raw_preview';
+ }).slice(0, 5).map(function (_ref17) {
+ var _ref18 = _slicedToArray(_ref17, 2),
+ k = _ref18[0],
+ v = _ref18[1];
+ return "".concat(k, ": ").concat(JSON.stringify(v).slice(0, 80));
+ }).join('\n')
+ });
+ _context10.n = 6;
+ return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.saveIngestedFile)(updated);
+ case 6:
+ status.textContent = "\u2713 Extracted ".concat(file.filename);
+ status.style.color = '#4ade80';
+ setTimeout(function () {
+ status.textContent = '';
+ status.style.color = '';
+ }, 3000);
+ _context10.n = 7;
+ return _this7._renderFileList(container);
+ case 7:
+ _context10.n = 8;
+ return _this7._offerFieldImport(updated, container);
+ case 8:
+ return _context10.a(2);
+ }
+ }, _callee1, null, [[2, 4]]);
}))();
},
_hasFieldData: function _hasFieldData(file) {
@@ -4982,7 +5098,7 @@ function _buildContextBundle() {
var preview = f.structuredData ? Object.entries(f.structuredData).filter(function (_ref2) {
var _ref3 = _slicedToArray(_ref2, 1),
k = _ref3[0];
- return k !== 'raw_preview' && k !== 'parse_error';
+ return k !== 'raw_preview' && k !== 'raw_text' && k !== 'parse_error';
}).slice(0, 5).map(function (_ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
k = _ref5[0],
diff --git a/agrifine-extension/src/modules/data-ingest/index.js b/agrifine-extension/src/modules/data-ingest/index.js
index 06a3de6..12d48d4 100644
--- a/agrifine-extension/src/modules/data-ingest/index.js
+++ b/agrifine-extension/src/modules/data-ingest/index.js
@@ -148,8 +148,16 @@ export function DataIngestModule() {
maxTokens: 1024,
});
structuredData = JSON.parse(raw);
- } catch (_) {
- structuredData = { raw_preview: extractedText.slice(0, 500), parse_error: 'AI extraction unavailable' };
+ } catch (err) {
+ const isNoKey = err.message?.toLowerCase().includes('no api key') ||
+ err.message?.toLowerCase().includes('api key set');
+ structuredData = {
+ raw_text: extractedText.slice(0, 6000), // preserved for re-extraction
+ parse_error: isNoKey ? 'no_api_key' : 'ai_error',
+ };
+ if (isNoKey) {
+ status.textContent = '⚙ Set API key in Settings to extract data';
+ }
}
const record = {
@@ -159,7 +167,7 @@ export function DataIngestModule() {
uploadedAt: new Date().toISOString(),
structuredData,
preview: Object.entries(structuredData ?? {})
- .filter(([k]) => k !== 'raw_preview' && k !== 'parse_error')
+ .filter(([k]) => k !== 'raw_preview' && k !== 'raw_text' && k !== 'parse_error')
.slice(0, 5)
.map(([k, v]) => `${k}: ${JSON.stringify(v).slice(0, 80)}`)
.join('\n'),
@@ -362,24 +370,46 @@ export function DataIngestModule() {
return;
}
- listEl.innerHTML = files.map((f) => `
-
-
-
-
${f.type}
-
${f.filename}
-
${new Date(f.uploadedAt).toLocaleDateString()}
+ listEl.innerHTML = files.map((f) => {
+ const parseError = f.structuredData?.parse_error;
+ let cardFooter = '';
+ if (parseError === 'no_api_key') {
+ cardFooter = `
+
+ ⚙ Add API key in Settings to extract
+
+
`;
+ } else if (parseError === 'ai_error') {
+ cardFooter = `
+
+ AI extraction failed
+
+
`;
+ } else if (f.preview) {
+ cardFooter = `
${f.preview}`;
+ }
+ const fieldLink = this._hasFieldData(f)
+ ? `
↗ Contains field data ·
`
+ : '';
+
+ return `
+
+
+
+
${f.type}
+
${f.filename}
+
${new Date(f.uploadedAt).toLocaleDateString()}
+
+
-
-
- ${f.preview ? `
${f.preview}` : ''}
- ${this._hasFieldData(f) ? `
↗ Contains field data ·
` : ''}
-
- `).join('');
+ ${cardFooter}
+ ${fieldLink}
+
`;
+ }).join('');
listEl.querySelectorAll('.file-delete-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
@@ -394,6 +424,64 @@ export function DataIngestModule() {
if (file) await this._offerFieldImport(file, container);
});
});
+
+ listEl.querySelectorAll('.reextract-btn').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const file = files.find((f) => f.id === btn.dataset.id);
+ if (file) await this._reExtractFile(file, container);
+ });
+ });
+ },
+
+ async _reExtractFile(file, container) {
+ const status = container.querySelector('#ingest-status');
+ const rawText = file.structuredData?.raw_text ?? file.structuredData?.raw_preview ?? '';
+
+ if (!rawText) {
+ status.textContent = 'No cached text — please re-upload the file.';
+ setTimeout(() => { status.textContent = ''; }, 4000);
+ return;
+ }
+
+ status.textContent = `Re-extracting ${file.filename}…`;
+ let structuredData = null;
+
+ try {
+ const raw = await callAnthropic({
+ system: 'You are an agricultural data analyst. Extract and return structured JSON from this document. Identify: operation type, field names (as "fields" array of strings), dates, quantities, equipment, crop types, financial figures, and any carbon or emissions data. For harvest data include avg_yield_bu_ac, avg_moisture_pct, harvest_date, and crop. Return only valid JSON.',
+ userMessage: rawText.slice(0, 6000),
+ maxTokens: 1024,
+ });
+ structuredData = JSON.parse(raw);
+ } catch (err) {
+ const isNoKey = err.message?.toLowerCase().includes('no api key') ||
+ err.message?.toLowerCase().includes('api key set');
+ status.textContent = isNoKey
+ ? '⚙ API key required — open Settings (gear icon) to add your Anthropic key.'
+ : `Extraction failed: ${err.message.slice(0, 80)}`;
+ status.style.color = '#f87171';
+ setTimeout(() => { status.textContent = ''; status.style.color = ''; }, 6000);
+ return;
+ }
+
+ const updated = {
+ ...file,
+ structuredData,
+ preview: Object.entries(structuredData)
+ .filter(([k]) => k !== 'raw_text' && k !== 'raw_preview')
+ .slice(0, 5)
+ .map(([k, v]) => `${k}: ${JSON.stringify(v).slice(0, 80)}`)
+ .join('\n'),
+ };
+
+ await saveIngestedFile(updated);
+
+ status.textContent = `✓ Extracted ${file.filename}`;
+ status.style.color = '#4ade80';
+ setTimeout(() => { status.textContent = ''; status.style.color = ''; }, 3000);
+
+ await this._renderFileList(container);
+ await this._offerFieldImport(updated, container);
},
_hasFieldData(file) {
diff --git a/agrifine-extension/src/utils/storage.js b/agrifine-extension/src/utils/storage.js
index 5c0007a..18f47d7 100644
--- a/agrifine-extension/src/utils/storage.js
+++ b/agrifine-extension/src/utils/storage.js
@@ -224,7 +224,7 @@ export async function buildContextBundle() {
const fileLines = files.length === 0 ? ['(none)'] : files.slice(0, 10).map((f) => {
const preview = f.structuredData
? Object.entries(f.structuredData)
- .filter(([k]) => k !== 'raw_preview' && k !== 'parse_error')
+ .filter(([k]) => k !== 'raw_preview' && k !== 'raw_text' && k !== 'parse_error')
.slice(0, 5)
.map(([k, v]) => `${k}: ${JSON.stringify(v).slice(0, 120)}`)
.join(' | ')