Skip to content

feat(dashboard): parallelize LLM analysis — per-session concurrent Map<sessionId, AbortController> #228

@melagiri

Description

@melagiri

Summary

Currently, AnalysisContext.tsx uses a single isAnalyzingRef boolean + a single AbortController that globally blocks any second analysis from starting while one is in-flight. This forces users to wait for one session's analysis to complete before triggering another.

The server (Hono) already handles concurrent requests fine. This is a client-side guard that was appropriate when the context was designed for a single panel, but is now a friction point as users browse multiple sessions.

Goal: Replace the single-analysis boolean guard with per-session concurrent analysis support, so users can trigger analysis on multiple sessions simultaneously.


Current Behavior

AnalysisContext.tsx state (CURRENT):
  state: AnalysisState         ← single slot (one session at a time)
  isAnalyzingRef: boolean      ← global lock
  abortControllerRef: AbortController | null  ← single cancel handle

Consequence: starting analysis on session B while session A is in-flight
shows "Analysis already in progress. Please wait or cancel it first."

All consumer components (AnalyzeButton, AnalyzePromptQualityButton, AnalyzeDropdown) have an isAnalyzingOther branch that renders a disabled "Analysis in progress... Waiting for X" state.

Target Behavior

AnalysisContext.tsx state (TARGET):
  analyses: Map<string, { state: PerSessionAnalysisState, controller: AbortController }>
  key = `${sessionId}:${type}`   ← "session-abc:session" or "session-abc:prompt_quality"

startAnalysis(session, type)  → adds entry to Map, streams independently
cancelAnalysis(sessionId, type) → aborts + removes that entry
getState(sessionId, type)     → returns per-session state (or null = idle)

Files to Change

File Change
dashboard/src/components/analysis/AnalysisContext.tsx Core refactor — Map-based state, updated context API
dashboard/src/components/analysis/AnalyzeButton.tsx Remove isAnalyzingOther branch; pass cancelAnalysis(session.id, 'session')
dashboard/src/components/analysis/AnalyzePromptQualityButton.tsx Remove isAnalyzingOtherSession + isAnalyzingThisSessionOtherType; per-session cancel
dashboard/src/components/analysis/AnalyzeDropdown.tsx Remove isAnalyzingOther branch + "Waiting for X" copy
dashboard/src/components/sessions/SessionDetailPanel.tsx Update isAnalyzingThisSession query to use per-session getter

No server changes needed. Dashboard-only.


Acceptance Criteria

  • Triggering analysis on session A while session B is already analyzing starts both concurrently (no block, no toast warning)
  • Each in-flight analysis renders its own loading state + Cancel button independently
  • Cancelling session A's analysis does not affect session B's analysis
  • Per-session progress (phase, chunk/total) is shown independently per session
  • isAnalyzingOther / "Waiting for X" disabled states are removed from all three button components
  • Toast strategy: each analysis uses its own toast ID (e.g. analysis-${sessionId}-${type}) so toasts do not clobber each other
  • Batch backfill (reflect backfill) is NOT impacted — it hits the server API directly and stays sequential
  • pnpm build from repo root passes with zero errors
  • TypeScript strict — no any, no implicit casts

Context API Shape (suggested, UX Engineer review may refine)

interface AnalysisContextValue {
  // Get state for a specific (sessionId, type) pair — null means idle
  getAnalysisState: (sessionId: string, type: 'session' | 'prompt_quality') => PerSessionAnalysisState | null;
  // Check if any analysis is in-flight (useful for global UI indicators if needed)
  hasActiveAnalysis: boolean;
  startAnalysis: (session: Session, type: 'session' | 'prompt_quality') => Promise<void>;
  cancelAnalysis: (sessionId: string, type: 'session' | 'prompt_quality') => void;
  clearResult: (sessionId: string, type: 'session' | 'prompt_quality') => void;
}

The PerSessionAnalysisState type mirrors the existing AnalysisState shape but without the top-level sessionId/sessionTitle (those become the Map key context).


Verification Tags

  • VISUAL: YES — per-session progress indicators, cancel buttons, removed blocked states
  • OUTPUT_ARTIFACT: NO
  • NEW_DEPS: NO — pure React state refactor
  • API_CHANGE: NO — dashboard-only

T-Shirt Size

M — 5 files, all within dashboard/src/. No new packages. No schema changes. No server changes. State management refactor with consumer updates.


Implementation Notes

  1. Use useCallback with stable Map ref (via useRef) rather than putting the Map in useState — Map mutations shouldn't trigger re-renders; only the derived state snapshots should
  2. A useState<Map<string, PerSessionAnalysisState>> (cloned on update) is the simpler React-idiomatic approach if the above creates complexity — pick whichever the UX review validates
  3. The key ${sessionId}:${type} uniquely identifies an in-flight analysis (session A can run session analysis AND prompt_quality analysis simultaneously)
  4. Existing ANALYSIS_TOAST_ID constant should be replaced with per-analysis toast IDs
  5. The clearResult function should accept (sessionId, type) to clear a specific entry

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew functionality

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions