diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 78e1c234..2bdefb2a 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -348,7 +348,9 @@ router.post( // Include attachments already saved on the appointment (e.g. via "Select Procedures") // that weren't re-uploaded in this request — read straight from disk by filePath. - if (claimData.appointmentId) { + // Skipped for chatbot-driven submissions, which already send every intended file and + // shouldn't have unrelated older appointment attachments silently tacked on. + if (claimData.appointmentId && !claimData.skipAppointmentAttachmentMerge) { const seenNames = new Set(filesForQueue.map((f) => f.originalname)); const apptFiles = await storage.getAppointmentFiles(Number(claimData.appointmentId)); for (const f of apptFiles) { diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 2fdc28f0..8eba768f 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -1021,6 +1021,10 @@ export function ClaimForm({ insuranceSiteKey: "MH", claimId: createdClaim.id, uploadedFiles, + // Chatbot-driven submissions already include every file the user intends to send — + // skip the backend's appointment-attachment fallback merge so old files saved on the + // appointment (e.g. via the Schedule editor) aren't silently added as extra uploads. + skipAppointmentAttachmentMerge: !!autoSubmit, }); // 5. Close form diff --git a/apps/Frontend/src/components/layout/chatbot.tsx b/apps/Frontend/src/components/layout/chatbot.tsx index 412086ab..ac94a7ac 100644 --- a/apps/Frontend/src/components/layout/chatbot.tsx +++ b/apps/Frontend/src/components/layout/chatbot.tsx @@ -12,7 +12,11 @@ import { RotateCcw, Paperclip, Image as ImageIcon, + Camera, + Check, } from "lucide-react"; +import ReactCrop, { Crop, PixelCrop, centerCrop, makeAspectCrop } from "react-image-crop"; +import "react-image-crop/dist/ReactCrop.css"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -23,11 +27,41 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; import { useLocation } from "wouter"; import { cn } from "@/lib/utils"; import { apiRequest } from "@/lib/queryClient"; import { setChatbotPendingFiles } from "@/lib/chatbotFileStore"; import { ServerAttachmentsPickerModal } from "@/components/file-upload/server-attachments-picker-modal"; +import { useScreenCapture } from "@/hooks/use-screen-capture"; + +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"); +} type Step = | "menu" @@ -242,6 +276,61 @@ export function ChatbotButton() { } | null>(null); const [pendingFiles, setPendingFiles] = useState([]); const [isServerPickerOpen, setIsServerPickerOpen] = useState(false); + const { capture, isCapturing, countdown } = useScreenCapture(); + const [screenshotDataUrl, setScreenshotDataUrl] = useState(null); + const [screenshotFileName, setScreenshotFileName] = useState(""); + const [crop, setCrop] = useState(); + const [completedCrop, setCompletedCrop] = useState(); + const screenshotImgRef = useRef(null); + + const closeScreenshotDialog = () => { + setScreenshotDataUrl(null); + setScreenshotFileName(""); + setCrop(undefined); + setCompletedCrop(undefined); + }; + + const handleScreenshotImageLoad = (e: React.SyntheticEvent) => { + screenshotImgRef.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 handleTakeScreenshot = async () => { + try { + const dataUrl = await capture(); + setScreenshotDataUrl(dataUrl); + setScreenshotFileName(`screenshot_${Date.now()}`); + } catch (err: any) { + addMsg("bot", err?.message ?? "Screen capture failed."); + } + }; + + const handleRetakeScreenshot = async () => { + closeScreenshotDialog(); + await handleTakeScreenshot(); + }; + + const handleAttachScreenshot = async () => { + if (!screenshotDataUrl) return; + const croppedDataUrl = + completedCrop && screenshotImgRef.current + ? getCroppedDataUrl(screenshotImgRef.current, completedCrop) + : screenshotDataUrl; + const response = await fetch(croppedDataUrl); + const blob = await response.blob(); + const safeName = + screenshotFileName.trim().replace(/[/\\?%*:|"<>]/g, "-") || `screenshot_${Date.now()}`; + const file = new File([blob], `${safeName}.png`, { type: "image/png" }); + closeScreenshotDialog(); + setPendingFiles((prev) => [...prev, file].slice(0, 5)); + }; const [, setLocation] = useLocation(); const messagesEndRef = useRef(null); const pasteRef = useRef(null); @@ -1670,12 +1759,28 @@ export function ChatbotButton() { size="sm" variant="ghost" className="h-9 w-9 p-0 shrink-0 text-gray-400 hover:text-gray-600" - title="Attach screenshot" + title="Choose from scanned attachments" onClick={() => setIsServerPickerOpen(true)} disabled={step === "ai-loading"} > + + + + + )}