# Sprint 13: Simulated Transcription Toggle

> **Priority**: 7 | **Owner**: Client + Tauri | **Complexity**: Low-Medium
> **Prerequisite**: Sprint 12

---

## Validation Status (2025-12-29)

### ✅ IMPLEMENTATION COMPLETE

**All Sprint 13 components are fully implemented and tested.**

| Component | Location | Status |
|-----------|----------|--------|
| Settings toggle | `client/src/components/settings/developer-options-section.tsx` | ✅ UI exists and persists |
| Preference storage | `client/src/lib/preferences.ts:78,278-282` | ✅ `simulate_transcription` stored |
| Mock transcription stream | `client/src/api/mock-transcription-stream.ts` | ✅ Full implementation with VAD simulation |
| Control logic | `client/src/pages/Recording.tsx:165,177-180,207,218-221` | ✅ Preference check + stream selection |
| Simulation indicator | `client/src/components/simulation-indicator.tsx` | ✅ Badge + compact variants with tooltips |
| Simulation indicator tests | `client/src/components/simulation-indicator.test.tsx` | ✅ Rendering tests |
| Recording page tests | `client/src/pages/Recording.test.tsx` | ✅ Simulation mode tests |

**Implementation Details**:
- Recording.tsx checks `preferences.get().simulate_transcription` before starting
- Uses `MockTranscriptionStream` when simulation is enabled, bypassing Tauri/backend
- `SimulationIndicator` component displayed inline during simulated recording
- Simulated data lives in `mock-transcription-stream.ts` (meeting phrases with random VAD)
- Browser-mode (non-Tauri) blocked unless simulation is enabled

---

## Objective

Make the "Simulate Transcription" toggle functional for development and demo purposes, with clear visual indication when simulation is active.

---

## Current State Analysis

### Settings Toggle

**UI** (`pages/Settings.tsx`):
- Toggle switch labeled "Simulate Transcription"
- Writes to `preferences.simulateTranscription`
- No functional impact on recording flow

**Preference** (`lib/preferences.ts`):
```typescript
interface Preferences {
  // ...
  simulateTranscription: boolean;
  // ...
}
```

### Recording Flow

**Current Behavior**:
1. User clicks "Start Recording"
2. Tauri captures real audio
3. Audio sent to backend for ASR
4. Transcript segments returned

**Missing**:
- No code path checks `simulateTranscription` preference
- No simulated transcript generator wired into Tauri/recording flow (only mock adapter has samples)
- No way to demo without live audio/backend

---

## Scope

| Task | Effort | Risk |
|------|--------|------|
| Add simulated transcript generator | Low | Low |
| Wire toggle to recording flow | Low | Low |
| Add simulation indicator | Low | Low |
| Bypass audio capture in simulation | Medium | Low |

---

## Implementation Plan

### Task 1: Simulated Transcript Generator

**File**: `client/src/lib/simulated-transcripts.ts`

```typescript
// NOTE: Consider extracting existing sample transcript content from
// `client/src/api/mock-adapter.ts` into this module for reuse.
/**
 * Generates simulated transcript segments for demo/development.
 * Uses realistic meeting content with varied speakers and timing.
 */

export interface SimulatedSegment {
  id: string;
  text: string;
  speaker: string;
  startTime: number;
  endTime: number;
  confidence: number;
}

const SAMPLE_TRANSCRIPTS = [
  {
    speaker: "Alex",
    lines: [
      "Good morning everyone, thanks for joining.",
      "Let's start with a quick status update on the project.",
      "Sarah, would you like to go first?",
    ],
  },
  {
    speaker: "Sarah",
    lines: [
      "Sure. I finished the API integration yesterday.",
      "We're now connected to the payment provider.",
      "There are a few edge cases I'm still working through.",
    ],
  },
  {
    speaker: "Marcus",
    lines: [
      "That's great progress, Sarah.",
      "I've been working on the frontend components.",
      "The checkout flow is almost done, just need styling.",
    ],
  },
  {
    speaker: "Alex",
    lines: [
      "Excellent. Any blockers we should discuss?",
      "I want to make sure we're on track for the demo.",
    ],
  },
  {
    speaker: "Sarah",
    lines: [
      "One thing - I need test credentials for the sandbox.",
      "Can someone from DevOps help with that?",
    ],
  },
  {
    speaker: "Marcus",
    lines: [
      "I can help with that after this meeting.",
      "I set up the sandbox environment last week.",
    ],
  },
];

let segmentIndex = 0;
let lineIndex = 0;
let accumulatedTime = 0;

export function resetSimulation(): void {
  segmentIndex = 0;
  lineIndex = 0;
  accumulatedTime = 0;
}

export function getNextSegment(): SimulatedSegment | null {
  if (segmentIndex >= SAMPLE_TRANSCRIPTS.length) {
    // Loop back to start
    segmentIndex = 0;
    lineIndex = 0;
  }

  const current = SAMPLE_TRANSCRIPTS[segmentIndex];
  const line = current.lines[lineIndex];

  // Calculate realistic timing (40-80ms per character)
  const duration = line.length * (40 + Math.random() * 40) / 1000;
  const startTime = accumulatedTime;
  const endTime = startTime + duration;
  accumulatedTime = endTime + 0.5 + Math.random() * 0.5; // Gap between segments

  const segment: SimulatedSegment = {
    id: `sim-${Date.now()}-${segmentIndex}-${lineIndex}`,
    text: line,
    speaker: current.speaker,
    startTime,
    endTime,
    confidence: 0.85 + Math.random() * 0.15,
  };

  // Move to next line/speaker
  lineIndex++;
  if (lineIndex >= current.lines.length) {
    lineIndex = 0;
    segmentIndex++;
  }

  return segment;
}

export class SimulatedTranscriptStream {
  private intervalId: ReturnType<typeof setInterval> | null = null;
  private onSegment: (segment: SimulatedSegment) => void;
  private intervalMs: number;

  constructor(
    onSegment: (segment: SimulatedSegment) => void,
    intervalMs: number = 3000, // New segment every 3 seconds
  ) {
    this.onSegment = onSegment;
    this.intervalMs = intervalMs;
  }

  start(): void {
    resetSimulation();
    this.emitNext();
    this.intervalId = setInterval(() => this.emitNext(), this.intervalMs);
  }

  stop(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  private emitNext(): void {
    const segment = getNextSegment();
    if (segment) {
      this.onSegment(segment);
    }
  }
}
```

