diff --git a/frontend/src/components/TimelineVisualization.tsx b/frontend/src/components/TimelineVisualization.tsx deleted file mode 100644 index 77e4032..0000000 --- a/frontend/src/components/TimelineVisualization.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { useState, useRef, useEffect } from 'react'; -import type { TimelineEntry, Utterance } from '../types'; - -interface TimelineVisualizationProps { - timeline: TimelineEntry[]; - utterances: Utterance[]; - pullPushTransitions: Array<{ - time_sec: number; - from: string; - to: string; - speaker: string; - }>; -} - -export function TimelineVisualization({ - timeline, - utterances, - pullPushTransitions -}: TimelineVisualizationProps) { - const [selectedId, setSelectedId] = useState(null); - const timelineRef = useRef(null); - - const formatTime = (seconds: number): string => { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; - }; - - const scrollToUtterance = (utteranceId: number) => { - setSelectedId(utteranceId); - const element = document.getElementById(`utterance-${utteranceId}`); - if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }; - - // Expose scrollToUtterance globally so action items can use it - useEffect(() => { - (window as any).scrollToUtterance = scrollToUtterance; - return () => { - delete (window as any).scrollToUtterance; - }; - }, []); - - const getBehaviorColor = (behavior: string): string => { - const pullBehaviors = [ - 'open_question', - 'closed_question', - 'testing_understanding', - 'summarizing', - 'bringing_in' - ]; - - if (pullBehaviors.includes(behavior)) { - return 'bg-green-500/20 border-green-500'; - } - - return 'bg-orange-500/20 border-orange-500'; - }; - - const getBehaviorLabel = (behavior: string): string => { - return behavior - .split('_') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - }; - - const isPullBehavior = (behavior: string): boolean => { - const pullBehaviors = [ - 'open_question', - 'closed_question', - 'testing_understanding', - 'summarizing', - 'bringing_in' - ]; - return pullBehaviors.includes(behavior); - }; - - // Find transitions at specific times - const getTransitionsAtTime = (timeRange: { start: number; end: number }) => { - return pullPushTransitions.filter( - t => t.time_sec >= timeRange.start && t.time_sec <= timeRange.end - ); - }; - - return ( -
-

Meeting Timeline

-

- Click on any utterance to highlight it. Pull behaviors are shown in green, Push behaviors in orange. -

- -
- {timeline.map((entry, index) => { - const utterance = utterances[entry.utterance_id]; - if (!utterance) return null; - - const isSelected = selectedId === entry.utterance_id; - const behaviorColor = getBehaviorColor(entry.behavior); - const isPull = isPullBehavior(entry.behavior); - - // Check for transitions near this utterance - const transitions = getTransitionsAtTime({ - start: entry.start_sec - 5, - end: entry.end_sec + 5 - }); - - return ( -
scrollToUtterance(entry.utterance_id)} - role="article" - aria-label={`Utterance by ${entry.speaker} at ${formatTime(entry.start_sec)}`} - tabIndex={0} - onKeyPress={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - scrollToUtterance(entry.utterance_id); - } - }} - > - {/* Header */} -
-
- {entry.speaker} - - {formatTime(entry.start_sec)} - {formatTime(entry.end_sec)} - -
-
- - {isPull ? 'PULL' : 'PUSH'} - - - {getBehaviorLabel(entry.behavior)} - -
-
- - {/* Utterance text */} -

- "{utterance.text}" -

- - {/* Proposal info (if applicable) */} - {(entry.proposal.build_on || !entry.proposal.appropriate_push) && ( -
- {entry.proposal.build_on && ( -
- - - - Builds on prior idea -
- )} - {!entry.proposal.appropriate_push && ( -
- - - - Inappropriate Push timing (lacks urgency or has high rejection risk) -
- )} -
- )} - - {/* Pull→Push transitions near this utterance */} - {transitions.length > 0 && ( -
-
- - - - Pull→Push Transition Nearby -
- {transitions.map((t, i) => ( -
- {t.speaker} transitioned from {t.from_behavior} to {t.to_behavior} at {formatTime(t.time_sec)} -
- ))} -
- )} -
- ); - })} -
- - {/* Legend */} -
-

