Pathora_Colposcopy_Assistant / src /pages /GuidedCapturePage.tsx
nusaibah0110's picture
Fix CORS errors by replacing hardcoded localhost URLs with relative API paths
b0950ec
import { useState, useEffect, useRef } from 'react';
import { Camera, Video, ArrowLeft, ArrowRight, CheckCircle2, Info, Pause, X, Edit2, RotateCcw, FileText, Sparkles, Upload } from 'lucide-react';
import { ImageAnnotator, type ImageAnnotatorHandle } from '../components/ImageAnnotator';
import { AceticAnnotator, type AceticAnnotatorHandle } from '../components/AceticAnnotator';
import { ImagingObservations } from '../components/ImagingObservations';
import { AceticFindingsForm } from '../components/AceticFindingsForm';
import { BiopsyMarking, type BiopsyCapturedImage } from './BiopsyMarking';
import { Compare } from './Compare';
import { ReportPage } from './ReportPage';
import { applyGreenFilter } from '../utils/filterUtils';
import { sessionStore } from '../store/sessionStore';
// Simple UI Component replacements
const Button: React.FC<any> = ({ children, onClick, disabled, variant, size, className, ...props }) => {
const baseClass = 'inline-flex items-center justify-center font-medium rounded transition-colors';
const variantClass = variant === 'ghost' ? 'hover:bg-gray-200 text-gray-700' : variant === 'outline' ? 'border border-gray-300 hover:bg-gray-50' : 'bg-blue-600 text-white hover:bg-blue-700';
const sizeClass = size === 'sm' ? 'px-2 py-1 text-sm' : 'px-4 py-2';
return <button className={`${baseClass} ${variantClass} ${sizeClass} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className || ''}`} onClick={onClick} disabled={disabled} {...props}>{children}</button>;
};
type ExamStep = 'native' | 'acetowhite' | 'greenFilter' | 'lugol' | 'biopsyMarking' | 'report';
type CapturedItem = {
id: string;
type: 'image' | 'video';
url: string;
originalUrl: string;
timestamp: Date;
annotations?: any[];
observations?: any;
};
type Props = {
onNext: () => void;
onGoToPatientRecords?: () => void;
initialMode?: 'capture' | 'annotation' | 'compare' | 'report';
onCapturedImagesChange?: (images: any[]) => void;
onModeChange?: (mode: 'capture' | 'annotation' | 'compare' | 'report') => void;
};
export function GuidedCapturePage({ onNext, onGoToPatientRecords, initialMode, onCapturedImagesChange, onModeChange }: Props) {
const imageAnnotatorRef = useRef<ImageAnnotatorHandle>(null);
const aceticAnnotatorRef = useRef<AceticAnnotatorHandle>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [currentStep, setCurrentStep] = useState<ExamStep>('native');
const [capturedItems, setCapturedItems] = useState<Record<ExamStep, CapturedItem[]>>({
native: [],
acetowhite: [],
greenFilter: [],
lugol: [],
biopsyMarking: [],
report: []
});
const [isRecording, setIsRecording] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [_annotations, setAnnotations] = useState<any[]>([]);
const [_observations, setObservations] = useState({});
const [isAnnotatingMode, setIsAnnotatingMode] = useState(false);
const [isCompareMode, setIsCompareMode] = useState(false);
const [liveAIResults, setLiveAIResults] = useState<{ cervixDetected: boolean; detectionConfidence: number; quality: string; qualityConfidence: number } | null>(null);
const [liveAIError, setLiveAIError] = useState<string | null>(null);
const [isContinuousAIEnabled, setIsContinuousAIEnabled] = useState(false);
const continuousAIIntervalRef = useRef<NodeJS.Timeout | null>(null);
const audibleAlert = true;
// Timer states for Acetowhite step
const [timerStarted, setTimerStarted] = useState(false);
const [seconds, setSeconds] = useState(0);
const [aceticApplied, setAceticApplied] = useState(false);
const [showFlash, setShowFlash] = useState(false);
const [timerPaused, setTimerPaused] = useState(false);
// Green filter state
const [greenApplied, setGreenApplied] = useState(false);
// Timer states for Lugol step
const [lugolTimerStarted, setLugolTimerStarted] = useState(false);
const [lugolSeconds, setLugolSeconds] = useState(0);
const [lugolApplied, setLugolApplied] = useState(false);
const [lugolShowFlash, setLugolShowFlash] = useState(false);
const [lugolTimerPaused, setLugolTimerPaused] = useState(false);
// Placeholder image URL for demo
const baseImageUrl = "/C87Aceto_(1).jpg";
const greenImageUrl = "/greenC87Aceto_(1).jpg";
const liveFeedImageUrl = currentStep === 'greenFilter' && greenApplied ? greenImageUrl : baseImageUrl;
// Timer effect for Acetowhite step
useEffect(() => {
if (!timerStarted || !aceticApplied || currentStep !== 'acetowhite' || timerPaused) return;
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [timerStarted, aceticApplied, currentStep, timerPaused]);
// Timer effect for Lugol step
useEffect(() => {
if (!lugolTimerStarted || !lugolApplied || currentStep !== 'lugol' || lugolTimerPaused) return;
const interval = setInterval(() => {
setLugolSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [lugolTimerStarted, lugolApplied, currentStep, lugolTimerPaused]);
// Check for 1 minute and 3 minute marks for Lugol
useEffect(() => {
if (currentStep !== 'lugol') return;
if (lugolSeconds === 60) {
setLugolShowFlash(true);
if (audibleAlert) {
console.log('BEEP - 1 minute mark');
}
setTimeout(() => setLugolShowFlash(false), 3000);
} else if (lugolSeconds === 180) {
setLugolShowFlash(true);
if (audibleAlert) {
console.log('BEEP - 3 minute mark');
}
setTimeout(() => setLugolShowFlash(false), 3000);
}
}, [lugolSeconds, audibleAlert, currentStep]);
// Check for 1 minute and 3 minute marks for Acetowhite
useEffect(() => {
if (currentStep !== 'acetowhite') return;
if (seconds === 60) {
setShowFlash(true);
if (audibleAlert) {
console.log('BEEP - 1 minute mark');
}
setTimeout(() => setShowFlash(false), 3000);
} else if (seconds === 180) {
setShowFlash(true);
if (audibleAlert) {
console.log('BEEP - 3 minute mark');
}
setTimeout(() => setShowFlash(false), 3000);
}
}, [seconds, audibleAlert, currentStep]);
const formatTime = (totalSeconds: number) => {
const mins = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// Reset annotation mode and clear selections when changing steps
useEffect(() => {
setSelectedImage(null);
setAnnotations([]);
setObservations({});
// Clear AI Assist results when changing steps
setLiveAIResults(null);
setLiveAIError(null);
setIsContinuousAIEnabled(false);
// Reset timer when leaving acetowhite step
if (currentStep !== 'acetowhite') {
setTimerStarted(false);
setSeconds(0);
setAceticApplied(false);
setShowFlash(false);
setTimerPaused(false);
}
// Reset green filter when leaving greenFilter step
if (currentStep !== 'greenFilter') {
setGreenApplied(false);
}
}, [currentStep]);
// Set initial mode based on initialMode prop
useEffect(() => {
if (initialMode) {
switch (initialMode) {
case 'capture':
setIsAnnotatingMode(false);
setIsCompareMode(false);
setSelectedImage(null);
break;
case 'annotation':
// Don't allow annotation mode for biopsy marking - switch to native step
if (currentStep === 'biopsyMarking') {
setCurrentStep('native');
}
setIsAnnotatingMode(true);
setIsCompareMode(false);
break;
case 'compare':
setIsCompareMode(true);
setIsAnnotatingMode(false);
break;
case 'report':
setCurrentStep('report');
break;
}
}
}, [initialMode]);
// Continuous AI Quality Checking on Live Feed
useEffect(() => {
// Compute selectedItem inline to avoid forward reference
const computedSelectedItem = selectedImage
? capturedItems[currentStep].find(item => item.id === selectedImage)
: null;
if (!isContinuousAIEnabled || !videoRef.current || !canvasRef.current || computedSelectedItem || isAnnotatingMode || isCompareMode) {
// Stop the interval if conditions are not met
if (continuousAIIntervalRef.current) {
clearInterval(continuousAIIntervalRef.current);
continuousAIIntervalRef.current = null;
}
return;
}
const checkFrameQuality = async () => {
try {
const canvas = canvasRef.current;
const video = videoRef.current;
if (!canvas || !video) return;
const ctx = canvas.getContext('2d');
const vw = video.videoWidth;
const vh = video.videoHeight;
if (!ctx || vw <= 0 || vh <= 0) return;
canvas.width = vw;
canvas.height = vh;
ctx.drawImage(video, 0, 0);
canvas.toBlob(async (blob) => {
if (!blob) return;
try {
const formData = new FormData();
formData.append('file', blob, 'frame.jpg');
const response = await fetch('/infer/image', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Backend error: ${response.statusText}`);
}
const result = await response.json();
const qualityScore = typeof result.quality_score === 'number'
? result.quality_score
: (typeof result.quality_percent === 'number' ? result.quality_percent / 100 : 0);
const detectionConf = typeof result.detection_confidence === 'number'
? result.detection_confidence
: 0;
setLiveAIResults({
cervixDetected: Boolean(result.detected),
detectionConfidence: detectionConf,
quality: mapQualityLabel(qualityScore),
qualityConfidence: qualityScore
});
setLiveAIError(null);
} catch (error) {
console.error('Continuous AI quality check error:', error);
// Don't set error for continuous checks to avoid cluttering UI
}
}, 'image/jpeg', 0.7);
} catch (error) {
console.error('Frame capture error:', error);
}
};
// Start continuous checking with 1-second interval
continuousAIIntervalRef.current = setInterval(checkFrameQuality, 1000);
return () => {
if (continuousAIIntervalRef.current) {
clearInterval(continuousAIIntervalRef.current);
continuousAIIntervalRef.current = null;
}
};
}, [isContinuousAIEnabled, selectedImage, isAnnotatingMode, isCompareMode, currentStep, capturedItems]);
// Cleanup continuous AI on unmount
useEffect(() => {
return () => {
if (continuousAIIntervalRef.current) {
clearInterval(continuousAIIntervalRef.current);
continuousAIIntervalRef.current = null;
}
};
}, []);
const handleAceticApplied = () => {
setAceticApplied(true);
setTimerStarted(true);
setSeconds(0);
};
const handleRestartTimer = () => {
setSeconds(0);
setTimerStarted(false);
setAceticApplied(false);
setShowFlash(false);
setTimerPaused(false);
};
const handleLugolApplied = () => {
setLugolApplied(true);
setLugolTimerStarted(true);
setLugolSeconds(0);
};
const handleLugolRestartTimer = () => {
setLugolSeconds(0);
setLugolTimerStarted(false);
setLugolApplied(false);
setLugolShowFlash(false);
setLugolTimerPaused(false);
};
const steps: { key: ExamStep; label: string; stepNum: number }[] = [
{ key: 'native', label: 'Native', stepNum: 1 },
{ key: 'acetowhite', label: 'Acetic Acid', stepNum: 2 },
{ key: 'greenFilter', label: 'Green Filter', stepNum: 3 },
{ key: 'lugol', label: 'Lugol', stepNum: 4 },
{ key: 'biopsyMarking', label: 'Biopsy Marking', stepNum: 5 }
];
const handleCaptureImage = async () => {
let rawUrl: string;
let displayUrl: string;
if (selectedItem) {
// Capturing a selected image - use its original and displayed URLs
displayUrl = selectedItem.url;
rawUrl = selectedItem.originalUrl || selectedItem.url;
} else {
// Capturing from live feed - grab video frame from canvas
rawUrl = liveFeedImageUrl;
if (videoRef.current && canvasRef.current) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const vw = videoRef.current.videoWidth;
const vh = videoRef.current.videoHeight;
if (ctx && vw > 0 && vh > 0) {
canvas.width = vw;
canvas.height = vh;
ctx.drawImage(videoRef.current, 0, 0);
try {
rawUrl = canvas.toDataURL('image/png');
} catch (secErr) {
console.warn('Canvas capture failed, using fallback', secErr);
rawUrl = liveFeedImageUrl;
}
}
}
displayUrl = rawUrl;
// Apply green filter only for green filter step
if (currentStep === 'greenFilter' && greenApplied && rawUrl) {
try {
displayUrl = await applyGreenFilter(rawUrl, 'dataUrl') as string;
} catch {
displayUrl = rawUrl;
}
}
}
const newCapture: CapturedItem = {
id: Date.now().toString(),
type: 'image',
url: displayUrl,
originalUrl: rawUrl,
timestamp: new Date()
};
setCapturedItems(prev => ({
...prev,
[currentStep]: [...prev[currentStep], newCapture]
}));
};
const handleToggleRecording = async () => {
if (!isRecording) {
setIsRecording(true);
return;
}
setIsRecording(false);
let rawUrl: string;
let displayUrl: string;
if (selectedItem) {
displayUrl = selectedItem.url;
rawUrl = selectedItem.originalUrl || selectedItem.url;
} else {
rawUrl = liveFeedImageUrl;
if (videoRef.current && canvasRef.current) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const vw = videoRef.current.videoWidth;
const vh = videoRef.current.videoHeight;
if (ctx && vw > 0 && vh > 0) {
canvas.width = vw;
canvas.height = vh;
ctx.drawImage(videoRef.current, 0, 0);
try {
rawUrl = canvas.toDataURL('image/png');
} catch (secErr) {
console.warn('Canvas capture failed, using fallback', secErr);
}
}
}
displayUrl = rawUrl;
if (currentStep === 'greenFilter' && greenApplied && rawUrl) {
try {
displayUrl = await applyGreenFilter(rawUrl, 'dataUrl') as string;
} catch {
displayUrl = rawUrl;
}
}
}
const newCapture: CapturedItem = {
id: Date.now().toString(),
type: 'video',
url: displayUrl,
originalUrl: rawUrl,
timestamp: new Date()
};
setCapturedItems(prev => ({
...prev,
[currentStep]: [...prev[currentStep], newCapture]
}));
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
Array.from(files).forEach(async (file) => {
const isVideo = file.type.startsWith('video/');
const objectUrl = URL.createObjectURL(file);
if (isVideo) {
const newCapture: CapturedItem = {
id: Date.now().toString() + Math.random(),
type: 'video',
url: objectUrl,
originalUrl: objectUrl,
timestamp: new Date()
};
console.log('File uploaded:', { name: file.name, type: file.type, isVideo, id: newCapture.id });
setCapturedItems(prev => ({
...prev,
[currentStep]: [...prev[currentStep], newCapture]
}));
} else {
// For images, always store raw objectUrl as originalUrl
let displayUrl = objectUrl;
if (currentStep === 'greenFilter' && greenApplied) {
try {
displayUrl = await applyGreenFilter(objectUrl, 'dataUrl') as string;
} catch (error) {
console.error('Error applying filter:', error);
displayUrl = objectUrl;
}
}
const newCapture: CapturedItem = {
id: Date.now().toString() + Math.random(),
type: 'image',
url: displayUrl,
originalUrl: objectUrl,
timestamp: new Date()
};
console.log('File uploaded:', { name: file.name, type: file.type, isVideo, id: newCapture.id });
setCapturedItems(prev => ({
...prev,
[currentStep]: [...prev[currentStep], newCapture]
}));
}
});
}
e.target.value = '';
};
const mapQualityLabel = (score: number) => {
if (score >= 0.8) return 'Excellent';
if (score >= 0.6) return 'Good';
return 'Bad';
};
const handleMainAIAssist = () => {
// Toggle continuous AI assist for live feed (now as a toggle button)
if (isContinuousAIEnabled) {
setIsContinuousAIEnabled(false);
setLiveAIError(null);
setLiveAIResults(null);
} else {
setIsContinuousAIEnabled(true);
setLiveAIError(null);
setLiveAIResults(null);
}
};
// Native Annotation Page - Cervix Bounding Box Detection
const handleNativeAnnotationAIAssist = async () => {
if (!imageAnnotatorRef.current) return;
try {
// Get the current image URL from the annotator
const imageUrls = imageCaptures.map(item => item.url);
if (imageUrls.length === 0) {
console.warn('⚠️ No images available for cervix detection');
return;
}
console.log('🔄 Starting cervix bounding box detection...');
// Get the current image (first one or from annotator state)
const currentImageUrl = imageUrls[0];
// Fetch the image and convert to blob
const response = await fetch(currentImageUrl);
const blob = await response.blob();
console.log(`✅ Image loaded, size: ${blob.size} bytes`);
// Prepare form data for backend
const formData = new FormData();
formData.append('file', blob, 'image.jpg');
console.log('🔄 Sending request to backend...');
// Call cervix bbox detection endpoint
const backendResponse = await fetch('/api/infer-cervix-bbox', {
method: 'POST',
body: formData,
});
if (!backendResponse.ok) {
throw new Error(`Backend error: ${backendResponse.statusText}`);
}
const result = await backendResponse.json();
console.log('✅ Backend response received:', result);
// Convert bounding boxes to annotation format
if (result.bounding_boxes && result.bounding_boxes.length > 0) {
console.log(`📦 Found ${result.bounding_boxes.length} cervix bounding box(es)`);
const aiAnnotations: any[] = result.bounding_boxes.map((bbox: any, idx: number) => ({
id: `ai-cervix-${Date.now()}-${idx}`,
type: 'rect',
x: bbox.x1,
y: bbox.y1,
width: bbox.width,
height: bbox.height,
color: '#FF6B6B',
label: `Cervix (${(bbox.confidence * 100).toFixed(1)}%)`,
source: 'ai',
identified: true,
accepted: false
}));
console.log('🎨 Adding annotations to canvas:', aiAnnotations);
// Add AI annotations to the image annotator
imageAnnotatorRef.current.addAIAnnotations(aiAnnotations);
console.log('✅ Cervix bounding boxes detected and displayed:', result.detections);
} else {
console.warn('⚠️ No cervix detected in image');
}
} catch (error) {
console.error('❌ Native annotation AI assist error:', error);
}
};
const handleDeleteCapture = (id: string) => {
const newItems = capturedItems[currentStep].filter(item => item.id !== id);
setCapturedItems(prev => ({
...prev,
[currentStep]: newItems
}));
if (selectedImage === id) {
setSelectedImage(null);
}
};
const selectedItem = selectedImage
? capturedItems[currentStep].find(item => item.id === selectedImage)
: null;
// Debug: Log when selectedItem changes
useEffect(() => {
if (selectedItem) {
console.log('Selected item:', { id: selectedItem.id, type: selectedItem.type, url: selectedItem.url });
} else {
console.log('No item selected');
}
}, [selectedItem]);
const totalCaptures = capturedItems[currentStep].length;
const imageCaptures = capturedItems[currentStep].filter(item => item.type === 'image');
const videoCaptures = capturedItems[currentStep].filter(item => item.type === 'video');
const biopsyCapturedImages: BiopsyCapturedImage[] = Object.entries(capturedItems).flatMap(([stepId, items]) =>
items.map(item => ({ id: item.id, src: item.url, stepId, type: item.type }))
);
// Notify parent component when captured images change
useEffect(() => {
if (onCapturedImagesChange) {
onCapturedImagesChange(biopsyCapturedImages);
}
}, [capturedItems, onCapturedImagesChange]);
// Initialize session and persist captured images to sessionStore
useEffect(() => {
const session = sessionStore.get();
if (!session.sessionStarted) {
sessionStore.merge({ sessionStarted: new Date().toISOString() });
}
}, []);
// Notify parent component when mode changes
useEffect(() => {
if (onModeChange) {
if (currentStep === 'report') {
onModeChange('report');
} else if (isCompareMode) {
onModeChange('compare');
} else if (isAnnotatingMode) {
onModeChange('annotation');
} else {
onModeChange('capture');
}
}
}, [currentStep, isCompareMode, isAnnotatingMode, onModeChange]);
const patientId = sessionStore.get().patientInfo?.id;
return (
<div className="w-full bg-white/95 relative">
<div className="relative z-10 py-4 md:py-6 lg:py-8">
<div className="w-full max-w-7xl mx-auto px-4 md:px-6">
{/* Patient ID Badge */}
{patientId && (
<div className="mb-4 flex justify-end">
<div className="flex items-center gap-2 px-3 md:px-4 py-1.5 md:py-2 bg-teal-50 border border-teal-200 rounded-full text-xs md:text-sm font-mono font-semibold text-[#0A2540]">
<span>Patient ID:</span>
<span>{patientId}</span>
</div>
</div>
)}
{/* Page Header */}
{currentStep !== 'report' && (
<div className="mb-4 md:mb-6">
{/* Progress Bar - Capture / Annotate / Compare / Report */}
<div className="mb-4 flex gap-1 md:gap-2">
{['Capture', 'Annotate', 'Compare', 'Report'].map((stage, idx) => (
<div key={stage} className="flex items-center flex-1">
<div
className={`flex-1 py-2 px-2 md:px-3 rounded-3xl font-medium text-sm md:text-base transition-all border-2 border-[#05998c] cursor-default pointer-events-none ${
(stage === 'Capture' && !selectedImage && !isAnnotatingMode && !isCompareMode) ||
(stage === 'Annotate' && (selectedImage || isAnnotatingMode) && !isCompareMode) ||
(stage === 'Compare' && isCompareMode)
? 'bg-[#05998c] text-white shadow-md'
: 'bg-gray-100 text-gray-600'
}`}
>
<span className="flex items-center justify-center gap-2">
{stage === 'Capture' && <Camera className="w-4 h-4" />}
{stage === 'Annotate' && <Edit2 className="w-4 h-4" />}
{stage === 'Compare' && <img src="/arrow.png" alt="Compare" className="w-6 h-6 brightness-0 opacity-70" />}
{stage === 'Report' && <FileText className="w-4 h-4" />}
<span>{stage}</span>
</span>
</div>
{idx < 3 && <div className="w-1.5 h-1.5 rounded-full bg-gray-300 mx-1" />}
</div>
))}
</div>
{/* Step Navigation */}
{!isCompareMode && (
<div className="flex gap-2 flex-wrap">
{steps.map(step => (
<button
key={step.key}
onClick={() => {
setCurrentStep(step.key);
// Exit annotation/compare modes when clicking Biopsy Marking
if (step.key === 'biopsyMarking') {
setIsAnnotatingMode(false);
setIsCompareMode(false);
setSelectedImage(null);
}
}}
className={`relative px-5 py-3 rounded-lg font-medium text-base transition-all ${
currentStep === step.key && !isCompareMode
? 'bg-[#05998c] text-white shadow-md'
: 'bg-white text-gray-600 border border-gray-200 hover:border-[#05998c]'
}`}
>
<div className="flex items-center gap-1.5">
{capturedItems[step.key].length > 0 && (
<CheckCircle2 className="w-3 h-3" />
)}
<span>{step.label}</span>
</div>
</button>
))}
</div>
)}
</div>
)}
{/* Back and Next Navigation for Guided Capture Steps */}
{currentStep !== 'biopsyMarking' && currentStep !== 'report' && !isAnnotatingMode && !isCompareMode && (
<div className="flex items-center gap-3 px-4 py-2.5 bg-white border-b border-slate-200 shadow-sm mb-4">
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-slate-700"
onClick={() => {
const currentIndex = steps.findIndex(s => s.key === currentStep);
if (currentIndex > 0) {
setCurrentStep(steps[currentIndex - 1].key);
}
}}
disabled={steps.findIndex(s => s.key === currentStep) === 0}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<h2 className="text-lg font-semibold text-slate-800 flex-1">
{steps.find(s => s.key === currentStep)?.label || 'Guided Capture'}
</h2>
<button
onClick={handleMainAIAssist}
disabled={false}
className={`px-6 py-3 rounded-lg transition-all font-semibold flex items-center justify-center gap-2 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed min-w-max ${
isContinuousAIEnabled
? 'bg-gradient-to-r from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 animate-pulse'
: 'bg-gradient-to-r from-blue-600 to-blue-700 text-white hover:from-blue-700 hover:to-blue-800'
}`}
title={isContinuousAIEnabled ? 'Continuous AI quality checking is ON - Click to turn OFF' : 'Enable continuous AI quality checking for live feed'}
>
<div className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${isContinuousAIEnabled ? 'bg-white' : 'bg-gray-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-gradient-to-br transition-transform ${isContinuousAIEnabled ? 'translate-x-6 from-green-400 to-green-600' : 'translate-x-1 from-blue-400 to-blue-600'}`} />
</div>
<Sparkles className="h-5 w-5" />
{isContinuousAIEnabled ? 'AI Live ✓' : 'AI Assist'}
</button>
<div className="flex items-center gap-2">
<button
onClick={handleUploadClick}
className="h-8 px-3 bg-[#05998c] text-white hover:bg-[#047569] rounded transition-colors flex items-center gap-2"
>
<Upload className="h-4 w-4" />
Upload
</button>
</div>
<button
onClick={() => {
const currentIndex = steps.findIndex(s => s.key === currentStep);
if (currentIndex < steps.length - 1) {
setCurrentStep(steps[currentIndex + 1].key);
}
}}
className="h-8 px-3 bg-gray-600 text-white hover:bg-slate-700 rounded transition-colors flex items-center gap-2"
disabled={steps.findIndex(s => s.key === currentStep) === steps.length - 1}
>
Next
<ArrowRight className="h-4 w-4" />
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*,video/*"
multiple
className="hidden"
onChange={handleFileUpload}
/>
</div>
)}
{currentStep === 'report' ? (
<ReportPage
onBack={() => {
setCurrentStep('biopsyMarking');
setIsCompareMode(true);
}}
onNext={onNext}
onGoToPatientRecords={onGoToPatientRecords}
capturedImages={biopsyCapturedImages}
/>
) : currentStep === 'biopsyMarking' && !isCompareMode && !isAnnotatingMode ? (
<BiopsyMarking
onBack={() => setCurrentStep('lugol')}
onNext={() => {
setIsCompareMode(true);
setIsAnnotatingMode(false);
setSelectedImage(null);
}}
capturedImages={biopsyCapturedImages}
/>
) : isCompareMode ? (
<Compare
onBack={() => {
setIsCompareMode(false);
setIsAnnotatingMode(false);
setSelectedImage(null);
}}
onNext={() => {
setCurrentStep('report');
setIsCompareMode(false);
setIsAnnotatingMode(false);
setSelectedImage(null);
}}
capturedImages={biopsyCapturedImages}
/>
) : isAnnotatingMode ? (
// Multi-Image Annotation Mode
<div>
<div className="mb-4 flex items-center justify-between">
<button
onClick={() => {
setIsAnnotatingMode(false);
}}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Live Feed
</button>
</div>
<div>
{currentStep === 'acetowhite' ? (
<AceticAnnotator
ref={aceticAnnotatorRef}
imageUrls={imageCaptures.map(item => item.url)}
onAnnotationsChange={setAnnotations}
/>
) : (
<ImageAnnotator
ref={imageAnnotatorRef}
imageUrls={imageCaptures.map(item => item.url)}
onAnnotationsChange={setAnnotations}
onAIAssist={handleNativeAnnotationAIAssist}
/>
)}
</div>
</div>
) : (
// Live Feed View
<>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
{/* Main Live Feed */}
<div className="lg:col-span-2">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
{/* Live Video Feed */}
<div className="relative bg-gray-900 rounded-xl overflow-hidden shadow-2xl border-2 border-gray-700 mb-4">
<div className="aspect-video flex items-center justify-center">
{selectedItem ? (
selectedItem.type === 'video' ? (
<video
key={selectedItem.id}
src={selectedItem.url}
controls
autoPlay
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
onError={(e) => console.error('Video playback error:', e)}
onLoadedMetadata={() => console.log('Video loaded:', selectedItem.url)}
/>
) : (
<img
src={selectedItem.url}
alt="Selected capture"
className="w-full h-full object-contain"
onError={(e) => console.error('Image load error:', e)}
onLoad={() => console.log('Image loaded:', selectedItem.url)}
/>
)
) : (
<>
<video
ref={videoRef}
src="/live.mp4"
autoPlay
loop
muted
crossOrigin="anonymous"
className="w-full h-full object-cover"
style={currentStep === 'greenFilter' && greenApplied ? { filter: 'saturate(0.3) hue-rotate(120deg) brightness(1.1)' } : {}}
/>
{currentStep === 'greenFilter' && greenApplied && (
<div className="absolute inset-0 bg-green-500 opacity-5 pointer-events-none" />
)}
</>
)}
{!selectedItem && (
<div className="absolute top-4 left-4 flex items-center gap-2 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
<div className={`w-2 h-2 rounded-full ${isRecording ? 'bg-white animate-pulse' : 'bg-white/70'}`} />
{isRecording ? 'Recording' : 'Live'}
</div>
)}
{selectedItem && (
<>
<button
onClick={() => {
console.log('Back to live feed clicked');
setSelectedImage(null);
}}
className="absolute top-4 left-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back to Live Feed
</button>
{selectedItem.type === 'image' && (
<button
onClick={handleCaptureImage}
className="absolute top-4 right-4 bg-[#05998c] text-white px-4 py-2 rounded-lg hover:bg-[#047569] transition-colors flex items-center gap-2 shadow-md font-semibold"
>
<Camera className="w-4 h-4" />
Capture This Image
</button>
)}
</>
)}
{!selectedItem && currentStep === 'acetowhite' && aceticApplied && (
<div className="absolute top-4 right-4 bg-black/70 text-white px-4 py-2 rounded-lg">
<p className="text-2xl font-mono font-bold">{formatTime(seconds)}</p>
</div>
)}
{!selectedItem && currentStep === 'lugol' && lugolApplied && (
<div className="absolute top-4 right-4 bg-black/70 text-white px-4 py-2 rounded-lg">
<p className="text-2xl font-mono font-bold">{formatTime(lugolSeconds)}</p>
</div>
)}
{!selectedItem && currentStep === 'acetowhite' && showFlash && (
<div className="absolute inset-0 bg-[#05998c]/30 animate-pulse flex items-center justify-center">
<div className="bg-white/90 px-6 py-4 rounded-lg">
<p className="text-2xl font-bold text-[#0A2540]">
{seconds >= 180 ? '3 Minutes!' : '1 Minute!'}
</p>
</div>
</div>
)}
{!selectedItem && currentStep === 'lugol' && lugolShowFlash && (
<div className="absolute inset-0 bg-[#05998c]/30 animate-pulse flex items-center justify-center">
<div className="bg-white/90 px-6 py-4 rounded-lg">
<p className="text-2xl font-bold text-[#0A2540]">
{lugolSeconds >= 180 ? '3 Minutes!' : '1 Minute!'}
</p>
</div>
</div>
)}
</div>
</div>
{totalCaptures === 0 && (
<div className="mt-4 flex items-center gap-2 text-sm text-amber-600 bg-amber-50 px-4 py-2 rounded-lg">
<Info className="w-4 h-4" />
<span>{(currentStep === 'acetowhite' && !timerStarted) || (currentStep === 'lugol' && !lugolTimerStarted) ? (currentStep === 'acetowhite' ? 'Apply acetic acid to start' : 'Apply Lugol iodine to start') : 'Capture Required'}</span>
</div>
)}
{/* Quality Results Display */}
{liveAIResults && !isAnnotatingMode && !isCompareMode && (
<div className={`mt-4 p-3 rounded-lg border-2 transition-all ${
isContinuousAIEnabled
? 'bg-gradient-to-r from-green-50 to-emerald-50 border-green-300'
: 'bg-green-50 border-green-200'
}`}>
<div className="flex items-start gap-2">
<div className="flex-1">
<h3 className="font-semibold text-green-900 mb-2 text-sm flex items-center gap-2">
{isContinuousAIEnabled ? '🎥 Quality Check' : 'Quality Check'}
{isContinuousAIEnabled && <span className="inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>}
</h3>
<div className="space-y-2">
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-sm text-gray-600 font-medium">Cervix</p>
<p className="text-sm font-semibold text-green-700">
{liveAIResults.cervixDetected ? '✓' : '✗'} ({(liveAIResults.detectionConfidence * 100).toFixed(0)}%)
</p>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-sm text-gray-600 font-medium">Quality</p>
<p className="text-sm font-semibold">
{liveAIResults.quality} ({(liveAIResults.qualityConfidence * 100).toFixed(0)}%)
</p>
</div>
{/* Progress Bar */}
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden shadow-inner">
<div
className={`h-full rounded-full transition-all duration-500 ${
liveAIResults.qualityConfidence >= 0.65
? 'bg-gradient-to-r from-green-500 to-green-600'
: liveAIResults.qualityConfidence >= 0.5
? 'bg-gradient-to-r from-orange-400 to-orange-500'
: 'bg-gradient-to-r from-red-500 to-red-600'
}`}
style={{ width: `${Math.min(liveAIResults.qualityConfidence * 100, 100)}%` }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{liveAIError && !isAnnotatingMode && !isCompareMode && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-700 font-medium text-sm">{liveAIError}</p>
</div>
)}
</div>
</div>
{/* Sidebar - Capture Controls and Media */}
<div className="lg:col-span-1">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
{/* Acetowhite Timer Section */}
{currentStep === 'acetowhite' && !timerStarted && (
<div className="mb-4">
{/* Apply Acetic Acid Message Box */}
<div className="bg-cyan-500 rounded-lg p-4 mb-4 shadow-md">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center">
<Info className="w-6 h-6 text-teal-600" />
</div>
<div>
<p className="text-white font-bold text-lg">Apply Acetic Acid Now</p>
<p className="text-teal-100 text-sm">Apply 3-5% acetic acid to the cervix</p>
</div>
</div>
</div>
<button
onClick={handleAceticApplied}
className="w-full px-6 py-3 bg-[#05998c] text-white rounded-lg font-semibold hover:bg-[#047569] transition-all shadow-md hover:shadow-lg"
>
Acetic acid applied — Start timer
</button>
</div>
)}
{currentStep === 'acetowhite' && timerStarted && (
<div className={`mb-4 rounded-lg p-4 border-2 transition-all ${
seconds < 50
? 'bg-gradient-to-r from-[#05998c]/10 to-[#0A2540]/10 border-[#05998c]'
: seconds < 55
? 'bg-red-50 border-red-200'
: seconds < 60
? 'bg-red-100 border-red-300'
: seconds >= 60 && seconds <= 60
? 'bg-red-200 border-red-400'
: seconds > 60 && seconds < 170
? 'bg-gradient-to-r from-green-50 to-green-50 border-green-300'
: seconds < 175
? 'bg-red-50 border-red-200'
: seconds < 180
? 'bg-red-100 border-red-300'
: 'bg-red-200 border-red-400'
}`}>
<div className="flex flex-col gap-3">
<div>
<p className="text-sm text-gray-600 mb-1">Timer</p>
<p className={`text-4xl font-bold font-mono ${
seconds >= 180 ? 'text-red-600' :
seconds >= 60 ? 'text-amber-500' :
seconds >= 50 ? 'text-amber-400' :
'text-[#0A2540]'
}`}>{formatTime(seconds)}</p>
{seconds >= 60 && seconds < 180 && (
<p className="text-sm text-amber-600 mt-1">Approaching 3-minute mark...</p>
)}
{seconds >= 180 && (
<p className="text-sm text-green-600 mt-1">3-minute observation period complete</p>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => setTimerPaused(!timerPaused)}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-white border-2 border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
{timerPaused ? (
<>
<Video className="w-4 h-4" />
<span className="text-sm font-medium">Play</span>
</>
) : (
<>
<Pause className="w-4 h-4" />
<span className="text-sm font-medium">Pause</span>
</>
)}
</button>
<button
onClick={handleRestartTimer}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-white border-2 border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<RotateCcw className="w-4 h-4" />
<span className="text-sm font-medium">Restart</span>
</button>
</div>
</div>
{seconds === 60 && (
<div className="mt-3 bg-amber-100 border border-amber-300 rounded-lg p-3">
<p className="text-amber-800 font-semibold text-sm">⏰ 1-minute mark - Capture recommended!</p>
</div>
)}
{seconds === 180 && (
<div className="mt-3 bg-green-100 border border-green-300 rounded-lg p-3">
<p className="text-green-800 font-semibold text-sm">⏰ 3-minute mark - Final capture recommended!</p>
</div>
)}
</div>
)}
{/* Green Filter Toggle */}
{currentStep === 'greenFilter' && (
<div className="mb-4 flex items-center justify-between gap-4">
<div className="flex-1 bg-[#05998c] text-white px-6 py-2 rounded-lg">
<span className="font-bold">Green Filter</span>
</div>
<button
onClick={() => setGreenApplied(prev => !prev)}
className={`relative inline-flex h-8 w-16 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
greenApplied ? 'bg-blue-500' : 'bg-gray-300'
}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform ${
greenApplied ? 'translate-x-9' : 'translate-x-1'
}`}
/>
<span className={`absolute text-xs font-semibold ${
greenApplied ? 'left-2 text-white' : 'right-2 text-gray-600'
}`}>
{greenApplied ? 'ON' : 'OFF'}
</span>
</button>
</div>
)}
{/* Lugol Timer Section */}
{currentStep === 'lugol' && !lugolTimerStarted && (
<div className="mb-4">
{/* Apply Lugol Message Box */}
<div className="bg-gradient-to-r from-yellow-500 to-yellow-600 rounded-lg p-4 mb-4 shadow-md">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center">
<Info className="w-6 h-6 text-yellow-600" />
</div>
<div>
<p className="text-white font-bold text-lg">Apply Lugol Iodine Now</p>
<p className="text-yellow-100 text-sm">Apply Lugol iodine solution to the cervix</p>
</div>
</div>
</div>
<button
onClick={handleLugolApplied}
className="w-full px-6 py-3 bg-[#05998c] text-white rounded-lg font-semibold hover:bg-[#047569] transition-all shadow-md hover:shadow-lg"
>
Lugol applied — Start timer
</button>
</div>
)}
{currentStep === 'lugol' && lugolTimerStarted && (
<div className={`mb-4 rounded-lg p-4 border-2 transition-all ${
lugolSeconds < 50
? 'bg-gradient-to-r from-[#05998c]/10 to-[#0A2540]/10 border-[#05998c]'
: lugolSeconds < 55
? 'bg-red-50 border-red-200'
: lugolSeconds < 60
? 'bg-red-100 border-red-300'
: lugolSeconds >= 60 && lugolSeconds <= 60
? 'bg-red-200 border-red-400'
: lugolSeconds > 60 && lugolSeconds < 170
? 'bg-gradient-to-r from-green-50 to-green-50 border-green-300'
: lugolSeconds < 175
? 'bg-red-50 border-red-200'
: lugolSeconds < 180
? 'bg-red-100 border-red-300'
: 'bg-red-200 border-red-400'
}`}>
<div className="flex flex-col gap-3">
<div>
<p className="text-sm text-gray-600 mb-1">Timer</p>
<p className={`text-4xl font-bold font-mono ${
lugolSeconds >= 180 ? 'text-red-600' :
lugolSeconds >= 60 ? 'text-amber-500' :
lugolSeconds >= 50 ? 'text-amber-400' :
'text-[#0A2540]'
}`}>{formatTime(lugolSeconds)}</p>
{lugolSeconds >= 60 && lugolSeconds < 180 && (
<p className="text-sm text-amber-600 mt-1">Approaching 3-minute mark...</p>
)}
{lugolSeconds >= 180 && (
<p className="text-sm text-green-600 mt-1">3-minute observation period complete</p>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => setLugolTimerPaused(!lugolTimerPaused)}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-white border-2 border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
{lugolTimerPaused ? (
<>
<Video className="w-4 h-4" />
<span className="text-sm font-medium">Play</span>
</>
) : (
<>
<Pause className="w-4 h-4" />
<span className="text-sm font-medium">Pause</span>
</>
)}
</button>
<button
onClick={handleLugolRestartTimer}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-white border-2 border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<RotateCcw className="w-4 h-4" />
<span className="text-sm font-medium">Restart</span>
</button>
</div>
</div>
{lugolSeconds === 60 && (
<div className="mt-3 bg-amber-100 border border-amber-300 rounded-lg p-3">
<p className="text-amber-800 font-semibold text-sm">⏰ 1-minute mark - Capture recommended!</p>
</div>
)}
{lugolSeconds === 180 && (
<div className="mt-3 bg-green-100 border border-green-300 rounded-lg p-3">
<p className="text-green-800 font-semibold text-sm">⏰ 3-minute mark - Final capture recommended!</p>
</div>
)}
</div>
)}
{/* Capture Controls */}
<div className="flex gap-2 mb-4">
<button
onClick={handleCaptureImage}
disabled={(currentStep === 'acetowhite' && !timerStarted) || (currentStep === 'lugol' && !lugolTimerStarted)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg font-semibold transition-colors text-sm ${
(currentStep === 'acetowhite' && showFlash && (seconds === 60 || seconds === 180)) ||
(currentStep === 'lugol' && lugolShowFlash && (lugolSeconds === 60 || lugolSeconds === 180))
? 'bg-[#05998c] text-white animate-pulse'
: 'bg-[#05998c] text-white hover:bg-[#047569]'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<Camera className="w-4 h-4" />
Capture
</button>
<button
onClick={handleToggleRecording}
disabled={(currentStep === 'acetowhite' && !timerStarted) || (currentStep === 'lugol' && !lugolTimerStarted)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg font-semibold transition-colors text-sm ${
isRecording
? 'bg-red-500 text-white hover:bg-red-600'
: 'bg-[#05998c] text-white hover:bg-[#047569]'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isRecording ? <Pause className="w-4 h-4" /> : <Video className="w-4 h-4" />}
{isRecording ? 'Stop' : 'Record'}
</button>
</div>
<h3 className="font-bold text-[#0A2540] mb-4">Captured Media</h3>
{totalCaptures === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Camera className="w-16 h-16 text-gray-300 mb-3" />
<p className="text-gray-500 font-medium">No captures yet</p>
<p className="text-sm text-gray-400 mt-1">Capture images or videos from live feed</p>
</div>
) : (
<div className="space-y-4">
{/* Annotate Images Button */}
<button
onClick={() => {
// Don't allow annotation mode for biopsy marking - switch to native step
if (currentStep === 'biopsyMarking') {
setCurrentStep('native');
}
setIsAnnotatingMode(true);
}}
className="w-full flex items-center justify-center gap-2 px-6 py-3 rounded-lg bg-[#05998c] text-white font-semibold hover:bg-[#047569] transition-colors text-base"
>
<Edit2 className="w-5 h-5" />
Annotate Images
</button>
<div className="space-y-3">
{/* Image Thumbnails */}
{imageCaptures.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-gray-500 uppercase mb-2">Images ({imageCaptures.length})</h4>
<div className="grid grid-cols-2 gap-2">
{imageCaptures.map(item => (
<div key={item.id} className="relative group">
<div
onClick={() => {
console.log('Clicked image thumbnail:', item.id);
setSelectedImage(item.id);
}}
className={`aspect-square bg-gray-100 rounded-lg overflow-hidden border-2 transition-all cursor-pointer hover:border-blue-500 ${
selectedImage === item.id ? 'border-blue-600 ring-2 ring-blue-300' : 'border-gray-200'
}`}
>
<img src={item.url} alt="Capture" className="w-full h-full object-cover" />
{item.annotations && item.annotations.length > 0 && (
<div className="absolute top-1 right-1 bg-green-500 text-white p-1 rounded">
<CheckCircle2 className="w-3 h-3" />
</div>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCapture(item.id);
}}
className="absolute top-1 right-1 bg-red-500 text-white p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
</div>
)}
{/* Video Items */}
{videoCaptures.length > 0 && (
<div className="mt-4">
<h4 className="text-xs font-semibold text-gray-500 uppercase mb-2">Videos ({videoCaptures.length})</h4>
<div className="space-y-2">
{videoCaptures.map(item => (
<div
key={item.id}
onClick={() => {
console.log('Clicked video thumbnail:', item.id);
setSelectedImage(item.id);
}}
className={`relative group rounded-lg p-3 flex items-center gap-3 cursor-pointer transition-all ${
selectedImage === item.id ? 'bg-blue-100 border-2 border-blue-600' : 'bg-gray-50 border-2 border-transparent hover:bg-gray-100'
}`}
>
<div className="w-12 h-12 bg-gray-200 rounded flex items-center justify-center">
<Video className="w-6 h-6 text-gray-500" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-700">Video Recording</p>
<p className="text-xs text-gray-500">{item.timestamp.toLocaleTimeString()}</p>
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCapture(item.id);
}}
className="p-1 hover:bg-red-50 rounded text-red-500"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
{/* Acetic Acid Clinical Findings — shown on acetowhite capture page */}
{currentStep === 'acetowhite' && (
<div className="w-full mt-4">
<AceticFindingsForm />
</div>
)}
{/* Visual Observations - Full Width on Native Step */}
{currentStep === 'native' && (
<div className="w-full">
<ImagingObservations
onObservationsChange={setObservations}
layout="horizontal"
stepId="native"
/>
</div>
)}
</>
)}
</div>
</div>
<canvas ref={canvasRef} className="hidden" />
</div>
);
}