added: grade, subject and chapter in new chat, fixed that shitty card bug
This commit is contained in:
+281
-24
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user