fixed production build errors

This commit is contained in:
michael 2025-11-04 06:58:51 -06:00
parent 82087bf08f
commit 4355efdc1c
8 changed files with 29 additions and 376 deletions

View file

@ -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<number | null>(null);
const timelineRef = useRef<HTMLDivElement>(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 (
<div className="p-6 bg-btg-bg/50 border border-btg-primary/30 rounded-lg">
<h2 className="text-xl font-semibold text-btg-fg mb-4">Meeting Timeline</h2>
<p className="text-sm text-btg-fg/70 mb-4">
Click on any utterance to highlight it. Pull behaviors are shown in green, Push behaviors in orange.
</p>
<div
ref={timelineRef}
className="space-y-3 max-h-[600px] overflow-y-auto pr-2"
role="log"
aria-label="Meeting timeline"
>
{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 (
<div
key={`${entry.utterance_id}-${index}`}
id={`utterance-${entry.utterance_id}`}
className={`p-4 border-l-4 rounded-md transition-all cursor-pointer ${behaviorColor} ${
isSelected ? 'ring-2 ring-btg-accent shadow-lg' : 'hover:shadow-md'
}`}
onClick={() => 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 */}
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-bold text-btg-fg">{entry.speaker}</span>
<span className="text-xs px-2 py-1 rounded-full bg-btg-bg/50 text-btg-fg/70">
{formatTime(entry.start_sec)} - {formatTime(entry.end_sec)}
</span>
</div>
<div className="flex items-center gap-2">
<span className={`text-xs px-2 py-1 rounded-full font-medium ${
isPull ? 'bg-green-500/30 text-green-200' : 'bg-orange-500/30 text-orange-200'
}`}>
{isPull ? 'PULL' : 'PUSH'}
</span>
<span className="text-xs px-2 py-1 rounded-full bg-btg-primary/30 text-btg-fg">
{getBehaviorLabel(entry.behavior)}
</span>
</div>
</div>
{/* Utterance text */}
<p className="text-sm text-btg-fg leading-relaxed mb-2">
"{utterance.text}"
</p>
{/* Proposal info (if applicable) */}
{(entry.proposal.build_on || !entry.proposal.appropriate_push) && (
<div className="mt-2 p-2 bg-btg-bg/50 rounded-md text-xs space-y-1">
{entry.proposal.build_on && (
<div className="flex items-center gap-2 text-btg-accent">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
<span>Builds on prior idea</span>
</div>
)}
{!entry.proposal.appropriate_push && (
<div className="flex items-center gap-2 text-btg-warn">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<span>Inappropriate Push timing (lacks urgency or has high rejection risk)</span>
</div>
)}
</div>
)}
{/* Pull→Push transitions near this utterance */}
{transitions.length > 0 && (
<div className="mt-2 p-2 bg-btg-accent/10 border border-btg-accent/30 rounded-md text-xs">
<div className="flex items-center gap-2 text-btg-accent font-medium mb-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span>PullPush Transition Nearby</span>
</div>
{transitions.map((t, i) => (
<div key={i} className="text-btg-fg/70 ml-6">
{t.speaker} transitioned from {t.from_behavior} to {t.to_behavior} at {formatTime(t.time_sec)}
</div>
))}
</div>
)}
</div>
);
})}
</div>
{/* Legend */}
<div className="mt-4 pt-4 border-t border-btg-primary/30">
<p className="text-xs font-semibold text-btg-fg mb-2">Legend:</p>
<div className="flex flex-wrap gap-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500/30 border border-green-500 rounded"></div>
<span className="text-btg-fg/70">Pull Behavior</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-orange-500/30 border border-orange-500 rounded"></div>
<span className="text-btg-fg/70">Push Behavior</span>
</div>
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-btg-accent" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
<span className="text-btg-fg/70">Builds on idea</span>
</div>
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-btg-warn" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<span className="text-btg-fg/70">Inappropriate Push</span>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<div className="p-6 bg-btg-bg/50 border border-btg-primary/30 rounded-lg">
<h3 className="text-lg font-semibold text-btg-fg mb-2">PullPush Transitions</h3>
<p className="text-sm text-btg-fg/70">
No significant PullPush transitions detected in this meeting.
</p>
</div>
);
}
return (
<div className="p-6 bg-btg-bg/50 border border-btg-primary/30 rounded-lg">
<h3 className="text-lg font-semibold text-btg-fg mb-2">PullPush Transitions</h3>
<p className="text-sm text-btg-fg/70 mb-4">
Key moments where speakers transitioned from Pull to Push behaviors (detected within 60-second windows)
</p>
<div className="space-y-3">
{transitions.map((transition, index) => (
<div
key={index}
className="p-4 bg-btg-accent/10 border border-btg-accent/30 rounded-md hover:bg-btg-accent/15 transition-colors cursor-pointer"
onClick={() => 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)}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<svg
className="w-5 h-5 text-btg-accent flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
<span className="font-semibold text-btg-fg">{transition.speaker}</span>
</div>
<span className="text-xs px-2 py-1 rounded-full bg-btg-bg/50 text-btg-fg/70">
{formatTime(transition.time_sec)}
</span>
</div>
<div className="ml-7 text-sm text-btg-fg">
<span className="text-green-400">{formatBehavior(transition.from_behavior)}</span>
<span className="text-btg-fg/50 mx-2"></span>
<span className="text-orange-400">{formatBehavior(transition.to_behavior)}</span>
</div>
<div className="ml-7 mt-2 text-xs text-btg-fg/60">
Click to view in timeline
</div>
</div>
))}
</div>
{transitions.length > 5 && (
<div className="mt-4 text-xs text-center text-btg-fg/60">
Showing {transitions.length} transitions
</div>
)}
</div>
);
}

View file

@ -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);
};

View file

@ -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';

View file

@ -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';

View file

@ -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 (
<div className="p-4 bg-btg-warn/20 border border-btg-warn rounded-md">
@ -145,7 +138,7 @@ export function DashboardPage() {
fill="#8884d8"
dataKey="value"
>
{speakingTimeData.map((entry, index) => (
{speakingTimeData.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>

View file

@ -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<typeof setInterval>;
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 */}
<div className="p-4 bg-btg-primary/10 border border-btg-primary/30 rounded-lg">
<p className="text-sm text-btg-fg">
{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.'}
</p>
{job.status === JobStatus.PROCESSING && (
{job.status === 'processing' && (
<p className="text-xs text-btg-fg/60 mt-2">
The AI is transcribing audio, performing speaker diarization, and analyzing Rackham behaviors.
</p>

View file

@ -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;