fix: AI chat loading indicator persists until response received
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- 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
This commit is contained in:
@@ -30,6 +30,7 @@ export function AiChat() {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
@@ -43,6 +44,7 @@ export function AiChat() {
|
|||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setMessages(prev => [...prev, data]);
|
setMessages(prev => [...prev, data]);
|
||||||
|
setIsWaitingForResponse(false);
|
||||||
// Refresh tasks and labels in case AI created/modified them
|
// Refresh tasks and labels in case AI created/modified them
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
||||||
@@ -50,22 +52,27 @@ export function AiChat() {
|
|||||||
},
|
},
|
||||||
onError: (err: Error) => {
|
onError: (err: Error) => {
|
||||||
setMessages(prev => [...prev, { role: 'assistant', content: t('ai.chat.error', 'Something went wrong') + ": " + err.message }]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [messages, isOpen, mutation.isPending]);
|
}, [messages, isOpen, isLoading]);
|
||||||
|
|
||||||
const handleSubmit = (e?: React.FormEvent) => {
|
const handleSubmit = (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if (!input.trim() || mutation.isPending) return;
|
if (!input.trim() || isLoading) return;
|
||||||
|
|
||||||
const newMsgs: Message[] = [...messages, { role: 'user', content: input }];
|
const newMsgs: Message[] = [...messages, { role: 'user', content: input }];
|
||||||
setMessages(newMsgs);
|
setMessages(newMsgs);
|
||||||
setInput('');
|
setInput('');
|
||||||
|
setIsWaitingForResponse(true);
|
||||||
mutation.mutate(newMsgs);
|
mutation.mutate(newMsgs);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ export function AiChat() {
|
|||||||
<div className="flex items-center gap-2 font-semibold">
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
<Bot className="w-5 h-5 text-primary" />
|
<Bot className="w-5 h-5 text-primary" />
|
||||||
{t('ai.chat.title', 'AI Assistant')}
|
{t('ai.chat.title', 'AI Assistant')}
|
||||||
{mutation.isPending && (
|
{isLoading && (
|
||||||
<span className="ml-2 text-xs font-normal text-muted-foreground animate-pulse">
|
<span className="ml-2 text-xs font-normal text-muted-foreground animate-pulse">
|
||||||
{t('ai.chat.thinking', 'Thinking')}...
|
{t('ai.chat.thinking', 'Thinking')}...
|
||||||
</span>
|
</span>
|
||||||
@@ -117,7 +124,7 @@ export function AiChat() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{mutation.isPending && (
|
{isLoading && (
|
||||||
<div className="flex w-full justify-start">
|
<div className="flex w-full justify-start">
|
||||||
<div className="bg-muted rounded-2xl rounded-bl-none px-4 py-3 flex items-center gap-3">
|
<div className="bg-muted rounded-2xl rounded-bl-none px-4 py-3 flex items-center gap-3">
|
||||||
<TypingIndicator />
|
<TypingIndicator />
|
||||||
@@ -134,9 +141,9 @@ export function AiChat() {
|
|||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder={t('ai.chat.placeholder', 'Ask me anything...')}
|
placeholder={t('ai.chat.placeholder', 'Ask me anything...')}
|
||||||
className="bg-muted/50 focus-visible:ring-primary/50"
|
className="bg-muted/50 focus-visible:ring-primary/50"
|
||||||
disabled={mutation.isPending}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" size="icon" disabled={mutation.isPending || !input.trim()}>
|
<Button type="submit" size="icon" disabled={isLoading || !input.trim()}>
|
||||||
<Send className="w-4 h-4" />
|
<Send className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user