### Task 2: Wire Toggle to Recording Flow

**File**: `client/src/hooks/use-recording.ts`

```typescript
import { useCallback, useState, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { usePreferences } from './use-preferences';
import {
  SimulatedTranscriptStream,
  SimulatedSegment,
} from '../lib/simulated-transcripts';
import { useSegmentStore } from '../store/segment-store';

export function useRecording() {
  const [isRecording, setIsRecording] = useState(false);
  const [isSimulating, setIsSimulating] = useState(false);
  const { preferences } = usePreferences();
  const { addSegment } = useSegmentStore();
  const simulatorRef = useRef<SimulatedTranscriptStream | null>(null);

  const startRecording = useCallback(async (options?: { source?: string }) => {
    if (preferences.simulateTranscription) {
      // Use simulated transcription
      setIsRecording(true);
      setIsSimulating(true);

      simulatorRef.current = new SimulatedTranscriptStream((segment) => {
        addSegment({
          id: segment.id,
          text: segment.text,
          speaker: segment.speaker,
          startTime: segment.startTime,
          endTime: segment.endTime,
          confidence: segment.confidence,
          isPartial: false,
        });
      });

      simulatorRef.current.start();
    } else {
      // Real recording via Tauri
      setIsRecording(true);
      setIsSimulating(false);
      await invoke('start_recording', options);
    }
  }, [preferences.simulateTranscription, addSegment]);

  const stopRecording = useCallback(async () => {
    if (simulatorRef.current) {
      simulatorRef.current.stop();
      simulatorRef.current = null;
    }

    if (!preferences.simulateTranscription) {
      await invoke('stop_recording');
    }

    setIsRecording(false);
    setIsSimulating(false);
  }, [preferences.simulateTranscription]);

  return {
    isRecording,
    isSimulating,
    startRecording,
    stopRecording,
  };
}
```

### Task 3: Add Simulation Indicator

**File**: `client/src/components/simulation-indicator.tsx`

```typescript
import { useRecording } from '../hooks/use-recording';
import { FlaskConical } from 'lucide-react';

export function SimulationIndicator() {
  const { isSimulating } = useRecording();

  if (!isSimulating) return null;

  return (
    <div
      role="status"
      aria-label="Simulation mode active"
      className="fixed bottom-4 right-4 z-50 flex items-center gap-2 px-3 py-2
                 bg-purple-600 text-white rounded-full shadow-lg animate-pulse"
    >
      <FlaskConical className="w-4 h-4" />
      <span className="text-sm font-medium">Simulating</span>
    </div>
  );
}
```

**Add to App Layout** (`App.tsx`):
```tsx
import { SimulationIndicator } from './components/simulation-indicator';

export function App() {
  return (
    <div className="app">
      {/* ... existing content ... */}
      <SimulationIndicator />
    </div>
  );
}
```

### Task 4: Settings Toggle Enhancement

**File**: `client/src/pages/Settings.tsx` (update)

