579 lines
15 KiB
JavaScript
579 lines
15 KiB
JavaScript
"use client";
|
|
|
|
import dynamic from "next/dynamic";
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
|
|
const API_BASE = "";
|
|
|
|
function AiChatPage() {
|
|
const { user, token, loading: authLoading } = useAuth();
|
|
const router = useRouter();
|
|
|
|
const [messages, setMessages] = useState([]);
|
|
const [input, setInput] = useState("");
|
|
const [sending, setSending] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const [switchingToHuman, setSwitchingToHuman] = useState(false);
|
|
const [humanRequested, setHumanRequested] = useState(false);
|
|
|
|
const messagesEndRef = useRef(null);
|
|
const inputRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
if (!authLoading && !user) {
|
|
router.push("/login?next=" + encodeURIComponent("/ai-chat"));
|
|
}
|
|
}, [authLoading, user, router]);
|
|
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [messages]);
|
|
|
|
const buildHistory = useCallback(() => {
|
|
|
|
return messages.map((m) => ({
|
|
role: m.role,
|
|
content: m.content,
|
|
}));
|
|
}, [messages]);
|
|
|
|
async function handleSend(e) {
|
|
e?.preventDefault();
|
|
const text = input.trim();
|
|
if (!text || sending) return;
|
|
|
|
setInput("");
|
|
setError(null);
|
|
setSending(true);
|
|
|
|
const userMsg = { role: "user", content: text, id: Date.now() };
|
|
setMessages((prev) => [...prev, userMsg]);
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/ai-chat/message`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
message: text,
|
|
history: buildHistory(),
|
|
}),
|
|
});
|
|
|
|
if (res.status === 401 || res.status === 403) {
|
|
router.push("/login?next=" + encodeURIComponent("/ai-chat"));
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok || !data.success) {
|
|
setError(data.error || "Failed to get a response. Please try again.");
|
|
return;
|
|
}
|
|
|
|
const aiMsg = { role: "assistant", content: data.message, id: Date.now() + 1 };
|
|
setMessages((prev) => [...prev, aiMsg]);
|
|
} catch {
|
|
setError("Network error. Please check your connection and try again.");
|
|
} finally {
|
|
setSending(false);
|
|
inputRef.current?.focus();
|
|
}
|
|
}
|
|
|
|
async function handleHumanChat() {
|
|
if (switchingToHuman || !token) return;
|
|
setSwitchingToHuman(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
message: "Hi, I'd like to speak with a real support agent.",
|
|
}),
|
|
});
|
|
|
|
if (res.status === 401) {
|
|
router.push("/login?next=" + encodeURIComponent("/ai-chat"));
|
|
return;
|
|
}
|
|
|
|
const data = await res.json().catch(() => null);
|
|
|
|
if (!res.ok) {
|
|
setError(data?.message || "Could not connect to live support. Please try again.");
|
|
return;
|
|
}
|
|
|
|
const conv = data;
|
|
|
|
//Flag conversation staff, also should notify them
|
|
await fetch(`${API_BASE}/api/v1/chat/conversations/${conv.id}/request-human`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
try {
|
|
for (const msg of messages) {
|
|
const prefix = msg.role === "assistant" ? "[AI Assistant] " : "[Customer] ";
|
|
await fetch(`${API_BASE}/api/v1/chat/conversations/${conv.id}/messages`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({ content: prefix + msg.content }),
|
|
});
|
|
}
|
|
} catch {
|
|
// Navigate anyway — agent has partial context
|
|
}
|
|
|
|
setHumanRequested(true);
|
|
router.push(`/chat?id=${conv.id}`);
|
|
} catch {
|
|
setError("Network error. Could not connect to live support.");
|
|
} finally {
|
|
setSwitchingToHuman(false);
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(e) {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSend();
|
|
}
|
|
}
|
|
|
|
if (authLoading) {
|
|
return (
|
|
<main style={s.page}>
|
|
<p style={s.loading}>Loading...</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<main style={s.page}>
|
|
<section style={s.hero}>
|
|
<h1 style={s.heroTitle}>AI Pet Assistant</h1>
|
|
<p style={s.heroSubtitle}>
|
|
Ask me anything about pet care, adoption advice, or your pets!
|
|
</p>
|
|
<div style={s.titleDecoration} />
|
|
</section>
|
|
|
|
<section style={s.chatSection}>
|
|
<div style={s.chatCard}>
|
|
<div style={s.chatHeader}>
|
|
<div style={s.chatHeaderLeft}>
|
|
<div style={s.aiAvatar}>🐾</div>
|
|
<div>
|
|
<div style={s.chatHeaderTitle}>Leon's Pet Assistant</div>
|
|
<div style={s.chatHeaderStatus}>
|
|
<span style={s.statusDot} /> Online
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
style={{
|
|
...s.humanBtn,
|
|
...(switchingToHuman ? s.humanBtnDisabled : {}),
|
|
}}
|
|
onClick={handleHumanChat}
|
|
disabled={switchingToHuman}
|
|
title="Connect with a human support agent"
|
|
>
|
|
{switchingToHuman ? "Connecting..." : "Chat with a Real Person"}
|
|
</button>
|
|
</div>
|
|
|
|
<div style={s.messagesArea}>
|
|
{messages.length === 0 && (
|
|
<div style={s.emptyState}>
|
|
<div style={s.emptyIcon}>🐾</div>
|
|
<p style={s.emptyText}>
|
|
Hello{user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant.
|
|
Ask me about pet recommendations, care tips, supplies, or anything pet-related!
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{messages.map((msg) => (
|
|
<div
|
|
key={msg.id}
|
|
style={{
|
|
...s.messageRow,
|
|
...(msg.role === "user" ? s.messageRowUser : s.messageRowAi),
|
|
}}
|
|
>
|
|
{msg.role === "assistant" && (
|
|
<div style={s.aiAvatarSmall}>🐾</div>
|
|
)}
|
|
<div
|
|
style={{
|
|
...s.messageBubble,
|
|
...(msg.role === "user" ? s.bubbleUser : s.bubbleAi),
|
|
}}
|
|
>
|
|
{msg.content.split("\n").map((line, i) => (
|
|
<span key={i}>
|
|
{line}
|
|
{i < msg.content.split("\n").length - 1 && <br />}
|
|
</span>
|
|
))}
|
|
</div>
|
|
{msg.role === "user" && (
|
|
<div style={s.userAvatarSmall}>
|
|
{user.fullName ? user.fullName.charAt(0).toUpperCase() : "U"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{sending && (
|
|
<div style={{ ...s.messageRow, ...s.messageRowAi }}>
|
|
<div style={s.aiAvatarSmall}>🐾</div>
|
|
<div style={{ ...s.messageBubble, ...s.bubbleAi, ...s.typingBubble }}>
|
|
<span style={s.dot} />
|
|
<span style={{ ...s.dot, animationDelay: "0.2s" }} />
|
|
<span style={{ ...s.dot, animationDelay: "0.4s" }} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{error && (
|
|
<div style={s.errorBar}>
|
|
{error}
|
|
<button style={s.errorClose} onClick={() => setError(null)}>✕</button>
|
|
</div>
|
|
)}
|
|
|
|
{humanRequested && (
|
|
<div style={s.successBar}>
|
|
Connected to live support! Redirecting to chat...
|
|
</div>
|
|
)}
|
|
|
|
<form style={s.inputArea} onSubmit={handleSend}>
|
|
<textarea
|
|
ref={inputRef}
|
|
style={s.textarea}
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Ask me about pet care, adoption, supplies..."
|
|
rows={1}
|
|
disabled={sending}
|
|
maxLength={2000}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
style={{
|
|
...s.sendBtn,
|
|
...((!input.trim() || sending) ? s.sendBtnDisabled : {}),
|
|
}}
|
|
disabled={!input.trim() || sending}
|
|
title="Send message"
|
|
>
|
|
{sending ? "..." : "Send"}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const s = {
|
|
page: {
|
|
minHeight: "100vh",
|
|
background: "#fafaf8",
|
|
fontFamily: "inherit",
|
|
},
|
|
loading: {
|
|
textAlign: "center",
|
|
padding: "4rem",
|
|
color: "#888",
|
|
fontSize: "1rem",
|
|
},
|
|
hero: {
|
|
background: "linear-gradient(135deg, #ff8c00 0%, #ffa500 100%)",
|
|
padding: "2.5rem 1.5rem 2rem",
|
|
textAlign: "center",
|
|
color: "white",
|
|
},
|
|
heroTitle: {
|
|
fontSize: "clamp(1.6rem, 4vw, 2.4rem)",
|
|
fontWeight: 800,
|
|
margin: 0,
|
|
letterSpacing: "-0.5px",
|
|
},
|
|
heroSubtitle: {
|
|
fontSize: "clamp(0.9rem, 2vw, 1.1rem)",
|
|
marginTop: "0.5rem",
|
|
opacity: 0.9,
|
|
},
|
|
titleDecoration: {
|
|
width: 60,
|
|
height: 4,
|
|
background: "rgba(255,255,255,0.6)",
|
|
borderRadius: 2,
|
|
margin: "1rem auto 0",
|
|
},
|
|
chatSection: {
|
|
maxWidth: 800,
|
|
margin: "0 auto",
|
|
padding: "1.5rem 1rem 2rem",
|
|
},
|
|
chatCard: {
|
|
background: "white",
|
|
borderRadius: 16,
|
|
boxShadow: "0 4px 24px rgba(0,0,0,0.08)",
|
|
overflow: "hidden",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
height: "calc(100vh - 220px)",
|
|
minHeight: 450,
|
|
},
|
|
chatHeader: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
padding: "1rem 1.25rem",
|
|
borderBottom: "1px solid #f0f0f0",
|
|
background: "#fff",
|
|
flexShrink: 0,
|
|
},
|
|
chatHeaderLeft: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.75rem",
|
|
},
|
|
aiAvatar: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: "50%",
|
|
background: "linear-gradient(135deg, #ff8c00, #ffa500)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: "1.3rem",
|
|
flexShrink: 0,
|
|
},
|
|
chatHeaderTitle: {
|
|
fontWeight: 700,
|
|
fontSize: "1rem",
|
|
color: "#1a1a1a",
|
|
},
|
|
chatHeaderStatus: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.35rem",
|
|
fontSize: "0.8rem",
|
|
color: "#4CAF50",
|
|
marginTop: 2,
|
|
},
|
|
statusDot: {
|
|
display: "inline-block",
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: "50%",
|
|
background: "#4CAF50",
|
|
},
|
|
humanBtn: {
|
|
background: "white",
|
|
border: "2px solid #ff8c00",
|
|
color: "#ff8c00",
|
|
borderRadius: 8,
|
|
padding: "0.45rem 0.9rem",
|
|
fontSize: "0.82rem",
|
|
fontWeight: 600,
|
|
cursor: "pointer",
|
|
transition: "all 0.2s",
|
|
whiteSpace: "nowrap",
|
|
},
|
|
humanBtnDisabled: {
|
|
opacity: 0.6,
|
|
cursor: "not-allowed",
|
|
},
|
|
messagesArea: {
|
|
flex: 1,
|
|
overflowY: "auto",
|
|
padding: "1.25rem",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "0.75rem",
|
|
},
|
|
emptyState: {
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
textAlign: "center",
|
|
padding: "2rem",
|
|
margin: "auto",
|
|
},
|
|
emptyIcon: {
|
|
fontSize: "3rem",
|
|
marginBottom: "1rem",
|
|
},
|
|
emptyText: {
|
|
color: "#666",
|
|
fontSize: "0.95rem",
|
|
maxWidth: 400,
|
|
lineHeight: 1.6,
|
|
},
|
|
messageRow: {
|
|
display: "flex",
|
|
alignItems: "flex-end",
|
|
gap: "0.5rem",
|
|
},
|
|
messageRowUser: {
|
|
flexDirection: "row-reverse",
|
|
},
|
|
messageRowAi: {
|
|
flexDirection: "row",
|
|
},
|
|
aiAvatarSmall: {
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: "50%",
|
|
background: "linear-gradient(135deg, #ff8c00, #ffa500)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: "0.85rem",
|
|
flexShrink: 0,
|
|
},
|
|
userAvatarSmall: {
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: "50%",
|
|
background: "#ff8c00",
|
|
color: "white",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: "0.8rem",
|
|
fontWeight: 700,
|
|
flexShrink: 0,
|
|
},
|
|
messageBubble: {
|
|
maxWidth: "72%",
|
|
padding: "0.65rem 0.9rem",
|
|
borderRadius: 14,
|
|
fontSize: "0.92rem",
|
|
lineHeight: 1.55,
|
|
wordBreak: "break-word",
|
|
},
|
|
bubbleUser: {
|
|
background: "#ff8c00",
|
|
color: "white",
|
|
borderBottomRightRadius: 4,
|
|
},
|
|
bubbleAi: {
|
|
background: "#f4f4f4",
|
|
color: "#1a1a1a",
|
|
borderBottomLeftRadius: 4,
|
|
},
|
|
typingBubble: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.3rem",
|
|
padding: "0.75rem 1rem",
|
|
},
|
|
dot: {
|
|
display: "inline-block",
|
|
width: 7,
|
|
height: 7,
|
|
borderRadius: "50%",
|
|
background: "#aaa",
|
|
animation: "bounce 1s infinite",
|
|
},
|
|
errorBar: {
|
|
background: "#fff0f0",
|
|
borderTop: "1px solid #ffd0d0",
|
|
color: "#c0392b",
|
|
padding: "0.65rem 1.25rem",
|
|
fontSize: "0.875rem",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
flexShrink: 0,
|
|
},
|
|
errorClose: {
|
|
background: "none",
|
|
border: "none",
|
|
color: "#c0392b",
|
|
cursor: "pointer",
|
|
fontSize: "0.9rem",
|
|
padding: "0 0.25rem",
|
|
},
|
|
successBar: {
|
|
background: "#f0fff4",
|
|
borderTop: "1px solid #c3f0c3",
|
|
color: "#27ae60",
|
|
padding: "0.65rem 1.25rem",
|
|
fontSize: "0.875rem",
|
|
flexShrink: 0,
|
|
},
|
|
inputArea: {
|
|
display: "flex",
|
|
gap: "0.6rem",
|
|
padding: "0.85rem 1.25rem",
|
|
borderTop: "1px solid #f0f0f0",
|
|
background: "#fff",
|
|
flexShrink: 0,
|
|
alignItems: "flex-end",
|
|
},
|
|
textarea: {
|
|
flex: 1,
|
|
border: "1.5px solid #e0e0e0",
|
|
borderRadius: 10,
|
|
padding: "0.6rem 0.85rem",
|
|
fontSize: "0.92rem",
|
|
resize: "none",
|
|
outline: "none",
|
|
fontFamily: "inherit",
|
|
lineHeight: 1.5,
|
|
maxHeight: 120,
|
|
overflowY: "auto",
|
|
transition: "border-color 0.2s",
|
|
},
|
|
sendBtn: {
|
|
background: "#ff8c00",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: 10,
|
|
padding: "0.6rem 1.2rem",
|
|
fontSize: "0.92rem",
|
|
fontWeight: 600,
|
|
cursor: "pointer",
|
|
flexShrink: 0,
|
|
transition: "background 0.2s",
|
|
},
|
|
sendBtnDisabled: {
|
|
background: "#ffd0a0",
|
|
cursor: "not-allowed",
|
|
},
|
|
};
|
|
|
|
export default dynamic(() => Promise.resolve(AiChatPage), { ssr: false });
|