diff --git a/apps/Frontend/package.json b/apps/Frontend/package.json index 50588ec9..02940c6b 100755 --- a/apps/Frontend/package.json +++ b/apps/Frontend/package.json @@ -76,6 +76,7 @@ "react-dom": "^19.1.0", "react-hook-form": "^7.55.0", "react-icons": "^5.4.0", + "react-image-crop": "^11.1.2", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", "socket.io-client": "^4.8.1", diff --git a/apps/Frontend/src/pages/ai-copy-agent-page.tsx b/apps/Frontend/src/pages/ai-copy-agent-page.tsx index 064860da..031c7cb3 100644 --- a/apps/Frontend/src/pages/ai-copy-agent-page.tsx +++ b/apps/Frontend/src/pages/ai-copy-agent-page.tsx @@ -1,30 +1,187 @@ -import { Copy, FileCheck, CreditCard, Shield, Zap } from "lucide-react"; -import { Card, CardContent } from "@/components/ui/card"; +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, RotateCcw, Save } 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"; -const CAPABILITIES = [ - { - icon: , - title: "Eligibility Results", - description: - "Automatically read insurance eligibility information — coverage status, plan details, deductibles, and co-pays — from the insurance portal.", - }, - { - icon: , - title: "Claim Information", - description: - "Extract claim numbers, claim status updates, and denial reasons from the insurance portal so they can be reused elsewhere without manual re-entry.", - }, - { - icon: , - title: "Insurance Payments", - description: - "Capture ERA / EOB payment details — amounts, adjustments, patient responsibility — straight from the source document or portal.", - }, -]; +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"); +} 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 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 handleSave = async () => { + if (!screenshot || !selectedPatient) return; + + setIsSaving(true); + try { + const croppedImage = + completedCrop && imgRef.current + ? getCroppedDataUrl(imgRef.current, completedCrop) + : screenshot; + + const response = await fetch(croppedImage); + const blob = await response.blob(); + const safeName = + fileName.trim().replace(/[/\\?%*:|"<>]/g, "-") || `screenshot_${Date.now()}`; + const file = new File([blob], `${safeName}.png`, { type: "image/png" }); + + const patientName = + selectedPatient.firstName && selectedPatient.lastName + ? `${selectedPatient.firstName} ${selectedPatient.lastName}` + : selectedPatient.firstName ?? `patient-${selectedPatient.id}`; + + const formData = new FormData(); + formData.append("patientId", String(selectedPatient.id)); + formData.append("patientName", patientName); + formData.append("files", file); + + await apiRequest("POST", "/api/claims/upload-to-cloud", formData); + + toast({ + title: "Saved", + description: `Screenshot saved to ${patientName}'s attachments.`, + }); + setScreenshot(null); + setFileName(""); + setCrop(undefined); + setCompletedCrop(undefined); + } catch (error) { + console.error("Error saving screenshot:", error); + toast({ + title: "Error", + description: "Failed to save screenshot to patient attachments.", + variant: "destructive", + }); + } finally { + setIsSaving(false); + } + }; + return ( -
+
{/* Hero */}
@@ -36,43 +193,92 @@ export default function AiCopyAgentPage() { An AI agent reads and copies data including images and brings them to this app automatically. No manual input required.

- - Coming Soon -
- {/* What it will copy */} -
-

What it will copy

-
- {CAPABILITIES.map((cap) => ( - - -
- {cap.icon} - {cap.title} + {/* Screenshot capture */} + + + Capture Screenshot + + Capture your screen and save it to a patient's attachments. + + + + {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
-

- {cap.description} +

+
+ + +
+ {!selectedPatient && ( +

+ Select a patient below to enable saving.

- - - ))} -
-
+ )} +
+ ) : ( + + )} + + - {/* Why */} - - - -
-

Why this matters

-

- Staff currently check eligibility or look up a claim on an insurance portal, then - manually copy every value into this app. This agent eliminates that step entirely — - one click pulls the data straight in. -

-
+ {/* Patient search */} + + + Patient Records + + Select the patient this screenshot belongs to. + + + + diff --git a/apps/Frontend/vite.config.js b/apps/Frontend/vite.config.js deleted file mode 100644 index ea296511..00000000 --- a/apps/Frontend/vite.config.js +++ /dev/null @@ -1,50 +0,0 @@ -import { defineConfig, loadEnv } from "vite"; -import react from "@vitejs/plugin-react"; -import path from "path"; -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); - return { - plugins: [react()], - server: { - host: env.HOST, - port: Number(env.PORT), - fs: { - allow: [".."], - }, - allowedHosts: [ - ...(env.VITE_CLOUDFLARE_HOST ? [env.VITE_CLOUDFLARE_HOST] : []), - "192.168.0.94", - ], - proxy: { - "/api": { - target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000", - changeOrigin: true, - configure: (proxy) => { - proxy.on("proxyReq", (proxyReq, req) => { - const auth = req.headers["authorization"]; - if (auth) - proxyReq.setHeader("Authorization", auth); - }); - }, - }, - "/socket.io": { - target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000", - changeOrigin: true, - ws: true, - }, - }, - }, - resolve: { - extensions: [".mts", ".ts", ".tsx", ".mjs", ".js", ".jsx", ".json"], - alias: { - "@": path.resolve(__dirname, "src"), - "@repo/db/usedSchemas": path.resolve(__dirname, "../../packages/db/usedSchemas/browser.ts"), - "@repo/db/types": path.resolve(__dirname, "../../packages/db/types/index.ts"), - "@repo/db": path.resolve(__dirname, "../../packages/db"), - }, - }, - optimizeDeps: { - exclude: ["@repo/db"], - }, - }; -}); diff --git a/package-lock.json b/package-lock.json index 6c292e3c..ffe4beb3 100755 --- a/package-lock.json +++ b/package-lock.json @@ -150,6 +150,7 @@ "react-dom": "^19.1.0", "react-hook-form": "^7.55.0", "react-icons": "^5.4.0", + "react-image-crop": "^11.1.2", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", "socket.io-client": "^4.8.1", @@ -11994,6 +11995,15 @@ "react": "*" } }, + "node_modules/react-image-crop": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.1.2.tgz", + "integrity": "sha512-+0Pc2fxpwKL4u4oLmdKBw8XSwUceFbXbKEHvFOlsl/MGB1OVNic4uBlAPmEHGXYgoJIq+b63xHbc/aJMG0AVkA==", + "license": "ISC", + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",