import { useRef, useState } from "react"; import ReactCrop, { Crop, PixelCrop, centerCrop, makeAspectCrop } from "react-image-crop"; import "react-image-crop/dist/ReactCrop.css"; import { Camera, Copy, Plus, RotateCcw, Save, Sparkles, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { PatientTable } from "@/components/patients/patient-table"; import { Patient } from "@repo/db/types"; function getCroppedDataUrl(image: HTMLImageElement, crop: PixelCrop): string { const scaleX = image.naturalWidth / image.width; const scaleY = image.naturalHeight / image.height; const canvas = document.createElement("canvas"); canvas.width = crop.width * scaleX; canvas.height = crop.height * scaleY; const ctx = canvas.getContext("2d"); ctx?.drawImage( image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, canvas.width, canvas.height ); return canvas.toDataURL("image/png"); } interface BatchedScreenshot { id: string; dataUrl: string; fileName: string; } interface TempPatient { id: number; firstName: string | null; lastName: string | null; } const PENDING_SCREENSHOTS_KEY = "copy_agent_pending_screenshots"; export default function AiCopyAgentPage() { const { toast } = useToast(); const [selectedPatient, setSelectedPatient] = useState(null); const [screenshot, setScreenshot] = useState(null); const [fileName, setFileName] = useState(""); const [crop, setCrop] = useState(); const [completedCrop, setCompletedCrop] = useState(); const [isCapturing, setIsCapturing] = useState(false); const [countdown, setCountdown] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isDetecting, setIsDetecting] = useState(false); const [batch, setBatch] = useState([]); const streamRef = useRef(null); const imgRef = useRef(null); const handleImageLoad = (e: React.SyntheticEvent) => { imgRef.current = e.currentTarget; const { width, height } = e.currentTarget; const fullCrop = centerCrop( makeAspectCrop({ unit: "%", width: 100 }, width / height, width, height), width, height ); setCrop(fullCrop); setCompletedCrop({ unit: "px", x: 0, y: 0, width, height, }); }; const stopStream = () => { streamRef.current?.getTracks().forEach((track) => track.stop()); streamRef.current = null; }; const handleCapture = async () => { setIsCapturing(true); try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "monitor" }, }); streamRef.current = stream; const video = document.createElement("video"); video.srcObject = stream; video.muted = true; await video.play(); await new Promise((resolve) => { if (video.readyState >= 2) resolve(); else video.onloadeddata = () => resolve(); }); for (let secondsLeft = 3; secondsLeft > 0; secondsLeft--) { setCountdown(secondsLeft); await new Promise((resolve) => setTimeout(resolve, 1000)); } setCountdown(null); const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); stopStream(); setScreenshot(canvas.toDataURL("image/png")); setFileName(`screenshot_${Date.now()}`); } catch (error) { console.error("Error capturing screenshot:", error); const name = error instanceof Error ? error.name : ""; const description = name === "NotAllowedError" ? "Screen share permission was denied or the picker was cancelled." : name === "NotFoundError" ? "No screen/window source was available to capture." : !navigator.mediaDevices?.getDisplayMedia ? "Screen capture isn't supported in this browser." : (error instanceof Error ? error.message : "Screen capture failed."); toast({ title: "Screenshot failed", description, variant: "destructive", }); stopStream(); } finally { setIsCapturing(false); setCountdown(null); } }; const handleRetake = () => { setScreenshot(null); setFileName(""); setCrop(undefined); setCompletedCrop(undefined); handleCapture(); }; const clearActive = () => { setScreenshot(null); setFileName(""); setCrop(undefined); setCompletedCrop(undefined); }; // Applies the current crop to the active screenshot and returns it, without touching state. const finalizeActive = (): BatchedScreenshot | null => { if (!screenshot) return null; const croppedImage = completedCrop && imgRef.current ? getCroppedDataUrl(imgRef.current, completedCrop) : screenshot; const safeName = fileName.trim().replace(/[/\\?%*:|"<>]/g, "-") || `screenshot_${Date.now()}`; return { id: crypto.randomUUID(), dataUrl: croppedImage, fileName: safeName }; }; const handleAddAnother = async () => { const item = finalizeActive(); if (!item) return; setBatch((prev) => [...prev, item]); clearActive(); await handleCapture(); }; const handleRemoveBatchItem = (id: string) => { setBatch((prev) => prev.filter((item) => item.id !== id)); }; const getPatientName = (patient: Patient) => patient.firstName && patient.lastName ? `${patient.firstName} ${patient.lastName}` : patient.firstName ?? `patient-${patient.id}`; const itemsToFiles = async (items: BatchedScreenshot[]): Promise => { const files: File[] = []; for (const item of items) { const response = await fetch(item.dataUrl); const blob = await response.blob(); files.push(new File([blob], `${item.fileName}.png`, { type: "image/png" })); } return files; }; const uploadToPatient = async ( patientId: number | undefined, patientName: string, files: File[], pending: boolean ) => { const formData = new FormData(); formData.append("patientId", String(patientId)); formData.append("patientName", patientName); if (pending) formData.append("pending", "true"); // Lets the backend keep a local-disk-only debug copy — not shown anywhere in the UI. formData.append("screenshotBackup", "true"); for (const file of files) formData.append("files", file); await apiRequest("POST", "/api/claims/upload-to-cloud", formData); }; const getPendingItems = (): BatchedScreenshot[] => { const items = [...batch]; const active = finalizeActive(); if (active) items.push(active); return items; }; const handleSave = async () => { if (!selectedPatient) return; const items = getPendingItems(); if (!items.length) return; setIsSaving(true); try { const patientName = getPatientName(selectedPatient); const files = await itemsToFiles(items); await uploadToPatient(selectedPatient.id, patientName, files, false); toast({ title: "Saved", description: `${items.length} screenshot${items.length > 1 ? "s" : ""} saved to ${patientName}'s attachments.`, }); setBatch([]); clearActive(); } catch (error) { console.error("Error saving screenshot:", error); toast({ title: "Error", description: "Failed to save screenshots to patient attachments.", variant: "destructive", }); } finally { setIsSaving(false); } }; const detectFromFiles = async (files: File[]) => { const detectFormData = new FormData(); for (const file of files) detectFormData.append("files", file); const response = await apiRequest("POST", "/api/ai/detect-eligibility-info", detectFormData); return response.json(); }; const handleAiDetectAndCheck = async () => { const items = getPendingItems(); if (!items.length) return; if (selectedPatient) { await runAiDetectForSelectedPatient(selectedPatient, items); } else { await runAiDetectWithoutPatient(items); } }; const runAiDetectForSelectedPatient = async (patient: Patient, items: BatchedScreenshot[]) => { setIsDetecting(true); try { const patientName = getPatientName(patient); const files = await itemsToFiles(items); const [uploadResult, detectResult] = await Promise.allSettled([ uploadToPatient(patient.id, patientName, files, true), detectFromFiles(files), ]); const saved = uploadResult.status === "fulfilled"; if (saved) { setBatch([]); clearActive(); } if (detectResult.status === "rejected" || detectResult.value?.error) { toast({ title: saved ? "Saved, but AI detection failed" : "Error", description: saved ? "Screenshots saved. Enter the Member ID/DOB manually in the chatbot to check eligibility." : "Failed to save screenshots and detect Member ID/DOB.", variant: saved ? undefined : "destructive", }); return; } if (!saved) { toast({ title: "Detected, but save failed", description: "Read the Member ID/DOB, but failed to save the screenshots. Try Save All again.", variant: "destructive", }); } const memberId: string | null = detectResult.value?.data?.memberId ?? null; const dob: string | null = detectResult.value?.data?.dob ?? null; const autoCheck: string = detectResult.value?.data?.autoCheck ?? "mh"; window.dispatchEvent( new CustomEvent("chatbot:open-eligibility-id-ready", { detail: { memberId, dob, autoCheck, patient: { id: patient.id, firstName: patient.firstName, lastName: patient.lastName, insuranceId: patient.insuranceId ?? null, insuranceProvider: null, dateOfBirth: patient.dateOfBirth ?? null, }, patientId: patient.id, pendingFolderPatientName: patientName, }, }) ); } finally { setIsDetecting(false); } }; // No patient selected: detect first (need memberId+dob to create a patient at all), then // either auto-create a temp "PID-" patient and save under it, or — if detection came back // incomplete — leave the screenshots unsaved and let the user complete the details in the // chatbot manually; insurance-status-page.tsx picks the pending screenshots back up once that // manual check resolves to a real patient. const runAiDetectWithoutPatient = async (items: BatchedScreenshot[]) => { setIsDetecting(true); try { const files = await itemsToFiles(items); let detected: { memberId: string | null; dob: string | null; autoCheck: string }; try { const json = await detectFromFiles(files); if (json?.error) throw new Error(json.message || "Detection failed"); detected = { memberId: json?.data?.memberId ?? null, dob: json?.data?.dob ?? null, autoCheck: json?.data?.autoCheck ?? "mh" }; } catch (error) { console.error("Error detecting Member ID/DOB:", error); toast({ title: "Error", description: "Failed to read the screenshots. Please try again.", variant: "destructive", }); return; } const { memberId, dob, autoCheck } = detected; if (memberId && dob) { let tempPatient: TempPatient; try { const res = await apiRequest("POST", "/api/patients/create-temp", { memberId, dob }); const json = await res.json(); tempPatient = json.data; } catch (error) { console.error("Error creating temp patient:", error); toast({ title: "Error", description: "Failed to create a patient record for this screenshot.", variant: "destructive", }); return; } const patientName = getPatientName(tempPatient as Patient); try { await uploadToPatient(tempPatient.id, patientName, files, false); } catch (error) { console.error("Error saving screenshots to temp patient:", error); toast({ title: "Detected, but save failed", description: `Created ${patientName}, but failed to save the screenshots. Select them from the patient list and try Save All.`, variant: "destructive", }); return; } setBatch([]); clearActive(); setSelectedPatient(tempPatient as Patient); toast({ title: "Patient created", description: `Saved as ${patientName}. Its name will update automatically once the eligibility check confirms it.`, }); window.dispatchEvent( new CustomEvent("chatbot:open-eligibility-id-ready", { detail: { memberId, dob, autoCheck, patient: { id: tempPatient.id, firstName: tempPatient.firstName, lastName: tempPatient.lastName, insuranceId: memberId, insuranceProvider: null, dateOfBirth: dob, }, patientId: tempPatient.id, }, }) ); } else { try { sessionStorage.setItem( PENDING_SCREENSHOTS_KEY, JSON.stringify(items.map((item) => ({ dataUrl: item.dataUrl, fileName: item.fileName }))) ); } catch {} toast({ title: "Couldn't read Member ID/DOB", description: "Complete the details in the chatbot — your screenshots will attach automatically once that check finishes.", }); window.dispatchEvent( new CustomEvent("chatbot:open-eligibility-id-ready", { detail: { memberId, dob, autoCheck, patient: null }, }) ); } } finally { setIsDetecting(false); } }; return (
{/* Hero */}

Copy Agent

An AI agent reads and copies data including images and brings them to this app automatically. No manual input required.

{/* Screenshot capture */} Capture Screenshot Capture your screen and save it to a patient's attachments. Take multiple screenshots and save them together as one batch. {batch.length > 0 && (
{batch.map((item) => (
{item.fileName}
))}
)} {screenshot ? (
setCrop(percentCrop)} onComplete={(pixelCrop) => setCompletedCrop(pixelCrop)} > Captured screenshot

Drag the corners to crop, or leave as-is to save the full screenshot.

setFileName(e.target.value)} placeholder="screenshot" /> .png
{!selectedPatient && (

Select a patient below to enable Save All, or use "AI Detect & Check Eligibility" without selecting one to create a patient automatically.

)}
) : (
{batch.length > 0 && ( <> )}
)}
{/* Patient search */} Patient Records Select the patient this screenshot belongs to, or leave unselected and use "AI Detect & Check Eligibility" to create one automatically.
); }