Merge branch 'main' into copilot/add-self-hosted-ocr-support
This commit is contained in:
@@ -802,6 +802,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showCopyFeedback(type, success) {
|
||||
const btn = document.getElementById(type + '-copy-btn');
|
||||
if (!btn) return;
|
||||
const original = btn.innerHTML;
|
||||
if (success) {
|
||||
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||
btn.style.backgroundColor = '#48bb78';
|
||||
} else {
|
||||
btn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
|
||||
btn.style.backgroundColor = '#f56565';
|
||||
}
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = original;
|
||||
btn.style.backgroundColor = '#4299e1';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function copyTextToClipboard(type) {
|
||||
const contentId = type + '-text-content';
|
||||
const content = document.getElementById(contentId);
|
||||
const text = content ? content.textContent : '';
|
||||
|
||||
if (!text) return;
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showCopyFeedback(type, true);
|
||||
}).catch(() => {
|
||||
// Fallback for older browsers
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
showCopyFeedback(type, ok);
|
||||
} catch (e) {
|
||||
showCopyFeedback(type, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal when clicking outside the content
|
||||
window.onclick = function(event) {
|
||||
const modals = document.querySelectorAll('.text-modal');
|
||||
@@ -1210,12 +1254,21 @@
|
||||
<div class="text-modal-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #e2e8f0;">
|
||||
<h3 style="margin: 0; color: #2d3748;">Extracted Text (Original)</h3>
|
||||
<button
|
||||
onclick="toggleTextModal('original-text-modal')"
|
||||
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
|
||||
>
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
id="original-copy-btn"
|
||||
onclick="copyTextToClipboard('original')"
|
||||
style="background-color: #4299e1; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
|
||||
>
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
</button>
|
||||
<button
|
||||
onclick="toggleTextModal('original-text-modal')"
|
||||
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
|
||||
>
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="original-text-loading" style="text-align: center; padding: 3rem; color: #4299e1;">
|
||||
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 1rem;"></i>
|
||||
@@ -1230,12 +1283,21 @@
|
||||
<div class="text-modal-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #e2e8f0;">
|
||||
<h3 style="margin: 0; color: #2d3748;">Extracted Text (Processed)</h3>
|
||||
<button
|
||||
onclick="toggleTextModal('processed-text-modal')"
|
||||
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
|
||||
>
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
id="processed-copy-btn"
|
||||
onclick="copyTextToClipboard('processed')"
|
||||
style="background-color: #4299e1; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
|
||||
>
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
</button>
|
||||
<button
|
||||
onclick="toggleTextModal('processed-text-modal')"
|
||||
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
|
||||
>
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="processed-text-loading" style="text-align: center; padding: 3rem; color: #48bb78;">
|
||||
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 1rem;"></i>
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
Enable {{ setting.key.replace('_', ' ').title() }}
|
||||
</label>
|
||||
</div>
|
||||
<<<<<<< copilot/add-self-hosted-ocr-support
|
||||
{% elif setting.metadata.type == 'multiselect' and setting.metadata.options %}
|
||||
<!-- Multi-select checkboxes: stored as comma-separated string -->
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
@@ -160,6 +161,31 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-400">Effective value: <code x-text="formData['{{ setting.key }}'] || '(none)'"></code></p>
|
||||
=======
|
||||
{% elif setting.metadata.type == 'model_picker' %}
|
||||
<!-- Model Picker: free-text input with datalist of common models -->
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="{{ setting.key }}"
|
||||
name="{{ setting.key }}"
|
||||
list="{{ setting.key }}_models"
|
||||
x-model="formData['{{ setting.key }}']"
|
||||
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Select a common model or type a custom name…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<datalist id="{{ setting.key }}_models">
|
||||
{% for m in setting.metadata.suggested_models %}
|
||||
<option value="{{ m }}">{{ m }}</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Pick from the list or type any model name supported by your provider.
|
||||
</p>
|
||||
</div>
|
||||
>>>>>>> main
|
||||
{% elif setting.metadata.options %}
|
||||
<!-- Dropdown Select for fields with a fixed list of values -->
|
||||
<select
|
||||
|
||||
@@ -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