import { ChangeEvent, DragEvent, KeyboardEvent, useRef, useState } from "react"; import { protocolsApi } from "../lib/api"; import { Card } from "./ui/card"; import { Button } from "./ui/button"; import { Textarea } from "./ui/textarea"; import { Badge } from "./ui/badge"; import { Input } from "./ui/input"; import { Upload, FileCheck, AlertTriangle, CheckCircle2, Info, Loader2, MessageSquare, Send, ExternalLink } from "lucide-react"; import { Alert, AlertDescription } from "./ui/alert"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import type { SafetyIssue } from "../shared/types"; interface ChatMessage { id: string; role: "user" | "assistant"; content: string; sources?: { title: string; url: string }[]; } export function ProtocolChecker() { const fileInputRef = useRef(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([]); const [chatMessages, setChatMessages] = useState([ { id: "1", role: "assistant", content: "Hi! I'm your lab safety AI assistant. I can help answer questions about chemical safety, proper handling procedures, and regulatory compliance. How can I help you today?", } ]); const [currentMessage, setCurrentMessage] = useState(""); const [isSending, setIsSending] = useState(false); 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) => { const file = event.target.files?.[0]; if (file) await loadProtocolFile(file); event.target.value = ""; }; const handleUploadKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); fileInputRef.current?.click(); } }; const handleDrop = async (event: DragEvent) => { 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( selectedFileName || `Protocol ${new Date().toLocaleDateString()}`, protocol ); 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); } }; const handleSendMessage = () => { if (!currentMessage.trim()) return; const userMessage: ChatMessage = { id: Date.now().toString(), role: "user", content: currentMessage }; setChatMessages([...chatMessages, userMessage]); setCurrentMessage(""); setIsSending(true); // Simulate AI response setTimeout(() => { const aiResponse: ChatMessage = { id: (Date.now() + 1).toString(), role: "assistant", content: generateAIResponse(currentMessage), sources: [ { title: "OSHA Laboratory Safety Guidance", url: "https://www.osha.gov/laboratories" }, { title: "CDC Laboratory Safety Manual", url: "https://www.cdc.gov/labs/safety.html" }, { title: "University EHS Guidelines", url: "#" } ] }; setChatMessages(prev => [...prev, aiResponse]); setIsSending(false); }, 1500); }; const generateAIResponse = (question: string): string => { // Mock AI responses based on common questions const lowerQ = question.toLowerCase(); if (lowerQ.includes("glove") || lowerQ.includes("ppe")) { return "For chemical handling, glove selection depends on the chemical. Nitrile gloves are suitable for most aqueous solutions and mild acids/bases. However, for concentrated acids (like concentrated HCl or H2SO4), you should use butyl rubber gloves. For organic solvents, neoprene or nitrile gloves are recommended. Always check the manufacturer's chemical resistance chart and inspect gloves before use. OSHA 29 CFR 1910.132 requires appropriate PPE selection based on hazard assessment."; } else if (lowerQ.includes("fume hood") || lowerQ.includes("ventilation")) { return "Fume hoods should be used when working with volatile, toxic, or odorous chemicals. Maintain a face velocity of 80-120 linear feet per minute. Keep the sash at the designated working height (typically 18 inches) and work at least 6 inches inside the hood. Never store chemicals in the fume hood permanently. According to ANSI/AIHA Z9.5, annual certification and daily inspections are required."; } else if (lowerQ.includes("waste") || lowerQ.includes("disposal")) { return "Chemical waste must be segregated by compatibility class. Never mix incompatible wastes. Label all waste containers with contents, hazards, and accumulation start date. Halogenated and non-halogenated organic solvents must be kept separate. Dispose through your institution's hazardous waste program within 90 days of accumulation. Follow EPA RCRA guidelines for proper classification and disposal."; } else if (lowerQ.includes("acid") || lowerQ.includes("base")) { return "When diluting acids, always add acid to water (never water to acid) to prevent violent reactions. Store acids and bases separately in dedicated cabinets. For spills, use appropriate neutralizing agents (sodium bicarbonate for acids, citric acid for bases). Wear appropriate PPE including face shield, acid-resistant gloves, and lab coat. Work in a fume hood when handling concentrated acids that produce vapors."; } return "Based on current laboratory safety standards and regulations from OSHA, EPA, and CDC, I recommend following your institution's chemical hygiene plan and consulting the relevant Safety Data Sheets (SDS) for specific chemicals. For detailed guidance on your specific situation, please consult with your institution's Environmental Health & Safety (EHS) office. I've provided relevant sources below for your reference."; }; const getIssueColor = (type: string) => { switch (type) { case "critical": return { bg: "bg-red-50", text: "text-red-700", border: "border-red-200" }; case "warning": return { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200" }; case "suggestion": return { bg: "bg-accent", text: "text-primary", border: "border-border" }; default: return { bg: "bg-muted", text: "text-muted-foreground", border: "border-border" }; } }; const getIssueIcon = (type: string) => { switch (type) { case "critical": return ; case "warning": return ; case "suggestion": return ; default: return null; } }; 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 (

Protocol Safety Checker & AI Assistant

Get safety feedback on protocols and ask questions with sourced answers

Protocol Checker AI Safety Assistant
{/* Input Section */}

Upload Protocol

Paste your protocol text or upload a file

fileInputRef.current?.click()} onKeyDown={handleUploadKeyDown} onDragOver={(event) => event.preventDefault()} onDrop={handleDrop} >

Click to upload or drag and drop

TXT files for now (max 10MB)

{selectedFileName && (

{selectedFileName}

)}
or paste text