nusaibah0110 commited on
Commit
3454dc0
·
1 Parent(s): bab7e89

Add live video feed, Swede Score assessment, and UI improvements

Browse files
.gitattributes CHANGED
@@ -4,3 +4,4 @@
4
  *.gif filter=lfs diff=lfs merge=lfs -text
5
  *.bmp filter=lfs diff=lfs merge=lfs -text
6
  *.webp filter=lfs diff=lfs merge=lfs -text
 
 
4
  *.gif filter=lfs diff=lfs merge=lfs -text
5
  *.bmp filter=lfs diff=lfs merge=lfs -text
6
  *.webp filter=lfs diff=lfs merge=lfs -text
7
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  # Pathora Colposcopy Assistant
2
 
3
  A React-based colposcopy assistant application with image annotation, patient history tracking, and AI-powered chatbot features.
 
1
+ ---
2
+ title: Pathora Colposcopy Assistant
3
+ emoji: 🔬
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
  # Pathora Colposcopy Assistant
11
 
12
  A React-based colposcopy assistant application with image annotation, patient history tracking, and AI-powered chatbot features.
public/arrow.png ADDED

Git LFS Details

  • SHA256: 0f25131a89c5eef4cd6424acffff2f66f7cef8b4060da191eb01224a11ec9471
  • Pointer size: 131 Bytes
  • Size of remote file: 382 kB
public/live.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d1f8b3b576dbbd578cdeca6685699fc64ba76c4a69d32d28c5beb8f8fc9a74ac
3
+ size 19704307
src/App.tsx CHANGED
@@ -75,17 +75,17 @@ export function App() {
75
  case 'lugol':
76
  return <LugolExamPage goBack={goToGreenFilter} onNext={goToGuidedCapture} />;
77
  case 'guidedcapture':
78
- return <GuidedCapturePage onNext={goToBiopsyMarking} onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
79
  case 'biopsymarking':
80
  return <BiopsyMarking onBack={goToGuidedCapture} onNext={goToReport} capturedImages={capturedImages} />;
81
  case 'capture':
82
- return <GuidedCapturePage onNext={goToBiopsyMarking} initialMode="capture" onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
83
  case 'annotation':
84
- return <GuidedCapturePage onNext={goToBiopsyMarking} initialMode="annotation" onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
85
  case 'compare':
86
- return <GuidedCapturePage onNext={goToBiopsyMarking} initialMode="compare" onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
87
  case 'report':
88
- return <ReportPage onBack={goToCompare} onNext={goToHome} capturedImages={capturedImages} />;
89
  default:
90
  return <div className="p-8">Page "{currentPage}" not implemented yet.</div>;
91
  }
 
75
  case 'lugol':
76
  return <LugolExamPage goBack={goToGreenFilter} onNext={goToGuidedCapture} />;
77
  case 'guidedcapture':
78
+ return <GuidedCapturePage onNext={goToBiopsyMarking} onGoToPatientRecords={goToPatientRegistry} onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
79
  case 'biopsymarking':
80
  return <BiopsyMarking onBack={goToGuidedCapture} onNext={goToReport} capturedImages={capturedImages} />;
81
  case 'capture':
82
+ return <GuidedCapturePage onNext={goToBiopsyMarking} onGoToPatientRecords={goToPatientRegistry} initialMode="capture" onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
83
  case 'annotation':
84
+ return <GuidedCapturePage onNext={goToBiopsyMarking} onGoToPatientRecords={goToPatientRegistry} initialMode="annotation" onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
85
  case 'compare':
86
+ return <GuidedCapturePage onNext={goToBiopsyMarking} onGoToPatientRecords={goToPatientRegistry} initialMode="compare" onCapturedImagesChange={setCapturedImages} onModeChange={setGuidanceMode} />;
87
  case 'report':
88
+ return <ReportPage onBack={goToCompare} onNext={goToHome} onGoToPatientRecords={goToPatientRegistry} capturedImages={capturedImages} />;
89
  default:
90
  return <div className="p-8">Page "{currentPage}" not implemented yet.</div>;
91
  }
src/components/AceticAnnotator.tsx CHANGED
@@ -620,6 +620,9 @@ const AceticAnnotatorComponent = forwardRef<AceticAnnotatorHandle, AceticAnnotat
620
  <>
621
  <button
622
  onClick={() => {
 
 
 
623
  setAnnotationAccepted(prev => ({
624
  ...prev,
625
  [a.id]: true
 
620
  <>
621
  <button
622
  onClick={() => {
623
+ setAnnotations(prev => prev.map(ann =>
624
+ ann.id === a.id ? { ...ann, identified: true } : ann
625
+ ));
626
  setAnnotationAccepted(prev => ({
627
  ...prev,
628
  [a.id]: true
src/components/ImageAnnotator.tsx CHANGED
@@ -551,6 +551,9 @@ const ImageAnnotatorComponent = forwardRef<ImageAnnotatorHandle, ImageAnnotatorP
551
  <>
552
  <button
553
  onClick={() => {
 
 
 
554
  setAnnotationAccepted(prev => ({
555
  ...prev,
556
  [a.id]: true
 
551
  <>
552
  <button
553
  onClick={() => {
554
+ setAnnotations(prev => prev.map(ann =>
555
+ ann.id === a.id ? { ...ann, identified: true } : ann
556
+ ));
557
  setAnnotationAccepted(prev => ({
558
  ...prev,
559
  [a.id]: true
src/components/Sidebar.tsx CHANGED
@@ -9,13 +9,13 @@ type Props = {
9
  };
10
 
11
  export function Sidebar({ current, onNavigate }: Props) {
12
- const [collapsed, setCollapsed] = useState(false);
13
  const items: { key: MenuKey; label: string; icon: React.ReactNode }[] = [
14
  { key: 'home', label: 'Home', icon: <Home className="w-5 h-5" /> },
15
  { key: 'patientinfo', label: 'Patients', icon: <User className="w-5 h-5" /> },
16
  { key: 'capture', label: 'Capture', icon: <Camera className="w-5 h-5" /> },
17
  { key: 'annotation', label: 'Annotate', icon: <Edit2 className="w-5 h-5" /> },
18
- { key: 'compare', label: 'Compare', icon: <Info className="w-5 h-5" /> },
19
  { key: 'report', label: 'Report', icon: <FileText className="w-5 h-5" /> }
20
  ];
21
 
 
9
  };
10
 
11
  export function Sidebar({ current, onNavigate }: Props) {
12
+ const [collapsed, setCollapsed] = useState(true);
13
  const items: { key: MenuKey; label: string; icon: React.ReactNode }[] = [
14
  { key: 'home', label: 'Home', icon: <Home className="w-5 h-5" /> },
15
  { key: 'patientinfo', label: 'Patients', icon: <User className="w-5 h-5" /> },
16
  { key: 'capture', label: 'Capture', icon: <Camera className="w-5 h-5" /> },
17
  { key: 'annotation', label: 'Annotate', icon: <Edit2 className="w-5 h-5" /> },
18
+ { key: 'compare', label: 'Compare', icon: <img src="/arrow.png" alt="Compare" className="w-6 h-6 brightness-0 opacity-70" /> },
19
  { key: 'report', label: 'Report', icon: <FileText className="w-5 h-5" /> }
20
  ];
21
 
src/pages/BiopsyMarking.tsx CHANGED
@@ -37,6 +37,14 @@ interface LesionMark {
37
  color: string;
38
  }
39
 
 
 
 
 
 
 
 
 
40
  const LESION_TYPES = [
41
  { code: 'EC', name: 'Ectopy', color: '#22c55e' },
42
  { code: 'TTZ', name: 'T.Trans Zone', color: '#3b82f6' },
@@ -73,6 +81,13 @@ const BiopsyMarking: React.FC<BiopsyMarkingProps> = ({ onBack, onNext, capturedI
73
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
74
  const [searchQuery, setSearchQuery] = useState('');
75
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
 
 
 
 
 
 
 
76
  const fileInputRef = useRef<HTMLInputElement>(null);
77
  const imageContainerRef = useRef<HTMLDivElement>(null);
78
 
@@ -184,6 +199,60 @@ const BiopsyMarking: React.FC<BiopsyMarkingProps> = ({ onBack, onNext, capturedI
184
 
185
  const overlaySize = overlayScale * 2.5;
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return (
188
  <div className="h-full flex flex-col bg-gradient-to-br from-slate-50 to-slate-100 overflow-hidden">
189
  <div className="flex items-center gap-3 px-4 py-2.5 bg-white border-b border-slate-200 shadow-sm shrink-0">
@@ -216,6 +285,7 @@ const BiopsyMarking: React.FC<BiopsyMarkingProps> = ({ onBack, onNext, capturedI
216
  <Button
217
  size="sm"
218
  className="h-8 px-3 bg-gray-600 text-white hover:bg-slate-700"
 
219
  >
220
  Next
221
  <ArrowRight className="h-4 w-4 ml-1" />
@@ -569,6 +639,107 @@ const BiopsyMarking: React.FC<BiopsyMarkingProps> = ({ onBack, onNext, capturedI
569
  </div>
570
  </div>
571
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  </div>
573
  );
574
  };
 
37
  color: string;
38
  }
39
 
40
+ interface SwedeScoreFeatures {
41
+ acetoUptake: number | null;
42
+ marginsAndSurface: number | null;
43
+ vessels: number | null;
44
+ lesionSize: number | null;
45
+ iodineStaining: number | null;
46
+ }
47
+
48
  const LESION_TYPES = [
49
  { code: 'EC', name: 'Ectopy', color: '#22c55e' },
50
  { code: 'TTZ', name: 'T.Trans Zone', color: '#3b82f6' },
 
81
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
82
  const [searchQuery, setSearchQuery] = useState('');
83
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
84
+ const [swedeScores, setSwedeScores] = useState<SwedeScoreFeatures>({
85
+ acetoUptake: null,
86
+ marginsAndSurface: null,
87
+ vessels: null,
88
+ lesionSize: null,
89
+ iodineStaining: null
90
+ });
91
  const fileInputRef = useRef<HTMLInputElement>(null);
92
  const imageContainerRef = useRef<HTMLDivElement>(null);
93
 
 
199
 
200
  const overlaySize = overlayScale * 2.5;
201
 
202
+ const swedeFeatures = [
203
+ {
204
+ id: 'acetoUptake',
205
+ label: 'Aceto Uptake',
206
+ scores: [
207
+ { value: 0, label: 'Zero or transparent' },
208
+ { value: 1, label: 'Shady, milky (not transparent not opaque)' },
209
+ { value: 2, label: 'Distinct, opaque white' }
210
+ ]
211
+ },
212
+ {
213
+ id: 'marginsAndSurface',
214
+ label: 'Margins & Surface',
215
+ scores: [
216
+ { value: 0, label: 'Diffuse' },
217
+ { value: 1, label: 'Sharp but irregular, jagged, "geographical" satellites' },
218
+ { value: 2, label: 'Sharp and even, difference in surface level "cuffing"' }
219
+ ]
220
+ },
221
+ {
222
+ id: 'vessels',
223
+ label: 'Vessels',
224
+ scores: [
225
+ { value: 0, label: 'Fine, regular' },
226
+ { value: 1, label: 'Absent' },
227
+ { value: 2, label: 'Coarse or atypical' }
228
+ ]
229
+ },
230
+ {
231
+ id: 'lesionSize',
232
+ label: 'Lesion Size',
233
+ scores: [
234
+ { value: 0, label: '<5mm' },
235
+ { value: 1, label: '5-15mm or 2 quadrants' },
236
+ { value: 2, label: '>15mm or 3-4 quadrants or endocervically undefined' }
237
+ ]
238
+ },
239
+ {
240
+ id: 'iodineStaining',
241
+ label: 'Iodine Staining',
242
+ scores: [
243
+ { value: 0, label: 'Brown' },
244
+ { value: 1, label: 'Faintly or patchy yellow' },
245
+ { value: 2, label: 'Distinct yellow' }
246
+ ]
247
+ }
248
+ ];
249
+
250
+ const totalSwedeScore = Object.values(swedeScores).reduce((sum, score) => {
251
+ return score !== null ? sum + score : sum;
252
+ }, 0);
253
+
254
+ const scoredFeatures = Object.values(swedeScores).filter(score => score !== null).length;
255
+
256
  return (
257
  <div className="h-full flex flex-col bg-gradient-to-br from-slate-50 to-slate-100 overflow-hidden">
258
  <div className="flex items-center gap-3 px-4 py-2.5 bg-white border-b border-slate-200 shadow-sm shrink-0">
 
285
  <Button
286
  size="sm"
287
  className="h-8 px-3 bg-gray-600 text-white hover:bg-slate-700"
288
+ onClick={onNext}
289
  >
290
  Next
291
  <ArrowRight className="h-4 w-4 ml-1" />
 
639
  </div>
640
  </div>
641
  </div>
642
+
643
+ {/* Swede Score Section */}
644
+ <div className="border-t border-slate-200 bg-white p-4">
645
+ <div className="max-w-full mx-auto">
646
+ <h3 className="text-lg font-bold text-[#0A2540] mb-4">Swede Score Assessment</h3>
647
+
648
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
649
+ {/* Score Input Section */}
650
+ <div className="space-y-4">
651
+ {swedeFeatures.map(feature => (
652
+ <div key={feature.id} className="bg-gray-50 rounded-lg border border-gray-200 p-4">
653
+ <label className="text-sm font-semibold text-gray-700 mb-3 block">{feature.label}</label>
654
+ <div className="space-y-2">
655
+ {feature.scores.map(score => (
656
+ <button
657
+ key={`${feature.id}-${score.value}`}
658
+ onClick={() => setSwedeScores(prev => ({
659
+ ...prev,
660
+ [feature.id]: swedeScores[feature.id as keyof SwedeScoreFeatures] === score.value ? null : score.value
661
+ }))}
662
+ className={`w-full text-left p-3 rounded-lg border-2 transition-all ${
663
+ swedeScores[feature.id as keyof SwedeScoreFeatures] === score.value
664
+ ? 'border-[#05998c] bg-[#05998c]/10'
665
+ : 'border-gray-300 hover:border-[#05998c] bg-white'
666
+ }`}
667
+ >
668
+ <div className="flex items-start gap-3">
669
+ <div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center flex-shrink-0 mt-0.5 ${
670
+ swedeScores[feature.id as keyof SwedeScoreFeatures] === score.value
671
+ ? 'border-[#05998c] bg-[#05998c]'
672
+ : 'border-gray-400'
673
+ }`}>
674
+ {swedeScores[feature.id as keyof SwedeScoreFeatures] === score.value && (
675
+ <span className="text-white text-sm font-bold">✓</span>
676
+ )}
677
+ </div>
678
+ <div className="flex-1">
679
+ <p className="text-sm font-semibold text-gray-800">Score {score.value}</p>
680
+ <p className="text-xs text-gray-600">{score.label}</p>
681
+ </div>
682
+ </div>
683
+ </button>
684
+ ))}
685
+ </div>
686
+ </div>
687
+ ))}
688
+ </div>
689
+
690
+ {/* Score Summary Section */}
691
+ <div className="space-y-4">
692
+ {/* Score Breakdown */}
693
+ <div className="bg-gradient-to-br from-[#05998c]/10 to-[#0A2540]/10 rounded-lg border-2 border-[#05998c] p-4">
694
+ <h4 className="text-sm font-bold text-[#0A2540] mb-4">Score Breakdown</h4>
695
+ <div className="space-y-3">
696
+ {swedeFeatures.map(feature => {
697
+ const score = swedeScores[feature.id as keyof SwedeScoreFeatures];
698
+ return (
699
+ <div key={feature.id} className="flex items-center justify-between p-2 bg-white rounded border border-gray-200">
700
+ <span className="text-sm font-medium text-gray-700">{feature.label}</span>
701
+ <span className={`text-sm font-bold px-3 py-1 rounded ${
702
+ score !== null
703
+ ? 'bg-[#05998c] text-white'
704
+ : 'bg-gray-200 text-gray-500'
705
+ }`}>
706
+ {score !== null ? score : '-'}
707
+ </span>
708
+ </div>
709
+ );
710
+ })}
711
+ </div>
712
+ </div>
713
+
714
+ {/* Total Score Card */}
715
+ <div className="bg-gradient-to-br from-[#05998c] to-[#047569] rounded-lg p-6 text-white shadow-lg">
716
+ <p className="text-sm font-semibold opacity-90 mb-2">Total Swede Score</p>
717
+ <div className="text-5xl font-bold mb-2">
718
+ {totalSwedeScore}/10
719
+ </div>
720
+ <p className="text-xs opacity-75">
721
+ {scoredFeatures} of {swedeFeatures.length} features scored
722
+ </p>
723
+
724
+ {scoredFeatures === swedeFeatures.length && (
725
+ <div className="mt-4 pt-4 border-t border-white/30">
726
+ <p className="text-xs font-semibold mb-2">Risk Assessment:</p>
727
+ <div className="text-sm">
728
+ {totalSwedeScore <= 4 ? (
729
+ <span>✓ Low Risk - Recommend routine follow-up</span>
730
+ ) : totalSwedeScore <= 7 ? (
731
+ <span>⚠ Intermediate Risk - Consider colposcopy</span>
732
+ ) : (
733
+ <span>⚠ High Risk - Recommend biopsy</span>
734
+ )}
735
+ </div>
736
+ </div>
737
+ )}
738
+ </div>
739
+ </div>
740
+ </div>
741
+ </div>
742
+ </div>
743
  </div>
744
  );
745
  };
src/pages/Compare.tsx CHANGED
@@ -17,6 +17,7 @@ type CompareImage = {
17
 
18
  type Props = {
19
  onBack: () => void;
 
20
  capturedImages: CapturedImage[];
21
  };
22
 
@@ -34,7 +35,7 @@ const stepColors: Record<string, string> = {
34
  lugol: 'from-amber-500 to-amber-600'
35
  };
36
 
37
- export function Compare({ onBack, capturedImages }: Props) {
38
  const [leftImage, setLeftImage] = useState<CompareImage | null>(null);
39
  const [rightImage, setRightImage] = useState<CompareImage | null>(null);
40
  const [zoomLevel, setZoomLevel] = useState(1);
@@ -193,14 +194,23 @@ export function Compare({ onBack, capturedImages }: Props) {
193
  </button>
194
  <h1 className="text-2xl font-bold text-gray-900">Image Comparison</h1>
195
  </div>
196
- <button
197
- onClick={handleDownloadComparison}
198
- disabled={!leftImage || !rightImage}
199
- className="flex items-center gap-2 px-6 py-3 bg-[#05998c] text-white rounded-lg font-semibold hover:bg-[#047569] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
200
- >
201
- <Download className="w-4 h-4" />
202
- Download Comparison
203
- </button>
 
 
 
 
 
 
 
 
 
204
  </div>
205
 
206
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
 
17
 
18
  type Props = {
19
  onBack: () => void;
20
+ onNext: () => void;
21
  capturedImages: CapturedImage[];
22
  };
23
 
 
35
  lugol: 'from-amber-500 to-amber-600'
36
  };
37
 
38
+ export function Compare({ onBack, onNext, capturedImages }: Props) {
39
  const [leftImage, setLeftImage] = useState<CompareImage | null>(null);
40
  const [rightImage, setRightImage] = useState<CompareImage | null>(null);
41
  const [zoomLevel, setZoomLevel] = useState(1);
 
194
  </button>
195
  <h1 className="text-2xl font-bold text-gray-900">Image Comparison</h1>
196
  </div>
197
+ <div className="flex items-center gap-3">
198
+ <button
199
+ onClick={handleDownloadComparison}
200
+ disabled={!leftImage || !rightImage}
201
+ className="flex items-center gap-2 px-6 py-3 bg-[#05998c] text-white rounded-lg font-semibold hover:bg-[#047569] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
202
+ >
203
+ <Download className="w-4 h-4" />
204
+ Download Comparison
205
+ </button>
206
+ <button
207
+ onClick={onNext}
208
+ className="flex items-center gap-2 px-6 py-3 bg-gray-600 text-white rounded-lg font-semibold hover:bg-slate-700 transition-colors"
209
+ >
210
+ Next
211
+ <ArrowLeft className="w-4 h-4 transform rotate-180" />
212
+ </button>
213
+ </div>
214
  </div>
215
 
216
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
src/pages/GuidedCapturePage.tsx CHANGED
@@ -5,6 +5,7 @@ import { AceticAnnotator, type AceticAnnotatorHandle } from '../components/Aceti
5
  import { ImagingObservations } from '../components/ImagingObservations';
6
  import { BiopsyMarking, type BiopsyCapturedImage } from './BiopsyMarking';
7
  import { Compare } from './Compare';
 
8
 
9
  // Simple UI Component replacements
10
  const Button: React.FC<any> = ({ children, onClick, disabled, variant, size, className, ...props }) => {
@@ -27,12 +28,13 @@ type CapturedItem = {
27
 
28
  type Props = {
29
  onNext: () => void;
 
30
  initialMode?: 'capture' | 'annotation' | 'compare' | 'report';
31
  onCapturedImagesChange?: (images: any[]) => void;
32
  onModeChange?: (mode: 'capture' | 'annotation' | 'compare' | 'report') => void;
33
  };
34
 
35
- export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange, onModeChange }: Props) {
36
  const imageAnnotatorRef = useRef<ImageAnnotatorHandle>(null);
37
  const aceticAnnotatorRef = useRef<AceticAnnotatorHandle>(null);
38
  const [currentStep, setCurrentStep] = useState<ExamStep>('native');
@@ -170,6 +172,10 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
170
  setSelectedImage(null);
171
  break;
172
  case 'annotation':
 
 
 
 
173
  setIsAnnotatingMode(true);
174
  setIsCompareMode(false);
175
  break;
@@ -369,9 +375,9 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
369
  <div key={stage} className="flex items-center flex-1">
370
  <div
371
  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 ${
372
- (stage === 'Capture' && !selectedImage && !isAnnotatingMode && !isCompareMode) ||
373
- (stage === 'Annotate' && (selectedImage || isAnnotatingMode) && !isCompareMode) ||
374
- (stage === 'Compare' && isCompareMode) ||
375
  (stage === 'Report' && currentStep === 'report')
376
  ? 'bg-[#05998c] text-white shadow-md'
377
  : 'bg-gray-100 text-gray-600'
@@ -380,7 +386,7 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
380
  <span className="flex items-center justify-center gap-2">
381
  {stage === 'Capture' && <Camera className="w-4 h-4" />}
382
  {stage === 'Annotate' && <Edit2 className="w-4 h-4" />}
383
- {stage === 'Compare' && <Info className="w-4 h-4" />}
384
  {stage === 'Report' && <FileText className="w-4 h-4" />}
385
  <span>{stage}</span>
386
  </span>
@@ -396,7 +402,15 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
396
  {steps.map(step => (
397
  <button
398
  key={step.key}
399
- onClick={() => setCurrentStep(step.key)}
 
 
 
 
 
 
 
 
400
  className={`relative px-5 py-3 rounded-lg font-medium text-base transition-all ${
401
  currentStep === step.key && !isCompareMode
402
  ? 'bg-[#05998c] text-white shadow-md'
@@ -452,10 +466,24 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
452
  </div>
453
  )}
454
 
455
- {currentStep === 'biopsyMarking' && !isCompareMode && !isAnnotatingMode ? (
 
 
 
 
 
 
 
 
 
 
456
  <BiopsyMarking
457
  onBack={() => setCurrentStep('lugol')}
458
- onNext={() => setCurrentStep('report')}
 
 
 
 
459
  capturedImages={biopsyCapturedImages}
460
  />
461
  ) : isCompareMode ? (
@@ -465,6 +493,12 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
465
  setIsAnnotatingMode(false);
466
  setSelectedImage(null);
467
  }}
 
 
 
 
 
 
468
  capturedImages={biopsyCapturedImages}
469
  />
470
  ) : isAnnotatingMode ? (
@@ -516,7 +550,7 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
516
  {/* Live Video Feed */}
517
  <div className="relative bg-gray-900 rounded-xl overflow-hidden shadow-2xl border-2 border-gray-700 mb-4">
518
  <div className="aspect-video flex items-center justify-center">
519
- <img src={liveFeedImageUrl} alt="Live Feed" className="w-full h-full object-cover" />
520
  <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">
521
  <div className={`w-2 h-2 rounded-full ${isRecording ? 'bg-white animate-pulse' : 'bg-white/70'}`} />
522
  {isRecording ? 'Recording' : 'Live'}
@@ -833,7 +867,13 @@ export function GuidedCapturePage({ onNext, initialMode, onCapturedImagesChange,
833
  <div className="space-y-4">
834
  {/* Annotate Images Button */}
835
  <button
836
- onClick={() => setIsAnnotatingMode(true)}
 
 
 
 
 
 
837
  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"
838
  >
839
  <Edit2 className="w-5 h-5" />
 
5
  import { ImagingObservations } from '../components/ImagingObservations';
6
  import { BiopsyMarking, type BiopsyCapturedImage } from './BiopsyMarking';
7
  import { Compare } from './Compare';
8
+ import { ReportPage } from './ReportPage';
9
 
10
  // Simple UI Component replacements
11
  const Button: React.FC<any> = ({ children, onClick, disabled, variant, size, className, ...props }) => {
 
28
 
29
  type Props = {
30
  onNext: () => void;
31
+ onGoToPatientRecords?: () => void;
32
  initialMode?: 'capture' | 'annotation' | 'compare' | 'report';
33
  onCapturedImagesChange?: (images: any[]) => void;
34
  onModeChange?: (mode: 'capture' | 'annotation' | 'compare' | 'report') => void;
35
  };
36
 
37
+ export function GuidedCapturePage({ onNext, onGoToPatientRecords, initialMode, onCapturedImagesChange, onModeChange }: Props) {
38
  const imageAnnotatorRef = useRef<ImageAnnotatorHandle>(null);
39
  const aceticAnnotatorRef = useRef<AceticAnnotatorHandle>(null);
40
  const [currentStep, setCurrentStep] = useState<ExamStep>('native');
 
172
  setSelectedImage(null);
173
  break;
174
  case 'annotation':
175
+ // Don't allow annotation mode for biopsy marking - switch to native step
176
+ if (currentStep === 'biopsyMarking') {
177
+ setCurrentStep('native');
178
+ }
179
  setIsAnnotatingMode(true);
180
  setIsCompareMode(false);
181
  break;
 
375
  <div key={stage} className="flex items-center flex-1">
376
  <div
377
  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 ${
378
+ (stage === 'Capture' && !selectedImage && !isAnnotatingMode && !isCompareMode && currentStep !== 'report') ||
379
+ (stage === 'Annotate' && (selectedImage || isAnnotatingMode) && !isCompareMode && currentStep !== 'report') ||
380
+ (stage === 'Compare' && isCompareMode && currentStep !== 'report') ||
381
  (stage === 'Report' && currentStep === 'report')
382
  ? 'bg-[#05998c] text-white shadow-md'
383
  : 'bg-gray-100 text-gray-600'
 
386
  <span className="flex items-center justify-center gap-2">
387
  {stage === 'Capture' && <Camera className="w-4 h-4" />}
388
  {stage === 'Annotate' && <Edit2 className="w-4 h-4" />}
389
+ {stage === 'Compare' && <img src="/arrow.png" alt="Compare" className="w-6 h-6 brightness-0 opacity-70" />}
390
  {stage === 'Report' && <FileText className="w-4 h-4" />}
391
  <span>{stage}</span>
392
  </span>
 
402
  {steps.map(step => (
403
  <button
404
  key={step.key}
405
+ onClick={() => {
406
+ setCurrentStep(step.key);
407
+ // Exit annotation/compare modes when clicking Biopsy Marking
408
+ if (step.key === 'biopsyMarking') {
409
+ setIsAnnotatingMode(false);
410
+ setIsCompareMode(false);
411
+ setSelectedImage(null);
412
+ }
413
+ }}
414
  className={`relative px-5 py-3 rounded-lg font-medium text-base transition-all ${
415
  currentStep === step.key && !isCompareMode
416
  ? 'bg-[#05998c] text-white shadow-md'
 
466
  </div>
467
  )}
468
 
469
+ {currentStep === 'report' ? (
470
+ <ReportPage
471
+ onBack={() => {
472
+ setCurrentStep('biopsyMarking');
473
+ setIsCompareMode(true);
474
+ }}
475
+ onNext={onNext}
476
+ onGoToPatientRecords={onGoToPatientRecords}
477
+ capturedImages={biopsyCapturedImages}
478
+ />
479
+ ) : currentStep === 'biopsyMarking' && !isCompareMode && !isAnnotatingMode ? (
480
  <BiopsyMarking
481
  onBack={() => setCurrentStep('lugol')}
482
+ onNext={() => {
483
+ setIsCompareMode(true);
484
+ setIsAnnotatingMode(false);
485
+ setSelectedImage(null);
486
+ }}
487
  capturedImages={biopsyCapturedImages}
488
  />
489
  ) : isCompareMode ? (
 
493
  setIsAnnotatingMode(false);
494
  setSelectedImage(null);
495
  }}
496
+ onNext={() => {
497
+ setCurrentStep('report');
498
+ setIsCompareMode(false);
499
+ setIsAnnotatingMode(false);
500
+ setSelectedImage(null);
501
+ }}
502
  capturedImages={biopsyCapturedImages}
503
  />
504
  ) : isAnnotatingMode ? (
 
550
  {/* Live Video Feed */}
551
  <div className="relative bg-gray-900 rounded-xl overflow-hidden shadow-2xl border-2 border-gray-700 mb-4">
552
  <div className="aspect-video flex items-center justify-center">
553
+ <video src="/live.mp4" autoPlay loop muted className="w-full h-full object-cover" />
554
  <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">
555
  <div className={`w-2 h-2 rounded-full ${isRecording ? 'bg-white animate-pulse' : 'bg-white/70'}`} />
556
  {isRecording ? 'Recording' : 'Live'}
 
867
  <div className="space-y-4">
868
  {/* Annotate Images Button */}
869
  <button
870
+ onClick={() => {
871
+ // Don't allow annotation mode for biopsy marking - switch to native step
872
+ if (currentStep === 'biopsyMarking') {
873
+ setCurrentStep('native');
874
+ }
875
+ setIsAnnotatingMode(true);
876
+ }}
877
  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"
878
  >
879
  <Edit2 className="w-5 h-5" />
src/pages/ReportPage.tsx CHANGED
@@ -12,6 +12,7 @@ type CapturedImage = {
12
  type Props = {
13
  onBack: () => void;
14
  onNext: () => void;
 
15
  patientData?: any;
16
  capturedImages?: CapturedImage[];
17
  biopsyData?: any;
@@ -21,6 +22,7 @@ type Props = {
21
  export function ReportPage({
22
  onBack,
23
  onNext,
 
24
  patientData = {},
25
  capturedImages = [],
26
  biopsyData = {},
@@ -62,6 +64,7 @@ export function ReportPage({
62
  lugol: [],
63
  biopsy: []
64
  });
 
65
  const imageBucketsRef = useRef(imageBuckets);
66
  const reportContentRef = useRef<HTMLDivElement>(null);
67
 
@@ -80,11 +83,11 @@ export function ReportPage({
80
  const reportDate = new Date().toISOString().split('T')[0];
81
 
82
  const options = {
83
- margin: [10, 10, 10, 10],
84
  filename: `Colposcopy_Report_${patientName}_${reportDate}.pdf`,
85
- image: { type: 'jpeg', quality: 0.98 },
86
  html2canvas: { scale: 2 },
87
- jsPDF: { orientation: 'portrait', unit: 'mm', format: 'a4' }
88
  };
89
 
90
  html2pdf().set(options).from(element).save();
@@ -235,7 +238,7 @@ export function ReportPage({
235
  Export PDF
236
  </button>
237
  <button
238
- onClick={onNext}
239
  className="px-5 py-3 bg-gradient-to-r from-[#05998c] to-[#047569] text-white rounded-xl hover:from-[#047569] hover:to-[#036356] transition-all duration-200 flex items-center gap-2 shadow-md hover:shadow-lg font-medium"
240
  >
241
  Complete
@@ -611,6 +614,44 @@ export function ReportPage({
611
  </div>
612
  </div>
613
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  </div>
615
  );
616
  }
 
12
  type Props = {
13
  onBack: () => void;
14
  onNext: () => void;
15
+ onGoToPatientRecords?: () => void;
16
  patientData?: any;
17
  capturedImages?: CapturedImage[];
18
  biopsyData?: any;
 
22
  export function ReportPage({
23
  onBack,
24
  onNext,
25
+ onGoToPatientRecords,
26
  patientData = {},
27
  capturedImages = [],
28
  biopsyData = {},
 
64
  lugol: [],
65
  biopsy: []
66
  });
67
+ const [showCompletionModal, setShowCompletionModal] = useState(false);
68
  const imageBucketsRef = useRef(imageBuckets);
69
  const reportContentRef = useRef<HTMLDivElement>(null);
70
 
 
83
  const reportDate = new Date().toISOString().split('T')[0];
84
 
85
  const options = {
86
+ margin: [10, 10, 10, 10] as [number, number, number, number],
87
  filename: `Colposcopy_Report_${patientName}_${reportDate}.pdf`,
88
+ image: { type: 'jpeg' as const, quality: 0.98 },
89
  html2canvas: { scale: 2 },
90
+ jsPDF: { orientation: 'portrait' as const, unit: 'mm', format: 'a4' }
91
  };
92
 
93
  html2pdf().set(options).from(element).save();
 
238
  Export PDF
239
  </button>
240
  <button
241
+ onClick={() => setShowCompletionModal(true)}
242
  className="px-5 py-3 bg-gradient-to-r from-[#05998c] to-[#047569] text-white rounded-xl hover:from-[#047569] hover:to-[#036356] transition-all duration-200 flex items-center gap-2 shadow-md hover:shadow-lg font-medium"
243
  >
244
  Complete
 
614
  </div>
615
  </div>
616
  </div>
617
+
618
+ {/* Completion Confirmation Modal */}
619
+ {showCompletionModal && (
620
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
621
+ <div className="bg-white rounded-2xl shadow-2xl p-6 md:p-8 max-w-md mx-4 animate-in">
622
+ <div className="flex items-center justify-center mb-4">
623
+ <div className="bg-gradient-to-br from-green-500 to-green-600 p-3 rounded-full">
624
+ <CheckCircle className="w-8 h-8 text-white" />
625
+ </div>
626
+ </div>
627
+ <h3 className="text-2xl font-bold text-gray-900 mb-2 text-center">Save Report</h3>
628
+ <p className="text-gray-600 text-center mb-6">
629
+ Your colposcopy examination report is ready. Click "Save & Continue" to export the PDF and proceed to the patient registry.
630
+ </p>
631
+ <div className="space-y-3">
632
+ <button
633
+ onClick={() => {
634
+ handleExportPDF();
635
+ setTimeout(() => {
636
+ setShowCompletionModal(false);
637
+ (onGoToPatientRecords || onNext)();
638
+ }, 1000);
639
+ }}
640
+ className="w-full px-6 py-3 bg-gradient-to-r from-[#05998c] to-[#047569] text-white rounded-xl hover:from-[#047569] hover:to-[#036356] transition-all duration-200 font-semibold shadow-md hover:shadow-lg flex items-center justify-center gap-2"
641
+ >
642
+ <Download className="w-4 h-4" />
643
+ Save & Continue
644
+ </button>
645
+ <button
646
+ onClick={() => setShowCompletionModal(false)}
647
+ className="w-full px-6 py-3 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-all duration-200 font-semibold"
648
+ >
649
+ Cancel
650
+ </button>
651
+ </div>
652
+ </div>
653
+ </div>
654
+ )}
655
  </div>
656
  );
657
  }