export base44 - Nov 18
This commit is contained in:
332
frontend-web/src/components/orders/RapidOrderChat.jsx
Normal file
332
frontend-web/src/components/orders/RapidOrderChat.jsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import React, { useState } from "react";
|
||||
import { base44 } from "@/api/base44Client";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Zap, Send, Check, Edit3, MapPin, Clock, Users, AlertCircle, Sparkles } from "lucide-react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
export default function RapidOrderChat({ onOrderCreated }) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [message, setMessage] = useState("");
|
||||
const [conversation, setConversation] = useState([]);
|
||||
const [detectedOrder, setDetectedOrder] = useState(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['current-user-rapid'],
|
||||
queryFn: () => base44.auth.me(),
|
||||
});
|
||||
|
||||
const { data: businesses } = useQuery({
|
||||
queryKey: ['user-businesses'],
|
||||
queryFn: () => base44.entities.Business.filter({ contact_name: user?.full_name }),
|
||||
enabled: !!user,
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
const createRapidOrderMutation = useMutation({
|
||||
mutationFn: (orderData) => base44.entities.Event.create(orderData),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['events'] });
|
||||
toast({
|
||||
title: "✅ RAPID Order Created",
|
||||
description: "Order sent to preferred vendor with priority notification",
|
||||
});
|
||||
if (onOrderCreated) onOrderCreated(data);
|
||||
// Reset
|
||||
setConversation([]);
|
||||
setDetectedOrder(null);
|
||||
setMessage("");
|
||||
},
|
||||
});
|
||||
|
||||
const analyzeMessage = async (msg) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
// Add user message to conversation
|
||||
setConversation(prev => [...prev, { role: 'user', content: msg }]);
|
||||
|
||||
try {
|
||||
// Use AI to parse the message
|
||||
const response = await base44.integrations.Core.InvokeLLM({
|
||||
prompt: `You are an order assistant. Analyze this message and extract order details:
|
||||
|
||||
Message: "${msg}"
|
||||
Current user: ${user?.full_name}
|
||||
User's locations: ${businesses.map(b => b.business_name).join(', ')}
|
||||
|
||||
Extract:
|
||||
1. Urgency keywords (ASAP, today, emergency, call out, urgent, rapid, now)
|
||||
2. Role/position needed (cook, bartender, server, dishwasher, etc.)
|
||||
3. Number of staff (if mentioned)
|
||||
4. Time frame (if mentioned)
|
||||
5. Location (if mentioned, otherwise use first available location)
|
||||
|
||||
Return a concise summary.`,
|
||||
response_json_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
is_urgent: { type: "boolean" },
|
||||
role: { type: "string" },
|
||||
count: { type: "number" },
|
||||
location: { type: "string" },
|
||||
time_mentioned: { type: "boolean" },
|
||||
start_time: { type: "string" },
|
||||
end_time: { type: "string" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = response;
|
||||
const primaryLocation = businesses[0]?.business_name || "Primary Location";
|
||||
|
||||
const order = {
|
||||
is_rapid: parsed.is_urgent || true,
|
||||
role: parsed.role || "Staff Member",
|
||||
count: parsed.count || 1,
|
||||
location: parsed.location || primaryLocation,
|
||||
start_time: parsed.start_time || "ASAP",
|
||||
end_time: parsed.end_time || "End of shift",
|
||||
business_name: primaryLocation,
|
||||
hub: businesses[0]?.hub_building || "Main Hub"
|
||||
};
|
||||
|
||||
setDetectedOrder(order);
|
||||
|
||||
// AI response
|
||||
const aiMessage = `Is this a RAPID ORDER for **${order.count} ${order.role}${order.count > 1 ? 's' : ''}** at **${order.location}**?\n\nTime: ${order.start_time} → ${order.end_time}`;
|
||||
|
||||
setConversation(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: aiMessage,
|
||||
showConfirm: true
|
||||
}]);
|
||||
|
||||
} catch (error) {
|
||||
setConversation(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: "I couldn't process that. Please provide more details like: role needed, how many, and when."
|
||||
}]);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!message.trim()) return;
|
||||
analyzeMessage(message);
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const handleConfirmOrder = () => {
|
||||
if (!detectedOrder) return;
|
||||
|
||||
const now = new Date();
|
||||
const orderData = {
|
||||
event_name: `RAPID: ${detectedOrder.count} ${detectedOrder.role}${detectedOrder.count > 1 ? 's' : ''}`,
|
||||
is_rapid: true,
|
||||
status: "Pending",
|
||||
business_name: detectedOrder.business_name,
|
||||
hub: detectedOrder.hub,
|
||||
event_location: detectedOrder.location,
|
||||
date: now.toISOString().split('T')[0],
|
||||
requested: detectedOrder.count,
|
||||
client_name: user?.full_name,
|
||||
client_email: user?.email,
|
||||
notes: `RAPID ORDER - ${detectedOrder.start_time} to ${detectedOrder.end_time}`,
|
||||
shifts: [{
|
||||
shift_name: "Emergency Shift",
|
||||
roles: [{
|
||||
role: detectedOrder.role,
|
||||
count: detectedOrder.count,
|
||||
start_time: "ASAP",
|
||||
end_time: "End of shift"
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
createRapidOrderMutation.mutate(orderData);
|
||||
};
|
||||
|
||||
const handleEditOrder = () => {
|
||||
setConversation(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: "Please describe what you'd like to change."
|
||||
}]);
|
||||
setDetectedOrder(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-red-50 via-orange-50 to-yellow-50 border-2 border-red-300 shadow-xl">
|
||||
<CardHeader className="border-b border-red-200 bg-white/50 backdrop-blur-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-red-500 to-orange-500 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<Zap className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-xl font-bold text-red-700 flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
RAPID Order Assistant
|
||||
</CardTitle>
|
||||
<p className="text-xs text-red-600 mt-1">Emergency staffing in minutes</p>
|
||||
</div>
|
||||
<Badge className="bg-red-600 text-white font-bold text-sm px-4 py-2 shadow-md animate-pulse">
|
||||
URGENT
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-6">
|
||||
{/* Chat Messages */}
|
||||
<div className="space-y-4 mb-6 max-h-[400px] overflow-y-auto">
|
||||
{conversation.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-red-500 to-orange-500 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<Zap className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-lg text-slate-900 mb-2">Need staff urgently?</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">Just describe what you need, I'll handle the rest</p>
|
||||
<div className="text-left max-w-md mx-auto space-y-2">
|
||||
<div className="bg-white p-3 rounded-lg border border-slate-200 text-xs text-slate-600">
|
||||
<strong>Example:</strong> "We had a call out. Need 2 cooks ASAP"
|
||||
</div>
|
||||
<div className="bg-white p-3 rounded-lg border border-slate-200 text-xs text-slate-600">
|
||||
<strong>Example:</strong> "Emergency! Need bartender for tonight"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{conversation.map((msg, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div className={`max-w-[80%] ${
|
||||
msg.role === 'user'
|
||||
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white'
|
||||
: 'bg-white border-2 border-red-200'
|
||||
} rounded-2xl p-4 shadow-md`}>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-6 h-6 bg-gradient-to-br from-red-500 to-orange-500 rounded-full flex items-center justify-center">
|
||||
<Sparkles className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-xs font-bold text-red-600">AI Assistant</span>
|
||||
</div>
|
||||
)}
|
||||
<p className={`text-sm whitespace-pre-line ${msg.role === 'user' ? 'text-white' : 'text-slate-900'}`}>
|
||||
{msg.content}
|
||||
</p>
|
||||
|
||||
{msg.showConfirm && detectedOrder && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 p-3 bg-gradient-to-br from-slate-50 to-blue-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Users className="w-4 h-4 text-blue-600" />
|
||||
<div>
|
||||
<p className="text-slate-500">Staff Needed</p>
|
||||
<p className="font-bold text-slate-900">{detectedOrder.count} {detectedOrder.role}{detectedOrder.count > 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<MapPin className="w-4 h-4 text-blue-600" />
|
||||
<div>
|
||||
<p className="text-slate-500">Location</p>
|
||||
<p className="font-bold text-slate-900">{detectedOrder.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs col-span-2">
|
||||
<Clock className="w-4 h-4 text-blue-600" />
|
||||
<div>
|
||||
<p className="text-slate-500">Time</p>
|
||||
<p className="font-bold text-slate-900">{detectedOrder.start_time} → {detectedOrder.end_time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleConfirmOrder}
|
||||
disabled={createRapidOrderMutation.isPending}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white font-bold shadow-lg"
|
||||
>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{createRapidOrderMutation.isPending ? "Creating..." : "CONFIRM & SEND"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleEditOrder}
|
||||
variant="outline"
|
||||
className="border-2 border-red-300 hover:bg-red-50"
|
||||
>
|
||||
<Edit3 className="w-4 h-4 mr-2" />
|
||||
EDIT
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{isProcessing && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div className="bg-white border-2 border-red-200 rounded-2xl p-4 shadow-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 bg-gradient-to-br from-red-500 to-orange-500 rounded-full flex items-center justify-center animate-pulse">
|
||||
<Sparkles className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">Processing your request...</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||
placeholder="Describe what you need... (e.g., 'Need 2 cooks ASAP')"
|
||||
className="flex-1 border-2 border-red-300 focus:border-red-500 text-base"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!message.trim() || isProcessing}
|
||||
className="bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white font-bold shadow-lg"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Helper Text */}
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-blue-800">
|
||||
<strong>Tip:</strong> Include role, quantity, and urgency for fastest processing.
|
||||
AI will auto-detect your location and send to your preferred vendor.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user