import { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardFooter, CardHeader } from '@/components/ui/card'; import { Bot, X, Send, Sparkles } from 'lucide-react'; import { apiRequest } from '@/lib/queryClient'; import { cn } from '@/lib/utils'; interface Message { role: 'user' | 'assistant'; content: string; } // Typing indicator with animated dots - v2 function TypingIndicator() { return (
); } export function AiChat() { const { t } = useTranslation(); const queryClient = useQueryClient(); const [isOpen, setIsOpen] = useState(false); const [input, setInput] = useState(''); const [messages, setMessages] = useState([]); const [isWaitingForResponse, setIsWaitingForResponse] = useState(false); const scrollRef = useRef(null); const mutation = useMutation({ mutationFn: async (msgs: Message[]) => { const res = await apiRequest("POST", "/api/ai/chat", { messages: msgs }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed"); } return res.json(); }, onSuccess: (data) => { setMessages(prev => [...prev, data]); setIsWaitingForResponse(false); // Refresh tasks and labels in case AI created/modified them queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); queryClient.invalidateQueries({ queryKey: ['/api/labels'] }); queryClient.invalidateQueries({ queryKey: ['/api/user'] }); }, onError: (err: Error) => { setMessages(prev => [...prev, { role: 'assistant', content: t('ai.chat.error', 'Something went wrong') + ": " + err.message }]); setIsWaitingForResponse(false); } }); // Use our own loading state OR mutation.isPending for the indicator const isLoading = isWaitingForResponse || mutation.isPending; useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages, isOpen, isLoading]); const handleSubmit = (e?: React.FormEvent) => { e?.preventDefault(); if (!input.trim() || isLoading) return; const newMsgs: Message[] = [...messages, { role: 'user', content: input }]; setMessages(newMsgs); setInput(''); setIsWaitingForResponse(true); mutation.mutate(newMsgs); }; if (!isOpen) { return ( ); } return (
{t('ai.chat.title', 'AI Assistant')} {isLoading && ( {t('ai.chat.thinking', 'Thinking')}... )}
{messages.length === 0 && (

{t('ai.chat.welcome', 'Hi! I can help you manage tasks. Try: "Create a task to buy groceries tomorrow"')}

)} {messages.map((msg, i) => (
{msg.content}
))} {isLoading && (
{t('ai.chat.thinking', 'Thinking')}...
)}
setInput(e.target.value)} placeholder={t('ai.chat.placeholder', 'Ask me anything...')} className="bg-muted/50 focus-visible:ring-primary/50" disabled={isLoading} />
); }