import React, { useState, useRef, useEffect } from 'react'; import { Send, Image as ImageIcon, Loader2, Bot, User, X, Leaf, Globe, Search, AlertTriangle, Camera } from 'lucide-react'; import { Message, LoadingState } from '../types'; import { geminiService } from '../services/geminiService'; const ChatInterface: React.FC = () => { const [messages, setMessages] = useState([ { id: 'welcome', role: 'model', text: "Welcome to Budtender AI. I can help you find strain information or analyze your cannabis products. Upload a photo or use your camera to show me a strain!" } ]); const [inputValue, setInputValue] = useState(''); const [selectedImage, setSelectedImage] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [loadingState, setLoadingState] = useState(LoadingState.IDLE); // Camera State const [isCameraOpen, setIsCameraOpen] = useState(false); const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); // Auto-scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, loadingState]); // Clean up camera stream when component unmounts useEffect(() => { return () => { stopCamera(); }; }, []); const startCamera = async () => { setIsCameraOpen(true); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } // Prefer back camera on mobile }); streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; } } catch (err) { console.error("Error accessing camera:", err); setIsCameraOpen(false); alert("Could not access camera. Please allow permissions."); } }; const stopCamera = () => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); streamRef.current = null; } setIsCameraOpen(false); }; const capturePhoto = () => { if (videoRef.current && canvasRef.current) { const video = videoRef.current; const canvas = canvasRef.current; // Set canvas dimensions to match video canvas.width = video.videoWidth; canvas.height = video.videoHeight; // Draw video frame to canvas const context = canvas.getContext('2d'); if (context) { context.drawImage(video, 0, 0, canvas.width, canvas.height); // Convert to data URL const dataUrl = canvas.toDataURL('image/jpeg'); setImagePreview(dataUrl); // Convert to File object (optional, but good for consistency) fetch(dataUrl) .then(res => res.blob()) .then(blob => { const file = new File([blob], "camera-capture.jpg", { type: "image/jpeg" }); setSelectedImage(file); }); stopCamera(); } } }; const handleImageSelect = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; setSelectedImage(file); const reader = new FileReader(); reader.onloadend = () => { setImagePreview(reader.result as string); }; reader.readAsDataURL(file); } }; const clearImage = () => { setSelectedImage(null); setImagePreview(null); if (fileInputRef.current) fileInputRef.current.value = ''; }; const handleSendMessage = async () => { if ((!inputValue.trim() && !selectedImage) || loadingState !== LoadingState.IDLE) return; const userText = inputValue.trim(); const currentImage = imagePreview; // Store ref for the message bubble // Clear inputs immediately setInputValue(''); clearImage(); setLoadingState(LoadingState.SEARCHING); // Default to searching/thinking since we use Google Search // Add user message to UI const newMessage: Message = { id: Date.now().toString(), role: 'user', text: userText, image: currentImage || undefined }; setMessages(prev => [...prev, newMessage]); try { // Prepare image for API (strip base64 header) let base64Data: string | undefined = undefined; if (currentImage) { base64Data = currentImage.split(',')[1]; } // Call Gemini const response = await geminiService.sendMessage( userText || (base64Data ? "Analyze this image and tell me about it." : "Hello"), base64Data ); setMessages(prev => [ ...prev, { id: (Date.now() + 1).toString(), role: 'model', text: response.text, groundingMetadata: response.groundingMetadata } ]); } catch (error: any) { console.error("Chat Error:", error); let errorMessage = "I'm sorry, I encountered an error connecting to the knowledge base."; if (error.message?.includes('API key')) { errorMessage = "Error: API Key is missing or invalid. Please check your configuration."; } else if (error.message) { errorMessage = `Error: ${error.message}`; } setMessages(prev => [ ...prev, { id: (Date.now() + 1).toString(), role: 'model', text: errorMessage, isError: true } ]); } finally { setLoadingState(LoadingState.IDLE); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; const getLoadingText = (state: LoadingState) => { switch (state) { case LoadingState.SEARCHING: return "Searching the Web..."; case LoadingState.THINKING: return "Thinking..."; default: return "Processing..."; } }; return (
{/* Header */}

Budtender AI

Powered by Gemini & Google Search

{/* Camera Modal */} {isCameraOpen && (

Position plant or product in frame

)} {/* Messages Area */}
{messages.map((msg) => (
{/* Avatar */}
{msg.role === 'model' ? ( msg.isError ? : ) : ( )}
{/* Bubble */}
{msg.image && (
User upload
)}
{msg.text}
{/* Grounding Sources */} {msg.groundingMetadata?.groundingChunks && msg.groundingMetadata.groundingChunks.length > 0 && (

Sources

{msg.groundingMetadata.groundingChunks.map((chunk: any, i: number) => { if (chunk.web?.uri) { return ( {chunk.web.title || new URL(chunk.web.uri).hostname} ); } return null; })}
)}
))} {/* Loading Indicator */} {loadingState !== LoadingState.IDLE && (
{getLoadingText(loadingState)}
)}
{/* Input Area */}
{imagePreview && (
Preview
)}
setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder="Ask about a strain..." className="relative w-full bg-gray-900 text-white border border-gray-700 rounded-xl py-3 pl-4 pr-12 focus:outline-none focus:border-transparent placeholder-gray-500 transition-all shadow-inner" />
); }; export default ChatInterface;