v1 of protocol checker
All checks were successful
Deploy to Server / deploy (push) Successful in 34s

This commit is contained in:
Aniketh Kalagara
2026-04-14 19:08:02 -05:00
parent 2f69f57cf7
commit 4098453c97
7 changed files with 328 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { ChangeEvent, DragEvent, KeyboardEvent, useRef, useState } from "react";
import { protocolsApi } from "../lib/api";
import { Card } from "./ui/card";
import { Button } from "./ui/button";
@@ -29,9 +29,13 @@ interface ChatMessage {
}
export function ProtocolChecker() {
const fileInputRef = useRef<HTMLInputElement>(null);
const [protocol, setProtocol] = useState("");
const [selectedFileName, setSelectedFileName] = useState("");
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analyzed, setAnalyzed] = useState(false);
const [analysisError, setAnalysisError] = useState("");
const [issues, setIssues] = useState<SafetyIssue[]>([]);
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
{
id: "1",
@@ -42,56 +46,71 @@ export function ProtocolChecker() {
const [currentMessage, setCurrentMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const mockIssues: SafetyIssue[] = [
{
type: "critical",
category: "PPE Requirements",
message: "No mention of glove type for handling concentrated acids. Nitrile gloves are required for HCl handling.",
source: "OSHA 29 CFR 1910.132",
sourceUrl: "https://www.osha.gov/laws-regs/regulations/standardnumber/1910/1910.132"
},
{
type: "warning",
category: "Ventilation",
message: "Protocol should specify use of fume hood when working with volatile organic compounds.",
source: "OSHA Laboratory Standard 29 CFR 1910.1450",
sourceUrl: "https://www.osha.gov/laws-regs/regulations/standardnumber/1910/1910.1450"
},
{
type: "suggestion",
category: "Waste Disposal",
message: "Consider adding specific waste disposal instructions for halogenated solvents per EPA regulations.",
source: "EPA RCRA Guidelines",
sourceUrl: "https://www.epa.gov/rcra"
},
{
type: "warning",
category: "Emergency Procedures",
message: "Include location of nearest eyewash station and emergency shower as required by ANSI Z358.1.",
source: "ANSI Z358.1-2014",
sourceUrl: "https://www.ansi.org"
},
{
type: "suggestion",
category: "Chemical Storage",
message: "Specify incompatible chemicals that should not be stored together per NFPA guidelines.",
source: "NFPA 45: Fire Protection for Laboratories",
sourceUrl: "https://www.nfpa.org"
const loadProtocolFile = async (file: File) => {
setAnalyzed(false);
setAnalysisError("");
setIssues([]);
if (file.size > 10 * 1024 * 1024) {
setAnalysisError("File must be 10MB or smaller.");
return;
}
];
const extension = file.name.split(".").pop()?.toLowerCase();
if (extension !== "txt") {
setAnalysisError("For now, upload a TXT file or paste protocol text. PDF, DOC, and DOCX text extraction is not wired yet.");
return;
}
try {
const text = await file.text();
if (!text.trim()) {
setAnalysisError("That file does not contain any readable protocol text.");
return;
}
setProtocol(text);
setSelectedFileName(file.name);
} catch {
setAnalysisError("Could not read that file. Try saving it as a plain TXT file and upload again.");
}
};
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) await loadProtocolFile(file);
event.target.value = "";
};
const handleUploadKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
fileInputRef.current?.click();
}
};
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
const file = event.dataTransfer.files?.[0];
if (file) await loadProtocolFile(file);
};
const handleAnalyze = async () => {
if (!protocol.trim()) return;
setIsAnalyzing(true);
setAnalyzed(false);
setAnalysisError("");
setIssues([]);
try {
const saved = await protocolsApi.createFromText(
`Protocol ${new Date().toLocaleDateString()}`,
selectedFileName || `Protocol ${new Date().toLocaleDateString()}`,
protocol
);
await protocolsApi.saveAnalysis(saved.id, mockIssues);
const analyzedProtocol = await protocolsApi.analyze(saved.id);
setIssues(analyzedProtocol.analysis_results || []);
setAnalyzed(true);
} catch (err) {
console.error("Failed to save protocol:", err);
setAnalysisError(err instanceof Error ? err.message : "Failed to analyze protocol");
} finally {
setIsAnalyzing(false);
}
@@ -162,9 +181,9 @@ export function ProtocolChecker() {
}
};
const criticalCount = mockIssues.filter(i => i.type === "critical").length;
const warningCount = mockIssues.filter(i => i.type === "warning").length;
const suggestionCount = mockIssues.filter(i => i.type === "suggestion").length;
const criticalCount = issues.filter(i => i.type === "critical").length;
const warningCount = issues.filter(i => i.type === "warning").length;
const suggestionCount = issues.filter(i => i.type === "suggestion").length;
return (
<div className="p-8">
@@ -196,10 +215,29 @@ export function ProtocolChecker() {
<p className="text-muted-foreground">Paste your protocol text or upload a file</p>
</div>
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center mb-4 hover:border-primary transition-colors cursor-pointer">
<input
ref={fileInputRef}
type="file"
accept=".txt,text/plain"
className="hidden"
onChange={handleFileChange}
/>
<div
role="button"
tabIndex={0}
className="border-2 border-dashed border-border rounded-lg p-8 text-center mb-4 hover:border-primary transition-colors cursor-pointer"
onClick={() => fileInputRef.current?.click()}
onKeyDown={handleUploadKeyDown}
onDragOver={(event) => event.preventDefault()}
onDrop={handleDrop}
>
<Upload className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
<p className="text-foreground mb-1">Click to upload or drag and drop</p>
<p className="text-muted-foreground">PDF, DOC, DOCX, TXT (max 10MB)</p>
<p className="text-muted-foreground">TXT files for now (max 10MB)</p>
{selectedFileName && (
<p className="text-primary mt-2">{selectedFileName}</p>
)}
</div>
<div className="relative mb-4">
@@ -241,6 +279,15 @@ Example:
</>
)}
</Button>
{analysisError && (
<Alert className="mt-4 border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4 text-red-600" />
<AlertDescription className="text-red-700">
{analysisError}
</AlertDescription>
</Alert>
)}
</Card>
{/* Results */}
@@ -258,7 +305,12 @@ Example:
</div>
<div className="space-y-4">
{mockIssues.map((issue, idx) => {
{issues.length === 0 && (
<p className="text-muted-foreground">
No safety issues were returned for this protocol.
</p>
)}
{issues.map((issue, idx) => {
const colors = getIssueColor(issue.type);
return (
<div