Files
LabWise/components/AIChat.tsx

206 lines
8.5 KiB
TypeScript
Raw Normal View History

2026-03-18 17:10:16 -05:00
import { useState, useRef, useEffect } from "react";
import { Card } from "./ui/card";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Badge } from "./ui/badge";
import { Send, Bot, User, Sparkles, FileText } from "lucide-react";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
timestamp: Date;
}
export function AIChat() {
const [messages, setMessages] = useState<Message[]>([
{
id: "1",
role: "assistant",
content: "Hello! I'm your Labwise AI assistant. I can help you with:\n\n• Chemical safety information\n• Protocol guidance\n• SDS lookups\n• EHS documentation questions\n• Emergency procedures\n\nWhat would you like to know?",
timestamp: new Date()
}
]);
const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages, isTyping]);
const mockResponses: { [key: string]: string } = {
"acetone": "Acetone (CAS 67-64-1) is a flammable liquid. Key safety considerations:\n\n• Store in flammables cabinet away from ignition sources\n• Use in well-ventilated areas or fume hood\n• Wear nitrile gloves and safety goggles\n• Keep away from oxidizing agents\n• Flash point: -20°C\n\nWould you like me to pull up the full SDS?",
"disposal": "For chemical waste disposal:\n\n1. Segregate waste by compatibility class\n2. Label all waste containers with contents and hazards\n3. Use proper waste containers (no food/beverage containers)\n4. Contact EHS for pickup when containers are 80% full\n5. Never pour chemicals down the drain unless specifically approved\n\nWhat type of waste are you disposing of?",
"ppe": "Personal Protective Equipment (PPE) requirements depend on the chemicals and procedures:\n\n**Minimum PPE:**\n• Safety glasses or goggles\n• Lab coat\n• Closed-toe shoes\n• Long pants\n\n**Additional PPE may include:**\n• Chemical-resistant gloves (nitrile, neoprene, etc.)\n• Face shield for splash hazards\n• Respirator for specific vapors\n\nWhat chemicals are you working with?",
"default": "I can help you with that! Based on your question, here are some key points:\n\n• Always refer to the Safety Data Sheet (SDS) for specific chemical hazards\n• Ensure proper PPE is worn at all times\n• Work in well-ventilated areas or fume hoods when handling volatile chemicals\n• Follow your lab's standard operating procedures\n\nWould you like more specific information about a particular chemical or procedure?"
};
const handleSend = () => {
if (!inputValue.trim()) return;
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: inputValue,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInputValue("");
setIsTyping(true);
setTimeout(() => {
const lowercaseInput = inputValue.toLowerCase();
let response = mockResponses.default;
if (lowercaseInput.includes("acetone")) {
response = mockResponses.acetone;
} else if (lowercaseInput.includes("disposal") || lowercaseInput.includes("waste")) {
response = mockResponses.disposal;
} else if (lowercaseInput.includes("ppe") || lowercaseInput.includes("protective equipment")) {
response = mockResponses.ppe;
}
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: response,
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
setIsTyping(false);
}, 1500);
};
const quickQuestions = [
"What PPE do I need for handling acids?",
"How do I dispose of organic solvents?",
"What are the hazards of acetone?",
"Where can I find the SDS for benzene?"
];
return (
<div className="p-8 h-full flex flex-col">
<div className="max-w-5xl mx-auto w-full flex flex-col h-full">
<div className="mb-6">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-accent rounded-lg">
<Sparkles className="w-6 h-6 text-primary" />
</div>
<h1 className="text-foreground">AI Safety Assistant</h1>
</div>
<p className="text-muted-foreground">Ask questions about chemical safety, protocols, and lab procedures</p>
</div>
<div className="flex-1 flex flex-col min-h-0">
{/* Messages */}
<Card className="flex-1 p-6 mb-4 overflow-y-auto">
<div className="space-y-6">
{messages.map((message) => (
<div
key={message.id}
className={`flex gap-4 ${
message.role === "user" ? "flex-row-reverse" : ""
}`}
>
<div
className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
message.role === "user"
? "bg-secondary"
: "bg-accent"
}`}
>
{message.role === "user" ? (
<User className="w-5 h-5 text-primary" />
) : (
<Bot className="w-5 h-5 text-primary" />
)}
</div>
<div
className={`flex-1 max-w-2xl ${
message.role === "user" ? "text-right" : ""
}`}
>
<div
className={`inline-block p-4 rounded-lg ${
message.role === "user"
? "bg-secondary text-foreground"
: "bg-muted text-foreground"
}`}
>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
<p className="text-muted-foreground mt-1 px-4">
{message.timestamp.toLocaleTimeString()}
</p>
</div>
</div>
))}
{isTyping && (
<div className="flex gap-4">
<div className="w-10 h-10 rounded-full bg-accent flex items-center justify-center">
<Bot className="w-5 h-5 text-primary" />
</div>
<div className="bg-muted p-4 rounded-lg">
<div className="flex gap-2">
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" />
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:0.2s]" />
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:0.4s]" />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</Card>
{/* Quick Questions */}
{messages.length === 1 && (
<div className="mb-4">
<p className="text-muted-foreground mb-3">Quick questions:</p>
<div className="flex flex-wrap gap-2">
{quickQuestions.map((question, idx) => (
<Button
key={idx}
variant="outline"
onClick={() => setInputValue(question)}
className="text-left h-auto py-2"
>
{question}
</Button>
))}
</div>
</div>
)}
{/* Input */}
<Card className="p-4">
<div className="flex gap-3">
<Input
placeholder="Ask about chemical safety, protocols, or procedures..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleSend()}
className="flex-1"
/>
<Button
onClick={handleSend}
disabled={!inputValue.trim() || isTyping}
className="bg-primary hover:bg-primary/90"
>
<Send className="w-4 h-4" />
</Button>
</div>
</Card>
</div>
</div>
</div>
);
}