feat: traçabilité source systématique + viewer interactif
Ajoute source_page/source_excerpt à tous les types (biologie, imagerie, traitements, actes CCAM, antécédents, complications). Convertit antecedents et complications en types structurés (Antecedent/Complication) avec validators backward-compat pour les vieux JSON. Étend _apply_source_tracking à tous les éléments du dossier. Ajoute un endpoint /api/source-text/ et un modal interactif dans le viewer avec surlignage du texte source. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -287,7 +287,10 @@
|
||||
{% set dp = dossier.diagnostic_principal %}
|
||||
<div class="card section">
|
||||
<h3>Diagnostic principal</h3>
|
||||
<div style="font-size:0.95rem;margin-bottom:0.5rem;">{{ dp.texte }}</div>
|
||||
<div style="font-size:0.95rem;margin-bottom:0.5rem;">
|
||||
{{ dp.texte }}
|
||||
{% if dp.source_page %}<button class="src-btn" onclick="showSource('{{ dp.source_excerpt|default('',true)|e }}', {{ dp.source_page }})">p.{{ dp.source_page }}</button>{% endif %}
|
||||
</div>
|
||||
{% if dp.cim10_suggestion %}
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;font-size:0.85rem;">{{ dp.cim10_suggestion }}</span>
|
||||
{{ dp.cim10_confidence | confidence_badge }}
|
||||
@@ -355,12 +358,7 @@
|
||||
<span class="badge" style="background:#e0e7ff;color:#3730a3;font-size:0.7rem;">{{ das.source }}</span>
|
||||
{% endif %}
|
||||
{% if das.source_page %}
|
||||
<span style="font-size:0.7rem;color:#64748b;">p.{{ das.source_page }}</span>
|
||||
{% endif %}
|
||||
{% if das.source_excerpt %}
|
||||
<details style="margin-top:0.2rem;"><summary style="font-size:0.7rem;color:#94a3b8;cursor:pointer;">extrait</summary>
|
||||
<pre style="font-size:0.7rem;white-space:pre-wrap;max-width:300px;color:#475569;">{{ das.source_excerpt }}</pre>
|
||||
</details>
|
||||
<button class="src-btn" onclick="showSource('{{ das.source_excerpt|default('',true)|e }}', {{ das.source_page }})">p.{{ das.source_page }}</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.8rem;color:#475569;">
|
||||
@@ -410,7 +408,7 @@
|
||||
<div class="card section">
|
||||
<h3>Actes CCAM ({{ dossier.actes_ccam|length }})</h3>
|
||||
<table>
|
||||
<thead><tr><th>Texte</th><th>Code CCAM</th><th>Regroupement</th><th>Date</th><th>Validité</th></tr></thead>
|
||||
<thead><tr><th>Texte</th><th>Code CCAM</th><th>Regroupement</th><th>Date</th><th>Validité</th><th>Source</th></tr></thead>
|
||||
<tbody>
|
||||
{% for a in dossier.actes_ccam %}
|
||||
<tr>
|
||||
@@ -432,6 +430,7 @@
|
||||
<div style="font-size:0.7rem;color:#dc2626;">{{ alerte }}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{% if a.source_page %}<button class="src-btn" onclick="showSource('{{ a.source_excerpt|default('',true)|e }}', {{ a.source_page }})">p.{{ a.source_page }}</button>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -444,13 +443,14 @@
|
||||
<div class="card section">
|
||||
<h3>Biologie clé ({{ dossier.biologie_cle|length }})</h3>
|
||||
<table>
|
||||
<thead><tr><th>Test</th><th>Valeur</th><th>Anomalie</th></tr></thead>
|
||||
<thead><tr><th>Test</th><th>Valeur</th><th>Anomalie</th><th>Source</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in dossier.biologie_cle %}
|
||||
<tr{% if b.anomalie %} class="anomalie"{% endif %}>
|
||||
<td>{{ b.test }}</td>
|
||||
<td>{{ b.valeur or '' }}</td>
|
||||
<td>{% if b.anomalie %}<span class="badge" style="background:#fee2e2;color:#dc2626;">Oui</span>{% else %}—{% endif %}</td>
|
||||
<td>{% if b.source_page %}<button class="src-btn" onclick="showSource('{{ b.source_excerpt|default('',true)|e }}', {{ b.source_page }})">p.{{ b.source_page }}</button>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -466,6 +466,7 @@
|
||||
<div style="margin-bottom:0.5rem;">
|
||||
<strong>{{ img.type }}</strong>
|
||||
{% if img.score %} — Score : {{ img.score }}{% endif %}
|
||||
{% if img.source_page %}<button class="src-btn" onclick="showSource('{{ img.source_excerpt|default('',true)|e }}', {{ img.source_page }})">p.{{ img.source_page }}</button>{% endif %}
|
||||
{% if img.conclusion %}
|
||||
<div style="font-size:0.85rem;color:#475569;">{{ img.conclusion }}</div>
|
||||
{% endif %}
|
||||
@@ -479,13 +480,14 @@
|
||||
<div class="card section">
|
||||
<h3>Traitements de sortie ({{ dossier.traitements_sortie|length }})</h3>
|
||||
<table>
|
||||
<thead><tr><th>Médicament</th><th>Posologie</th><th>Code ATC</th></tr></thead>
|
||||
<thead><tr><th>Médicament</th><th>Posologie</th><th>Code ATC</th><th>Source</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in dossier.traitements_sortie %}
|
||||
<tr>
|
||||
<td>{{ t.medicament }}</td>
|
||||
<td>{{ t.posologie or '' }}</td>
|
||||
<td>{% if t.code_atc %}<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ t.code_atc }}</span>{% endif %}</td>
|
||||
<td>{% if t.source_page %}<button class="src-btn" onclick="showSource('{{ t.source_excerpt|default('',true)|e }}', {{ t.source_page }})">p.{{ t.source_page }}</button>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -499,7 +501,7 @@
|
||||
<h3>Antécédents ({{ dossier.antecedents|length }})</h3>
|
||||
<ul class="bullet">
|
||||
{% for a in dossier.antecedents %}
|
||||
<li>{{ a }}</li>
|
||||
<li>{{ a.texte }}{% if a.source_page %} <button class="src-btn" onclick="showSource('{{ a.source_excerpt|default('',true)|e }}', {{ a.source_page }})">p.{{ a.source_page }}</button>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -511,16 +513,109 @@
|
||||
<h3>Complications ({{ dossier.complications|length }})</h3>
|
||||
<ul class="bullet">
|
||||
{% for c in dossier.complications %}
|
||||
<li>{{ c }}</li>
|
||||
<li>{{ c.texte }}{% if c.source_page %} <button class="src-btn" onclick="showSource('{{ c.source_excerpt|default('',true)|e }}', {{ c.source_page }})">p.{{ c.source_page }}</button>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ---- Modal source ---- #}
|
||||
<div id="source-modal">
|
||||
<div id="source-modal-inner">
|
||||
<div id="source-header">
|
||||
<span id="source-title">Document source</span>
|
||||
<button id="source-close-btn" onclick="closeSource()">Fermer</button>
|
||||
</div>
|
||||
<div id="source-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
/* --- Source modal --- */
|
||||
let _sourceCache = null;
|
||||
|
||||
function getDossierId() {
|
||||
// filepath = "103_23056749/103_23056749_fusionne_cim10.json"
|
||||
// dossier_id = "103_23056749"
|
||||
const fp = '{{ filepath }}';
|
||||
const parts = fp.split('/');
|
||||
return parts.length > 1 ? parts.slice(0, -1).join('/') : '';
|
||||
}
|
||||
|
||||
async function loadSourceTexts() {
|
||||
if (_sourceCache !== null) return _sourceCache;
|
||||
const dossierId = getDossierId();
|
||||
if (!dossierId) { _sourceCache = {}; return _sourceCache; }
|
||||
try {
|
||||
const resp = await fetch('/api/source-text/' + dossierId);
|
||||
if (resp.ok) { _sourceCache = await resp.json(); }
|
||||
else { _sourceCache = {}; }
|
||||
} catch (e) { _sourceCache = {}; }
|
||||
return _sourceCache;
|
||||
}
|
||||
|
||||
async function showSource(excerpt, page) {
|
||||
const modal = document.getElementById('source-modal');
|
||||
const content = document.getElementById('source-content');
|
||||
const title = document.getElementById('source-title');
|
||||
|
||||
title.textContent = 'Document source — Page ' + page;
|
||||
content.innerHTML = '<em style="color:#94a3b8;">Chargement...</em>';
|
||||
modal.style.display = 'block';
|
||||
|
||||
const texts = await loadSourceTexts();
|
||||
const allText = Object.values(texts).join('\n\n--- ---\n\n');
|
||||
|
||||
if (!allText) {
|
||||
content.innerHTML = '<em style="color:#94a3b8;">Texte source non disponible</em>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Chercher l'extrait dans le texte et le surligner
|
||||
if (excerpt && excerpt.length > 10) {
|
||||
const idx = allText.indexOf(excerpt);
|
||||
if (idx >= 0) {
|
||||
const before = allText.substring(0, idx);
|
||||
const match = allText.substring(idx, idx + excerpt.length);
|
||||
const after = allText.substring(idx + excerpt.length);
|
||||
content.innerHTML = '';
|
||||
content.appendChild(document.createTextNode(before));
|
||||
const mark = document.createElement('mark');
|
||||
mark.textContent = match;
|
||||
mark.id = 'source-highlight';
|
||||
content.appendChild(mark);
|
||||
content.appendChild(document.createTextNode(after));
|
||||
// Scroll vers le surlignage
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById('source-highlight');
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback : afficher le texte brut sans surlignage
|
||||
content.textContent = allText;
|
||||
}
|
||||
|
||||
function closeSource() {
|
||||
document.getElementById('source-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Fermer le modal en cliquant sur le fond
|
||||
document.getElementById('source-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeSource();
|
||||
});
|
||||
|
||||
// Fermer avec Escape
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeSource();
|
||||
});
|
||||
|
||||
/* --- Reprocess --- */
|
||||
document.getElementById('reprocess-btn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('reprocess-btn');
|
||||
const status = document.getElementById('reprocess-status');
|
||||
|
||||
Reference in New Issue
Block a user