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>