fixed page state while generating a message
This commit is contained in:
@@ -2,167 +2,256 @@
|
||||
ChatHistory Component — padhle (Refined Workspace)
|
||||
======================================== */
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import Message from "../message/Message";
|
||||
import SelectorFlow from "../selector-flow/SelectorFlow";
|
||||
import "./ChatHistory.css";
|
||||
|
||||
const ChatHistory = ({
|
||||
messages,
|
||||
activeSubject,
|
||||
activeGrade,
|
||||
activeChapter,
|
||||
onPromptClick,
|
||||
isTyping,
|
||||
selectorStep,
|
||||
selectorData,
|
||||
messages,
|
||||
activeSubject,
|
||||
activeGrade,
|
||||
activeChapter,
|
||||
onPromptClick,
|
||||
isTyping,
|
||||
selectorStep,
|
||||
selectorData,
|
||||
}) => {
|
||||
const hasMessages = messages.length > 0;
|
||||
const isSelector = selectorStep !== "chat";
|
||||
const hasMessages = messages.length > 0;
|
||||
const isSelector = selectorStep !== "chat";
|
||||
|
||||
const suggestions = [
|
||||
{
|
||||
id: 1,
|
||||
icon: "eco",
|
||||
iconFilled: true,
|
||||
color: "--color-accent-green",
|
||||
colorBg: "--color-accent-green-bg",
|
||||
keywords: ["photosynthesis"],
|
||||
text: "Explain how ",
|
||||
textEnd: " works in simple terms",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: "calculate",
|
||||
iconFilled: false,
|
||||
color: "--color-accent-blue",
|
||||
colorBg: "--color-accent-blue-bg",
|
||||
keywords: ["derivatives"],
|
||||
text: "Help me understand ",
|
||||
textEnd: " in calculus",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: "account_balance",
|
||||
iconFilled: false,
|
||||
color: "--color-accent-orange",
|
||||
colorBg: "--color-accent-orange-bg",
|
||||
keywords: ["Industrial"],
|
||||
text: "What caused the ",
|
||||
textEnd: " Revolution?",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: "edit",
|
||||
iconFilled: true,
|
||||
color: "--color-accent-purple",
|
||||
colorBg: "--color-accent-purple-bg",
|
||||
keywords: ["Romeo and Juliet"],
|
||||
text: "Help me analyze themes in ",
|
||||
textEnd: "",
|
||||
},
|
||||
];
|
||||
const suggestions = [
|
||||
{
|
||||
id: 1,
|
||||
icon: "eco",
|
||||
iconFilled: true,
|
||||
color: "--color-accent-green",
|
||||
colorBg: "--color-accent-green-bg",
|
||||
keywords: ["photosynthesis"],
|
||||
text: "Explain how ",
|
||||
textEnd: " works in simple terms",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: "calculate",
|
||||
iconFilled: false,
|
||||
color: "--color-accent-blue",
|
||||
colorBg: "--color-accent-blue-bg",
|
||||
keywords: ["derivatives"],
|
||||
text: "Help me understand ",
|
||||
textEnd: " in calculus",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: "account_balance",
|
||||
iconFilled: false,
|
||||
color: "--color-accent-orange",
|
||||
colorBg: "--color-accent-orange-bg",
|
||||
keywords: ["Industrial"],
|
||||
text: "What caused the ",
|
||||
textEnd: " Revolution?",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: "edit",
|
||||
iconFilled: true,
|
||||
color: "--color-accent-purple",
|
||||
colorBg: "--color-accent-purple-bg",
|
||||
keywords: ["Romeo and Juliet"],
|
||||
text: "Help me analyze themes in ",
|
||||
textEnd: "",
|
||||
},
|
||||
];
|
||||
|
||||
const fullPromptTexts = suggestions.map((s) => s.text + s.keywords.join(" ") + s.textEnd);
|
||||
const fullPromptTexts = suggestions.map(
|
||||
(s) => s.text + s.keywords.join(" ") + s.textEnd,
|
||||
);
|
||||
|
||||
const streamingMsg = messages.find((m) => m.streaming);
|
||||
const streamingMsg = messages.find((m) => m.streaming);
|
||||
const historyRef = useRef(null);
|
||||
|
||||
/* ─── Messages mode ─── */
|
||||
if (hasMessages) {
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<div className="chat-history__list">
|
||||
{messages.map((message) => (
|
||||
<Message key={message.id} message={message} />
|
||||
))}
|
||||
{isTyping && !streamingMsg && (
|
||||
<Message message={{ id: "typing", role: "assistant", text: "", streaming: true }} />
|
||||
)}
|
||||
<div ref={(el) => el && el.scrollIntoView({ behavior: "smooth", block: "end" })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Scroll only the chat pane, and only when a message is added. Streaming
|
||||
// token updates must not move the page or restart a smooth-scroll animation.
|
||||
useEffect(() => {
|
||||
const history = historyRef.current;
|
||||
if (history) history.scrollTop = history.scrollHeight;
|
||||
}, [messages.length]);
|
||||
|
||||
/* ─── Selector mode (grade → subject → chapter) ─── */
|
||||
if (isSelector && selectorData) {
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<SelectorFlow
|
||||
title={selectorData.title}
|
||||
subtitle={selectorData.subtitle}
|
||||
selected={selectorData.selected}
|
||||
items={selectorData.items}
|
||||
onSelect={selectorData.onSelect}
|
||||
step={selectorStep}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/* ─── Messages mode ─── */
|
||||
if (hasMessages) {
|
||||
return (
|
||||
<div className="chat-history" ref={historyRef}>
|
||||
<div className="chat-history__list">
|
||||
{messages.map((message) => (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
/>
|
||||
))}
|
||||
{isTyping && !streamingMsg && (
|
||||
<Message
|
||||
message={{
|
||||
id: "typing",
|
||||
role: "assistant",
|
||||
text: "",
|
||||
streaming: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Welcome / suggestions mode ─── */
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<div className="chat-history__focus">
|
||||
{/* Welcome Header */}
|
||||
<div className="chat-history__welcome">
|
||||
<div className="chat-history__welcome-icon">
|
||||
<span className="material-symbols-outlined" style={{ fontWeight: 200 }}>menu_book</span>
|
||||
<span className="material-symbols-outlined chat-history__welcome-sparkle">auto_awesome</span>
|
||||
</div>
|
||||
<h2 className="chat-history__welcome-title">Welcome to padhle</h2>
|
||||
<p className="chat-history__welcome-subtitle">Your AI tutor for grades 6–12. Ask anything!</p>
|
||||
</div>
|
||||
/* ─── Selector mode (grade → subject → chapter) ─── */
|
||||
if (isSelector && selectorData) {
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<SelectorFlow
|
||||
title={selectorData.title}
|
||||
subtitle={selectorData.subtitle}
|
||||
selected={selectorData.selected}
|
||||
items={selectorData.items}
|
||||
onSelect={selectorData.onSelect}
|
||||
step={selectorStep}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Bento Grid Suggestions */}
|
||||
<div className="chat-history__bento">
|
||||
{suggestions.map((suggestion) => {
|
||||
const colorVar = suggestion.color.replace("--", "var(");
|
||||
const bgColorVar = suggestion.colorBg.replace("--", "var(");
|
||||
/* ─── Welcome / suggestions mode ─── */
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<div className="chat-history__focus">
|
||||
{/* Welcome Header */}
|
||||
<div className="chat-history__welcome">
|
||||
<div className="chat-history__welcome-icon">
|
||||
<span
|
||||
className="material-symbols-outlined"
|
||||
style={{
|
||||
fontWeight: 200,
|
||||
}}
|
||||
>
|
||||
menu_book
|
||||
</span>
|
||||
<span className="material-symbols-outlined chat-history__welcome-sparkle">
|
||||
auto_awesome
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="chat-history__welcome-title">
|
||||
Welcome to padhle
|
||||
</h2>
|
||||
<p className="chat-history__welcome-subtitle">
|
||||
Your AI tutor for grades 6–12.
|
||||
Ask anything!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
className="chat-history__bento-card"
|
||||
onClick={() => {
|
||||
const text = fullPromptTexts[suggestion.id - 1];
|
||||
onPromptClick(text);
|
||||
}}
|
||||
>
|
||||
<div className="chat-history__bento-icon-wrap" style={{ backgroundColor: `var(${bgColorVar})` }}>
|
||||
<span className="material-symbols-outlined" style={{ color: `var(${colorVar})`, fontWeight: suggestion.iconFilled ? 700 : 200 }}>
|
||||
{suggestion.icon}
|
||||
</span>
|
||||
{/* Bento Grid Suggestions */}
|
||||
<div className="chat-history__bento">
|
||||
{suggestions.map((suggestion) => {
|
||||
const colorVar =
|
||||
suggestion.color.replace(
|
||||
"--",
|
||||
"var(",
|
||||
);
|
||||
const bgColorVar =
|
||||
suggestion.colorBg.replace(
|
||||
"--",
|
||||
"var(",
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={
|
||||
suggestion.id
|
||||
}
|
||||
className="chat-history__bento-card"
|
||||
onClick={() => {
|
||||
const text =
|
||||
fullPromptTexts[
|
||||
suggestion.id -
|
||||
1
|
||||
];
|
||||
onPromptClick(
|
||||
text,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="chat-history__bento-icon-wrap"
|
||||
style={{
|
||||
backgroundColor: `var(${bgColorVar})`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="material-symbols-outlined"
|
||||
style={{
|
||||
color: `var(${colorVar})`,
|
||||
fontWeight: suggestion.iconFilled
|
||||
? 700
|
||||
: 200,
|
||||
}}
|
||||
>
|
||||
{
|
||||
suggestion.icon
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="chat-history__bento-text">
|
||||
<span className="chat-history__bento-text-main">
|
||||
{
|
||||
suggestion.text
|
||||
}
|
||||
</span>
|
||||
<span
|
||||
className="chat-history__bento-text-highlight"
|
||||
style={{
|
||||
color: `var(${colorVar})`,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{
|
||||
suggestion
|
||||
.keywords[0]
|
||||
}
|
||||
</span>
|
||||
{
|
||||
suggestion.textEnd
|
||||
}
|
||||
</div>
|
||||
<span className="material-symbols-outlined chat-history__bento-arrow">
|
||||
arrow_forward
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info Panel */}
|
||||
<div className="chat-history__info">
|
||||
<div className="chat-history__info-icon-wrap">
|
||||
<span className="material-symbols-outlined">
|
||||
lightbulb
|
||||
</span>
|
||||
</div>
|
||||
<div className="chat-history__info-content">
|
||||
<h4 className="chat-history__info-title">
|
||||
Focused on your learning
|
||||
</h4>
|
||||
<p className="chat-history__info-text">
|
||||
I'll answer questions
|
||||
related to your selected
|
||||
subject and chapter
|
||||
<br />
|
||||
using your textbook
|
||||
content.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-history__bento-text">
|
||||
<span className="chat-history__bento-text-main">{suggestion.text}</span>
|
||||
<span className="chat-history__bento-text-highlight" style={{ color: `var(${colorVar})`, fontWeight: 600 }}>
|
||||
{suggestion.keywords[0]}
|
||||
</span>
|
||||
{suggestion.textEnd}
|
||||
</div>
|
||||
<span className="material-symbols-outlined chat-history__bento-arrow">arrow_forward</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info Panel */}
|
||||
<div className="chat-history__info">
|
||||
<div className="chat-history__info-icon-wrap">
|
||||
<span className="material-symbols-outlined">lightbulb</span>
|
||||
</div>
|
||||
<div className="chat-history__info-content">
|
||||
<h4 className="chat-history__info-title">Focused on your learning</h4>
|
||||
<p className="chat-history__info-text">
|
||||
I'll answer questions related to your selected subject and chapter
|
||||
<br />
|
||||
using your textbook content.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatHistory;
|
||||
|
||||
Reference in New Issue
Block a user