playgo_next / app /components /landing-page-chat-bot.tsx
ChenyuRabbitLove's picture
feat: add embeddedable chat component
c4412d0
"use client";
// Core React and AI chat hooks
import { useState, useRef, useEffect } from "react";
import { useChat } from "ai/react";
// UI Icons for example prompts
import {
AcademicCapIcon,
ClockIcon,
BookOpenIcon,
LightBulbIcon,
BriefcaseIcon,
} from "@heroicons/react/24/outline";
// Extends the basic message type to include streaming state
type MessageWithLoading = {
content: string;
role: string;
isStreaming?: boolean;
};
// Predefined example prompts for users to quickly start conversations
const EXAMPLE_PROMPTS = [
{
title: "教案規劃", // Lesson Planning
prompt: "我想規劃一堂關於數學的課程",
icon: AcademicCapIcon,
},
{
title: "英語對話",
prompt: "我想練習英語對話",
icon: ClockIcon,
},
{
title: "作業批改",
prompt: "我想批改學生的作文作業",
icon: BookOpenIcon,
},
{
title: "數學解題",
prompt: "我想解數學題目",
icon: LightBulbIcon,
},
{
title: "我想學程式",
prompt: "我想學習如何寫程式",
icon: BriefcaseIcon,
},
];
export default function LandingPageChatBot() {
// Initialize chat functionality with error handling and streaming support
const {
messages: rawMessages,
input,
handleInputChange,
handleSubmit,
isLoading,
append,
} = useChat({
api: "/api/landing_page_chat",
streamProtocol: "data",
onError: (error) => {
console.error("Chat error:", error);
},
onFinish: (message) => {
console.log("Chat finished:", message);
// Reset streaming state when message is complete
setMessages((prev) =>
prev.map((msg) => ({ ...msg, isStreaming: false })),
);
},
keepLastMessageOnError: true,
});
// Refs and state management
const chatContainerRef = useRef<HTMLDivElement>(null);
const [hasInteracted, setHasInteracted] = useState(false);
const [messages, setMessages] = useState<MessageWithLoading[]>([]);
// Update messages with streaming state when new messages arrive
useEffect(() => {
setMessages(
rawMessages.map((msg, index) => ({
...msg,
isStreaming:
isLoading &&
index === rawMessages.length - 1 &&
msg.role === "assistant",
})),
);
}, [rawMessages, isLoading]);
// Auto-scroll chat to bottom when new messages arrive
const scrollToBottom = () => {
if (chatContainerRef.current) {
const { scrollHeight, clientHeight } = chatContainerRef.current;
chatContainerRef.current.scrollTop = scrollHeight - clientHeight;
}
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// Message handling functions
const sendMessage = async (text: string) => {
setHasInteracted(true);
await append({
content: text,
role: "user",
});
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
setHasInteracted(true);
handleSubmit(e);
};
return (
<section className="w-full h-[1000px] bg-gradient-to-b from-background-secondary to-background-primary flex items-center justify-center px-4">
<div className="w-full max-w-5xl mx-auto h-full flex items-center">
{/* Initial landing view with example prompts */}
{!hasInteracted && messages.length === 0 ? (
<div className="text-center space-y-8 w-full">
{/* Hero section with title and input */}
<div className="space-y-4">
<h2 className="text-5xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
今天你想學什麼?
</h2>
<p className="text-text-secondary text-xl max-w-2xl mx-auto leading-relaxed">
探索新主題、獲得教學協助,或發現學習資源
</p>
</div>
{/* Message input form */}
<form onSubmit={onSubmit} className="max-w-3xl mx-auto">
<div className="relative flex items-center">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="請問任何關於學習的問題..."
className="w-full p-6 pr-36 rounded-2xl border border-border/50 bg-background-primary/50
backdrop-blur-sm shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/50
text-lg transition-all duration-200 placeholder:text-text-secondary/50"
/>
<button
type="submit"
disabled={isLoading}
className="absolute right-2 p-3 bg-primary text-white rounded-full
hover:bg-primary/90 transition-all duration-200
shadow-md hover:shadow-lg active:scale-95 disabled:opacity-50
disabled:hover:bg-primary disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={`size-6 ${isLoading ? "animate-pulse" : ""}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
/>
</svg>
</button>
</div>
</form>
{/* Example prompts grid */}
<div className="mt-12">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{EXAMPLE_PROMPTS.map((prompt, index) => (
<button
key={index}
onClick={() => sendMessage(prompt.prompt)}
className="group p-4 rounded-2xl border border-border/50 bg-background-primary/30
hover:bg-background-primary/50 backdrop-blur-sm transition-all duration-200
hover:border-primary/50 hover:shadow-lg text-left w-full"
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-3">
<prompt.icon className="w-8 h-8 text-primary" />
<h4 className="font-semibold text-text-primary text-md">
{prompt.title}
</h4>
</div>
<svg
className="w-6 h-6 text-primary opacity-0 group-hover:opacity-100 transition-opacity duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</button>
))}
</div>
</div>
</div>
) : (
/* Chat interface after interaction */
<div className="bg-background-primary/50 backdrop-blur-sm rounded-2xl shadow-lg border border-border/50 overflow-hidden w-full h-[700px] flex flex-col">
{/* Chat header */}
<div className="p-6 border-b border-border/50 bg-background-secondary/30">
<h2 className="text-xl font-semibold text-text-primary">
PlayGO 導覽員
</h2>
</div>
{/* Messages container with auto-scroll */}
<div
ref={chatContainerRef}
className="flex-1 overflow-y-auto p-6 space-y-6"
>
{/* Message bubbles with different styles for user/assistant */}
{messages.map((message, index) => (
<div
key={index}
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] p-4 rounded-2xl shadow-sm ${
message.role === "user"
? "bg-primary text-white rounded-br-none"
: "bg-background-secondary/50 text-text-primary rounded-bl-none"
}`}
>
{message.content}
{message.isStreaming && (
<span className="inline-block w-2.5 h-2.5 ml-1.5 bg-primary rounded-full animate-pulse" />
)}
</div>
</div>
))}
</div>
{/* Input form at bottom of chat */}
<div className="p-6 border-t border-border/50 bg-background-secondary/30">
<form onSubmit={onSubmit}>
<div className="relative flex items-center">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder={
isLoading ? "AI is thinking..." : "Type your message..."
}
disabled={isLoading}
className="w-full p-4 pr-32 rounded-xl border border-border/50 bg-background-primary/50
backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-primary/50
text-base transition-all duration-200 disabled:opacity-50"
/>
<button
type="submit"
disabled={isLoading}
className="absolute right-2 p-2 bg-primary text-white rounded-full
hover:bg-primary/90 transition-all duration-200
shadow-md hover:shadow-lg active:scale-95 disabled:opacity-50
disabled:hover:bg-primary disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={`size-5 ${isLoading ? "animate-pulse" : ""}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
/>
</svg>
</button>
</div>
</form>
</div>
</div>
)}
</div>
</section>
);
}