diff --git a/assets/css/tools.css b/assets/css/tools.css index 1abf911..2a8ea7a 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -3232,6 +3232,15 @@ a.dr-source-title-link:hover { color: #fff; } +.search-cats { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--line); +} + /* ── Search results ───────────────────────────────────────────────────── */ .corpus-search-results { margin: 0 0 32px; @@ -3318,6 +3327,33 @@ a.dr-source-title-link:hover { padding: 0 1px; } +.passage-expand-btn { + display: inline-block; + margin-top: 8px; + padding: 2px 10px; + border: 1px solid var(--line); + border-radius: 999px; + background: transparent; + color: var(--muted); + font-size: 0.75rem; + cursor: pointer; + transition: border-color 0.12s, color 0.12s; +} +.passage-expand-btn:hover { border-color: var(--teal); color: var(--teal); } + +.passage-full-text { + margin-top: 10px; + padding: 10px 14px; + background: var(--bg); + border: 1px solid var(--line); + border-radius: 6px; + font-size: 0.8rem; + color: var(--ink); + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + /* ── Category card browse button ──────────────────────────────────────── */ .cat-browse-btn { display: inline-block; @@ -3373,6 +3409,56 @@ a.dr-source-title-link:hover { } .drill-close-btn:hover { border-color: var(--teal); color: var(--teal); } +.drill-controls { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--line); +} + +.drill-count { + font-size: 0.78rem; + color: var(--muted); +} + +.drill-controls-right { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} + +.drill-search-input { + height: 30px; + padding: 0 10px; + border: 1px solid var(--line); + border-radius: 6px; + font-size: 0.8rem; + background: var(--panel); + color: var(--ink); + outline: none; + width: 160px; + transition: border-color 0.15s; +} +.drill-search-input:focus { border-color: var(--teal); } + +.drill-sort-select { + height: 30px; + padding: 0 8px; + border: 1px solid var(--line); + border-radius: 6px; + font-size: 0.8rem; + background: var(--panel); + color: var(--ink); + cursor: pointer; + outline: none; +} +.drill-sort-select:focus { border-color: var(--teal); } + .drill-loading, .drill-empty, .drill-error { @@ -3515,6 +3601,8 @@ a.dr-source-title-link:hover { @media (max-width: 760px) { .source-expand-grid { grid-template-columns: 1fr; } .corpus-search-controls { flex-direction: column; align-items: flex-start; } + .drill-controls { flex-direction: column; align-items: flex-start; } + .drill-search-input { width: 100%; } } /* ===================================================================== @@ -5574,3 +5662,123 @@ body.lt-landing { .lt-auth-nav__email { display: none; } .lt-hero__auth-cta { gap: 8px; } } + +/* ── Transcription UX improvements ──────────────────────────────────────── */ + +/* Vocab footer: hint + char counter side by side */ +.vocab-footer { + display: flex; + justify-content: space-between; + align-items: baseline; + flex-wrap: wrap; + gap: 0.25rem; +} +.vocab-char-count { margin: 0; white-space: nowrap; } +.vocab-char-count--warn { color: #b45309; font-weight: 600; } + +/* Progress bar shown while transcribing */ +.transcribe-progress-wrap { + padding: 2.5rem 1.5rem; + text-align: center; +} +.transcribe-progress-track { + height: 6px; + background: var(--line); + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.75rem; +} +.transcribe-progress-fill { + height: 100%; + background: var(--teal); + border-radius: 3px; + transition: width 0.4s ease; +} +.transcribe-progress-fill.is-indeterminate { + width: 40%; + animation: progress-slide 1.4s ease-in-out infinite; +} +@keyframes progress-slide { + 0% { transform: translateX(-150%); } + 100% { transform: translateX(400%); } +} +.transcribe-progress-label { + font-size: 0.85rem; + color: var(--muted); + margin: 0; +} + +/* Transcript meta stats row */ +.transcript-meta-row { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 1rem; + font-size: 0.8rem; + color: var(--muted); + margin-bottom: 0.75rem; +} +.transcript-meta-row span { + display: inline-flex; + align-items: center; + gap: 0.3rem; +} + +/* AI cleanup badge */ +.cleanup-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-left: 0.6rem; + font-size: 0.72rem; + font-weight: 600; + color: #166534; + background: #dcfce7; + padding: 0.1rem 0.45rem; + border-radius: 4px; + vertical-align: middle; +} + +/* Action row above transcript (copy + download txt) */ +.transcript-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +/* Segment legend (speaker role key inside the segments panel) */ +.segment-legend { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + padding: 0.45rem 0.75rem; + border-bottom: 1px solid var(--line); + background: var(--bg); +} + +/* === Advocate UX additions 2026-05-18 === */ +.adv-input-footer { display:flex; justify-content:flex-end; margin-top:4px; } +.adv-char-count { font-size:.78rem; color:var(--muted); } +.adv-char-count.is-warn { color:var(--amber); } +.adv-char-count.is-crit { color:var(--coral); font-weight:600; } +.dr-section-head { display:flex; align-items:baseline; gap:12px; margin-bottom:10px; } +.dr-section-head h3 { margin:0; font-size:1rem; } +.dr-copy-btn { font-size:.78rem; padding:2px 10px; border:1px solid var(--line); border-radius:4px; background:var(--panel); color:var(--teal); cursor:pointer; } +.dr-copy-btn:hover { background:var(--soft-teal); } +.adv-result-actions { display:flex; justify-content:flex-end; margin-bottom:16px; } +.adv-new-query-btn { font-size:.82rem; padding:4px 14px; border:1px solid var(--teal); border-radius:4px; background:var(--soft-teal); color:var(--teal-dark); cursor:pointer; } +.adv-restore-banner { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 14px; border-radius:6px; background:var(--soft-teal); border:1px solid var(--teal); margin-bottom:12px; font-size:.85rem; flex-wrap:wrap; } +.adv-restore-banner__text em { font-style:normal; color:var(--muted); } +.adv-restore-banner__actions { display:flex; gap:8px; } +.adv-restore-banner__actions button { font-size:.82rem; padding:3px 12px; border-radius:4px; cursor:pointer; } +.adv-restore-banner__actions button:first-child { background:var(--teal); color:#fff; border:none; } +.adv-restore-banner__actions button:last-child { background:transparent; border:1px solid var(--line); color:var(--muted); } +.dr-collapsible > summary { list-style:none; cursor:pointer; display:flex; align-items:baseline; gap:12px; padding:8px 0; } +.dr-collapsible > summary::-webkit-details-marker { display:none; } +.dr-collapsible > summary h3 { margin:0; font-size:.95rem; } +.dr-collapsible > summary::before { content:'▸'; font-size:.7rem; color:var(--muted); margin-right:4px; } +.dr-collapsible[open] > summary::before { content:'▾'; } +.adv-slice-hint { margin:8px 0 0; font-size:.83rem; color:var(--ink); background:var(--soft-teal); border:1px solid var(--teal); border-radius:6px; padding:8px 12px; display:flex; gap:10px; align-items:center; flex-wrap:wrap; } +.adv-slice-hint button { font-size:.8rem; padding:2px 10px; border-radius:4px; cursor:pointer; } +#advSliceHintEnable { background:var(--teal); color:#fff; border:none; } +#advSliceHintDismiss { background:transparent; border:1px solid var(--line); color:var(--muted); } diff --git a/assets/js/tools.js b/assets/js/tools.js index 88e7db7..a1c04bd 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -381,8 +381,10 @@ let lastTimelineEvents = []; let lastTimelineEventsOriginal = []; let audioQueue = []; // [{file, status: 'pending'|'processing'|'done'|'error', result}] let lastTranscriptData = null; -let lastRedactedText = null; -let lastRunEngine = null; +let lastRedactedText = null; +let lastOriginalText = ''; +let lastRedactPayload = null; +let lastRunEngine = null; const VOCAB_PRESETS = { barnerett: 'Barnevernet, Fylkesnemnda, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, sakkyndig, advokat, prosessfullmektig, dommer, vitne, tolk, bistandsadvokat, fosterforeldre, fosterhjem, akuttvedtak, statsforvalter, Bufetat, saksbehandler, rettslig medhold, begjæring, samtykke, tilsynsfører', @@ -933,11 +935,12 @@ document.addEventListener('DOMContentLoaded', () => { } els.results?.addEventListener('click', (e) => { if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents); - if (e.target.closest('#dlTxt')) downloadTranscriptTxt(); - if (e.target.closest('#dlSrt')) downloadTranscriptSrt(); - if (e.target.closest('#dlVtt')) downloadTranscriptVtt(); + if (e.target.closest('#txCopy')) copyTranscriptText(); + if (e.target.closest('#dlTxt')) downloadTranscriptTxt(); + if (e.target.closest('#dlSrt')) downloadTranscriptSrt(); + if (e.target.closest('#dlVtt')) downloadTranscriptVtt(); if (e.target.closest('#rdlCopy')) copyRedactedText(); - if (e.target.closest('#rdlTxt')) downloadRedactedTxt(); + if (e.target.closest('#rdlTxt')) downloadRedactedTxt(); if (e.target.closest('#rdlDocx')) downloadRedactedDocx(); }); const activeTool = document.body.dataset.activeTool || state.activeTool; @@ -1043,14 +1046,16 @@ async function runTool(event) { payload.limit = 7; } if (state.activeTool === 'redact') { - payload.mode = currentRedactionMode(); - payload.region = currentRedactionRegion(); - payload.aliases = getAliases(); - payload.engine = currentRedactEngine(); - payload.output_format = currentOutputFormat(); + lastOriginalText = text; + payload.mode = currentRedactionMode(); + payload.region = currentRedactionRegion(); + payload.aliases = getAliases(); + payload.engine = currentRedactEngine(); + payload.output_format = currentOutputFormat(); payload.keep_officials = currentKeepOfficials(); - payload.exempt_names = getExemptNames(); - payload.redact_types = currentRedactTypes(); + payload.exempt_names = getExemptNames(); + payload.redact_types = currentRedactTypes(); + lastRedactPayload = { ...payload }; } if (state.activeTool === 'timeline') { payload.engine = currentTimelineEngine(); @@ -1330,6 +1335,105 @@ function renderResults(data) { } } +// ── Redact: tag colour helpers ───────────────────────────────────────────── + +function tagColorClass(root) { + if (/^(FATHER|MOTHER|CHILD|GRANDPARENT|SIBLING|ATTORNEY|JUDGE|CASEWORKER|EXPERT_WITNESS|PERSON)/.test(root)) return 'person'; + if (root === 'ORG') return 'org'; + if (root === 'PLACE') return 'place'; + if (/^(DATE|DOB)/.test(root)) return 'date'; + return 'id'; +} + +function highlightRedactedText(text) { + const escaped = escapeHtml(text); + return escaped.replace(/\[([A-Z][A-Z0-9_-]*)(?::\s*([^\]]+))?\]/g, (_m, root, suffix) => { + const cls = tagColorClass(root); + const display = suffix ? `[${root}: ${suffix}]` : `[${root}]`; + return `${display}`; + }); +} + +function renderRedactionInventory(redactionMap, entityCounts) { + const map = redactionMap || {}; + const counts = entityCounts || {}; + const entries = []; + + for (const [tag, info] of Object.entries(map)) { + const root = tag.replace(/^\[|\]$/g, '').split(':')[0].trim(); + entries.push({ tag, originals: info.originals || [], occurrences: info.occurrences || 0, cls: tagColorClass(root) }); + } + + const mappedTypes = new Set(Object.values(map).map(e => e.type)); + for (const [type, count] of Object.entries(counts)) { + if (Number(count) > 0 && !mappedTypes.has(type)) { + entries.push({ tag: type, originals: [], occurrences: Number(count), cls: tagColorClass(type.toUpperCase()) }); + } + } + + if (!entries.length) return ''; + + const rows = entries.map(e => { + const tagSpan = `${escapeHtml(e.tag)}`; + const originalsHtml = e.originals.length + ? `→ ${e.originals.map(o => `${escapeHtml(o)}`).join(', ')} ` + : ''; + const countHtml = e.occurrences > 0 ? `${e.occurrences}×` : ''; + return `
${escapeHtml(data.answer || data.what_we_found || '')}
`; @@ -1337,12 +1441,26 @@ function renderMainFinding(data) { if (data.tool === 'redact') { lastRedactedText = data.redacted_text || ''; const t = (k) => currentRedactT(k) || k; + + const viewToggle = `${escapeHtml(lastRedactedText)}${renderEntityCounts(data.entity_counts)}${dlRow}`;
+
+ return `${viewToggle}${highlightRedactedText(lastRedactedText)}${inventoryHtml}${upgradeBtn}${dlRow}`;
}
if (data.tool === 'timeline') {
lastTimelineEventsOriginal = data.events || [];
@@ -1520,6 +1638,36 @@ function currentTask() {
return el ? el.value : 'transcribe';
}
+function buildTranscribeError(err, file) {
+ const name = file?.name ?? 'file';
+ const sizeMB = file?.size ? (file.size / 1024 / 1024).toFixed(1) : null;
+ let reason = err?.message || 'Unknown error';
+ if (/too large|size/i.test(reason) || reason.includes('413'))
+ reason = `File too large${sizeMB ? ` (${sizeMB} MB)` : ''}`;
+ else if (/format|unsupported|415/i.test(reason))
+ reason = 'Unsupported audio format';
+ else if (/timeout|timed out|504/i.test(reason))
+ reason = 'Request timed out — try a shorter clip';
+ else if (/50[0-9]/.test(reason))
+ reason = 'Server error';
+ return `${name}: ${reason}`;
+}
+
+function showTranscribeProgress(clip, total) {
+ if (!els.results) return;
+ const pct = total > 1 ? Math.round(((clip - 1) / total) * 100) : null;
+ const label = total > 1 ? `Clip ${clip} / ${total}` : 'Transcribing…';
+ const barClass = pct !== null ? '' : ' is-indeterminate';
+ const barStyle = pct !== null ? ` style="width:${pct}%"` : '';
+ els.results.innerHTML = `
+ ${escapeHtml(label)}
+${speakerOrder.map((id, i) => { const role = speakerRoles[id] || id; @@ -1669,8 +1834,17 @@ function renderTranscriptResults(data) { }).join('')}
` : ''; + // Segments panel — includes inline speaker legend at the top + const legendHtml = speakerOrder.length + ? `Transcribed with ${escapeHtml(data.model)}${cleanupBadge}
` + : ''; + + // Action row above transcript (copy + TXT download) + const actionRow = ` +Transcribed with ${escapeHtml(data.model)}
` : ''} + ${engineLine} + ${metaRow} ${rolesHtml} + ${actionRow}${escapeHtml(data.transcript)}Helps Whisper recognise technical terms. Not included in the transcript.
+