fix: skip appointment-file merge for chatbot claims; add live screenshot capture to chatbot
- selenium-claim route no longer re-appends previously saved appointment attachments when the chatbot's autoSubmit flow already sent every intended file, preventing an unwanted extra upload (e.g. attach 2 -> Selenium sees 3). - Add a "Take screenshot" button to the AI chatbot's attachment bar, reusing the same capture/crop flow as the appointment editor, so claims and preauths can be submitted from the chatbot with a live screenshot attached. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<File[]>([]);
|
||||
const [isServerPickerOpen, setIsServerPickerOpen] = useState(false);
|
||||
const { capture, isCapturing, countdown } = useScreenCapture();
|
||||
const [screenshotDataUrl, setScreenshotDataUrl] = useState<string | null>(null);
|
||||
const [screenshotFileName, setScreenshotFileName] = useState("");
|
||||
const [crop, setCrop] = useState<Crop>();
|
||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
||||
const screenshotImgRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
const closeScreenshotDialog = () => {
|
||||
setScreenshotDataUrl(null);
|
||||
setScreenshotFileName("");
|
||||
setCrop(undefined);
|
||||
setCompletedCrop(undefined);
|
||||
};
|
||||
|
||||
const handleScreenshotImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
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<HTMLDivElement>(null);
|
||||
const pasteRef = useRef<HTMLTextAreaElement>(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"}
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 w-9 p-0 shrink-0 text-gray-400 hover:text-gray-600"
|
||||
title="Take screenshot"
|
||||
onClick={handleTakeScreenshot}
|
||||
disabled={step === "ai-loading" || isCapturing}
|
||||
>
|
||||
{isCapturing ? (
|
||||
<span className="flex h-4 w-4 items-center justify-center text-xs">
|
||||
{countdown ?? <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</span>
|
||||
) : (
|
||||
<Camera className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-9 w-9 p-0 shrink-0"
|
||||
@@ -1711,6 +1816,54 @@ export function ChatbotButton() {
|
||||
setPendingFiles((prev) => [...prev, ...files].slice(0, 5))
|
||||
}
|
||||
/>
|
||||
<Dialog open={!!screenshotDataUrl} onOpenChange={(open) => !open && closeScreenshotDialog()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Crop screenshot</DialogTitle>
|
||||
</DialogHeader>
|
||||
{screenshotDataUrl && (
|
||||
<div className="space-y-3">
|
||||
<ReactCrop
|
||||
crop={crop}
|
||||
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
||||
onComplete={(pixelCrop) => setCompletedCrop(pixelCrop)}
|
||||
>
|
||||
<img
|
||||
src={screenshotDataUrl}
|
||||
alt="Captured screenshot"
|
||||
onLoad={handleScreenshotImageLoad}
|
||||
className="w-full rounded-lg border object-contain max-h-96"
|
||||
/>
|
||||
</ReactCrop>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Drag the corners to crop, or leave as-is to attach the full screenshot.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="chatbot-screenshot-filename">File name</Label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
id="chatbot-screenshot-filename"
|
||||
value={screenshotFileName}
|
||||
onChange={(e) => setScreenshotFileName(e.target.value)}
|
||||
placeholder="screenshot"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">.png</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={handleRetakeScreenshot} disabled={isCapturing}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Retake
|
||||
</Button>
|
||||
<Button type="button" onClick={handleAttachScreenshot}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Attach
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user