`;
}).join("");
}
function updateCharts() {
// Collect annotations from filtered messages only
const anns = filtered.flatMap(m => m.annotations);
renderBars("bars-emotion", countBy(anns, "emotion1"), EMOTION_COLORS);
renderBars("bars-mode", countBy(anns, "mode"), MODE_COLORS);
}
/* ── Annotated text rendering ─────────────────────────────────────────── */
function modeColor(mode) { return MODE_HL[mode] || MODE_HL_DEFAULT; }
/**
* Render full message text with all annotations highlighted.
* - Segments → background coloured by mode
* - Déclencheurs → red bold underline (class hl-decl)
* - Arrows → for eligible (non-disc, non-overlapping) annotations
*/
function renderAnnotated(msg, textSlice, offsetShift) {
const txt = textSlice !== undefined ? textSlice : (texts[msg.message_id] || "");
const shift = offsetShift || 0;
if (!txt) return esc(msg.annotations[0]?.segment_text || "–");
const len = txt.length;
const anns = msg.annotations;
// Determine which annotations are eligible for arrows.
// Two cases:
// A) Has déclencheur → arrow on déclencheur (single span, non-overlapping with other décl.)
// B) No déclencheur, has segment → arrow on segment (single span, non-overlapping with other seg.)
const eligDecl = new Set(); // case A
const eligSegOnly = new Set(); // case B
for (let i = 0; i < anns.length; i++) {
const decls = anns[i].declencheur_offsets || [];
const segs = anns[i].segment_offsets || [];
if (decls.length === 1) {
// Case A: has déclencheur
const [d1s, d1e] = decls[0];
let ov = false;
for (let j = 0; j < anns.length; j++) {
if (i === j) continue;
for (const [d2s, d2e] of (anns[j].declencheur_offsets || [])) {
if (Math.max(d1s, d2s) < Math.min(d1e, d2e)) { ov = true; break; }
}
if (ov) break;
}
if (!ov) eligDecl.add(i);
} else if (decls.length === 0 && segs.length === 1 && !anns[i].is_discontinuous) {
// Case B: no déclencheur, single segment
const [s1, e1] = segs[0];
let ov = false;
for (let j = 0; j < anns.length; j++) {
if (i === j) continue;
for (const [s2, e2] of (anns[j].segment_offsets || [])) {
if (Math.max(s1, s2) < Math.min(e1, e2)) { ov = true; break; }
}
if (ov) break;
}
if (!ov) eligSegOnly.add(i);
}
}
// Collect boundary events
const events = [];
anns.forEach((ann, ai) => {
const mode = ann.mode || "Autre";
const emo = ann.emotion1 || "";
for (const [s, e] of (ann.segment_offsets || [])) {
const ls = s - shift, le = e - shift;
if (le <= 0 || ls >= len) continue;
events.push({ pos: Math.max(0, ls), type: "seg", open: true, mode, emo, ai, elig: eligSegOnly.has(ai) });
events.push({ pos: Math.min(len, le), type: "seg", open: false, mode, emo, ai, elig: eligSegOnly.has(ai) });
}
for (const [s, e] of (ann.declencheur_offsets || [])) {
const ls = s - shift, le = e - shift;
if (le <= 0 || ls >= len) continue;
events.push({ pos: Math.max(0, ls), type: "decl", open: true, ai, emo, elig: eligDecl.has(ai) });
events.push({ pos: Math.min(len, le), type: "decl", open: false, ai, emo, elig: eligDecl.has(ai) });
}
});
events.sort((a, b) => a.pos - b.pos || (a.open === b.open ? 0 : a.open ? 1 : -1));
let html = "", pos = 0;
let activeSegs = [];
let activeDecls = [];
function flush(end) {
if (end <= pos) return;
const slice = esc(txt.slice(pos, end));
if (activeSegs.length > 0 && activeDecls.length > 0) {
html += `${slice}`;
} else if (activeSegs.length > 0) {
html += `${slice}`;
} else if (activeDecls.length > 0) {
html += `${slice}`;
} else {
html += slice;
}
pos = end;
}
// Track open arrow wrappers (annotation index → source type)
const openWrappers = new Set();
for (const ev of events) {
flush(ev.pos);
if (ev.type === "seg") {
if (ev.open) {
// Case B: segment-only arrow wrapper
if (ev.elig) {
html += ``;
openWrappers.add(ev.ai);
}
activeSegs.push(ev);
} else {
const idx = activeSegs.findIndex(s => s.ai === ev.ai);
if (idx >= 0) activeSegs.splice(idx, 1);
// Close segment-only arrow wrapper
if (ev.elig && openWrappers.has(ev.ai)) {
openWrappers.delete(ev.ai);
html += `↓${esc(ev.emo)}`;
}
}
} else { // decl
if (ev.open) {
// Case A: déclencheur arrow wrapper
if (ev.elig) {
html += ``;
openWrappers.add(ev.ai);
}
activeDecls.push(ev);
} else {
const idx = activeDecls.findIndex(d => d.ai === ev.ai);
if (idx >= 0) activeDecls.splice(idx, 1);
// Close déclencheur arrow wrapper
if (ev.elig && openWrappers.has(ev.ai)) {
openWrappers.delete(ev.ai);
html += `↓${esc(ev.emo)}`;
}
}
}
}
flush(len);
return html;
}
/**
* For table: extract a context window around annotations for long texts.
*/
function renderContext(msg) {
const fullText = texts[msg.message_id];
if (!fullText) return esc(msg.annotations[0]?.segment_text || "–");
if (fullText.length <= 500) return renderAnnotated(msg);
// Find bounds of all annotations
const allOff = msg.annotations.flatMap(a => [...(a.segment_offsets||[]), ...(a.declencheur_offsets||[])]);
if (!allOff.length) return esc(fullText.slice(0, 200)) + " …";
const minS = Math.min(...allOff.map(o => o[0]));
const maxE = Math.max(...allOff.map(o => o[1]));
// Expand to sentence boundaries
let ctxStart = Math.max(0, minS - 150);
let ctxEnd = Math.min(fullText.length, maxE + 150);
// Find sentence start
const left = fullText.slice(ctxStart, minS);
const lm = left.match(/[.!?\n]\s+/g);
if (lm) {
const li = left.lastIndexOf(lm[lm.length - 1]);
if (li >= 0) ctxStart += li + lm[lm.length - 1].length;
}
// Find sentence end
const right = fullText.slice(maxE, ctxEnd);
const rm = right.match(/[.!?…]\s/);
if (rm) ctxEnd = maxE + rm.index + 1;
while (ctxStart < minS && fullText[ctxStart] === " ") ctxStart++;
const prefix = ctxStart > 0 ? "… " : "";
const suffix = ctxEnd < fullText.length ? " …" : "";
return prefix + renderAnnotated(msg, fullText.slice(ctxStart, ctxEnd), ctxStart) + suffix;
}
/* ── Table helpers ────────────────────────────────────────────────────── */
function esc(s) {
if (!s) return "";
return s.replace(/&/g,"&").replace(//g,">").replace(/"/g,""");
}
function emotionPill(v) { return `${esc(v)}`; }
function modePill(v) { return `${esc(v)}`; }
function uniqueValues(anns, key) {
return [...new Set(anns.map(a => a[key]).filter(Boolean))];
}
/* ── Table ────────────────────────────────────────────────────────────── */
function renderTable() {
const total = Math.ceil(filtered.length / PAGE_SIZE);
const start = page * PAGE_SIZE;
const end = Math.min(start + PAGE_SIZE, filtered.length);
const slice = filtered.slice(start, end);
$("#table-count").textContent = filtered.length === 0
? "Aucun résultat"
: `${(start+1).toLocaleString("fr-FR")}–${end.toLocaleString("fr-FR")} sur ${filtered.length.toLocaleString("fr-FR")} messages`;
const tbody = document.getElementById("table-body");
tbody.innerHTML = slice.map((msg, i) => {
const emos = uniqueValues(msg.annotations, "emotion1");
const modes = uniqueValues(msg.annotations, "mode");
const nats = uniqueValues(msg.annotations, "nature_linguistique");
return `
${renderContext(msg)}
${emos.map(emotionPill).join("")}
${modes.map(modePill).join("")}
${nats.map(n => `${esc(n)}`).join("") || '–'}
`;
}).join("");
tbody.querySelectorAll("tr").forEach(tr => {
tr.addEventListener("click", () => openModal(filtered[parseInt(tr.dataset.idx, 10)]));
});
renderPagination(total);
}
function renderPagination(total) {
const c = document.getElementById("pagination");
if (total <= 1) { c.innerHTML = ""; return; }
let h = ``;
const mx = 7;
let s = Math.max(0, page - Math.floor(mx/2));
let e = Math.min(total, s + mx);
if (e - s < mx) s = Math.max(0, e - mx);
if (s > 0) { h += ``; if (s > 1) h += `…`; }
for (let i = s; i < e; i++) h += ``;
if (e < total) { if (e < total-1) h += `…`; h += ``; }
h += ``;
c.innerHTML = h;
c.querySelectorAll("button").forEach(btn => {
btn.addEventListener("click", () => {
const p = parseInt(btn.dataset.page, 10);
if (!isNaN(p) && p >= 0 && p < total) { page = p; renderTable(); document.getElementById("table-container").scrollIntoView({behavior:"smooth",block:"start"}); }
});
});
}
/* ── Modal ────────────────────────────────────────────────────────────── */
function openModal(msg) {
const body = document.getElementById("modal-body");
const fullText = texts[msg.message_id] || "";
// Render full annotated text
const annotatedHtml = fullText
? renderAnnotated(msg)
: esc(msg.annotations[0]?.segment_text || "–");
// List all annotations
const annHtml = msg.annotations.map((a, i) => {
const emos = [a.emotion1, a.emotion2, a.emotion3].filter(Boolean);
return `