188 lines
7.7 KiB
Twig
188 lines
7.7 KiB
Twig
|
|
{% extends '@EasyAdmin/page/content.html.twig' %}
|
||
|
|
|
||
|
|
{% block page_title %}Ingest Article{% endblock %}
|
||
|
|
|
||
|
|
{% block main %}
|
||
|
|
|
||
|
|
{% if catalogNumber %}
|
||
|
|
<div class="alert alert-success d-flex align-items-center gap-3 mb-4" role="alert">
|
||
|
|
<i class="fa fa-check-circle fa-2x"></i>
|
||
|
|
<div>
|
||
|
|
<strong>Queued — catalog number:</strong>
|
||
|
|
<span class="fs-4 ms-2 font-monospace fw-bold">{{ catalogNumber }}</span>
|
||
|
|
<div class="small mt-1">
|
||
|
|
Pipeline running.
|
||
|
|
<a href="{{ ea_url().setController('App\\Infrastructure\\Http\\Controller\\Admin\\AIPipelineJobCrudController').setAction('index').generateUrl() }}">Check job status →</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
<div class="row g-4">
|
||
|
|
|
||
|
|
{# ── Left: ingest form ─────────────────────────────────── #}
|
||
|
|
<div class="col-lg-7">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<h5 class="card-title mb-0"><i class="fa fa-camera me-2"></i>Scan Nameplate</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
{{ form_start(form, {'attr': {'enctype': 'multipart/form-data', 'novalidate': 'novalidate'}}) }}
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
{{ form_label(form.articleType) }}
|
||
|
|
{{ form_widget(form.articleType) }}
|
||
|
|
{{ form_errors(form.articleType) }}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
{{ form_label(form.condition) }}
|
||
|
|
{{ form_widget(form.condition) }}
|
||
|
|
{{ form_errors(form.condition) }}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
{{ form_label(form.image) }}
|
||
|
|
<div id="image-preview-wrapper" class="mb-2" style="display:none">
|
||
|
|
<img id="image-preview" src="" alt="Preview" class="img-thumbnail" style="max-height:220px">
|
||
|
|
</div>
|
||
|
|
{{ form_widget(form.image, {'attr': {'class': 'form-control', 'id': 'ingest-image-input'}}) }}
|
||
|
|
{{ form_errors(form.image) }}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
{{ form_label(form.conditionNotes) }}
|
||
|
|
{{ form_widget(form.conditionNotes, {'attr': {'class': 'form-control'}}) }}
|
||
|
|
{{ form_errors(form.conditionNotes) }}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||
|
|
<i class="fa fa-rocket me-2"></i>Submit to AI Pipeline
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{{ form_end(form) }}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{# ── Right: AI config panel ────────────────────────────── #}
|
||
|
|
<div class="col-lg-5">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||
|
|
<h5 class="card-title mb-0"><i class="fa fa-robot me-2"></i>AI Configuration</h5>
|
||
|
|
<span class="badge bg-{{ aiConfig.backend == 'Mistral' ? 'primary' : 'secondary' }}">
|
||
|
|
{{ aiConfig.backend }} active
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body p-0">
|
||
|
|
<table class="table table-sm table-borderless mb-0">
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<th class="ps-3 text-muted fw-normal" style="width:45%">Vision model</th>
|
||
|
|
<td class="font-monospace">{{ aiConfig.vision_model }}</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th class="ps-3 text-muted fw-normal">Text model</th>
|
||
|
|
<td class="font-monospace">{{ aiConfig.text_model }}</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th class="ps-3 text-muted fw-normal">Mistral endpoint</th>
|
||
|
|
<td class="font-monospace small">{{ aiConfig.mistral_base_url }}</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th class="ps-3 text-muted fw-normal">Mistral API key</th>
|
||
|
|
<td>
|
||
|
|
{% if aiConfig.mistral_key_set %}
|
||
|
|
<span class="badge bg-success me-1">set</span>
|
||
|
|
<span class="font-monospace small text-muted">{{ aiConfig.mistral_key_hint }}</span>
|
||
|
|
{% else %}
|
||
|
|
<span class="badge bg-danger">not set</span>
|
||
|
|
{% endif %}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th class="ps-3 text-muted fw-normal">Ollama endpoint</th>
|
||
|
|
<td class="font-monospace small">{{ aiConfig.ollama_base_url }}</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div class="card-footer">
|
||
|
|
<button id="btn-test-mistral" class="btn btn-outline-primary btn-sm w-100"
|
||
|
|
{% if not aiConfig.mistral_key_set %}disabled title="Set MISTRAL_API_KEY first"{% endif %}>
|
||
|
|
<i class="fa fa-plug me-1"></i>Test Mistral Connection
|
||
|
|
</button>
|
||
|
|
<div id="test-result" class="mt-3" style="display:none"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
document.getElementById('ingest-image-input')?.addEventListener('change', function () {
|
||
|
|
const file = this.files[0];
|
||
|
|
if (!file) return;
|
||
|
|
const wrapper = document.getElementById('image-preview-wrapper');
|
||
|
|
const img = document.getElementById('image-preview');
|
||
|
|
img.src = URL.createObjectURL(file);
|
||
|
|
wrapper.style.display = 'block';
|
||
|
|
});
|
||
|
|
|
||
|
|
document.getElementById('btn-test-mistral')?.addEventListener('click', async function () {
|
||
|
|
const btn = this;
|
||
|
|
const resultDiv = document.getElementById('test-result');
|
||
|
|
|
||
|
|
btn.disabled = true;
|
||
|
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i>Testing…';
|
||
|
|
resultDiv.style.display = 'none';
|
||
|
|
|
||
|
|
const body = new FormData();
|
||
|
|
body.append('target', 'mistral');
|
||
|
|
|
||
|
|
try {
|
||
|
|
const resp = await fetch('{{ path('admin_ai_test') }}', { method: 'POST', body });
|
||
|
|
const data = await resp.json();
|
||
|
|
|
||
|
|
resultDiv.style.display = 'block';
|
||
|
|
resultDiv.innerHTML = renderTestResult(data);
|
||
|
|
} catch (e) {
|
||
|
|
resultDiv.style.display = 'block';
|
||
|
|
resultDiv.innerHTML = '<div class="alert alert-danger py-2 mb-0">Request failed: ' + e.message + '</div>';
|
||
|
|
} finally {
|
||
|
|
btn.disabled = false;
|
||
|
|
btn.innerHTML = '<i class="fa fa-plug me-1"></i>Test Mistral Connection';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function renderTestResult(data) {
|
||
|
|
const rows = [
|
||
|
|
modelRow('Text', data.text),
|
||
|
|
modelRow('Vision', data.vision),
|
||
|
|
].join('');
|
||
|
|
|
||
|
|
return `<table class="table table-sm mb-0">${rows}</table>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function modelRow(label, r) {
|
||
|
|
if (r.ok) {
|
||
|
|
return `<tr>
|
||
|
|
<td><span class="badge bg-success">OK</span> <strong>${label}</strong></td>
|
||
|
|
<td class="font-monospace small">${escHtml(r.model)}</td>
|
||
|
|
<td class="text-muted small">${r.ms} ms</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="3" class="ps-3 text-muted small">${escHtml(r.response ?? '')}</td></tr>`;
|
||
|
|
}
|
||
|
|
return `<tr>
|
||
|
|
<td><span class="badge bg-danger">FAIL</span> <strong>${label}</strong></td>
|
||
|
|
<td colspan="2" class="font-monospace small text-danger">${escHtml(r.error ?? 'unknown error')}</td>
|
||
|
|
</tr>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function escHtml(s) {
|
||
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
{% endblock %}
|