fix: AI chat loading indicator and auto-refresh tasks
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Add animated typing indicator (bouncing dots) when AI is processing - Show "Thinking..." status in chat header while processing - Add translations for AI chat (en/de): title, welcome, placeholder, thinking, error - Auto-refresh tasks, labels, and user data after AI response - Disable input field while AI is processing - Fix translation keys to use ai.chat.* namespace
This commit is contained in:
@@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardFooter, CardHeader } from '@/components/ui/card';
|
import { Card, CardFooter, CardHeader } from '@/components/ui/card';
|
||||||
import { Bot, X, Send, Sparkles, Loader2 } from 'lucide-react';
|
import { Bot, X, Send, Sparkles } from 'lucide-react';
|
||||||
import { apiRequest } from '@/lib/queryClient';
|
import { apiRequest } from '@/lib/queryClient';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -13,8 +13,20 @@ interface Message {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Typing indicator with animated dots
|
||||||
|
function TypingIndicator() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||||
|
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||||
|
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AiChat() {
|
export function AiChat() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
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[]>([]);
|
||||||
@@ -31,9 +43,13 @@ export function AiChat() {
|
|||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setMessages(prev => [...prev, data]);
|
setMessages(prev => [...prev, data]);
|
||||||
|
// 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) => {
|
onError: (err: Error) => {
|
||||||
setMessages(prev => [...prev, { role: 'assistant', content: t('ai.error') + ": " + err.message }]);
|
setMessages(prev => [...prev, { role: 'assistant', content: t('ai.chat.error', 'Something went wrong') + ": " + err.message }]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +86,12 @@ export function AiChat() {
|
|||||||
<CardHeader className="p-4 border-b bg-primary/5 flex flex-row items-center justify-between shrink-0">
|
<CardHeader className="p-4 border-b bg-primary/5 flex flex-row items-center justify-between shrink-0">
|
||||||
<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.title')}
|
{t('ai.chat.title', 'AI Assistant')}
|
||||||
|
{mutation.isPending && (
|
||||||
|
<span className="ml-2 text-xs font-normal text-muted-foreground animate-pulse">
|
||||||
|
{t('ai.chat.thinking', 'Thinking')}...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setIsOpen(false)}>
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setIsOpen(false)}>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
@@ -81,7 +102,7 @@ export function AiChat() {
|
|||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="text-center text-muted-foreground mt-10">
|
<div className="text-center text-muted-foreground mt-10">
|
||||||
<Bot className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
<Bot className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
<p className="text-sm">{t('ai.welcome')}</p>
|
<p className="text-sm px-4">{t('ai.chat.welcome', 'Hi! I can help you manage tasks. Try: "Create a task to buy groceries tomorrow"')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.map((msg, i) => (
|
{messages.map((msg, i) => (
|
||||||
@@ -98,9 +119,9 @@ export function AiChat() {
|
|||||||
))}
|
))}
|
||||||
{mutation.isPending && (
|
{mutation.isPending && (
|
||||||
<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-2 flex items-center gap-2">
|
<div className="bg-muted rounded-2xl rounded-bl-none px-4 py-3 flex items-center gap-3">
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<TypingIndicator />
|
||||||
<span className="text-xs text-muted-foreground">{t('ai.thinking')}</span>
|
<span className="text-sm text-muted-foreground">{t('ai.chat.thinking', 'Thinking')}...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -111,8 +132,9 @@ export function AiChat() {
|
|||||||
<Input
|
<Input
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder={t('ai.placeholder')}
|
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}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" size="icon" disabled={mutation.isPending || !input.trim()}>
|
<Button type="submit" size="icon" disabled={mutation.isPending || !input.trim()}>
|
||||||
<Send className="w-4 h-4" />
|
<Send className="w-4 h-4" />
|
||||||
|
|||||||
@@ -434,6 +434,13 @@
|
|||||||
"pullSuccess": "Modell erfolgreich geladen",
|
"pullSuccess": "Modell erfolgreich geladen",
|
||||||
"pullError": "Fehler beim Laden des Modells",
|
"pullError": "Fehler beim Laden des Modells",
|
||||||
"connectError": "Verbindung zu Ollama fehlgeschlagen"
|
"connectError": "Verbindung zu Ollama fehlgeschlagen"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "KI-Assistent",
|
||||||
|
"welcome": "Hallo! Ich kann dir bei Aufgaben helfen, Erinnerungen erstellen und deinen Tag organisieren. Probiere: \"Erstelle eine Aufgabe zum Einkaufen für morgen\"",
|
||||||
|
"placeholder": "Frag mich etwas...",
|
||||||
|
"thinking": "Denke nach",
|
||||||
|
"error": "Etwas ist schiefgelaufen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routines": {
|
"routines": {
|
||||||
|
|||||||
@@ -440,6 +440,13 @@
|
|||||||
"pullSuccess": "Model pulled successfully",
|
"pullSuccess": "Model pulled successfully",
|
||||||
"pullError": "Failed to pull model",
|
"pullError": "Failed to pull model",
|
||||||
"connectError": "Could not connect to Ollama"
|
"connectError": "Could not connect to Ollama"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "AI Assistant",
|
||||||
|
"welcome": "Hi! I can help you manage tasks, create reminders, and organize your day. Try saying: \"Create a task to buy groceries tomorrow\"",
|
||||||
|
"placeholder": "Ask me anything...",
|
||||||
|
"thinking": "Thinking",
|
||||||
|
"error": "Something went wrong"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routines": {
|
"routines": {
|
||||||
|
|||||||
@@ -260,14 +260,10 @@ export default function AiChatPage() {
|
|||||||
onSuccess: (userMessage, vars, context) => { // Returns USER message now
|
onSuccess: (userMessage, vars, context) => { // Returns USER message now
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations", vars.conversationId, "messages"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations", vars.conversationId, "messages"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations"] });
|
||||||
|
// Refresh tasks and labels in case AI created/modified them
|
||||||
// Note: We don't get the bot message here anymore immediately.
|
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
||||||
// The polling (refetchInterval) will pick it up when ready.
|
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/user'] });
|
||||||
// Auto-title if this was the first exchange?
|
|
||||||
// We can still do it, but we won't have the bot response content yet for the title gen.
|
|
||||||
// Maybe title gen should happen on backend in the background too?
|
|
||||||
// For now, let's skip auto-title on first msg OR move it to backend trigger.
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user