fix: QA report BUG-01 to BUG-12 full resolution - demo switcher, AI tutor stream, timeline alignment, cbt filtering, danger styling, streak celebration
This commit is contained in:
@@ -10,6 +10,8 @@ import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import 'katex/dist/katex.min.css' // Import Katex CSS for math rendering
|
||||
|
||||
import { useActiveUser, DEFAULT_USER_ID } from "@/hooks/useActiveUser"
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'ai';
|
||||
@@ -35,11 +37,13 @@ const INITIAL_MESSAGES: Message[] = [
|
||||
];
|
||||
|
||||
export function AITutorChat() {
|
||||
const { user } = useActiveUser();
|
||||
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
|
||||
const [input, setInput] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// BUG-03 fix: ref to anchor bottom of chat for auto-scroll
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
// Ref to anchor bottom of chat for auto-scroll
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to latest message whenever messages change
|
||||
@@ -47,22 +51,60 @@ export function AITutorChat() {
|
||||
if (bottomRef.current) {
|
||||
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [messages]);
|
||||
}, [messages, isStreaming]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
|
||||
setMessages([...messages, newMessage]);
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isStreaming) return;
|
||||
const userText = input.trim();
|
||||
const userMsgId = Date.now().toString();
|
||||
const aiMsgId = (Date.now() + 1).toString();
|
||||
|
||||
setMessages(prev => [...prev, { id: userMsgId, role: 'user', content: userText }]);
|
||||
setInput('');
|
||||
|
||||
// Mock AI response
|
||||
setTimeout(() => {
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
const activeId = user?.id || (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||
|
||||
const res = await fetch('/api/ai-coach/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: activeId, message: userText })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
if (res.body) {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let accumulated = "";
|
||||
|
||||
// Add empty AI message container for live streaming
|
||||
setMessages(prev => [...prev, { id: aiMsgId, role: 'ai', content: "" }]);
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
accumulated += chunk;
|
||||
setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, content: accumulated } : m));
|
||||
}
|
||||
} else {
|
||||
const text = await res.text();
|
||||
setMessages(prev => [...prev, { id: aiMsgId, role: 'ai', content: text }]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("AI Tutor streaming fallback:", err);
|
||||
setMessages(prev => [...prev, {
|
||||
id: (Date.now() + 1).toString(),
|
||||
id: aiMsgId,
|
||||
role: 'ai',
|
||||
content: "That's an interesting point! Let's explore the math behind it: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
|
||||
content: `Great question regarding **${userText}**! In JEE & NEET problem-solving, start by listing the knowns and unknowns, identify the key governing formula, and verify units. Practice similar questions in PrepPilot AI to build speed!`
|
||||
}]);
|
||||
}, 1000);
|
||||
} finally {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
|
||||
@@ -70,23 +70,24 @@ export function AdaptiveStudyPlan() {
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-6">
|
||||
<div className="relative space-y-6 before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-border before:to-transparent">
|
||||
{items.map((item, index) => {
|
||||
{/* BUG-12 fix: Clean left-aligned continuous timeline that never detaches on desktop */}
|
||||
<div className="relative space-y-6 before:absolute before:top-3 before:bottom-3 before:left-5 before:-translate-x-1/2 before:w-0.5 before:bg-gradient-to-b before:from-primary/30 before:via-border before:to-transparent">
|
||||
{items.map((item) => {
|
||||
const isDone = item.status === 'DONE';
|
||||
const isSkipped = item.status === 'SKIPPED';
|
||||
|
||||
return (
|
||||
<div key={item.id} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group is-active">
|
||||
<div key={item.id} className="relative flex items-start gap-4 group">
|
||||
{/* Timeline Icon */}
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-4 border-background shrink-0 md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 shadow-sm z-10 transition-colors
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-4 border-background shrink-0 shadow-sm z-10 transition-all duration-200 mt-1
|
||||
${isDone ? 'bg-green-500 text-white' : isSkipped ? 'bg-muted border-muted text-muted-foreground' : 'bg-primary/20 text-primary border-primary/30'}
|
||||
`}>
|
||||
{isDone ? <CheckCircle2 size={18} /> : isSkipped ? <SkipForward size={18} /> : <div className="size-2.5 rounded-full bg-primary animate-pulse" />}
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] p-4 rounded-xl border border-border bg-background shadow-sm transition-all hover:shadow-md">
|
||||
<div className={`flex items-center justify-between mb-1 ${isSkipped ? 'opacity-50' : ''}`}>
|
||||
<div className="flex-1 p-4 rounded-xl border border-border bg-background shadow-sm transition-all hover:shadow-md hover:border-[#00F0FF]/30">
|
||||
<div className={`flex items-center justify-between mb-1.5 ${isSkipped ? 'opacity-50' : ''}`}>
|
||||
<span className="text-[10px] uppercase font-bold tracking-wider text-muted-foreground bg-secondary/50 px-2 py-0.5 rounded-md">
|
||||
{item.type}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user