```typescript
// Update the simulate toggle section
<div className="space-y-2">
  <div className="flex items-center justify-between">
    <Label htmlFor="simulate-transcription">
      Simulate Transcription
      <span className="ml-2 text-xs text-muted-foreground">
        (Development/Demo mode)
      </span>
    </Label>
    <Switch
      id="simulate-transcription"
      checked={preferences.simulateTranscription}
      onCheckedChange={(checked) => {
        setPreferences({ simulateTranscription: checked });
        if (checked) {
          toast.info('Simulation enabled. Recordings will use demo transcripts.');
        }
      }}
    />
  </div>
  <p className="text-xs text-muted-foreground">
    When enabled, recordings use pre-recorded demo transcripts instead of real
    audio capture. Useful for testing the UI without a microphone.
  </p>
</div>
```

---

## Behavioral Test Specifications

### Simulated Transcript Generator Tests

**File**: `client/src/lib/simulated-transcripts.test.ts`

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import {
  getNextSegment,
  resetSimulation,
  SimulatedTranscriptStream,
} from './simulated-transcripts';

describe('simulated-transcripts', () => {
  beforeEach(() => {
    resetSimulation();
  });

  describe('getNextSegment', () => {
    it('returns segment with required fields', () => {
      const segment = getNextSegment();

      expect(segment).not.toBeNull();
      expect(segment!.id).toBeTruthy();
      expect(segment!.text).toBeTruthy();
      expect(segment!.speaker).toBeTruthy();
      expect(segment!.startTime).toBeGreaterThanOrEqual(0);
      expect(segment!.endTime).toBeGreaterThan(segment!.startTime);
      expect(segment!.confidence).toBeGreaterThan(0);
      expect(segment!.confidence).toBeLessThanOrEqual(1);
    });

    it('cycles through multiple speakers', () => {
      const speakers = new Set<string>();

      for (let i = 0; i < 20; i++) {
        const segment = getNextSegment();
        if (segment) {
          speakers.add(segment.speaker);
        }
      }

      expect(speakers.size).toBeGreaterThan(1);
    });

    it('generates unique IDs', () => {
      const ids = new Set<string>();

      for (let i = 0; i < 10; i++) {
        const segment = getNextSegment();
        if (segment) {
          ids.add(segment.id);
        }
      }

      expect(ids.size).toBe(10);
    });

    it('maintains increasing timestamps', () => {
      // Collect segments first
      const segments = Array.from({ length: 10 }, () => getNextSegment())
        .filter((s): s is NonNullable<typeof s> => s !== null);

      // Verify each segment starts after the previous ends
      const isMonotonicallyIncreasing = segments.every((segment, index) => {
        const prevEnd = index === 0 ? 0 : segments[index - 1].endTime;
        return segment.startTime >= prevEnd;
      });

      expect(isMonotonicallyIncreasing).toBe(true);
    });

    it('loops back after exhausting content', () => {
      // Get enough segments to loop
      const segments: string[] = [];
      for (let i = 0; i < 50; i++) {
        const segment = getNextSegment();
        if (segment) {
          segments.push(segment.text);
        }
      }

      // Should have repeated content
      const unique = new Set(segments);
      expect(unique.size).toBeLessThan(segments.length);
    });
  });

  describe('resetSimulation', () => {
    it('restarts from beginning', () => {
      const first = getNextSegment();
      getNextSegment();
      getNextSegment();

      resetSimulation();

      const afterReset = getNextSegment();

      expect(afterReset?.text).toBe(first?.text);
      expect(afterReset?.startTime).toBe(0);
    });
  });
});

describe('SimulatedTranscriptStream', () => {
  it('emits segments at configured interval', async () => {
    const segments: SimulatedSegment[] = [];
    const stream = new SimulatedTranscriptStream(
      (segment) => segments.push(segment),
      100, // Fast interval for testing
    );

    stream.start();

    await new Promise(resolve => setTimeout(resolve, 350));

    stream.stop();

    // Should have 3-4 segments (immediate + 2-3 interval emissions)
    expect(segments.length).toBeGreaterThanOrEqual(3);
    expect(segments.length).toBeLessThanOrEqual(4);
  });

  it('stops emitting after stop()', async () => {
    const segments: SimulatedSegment[] = [];
    const stream = new SimulatedTranscriptStream(
      (segment) => segments.push(segment),
      50,
    );

    stream.start();
    await new Promise(resolve => setTimeout(resolve, 100));
    stream.stop();

    const countAfterStop = segments.length;

    await new Promise(resolve => setTimeout(resolve, 100));

    expect(segments.length).toBe(countAfterStop);
  });
});
```

### Recording Hook Integration Tests

**File**: `client/src/hooks/use-recording.test.ts`

```typescript
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useRecording } from './use-recording';
import { invoke } from '@tauri-apps/api/core';

