From 9bfc9e2f961dba3268157becd33f93456bbaea9e Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sat, 17 Jan 2026 10:15:58 +0100 Subject: [PATCH] fix: AI chat loading indicator persists until response received - Add separate isWaitingForResponse state to track loading - Set to true before mutation, false only in onSuccess/onError - Combine with mutation.isPending for reliable isLoading state - Typing indicator now stays visible throughout entire AI request --- client/src/components/AiChat.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/src/components/AiChat.tsx b/client/src/components/AiChat.tsx index ce17c7a..77415c4 100644 --- a/client/src/components/AiChat.tsx +++ b/client/src/components/AiChat.tsx @@ -30,6 +30,7 @@ export function AiChat() { 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({ @@ -43,6 +44,7 @@ export function AiChat() { }, 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'] }); @@ -50,22 +52,27 @@ export function AiChat() { }, 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, mutation.isPending]); + }, [messages, isOpen, isLoading]); const handleSubmit = (e?: React.FormEvent) => { e?.preventDefault(); - if (!input.trim() || mutation.isPending) return; + if (!input.trim() || isLoading) return; const newMsgs: Message[] = [...messages, { role: 'user', content: input }]; setMessages(newMsgs); setInput(''); + setIsWaitingForResponse(true); mutation.mutate(newMsgs); }; @@ -87,7 +94,7 @@ export function AiChat() {
{t('ai.chat.title', 'AI Assistant')} - {mutation.isPending && ( + {isLoading && ( {t('ai.chat.thinking', 'Thinking')}... @@ -117,7 +124,7 @@ export function AiChat() {
))} - {mutation.isPending && ( + {isLoading && (
@@ -134,9 +141,9 @@ export function AiChat() { onChange={(e) => setInput(e.target.value)} placeholder={t('ai.chat.placeholder', 'Ask me anything...')} className="bg-muted/50 focus-visible:ring-primary/50" - disabled={mutation.isPending} + disabled={isLoading} /> -