Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { motion } from 'framer-motion'; | |
| import { | |
| BrainCircuit, | |
| CheckCircle2, | |
| Circle, | |
| Loader2, | |
| Mic, | |
| UploadCloud, | |
| } from 'lucide-react'; | |
| import type { LucideIcon } from 'lucide-react'; | |
| import { useMemo } from 'react'; | |
| import { useAppStore } from '@/store/useAppStore'; | |
| import { Card } from '@/components/ui/Card'; | |
| import { Reveal } from '@/components/ui/Reveal'; | |
| import { SectionHeader } from '@/components/ui/SectionHeader'; | |
| import { cn } from '@/lib/utils'; | |
| type StepStatus = 'pending' | 'active' | 'complete' | 'error'; | |
| type PipelineStep = { | |
| id: string; | |
| label: string; | |
| description: string; | |
| icon: LucideIcon; | |
| }; | |
| const PIPELINE_STEPS: PipelineStep[] = [ | |
| { | |
| id: 'upload', | |
| label: 'Upload', | |
| description: 'Audio file sent to the API', | |
| icon: UploadCloud, | |
| }, | |
| { | |
| id: 'transcribe', | |
| label: 'Transcription', | |
| description: 'Whisper converts speech to text', | |
| icon: Mic, | |
| }, | |
| { | |
| id: 'extract', | |
| label: 'Feature Extraction', | |
| description: 'LLaMA analyzes transcript signals', | |
| icon: BrainCircuit, | |
| }, | |
| { | |
| id: 'ready', | |
| label: 'Pipeline Complete', | |
| description: 'Transcript and insights ready', | |
| icon: CheckCircle2, | |
| }, | |
| ]; | |
| function resolveStepStatuses(state: { | |
| audioFile: File | null; | |
| isUploading: boolean; | |
| uploadProgress: number; | |
| isExtracting: boolean; | |
| transcription: string; | |
| features: unknown; | |
| error: string | null; | |
| recordingState: string; | |
| }): Record<string, StepStatus> { | |
| const { | |
| audioFile, | |
| isUploading, | |
| uploadProgress, | |
| isExtracting, | |
| transcription, | |
| features, | |
| error, | |
| recordingState, | |
| } = state; | |
| const statuses: Record<string, StepStatus> = { | |
| upload: 'pending', | |
| transcribe: 'pending', | |
| extract: 'pending', | |
| ready: 'pending', | |
| }; | |
| if (error && audioFile) { | |
| if (isUploading || uploadProgress < 100) statuses.upload = 'error'; | |
| else if (!transcription) statuses.transcribe = 'error'; | |
| else statuses.extract = 'error'; | |
| return statuses; | |
| } | |
| const liveActive = | |
| recordingState === 'recording' || | |
| recordingState === 'processing' || | |
| recordingState === 'analyzing'; | |
| if (!audioFile && !liveActive && !transcription) { | |
| return statuses; | |
| } | |
| if (isUploading) { | |
| statuses.upload = uploadProgress < 60 ? 'active' : 'complete'; | |
| statuses.transcribe = uploadProgress >= 60 ? 'active' : 'pending'; | |
| return statuses; | |
| } | |
| if (isExtracting || recordingState === 'analyzing') { | |
| statuses.upload = 'complete'; | |
| statuses.transcribe = 'complete'; | |
| statuses.extract = 'active'; | |
| return statuses; | |
| } | |
| if (recordingState === 'recording' || recordingState === 'processing') { | |
| statuses.upload = 'active'; | |
| statuses.transcribe = recordingState === 'processing' ? 'active' : 'pending'; | |
| return statuses; | |
| } | |
| if (transcription && features) { | |
| statuses.upload = 'complete'; | |
| statuses.transcribe = 'complete'; | |
| statuses.extract = 'complete'; | |
| statuses.ready = 'complete'; | |
| return statuses; | |
| } | |
| if (transcription) { | |
| statuses.upload = 'complete'; | |
| statuses.transcribe = 'complete'; | |
| return statuses; | |
| } | |
| if (audioFile) { | |
| statuses.upload = 'complete'; | |
| statuses.transcribe = 'active'; | |
| } | |
| return statuses; | |
| } | |
| function StatusIcon({ status }: { status: StepStatus }) { | |
| if (status === 'active') { | |
| return <Loader2 className="h-4 w-4 animate-spin text-nexus-accent" />; | |
| } | |
| if (status === 'complete') { | |
| return <CheckCircle2 className="h-4 w-4 text-emerald-500" />; | |
| } | |
| if (status === 'error') { | |
| return <Circle className="h-4 w-4 text-red-500" />; | |
| } | |
| return <Circle className="h-4 w-4 text-nexus-border" />; | |
| } | |
| export function ProcessingPipelineSection() { | |
| const audioFile = useAppStore((s) => s.audioFile); | |
| const isUploading = useAppStore((s) => s.isUploading); | |
| const uploadProgress = useAppStore((s) => s.uploadProgress); | |
| const isExtracting = useAppStore((s) => s.isExtracting); | |
| const transcription = useAppStore((s) => s.transcription); | |
| const features = useAppStore((s) => s.features); | |
| const error = useAppStore((s) => s.error); | |
| const recordingState = useAppStore((s) => s.recordingState); | |
| const stepStatuses = useMemo( | |
| () => | |
| resolveStepStatuses({ | |
| audioFile, | |
| isUploading, | |
| uploadProgress, | |
| isExtracting, | |
| transcription, | |
| features, | |
| error, | |
| recordingState, | |
| }), | |
| [audioFile, isUploading, uploadProgress, isExtracting, transcription, features, error, recordingState], | |
| ); | |
| const activeIndex = PIPELINE_STEPS.findIndex((step) => stepStatuses[step.id] === 'active'); | |
| const progress = | |
| activeIndex >= 0 | |
| ? ((activeIndex + 0.5) / PIPELINE_STEPS.length) * 100 | |
| : stepStatuses.ready === 'complete' | |
| ? 100 | |
| : 0; | |
| const statusLabel = useMemo(() => { | |
| if (error && audioFile) return 'Pipeline encountered an error'; | |
| if (stepStatuses.ready === 'complete') return 'All stages complete'; | |
| if (isExtracting || recordingState === 'analyzing') return 'Extracting features…'; | |
| if (isUploading) return 'Uploading and transcribing…'; | |
| if (recordingState === 'recording') return 'Recording live audio…'; | |
| if (recordingState === 'processing') return 'Processing recorded audio…'; | |
| if (!audioFile && !transcription) return 'Waiting for audio input'; | |
| return 'Pipeline idle'; | |
| }, [audioFile, error, isExtracting, isUploading, recordingState, stepStatuses.ready, transcription]); | |
| return ( | |
| <section id="processing" className="relative mx-auto max-w-6xl px-6 py-20 md:px-10 md:py-28"> | |
| <Reveal> | |
| <SectionHeader | |
| eyebrow="Pipeline" | |
| title="Audio Processing" | |
| description="Track each stage as your recording moves from upload through transcription and feature extraction." | |
| align="center" | |
| /> | |
| </Reveal> | |
| <Reveal delay={0.08}> | |
| <Card className="overflow-hidden" padding="lg"> | |
| <div className="mb-8 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> | |
| <div> | |
| <p className="text-sm font-medium text-nexus-fg">{statusLabel}</p> | |
| <p className="mt-1 text-xs text-nexus-muted"> | |
| {audioFile | |
| ? `${audioFile.name} · ${(audioFile.size / (1024 * 1024)).toFixed(2)} MB` | |
| : recordingState !== 'idle' | |
| ? 'Live recording session' | |
| : 'No active job'} | |
| </p> | |
| </div> | |
| {(isUploading || isExtracting || recordingState !== 'idle') && ( | |
| <span className="inline-flex items-center gap-2 rounded-full border border-nexus-accent/20 bg-nexus-accent/10 px-3 py-1 text-xs font-medium text-nexus-accent"> | |
| <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-nexus-accent" /> | |
| In progress | |
| </span> | |
| )} | |
| </div> | |
| <div className="relative mb-10 hidden h-1 overflow-hidden rounded-full bg-nexus-border/60 sm:block"> | |
| <motion.div | |
| className="h-full rounded-full bg-nexus-accent" | |
| initial={false} | |
| animate={{ width: `${progress}%` }} | |
| transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }} | |
| /> | |
| </div> | |
| <ol className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> | |
| {PIPELINE_STEPS.map((step, index) => { | |
| const status = stepStatuses[step.id]; | |
| const Icon = step.icon; | |
| return ( | |
| <li key={step.id}> | |
| <div | |
| className={cn( | |
| 'relative h-full rounded-2xl border p-5 transition-colors duration-300', | |
| status === 'active' && 'border-nexus-accent/40 bg-nexus-accent/5', | |
| status === 'complete' && 'border-emerald-500/25 bg-emerald-500/5', | |
| status === 'error' && 'border-red-500/25 bg-red-500/5', | |
| status === 'pending' && 'border-nexus-border bg-nexus-card/40', | |
| )} | |
| > | |
| <div className="mb-4 flex items-center justify-between"> | |
| <div | |
| className={cn( | |
| 'flex h-10 w-10 items-center justify-center rounded-xl border', | |
| status === 'active' | |
| ? 'border-nexus-accent/30 bg-nexus-accent/10 text-nexus-accent' | |
| : status === 'complete' | |
| ? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-600' | |
| : 'border-nexus-border bg-nexus-bg text-nexus-muted', | |
| )} | |
| > | |
| <Icon className="h-4 w-4" /> | |
| </div> | |
| <StatusIcon status={status} /> | |
| </div> | |
| <p className="text-[10px] font-semibold uppercase tracking-[0.16em] text-nexus-muted"> | |
| Step {index + 1} | |
| </p> | |
| <h3 className="mt-1 text-sm font-semibold text-nexus-fg">{step.label}</h3> | |
| <p className="mt-2 text-xs leading-relaxed text-nexus-muted">{step.description}</p> | |
| </div> | |
| </li> | |
| ); | |
| })} | |
| </ol> | |
| {transcription && ( | |
| <div className="mt-8 rounded-2xl border border-nexus-border bg-nexus-bg/50 p-4"> | |
| <p className="text-[10px] font-semibold uppercase tracking-[0.16em] text-nexus-muted"> | |
| Latest output | |
| </p> | |
| <p className="mt-2 line-clamp-2 text-sm leading-relaxed text-nexus-fg"> | |
| {transcription} | |
| </p> | |
| </div> | |
| )} | |
| </Card> | |
| </Reveal> | |
| </section> | |
| ); | |
| } | |