diff --git a/client/src/components/ai/AIChat.tsx b/client/src/components/ai/AIChat.tsx new file mode 100644 index 0000000..a3a19c8 --- /dev/null +++ b/client/src/components/ai/AIChat.tsx @@ -0,0 +1,363 @@ +import { useState, useRef, useEffect } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useAuth } from "@/hooks/useAuth"; +import { useVoiceContext } from "@/context/VoiceProvider"; +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 { Separator } from "@/components/ui/separator"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Brain, + Send, + Mic, + Bot, + User, + Loader2, + Volume2, + MessageSquare, + Lightbulb +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiRequest } from "@/lib/queryClient"; + +interface Message { + id: string; + role: 'user' | 'assistant'; + content: string; + timestamp: Date; + type?: 'text' | 'voice' | 'insight'; +} + +interface AIChatProps { + className?: string; + variant?: "full" | "compact"; +} + +export default function AIChat({ className, variant = "full" }: AIChatProps) { + const { user } = useAuth(); + const { speak, isListening, startListening } = useVoiceContext(); + const [message, setMessage] = useState(""); + const [messages, setMessages] = useState([ + { + id: "welcome", + role: "assistant", + content: "مرحباً! I'm your AI assistant. I can help you with tasks, financial insights, and answer questions in both Arabic and English. How can I assist you today?", + timestamp: new Date(), + type: "text" + } + ]); + const scrollRef = useRef(null); + const queryClient = useQueryClient(); + + // AI Chat mutation + const chatMutation = useMutation({ + mutationFn: (message: string) => apiRequest('/api/ai/chat', { + method: 'POST', + body: JSON.stringify({ message, userId: user?.id }), + }), + onSuccess: (response) => { + const assistantMessage: Message = { + id: `assistant-${Date.now()}`, + role: "assistant", + content: response.content || "I'm processing your request...", + timestamp: new Date(), + type: "text" + }; + setMessages(prev => [...prev, assistantMessage]); + + // Speak the response + speak(response.content); + }, + onError: (error) => { + const errorMessage: Message = { + id: `error-${Date.now()}`, + role: "assistant", + content: "I'm sorry, I encountered an error processing your request. Please try again.", + timestamp: new Date(), + type: "text" + }; + setMessages(prev => [...prev, errorMessage]); + } + }); + + // AI Insights query + const { data: insights } = useQuery({ + queryKey: ["/api/ai/insights", user?.id], + enabled: !!user, + staleTime: 5 * 60 * 1000, // 5 minutes + }); + + const handleSendMessage = async () => { + if (!message.trim()) return; + + const userMessage: Message = { + id: `user-${Date.now()}`, + role: "user", + content: message, + timestamp: new Date(), + type: "text" + }; + + setMessages(prev => [...prev, userMessage]); + setMessage(""); + chatMutation.mutate(message); + }; + + const handleVoiceMessage = async () => { + try { + const result = await startListening(); + if (result?.transcription) { + const userMessage: Message = { + id: `user-voice-${Date.now()}`, + role: "user", + content: result.transcription, + timestamp: new Date(), + type: "voice" + }; + setMessages(prev => [...prev, userMessage]); + chatMutation.mutate(result.transcription); + } + } catch (error) { + console.error("Voice input error:", error); + } + }; + + const handlePlayMessage = (content: string) => { + speak(content); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + if (variant === "compact") { + return ( + + + + + AI Assistant + + + + +
+ {messages.slice(-3).map((msg) => ( +
+
+ {msg.content} +
+
+ ))} +
+
+ +
+ setMessage(e.target.value)} + onKeyPress={handleKeyPress} + className="text-xs" + /> + +
+
+
+ ); + } + + return ( + + +
+ + + AI Assistant + + Multilingual + + + +
+ +
+
+
+ + + {/* AI Insights Section */} + {insights && ( + + +
+ + AI Insight +
+

+ {insights.content || "Your productivity is trending upward this week!"} +

+
+
+ )} + + {/* Messages */} + +
+ {messages.map((msg) => ( +
+ {msg.role === "assistant" && ( +
+ +
+ )} + +
+
{msg.content}
+ +
+
+ {msg.type === "voice" && ( + + + Voice + + )} + + {msg.timestamp.toLocaleTimeString()} + +
+ + {msg.role === "assistant" && ( + + )} +
+
+ + {msg.role === "user" && ( +
+ +
+ )} +
+ ))} + + {chatMutation.isPending && ( +
+
+ +
+
+
+ + Thinking... +
+
+
+ )} +
+
+ + + + {/* Input */} +
+ setMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={chatMutation.isPending} + /> + +
+ + {/* Quick Actions */} +
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/financial/FinancialRecordForm.tsx b/client/src/components/financial/FinancialRecordForm.tsx new file mode 100644 index 0000000..fb6161c --- /dev/null +++ b/client/src/components/financial/FinancialRecordForm.tsx @@ -0,0 +1,280 @@ +import { useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { CalendarIcon, DollarSign, TrendingUp, TrendingDown } from "lucide-react"; +import { format } from "date-fns"; + +const financialRecordSchema = z.object({ + type: z.enum(["income", "expense"]), + amount: z.string().min(1, "Amount is required").refine((val) => !isNaN(Number(val)) && Number(val) > 0, "Amount must be a positive number"), + category: z.string().min(1, "Category is required"), + description: z.string().optional(), + date: z.date(), +}); + +type FinancialRecordFormData = z.infer; + +interface FinancialRecordFormProps { + type?: "income" | "expense"; + onSuccess?: () => void; + onCancel?: () => void; + className?: string; +} + +export default function FinancialRecordForm({ + type, + onSuccess, + onCancel, + className +}: FinancialRecordFormProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const form = useForm({ + resolver: zodResolver(financialRecordSchema), + defaultValues: { + type: type || "expense", + amount: "", + category: "", + description: "", + date: new Date(), + }, + }); + + const watchType = form.watch("type"); + + const incomeCategories = [ + "Salary", "Freelance", "Business", "Investments", "Rental", "Other Income" + ]; + + const expenseCategories = [ + "Food & Dining", "Transportation", "Shopping", "Entertainment", + "Bills & Utilities", "Healthcare", "Education", "Travel", "Other Expense" + ]; + + const createRecordMutation = useMutation({ + mutationFn: async (data: FinancialRecordFormData) => { + const response = await fetch('/api/financial/records', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + ...data, + amount: parseFloat(data.amount), + }), + }); + if (!response.ok) throw new Error('Failed to create financial record'); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/financial/records"] }); + queryClient.invalidateQueries({ queryKey: ["/api/financial/summary"] }); + toast({ + title: "Record created", + description: `${watchType === "income" ? "Income" : "Expense"} record has been created successfully`, + }); + form.reset(); + onSuccess?.(); + }, + onError: (error) => { + toast({ + title: "Error", + description: "Failed to create financial record. Please try again.", + variant: "destructive", + }); + }, + }); + + const onSubmit = async (data: FinancialRecordFormData) => { + setIsSubmitting(true); + try { + await createRecordMutation.mutateAsync(data); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {watchType === "income" ? ( + + ) : ( + + )} + Add {watchType === "income" ? "Income" : "Expense"} + + + +
+ + {!type && ( + ( + + Type + + + + )} + /> + )} + +
+ ( + + Amount + +
+ + +
+
+ +
+ )} + /> + + ( + + Category + + + + )} + /> +
+ + ( + + Description + +