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 \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(' | ')