Files

172 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Image from "next/image";
import { useState, useRef, useEffect } from "react";
interface Message {
role: "user" | "assistant";
content: string;
}
const RAG_API_URL = "https://ai.apply4jobs.de/api/v1/chat/completions";
export default function ChatWidget() {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
role: "assistant",
content:
"Hallo! Ich bin Sebastians KI-Assistent. Stellen Sie mir gerne Fragen zu seiner Berufserfahrung, seinen Fähigkeiten oder Projekten. 👋\n\nHello! I'm Sebastian's AI assistant. Feel free to ask me about his professional experience, skills or projects.",
},
]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isLoading]);
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = { role: "user", content: input.trim() };
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
try {
const response = await fetch(RAG_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "sebastian-rag",
messages: [...messages, userMessage],
stream: false,
}),
});
const data = await response.json();
const assistantContent =
data?.choices?.[0]?.message?.content ?? "Entschuldigung, ich konnte keine Antwort generieren.";
setMessages((prev) => [
...prev,
{ role: "assistant", content: assistantContent },
]);
} catch {
setMessages((prev) => [
...prev,
{
role: "assistant",
content: "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
},
]);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
return (
<>
{/* Chat Window */}
{isOpen && (
<div className="fixed bottom-24 right-6 z-50 w-96 max-w-[calc(100vw-2rem)] bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-gray-200"
style={{ height: "520px" }}>
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full overflow-hidden bg-white/20 flex-shrink-0">
<Image src="/img/Bewerbungsfoto_2025.jpg" alt="Sebastian Fröhlich" width={36} height={36} className="w-full h-full object-cover object-center" />
</div>
<div>
<p className="text-white font-semibold text-sm">Sebastian Fröhlich</p>
<p className="text-blue-200 text-xs">KI-Assistent · AI Assistant</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="text-white/80 hover:text-white transition-colors text-xl leading-none"
>
×
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3 bg-gray-50">
{messages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm whitespace-pre-wrap ${
msg.role === "user"
? "bg-blue-600 text-white rounded-br-sm"
: "bg-white text-gray-800 shadow-sm border border-gray-100 rounded-bl-sm"
}`}
>
{msg.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white text-gray-500 rounded-2xl rounded-bl-sm px-4 py-3 text-sm shadow-sm border border-gray-100 flex items-center gap-2">
<span className="flex gap-1">
<span className="w-1.5 h-1.5 bg-blue-400 rounded-full animate-bounce" style={{ animationDelay: "0ms" }} />
<span className="w-1.5 h-1.5 bg-blue-400 rounded-full animate-bounce" style={{ animationDelay: "150ms" }} />
<span className="w-1.5 h-1.5 bg-blue-400 rounded-full animate-bounce" style={{ animationDelay: "300ms" }} />
</span>
<span className="text-xs text-gray-400">Antwort wird generiert</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="px-3 py-3 bg-white border-t border-gray-100 flex gap-2 items-end">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ihre Frage / Your question…"
rows={1}
disabled={isLoading}
className="flex-1 resize-none rounded-xl border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:border-blue-400 disabled:opacity-50 max-h-24"
/>
<button
onClick={sendMessage}
disabled={isLoading || !input.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed text-white rounded-xl px-4 py-2 text-sm font-medium transition-colors"
>
</button>
</div>
</div>
)}
{/* Floating Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="fixed bottom-6 right-6 z-50 w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg hover:shadow-xl transition-all flex items-center justify-center"
aria-label="Chat öffnen"
>
{isOpen ? (
<span className="text-2xl leading-none">×</span>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
)}
</button>
</>
);
}