/* ═══════════════════════════════════════════════════════════════════════════ Expression Émotionnelle — Viewer JavaScript 1 row = 1 message, all annotations shown simultaneously ═══════════════════════════════════════════════════════════════════════════ */ (() => { "use strict"; const PAGE_SIZE = 30; /* ── Colour maps ──────────────────────────────────────────────────────── */ const MODE_HL = { "Désignée": "rgba(40,80,160,.30)", "Comportementale": "rgba(0,130,100,.30)", "Suggérée": "rgba(180,100,20,.30)", "Montrée": "rgba(120,50,140,.30)", }; const MODE_HL_DEFAULT = "rgba(120,120,120,.30)"; const EMOTION_CSS = { "Colère":"pill-colere","Dégoût":"pill-degout","Joie":"pill-joie", "Peur":"pill-peur","Surprise":"pill-surprise","Tristesse":"pill-tristesse", "Admiration":"pill-admiration","Culpabilité":"pill-culpabilite", "Embarras":"pill-embarras","Fierté":"pill-fierte", "Jalousie":"pill-jalousie","Autre":"pill-autre", }; const MODE_CSS = { "Comportementale":"pill-mode-comportementale","Désignée":"pill-mode-designee", "Montrée":"pill-mode-montree","Suggérée":"pill-mode-suggeree", }; const EMOTION_COLORS = { "Colère":"#e8575a","Dégoût":"#7b6d8d","Joie":"#f4b942","Peur":"#484d6d", "Surprise":"#3ea8c2","Tristesse":"#5e7fa0","Admiration":"#e09f5b", "Culpabilité":"#9b8ea3","Embarras":"#cc8899","Fierté":"#5ea87b", "Jalousie":"#8b5d5d","Autre":"#a0a4b0", }; const MODE_COLORS = { "Comportementale":"#4361ee","Désignée":"#06d6a0", "Montrée":"#f4b942","Suggérée":"#e8575a", }; /* ── State ────────────────────────────────────────────────────────────── */ let allMessages = []; // grouped by message_id let allRecords = []; // flat (for chart counting) let texts = {}; let meta = {}; let filtered = []; let page = 0; let sortKey = null, sortAsc = true; let debounceTimer = null; let showDecl = true, showArrows = true; const $ = s => document.querySelector(s); const $$ = s => document.querySelectorAll(s); /* ── Load & group ─────────────────────────────────────────────────────── */ async function loadData() { try { const resp = await fetch("data.json"); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const payload = await resp.json(); allRecords = payload.records; texts = payload.texts || {}; meta = payload.meta; // Group records by message_id const map = new Map(); for (const r of allRecords) { const key = r.message_id; if (!map.has(key)) { map.set(key, { message_id: key, corpus: r.corpus, source_file: r.source_file, annotations: [] }); } map.get(key).annotations.push(r); } allMessages = [...map.values()]; initFilters(); applyFilters(); $("#loading-overlay").classList.add("hidden"); } catch (err) { $("#loading-overlay").innerHTML = `

Erreur : ${err.message}

Vérifiez data.json.

`; } } /* ── Filters ──────────────────────────────────────────────────────────── */ function buildCheckboxGroup(id, values) { const c = document.getElementById(id); c.innerHTML = ""; values.forEach(v => { const lbl = document.createElement("label"); lbl.className = "checkbox-item"; const cb = document.createElement("input"); cb.type = "checkbox"; cb.value = v; cb.checked = true; cb.addEventListener("change", scheduleFilter); const sp = document.createElement("span"); sp.textContent = v; lbl.append(cb, sp); c.appendChild(lbl); }); } function initFilters() { buildCheckboxGroup("corpus-checks", meta.corpora); buildCheckboxGroup("type-checks", meta.unit_types); buildCheckboxGroup("emotion-checks", meta.emotions); buildCheckboxGroup("mode-checks", meta.modes); buildCheckboxGroup("nature-checks", meta.natures); $("#search-input").addEventListener("input", scheduleFilter); $$("#discontinuous-checks input").forEach(cb => cb.addEventListener("change", scheduleFilter)); // Sidebar toggle $("#btn-close-sidebar").addEventListener("click", () => $("#sidebar").classList.add("collapsed")); $("#btn-open-sidebar").addEventListener("click", () => $("#sidebar").classList.remove("collapsed")); // Display toggles const declCb = $("#toggle-decl"); declCb.addEventListener("change", () => { showDecl = declCb.checked; document.body.classList.toggle("hide-decl", !showDecl); }); const arrowCb = $("#toggle-arrows"); arrowCb.addEventListener("change", () => { showArrows = arrowCb.checked; document.body.classList.toggle("show-arrows", showArrows); renderTable(); }); // Charts toggle const chartsToggle = $("#charts-toggle"); const chartsRow = $("#charts-row"); chartsToggle.addEventListener("click", () => { const isOpen = !chartsRow.classList.contains("charts-collapsed"); chartsRow.classList.toggle("charts-collapsed", isOpen); chartsToggle.classList.toggle("open", !isOpen); chartsToggle.firstElementChild.nextSibling; // force reflow if (!isOpen) { chartsRow.style.maxHeight = chartsRow.scrollHeight + "px"; } else { chartsRow.style.maxHeight = ""; } }); // Reset $("#btn-reset").addEventListener("click", resetFilters); // Sort $$("th.sortable").forEach(th => { th.addEventListener("click", () => { const key = th.dataset.sort; if (sortKey === key) sortAsc = !sortAsc; else { sortKey = key; sortAsc = true; } $$("th.sortable").forEach(h => h.classList.remove("sort-asc","sort-desc")); th.classList.add(sortAsc ? "sort-asc" : "sort-desc"); applyFilters(); }); }); // Modal $("#modal-close").addEventListener("click", closeModal); $("#modal-backdrop").addEventListener("click", e => { if (e.target === e.currentTarget) closeModal(); }); document.addEventListener("keydown", e => { if (e.key === "Escape") closeModal(); }); } function resetFilters() { $$("#sidebar input[type='checkbox']").forEach(cb => cb.checked = true); $("#search-input").value = ""; sortKey = null; sortAsc = true; showDecl = true; showArrows = true; $("#toggle-decl").checked = true; $("#toggle-arrows").checked = true; document.body.classList.remove("hide-decl"); document.body.classList.add("show-arrows"); $$("th.sortable").forEach(h => h.classList.remove("sort-asc","sort-desc")); applyFilters(); } function scheduleFilter() { clearTimeout(debounceTimer); debounceTimer = setTimeout(applyFilters, 120); } function checkedVals(id) { return new Set([...document.querySelectorAll(`#${id} input:checked`)].map(cb => cb.value)); } function applyFilters() { const corpora = checkedVals("corpus-checks"); const types = checkedVals("type-checks"); const emotions = checkedVals("emotion-checks"); const modes = checkedVals("mode-checks"); const natures = checkedVals("nature-checks"); const discont = checkedVals("discontinuous-checks"); const searchRaw = ($("#search-input").value || "").trim().toLowerCase(); const terms = searchRaw ? searchRaw.split(/\s+/) : []; filtered = allMessages.filter(msg => { if (!corpora.has(msg.corpus)) return false; // At least one annotation must pass const ok = msg.annotations.some(a => { if (!types.has(a.unit_type)) return false; const emos = [a.emotion1, a.emotion2, a.emotion3].filter(Boolean); if (emos.length > 0 && !emos.some(e => emotions.has(e))) return false; if (a.mode && !modes.has(a.mode)) return false; if (a.nature_linguistique && !natures.has(a.nature_linguistique)) return false; const d = a.is_discontinuous ? "true" : "false"; if (!discont.has(d)) return false; return true; }); if (!ok) return false; if (terms.length) { const hay = (texts[msg.message_id] || "").toLowerCase(); if (!terms.every(t => hay.includes(t))) return false; } return true; }); if (sortKey) { filtered.sort((a, b) => { const va = (a[sortKey] || "").toLowerCase(); const vb = (b[sortKey] || "").toLowerCase(); return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va); }); } page = 0; updateCharts(); renderTable(); } /* ── Charts (count individual annotations) ──────────────────────────── */ function countBy(arr, key) { const c = {}; arr.forEach(r => { const v = r[key]; if (v) c[v] = (c[v]||0)+1; }); return c; } function renderBars(id, counts, cmap) { const el = document.getElementById(id); const entries = Object.entries(counts).sort((a,b) => b[1]-a[1]); const mx = entries.length ? entries[0][1] : 1; el.innerHTML = entries.map(([l,c]) => { const pct = Math.max(c/mx*100, 1); const col = cmap[l] || "#a0a4b0"; return `
${esc(l)}
${c}
`; }).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 ` `; }).join(""); body.innerHTML = ` `; $("#modal-backdrop").classList.remove("hidden"); document.body.style.overflow = "hidden"; } function closeModal() { $("#modal-backdrop").classList.add("hidden"); document.body.style.overflow = ""; } /* ── Init ──────────────────────────────────────────────────────────────── */ document.addEventListener("DOMContentLoaded", loadData); })();