Build your first AI chat interface in minutes using the ShipSafe AI-SaaS boilerplate.
Overview
This tutorial walks you through creating a simple AI chat interface that:
- Sends messages to the AI
- Receives responses
- Displays conversation history
- Handles errors gracefully
Step 1: Set Up API Keys
Add your OpenAI API key to .env.local:
OPENAI_API_KEY=sk-your-key-here
Get your API key:
- Go to OpenAI Platform
- Navigate to API Keys
- Create a new secret key
- Copy it to your
.env.localfile
Step 2: Create the Chat Page
Create src/app/chat/page.tsx:
"use client";
import { useState } from "react";
import { useAuth } from "@core/src/lib/firebase/client";
import { useRouter } from "next/navigation";
interface Message {
role: "user" | "assistant";
content: string;
}
export default function ChatPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Redirect if not authenticated
if (!loading && !user) {
router.push("/auth");
return null;
}
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = { role: "user", content: input };
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
try {
const response = await fetch("/api/ai/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: input,
messages: [...messages, userMessage],
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to get response");
}
const assistantMessage: Message = {
role: "assistant",
content: data.data.response,
};
setMessages((prev) => [...prev, assistantMessage]);
} catch (error) {
console.error("Chat error:", error);
alert("Failed to send message. Please try again.");
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-base-200 p-4">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-6">AI Chat</h1>
{/* Messages */}
<div className="bg-base-100 rounded-lg p-4 mb-4 min-h-[400px] max-h-[600px] overflow-y-auto">
{messages.length === 0 ? (
<p className="text-base-content/70 text-center py-8">
Start a conversation by typing a message below.
</p>
) : (
<div className="space-y-4">
{messages.map((msg, idx) => (
<div
key={idx}
className={`flex ${
msg.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-lg p-3 ${
msg.role === "user"
? "bg-primary text-primary-content"
: "bg-base-300 text-base-content"
}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
</div>
</div>
))}
</div>
)}
</div>
{/* Input */}
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleSend()}
placeholder="Type your message..."
className="flex-1 input input-bordered"
disabled={isLoading}
/>
<button
onClick={handleSend}
disabled={isLoading || !input.trim()}
className="btn btn-primary"
>
{isLoading ? (
<span className="loading loading-spinner"></span>
) : (
"Send"
)}
</button>
</div>
</div>
</div>
);
}
Step 3: Add Authentication Check
The chat endpoint requires authentication. Make sure users are logged in:
// The API route already handles this, but you can add client-side check:
import { useAuth } from "@core/src/lib/firebase/client";
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
router.push("/auth");
return null;
}
Step 4: Test Your Chat
-
Start the dev server:
npm run dev -
Navigate to
/chat -
Log in if not already authenticated
-
Send a message like "Hello, how are you?"
-
See the AI response
Step 5: Enhance the UI
Add Message Timestamps
interface Message {
role: "user" | "assistant";
content: string;
timestamp: Date;
}
// When creating messages:
const userMessage: Message = {
role: "user",
content: input,
timestamp: new Date(),
};
Add Typing Indicator
{isLoading && (
<div className="flex justify-start">
<div className="bg-base-300 rounded-lg p-3">
<span className="loading loading-dots"></span>
</div>
</div>
)}
Add Error Handling
try {
const response = await fetch("/api/ai/chat", { ... });
if (response.status === 403) {
// Subscription required
const data = await response.json();
alert(data.message || "Subscription required. Please upgrade your plan.");
} else if (response.status === 400) {
// Invalid input
const data = await response.json();
alert(data.error || "Invalid request");
}
} catch (error) {
// Network or other errors
console.error("Chat error:", error);
alert("Failed to connect. Please check your internet connection.");
}
Common Issues
"Subscription required" Error
Solution: The API requires an active subscription. For development:
- Make sure you have an active subscription, or
- Modify the subscription check temporarily for testing (not recommended for production)
"API key not found" Error
Solution:
- Check
.env.localhasOPENAI_API_KEY - Restart the dev server after adding the key
- Verify the key is valid
Messages Not Appearing
Solution:
- Check browser console for errors
- Verify authentication is working
- Check network tab for API responses
Next Steps
- Learn about Streaming Responses for real-time updates
- Explore Custom Prompts for specialized assistants
- Check out AI Components for pre-built UI