github-actions[bot]
Deploy demo from GitHub Actions - 2025-12-24 02:23:20
6cdce85
'use client';
import { useEffect, useState, useCallback } from 'react';
import { Loader2, Server, Cpu, Zap, Clock, AlertCircle } from 'lucide-react';
import { clsx } from 'clsx';
import type { StatusResponse } from '@/app/api/status/route';
type LoadingPhase =
| 'sending' // Initial request sent
| 'cold_start' // Workers starting up
| 'initializing' // Model loading
| 'processing' // Actively generating
| 'streaming'; // Receiving response
interface LoadingStatusProps {
isLoading: boolean;
hasStartedStreaming: boolean;
onStatusChange?: (status: StatusResponse | null) => void;
}
const PHASE_CONFIG: Record<LoadingPhase, {
icon: React.ElementType;
label: string;
color: string;
pulseColor: string;
}> = {
sending: {
icon: Zap,
label: 'Sending request...',
color: 'text-blue-400',
pulseColor: 'bg-blue-400',
},
cold_start: {
icon: Server,
label: 'Starting worker...',
color: 'text-amber-400',
pulseColor: 'bg-amber-400',
},
initializing: {
icon: Cpu,
label: 'Loading model...',
color: 'text-purple-400',
pulseColor: 'bg-purple-400',
},
processing: {
icon: Loader2,
label: 'Generating response...',
color: 'text-teal-400',
pulseColor: 'bg-teal-400',
},
streaming: {
icon: Zap,
label: 'Receiving...',
color: 'text-emerald-400',
pulseColor: 'bg-emerald-400',
},
};
export function LoadingStatus({ isLoading, hasStartedStreaming, onStatusChange }: LoadingStatusProps) {
const [phase, setPhase] = useState<LoadingPhase>('sending');
const [elapsedTime, setElapsedTime] = useState(0);
const [estimatedWait, setEstimatedWait] = useState<number | undefined>();
const [statusMessage, setStatusMessage] = useState<string>('');
// Poll for status while loading
const checkStatus = useCallback(async () => {
try {
const response = await fetch('/api/status');
if (response.ok) {
const status: StatusResponse = await response.json();
onStatusChange?.(status);
// Map status to phase
if (status.status === 'cold_start') {
setPhase('cold_start');
setStatusMessage(status.message);
} else if (status.status === 'initializing') {
setPhase('initializing');
setStatusMessage(status.message);
} else if (status.status === 'processing') {
setPhase('processing');
setStatusMessage(status.message);
}
if (status.estimatedWait) {
setEstimatedWait(status.estimatedWait);
}
}
} catch {
// Silently fail, keep current phase
}
}, [onStatusChange]);
// Start polling when loading starts
useEffect(() => {
if (!isLoading) {
setPhase('sending');
setElapsedTime(0);
setEstimatedWait(undefined);
setStatusMessage('');
return;
}
// Initial status check
checkStatus();
// Poll every 2 seconds while loading and not streaming
const statusInterval = setInterval(() => {
if (!hasStartedStreaming) {
checkStatus();
}
}, 2000);
// Track elapsed time
const timeInterval = setInterval(() => {
setElapsedTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(statusInterval);
clearInterval(timeInterval);
};
}, [isLoading, hasStartedStreaming, checkStatus]);
// Update phase based on streaming state
useEffect(() => {
if (hasStartedStreaming) {
setPhase('streaming');
}
}, [hasStartedStreaming]);
// After 3 seconds without response, likely a cold start
useEffect(() => {
if (isLoading && !hasStartedStreaming && elapsedTime >= 3 && phase === 'sending') {
setPhase('cold_start');
}
}, [isLoading, hasStartedStreaming, elapsedTime, phase]);
if (!isLoading) return null;
const config = PHASE_CONFIG[phase];
const Icon = config.icon;
const showEstimate = estimatedWait && phase !== 'streaming';
// Calculate progress percentage
const progress = estimatedWait ? Math.min((elapsedTime / estimatedWait) * 100, 95) : undefined;
return (
<div className="flex flex-col gap-2">
{/* Main status indicator */}
<div className="flex items-center gap-3">
{/* Animated icon */}
<div className="relative">
<div className={clsx(
'w-8 h-8 rounded-lg flex items-center justify-center',
'bg-zinc-800/80 border border-zinc-700/50'
)}>
<Icon className={clsx(
'w-4 h-4',
config.color,
phase !== 'streaming' && 'animate-pulse'
)} />
</div>
{/* Pulse effect for cold start */}
{(phase === 'cold_start' || phase === 'initializing') && (
<span className={clsx(
'absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full',
config.pulseColor,
'animate-ping opacity-75'
)} />
)}
</div>
{/* Status text */}
<div className="flex-1">
<div className={clsx('text-sm font-medium', config.color)}>
{statusMessage || config.label}
</div>
{/* Time indicator */}
<div className="flex items-center gap-2 text-xs text-zinc-500">
<Clock className="w-3 h-3" />
<span>{formatTime(elapsedTime)}</span>
{showEstimate && (
<>
<span className="text-zinc-600"></span>
<span>~{formatTime(Math.max(0, estimatedWait - elapsedTime))} remaining</span>
</>
)}
</div>
</div>
</div>
{/* Progress bar for cold start */}
{showEstimate && progress !== undefined && (
<div className="h-1 bg-zinc-800 rounded-full overflow-hidden">
<div
className={clsx(
'h-full rounded-full transition-all duration-1000',
phase === 'cold_start' && 'bg-amber-500/50',
phase === 'initializing' && 'bg-purple-500/50',
phase === 'processing' && 'bg-teal-500/50'
)}
style={{ width: `${progress}%` }}
/>
</div>
)}
{/* Cold start explanation */}
{phase === 'cold_start' && elapsedTime >= 5 && (
<div className="flex items-start gap-2 p-2 bg-amber-500/5 border border-amber-500/20 rounded-lg text-xs">
<AlertCircle className="w-3.5 h-3.5 text-amber-400 mt-0.5 flex-shrink-0" />
<p className="text-zinc-400">
<span className="text-amber-400 font-medium">Cold start detected.</span>{' '}
The model is scaling up from zero. This typically takes 30-60 seconds on first request.
</p>
</div>
)}
</div>
);
}
function formatTime(seconds: number): string {
if (seconds < 60) {
return `${seconds}s`;
}
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs}s`;
}