// ─── Shared utilities (used by both ASR and TTS tabs) ───────────────────────── const SPEAKER_COLORS = [ '#4fa3e0','#e06b4f','#4fe09c','#e0c44f','#a04fe0', '#e04fa3','#4fd4e0','#e0a44f','#7fe04f','#9b59b6', ]; const colorMap = {}; let colorIdx = 0; function speakerColor(speaker) { if (!(speaker in colorMap)) { colorMap[speaker] = speaker === 'UNKNOWN' ? '#7a85a8' : SPEAKER_COLORS[colorIdx++ % SPEAKER_COLORS.length]; } return colorMap[speaker]; } function resetColors() { Object.keys(colorMap).forEach(k => delete colorMap[k]); colorIdx = 0; } function esc(text) { const d = document.createElement('div'); d.appendChild(document.createTextNode(text)); return d.innerHTML; } function fmtTime(s) { const m = Math.floor(s / 60); const sec = (s % 60).toFixed(1).padStart(4, '0'); return `${String(m).padStart(2,'0')}:${sec}`; } function fmtTimestamp(id) { // id: YYYYMMDD_HHMMSS_ffffff if (!id || id.length < 15) return id; const y = id.slice(0,4), mo = id.slice(4,6), d = id.slice(6,8); const h = id.slice(9,11), mi = id.slice(11,13), s = id.slice(13,15); return `${y}-${mo}-${d} ${h}:${mi}:${s}`; } function renderSpeakerBlocks(segments, container, sectionEl) { if (!segments || !segments.length || !segments.some(s => s.speaker)) { sectionEl.style.display = 'none'; return; } resetColors(); const groups = []; let cur = null; for (const seg of segments) { const spk = seg.speaker || 'UNKNOWN'; if (!cur || cur.speaker !== spk) { cur = {speaker: spk, segs: [seg]}; groups.push(cur); } else cur.segs.push(seg); } let html = ''; for (const g of groups) { const c = speakerColor(g.speaker); const text = g.segs.map(s => (s.text||'').trim()).join(' '); const t0 = g.segs[0].start, t1 = g.segs[g.segs.length-1].end; html += `
${esc(g.speaker)} ${fmtTime(t0)} – ${fmtTime(t1)}
${esc(text)}
`; } container.innerHTML = html; sectionEl.style.display = ''; } function downloadBlob(name, text, type) { const blob = new Blob([text], {type}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } // ─── Collapsible toggles ────────────────────────────────────────────────────── function makeToggle(btnId, panelId, chevronId) { const btn = document.getElementById(btnId); const panel = document.getElementById(panelId); const chevron = document.getElementById(chevronId); if (!btn || !panel) return; btn.addEventListener('click', () => { const open = panel.style.display !== 'none'; panel.style.display = open ? 'none' : ''; if (chevron) chevron.style.transform = open ? '' : 'rotate(90deg)'; }); } // ─── Nestable tab groups (top-level ASR/TTS switch + each domain's sub-tabs) ─── // // Each `.tab-group` owns one `.tabs` row of `.tab-btn` and a set of sibling // `.tab-panel` elements. Scoping with `:scope >` means groups can nest freely // (e.g. the top ASR/TTS switch contains a `.tab-group` per domain) without // their click handlers interfering with each other. function initTabGroups() { document.querySelectorAll('.tab-group').forEach(group => { const buttons = group.querySelectorAll(':scope > .tabs > .tab-btn'); const panels = group.querySelectorAll(':scope > .tab-panel'); buttons.forEach(btn => { btn.addEventListener('click', () => { buttons.forEach(b => b.classList.remove('active')); panels.forEach(p => p.style.display = 'none'); btn.classList.add('active'); const target = document.getElementById(btn.dataset.target); if (target) target.style.display = ''; }); }); }); } initTabGroups();