206 lines
8.5 KiB
TypeScript
206 lines
8.5 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|