Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- web/src/App.tsx +212 -123
web/src/App.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { LoginScreen } from './components/LoginScreen';
|
|
| 6 |
import { ProfileEditor } from './components/ProfileEditor';
|
| 7 |
import { ReviewBanner } from './components/ReviewBanner';
|
| 8 |
import { Onboarding } from './components/Onboarding';
|
| 9 |
-
import {
|
| 10 |
import { Button } from './components/ui/button';
|
| 11 |
import { Toaster } from './components/ui/sonner';
|
| 12 |
import { toast } from 'sonner';
|
|
@@ -99,12 +99,25 @@ export interface SavedChat {
|
|
| 99 |
timestamp: Date;
|
| 100 |
}
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
function App() {
|
| 103 |
const [isDarkMode, setIsDarkMode] = useState(() => {
|
| 104 |
const saved = localStorage.getItem('theme');
|
| 105 |
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
| 106 |
});
|
| 107 |
const [user, setUser] = useState<User | null>(null);
|
|
|
|
| 108 |
// Global current course selection
|
| 109 |
const [currentCourseId, setCurrentCourseId] = useState<string>(() => localStorage.getItem('myspace_selected_course') || 'course1');
|
| 110 |
|
|
@@ -135,7 +148,7 @@ function App() {
|
|
| 135 |
teachingAssistant: { name: 'James Brown', email: 'james.brown@university.edu' }
|
| 136 |
},
|
| 137 |
];
|
| 138 |
-
|
| 139 |
// Separate messages for each chat mode
|
| 140 |
const [askMessages, setAskMessages] = useState<Message[]>([
|
| 141 |
{
|
|
@@ -161,23 +174,23 @@ function App() {
|
|
| 161 |
timestamp: new Date(),
|
| 162 |
}
|
| 163 |
]);
|
| 164 |
-
|
| 165 |
const [learningMode, setLearningMode] = useState<LearningMode>('concept');
|
| 166 |
const [language, setLanguage] = useState<Language>('auto');
|
| 167 |
const [chatMode, setChatMode] = useState<ChatMode>('ask');
|
| 168 |
-
|
| 169 |
// Get current messages based on chat mode
|
| 170 |
const messages = chatMode === 'ask' ? askMessages : chatMode === 'review' ? reviewMessages : quizMessages;
|
| 171 |
-
|
| 172 |
// Track previous chat mode to detect mode changes
|
| 173 |
const prevChatModeRef = useRef<ChatMode>(chatMode);
|
| 174 |
-
|
| 175 |
// Ensure welcome message exists when switching modes or when messages are empty
|
| 176 |
useEffect(() => {
|
| 177 |
// Check the actual state arrays, not the computed messages
|
| 178 |
let currentMessages: Message[];
|
| 179 |
let setCurrentMessages: (messages: Message[]) => void;
|
| 180 |
-
|
| 181 |
if (chatMode === 'ask') {
|
| 182 |
currentMessages = askMessages;
|
| 183 |
setCurrentMessages = setAskMessages;
|
|
@@ -188,12 +201,12 @@ function App() {
|
|
| 188 |
currentMessages = quizMessages;
|
| 189 |
setCurrentMessages = setQuizMessages;
|
| 190 |
}
|
| 191 |
-
|
| 192 |
const hasUserMessages = currentMessages.some(msg => msg.role === 'user');
|
| 193 |
const expectedWelcomeId = chatMode === 'ask' ? '1' : chatMode === 'review' ? 'review-1' : 'quiz-1';
|
| 194 |
const hasWelcomeMessage = currentMessages.some(msg => msg.id === expectedWelcomeId && msg.role === 'assistant');
|
| 195 |
const modeChanged = prevChatModeRef.current !== chatMode;
|
| 196 |
-
|
| 197 |
// If mode changed or messages are empty or missing welcome message, restore welcome message
|
| 198 |
if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
|
| 199 |
const initialMessages: Record<ChatMode, Message[]> = {
|
|
@@ -216,12 +229,13 @@ function App() {
|
|
| 216 |
timestamp: new Date(),
|
| 217 |
}],
|
| 218 |
};
|
| 219 |
-
|
| 220 |
setCurrentMessages(initialMessages[chatMode]);
|
| 221 |
}
|
| 222 |
-
|
| 223 |
prevChatModeRef.current = chatMode;
|
| 224 |
}, [chatMode, askMessages.length, reviewMessages.length, quizMessages.length]); // Only depend on lengths to avoid infinite loops
|
|
|
|
| 225 |
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
| 226 |
const [memoryProgress, setMemoryProgress] = useState(36);
|
| 227 |
const [quizState, setQuizState] = useState<{
|
|
@@ -239,6 +253,7 @@ function App() {
|
|
| 239 |
const [showProfileEditor, setShowProfileEditor] = useState(false);
|
| 240 |
const [showOnboarding, setShowOnboarding] = useState(false);
|
| 241 |
const [exportResult, setExportResult] = useState('');
|
|
|
|
| 242 |
// Review banner state
|
| 243 |
const [showReviewBanner, setShowReviewBanner] = useState(() => {
|
| 244 |
// Temporarily force show for testing - remove this after confirming it works
|
|
@@ -248,14 +263,14 @@ function App() {
|
|
| 248 |
});
|
| 249 |
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 250 |
const [showClearDialog, setShowClearDialog] = useState(false);
|
| 251 |
-
|
| 252 |
// Saved conversations/summaries
|
| 253 |
const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
|
| 254 |
const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
|
| 255 |
-
|
| 256 |
// Saved chats
|
| 257 |
const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
|
| 258 |
-
|
| 259 |
// Mock group members
|
| 260 |
const [groupMembers] = useState<GroupMember[]>([
|
| 261 |
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
|
@@ -268,13 +283,90 @@ function App() {
|
|
| 268 |
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
| 269 |
const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>('individual');
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
// Initialize workspaces when user logs in
|
| 272 |
useEffect(() => {
|
| 273 |
if (user) {
|
| 274 |
const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
|
| 275 |
const course1Info = availableCourses.find(c => c.id === 'course1');
|
| 276 |
const course2Info = availableCourses.find(c => c.name === 'AI Ethics');
|
| 277 |
-
|
| 278 |
setWorkspaces([
|
| 279 |
{
|
| 280 |
id: 'individual',
|
|
@@ -363,12 +455,12 @@ function App() {
|
|
| 363 |
explanation: "Transparency is crucial because it helps users understand AI decision-making processes, enables debugging, and ensures accountability."
|
| 364 |
}
|
| 365 |
];
|
| 366 |
-
|
| 367 |
const randomIndex = Math.floor(Math.random() * questions.length);
|
| 368 |
return questions[randomIndex];
|
| 369 |
};
|
| 370 |
|
| 371 |
-
const handleSendMessage = (content: string) => {
|
| 372 |
if (!content.trim() || !user) return;
|
| 373 |
|
| 374 |
// Attach sender info for all user messages
|
|
@@ -396,20 +488,22 @@ function App() {
|
|
| 396 |
setQuizMessages(prev => [...prev, userMessage]);
|
| 397 |
}
|
| 398 |
|
| 399 |
-
//
|
|
|
|
|
|
|
| 400 |
if (chatMode === 'quiz') {
|
| 401 |
if (quizState.waitingForAnswer) {
|
| 402 |
// User is answering a question
|
| 403 |
const isCorrect = Math.random() > 0.3; // Simulate answer checking
|
| 404 |
-
|
| 405 |
setIsTyping(true);
|
| 406 |
setTimeout(() => {
|
| 407 |
-
const feedback = isCorrect
|
| 408 |
? "✅ Correct! Great job!"
|
| 409 |
: "❌ Not quite right, but good effort!";
|
| 410 |
-
|
| 411 |
const explanation = "Here's the explanation: The correct answer demonstrates understanding of the key concepts. Let me break it down for you...";
|
| 412 |
-
|
| 413 |
const assistantMessage: Message = {
|
| 414 |
id: (Date.now() + 1).toString(),
|
| 415 |
role: 'assistant',
|
|
@@ -421,7 +515,7 @@ function App() {
|
|
| 421 |
|
| 422 |
// Close typing indicator first
|
| 423 |
setIsTyping(false);
|
| 424 |
-
|
| 425 |
// Wait a bit to ensure typing indicator disappears before adding message
|
| 426 |
setTimeout(() => {
|
| 427 |
setQuizMessages(prev => [...prev, assistantMessage]);
|
|
@@ -432,70 +526,65 @@ function App() {
|
|
| 432 |
return;
|
| 433 |
}
|
| 434 |
|
| 435 |
-
//
|
| 436 |
-
//
|
|
|
|
| 437 |
const shouldAIRespond = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
setTimeout(() => {
|
| 442 |
-
let response = '';
|
| 443 |
-
|
| 444 |
if (chatMode === 'ask') {
|
| 445 |
-
|
| 446 |
-
general: "I'd be happy to help! To provide you with the most accurate and relevant answer, could you please provide some context about what you're asking? For example, what subject or topic is this related to?",
|
| 447 |
-
concept: "Great question! Let me break this concept down for you. In Responsible AI, this relates to ensuring our AI systems are fair, transparent, and accountable. Would you like me to explain any specific aspect in more detail?",
|
| 448 |
-
socratic: "That's an interesting point! Let me ask you this: What do you think are the key ethical considerations when deploying AI systems? Take a moment to think about it.",
|
| 449 |
-
exam: "Let me test your understanding with a quick question: Which of the following is NOT a principle of Responsible AI? A) Fairness B) Transparency C) Profit Maximization D) Accountability",
|
| 450 |
-
assignment: "I can help you with that assignment! Let's break it down into manageable steps. First, what specific aspect are you working on?",
|
| 451 |
-
summary: "Here's a quick summary: Responsible AI focuses on developing and deploying AI systems that are ethical, fair, transparent, and accountable to society.",
|
| 452 |
-
};
|
| 453 |
-
response = responses[learningMode];
|
| 454 |
} else if (chatMode === 'review') {
|
| 455 |
-
|
| 456 |
-
const reviewData = (window as any).__lastReviewData;
|
| 457 |
-
if (reviewData) {
|
| 458 |
-
if (reviewData.startsWith('REVIEW_TOPIC:')) {
|
| 459 |
-
// Parse review topic data
|
| 460 |
-
const data = reviewData.replace('REVIEW_TOPIC:', '').split('|');
|
| 461 |
-
const [title, previousQuestion, memoryRetention, schedule, status, weight, lastReviewed] = data;
|
| 462 |
-
|
| 463 |
-
response = `Let's review **${title}** together!\n\n**Your Previous Question:**\n"${previousQuestion}"\n\n**Review Details:**\n- **Memory Retention:** ${memoryRetention}%\n- **Schedule:** ${schedule}\n- **Status:** ${status.toUpperCase()}\n- **Weight:** ${weight}%\n- **Last Reviewed:** ${lastReviewed}\n\nLet's go through this topic step by step. What would you like to focus on first?`;
|
| 464 |
-
// Clear the stored data
|
| 465 |
-
delete (window as any).__lastReviewData;
|
| 466 |
-
} else if (reviewData === 'REVIEW_ALL') {
|
| 467 |
-
response = `I'll help you review all the topics that need your attention. Based on your learning history, here are the topics we should focus on:\n\n1. **Main Concept of Lab 3** (Urgent - Memory Retention: 25%)\n2. **Effective Prompt Engineering** (Review - Memory Retention: 60%)\n3. **Objective LLM Evaluation** (Stable - Memory Retention: 90%)\n\nLet's start with the most urgent ones first. Which topic would you like to begin with?`;
|
| 468 |
-
// Clear the stored data
|
| 469 |
-
delete (window as any).__lastReviewData;
|
| 470 |
-
} else {
|
| 471 |
-
response = "Let's review what you've learned! Based on your previous conversations, here are the key concepts we covered: [Review content would go here]";
|
| 472 |
-
}
|
| 473 |
-
} else {
|
| 474 |
-
response = "Let's review what you've learned! Based on your previous conversations, here are the key concepts we covered: [Review content would go here]";
|
| 475 |
-
}
|
| 476 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
setReviewMessages(prev => [...prev, assistantMessage]);
|
| 496 |
-
}
|
| 497 |
-
}, 50);
|
| 498 |
-
}, 2000);
|
| 499 |
}
|
| 500 |
};
|
| 501 |
|
|
@@ -503,11 +592,11 @@ function App() {
|
|
| 503 |
setIsTyping(true);
|
| 504 |
const question = generateQuizQuestion();
|
| 505 |
let questionText = question.question;
|
| 506 |
-
|
| 507 |
if (question.type === 'multiple-choice') {
|
| 508 |
-
questionText += '\n\n' + question.options
|
| 509 |
}
|
| 510 |
-
|
| 511 |
setTimeout(() => {
|
| 512 |
const assistantMessage: Message = {
|
| 513 |
id: Date.now().toString(),
|
|
@@ -520,15 +609,15 @@ function App() {
|
|
| 520 |
|
| 521 |
// Close typing indicator first
|
| 522 |
setIsTyping(false);
|
| 523 |
-
|
| 524 |
// Wait a bit to ensure typing indicator disappears before adding message
|
| 525 |
setTimeout(() => {
|
| 526 |
setQuizMessages(prev => [...prev, assistantMessage]);
|
| 527 |
-
setQuizState(prev => ({
|
| 528 |
-
...prev,
|
| 529 |
currentQuestion: prev.currentQuestion + 1,
|
| 530 |
waitingForAnswer: true,
|
| 531 |
-
showNextButton: false
|
| 532 |
}));
|
| 533 |
}, 50);
|
| 534 |
}, 2000);
|
|
@@ -551,7 +640,7 @@ function App() {
|
|
| 551 |
};
|
| 552 |
|
| 553 |
const handleFileTypeChange = (index: number, type: FileType) => {
|
| 554 |
-
setUploadedFiles(prev => prev.map((file, i) =>
|
| 555 |
i === index ? { ...file, type } : file
|
| 556 |
));
|
| 557 |
};
|
|
@@ -559,12 +648,12 @@ function App() {
|
|
| 559 |
// Helper function to check if current chat is already saved
|
| 560 |
const isCurrentChatSaved = (): SavedChat | null => {
|
| 561 |
if (messages.length <= 1) return null;
|
| 562 |
-
|
| 563 |
// Find a saved chat that matches the current messages and chatMode
|
| 564 |
return savedChats.find(chat => {
|
| 565 |
if (chat.chatMode !== chatMode) return false;
|
| 566 |
if (chat.messages.length !== messages.length) return false;
|
| 567 |
-
|
| 568 |
// Check if all messages match
|
| 569 |
return chat.messages.every((savedMsg, index) => {
|
| 570 |
const currentMsg = messages[index];
|
|
@@ -582,7 +671,7 @@ function App() {
|
|
| 582 |
toast.info('No conversation to save');
|
| 583 |
return;
|
| 584 |
}
|
| 585 |
-
|
| 586 |
// Check if already saved
|
| 587 |
const existingChat = isCurrentChatSaved();
|
| 588 |
if (existingChat) {
|
|
@@ -591,7 +680,7 @@ function App() {
|
|
| 591 |
toast.success('Chat unsaved');
|
| 592 |
return;
|
| 593 |
}
|
| 594 |
-
|
| 595 |
// Save: add new chat
|
| 596 |
const title = `Chat - ${chatMode === 'ask' ? 'Ask' : chatMode === 'review' ? 'Review' : 'Quiz'} - ${new Date().toLocaleDateString()}`;
|
| 597 |
const newChat: SavedChat = {
|
|
@@ -601,7 +690,7 @@ function App() {
|
|
| 601 |
chatMode,
|
| 602 |
timestamp: new Date(),
|
| 603 |
};
|
| 604 |
-
|
| 605 |
setSavedChats(prev => [newChat, ...prev]);
|
| 606 |
setLeftPanelVisible(true);
|
| 607 |
toast.success('Chat saved!');
|
|
@@ -610,7 +699,7 @@ function App() {
|
|
| 610 |
const handleLoadChat = (savedChat: SavedChat) => {
|
| 611 |
// Set the chat mode first
|
| 612 |
setChatMode(savedChat.chatMode);
|
| 613 |
-
|
| 614 |
// Then set the messages for that mode
|
| 615 |
if (savedChat.chatMode === 'ask') {
|
| 616 |
setAskMessages(savedChat.messages);
|
|
@@ -625,7 +714,7 @@ function App() {
|
|
| 625 |
showNextButton: false,
|
| 626 |
});
|
| 627 |
}
|
| 628 |
-
|
| 629 |
toast.success('Chat loaded!');
|
| 630 |
};
|
| 631 |
|
|
@@ -635,7 +724,7 @@ function App() {
|
|
| 635 |
};
|
| 636 |
|
| 637 |
const handleRenameSavedChat = (id: string, newTitle: string) => {
|
| 638 |
-
setSavedChats(prev => prev.map(chat =>
|
| 639 |
chat.id === id ? { ...chat, title: newTitle } : chat
|
| 640 |
));
|
| 641 |
toast.success('Chat renamed');
|
|
@@ -645,7 +734,7 @@ function App() {
|
|
| 645 |
if (shouldSave) {
|
| 646 |
handleSaveChat();
|
| 647 |
}
|
| 648 |
-
|
| 649 |
const initialMessages: Record<ChatMode, Message[]> = {
|
| 650 |
ask: [{
|
| 651 |
id: '1',
|
|
@@ -666,7 +755,7 @@ function App() {
|
|
| 666 |
timestamp: new Date(),
|
| 667 |
}],
|
| 668 |
};
|
| 669 |
-
|
| 670 |
// Clear only the current mode's conversation
|
| 671 |
if (chatMode === 'ask') {
|
| 672 |
setAskMessages(initialMessages.ask);
|
|
@@ -696,7 +785,7 @@ This conversation covered key concepts in Module 10 – Responsible AI, includin
|
|
| 696 |
3. Best practices for ethical AI development
|
| 697 |
|
| 698 |
Exported successfully! ✓`;
|
| 699 |
-
|
| 700 |
setExportResult(result);
|
| 701 |
setResultType('export');
|
| 702 |
toast.success('Conversation exported!');
|
|
@@ -717,7 +806,7 @@ d) Cost reduction
|
|
| 717 |
**Question 3:** True or False: AI systems should always prioritize accuracy over fairness.
|
| 718 |
|
| 719 |
Generate quiz based on your conversation!`;
|
| 720 |
-
|
| 721 |
setExportResult(quiz);
|
| 722 |
setResultType('quiz');
|
| 723 |
toast.success('Quiz generated!');
|
|
@@ -743,7 +832,7 @@ Generate quiz based on your conversation!`;
|
|
| 743 |
|
| 744 |
## Progress Update
|
| 745 |
You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
| 746 |
-
|
| 747 |
setExportResult(summary);
|
| 748 |
setResultType('summary');
|
| 749 |
toast.success('Summary generated!');
|
|
@@ -757,7 +846,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 757 |
workspaceId?: string
|
| 758 |
) => {
|
| 759 |
if (!content.trim()) return;
|
| 760 |
-
|
| 761 |
// Summary should always be saved as file, not chat
|
| 762 |
// If saving as chat (from RightPanel export/quiz only, not summary)
|
| 763 |
if (saveAsChat && type !== 'summary') {
|
|
@@ -777,7 +866,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 777 |
timestamp: new Date(),
|
| 778 |
}
|
| 779 |
];
|
| 780 |
-
|
| 781 |
const title = type === 'export' ? 'Exported Conversation' : 'Micro-Quiz';
|
| 782 |
const newChat: SavedChat = {
|
| 783 |
id: Date.now().toString(),
|
|
@@ -786,13 +875,13 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 786 |
chatMode: 'ask',
|
| 787 |
timestamp: new Date(),
|
| 788 |
};
|
| 789 |
-
|
| 790 |
setSavedChats(prev => [newChat, ...prev]);
|
| 791 |
setLeftPanelVisible(true);
|
| 792 |
toast.success('Chat saved!');
|
| 793 |
return;
|
| 794 |
}
|
| 795 |
-
|
| 796 |
// Otherwise, save as file (existing behavior)
|
| 797 |
// Check if already saved
|
| 798 |
const existingItem = savedItems.find(item => item.content === content && item.type === type);
|
|
@@ -801,7 +890,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 801 |
handleUnsave(existingItem.id);
|
| 802 |
return;
|
| 803 |
}
|
| 804 |
-
|
| 805 |
// Save: add new item
|
| 806 |
const title = type === 'export' ? 'Exported Conversation' : type === 'quiz' ? 'Micro-Quiz' : 'Summarization';
|
| 807 |
const newItem: SavedItem = {
|
|
@@ -814,16 +903,16 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 814 |
format,
|
| 815 |
workspaceId: workspaceId || currentWorkspaceId,
|
| 816 |
};
|
| 817 |
-
|
| 818 |
setSavedItems(prev => [newItem, ...prev]);
|
| 819 |
setRecentlySavedId(newItem.id);
|
| 820 |
setLeftPanelVisible(true); // Open left panel
|
| 821 |
-
|
| 822 |
// Clear the highlight after animation
|
| 823 |
setTimeout(() => {
|
| 824 |
setRecentlySavedId(null);
|
| 825 |
}, 2000);
|
| 826 |
-
|
| 827 |
toast.success('Saved for later!');
|
| 828 |
};
|
| 829 |
|
|
@@ -841,7 +930,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 841 |
}) => {
|
| 842 |
const id = `group-${Date.now()}`;
|
| 843 |
const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
|
| 844 |
-
|
| 845 |
// Add creator as first member
|
| 846 |
const creatorMember: GroupMember = user ? {
|
| 847 |
id: user.email,
|
|
@@ -849,7 +938,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 849 |
email: user.email,
|
| 850 |
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
|
| 851 |
} : { id: 'unknown', name: 'Unknown', email: 'unknown@email.com' };
|
| 852 |
-
|
| 853 |
const members: GroupMember[] = [
|
| 854 |
creatorMember,
|
| 855 |
...payload.invites.map(email => ({
|
|
@@ -860,7 +949,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 860 |
];
|
| 861 |
|
| 862 |
let newWorkspace: Workspace;
|
| 863 |
-
|
| 864 |
if (payload.category === 'course') {
|
| 865 |
const courseInfo = availableCourses.find(c => c.id === payload.courseId);
|
| 866 |
newWorkspace = {
|
|
@@ -888,12 +977,12 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 888 |
|
| 889 |
setWorkspaces(prev => [...prev, newWorkspace]);
|
| 890 |
setCurrentWorkspaceId(id);
|
| 891 |
-
|
| 892 |
// Set current course if it's a course workspace
|
| 893 |
if (payload.category === 'course' && payload.courseId) {
|
| 894 |
setCurrentCourseId(payload.courseId);
|
| 895 |
}
|
| 896 |
-
|
| 897 |
toast.success('New group workspace created');
|
| 898 |
};
|
| 899 |
|
|
@@ -916,7 +1005,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 916 |
// For testing: always show onboarding (comment out localStorage check)
|
| 917 |
// const onboardingCompleted = localStorage.getItem(`onboarding_completed_${newUser.email}`);
|
| 918 |
// if (!onboardingCompleted) {
|
| 919 |
-
|
| 920 |
// }
|
| 921 |
};
|
| 922 |
|
|
@@ -946,8 +1035,8 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 946 |
if (showOnboarding && user) {
|
| 947 |
return (
|
| 948 |
<>
|
| 949 |
-
<Onboarding
|
| 950 |
-
user={user}
|
| 951 |
onComplete={handleOnboardingComplete}
|
| 952 |
onSkip={handleOnboardingSkip}
|
| 953 |
/>
|
|
@@ -958,7 +1047,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 958 |
return (
|
| 959 |
<div className="min-h-screen bg-background flex flex-col">
|
| 960 |
<Toaster />
|
| 961 |
-
<Header
|
| 962 |
user={user}
|
| 963 |
onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
|
| 964 |
onUserClick={() => {}}
|
|
@@ -992,7 +1081,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 992 |
</div>
|
| 993 |
)}
|
| 994 |
|
| 995 |
-
<div
|
| 996 |
className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative"
|
| 997 |
style={{ overscrollBehavior: 'none' }}
|
| 998 |
>
|
|
@@ -1011,7 +1100,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 1011 |
)}
|
| 1012 |
{/* Mobile Sidebar Toggle - Left */}
|
| 1013 |
{leftSidebarOpen && (
|
| 1014 |
-
<div
|
| 1015 |
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 1016 |
onClick={() => setLeftSidebarOpen(false)}
|
| 1017 |
/>
|
|
@@ -1019,7 +1108,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 1019 |
|
| 1020 |
{/* Left Sidebar */}
|
| 1021 |
{leftPanelVisible ? (
|
| 1022 |
-
<aside
|
| 1023 |
className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative"
|
| 1024 |
style={{ borderRight: '1px solid var(--border)', height: 'calc(100vh - 4rem)' }}
|
| 1025 |
>
|
|
@@ -1063,7 +1152,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 1063 |
) : null}
|
| 1064 |
|
| 1065 |
{/* Left Sidebar - Mobile */}
|
| 1066 |
-
<aside
|
| 1067 |
className={`
|
| 1068 |
fixed lg:hidden inset-y-0 left-0 z-50
|
| 1069 |
w-80 bg-card border-r border-border
|
|
@@ -1156,4 +1245,4 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
|
| 1156 |
);
|
| 1157 |
}
|
| 1158 |
|
| 1159 |
-
export default App;
|
|
|
|
| 6 |
import { ProfileEditor } from './components/ProfileEditor';
|
| 7 |
import { ReviewBanner } from './components/ReviewBanner';
|
| 8 |
import { Onboarding } from './components/Onboarding';
|
| 9 |
+
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
| 10 |
import { Button } from './components/ui/button';
|
| 11 |
import { Toaster } from './components/ui/sonner';
|
| 12 |
import { toast } from 'sonner';
|
|
|
|
| 99 |
timestamp: Date;
|
| 100 |
}
|
| 101 |
|
| 102 |
+
type BackendRef = {
|
| 103 |
+
source_file?: string;
|
| 104 |
+
section?: string;
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
type ChatApiResponse = {
|
| 108 |
+
reply: string;
|
| 109 |
+
session_status_md?: string;
|
| 110 |
+
refs?: BackendRef[];
|
| 111 |
+
latency_ms?: number;
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
function App() {
|
| 115 |
const [isDarkMode, setIsDarkMode] = useState(() => {
|
| 116 |
const saved = localStorage.getItem('theme');
|
| 117 |
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
| 118 |
});
|
| 119 |
const [user, setUser] = useState<User | null>(null);
|
| 120 |
+
|
| 121 |
// Global current course selection
|
| 122 |
const [currentCourseId, setCurrentCourseId] = useState<string>(() => localStorage.getItem('myspace_selected_course') || 'course1');
|
| 123 |
|
|
|
|
| 148 |
teachingAssistant: { name: 'James Brown', email: 'james.brown@university.edu' }
|
| 149 |
},
|
| 150 |
];
|
| 151 |
+
|
| 152 |
// Separate messages for each chat mode
|
| 153 |
const [askMessages, setAskMessages] = useState<Message[]>([
|
| 154 |
{
|
|
|
|
| 174 |
timestamp: new Date(),
|
| 175 |
}
|
| 176 |
]);
|
| 177 |
+
|
| 178 |
const [learningMode, setLearningMode] = useState<LearningMode>('concept');
|
| 179 |
const [language, setLanguage] = useState<Language>('auto');
|
| 180 |
const [chatMode, setChatMode] = useState<ChatMode>('ask');
|
| 181 |
+
|
| 182 |
// Get current messages based on chat mode
|
| 183 |
const messages = chatMode === 'ask' ? askMessages : chatMode === 'review' ? reviewMessages : quizMessages;
|
| 184 |
+
|
| 185 |
// Track previous chat mode to detect mode changes
|
| 186 |
const prevChatModeRef = useRef<ChatMode>(chatMode);
|
| 187 |
+
|
| 188 |
// Ensure welcome message exists when switching modes or when messages are empty
|
| 189 |
useEffect(() => {
|
| 190 |
// Check the actual state arrays, not the computed messages
|
| 191 |
let currentMessages: Message[];
|
| 192 |
let setCurrentMessages: (messages: Message[]) => void;
|
| 193 |
+
|
| 194 |
if (chatMode === 'ask') {
|
| 195 |
currentMessages = askMessages;
|
| 196 |
setCurrentMessages = setAskMessages;
|
|
|
|
| 201 |
currentMessages = quizMessages;
|
| 202 |
setCurrentMessages = setQuizMessages;
|
| 203 |
}
|
| 204 |
+
|
| 205 |
const hasUserMessages = currentMessages.some(msg => msg.role === 'user');
|
| 206 |
const expectedWelcomeId = chatMode === 'ask' ? '1' : chatMode === 'review' ? 'review-1' : 'quiz-1';
|
| 207 |
const hasWelcomeMessage = currentMessages.some(msg => msg.id === expectedWelcomeId && msg.role === 'assistant');
|
| 208 |
const modeChanged = prevChatModeRef.current !== chatMode;
|
| 209 |
+
|
| 210 |
// If mode changed or messages are empty or missing welcome message, restore welcome message
|
| 211 |
if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
|
| 212 |
const initialMessages: Record<ChatMode, Message[]> = {
|
|
|
|
| 229 |
timestamp: new Date(),
|
| 230 |
}],
|
| 231 |
};
|
| 232 |
+
|
| 233 |
setCurrentMessages(initialMessages[chatMode]);
|
| 234 |
}
|
| 235 |
+
|
| 236 |
prevChatModeRef.current = chatMode;
|
| 237 |
}, [chatMode, askMessages.length, reviewMessages.length, quizMessages.length]); // Only depend on lengths to avoid infinite loops
|
| 238 |
+
|
| 239 |
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
| 240 |
const [memoryProgress, setMemoryProgress] = useState(36);
|
| 241 |
const [quizState, setQuizState] = useState<{
|
|
|
|
| 253 |
const [showProfileEditor, setShowProfileEditor] = useState(false);
|
| 254 |
const [showOnboarding, setShowOnboarding] = useState(false);
|
| 255 |
const [exportResult, setExportResult] = useState('');
|
| 256 |
+
|
| 257 |
// Review banner state
|
| 258 |
const [showReviewBanner, setShowReviewBanner] = useState(() => {
|
| 259 |
// Temporarily force show for testing - remove this after confirming it works
|
|
|
|
| 263 |
});
|
| 264 |
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 265 |
const [showClearDialog, setShowClearDialog] = useState(false);
|
| 266 |
+
|
| 267 |
// Saved conversations/summaries
|
| 268 |
const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
|
| 269 |
const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
|
| 270 |
+
|
| 271 |
// Saved chats
|
| 272 |
const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
|
| 273 |
+
|
| 274 |
// Mock group members
|
| 275 |
const [groupMembers] = useState<GroupMember[]>([
|
| 276 |
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
|
|
|
| 283 |
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
| 284 |
const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>('individual');
|
| 285 |
|
| 286 |
+
// -----------------------------
|
| 287 |
+
// Backend mapping helpers
|
| 288 |
+
// -----------------------------
|
| 289 |
+
function toBackendLearningMode(mode: LearningMode): string {
|
| 290 |
+
switch (mode) {
|
| 291 |
+
case 'concept': return 'Concept Explainer';
|
| 292 |
+
case 'socratic': return 'Socratic Tutor';
|
| 293 |
+
case 'exam': return 'Exam Prep';
|
| 294 |
+
case 'assignment': return 'Assignment Helper';
|
| 295 |
+
case 'summary': return 'Quick Summary';
|
| 296 |
+
case 'general':
|
| 297 |
+
default: return 'General';
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
function toBackendLanguagePreference(lang: Language): string {
|
| 302 |
+
switch (lang) {
|
| 303 |
+
case 'en': return 'English';
|
| 304 |
+
case 'zh': return '中文';
|
| 305 |
+
case 'auto':
|
| 306 |
+
default: return 'Auto';
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
function toBackendDocTypeFromUploaded(files: UploadedFile[]): string {
|
| 311 |
+
// Prefer non-syllabus doc type if present; else Syllabus if exists; else Other
|
| 312 |
+
const hasSyllabus = files.some(f => f.type === 'syllabus');
|
| 313 |
+
const firstNonSyllabus = files.find(f => f.type !== 'syllabus');
|
| 314 |
+
|
| 315 |
+
const map: Record<FileType, string> = {
|
| 316 |
+
'syllabus': 'Syllabus',
|
| 317 |
+
'lecture-slides': 'Lecture Slides / PPT',
|
| 318 |
+
'literature-review': 'Literature Review / Paper',
|
| 319 |
+
'other': 'Other Course Document',
|
| 320 |
+
};
|
| 321 |
+
|
| 322 |
+
if (firstNonSyllabus) return map[firstNonSyllabus.type];
|
| 323 |
+
if (hasSyllabus) return map['syllabus'];
|
| 324 |
+
return map['other'];
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
function formatRefs(refs?: BackendRef[]): string[] | undefined {
|
| 328 |
+
if (!refs || refs.length === 0) return undefined;
|
| 329 |
+
const lines = refs
|
| 330 |
+
.map(r => {
|
| 331 |
+
const a = (r.source_file || '').trim();
|
| 332 |
+
const b = (r.section || '').trim();
|
| 333 |
+
if (a && b) return `${a} — ${b}`;
|
| 334 |
+
if (a) return a;
|
| 335 |
+
if (b) return b;
|
| 336 |
+
return '';
|
| 337 |
+
})
|
| 338 |
+
.filter(Boolean);
|
| 339 |
+
return lines.length ? lines : undefined;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
async function callChatApi(payload: {
|
| 343 |
+
user_id: string;
|
| 344 |
+
message: string;
|
| 345 |
+
learning_mode: string;
|
| 346 |
+
language_preference: string;
|
| 347 |
+
doc_type: string;
|
| 348 |
+
}): Promise<ChatApiResponse> {
|
| 349 |
+
const res = await fetch('/api/chat', {
|
| 350 |
+
method: 'POST',
|
| 351 |
+
headers: { 'Content-Type': 'application/json' },
|
| 352 |
+
body: JSON.stringify(payload),
|
| 353 |
+
});
|
| 354 |
+
|
| 355 |
+
if (!res.ok) {
|
| 356 |
+
const text = await res.text().catch(() => '');
|
| 357 |
+
throw new Error(`Chat API failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
return res.json();
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
// Initialize workspaces when user logs in
|
| 364 |
useEffect(() => {
|
| 365 |
if (user) {
|
| 366 |
const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
|
| 367 |
const course1Info = availableCourses.find(c => c.id === 'course1');
|
| 368 |
const course2Info = availableCourses.find(c => c.name === 'AI Ethics');
|
| 369 |
+
|
| 370 |
setWorkspaces([
|
| 371 |
{
|
| 372 |
id: 'individual',
|
|
|
|
| 455 |
explanation: "Transparency is crucial because it helps users understand AI decision-making processes, enables debugging, and ensures accountability."
|
| 456 |
}
|
| 457 |
];
|
| 458 |
+
|
| 459 |
const randomIndex = Math.floor(Math.random() * questions.length);
|
| 460 |
return questions[randomIndex];
|
| 461 |
};
|
| 462 |
|
| 463 |
+
const handleSendMessage = async (content: string) => {
|
| 464 |
if (!content.trim() || !user) return;
|
| 465 |
|
| 466 |
// Attach sender info for all user messages
|
|
|
|
| 488 |
setQuizMessages(prev => [...prev, userMessage]);
|
| 489 |
}
|
| 490 |
|
| 491 |
+
// -----------------------------
|
| 492 |
+
// Quiz mode: keep existing local simulation
|
| 493 |
+
// -----------------------------
|
| 494 |
if (chatMode === 'quiz') {
|
| 495 |
if (quizState.waitingForAnswer) {
|
| 496 |
// User is answering a question
|
| 497 |
const isCorrect = Math.random() > 0.3; // Simulate answer checking
|
| 498 |
+
|
| 499 |
setIsTyping(true);
|
| 500 |
setTimeout(() => {
|
| 501 |
+
const feedback = isCorrect
|
| 502 |
? "✅ Correct! Great job!"
|
| 503 |
: "❌ Not quite right, but good effort!";
|
| 504 |
+
|
| 505 |
const explanation = "Here's the explanation: The correct answer demonstrates understanding of the key concepts. Let me break it down for you...";
|
| 506 |
+
|
| 507 |
const assistantMessage: Message = {
|
| 508 |
id: (Date.now() + 1).toString(),
|
| 509 |
role: 'assistant',
|
|
|
|
| 515 |
|
| 516 |
// Close typing indicator first
|
| 517 |
setIsTyping(false);
|
| 518 |
+
|
| 519 |
// Wait a bit to ensure typing indicator disappears before adding message
|
| 520 |
setTimeout(() => {
|
| 521 |
setQuizMessages(prev => [...prev, assistantMessage]);
|
|
|
|
| 526 |
return;
|
| 527 |
}
|
| 528 |
|
| 529 |
+
// -----------------------------
|
| 530 |
+
// Ask + Review: call backend /api/chat
|
| 531 |
+
// -----------------------------
|
| 532 |
const shouldAIRespond = true;
|
| 533 |
+
if (!shouldAIRespond) return;
|
| 534 |
+
|
| 535 |
+
setIsTyping(true);
|
| 536 |
+
|
| 537 |
+
try {
|
| 538 |
+
const docType = toBackendDocTypeFromUploaded(uploadedFiles);
|
| 539 |
+
|
| 540 |
+
const resp = await callChatApi({
|
| 541 |
+
user_id: user.email,
|
| 542 |
+
message: content,
|
| 543 |
+
learning_mode: toBackendLearningMode(learningMode),
|
| 544 |
+
language_preference: toBackendLanguagePreference(language),
|
| 545 |
+
doc_type: docType,
|
| 546 |
+
});
|
| 547 |
|
| 548 |
+
const assistantMessage: Message = {
|
| 549 |
+
id: (Date.now() + 1).toString(),
|
| 550 |
+
role: 'assistant',
|
| 551 |
+
content: (resp.reply || '').trim() || '(No response)',
|
| 552 |
+
timestamp: new Date(),
|
| 553 |
+
references: formatRefs(resp.refs),
|
| 554 |
+
sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
|
| 555 |
+
};
|
| 556 |
+
|
| 557 |
+
setIsTyping(false);
|
| 558 |
+
|
| 559 |
+
// Ensure typing indicator disappears before adding message
|
| 560 |
setTimeout(() => {
|
|
|
|
|
|
|
| 561 |
if (chatMode === 'ask') {
|
| 562 |
+
setAskMessages(prev => [...prev, assistantMessage]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
} else if (chatMode === 'review') {
|
| 564 |
+
setReviewMessages(prev => [...prev, assistantMessage]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
}
|
| 566 |
+
}, 50);
|
| 567 |
+
} catch (err: any) {
|
| 568 |
+
console.error(err);
|
| 569 |
+
setIsTyping(false);
|
| 570 |
|
| 571 |
+
const assistantMessage: Message = {
|
| 572 |
+
id: (Date.now() + 1).toString(),
|
| 573 |
+
role: 'assistant',
|
| 574 |
+
content: "Sorry — I couldn't reach the chat server right now. Please try again in a moment.",
|
| 575 |
+
timestamp: new Date(),
|
| 576 |
+
sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
|
| 577 |
+
};
|
| 578 |
+
|
| 579 |
+
setTimeout(() => {
|
| 580 |
+
if (chatMode === 'ask') {
|
| 581 |
+
setAskMessages(prev => [...prev, assistantMessage]);
|
| 582 |
+
} else if (chatMode === 'review') {
|
| 583 |
+
setReviewMessages(prev => [...prev, assistantMessage]);
|
| 584 |
+
}
|
| 585 |
+
}, 50);
|
| 586 |
+
|
| 587 |
+
toast.error('Chat failed. Check the API server logs.');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
}
|
| 589 |
};
|
| 590 |
|
|
|
|
| 592 |
setIsTyping(true);
|
| 593 |
const question = generateQuizQuestion();
|
| 594 |
let questionText = question.question;
|
| 595 |
+
|
| 596 |
if (question.type === 'multiple-choice') {
|
| 597 |
+
questionText += '\n\n' + question.options!.join('\n');
|
| 598 |
}
|
| 599 |
+
|
| 600 |
setTimeout(() => {
|
| 601 |
const assistantMessage: Message = {
|
| 602 |
id: Date.now().toString(),
|
|
|
|
| 609 |
|
| 610 |
// Close typing indicator first
|
| 611 |
setIsTyping(false);
|
| 612 |
+
|
| 613 |
// Wait a bit to ensure typing indicator disappears before adding message
|
| 614 |
setTimeout(() => {
|
| 615 |
setQuizMessages(prev => [...prev, assistantMessage]);
|
| 616 |
+
setQuizState(prev => ({
|
| 617 |
+
...prev,
|
| 618 |
currentQuestion: prev.currentQuestion + 1,
|
| 619 |
waitingForAnswer: true,
|
| 620 |
+
showNextButton: false
|
| 621 |
}));
|
| 622 |
}, 50);
|
| 623 |
}, 2000);
|
|
|
|
| 640 |
};
|
| 641 |
|
| 642 |
const handleFileTypeChange = (index: number, type: FileType) => {
|
| 643 |
+
setUploadedFiles(prev => prev.map((file, i) =>
|
| 644 |
i === index ? { ...file, type } : file
|
| 645 |
));
|
| 646 |
};
|
|
|
|
| 648 |
// Helper function to check if current chat is already saved
|
| 649 |
const isCurrentChatSaved = (): SavedChat | null => {
|
| 650 |
if (messages.length <= 1) return null;
|
| 651 |
+
|
| 652 |
// Find a saved chat that matches the current messages and chatMode
|
| 653 |
return savedChats.find(chat => {
|
| 654 |
if (chat.chatMode !== chatMode) return false;
|
| 655 |
if (chat.messages.length !== messages.length) return false;
|
| 656 |
+
|
| 657 |
// Check if all messages match
|
| 658 |
return chat.messages.every((savedMsg, index) => {
|
| 659 |
const currentMsg = messages[index];
|
|
|
|
| 671 |
toast.info('No conversation to save');
|
| 672 |
return;
|
| 673 |
}
|
| 674 |
+
|
| 675 |
// Check if already saved
|
| 676 |
const existingChat = isCurrentChatSaved();
|
| 677 |
if (existingChat) {
|
|
|
|
| 680 |
toast.success('Chat unsaved');
|
| 681 |
return;
|
| 682 |
}
|
| 683 |
+
|
| 684 |
// Save: add new chat
|
| 685 |
const title = `Chat - ${chatMode === 'ask' ? 'Ask' : chatMode === 'review' ? 'Review' : 'Quiz'} - ${new Date().toLocaleDateString()}`;
|
| 686 |
const newChat: SavedChat = {
|
|
|
|
| 690 |
chatMode,
|
| 691 |
timestamp: new Date(),
|
| 692 |
};
|
| 693 |
+
|
| 694 |
setSavedChats(prev => [newChat, ...prev]);
|
| 695 |
setLeftPanelVisible(true);
|
| 696 |
toast.success('Chat saved!');
|
|
|
|
| 699 |
const handleLoadChat = (savedChat: SavedChat) => {
|
| 700 |
// Set the chat mode first
|
| 701 |
setChatMode(savedChat.chatMode);
|
| 702 |
+
|
| 703 |
// Then set the messages for that mode
|
| 704 |
if (savedChat.chatMode === 'ask') {
|
| 705 |
setAskMessages(savedChat.messages);
|
|
|
|
| 714 |
showNextButton: false,
|
| 715 |
});
|
| 716 |
}
|
| 717 |
+
|
| 718 |
toast.success('Chat loaded!');
|
| 719 |
};
|
| 720 |
|
|
|
|
| 724 |
};
|
| 725 |
|
| 726 |
const handleRenameSavedChat = (id: string, newTitle: string) => {
|
| 727 |
+
setSavedChats(prev => prev.map(chat =>
|
| 728 |
chat.id === id ? { ...chat, title: newTitle } : chat
|
| 729 |
));
|
| 730 |
toast.success('Chat renamed');
|
|
|
|
| 734 |
if (shouldSave) {
|
| 735 |
handleSaveChat();
|
| 736 |
}
|
| 737 |
+
|
| 738 |
const initialMessages: Record<ChatMode, Message[]> = {
|
| 739 |
ask: [{
|
| 740 |
id: '1',
|
|
|
|
| 755 |
timestamp: new Date(),
|
| 756 |
}],
|
| 757 |
};
|
| 758 |
+
|
| 759 |
// Clear only the current mode's conversation
|
| 760 |
if (chatMode === 'ask') {
|
| 761 |
setAskMessages(initialMessages.ask);
|
|
|
|
| 785 |
3. Best practices for ethical AI development
|
| 786 |
|
| 787 |
Exported successfully! ✓`;
|
| 788 |
+
|
| 789 |
setExportResult(result);
|
| 790 |
setResultType('export');
|
| 791 |
toast.success('Conversation exported!');
|
|
|
|
| 806 |
**Question 3:** True or False: AI systems should always prioritize accuracy over fairness.
|
| 807 |
|
| 808 |
Generate quiz based on your conversation!`;
|
| 809 |
+
|
| 810 |
setExportResult(quiz);
|
| 811 |
setResultType('quiz');
|
| 812 |
toast.success('Quiz generated!');
|
|
|
|
| 832 |
|
| 833 |
## Progress Update
|
| 834 |
You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
|
| 835 |
+
|
| 836 |
setExportResult(summary);
|
| 837 |
setResultType('summary');
|
| 838 |
toast.success('Summary generated!');
|
|
|
|
| 846 |
workspaceId?: string
|
| 847 |
) => {
|
| 848 |
if (!content.trim()) return;
|
| 849 |
+
|
| 850 |
// Summary should always be saved as file, not chat
|
| 851 |
// If saving as chat (from RightPanel export/quiz only, not summary)
|
| 852 |
if (saveAsChat && type !== 'summary') {
|
|
|
|
| 866 |
timestamp: new Date(),
|
| 867 |
}
|
| 868 |
];
|
| 869 |
+
|
| 870 |
const title = type === 'export' ? 'Exported Conversation' : 'Micro-Quiz';
|
| 871 |
const newChat: SavedChat = {
|
| 872 |
id: Date.now().toString(),
|
|
|
|
| 875 |
chatMode: 'ask',
|
| 876 |
timestamp: new Date(),
|
| 877 |
};
|
| 878 |
+
|
| 879 |
setSavedChats(prev => [newChat, ...prev]);
|
| 880 |
setLeftPanelVisible(true);
|
| 881 |
toast.success('Chat saved!');
|
| 882 |
return;
|
| 883 |
}
|
| 884 |
+
|
| 885 |
// Otherwise, save as file (existing behavior)
|
| 886 |
// Check if already saved
|
| 887 |
const existingItem = savedItems.find(item => item.content === content && item.type === type);
|
|
|
|
| 890 |
handleUnsave(existingItem.id);
|
| 891 |
return;
|
| 892 |
}
|
| 893 |
+
|
| 894 |
// Save: add new item
|
| 895 |
const title = type === 'export' ? 'Exported Conversation' : type === 'quiz' ? 'Micro-Quiz' : 'Summarization';
|
| 896 |
const newItem: SavedItem = {
|
|
|
|
| 903 |
format,
|
| 904 |
workspaceId: workspaceId || currentWorkspaceId,
|
| 905 |
};
|
| 906 |
+
|
| 907 |
setSavedItems(prev => [newItem, ...prev]);
|
| 908 |
setRecentlySavedId(newItem.id);
|
| 909 |
setLeftPanelVisible(true); // Open left panel
|
| 910 |
+
|
| 911 |
// Clear the highlight after animation
|
| 912 |
setTimeout(() => {
|
| 913 |
setRecentlySavedId(null);
|
| 914 |
}, 2000);
|
| 915 |
+
|
| 916 |
toast.success('Saved for later!');
|
| 917 |
};
|
| 918 |
|
|
|
|
| 930 |
}) => {
|
| 931 |
const id = `group-${Date.now()}`;
|
| 932 |
const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
|
| 933 |
+
|
| 934 |
// Add creator as first member
|
| 935 |
const creatorMember: GroupMember = user ? {
|
| 936 |
id: user.email,
|
|
|
|
| 938 |
email: user.email,
|
| 939 |
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
|
| 940 |
} : { id: 'unknown', name: 'Unknown', email: 'unknown@email.com' };
|
| 941 |
+
|
| 942 |
const members: GroupMember[] = [
|
| 943 |
creatorMember,
|
| 944 |
...payload.invites.map(email => ({
|
|
|
|
| 949 |
];
|
| 950 |
|
| 951 |
let newWorkspace: Workspace;
|
| 952 |
+
|
| 953 |
if (payload.category === 'course') {
|
| 954 |
const courseInfo = availableCourses.find(c => c.id === payload.courseId);
|
| 955 |
newWorkspace = {
|
|
|
|
| 977 |
|
| 978 |
setWorkspaces(prev => [...prev, newWorkspace]);
|
| 979 |
setCurrentWorkspaceId(id);
|
| 980 |
+
|
| 981 |
// Set current course if it's a course workspace
|
| 982 |
if (payload.category === 'course' && payload.courseId) {
|
| 983 |
setCurrentCourseId(payload.courseId);
|
| 984 |
}
|
| 985 |
+
|
| 986 |
toast.success('New group workspace created');
|
| 987 |
};
|
| 988 |
|
|
|
|
| 1005 |
// For testing: always show onboarding (comment out localStorage check)
|
| 1006 |
// const onboardingCompleted = localStorage.getItem(`onboarding_completed_${newUser.email}`);
|
| 1007 |
// if (!onboardingCompleted) {
|
| 1008 |
+
setShowOnboarding(true);
|
| 1009 |
// }
|
| 1010 |
};
|
| 1011 |
|
|
|
|
| 1035 |
if (showOnboarding && user) {
|
| 1036 |
return (
|
| 1037 |
<>
|
| 1038 |
+
<Onboarding
|
| 1039 |
+
user={user}
|
| 1040 |
onComplete={handleOnboardingComplete}
|
| 1041 |
onSkip={handleOnboardingSkip}
|
| 1042 |
/>
|
|
|
|
| 1047 |
return (
|
| 1048 |
<div className="min-h-screen bg-background flex flex-col">
|
| 1049 |
<Toaster />
|
| 1050 |
+
<Header
|
| 1051 |
user={user}
|
| 1052 |
onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
|
| 1053 |
onUserClick={() => {}}
|
|
|
|
| 1081 |
</div>
|
| 1082 |
)}
|
| 1083 |
|
| 1084 |
+
<div
|
| 1085 |
className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative"
|
| 1086 |
style={{ overscrollBehavior: 'none' }}
|
| 1087 |
>
|
|
|
|
| 1100 |
)}
|
| 1101 |
{/* Mobile Sidebar Toggle - Left */}
|
| 1102 |
{leftSidebarOpen && (
|
| 1103 |
+
<div
|
| 1104 |
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 1105 |
onClick={() => setLeftSidebarOpen(false)}
|
| 1106 |
/>
|
|
|
|
| 1108 |
|
| 1109 |
{/* Left Sidebar */}
|
| 1110 |
{leftPanelVisible ? (
|
| 1111 |
+
<aside
|
| 1112 |
className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative"
|
| 1113 |
style={{ borderRight: '1px solid var(--border)', height: 'calc(100vh - 4rem)' }}
|
| 1114 |
>
|
|
|
|
| 1152 |
) : null}
|
| 1153 |
|
| 1154 |
{/* Left Sidebar - Mobile */}
|
| 1155 |
+
<aside
|
| 1156 |
className={`
|
| 1157 |
fixed lg:hidden inset-y-0 left-0 z-50
|
| 1158 |
w-80 bg-card border-r border-border
|
|
|
|
| 1245 |
);
|
| 1246 |
}
|
| 1247 |
|
| 1248 |
+
export default App;
|