Add Claude.ai-style homepage with centered empty state

Show centered cat icon + "Ask me anything" + input when no messages
exist. Transition to scrollable messages + bottom input once chat
starts. Auto-create a conversation on first message if none selected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ryan
2026-03-11 09:47:37 -04:00
parent d1cb55ff1a
commit da9b52dda1

View File

@@ -94,7 +94,6 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
const fetched = await conversationService.getAllConversations(); const fetched = await conversationService.getAllConversations();
const parsed = fetched.map((c) => ({ id: c.id, title: c.name })); const parsed = fetched.map((c) => ({ id: c.id, title: c.name }));
setConversations(parsed); setConversations(parsed);
setSelectedConversation(parsed[0] ?? null);
} catch (err) { } catch (err) {
console.error("Failed to load conversations:", err); console.error("Failed to load conversations:", err);
} }
@@ -132,6 +131,14 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
const handleQuestionSubmit = async () => { const handleQuestionSubmit = async () => {
if (!query.trim() || isLoading) return; if (!query.trim() || isLoading) return;
let activeConversation = selectedConversation;
if (!activeConversation) {
const newConv = await conversationService.createConversation();
activeConversation = { title: newConv.name, id: newConv.id };
setSelectedConversation(activeConversation);
setConversations((prev) => [activeConversation!, ...prev]);
}
const currMessages = messages.concat([{ text: query, speaker: "user" }]); const currMessages = messages.concat([{ text: query, speaker: "user" }]);
setMessages(currMessages); setMessages(currMessages);
setQuery(""); setQuery("");
@@ -150,7 +157,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
try { try {
await conversationService.streamQuery( await conversationService.streamQuery(
query, query,
selectedConversation!.id, activeConversation.id,
(event) => { (event) => {
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
if (event.type === "tool_start") { if (event.type === "tool_start") {
@@ -309,21 +316,12 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
</div> </div>
</header> </header>
{/* Conversation title bar */} {messages.length === 0 ? (
{selectedConversation && ( /* ── Empty / homepage state ── */
<div className="bg-warm-white/80 backdrop-blur-sm border-b border-sand-light/50 px-6 py-2.5"> <div className="flex-1 flex flex-col items-center justify-center px-4 gap-6">
<p className="text-xs font-semibold text-warm-gray truncate max-w-2xl mx-auto uppercase tracking-wider">
{selectedConversation.title || "Untitled Conversation"}
</p>
</div>
)}
{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-6">
<div className="max-w-2xl mx-auto flex flex-col gap-3">
{/* Mobile conversation drawer */} {/* Mobile conversation drawer */}
{showConversations && ( {showConversations && (
<div className="md:hidden mb-3 bg-warm-white rounded-2xl border border-sand-light p-3 shadow-sm"> <div className="md:hidden w-full max-w-2xl bg-warm-white rounded-2xl border border-sand-light p-3 shadow-sm">
<ConversationList <ConversationList
conversations={conversations} conversations={conversations}
onCreateNewConversation={handleCreateNewConversation} onCreateNewConversation={handleCreateNewConversation}
@@ -332,50 +330,71 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
/> />
</div> </div>
)} )}
<div className="relative">
<div className="absolute -inset-6 bg-amber-soft/20 rounded-full blur-3xl" />
<img src={catIcon} alt="Simba" className="relative w-20 h-20" />
</div>
<h1
className="text-2xl font-bold text-charcoal"
style={{ fontFamily: "var(--font-display)" }}
>
Ask me anything
</h1>
<div className="w-full max-w-2xl">
<MessageInput
query={query}
handleQueryChange={handleQueryChange}
handleKeyDown={handleKeyDown}
handleQuestionSubmit={handleQuestionSubmit}
setSimbaMode={setSimbaMode}
isLoading={isLoading}
/>
</div>
</div>
) : (
/* ── Active chat state ── */
<>
<div className="flex-1 overflow-y-auto px-4 py-6">
<div className="max-w-2xl mx-auto flex flex-col gap-3">
{/* Mobile conversation drawer */}
{showConversations && (
<div className="md:hidden mb-3 bg-warm-white rounded-2xl border border-sand-light p-3 shadow-sm">
<ConversationList
conversations={conversations}
onCreateNewConversation={handleCreateNewConversation}
onSelectConversation={handleSelectConversation}
selectedId={selectedConversation?.id}
/>
</div>
)}
{/* Empty state */} {messages.map((msg, index) => {
{messages.length === 0 && !isLoading && ( if (msg.speaker === "tool")
<div className="flex flex-col items-center justify-center py-24 gap-5"> return <ToolBubble key={index} text={msg.text} />;
<div className="relative"> if (msg.speaker === "simba")
<div className="absolute -inset-6 bg-amber-soft/20 rounded-full blur-3xl" /> return <AnswerBubble key={index} text={msg.text} />;
<img return <QuestionBubble key={index} text={msg.text} />;
src={catIcon} })}
alt="Simba"
className="relative w-16 h-16 opacity-50" {isLoading && <AnswerBubble text="" loading={true} />}
/> <div ref={messagesEndRef} />
</div>
<p className="text-warm-gray/60 text-sm">
Ask Simba anything
</p>
</div> </div>
)} </div>
{messages.map((msg, index) => { <footer className="border-t border-sand-light/40 bg-cream/80 backdrop-blur-sm">
if (msg.speaker === "tool") <div className="max-w-2xl mx-auto px-4 py-3">
return <ToolBubble key={index} text={msg.text} />; <MessageInput
if (msg.speaker === "simba") query={query}
return <AnswerBubble key={index} text={msg.text} />; handleQueryChange={handleQueryChange}
return <QuestionBubble key={index} text={msg.text} />; handleKeyDown={handleKeyDown}
})} handleQuestionSubmit={handleQuestionSubmit}
setSimbaMode={setSimbaMode}
{isLoading && <AnswerBubble text="" loading={true} />} isLoading={isLoading}
<div ref={messagesEndRef} /> />
</div> </div>
</div> </footer>
</>
{/* Input */} )}
<footer className="border-t border-sand-light/40 bg-cream/80 backdrop-blur-sm">
<div className="max-w-2xl mx-auto px-4 py-3">
<MessageInput
query={query}
handleQueryChange={handleQueryChange}
handleKeyDown={handleKeyDown}
handleQuestionSubmit={handleQuestionSubmit}
setSimbaMode={setSimbaMode}
isLoading={isLoading}
/>
</div>
</footer>
</div> </div>
</div> </div>
); );