feat(api): add POST /api/ai/test-extraction endpoint and Test Extraction UI button

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-23 21:09:27 +00:00
parent 1145ea3437
commit ce73f23a89
3 changed files with 497 additions and 3 deletions
+231
View File
@@ -181,6 +181,12 @@
data-provider="ai_provider">
Test Connection
</button>
<button
id="testAiExtractionBtn"
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-flask mr-1"></i>
Test Extraction
</button>
{% endif %}
{% elif name == "Azure AI" %}
{% if provider.configured %}
@@ -273,6 +279,85 @@
</div>
</div>
</div>
<!-- AI Extraction Test Modal -->
<div id="aiExtractionModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div class="relative top-10 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-2/3 xl:w-1/2 shadow-lg rounded-md bg-white">
<div class="absolute top-0 right-0 pt-4 pr-4">
<button type="button" id="closeAiExtractionModal" class="text-gray-400 hover:text-gray-500">
<span class="sr-only">Close</span>
<i class="fa-solid fa-xmark h-6 w-6"></i>
</button>
</div>
<div class="mt-3">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-1">
<i class="fa-solid fa-flask mr-2 text-indigo-600"></i>AI Extraction Test
</h3>
<p class="text-sm text-gray-500 mb-4">
Paste the plain-text content of a document below and run it through the configured AI provider
to inspect the raw response, extracted JSON, and tags.
</p>
<!-- Input area -->
<div id="aiExtractionInput">
<label for="aiExtractionText" class="block text-sm font-medium text-gray-700 mb-1">Document Text</label>
<textarea
id="aiExtractionText"
rows="10"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-mono focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Paste the plain-text content of your document here…"></textarea>
<div class="mt-3 flex justify-end space-x-2">
<button type="button" id="cancelAiExtractionBtn"
class="px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</button>
<button type="button" id="runAiExtractionBtn"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-play mr-2"></i>Run Extraction
</button>
</div>
</div>
<!-- Results area (hidden until extraction completes) -->
<div id="aiExtractionResults" class="hidden mt-4 space-y-4">
<!-- Provider / model badge -->
<div id="aiExtractionMeta" class="text-xs text-gray-500"></div>
<!-- Parse warning -->
<div id="aiParseWarning" class="hidden bg-yellow-50 border-l-4 border-yellow-400 p-3 text-sm text-yellow-800"></div>
<!-- Raw response -->
<div>
<h4 class="text-sm font-medium text-gray-900 mb-1">Raw LLM Response</h4>
<pre id="aiRawResponse" class="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-auto max-h-48 whitespace-pre-wrap break-words"></pre>
</div>
<!-- Parsed JSON -->
<div id="aiParsedJsonSection">
<h4 class="text-sm font-medium text-gray-900 mb-1">Parsed JSON</h4>
<pre id="aiParsedJson" class="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-auto max-h-64 whitespace-pre-wrap break-words"></pre>
</div>
<!-- Tags -->
<div id="aiTagsSection">
<h4 class="text-sm font-medium text-gray-900 mb-1">Extracted Tags</h4>
<div id="aiTags" class="flex flex-wrap gap-2"></div>
</div>
<div class="flex justify-end space-x-2 pt-2">
<button type="button" id="aiExtractionBackBtn"
class="px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-arrow-left mr-1"></i>Back
</button>
<button type="button" id="aiExtractionCloseBtn"
class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600">
Close
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -573,6 +658,152 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
});
// ── AI Extraction Test Modal ──────────────────────────────────────────────
const aiExtractionModal = document.getElementById('aiExtractionModal');
const aiExtractionText = document.getElementById('aiExtractionText');
const aiExtractionInput = document.getElementById('aiExtractionInput');
const aiExtractionResults = document.getElementById('aiExtractionResults');
const aiExtractionMeta = document.getElementById('aiExtractionMeta');
const aiRawResponse = document.getElementById('aiRawResponse');
const aiParsedJson = document.getElementById('aiParsedJson');
const aiParsedJsonSection = document.getElementById('aiParsedJsonSection');
const aiTags = document.getElementById('aiTags');
const aiTagsSection = document.getElementById('aiTagsSection');
const aiParseWarning = document.getElementById('aiParseWarning');
const testAiExtractionBtn = document.getElementById('testAiExtractionBtn');
const runAiExtractionBtn = document.getElementById('runAiExtractionBtn');
const cancelAiExtractionBtn= document.getElementById('cancelAiExtractionBtn');
const aiExtractionBackBtn = document.getElementById('aiExtractionBackBtn');
const aiExtractionCloseBtn = document.getElementById('aiExtractionCloseBtn');
const closeAiExtractionModal = document.getElementById('closeAiExtractionModal');
function openAiExtractionModal() {
// Reset to input view
aiExtractionText.value = '';
aiExtractionInput.classList.remove('hidden');
aiExtractionResults.classList.add('hidden');
aiExtractionModal.classList.remove('hidden');
}
function closeAiExtractionModalFn() {
aiExtractionModal.classList.add('hidden');
}
function showAiExtractionResults(data) {
// Provider / model meta
aiExtractionMeta.textContent = `Provider: ${data.provider || '—'} | Model: ${data.model || '—'}`;
// Raw response
aiRawResponse.textContent = data.raw_response || '(empty)';
// Parse warning
if (data.parse_error) {
aiParseWarning.textContent = `JSON parse issue: ${data.parse_error}`;
aiParseWarning.classList.remove('hidden');
} else {
aiParseWarning.classList.add('hidden');
}
// Parsed JSON
if (data.parsed_json) {
aiParsedJson.textContent = JSON.stringify(data.parsed_json, null, 2);
aiParsedJsonSection.classList.remove('hidden');
} else {
aiParsedJsonSection.classList.add('hidden');
}
// Tags
aiTags.innerHTML = '';
const tags = Array.isArray(data.tags) ? data.tags : [];
if (tags.length > 0) {
tags.forEach(tag => {
const span = document.createElement('span');
span.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800';
span.textContent = tag;
aiTags.appendChild(span);
});
aiTagsSection.classList.remove('hidden');
} else {
aiTagsSection.classList.add('hidden');
}
// Switch view
aiExtractionInput.classList.add('hidden');
aiExtractionResults.classList.remove('hidden');
}
if (testAiExtractionBtn) {
testAiExtractionBtn.addEventListener('click', openAiExtractionModal);
}
if (closeAiExtractionModal) {
closeAiExtractionModal.addEventListener('click', closeAiExtractionModalFn);
}
if (cancelAiExtractionBtn) {
cancelAiExtractionBtn.addEventListener('click', closeAiExtractionModalFn);
}
if (aiExtractionCloseBtn) {
aiExtractionCloseBtn.addEventListener('click', closeAiExtractionModalFn);
}
if (aiExtractionBackBtn) {
aiExtractionBackBtn.addEventListener('click', function() {
aiExtractionResults.classList.add('hidden');
aiExtractionInput.classList.remove('hidden');
});
}
// Close when clicking the backdrop
if (aiExtractionModal) {
aiExtractionModal.addEventListener('click', function(e) {
if (e.target === aiExtractionModal) {
closeAiExtractionModalFn();
}
});
}
if (runAiExtractionBtn) {
runAiExtractionBtn.addEventListener('click', function() {
const text = aiExtractionText.value.trim();
if (!text) {
aiExtractionText.focus();
return;
}
const originalHTML = runAiExtractionBtn.innerHTML;
runAiExtractionBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i>Running…';
runAiExtractionBtn.disabled = true;
cancelAiExtractionBtn.disabled = true;
fetch('/api/ai/test-extraction', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text }),
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showAiExtractionResults(data);
} else {
closeAiExtractionModalFn();
showModal('error', 'AI Extraction Failed', data.message || 'Unknown error');
}
})
.catch(error => {
closeAiExtractionModalFn();
showModal('error', 'Connection Error', 'Error running extraction: ' + error.message);
})
.finally(() => {
runAiExtractionBtn.innerHTML = originalHTML;
runAiExtractionBtn.disabled = false;
cancelAiExtractionBtn.disabled = false;
});
});
}
});
</script>
{% endblock %}