feat: Implement AI Chat Agent, Email Notifications, and UI enhancements
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-12 08:35:48 +01:00
parent ccfb674318
commit 5d8976b1cd
58 changed files with 7867 additions and 844 deletions
+124
View File
@@ -0,0 +1,124 @@
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, Loader2 } from 'lucide-react';
import { apiRequest } from '@/lib/queryClient';
import { cn } from '@/lib/utils';
interface Message {
role: 'user' | 'assistant';
content: string;
}
export function AiChat() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [input, setInput] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
const scrollRef = useRef<HTMLDivElement>(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]);
},
onError: (err: Error) => {
setMessages(prev => [...prev, { role: 'assistant', content: t('ai.error') + ": " + err.message }]);
}
});
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, isOpen, mutation.isPending]);
const handleSubmit = (e?: React.FormEvent) => {
e?.preventDefault();
if (!input.trim() || mutation.isPending) return;
const newMsgs: Message[] = [...messages, { role: 'user', content: input }];
setMessages(newMsgs);
setInput('');
mutation.mutate(newMsgs);
};
if (!isOpen) {
return (
<Button
onClick={() => setIsOpen(true)}
className="fixed bottom-24 right-8 z-50 rounded-full h-14 w-14 shadow-xl bg-gradient-to-r from-pink-500 to-purple-600 hover:scale-110 transition-transform duration-200"
size="icon"
>
<Sparkles className="h-6 w-6 text-white animate-pulse" />
</Button>
);
}
return (
<Card className="fixed bottom-24 right-8 z-50 w-80 md:w-96 h-[500px] flex flex-col shadow-2xl border-primary/20 animate-in slide-in-from-bottom-10 fade-in duration-200">
<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">
<Bot className="w-5 h-5 text-primary" />
{t('ai.title')}
</div>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setIsOpen(false)}>
<X className="w-4 h-4" />
</Button>
</CardHeader>
<div className="flex-1 overflow-y-auto p-4 space-y-4" ref={scrollRef}>
{messages.length === 0 && (
<div className="text-center text-muted-foreground mt-10">
<Bot className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p className="text-sm">{t('ai.welcome')}</p>
</div>
)}
{messages.map((msg, i) => (
<div key={i} className={cn("flex w-full", msg.role === 'user' ? "justify-end" : "justify-start")}>
<div className={cn(
"max-w-[80%] rounded-2xl px-4 py-2 text-sm shadow-sm whitespace-pre-wrap",
msg.role === 'user'
? "bg-primary text-primary-foreground rounded-br-none"
: "bg-muted text-foreground rounded-bl-none"
)}>
{msg.content}
</div>
</div>
))}
{mutation.isPending && (
<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">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-xs text-muted-foreground">{t('ai.thinking')}</span>
</div>
</div>
)}
</div>
<CardFooter className="p-3 border-t bg-background shrink-0">
<form onSubmit={handleSubmit} className="flex w-full gap-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={t('ai.placeholder')}
className="bg-muted/50 focus-visible:ring-primary/50"
/>
<Button type="submit" size="icon" disabled={mutation.isPending || !input.trim()}>
<Send className="w-4 h-4" />
</Button>
</form>
</CardFooter>
</Card>
);
}