vi.mock('@tauri-apps/api/core');

describe('useRecording', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  describe('when simulation is enabled', () => {
    beforeEach(() => {
      vi.mock('./use-preferences', () => ({
        usePreferences: () => ({
          preferences: { simulateTranscription: true },
        }),
      }));
    });

    it('does not invoke Tauri commands', async () => {
      const { result } = renderHook(() => useRecording());

      await act(async () => {
        await result.current.startRecording();
      });

      expect(invoke).not.toHaveBeenCalled();
      expect(result.current.isSimulating).toBe(true);
    });

    it('generates simulated segments', async () => {
      const mockAddSegment = vi.fn();
      vi.mock('../store/segment-store', () => ({
        useSegmentStore: () => ({ addSegment: mockAddSegment }),
      }));

      const { result } = renderHook(() => useRecording());

      await act(async () => {
        await result.current.startRecording();
      });

      // Wait for first simulated segment
      await waitFor(() => {
        expect(mockAddSegment).toHaveBeenCalled();
      }, { timeout: 4000 });

      expect(result.current.isRecording).toBe(true);
      expect(result.current.isSimulating).toBe(true);
    });

    it('stops simulation on stopRecording', async () => {
      const { result } = renderHook(() => useRecording());

      await act(async () => {
        await result.current.startRecording();
      });

      expect(result.current.isSimulating).toBe(true);

      await act(async () => {
        await result.current.stopRecording();
      });

      expect(result.current.isRecording).toBe(false);
      expect(result.current.isSimulating).toBe(false);
    });
  });

  describe('when simulation is disabled', () => {
    beforeEach(() => {
      vi.mock('./use-preferences', () => ({
        usePreferences: () => ({
          preferences: { simulateTranscription: false },
        }),
      }));
    });

    it('invokes Tauri start_recording command', async () => {
      vi.mocked(invoke).mockResolvedValue(undefined);

      const { result } = renderHook(() => useRecording());

      await act(async () => {
        await result.current.startRecording();
      });

      expect(invoke).toHaveBeenCalledWith('start_recording', undefined);
      expect(result.current.isRecording).toBe(true);
      expect(result.current.isSimulating).toBe(false);
    });

    it('invokes Tauri stop_recording command', async () => {
      vi.mocked(invoke).mockResolvedValue(undefined);

      const { result } = renderHook(() => useRecording());

      await act(async () => {
        await result.current.startRecording();
      });

      await act(async () => {
        await result.current.stopRecording();
      });

      expect(invoke).toHaveBeenCalledWith('stop_recording');
    });
  });
});
```

### Simulation Indicator Tests

**File**: `client/src/components/simulation-indicator.test.tsx`

```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { SimulationIndicator } from './simulation-indicator';

describe('SimulationIndicator', () => {
  describe('when simulating', () => {
    beforeEach(() => {
      vi.mock('../hooks/use-recording', () => ({
        useRecording: () => ({ isSimulating: true }),
      }));
    });

    it('renders indicator', () => {
      render(<SimulationIndicator />);

      expect(screen.getByRole('status')).toBeInTheDocument();
      expect(screen.getByText('Simulating')).toBeInTheDocument();
    });

    it('has accessible label', () => {
      render(<SimulationIndicator />);

      expect(screen.getByLabelText('Simulation mode active')).toBeInTheDocument();
    });
  });

  describe('when not simulating', () => {
    beforeEach(() => {
      vi.mock('../hooks/use-recording', () => ({
        useRecording: () => ({ isSimulating: false }),
      }));
    });

    it('renders nothing', () => {
      const { container } = render(<SimulationIndicator />);

      expect(container.firstChild).toBeNull();
    });
  });
});
```

---

## Acceptance Criteria

### Functional

- [x] Toggle in Settings controls simulation mode
- [x] Simulated recordings show transcript segments
- [x] Segments have varied speakers and realistic timing
- [x] Simulation indicator visible when active
- [x] Stopping recording stops simulation

### Technical

- [x] No Tauri commands called during simulation
- [x] Simulated segments added to same store as real ones
- [x] Simulation loops for long recordings
- [x] No audio capture during simulation

### Quality Gates

- [x] `npm run test` passes for all components
- [x] Manual verification: toggle works, segments appear
- [x] Simulation distinguishable from real recording

---

## Rollback Plan

1. **Toggle issues**: Revert to non-functional toggle
2. **Simulation bugs**: Disable toggle in production builds
3. **Store conflicts**: Add separate simulation segment store

---

## Post-Sprint

- Add configurable simulation speed
- Allow custom transcript content upload
- Add simulation of other meeting events (entities, summaries)
- Consider dev-only build flag for simulation
