Files
smoothschedule/frontend/src/components/booking/GeminiChat.tsx
poduck 4a66246708 Add booking flow, business hours, and dark mode support
Features:
- Complete multi-step booking flow with service selection, date/time picker,
  auth (login/signup with email verification), payment, and confirmation
- Business hours settings page for defining when business is open
- TimeBlock purpose field (BUSINESS_HOURS, CLOSURE, UNAVAILABLE)
- Service resource assignment with prep/takedown time buffers
- Availability checking respects business hours and service buffers
- Customer registration via email verification code

UI/UX:
- Full dark mode support for all booking components
- Separate first/last name fields in signup form
- Back buttons on each wizard step
- Removed auto-redirect from confirmation page

API:
- Public endpoints for services, availability, business hours
- Customer verification and registration endpoints
- Tenant lookup from X-Business-Subdomain header

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 20:20:18 -05:00

135 lines
5.5 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { MessageCircle, X, Send, Sparkles } from 'lucide-react';
import { BookingState, ChatMessage } from './types';
// TODO: Implement Gemini service
const sendMessageToGemini = async (message: string, bookingState: BookingState): Promise<string> => {
// Mock implementation - replace with actual Gemini API call
return "I'm here to help you book your appointment. Please use the booking form above.";
};
interface GeminiChatProps {
currentBookingState: BookingState;
}
export const GeminiChat: React.FC<GeminiChatProps> = ({ currentBookingState }) => {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<ChatMessage[]>([
{ role: 'model', text: 'Hi! I can help you choose a service or answer questions about booking.' }
]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isOpen]);
const handleSend = async () => {
if (!inputText.trim() || isLoading) return;
const userMsg: ChatMessage = { role: 'user', text: inputText };
setMessages(prev => [...prev, userMsg]);
setInputText('');
setIsLoading(true);
try {
const responseText = await sendMessageToGemini(inputText, messages, currentBookingState);
setMessages(prev => [...prev, { role: 'model', text: responseText }]);
} catch (error) {
setMessages(prev => [...prev, { role: 'model', text: "Sorry, I'm having trouble connecting." }]);
} finally {
setIsLoading(false);
}
};
return (
<div className="fixed bottom-6 right-6 z-50 flex flex-col items-end">
{/* Chat Window */}
{isOpen && (
<div className="bg-white w-80 sm:w-96 h-[500px] rounded-2xl shadow-2xl border border-gray-200 flex flex-col overflow-hidden mb-4 animate-in slide-in-from-bottom-10 fade-in duration-200">
<div className="bg-indigo-600 p-4 flex justify-between items-center text-white">
<div className="flex items-center space-x-2">
<Sparkles className="w-4 h-4" />
<span className="font-semibold">Lumina Assistant</span>
</div>
<button onClick={() => setIsOpen(false)} className="hover:bg-indigo-500 rounded-full p-1 transition-colors">
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 scrollbar-hide">
{messages.map((msg, idx) => (
<div
key={idx}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`
max-w-[80%] px-4 py-2 rounded-2xl text-sm
${msg.role === 'user'
? 'bg-indigo-600 text-white rounded-br-none'
: 'bg-white text-gray-800 border border-gray-200 shadow-sm rounded-bl-none'
}
`}
>
{msg.text}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white px-4 py-2 rounded-2xl rounded-bl-none border border-gray-200 shadow-sm">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="p-3 bg-white border-t border-gray-100">
<form
onSubmit={(e) => { e.preventDefault(); handleSend(); }}
className="flex items-center gap-2"
>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Ask about services..."
className="flex-1 px-4 py-2 rounded-full border border-gray-300 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-sm"
/>
<button
type="submit"
disabled={isLoading || !inputText.trim()}
className="p-2 bg-indigo-600 text-white rounded-full hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Send className="w-4 h-4" />
</button>
</form>
</div>
</div>
)}
{/* Toggle Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className={`
p-4 rounded-full shadow-xl transition-all duration-300 flex items-center justify-center
${isOpen ? 'bg-gray-800 rotate-90 scale-0' : 'bg-indigo-600 hover:bg-indigo-700 scale-100'}
`}
style={{display: isOpen ? 'none' : 'flex'}}
>
<MessageCircle className="w-6 h-6 text-white" />
</button>
</div>
);
};