Legend:

-
-
-
- Pull Behavior -
-
-
- Push Behavior -
-
- - - - Builds on idea -
-
- - - - Inappropriate Push -
-
-
-
- ); -} diff --git a/frontend/src/components/TransitionsPanel.tsx b/frontend/src/components/TransitionsPanel.tsx deleted file mode 100644 index 7a79c7b..0000000 --- a/frontend/src/components/TransitionsPanel.tsx +++ /dev/null @@ -1,115 +0,0 @@ -interface Transition { - time_sec: number; - from_behavior: string; - to_behavior: string; - speaker: string; -} - -interface TransitionsPanelProps { - transitions: Transition[]; -} - -export function TransitionsPanel({ transitions }: TransitionsPanelProps) { - const formatTime = (seconds: number): string => { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; - }; - - const scrollToUtterance = (timeSec: number) => { - // Find closest utterance to this time - // This will be handled by the timeline component - if ((window as any).scrollToUtterance) { - // We'll need to find the utterance ID that matches this time - // For now, just scroll to the timeline section - const timelineElement = document.getElementById('timeline-section'); - if (timelineElement) { - timelineElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - }; - - const formatBehavior = (behavior: string): string => { - return behavior - .split('_') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - }; - - if (transitions.length === 0) { - return ( -
-

Pull→Push Transitions

-

- No significant Pull→Push transitions detected in this meeting. -

-
- ); - } - - return ( -
-

Pull→Push Transitions

-

- Key moments where speakers transitioned from Pull to Push behaviors (detected within 60-second windows) -

- -
- {transitions.map((transition, index) => ( -
scrollToUtterance(transition.time_sec)} - role="button" - tabIndex={0} - onKeyPress={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - scrollToUtterance(transition.time_sec); - } - }} - aria-label={`Transition at ${formatTime(transition.time_sec)}`} - > -
-
- - {transition.speaker} -
- - {formatTime(transition.time_sec)} - -
- -
- {formatBehavior(transition.from_behavior)} - - {formatBehavior(transition.to_behavior)} -
- -
- Click to view in timeline -
-
- ))} -
- - {transitions.length > 5 && ( -
- Showing {transitions.length} transitions -
- )} -
- ); -} diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 6851c48..8ccbac9 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,4 +1,5 @@ -import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useEffect } from 'react'; +import type { ReactNode } from 'react'; import { authAPI } from '../services/api'; import type { User, LoginRequest, RegisterRequest } from '../types'; @@ -48,7 +49,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => }; const register = async (data: RegisterRequest) => { - const newUser = await authAPI.register(data); + await authAPI.register(data); // After registration, user needs to login separately setUser(null); }; diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index a10dcbd..70a4497 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,4 +1,5 @@ -import { useState, FormEvent } from 'react'; +import { useState } from 'react'; +import type { FormEvent } from 'react'; import { Link } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index 883a8d0..04a7982 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -1,4 +1,5 @@ -import { useState, FormEvent } from 'react'; +import { useState } from 'react'; +import type { FormEvent } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index f2304af..e43a9c9 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; import { analysesAPI } from '../../services/api'; -import type { AnalysisResponse, Participant, BehaviorExample } from '../../types'; +import type { AnalysisResponse, Participant } from '../../types'; import { PullPushGauge } from '../../components/PullPushGauge'; export function DashboardPage() { @@ -63,13 +63,6 @@ export function DashboardPage() { return participant.name || participant.id; }; - // Get speaker display from ID (for behavior examples) - const getSpeakerNameById = (speakerId: string, speakerName?: string | null): string => { - if (speakerName) return speakerName; - const participant = participants.find(p => p.id === speakerId); - return participant?.name || speakerId; - }; - if (error) { return (
@@ -145,7 +138,7 @@ export function DashboardPage() { fill="#8884d8" dataKey="value" > - {speakingTimeData.map((entry, index) => ( + {speakingTimeData.map((_, index) => ( ))} diff --git a/frontend/src/pages/processing/ProcessingPage.tsx b/frontend/src/pages/processing/ProcessingPage.tsx index 37d2f4b..f5baa00 100644 --- a/frontend/src/pages/processing/ProcessingPage.tsx +++ b/frontend/src/pages/processing/ProcessingPage.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { jobsAPI } from '../../services/api'; -import type { Job } from '../../types'; -import { JobStatus } from '../../types'; +import type { Job, JobStatus } from '../../types'; export function ProcessingPage() { const { jobId } = useParams<{ jobId: string }>(); @@ -13,7 +12,7 @@ export function ProcessingPage() { useEffect(() => { if (!jobId) return; - let interval: NodeJS.Timeout; + let interval: ReturnType; const checkStatus = async () => { try { @@ -21,7 +20,7 @@ export function ProcessingPage() { setJob(jobData); // If completed, navigate to dashboard - if (jobData.status === JobStatus.COMPLETED) { + if (jobData.status === 'completed') { clearInterval(interval); setTimeout(() => { navigate(`/dashboard/${jobId}`); @@ -29,7 +28,7 @@ export function ProcessingPage() { } // If failed, show error - if (jobData.status === JobStatus.FAILED) { + if (jobData.status === 'failed') { clearInterval(interval); setError(jobData.error_message || 'Analysis failed'); } @@ -50,12 +49,12 @@ export function ProcessingPage() { const getStepStatus = (status: JobStatus) => { const steps = [ - { key: 'uploaded', label: 'Upload', statuses: [JobStatus.UPLOADED, JobStatus.PROCESSING, JobStatus.COMPLETED] }, - { key: 'processing', label: 'Analyze', statuses: [JobStatus.PROCESSING, JobStatus.COMPLETED] }, - { key: 'completed', label: 'Render', statuses: [JobStatus.COMPLETED] }, + { key: 'uploaded', label: 'Upload', statuses: ['uploaded' as JobStatus, 'processing' as JobStatus, 'completed' as JobStatus] }, + { key: 'processing', label: 'Analyze', statuses: ['processing' as JobStatus, 'completed' as JobStatus] }, + { key: 'completed', label: 'Render', statuses: ['completed' as JobStatus] }, ]; - return steps.map((step, index) => ({ + return steps.map((step) => ({ ...step, isActive: step.statuses.includes(status), isComplete: step.statuses.includes(status) && status !== step.statuses[0], @@ -159,12 +158,12 @@ export function ProcessingPage() { {/* Status message */}

- {job.status === JobStatus.UPLOADED && 'Video uploaded successfully. Starting analysis...'} - {job.status === JobStatus.PROCESSING && 'Analyzing video with AI. This may take several minutes...'} - {job.status === JobStatus.COMPLETED && 'Analysis complete! Redirecting to dashboard...'} - {job.status === JobStatus.FAILED && 'Analysis failed. Please try again.'} + {job.status === 'uploaded' && 'Video uploaded successfully. Starting analysis...'} + {job.status === 'processing' && 'Analyzing video with AI. This may take several minutes...'} + {job.status === 'completed' && 'Analysis complete! Redirecting to dashboard...'} + {job.status === 'failed' && 'Analysis failed. Please try again.'}

- {job.status === JobStatus.PROCESSING && ( + {job.status === 'processing' && (

The AI is transcribing audio, performing speaker diarization, and analyzing Rackham behaviors.

diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index a26ca52..10d3f1e 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -18,14 +18,13 @@ export interface RegisterRequest { } // Job types -export enum JobStatus { - PENDING = 'pending', - UPLOADING = 'uploading', - UPLOADED = 'uploaded', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', -} +export type JobStatus = + | 'pending' + | 'uploading' + | 'uploaded' + | 'processing' + | 'completed' + | 'failed'; export interface Job { _id: string;