added: grade, subject and chapter in new chat, fixed that shitty card bug

This commit is contained in:
2026-08-18 04:44:10 -04:00
parent ab88ae30f1
commit 93a35a7343
24 changed files with 2683 additions and 66 deletions
+281 -24
View File
@@ -1,48 +1,299 @@
/* ========================================
App Component — padhle (Refined Workspace)
App Component — padhle (Backend-Connected)
======================================== */
import { useState } from "react";
import { useState, useRef, useCallback, useEffect } from "react";
import Sidebar from "./components/sidebar/Sidebar";
import TopNav from "./components/top-nav/TopNav";
import ChatHistory from "./components/chat-history/ChatHistory";
import ChatInput from "./components/chat-input/ChatInput";
import SelectorFlow from "./components/selector-flow/SelectorFlow";
const API_BASE = ""; // Uses Vite proxy → localhost:3001 in dev
// Default selectors
const DEFAULTS = {
grade: "Choose standard",
subject: "Choose Subject",
chapter: "Choose Chapter",
};
function App() {
const [messages, setMessages] = useState([]);
const [activeSubject, setActiveSubject] = useState("Choose Subject");
const [activeSubject, setActiveSubject] = useState(DEFAULTS.subject);
const [activeChat, setActiveChat] = useState(null);
const [activeGrade, setActiveGrade] = useState("Choose standard");
const [activeChapter, setActiveChapter] = useState("Choose Chapter");
const [activeGrade, setActiveGrade] = useState(DEFAULTS.grade);
const [activeChapter, setActiveChapter] = useState(DEFAULTS.chapter);
const [isTyping, setIsTyping] = useState(false);
const [sessions, setSessions] = useState([]);
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
const [showSelectorFlow, setShowSelectorFlow] = useState(false);
const abortRef = useRef(null);
const handleSend = (text) => {
if (!text.trim()) return;
// Load sessions on mount
useEffect(() => {
loadSessions();
}, []);
setMessages((prev) => [...prev, { id: Date.now(), role: "user", text }]);
setTimeout(() => {
setMessages((prev) => [
...prev,
{
id: Date.now() + 1,
role: "assistant",
text: "This is a simulated response. Connect the backend to get real AI answers!",
},
]);
}, 1000);
const loadSessions = async () => {
setIsLoadingSessions(true);
try {
const res = await fetch(`${API_BASE}/api/sessions`);
if (res.ok) {
const data = await res.json();
setSessions(data);
}
} catch (err) {
console.error("Failed to load sessions:", err);
} finally {
setIsLoadingSessions(false);
}
};
const handlePromptClick = (promptText) => {
handleSend(promptText);
const handleNewChat = () => {
// Abort any in-flight request
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
}
// Clear everything
setMessages([]);
setActiveChat(null);
setActiveGrade(DEFAULTS.grade);
setActiveSubject(DEFAULTS.subject);
setActiveChapter(DEFAULTS.chapter);
setIsTyping(false);
// Show the selector flow
setShowSelectorFlow(true);
// Refresh sessions list from backend
loadSessions();
};
const handleChatSelect = async (sessionId) => {
// Abort any in-flight request
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
}
setIsTyping(true);
setMessages([]);
try {
const res = await fetch(`${API_BASE}/api/chat/${sessionId}`);
if (!res.ok) {
throw new Error("Session not found");
}
const data = await res.json();
// Set the session's context (grade, subject, chapter)
if (data.session) {
setActiveGrade(data.session.grade || DEFAULTS.grade);
setActiveSubject(data.session.subject || DEFAULTS.subject);
setActiveChapter(data.session.chapter || DEFAULTS.chapter);
}
// Hide selector flow when loading an existing chat
setShowSelectorFlow(false);
// Load the messages
if (data.messages) {
setMessages(data.messages.map((m) => ({ ...m, streaming: false })));
}
setActiveChat(sessionId);
} catch (err) {
console.error("Failed to load session:", err);
setMessages([
{
id: Date.now(),
role: "assistant",
text: `⚠️ Could not load that chat. It may have been deleted.`,
},
]);
} finally {
setIsTyping(false);
}
};
const handleDeleteChat = async (sessionId) => {
try {
const res = await fetch(`${API_BASE}/api/sessions/${sessionId}`, {
method: "DELETE",
});
if (res.ok) {
// If we were on the deleted chat, clear it
if (activeChat === sessionId) {
handleNewChat();
} else {
loadSessions();
}
}
} catch (err) {
console.error("Failed to delete session:", err);
}
};
const handleSend = useCallback(
(text) => {
if (!text.trim() || isTyping) return;
// Abort any previous in-flight request
if (abortRef.current) {
abortRef.current.abort();
}
// Add user message immediately
const userMsg = { id: Date.now(), role: "user", text };
setMessages((prev) => [...prev, userMsg]);
setIsTyping(true);
const fetchChatId = activeChat;
// Build request body
const body = {
text: text.trim(),
chatId: fetchChatId,
grade: activeGrade !== DEFAULTS.grade ? activeGrade : undefined,
subject: activeSubject !== DEFAULTS.subject ? activeSubject : undefined,
chapter: activeChapter !== DEFAULTS.chapter ? activeChapter : undefined,
};
// Open SSE connection
const controller = new AbortController();
abortRef.current = controller;
fetch(`${API_BASE}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: controller.signal,
})
.then(async (response) => {
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let assistantText = "";
// Create placeholder assistant message
const assistantId = Date.now() + 1;
setMessages((prev) => [
...prev,
{ id: assistantId, role: "assistant", text: "", streaming: true },
]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const jsonStr = line.slice(6);
if (jsonStr === "[DONE]") continue;
try {
const data = JSON.parse(jsonStr);
if (data.type === "chunk") {
assistantText += data.content;
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, text: assistantText }
: m
)
);
} else if (data.type === "done") {
// Mark as non-streaming
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, streaming: false }
: m
)
);
} else if (data.type === "error") {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? {
...m,
text: `⚠️ Error: ${data.message}`,
streaming: false,
}
: m
)
);
}
} catch {
// Skip malformed SSE frames
}
}
}
setIsTyping(false);
abortRef.current = null;
// Refresh sessions to update timestamps
loadSessions();
})
.catch((err) => {
if (err.name === "AbortError") return; // intentional abort
setIsTyping(false);
setMessages((prev) => [
...prev,
{
id: Date.now() + 1,
role: "assistant",
text: `⚠️ Connection error: ${err.message}. Make sure the backend is running on port 3001.`,
},
]);
abortRef.current = null;
});
},
[isTyping, activeChat, activeGrade, activeSubject, activeChapter]
);
const handlePromptClick = useCallback(
(promptText) => {
handleSend(promptText);
},
[handleSend]
);
const handleGradeChange = (gradeLabel) => {
setActiveGrade(gradeLabel);
};
const handleSubjectChange = (subjectId) => {
setActiveSubject(subjectId);
};
const handleChapterChange = (chapterLabel) => {
setActiveChapter(chapterLabel);
};
return (
<div className="page">
<Sidebar
activeSubject={activeSubject}
onSubjectChange={setActiveSubject}
onSubjectChange={handleSubjectChange}
activeChat={activeChat}
onChatSelect={setActiveChat}
onChatSelect={handleChatSelect}
onNewChat={handleNewChat}
sessions={sessions}
onDeleteChat={handleDeleteChat}
isLoadingSessions={isLoadingSessions}
/>
<div className="page__main">
@@ -50,7 +301,7 @@ function App() {
activeGrade={activeGrade}
onGradeChange={setActiveGrade}
activeSubject={activeSubject}
onSubjectChange={setActiveSubject}
onSubjectChange={handleSubjectChange}
activeChapter={activeChapter}
onChapterChange={setActiveChapter}
/>
@@ -62,12 +313,18 @@ function App() {
activeGrade={activeGrade}
activeChapter={activeChapter}
onPromptClick={handlePromptClick}
isTyping={isTyping}
showSelectorFlow={showSelectorFlow}
onGradeChange={handleGradeChange}
onSubjectChange={handleSubjectChange}
onChapterChange={handleChapterChange}
/>
</div>
<ChatInput
onSend={handleSend}
placeholder={`Ask anything about ${activeGrade} ${activeSubject} ${activeChapter}...`}
disabled={isTyping}
/>
</div>
</div>
@@ -3,9 +3,17 @@
======================================== */
import Message from "../message/Message";
import SelectorFlow from "../selector-flow/SelectorFlow";
import "./ChatHistory.css";
const ChatHistory = ({ messages, activeSubject, activeGrade, activeChapter, onPromptClick }) => {
const ChatHistory = ({
messages,
activeGrade,
onPromptClick,
isTyping,
showSelectorFlow,
onGradeChange,
}) => {
const hasMessages = messages.length > 0;
const suggestions = [
@@ -53,6 +61,8 @@ const ChatHistory = ({ messages, activeSubject, activeGrade, activeChapter, onPr
const fullPromptTexts = suggestions.map((s) => s.text + s.keywords.join(" ") + s.textEnd);
const streamingMsg = messages.find((m) => m.streaming);
if (hasMessages) {
return (
<div className="chat-history">
@@ -60,11 +70,23 @@ const ChatHistory = ({ messages, activeSubject, activeGrade, activeChapter, onPr
{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>
);
}
if (showSelectorFlow) {
return (
<div className="chat-history">
<SelectorFlow grade={activeGrade} onGradeChange={onGradeChange} />
</div>
);
}
return (
<div className="chat-history">
<div className="chat-history__focus">
@@ -88,6 +88,11 @@
font-weight: var(--font-weight-regular);
}
.chat-input__field:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Actions Container */
.chat-input__actions {
display: flex;
@@ -150,6 +155,11 @@
opacity: 0.9;
}
.chat-input__send-btn:disabled {
cursor: not-allowed;
opacity: 0.3;
}
.chat-input__send-btn .material-symbols-outlined {
font-size: 20px;
line-height: 1;
@@ -5,7 +5,7 @@
import { useState, useRef, useEffect } from "react";
import "./ChatInput.css";
const ChatInput = ({ onSend, placeholder }) => {
const ChatInput = ({ onSend, placeholder, disabled }) => {
const [text, setText] = useState("");
const textareaRef = useRef(null);
@@ -75,9 +75,9 @@ const ChatInput = ({ onSend, placeholder }) => {
{/* Send */}
<button
className={`chat-input__send-btn ${text.trim() ? "chat-input__send-btn--active" : ""}`}
className={`chat-input__send-btn ${text.trim() && !disabled ? "chat-input__send-btn--active" : ""}`}
type="submit"
disabled={!text.trim()}
disabled={!text.trim() || disabled}
aria-label="Send message"
>
<span className="material-symbols-outlined" style={{ fontWeight: text.trim() ? 700 : 200 }}>
@@ -131,3 +131,43 @@
.message__action--primary:hover {
opacity: 0.9;
}
/* --- TYPING INDICATOR --- */
.message__typing-dots {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
padding-bottom: 2px;
}
.message__typing-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--color-primary);
animation: typingBounce 1.4s infinite ease-in-out;
}
.message__typing-dot:nth-child(1) {
animation-delay: 0s;
}
.message__typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.message__typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingBounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
+20 -3
View File
@@ -2,11 +2,20 @@
Message Component — padhle (Refined Workspace)
======================================== */
import { useEffect, useRef } from "react";
import "./Message.css";
const Message = ({ message }) => {
const { role, text } = message;
const { role, text, streaming } = message;
const isUser = role === "user";
const endRef = useRef(null);
// Auto-scroll to bottom when text updates
useEffect(() => {
if (endRef.current) {
endRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
}
}, [text, streaming]);
return (
<div className={`message ${isUser ? "message--user" : "message--assistant"}`}>
@@ -18,11 +27,18 @@ const Message = ({ message }) => {
</div>
)}
<div className="message__bubble">
<div className="message__text">{text}</div>
<div className="message__text">
{text || (streaming ? "" : null)}
{streaming && <span className="message__typing-dots">
<span className="message__typing-dot"></span>
<span className="message__typing-dot"></span>
<span className="message__typing-dot"></span>
</span>}
</div>
</div>
{/* Action buttons for assistant messages */}
{!isUser && (
{!isUser && text && (
<div className="message__actions">
<button className="message__action" aria-label="Helpful" title="Helpful">
<span className="material-symbols-outlined">thumb_up</span>
@@ -35,6 +51,7 @@ const Message = ({ message }) => {
</button>
</div>
)}
<div ref={endRef} />
</div>
);
};
@@ -0,0 +1,79 @@
.selector-flow {
display: flex;
justify-content: center;
padding: 48px 24px;
width: 100%;
min-height: 320px;
}
.selector-flow__content {
width: 100%;
max-width: 640px;
}
.selector-flow__title {
font-family: var(--font-heading);
font-size: 24px;
font-weight: var(--font-weight-bold);
color: var(--color-on-surface);
text-align: center;
margin-bottom: 4px;
}
.selector-flow__subtitle {
font-size: 14px;
font-weight: var(--font-weight-regular);
color: var(--color-on-surface-variant);
text-align: center;
margin-bottom: 32px;
}
.selector-flow__options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.selector-flow__option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px 12px;
border: var(--border-thin);
border-radius: var(--radius-md);
background-color: var(--color-surface);
cursor: pointer;
font-family: var(--font-body);
font-size: 14px;
font-weight: var(--font-weight-medium);
color: var(--color-on-surface);
text-align: center;
}
.selector-flow__option:hover:not(.selector-flow__option--selected) {
background-color: var(--color-surface-low);
border-color: var(--color-outline);
}
.selector-flow__option--selected {
background-color: var(--color-primary);
color: var(--color-on-primary);
border-color: var(--color-primary);
}
.selector-flow__option--selected:hover {
background-color: var(--color-primary);
border-color: var(--color-primary);
box-shadow: var(--shadow-card-hover);
}
.selector-flow__icon {
font-size: 28px;
color: var(--color-primary);
}
.selector-flow__option--selected .selector-flow__icon {
color: var(--color-on-primary);
}
@@ -0,0 +1,29 @@
import { useState } from "react";
import "./SelectorFlow.css";
const grades = ["Grade 6", "Grade 7", "Grade 8", "Grade 9", "Grade 10", "Grade 11", "Grade 12"];
export default function SelectorFlow({ grade, onGradeChange }) {
const handleClick = (g) => onGradeChange(g);
return (
<div className="selector-flow">
<div className="selector-flow__content">
<h3 className="selector-flow__title">Select your standard</h3>
<p className="selector-flow__subtitle">Choose the grade you are studying in</p>
<div className="selector-flow__options">
{grades.map((g) => (
<button
key={g}
className={`selector-flow__option${grade === g ? " selector-flow__option--selected" : ""}`}
onClick={() => handleClick(g)}
>
<span className="selector-flow__icon">school</span>
<span className="selector-flow__label">{g}</span>
</button>
))}
</div>
</div>
</div>
);
}
+194 -8
View File
@@ -62,12 +62,42 @@
font-size: var(--font-size-label-md);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: opacity var(--transition-fast);
transition: all var(--transition-normal);
margin-bottom: 24px;
position: relative;
overflow: hidden;
}
.sidebar__new-chat::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
opacity: 0;
transition: opacity var(--transition-fast);
}
.sidebar__new-chat:hover {
opacity: 0.9;
background-color: #1a1a1a;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.sidebar__new-chat:hover::before {
opacity: 1;
}
.sidebar__new-chat:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.sidebar__new-chat-icon {
transition: transform var(--transition-normal);
}
.sidebar__new-chat:hover .sidebar__new-chat-icon {
transform: rotate(90deg);
}
/* Section */
@@ -78,9 +108,28 @@
.sidebar__section--history {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar__section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.sidebar__chat-count {
font-size: var(--font-size-label-xs);
font-weight: var(--font-weight-bold);
color: var(--color-on-surface-variant);
background-color: var(--color-surface-container);
padding: 2px 8px;
border-radius: var(--radius-sm);
margin-left: auto;
}
.sidebar__section-title {
font-size: 11px;
font-weight: var(--font-weight-bold);
@@ -88,7 +137,6 @@
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 0 24px;
margin-bottom: 8px;
}
/* Subject List */
@@ -100,6 +148,7 @@
padding: 0 8px;
}
/* Subject Link — Enhanced Hover Effects */
.sidebar__link {
display: flex;
align-items: center;
@@ -115,23 +164,60 @@
font-weight: var(--font-weight-medium);
color: var(--color-on-surface-variant);
text-align: left;
transition: background-color var(--transition-fast), color var(--transition-fast);
transition: all var(--transition-fast);
position: relative;
overflow: hidden;
}
/* Slide indicator — hidden by default, slides in on hover */
.sidebar__link-indicator {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%) scaleY(0);
width: 3px;
height: 60%;
background-color: var(--color-primary);
border-radius: 0 2px 2px 0;
transition: transform var(--transition-fast);
opacity: 0;
}
.sidebar__link:hover {
background-color: var(--color-surface-low);
color: var(--color-on-surface);
padding-left: 20px;
}
.sidebar__link:hover .sidebar__link-indicator {
transform: translateY(-50%) scaleY(1);
opacity: 1;
}
.sidebar__link:hover .sidebar__icon {
color: var(--color-primary);
}
.sidebar__link--active {
background-color: var(--color-surface-low);
color: var(--color-primary);
font-weight: var(--font-weight-bold);
border-left: 2px solid var(--color-primary);
}
.sidebar__link--active .sidebar__link-indicator {
transform: translateY(-50%) scaleY(1);
opacity: 1;
background-color: var(--color-primary);
}
.sidebar__link--active .sidebar__icon {
color: var(--color-primary);
}
.sidebar__icon {
font-size: 20px;
line-height: 1;
transition: color var(--transition-fast);
}
/* Chat List */
@@ -143,10 +229,24 @@
padding: 0 8px;
}
/* Chat Item — wrapper for link + delete */
.sidebar__chat-item {
position: relative;
display: flex;
align-items: center;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.sidebar__chat-item:hover {
background-color: var(--color-surface-low);
}
/* Chat Link */
.sidebar__chat-link {
display: flex;
flex-direction: column;
width: 100%;
width: calc(100% - 32px);
padding: 8px 16px;
border: none;
border-radius: var(--radius-md);
@@ -154,7 +254,8 @@
cursor: pointer;
font-family: var(--font-body);
text-align: left;
transition: background-color var(--transition-fast);
transition: all var(--transition-fast);
z-index: 1;
}
.sidebar__chat-link:hover {
@@ -163,6 +264,7 @@
.sidebar__chat-link--active {
background-color: var(--color-surface-low);
border-left: 2px solid var(--color-primary);
}
.sidebar__chat-preview {
@@ -172,6 +274,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.sidebar__chat-time {
@@ -181,6 +284,87 @@
margin-top: 2px;
}
/* Delete Button — hidden until hover */
.sidebar__chat-delete {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
cursor: pointer;
color: var(--color-on-surface-variant);
opacity: 0;
transform: translateX(4px);
transition: all var(--transition-fast);
flex-shrink: 0;
margin-left: auto;
z-index: 2;
}
.sidebar__chat-item:hover .sidebar__chat-delete {
opacity: 1;
transform: translateX(0);
}
.sidebar__chat-delete:hover {
background-color: #fee2e2;
color: #dc2626;
}
.sidebar__chat-delete .material-symbols-outlined {
font-size: 16px;
line-height: 1;
}
.sidebar__chat-delete--visible {
opacity: 0.6;
transform: translateX(0);
}
.sidebar__chat-delete--visible:hover {
opacity: 1;
background-color: #fee2e2;
color: #dc2626;
}
/* Loading State */
.sidebar__loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 24px 16px;
color: var(--color-on-surface-variant);
font-size: var(--font-size-label-md);
}
.sidebar__loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--color-outline-variant);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Empty State */
.sidebar__empty {
padding: 24px 16px;
color: var(--color-on-surface-variant);
font-size: var(--font-size-label-md);
text-align: center;
font-style: italic;
}
/* Footer */
.sidebar__footer {
display: flex;
@@ -234,11 +418,12 @@
gap: 8px;
position: relative;
cursor: pointer;
transition: border-color var(--transition-fast);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.sidebar__upgrade:hover {
border-color: var(--color-outline);
box-shadow: var(--shadow-card-hover);
}
.sidebar__upgrade-header {
@@ -278,4 +463,5 @@
.sidebar__upgrade:hover .sidebar__upgrade-arrow {
color: var(--color-primary);
transform: translateY(-50%) translateX(2px);
}
+69 -23
View File
@@ -4,7 +4,16 @@
import "./Sidebar.css";
const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) => {
const Sidebar = ({
activeSubject,
onSubjectChange,
activeChat,
onChatSelect,
onNewChat,
sessions,
onDeleteChat,
isLoadingSessions,
}) => {
const subjects = [
{ id: "math", name: "Mathematics", icon: "functions" },
{ id: "science", name: "Science", icon: "science" },
@@ -14,12 +23,20 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
{ id: "geography", name: "Geography", icon: "public" },
];
const chats = [
{ id: 1, preview: "Explain photosynthesis", time: "Today, 10:42 AM" },
{ id: 2, preview: "Help with integrals", time: "Yesterday, 3:15 PM" },
{ id: 3, preview: "What is cell respiration?", time: "Yesterday, 11:08 AM" },
{ id: 4, preview: "Newton's laws of motion", time: "May 18, 6:20 PM" },
];
const formatTime = (isoString) => {
const date = new Date(isoString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHrs = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHrs < 24) return `${diffHrs}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
};
return (
<aside className="sidebar">
@@ -32,8 +49,8 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
</div>
{/* New Chat Button */}
<button className="sidebar__new-chat">
<span className="material-symbols-outlined">add</span>
<button className="sidebar__new-chat" onClick={onNewChat} aria-label="Start new chat">
<span className="material-symbols-outlined sidebar__new-chat-icon">add</span>
<span>New Chat</span>
</button>
@@ -47,6 +64,7 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
className={`sidebar__link ${activeSubject === subject.id ? "sidebar__link--active" : ""}`}
onClick={() => onSubjectChange(subject.id)}
>
<span className={`sidebar__link-indicator`} />
<span className={`material-symbols-outlined sidebar__icon`}>{subject.icon}</span>
<span className="sidebar__label">{subject.name}</span>
</button>
@@ -57,20 +75,48 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
{/* Chat History Section */}
<div className="sidebar__section sidebar__section--history">
<h2 className="sidebar__section-title">Chat History</h2>
<ul className="sidebar__chat-list">
{chats.map((chat) => (
<li key={chat.id}>
<button
className={`sidebar__chat-link ${activeChat === chat.id ? "sidebar__chat-link--active" : ""}`}
onClick={() => onChatSelect(chat.id)}
>
<span className="sidebar__chat-preview">{chat.preview}</span>
<span className="sidebar__chat-time">{chat.time}</span>
</button>
</li>
))}
</ul>
<div className="sidebar__section-header">
<h2 className="sidebar__section-title">Chat History</h2>
<span className="sidebar__chat-count">{sessions.length}</span>
</div>
{isLoadingSessions ? (
<div className="sidebar__loading">
<span className="sidebar__loading-spinner" />
<span>Loading...</span>
</div>
) : sessions.length === 0 ? (
<p className="sidebar__empty">No chats yet</p>
) : (
<ul className="sidebar__chat-list">
{sessions.map((session) => {
const preview = session.preview || session.subject || "New Chat";
const isActive = activeChat === session.id;
return (
<li key={session.id} className="sidebar__chat-item">
<button
className={`sidebar__chat-link ${isActive ? "sidebar__chat-link--active" : ""}`}
onClick={() => onChatSelect(session.id)}
>
<span className="sidebar__chat-preview">{preview}</span>
<span className="sidebar__chat-time">{formatTime(session.updatedAt)}</span>
</button>
<button
className={`sidebar__chat-delete ${isActive ? "sidebar__chat-delete--visible" : ""}`}
onClick={(e) => {
e.stopPropagation();
onDeleteChat(session.id);
}}
aria-label="Delete chat"
title="Delete chat"
>
<span className="material-symbols-outlined">delete</span>
</button>
</li>
);
})}
</ul>
)}
</div>
{/* Footer */}