Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/plans/74-warn-unnamed-speakers-on-analysis.md
Original file line number Diff line number Diff line change
@@ -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._
42 changes: 41 additions & 1 deletion frontend/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1239,7 +1239,47 @@ body {
color: var(--text-muted);
}

/* Analysis */
/* Unnamed-speakers warning (shared by Plain Text + Analysis tabs) */
.unnamed-speakers-warning {
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;
}

.unnamed-speakers-warning-icon {
flex: 0 0 auto;
font-size: 20px;
line-height: 1.2;
color: var(--warning);
}

.unnamed-speakers-warning-body {
flex: 1 1 auto;
}

.unnamed-speakers-warning-body strong {
display: block;
margin-bottom: 2px;
color: var(--text);
}

.unnamed-speakers-warning code {
background: var(--bg);
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
border: 1px solid var(--border);
}

.analysis-generator {
text-align: center;
padding: 40px 20px;
Expand Down
2 changes: 2 additions & 0 deletions frontend/js/components/analysis-viewer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
function renderAnalysisTab(container, meetingId, meetingType) {
if (meetingId) container.dataset.meetingId = meetingId;
container.innerHTML = `
${renderUnnamedSpeakersWarning()}
<div class="analysis-generator">
<p>Select a template to generate a prompt you can paste into any LLM for analysis.</p>
<div class="form-group">
Expand Down Expand Up @@ -84,6 +85,7 @@ function buildPlainTextTranscript() {

function renderPromptContent(container, prompt) {
container.innerHTML = `
${renderUnnamedSpeakersWarning()}
<div class="analysis-content">
<div class="analysis-actions">
<button class="btn btn-primary" onclick="copyPrompt()">Copy to clipboard</button>
Expand Down
1 change: 1 addition & 0 deletions frontend/js/components/transcript-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ function renderPlainTextTab(container) {
const plainText = lines.join('\n');

container.innerHTML = `
${renderUnnamedSpeakersWarning()}
<div class="plaintext-view">
<div class="plaintext-actions">
<button class="btn btn-primary" onclick="copyPlainText()">Copy to clipboard</button>
Expand Down
30 changes: 30 additions & 0 deletions frontend/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `
<div class="unnamed-speakers-warning" role="alert">
<span class="unnamed-speakers-warning-icon" aria-hidden="true">⚠</span>
<div class="unnamed-speakers-warning-body">
<strong>${unnamed} of ${total} ${noun} ${verb} still unnamed.</strong>
Rename them on the Transcript tab — copied text will use raw labels like <code>SPEAKER_00</code> until you do.
</div>
</div>
`;
}

function getRecentSpeakerNames() {
try {
return JSON.parse(localStorage.getItem('recentSpeakerNames') || '[]');
Expand Down
Loading