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:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user