From f80703982fc7622e40609170a100507b7a6836f5 Mon Sep 17 00:00:00 2001 From: Julien Liabeuf Date: Fri, 29 May 2026 18:48:39 +0700 Subject: [PATCH 1/4] [#74] Add implementation plan for unnamed speakers warning Plan for surfacing a warning on the Analysis tab when one or more speakers still carry raw diarization labels. --- .../74-warn-unnamed-speakers-on-analysis.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/plans/74-warn-unnamed-speakers-on-analysis.md diff --git a/docs/plans/74-warn-unnamed-speakers-on-analysis.md b/docs/plans/74-warn-unnamed-speakers-on-analysis.md new file mode 100644 index 0000000..340a876 --- /dev/null +++ b/docs/plans/74-warn-unnamed-speakers-on-analysis.md @@ -0,0 +1,49 @@ +# Plan: Warn on Analysis tab when speakers are unnamed + +**Story**: #74 +**Spec**: N/A +**Branch**: feature/74-warn-unnamed-speakers-on-analysis +**Date**: 2026-05-29 +**Mode**: Standard — small vanilla JS/CSS change; project has no frontend test framework, no need for TDD scaffolding. + +## Technical Decisions + +### TD-1: Reuse `.overview-notice` CSS class instead of introducing `.analysis-warning` +- **Context**: The Analysis tab needs a visual banner; `.overview-notice` already provides the exact look (warning-colored left border, surface background, muted text) used by the Overview tab. +- **Decision**: Reuse `.overview-notice` directly on the Analysis tab banner. +- **Alternatives considered**: A parallel `.analysis-warning` class — rejected to avoid visual drift between two identical banners (Architect Minor finding). + +### TD-2: Read unnamed-speaker info from `window._speakerEditorState` +- **Context**: The transcript viewer already computes `speakers` and `speakerIds` and stashes them on a global state bag. Re-deriving from the meeting object would duplicate logic. +- **Decision**: Read `window._speakerEditorState` in `analysis-viewer.js` with an inline comment documenting the dependency on `transcript-viewer.renderSegments()`. +- **Alternatives considered**: Re-fetching from a `currentMeeting` global — none exists; would require a new global. + +### TD-3: Show the warning both before and after Generate Prompt +- **Context**: After generation the prompt UI replaces the generator UI. Hiding the warning at that point would let users copy the prompt without seeing that it still contains raw `SPEAKER_xx` labels. +- **Decision**: Render the warning in both `renderAnalysisTab` (initial) and `renderPromptContent` (post-generate). +- **Alternatives considered**: Hide post-generate — rejected; the warning is more useful precisely when the user is about to copy the prompt. + +## Files to Create or Modify + +- `frontend/js/components/analysis-viewer.js` — add helpers `getUnnamedSpeakersInfo()` and `renderUnnamedSpeakersWarning()`; prepend the banner in both render functions. +- `frontend/css/styles.css` — no new class; if a tab-context margin override is needed, add a one-liner. + +## Approach per AC + +### AC: Warning visible on the Analysis tab when speakers are not labeled +- On Analysis tab render, count speakers in `window._speakerEditorState.speakerIds` where `isUnidentifiedSpeaker(state.speakers[id] || id)` is true. +- If `unnamed > 0`, render an `.overview-notice` banner at the top with a sentence explaining the impact and pointing users to the Transcript tab. +- If `unnamed === 0` or state is missing, render nothing. + +## Commit Sequence + +1. `[#74] Warn on analysis tab when speakers are unnamed` + +## Risks and Trade-offs + +- Cross-component read of `window._speakerEditorState` couples `analysis-viewer.js` to `transcript-viewer.js` lifecycle. Mitigated by an inline comment so a future refactor flags the dependency. +- Persistent warning after Generate Prompt is intentional (see TD-3). Worth a note in the PR description so reviewers don't read it as accidental duplication. + +## Deviations from Plan + +_Populated after implementation._ From d75cb0045cf5253654595ff98f7a6788ba9c4bcf Mon Sep 17 00:00:00 2001 From: Julien Liabeuf Date: Fri, 29 May 2026 18:52:09 +0700 Subject: [PATCH 2/4] [#74] Warn on analysis tab when speakers are unnamed Surface a notice at the top of the Analysis tab listing how many speakers still carry raw diarization labels (SPEAKER_xx / UNKNOWN). The notice persists after Generate Prompt so users who copy the generated prompt are reminded that it contains raw labels until they rename speakers on the Transcript tab. --- frontend/css/styles.css | 11 +++++++++ frontend/js/components/analysis-viewer.js | 28 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 614cc97..60d763b 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1240,6 +1240,17 @@ body { } /* Analysis */ +.analysis-warning { + margin-bottom: 16px; +} + +.analysis-warning code { + background: var(--bg); + padding: 1px 6px; + border-radius: 3px; + font-size: 12px; +} + .analysis-generator { text-align: center; padding: 40px 20px; diff --git a/frontend/js/components/analysis-viewer.js b/frontend/js/components/analysis-viewer.js index d5d8071..a296152 100644 --- a/frontend/js/components/analysis-viewer.js +++ b/frontend/js/components/analysis-viewer.js @@ -1,6 +1,7 @@ function renderAnalysisTab(container, meetingId, meetingType) { if (meetingId) container.dataset.meetingId = meetingId; container.innerHTML = ` + ${renderUnnamedSpeakersWarning()}

Select a template to generate a prompt you can paste into any LLM for analysis.

@@ -84,6 +85,7 @@ function buildPlainTextTranscript() { function renderPromptContent(container, prompt) { container.innerHTML = ` + ${renderUnnamedSpeakersWarning()}
@@ -95,6 +97,32 @@ function renderPromptContent(container, prompt) { container.dataset.rawPrompt = prompt; } +// Reads `window._speakerEditorState`, which is populated by +// `renderSegments()` in transcript-viewer.js when the meeting loads. +// If transcript-viewer.js changes that contract, update this read site. +function getUnnamedSpeakersInfo() { + const state = window._speakerEditorState; + if (!state || !state.speakerIds || !state.speakers) return null; + const total = state.speakerIds.length; + const unnamed = state.speakerIds.filter(id => + isUnidentifiedSpeaker(state.speakers[id] || id) + ).length; + return { unnamed, total }; +} + +function renderUnnamedSpeakersWarning() { + const info = getUnnamedSpeakersInfo(); + if (!info || info.unnamed === 0) return ''; + const { unnamed, total } = info; + const verb = unnamed === 1 ? 'is' : 'are'; + const noun = unnamed === 1 ? 'speaker' : 'speakers'; + return ` +
+ ${unnamed} of ${total} ${noun} ${verb} still unnamed. Rename them on the Transcript tab — the generated prompt will use raw labels like SPEAKER_00 until you do. +
+ `; +} + function copyPrompt() { const container = document.getElementById('analysis-tab'); const raw = container.dataset.rawPrompt || ''; From 389d3339e79f8edc81dac00dbb7e19b59e44032d Mon Sep 17 00:00:00 2001 From: Julien Liabeuf Date: Fri, 29 May 2026 19:03:26 +0700 Subject: [PATCH 3/4] [#74] Strengthen unnamed-speakers warning visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the subtle .overview-notice borrow with a dedicated, higher-contrast banner: warning-tinted background, full warning-colored border (1px + 4px left accent), full-strength text color (was muted), 14px body with a bold lead-in count, and a ⚠ icon. Role upgraded from status to alert. Works in both themes via existing CSS tokens. --- frontend/css/styles.css | 31 ++++++++++++++++++++++- frontend/js/components/analysis-viewer.js | 8 ++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 60d763b..884c5ae 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1241,7 +1241,35 @@ body { /* Analysis */ .analysis-warning { - margin-bottom: 16px; + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 20px; + padding: 14px 18px; + background: color-mix(in srgb, var(--warning) 18%, var(--bg-surface)); + border: 1px solid var(--warning); + border-left: 4px solid var(--warning); + border-radius: var(--radius-sm); + color: var(--text); + font-size: 14px; + line-height: 1.5; +} + +.analysis-warning-icon { + flex: 0 0 auto; + font-size: 20px; + line-height: 1.2; + color: var(--warning); +} + +.analysis-warning-body { + flex: 1 1 auto; +} + +.analysis-warning-body strong { + display: block; + margin-bottom: 2px; + color: var(--text); } .analysis-warning code { @@ -1249,6 +1277,7 @@ body { padding: 1px 6px; border-radius: 3px; font-size: 12px; + border: 1px solid var(--border); } .analysis-generator { diff --git a/frontend/js/components/analysis-viewer.js b/frontend/js/components/analysis-viewer.js index a296152..822bf9c 100644 --- a/frontend/js/components/analysis-viewer.js +++ b/frontend/js/components/analysis-viewer.js @@ -117,8 +117,12 @@ function renderUnnamedSpeakersWarning() { const verb = unnamed === 1 ? 'is' : 'are'; const noun = unnamed === 1 ? 'speaker' : 'speakers'; return ` -
- ${unnamed} of ${total} ${noun} ${verb} still unnamed. Rename them on the Transcript tab — the generated prompt will use raw labels like SPEAKER_00 until you do. + `; } From 75b9061a45723b825f98613641e0e9a3e22434fc Mon Sep 17 00:00:00 2001 From: Julien Liabeuf Date: Fri, 29 May 2026 19:08:25 +0700 Subject: [PATCH 4/4] [#74] Show unnamed-speakers warning on plain-text tab Move getUnnamedSpeakersInfo/renderUnnamedSpeakersWarning into utils.js so both the Analysis and Plain Text tabs can render the same warning. Rename CSS classes from analysis-warning to unnamed-speakers-warning to match shared usage. Copy generalized from 'generated prompt' to 'copied text' so it reads correctly for plain text exports too. --- frontend/css/styles.css | 12 ++++----- frontend/js/components/analysis-viewer.js | 30 --------------------- frontend/js/components/transcript-viewer.js | 1 + frontend/js/utils.js | 30 +++++++++++++++++++++ 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 884c5ae..2cb2cfa 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1239,8 +1239,8 @@ body { color: var(--text-muted); } -/* Analysis */ -.analysis-warning { +/* Unnamed-speakers warning (shared by Plain Text + Analysis tabs) */ +.unnamed-speakers-warning { display: flex; align-items: flex-start; gap: 12px; @@ -1255,24 +1255,24 @@ body { line-height: 1.5; } -.analysis-warning-icon { +.unnamed-speakers-warning-icon { flex: 0 0 auto; font-size: 20px; line-height: 1.2; color: var(--warning); } -.analysis-warning-body { +.unnamed-speakers-warning-body { flex: 1 1 auto; } -.analysis-warning-body strong { +.unnamed-speakers-warning-body strong { display: block; margin-bottom: 2px; color: var(--text); } -.analysis-warning code { +.unnamed-speakers-warning code { background: var(--bg); padding: 1px 6px; border-radius: 3px; diff --git a/frontend/js/components/analysis-viewer.js b/frontend/js/components/analysis-viewer.js index 822bf9c..f22548a 100644 --- a/frontend/js/components/analysis-viewer.js +++ b/frontend/js/components/analysis-viewer.js @@ -97,36 +97,6 @@ function renderPromptContent(container, prompt) { container.dataset.rawPrompt = prompt; } -// Reads `window._speakerEditorState`, which is populated by -// `renderSegments()` in transcript-viewer.js when the meeting loads. -// If transcript-viewer.js changes that contract, update this read site. -function getUnnamedSpeakersInfo() { - const state = window._speakerEditorState; - if (!state || !state.speakerIds || !state.speakers) return null; - const total = state.speakerIds.length; - const unnamed = state.speakerIds.filter(id => - isUnidentifiedSpeaker(state.speakers[id] || id) - ).length; - return { unnamed, total }; -} - -function renderUnnamedSpeakersWarning() { - const info = getUnnamedSpeakersInfo(); - if (!info || info.unnamed === 0) return ''; - const { unnamed, total } = info; - const verb = unnamed === 1 ? 'is' : 'are'; - const noun = unnamed === 1 ? 'speaker' : 'speakers'; - return ` - - `; -} - function copyPrompt() { const container = document.getElementById('analysis-tab'); const raw = container.dataset.rawPrompt || ''; diff --git a/frontend/js/components/transcript-viewer.js b/frontend/js/components/transcript-viewer.js index fb4753f..c7dc629 100644 --- a/frontend/js/components/transcript-viewer.js +++ b/frontend/js/components/transcript-viewer.js @@ -478,6 +478,7 @@ function renderPlainTextTab(container) { const plainText = lines.join('\n'); container.innerHTML = ` + ${renderUnnamedSpeakersWarning()}
diff --git a/frontend/js/utils.js b/frontend/js/utils.js index 16c25c5..e6dd051 100644 --- a/frontend/js/utils.js +++ b/frontend/js/utils.js @@ -46,6 +46,36 @@ function isUnidentifiedSpeaker(name) { return name === 'UNKNOWN' || /^SPEAKER_\d+$/.test(name); } +// Reads `window._speakerEditorState`, populated by `renderSegments()` in +// transcript-viewer.js when the meeting loads. If that contract changes, +// update this read site. +function getUnnamedSpeakersInfo() { + const state = window._speakerEditorState; + if (!state || !state.speakerIds || !state.speakers) return null; + const total = state.speakerIds.length; + const unnamed = state.speakerIds.filter(id => + isUnidentifiedSpeaker(state.speakers[id] || id) + ).length; + return { unnamed, total }; +} + +function renderUnnamedSpeakersWarning() { + const info = getUnnamedSpeakersInfo(); + if (!info || info.unnamed === 0) return ''; + const { unnamed, total } = info; + const verb = unnamed === 1 ? 'is' : 'are'; + const noun = unnamed === 1 ? 'speaker' : 'speakers'; + return ` + + `; +} + function getRecentSpeakerNames() { try { return JSON.parse(localStorage.getItem('recentSpeakerNames') || '[]');