Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>AI-Interactive Metacognition Tracker</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- React & ReactDOM --> | |
| <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
| <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
| <!-- @babel/standalone --> | |
| <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <!-- Framer Motion - using different CDN --> | |
| <script crossorigin src="https://cdn.jsdelivr.net/npm/framer-motion@10.16.4/dist/framer-motion.umd.js"></script> | |
| <!-- Recharts - using different CDN --> | |
| <script crossorigin src="https://cdn.jsdelivr.net/npm/recharts@2.8.0/umd/Recharts.min.js"></script> | |
| <style> | |
| html, body { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 100%); | |
| min-height: 100%; | |
| margin: 0; | |
| } | |
| .glass { | |
| background: rgba(255, 255, 255, 0.85); | |
| backdrop-filter: blur(10px); | |
| border-radius: 1.25rem; | |
| box-shadow: 0 8px 32px rgba(59, 130, 246, 0.15); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .ai-message { | |
| background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); | |
| color: white; | |
| border-radius: 1rem 1rem 0.25rem 1rem; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| max-width: 85%; | |
| animation: slideInLeft 0.3s ease-out; | |
| line-height: 1.6; | |
| } | |
| .ai-message p { | |
| margin: 0.5rem 0; | |
| } | |
| .ai-message ul, .ai-message ol { | |
| margin: 0.5rem 0; | |
| padding-left: 1.5rem; | |
| } | |
| .ai-message li { | |
| margin: 0.25rem 0; | |
| } | |
| .ai-message h3, .ai-message h4 { | |
| margin: 1rem 0 0.5rem 0; | |
| font-weight: bold; | |
| } | |
| .ai-message strong { | |
| font-weight: bold; | |
| } | |
| .user-message { | |
| background: linear-gradient(135deg, #10b981 0%, #059669 100%); | |
| color: white; | |
| border-radius: 1rem 1rem 1rem 0.25rem; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| max-width: 80%; | |
| margin-left: auto; | |
| animation: slideInRight 0.3s ease-out; | |
| } | |
| .typing-indicator { | |
| background: #f3f4f6; | |
| border-radius: 1rem; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| max-width: 80%; | |
| animation: pulse 1.5s infinite; | |
| } | |
| .chat-container { | |
| max-height: 500px; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 0.75rem; | |
| background: rgba(255, 255, 255, 0.5); | |
| } | |
| .progress-bar { | |
| background: linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%); | |
| height: 6px; | |
| border-radius: 3px; | |
| transition: width 0.5s ease; | |
| } | |
| @keyframes slideInLeft { | |
| from { opacity: 0; transform: translateX(-20px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| @keyframes slideInRight { | |
| from { opacity: 0; transform: translateX(20px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-in; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Accessibility improvements */ | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border: 0; | |
| } | |
| button:focus, input:focus, textarea:focus, select:focus { | |
| outline: 2px solid #3b82f6; | |
| outline-offset: 2px; | |
| } | |
| @media (max-width: 640px) { | |
| .ai-message, .user-message { | |
| max-width: 95%; | |
| } | |
| .chat-container { | |
| max-height: 400px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect, useCallback, useRef } = React; | |
| // Fallback motion component if framer-motion fails to load | |
| const motion = window['framer-motion'] ? window['framer-motion'].motion : { | |
| div: ({ children, className, initial, animate, ...props }) => | |
| React.createElement('div', { className: `${className} fade-in`, ...props }, children) | |
| }; | |
| // Fallback chart components if Recharts fails to load | |
| const Recharts = window['Recharts'] || {}; | |
| const { | |
| LineChart = () => React.createElement('div', { className: 'text-center text-gray-500 p-4' }, 'Charts unavailable - external library loading issue'), | |
| Line = () => null, | |
| XAxis = () => null, | |
| YAxis = () => null, | |
| CartesianGrid = () => null, | |
| Tooltip = () => null, | |
| ResponsiveContainer = ({ children }) => React.createElement('div', { style: { width: '100%', height: '160px' } }, children), | |
| RadarChart = () => React.createElement('div', { className: 'text-center text-gray-500 p-4' }, 'Charts unavailable - external library loading issue'), | |
| PolarGrid = () => null, | |
| PolarAngleAxis = () => null, | |
| PolarRadiusAxis = () => null, | |
| Radar = () => null | |
| } = Recharts; | |
| const AI_PROMPTS = { | |
| welcome: `You are an AI learning coach specializing in metacognition. Your role is to guide users through a reflective learning process. | |
| FORMATTING REQUIREMENTS: | |
| - Use proper paragraph breaks with double line breaks | |
| - Use bullet points (•) for lists | |
| - Use numbered lists (1., 2., 3.) for sequential steps | |
| - Use **bold** for emphasis on key points | |
| - Structure responses with clear sections | |
| - Keep paragraphs concise (2-3 sentences max) | |
| Be encouraging, supportive, and ask thoughtful questions. Start by welcoming the user and explaining that you'll guide them through a metacognitive reflection process to improve their learning.`, | |
| taskPlanning: `Help the user articulate their learning task clearly. | |
| FORMATTING REQUIREMENTS: | |
| - Use proper paragraph breaks | |
| - Use bullet points for suggestions | |
| - Use **bold** for key concepts | |
| - Structure your response with clear sections | |
| Ask about: | |
| • Their specific learning goals | |
| • What they want to achieve | |
| • Help them break down complex tasks | |
| Suggest appropriate learning strategies based on the task type. Be encouraging and help them set realistic expectations.`, | |
| preLearning: `Guide the user through pre-learning reflection. | |
| FORMATTING REQUIREMENTS: | |
| - Use clear paragraph structure | |
| - Use bullet points for strategies | |
| - Use **bold** for important concepts | |
| - Include numbered steps when appropriate | |
| Help them: | |
| • Assess their current confidence level | |
| • Identify potential challenges | |
| • Prepare mentally for the learning task | |
| • Suggest specific strategies and resources | |
| • Explore their prior knowledge and experience`, | |
| duringLearning: `Support the user during their learning process. | |
| FORMATTING REQUIREMENTS: | |
| - Use paragraph breaks for readability | |
| - Use bullet points for suggestions | |
| - Use **bold** for key advice | |
| - Structure responses clearly | |
| Ask about: | |
| • Their current progress | |
| • Any difficulties they're encountering | |
| • Whether their chosen strategies are working | |
| Provide adaptive guidance and suggest adjustments if needed. Be encouraging and help them stay motivated.`, | |
| postLearning: `Help the user evaluate their learning experience. | |
| FORMATTING REQUIREMENTS: | |
| - Use clear paragraph structure | |
| - Use bullet points for reflection prompts | |
| - Use **bold** for key insights | |
| - Organize thoughts into sections | |
| Guide them to reflect on: | |
| • What worked well and what didn't | |
| • Whether they achieved their goals | |
| • What they learned about their own learning process | |
| • Patterns and insights about their metacognitive strategies`, | |
| developmentPlan: `Based on the entire conversation history, provide a comprehensive personalized development plan. | |
| FORMATTING REQUIREMENTS: | |
| - Use clear headings with **bold** | |
| - Use bullet points and numbered lists extensively | |
| - Use paragraph breaks for readability | |
| - Structure as a complete action plan | |
| Create a detailed plan that includes: | |
| **METACOGNITIVE STRENGTHS IDENTIFIED:** | |
| • List specific strengths observed | |
| • Evidence from their responses | |
| **AREAS FOR IMPROVEMENT:** | |
| • Specific skills to develop | |
| • Patterns that need attention | |
| **PERSONALIZED LEARNING STRATEGIES:** | |
| • Tailored strategies based on their task type | |
| • Specific techniques that match their learning style | |
| **ACTION PLAN:** | |
| 1. Immediate next steps (this week) | |
| 2. Short-term goals (next month) | |
| 3. Long-term development (next 3 months) | |
| **RECOMMENDED ACTIVITIES:** | |
| • Specific exercises and practices | |
| • Resources and tools to use | |
| • Frequency and timing suggestions | |
| **PROGRESS TRACKING:** | |
| • How to monitor improvement | |
| • Key indicators to watch for | |
| • When to reassess and adjust | |
| Make this a comprehensive, actionable plan they can follow.` | |
| }; | |
| function AIInteractiveMetacognitionTracker() { | |
| // Core state | |
| const [stage, setStage] = useState(0); // 0: Setup, 1: Task Planning, 2: Pre-Learning, 3: During Learning, 4: Post-Learning, 5: Development Plan | |
| const [apiKey, setApiKey] = useState(""); | |
| const [apiKeyInput, setApiKeyInput] = useState(""); | |
| const [apiError, setApiError] = useState(""); | |
| // Conversation state | |
| const [conversations, setConversations] = useState({}); | |
| const [currentInput, setCurrentInput] = useState(""); | |
| const [isAITyping, setIsAITyping] = useState(false); | |
| const [aiLoading, setAILoading] = useState(false); | |
| // User data | |
| const [userProfile, setUserProfile] = useState({}); | |
| const [sessionData, setSessionData] = useState({}); | |
| const [reflectionHistory, setReflectionHistory] = useState([]); | |
| // Refs | |
| const chatEndRef = useRef(null); | |
| // Load data from localStorage on mount | |
| useEffect(() => { | |
| try { | |
| const savedApiKey = localStorage.getItem('ai-metacognition-api-key'); | |
| const savedProfile = localStorage.getItem('ai-metacognition-profile'); | |
| const savedHistory = localStorage.getItem('ai-metacognition-history'); | |
| if (savedApiKey) { | |
| setApiKey(savedApiKey); | |
| } | |
| if (savedProfile) { | |
| setUserProfile(JSON.parse(savedProfile)); | |
| } | |
| if (savedHistory) { | |
| setReflectionHistory(JSON.parse(savedHistory)); | |
| } | |
| } catch (error) { | |
| console.error('Error loading saved data:', error); | |
| } | |
| }, []); | |
| // Save data to localStorage | |
| useEffect(() => { | |
| try { | |
| if (apiKey) { | |
| localStorage.setItem('ai-metacognition-api-key', apiKey); | |
| } | |
| } catch (error) { | |
| console.error('Error saving API key:', error); | |
| } | |
| }, [apiKey]); | |
| useEffect(() => { | |
| try { | |
| localStorage.setItem('ai-metacognition-profile', JSON.stringify(userProfile)); | |
| } catch (error) { | |
| console.error('Error saving profile:', error); | |
| } | |
| }, [userProfile]); | |
| useEffect(() => { | |
| try { | |
| localStorage.setItem('ai-metacognition-history', JSON.stringify(reflectionHistory)); | |
| } catch (error) { | |
| console.error('Error saving history:', error); | |
| } | |
| }, [reflectionHistory]); | |
| // Auto-scroll to bottom of chat | |
| useEffect(() => { | |
| chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [conversations, isAITyping]); | |
| // Initialize conversation when stage changes | |
| useEffect(() => { | |
| if (stage > 0 && apiKey && !conversations[stage]) { | |
| initializeStageConversation(stage); | |
| } | |
| }, [stage, apiKey]); | |
| const initializeStageConversation = async (stageNum) => { | |
| const stageNames = ['', 'Task Planning', 'Pre-Learning Reflection', 'During Learning Support', 'Post-Learning Evaluation', 'Development Plan']; | |
| const stageName = stageNames[stageNum]; | |
| setIsAITyping(true); | |
| try { | |
| const contextPrompt = getContextPrompt(stageNum); | |
| const aiResponse = await callAI(contextPrompt, stageNum); | |
| setConversations(prev => ({ | |
| ...prev, | |
| [stageNum]: [ | |
| { type: 'ai', content: aiResponse, timestamp: new Date().toISOString() } | |
| ] | |
| })); | |
| } catch (error) { | |
| console.error('Error initializing conversation:', error); | |
| setApiError(`Failed to initialize ${stageName}: ${error.message}`); | |
| } finally { | |
| setIsAITyping(false); | |
| } | |
| }; | |
| const getContextPrompt = (stageNum) => { | |
| const basePrompt = AI_PROMPTS[Object.keys(AI_PROMPTS)[stageNum]]; | |
| // For the final stage, include comprehensive context from all previous stages | |
| if (stageNum === 5) { | |
| const allConversations = Object.keys(conversations) | |
| .filter(key => parseInt(key) < 5) | |
| .map(key => ({ | |
| stage: parseInt(key), | |
| stageName: ['', 'Task Planning', 'Pre-Learning', 'During Learning', 'Post-Learning'][parseInt(key)], | |
| conversation: conversations[key] | |
| })); | |
| const contextSummary = allConversations.map(stage => { | |
| const userMessages = stage.conversation.filter(msg => msg.type === 'user').map(msg => msg.content); | |
| const aiMessages = stage.conversation.filter(msg => msg.type === 'ai').map(msg => msg.content); | |
| return `**${stage.stageName} (Stage ${stage.stage}):** | |
| User Responses: ${userMessages.join(' | ')} | |
| AI Guidance: ${aiMessages.slice(-1)[0] || 'No AI response'}`; | |
| }).join('\n\n'); | |
| return `${basePrompt} | |
| **COMPLETE SESSION CONTEXT:** | |
| ${contextSummary} | |
| **SESSION DATA:** | |
| ${JSON.stringify(sessionData, null, 2)} | |
| Based on this complete interaction history, create a comprehensive, personalized development plan.`; | |
| } | |
| const context = { | |
| userProfile, | |
| sessionData, | |
| previousConversations: conversations, | |
| stage: stageNum | |
| }; | |
| return `${basePrompt}\n\nContext: ${JSON.stringify(context, null, 2)}\n\nProvide a welcoming message to start this stage.`; | |
| }; | |
| const callAI = async (prompt, stageNum, userMessage = '') => { | |
| if (!apiKey) { | |
| throw new Error('API key not provided'); | |
| } | |
| const messages = [ | |
| { role: 'system', content: prompt } | |
| ]; | |
| // Add conversation history | |
| const stageConversation = conversations[stageNum] || []; | |
| stageConversation.forEach(msg => { | |
| messages.push({ | |
| role: msg.type === 'ai' ? 'assistant' : 'user', | |
| content: msg.content | |
| }); | |
| }); | |
| if (userMessage) { | |
| messages.push({ role: 'user', content: userMessage }); | |
| } | |
| const response = await fetch('https://api.openai.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${apiKey.trim()}` | |
| }, | |
| body: JSON.stringify({ | |
| model: 'gpt-4o-mini', | |
| messages, | |
| max_tokens: stageNum === 5 ? 1000 : 600, // More tokens for final comprehensive plan | |
| temperature: 0.7 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`OpenAI API error: ${response.status} ${response.statusText}${errorData.error ? ` - ${errorData.error.message}` : ''}`); | |
| } | |
| const data = await response.json(); | |
| if (!data.choices || !data.choices[0] || !data.choices[0].message) { | |
| throw new Error('Invalid response format from OpenAI API'); | |
| } | |
| return data.choices[0].message.content; | |
| }; | |
| const formatAIResponse = (content) => { | |
| // Convert markdown-style formatting to HTML | |
| let formatted = content | |
| // Convert **bold** to <strong> | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | |
| // Convert bullet points | |
| .replace(/^• (.+)$/gm, '<li>$1</li>') | |
| .replace(/^- (.+)$/gm, '<li>$1</li>') | |
| // Convert numbered lists | |
| .replace(/^(\d+)\. (.+)$/gm, '<li>$2</li>') | |
| // Convert double line breaks to paragraphs | |
| .split('\n\n') | |
| .map(paragraph => { | |
| if (paragraph.includes('<li>')) { | |
| return `<ul>${paragraph}</ul>`; | |
| } | |
| return paragraph.trim() ? `<p>${paragraph.trim()}</p>` : ''; | |
| }) | |
| .join(''); | |
| return formatted; | |
| }; | |
| const handleSendMessage = async () => { | |
| if (!currentInput.trim() || isAITyping) return; | |
| const userMessage = currentInput.trim(); | |
| setCurrentInput(""); | |
| // Add user message to conversation | |
| setConversations(prev => ({ | |
| ...prev, | |
| [stage]: [ | |
| ...(prev[stage] || []), | |
| { type: 'user', content: userMessage, timestamp: new Date().toISOString() } | |
| ] | |
| })); | |
| setIsAITyping(true); | |
| setAILoading(true); | |
| try { | |
| const contextPrompt = getContextPrompt(stage); | |
| const aiResponse = await callAI(contextPrompt, stage, userMessage); | |
| // Add AI response to conversation | |
| setConversations(prev => ({ | |
| ...prev, | |
| [stage]: [ | |
| ...prev[stage], | |
| { type: 'ai', content: aiResponse, timestamp: new Date().toISOString() } | |
| ] | |
| })); | |
| // Update session data based on stage | |
| updateSessionData(stage, userMessage, aiResponse); | |
| } catch (error) { | |
| console.error('Error getting AI response:', error); | |
| setApiError(`AI Error: ${error.message}`); | |
| } finally { | |
| setIsAITyping(false); | |
| setAILoading(false); | |
| } | |
| }; | |
| const updateSessionData = (stageNum, userMessage, aiResponse) => { | |
| const timestamp = new Date().toISOString(); | |
| setSessionData(prev => ({ | |
| ...prev, | |
| [`stage${stageNum}`]: { | |
| ...prev[`stage${stageNum}`], | |
| lastUpdate: timestamp, | |
| userInput: userMessage, | |
| aiGuidance: aiResponse, | |
| keyInsights: extractKeyInsights(userMessage, stageNum) | |
| } | |
| })); | |
| }; | |
| const extractKeyInsights = (userMessage, stageNum) => { | |
| // Extract key insights based on stage | |
| const insights = {}; | |
| switch(stageNum) { | |
| case 1: // Task Planning | |
| insights.learningTask = userMessage; | |
| break; | |
| case 2: // Pre-Learning | |
| insights.initialConfidence = userMessage; | |
| break; | |
| case 3: // During Learning | |
| insights.learningProgress = userMessage; | |
| break; | |
| case 4: // Post-Learning | |
| insights.learningOutcome = userMessage; | |
| break; | |
| case 5: // Development Plan | |
| insights.developmentCommitment = userMessage; | |
| break; | |
| } | |
| return insights; | |
| }; | |
| const handleKeyPress = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleSendMessage(); | |
| } | |
| }; | |
| const validateApiKey = async () => { | |
| if (!apiKeyInput.trim()) { | |
| setApiError("Please enter your OpenAI API key"); | |
| return; | |
| } | |
| setAILoading(true); | |
| setApiError(""); | |
| try { | |
| const response = await fetch('https://api.openai.com/v1/models', { | |
| headers: { | |
| 'Authorization': `Bearer ${apiKeyInput.trim()}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Invalid API key'); | |
| } | |
| setApiKey(apiKeyInput.trim()); | |
| setStage(1); | |
| setApiKeyInput(""); | |
| } catch (error) { | |
| setApiError("Invalid API key. Please check and try again."); | |
| } finally { | |
| setAILoading(false); | |
| } | |
| }; | |
| const nextStage = () => { | |
| if (stage < 5) { | |
| setStage(stage + 1); | |
| } | |
| }; | |
| const previousStage = () => { | |
| if (stage > 1) { | |
| setStage(stage - 1); | |
| } | |
| }; | |
| const completeSession = () => { | |
| const sessionSummary = { | |
| id: Date.now(), | |
| timestamp: new Date().toISOString(), | |
| conversations, | |
| sessionData, | |
| userProfile, | |
| completedStages: 5 | |
| }; | |
| setReflectionHistory(prev => [...prev, sessionSummary]); | |
| // Reset for new session | |
| setStage(1); | |
| setConversations({}); | |
| setSessionData({}); | |
| setCurrentInput(""); | |
| }; | |
| const getProgressPercentage = () => { | |
| return (stage / 5) * 100; | |
| }; | |
| const stageNames = [ | |
| 'Setup', | |
| 'Task Planning', | |
| 'Pre-Learning Reflection', | |
| 'During Learning Support', | |
| 'Post-Learning Evaluation', | |
| 'Development Plan' | |
| ]; | |
| // Stage 0: API Key Setup | |
| if (stage === 0) { | |
| return ( | |
| <div className="min-h-screen flex flex-col items-center justify-center py-8"> | |
| <motion.div className="max-w-md w-full px-4"> | |
| <div className="glass p-8 text-center"> | |
| <div className="mb-6"> | |
| <h1 className="text-3xl font-bold text-blue-900 mb-2">🤖 AI Learning Coach</h1> | |
| <p className="text-gray-600">Your personal AI guide for metacognitive reflection</p> | |
| </div> | |
| <div className="mb-6"> | |
| <label htmlFor="api-key" className="block font-semibold mb-2 text-left">OpenAI API Key</label> | |
| <input | |
| id="api-key" | |
| type="password" | |
| className="w-full px-4 py-3 border rounded-lg text-center" | |
| placeholder="sk-..." | |
| value={apiKeyInput} | |
| onChange={e => setApiKeyInput(e.target.value)} | |
| onKeyPress={e => e.key === 'Enter' && validateApiKey()} | |
| autoFocus | |
| /> | |
| <p className="text-xs text-gray-500 mt-2"> | |
| Get your API key at <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">OpenAI</a> | |
| </p> | |
| </div> | |
| {apiError && ( | |
| <div className="mb-4 text-red-600 text-sm">{apiError}</div> | |
| )} | |
| <button | |
| onClick={validateApiKey} | |
| disabled={aiLoading || !apiKeyInput.trim()} | |
| className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition disabled:opacity-50" | |
| > | |
| {aiLoading ? "Validating..." : "Start AI-Guided Learning"} | |
| </button> | |
| <div className="mt-6 text-xs text-gray-500"> | |
| <p>🔒 Your API key is stored locally and used only for this session</p> | |
| <p>🎯 AI will guide you through 5 stages of metacognitive reflection</p> | |
| <p>📝 Responses will be structured with clear formatting</p> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |
| // Main Application Interface | |
| return ( | |
| <div className="min-h-screen flex flex-col py-4"> | |
| <div className="max-w-4xl w-full mx-auto px-4"> | |
| {/* Header */} | |
| <div className="glass p-4 mb-4"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <div> | |
| <h1 className="text-2xl font-bold text-blue-900">🤖 AI Learning Coach</h1> | |
| <p className="text-sm text-gray-600">Stage {stage}: {stageNames[stage]}</p> | |
| </div> | |
| <div className="text-right"> | |
| <div className="text-xs text-gray-500 mb-1">Progress</div> | |
| <div className="w-32 bg-gray-200 rounded-full h-2"> | |
| <div | |
| className="progress-bar rounded-full h-2" | |
| style={{ width: `${getProgressPercentage()}%` }} | |
| ></div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Stage Navigation */} | |
| <div className="flex gap-2 text-xs flex-wrap"> | |
| {stageNames.slice(1).map((name, i) => ( | |
| <span | |
| key={i} | |
| className={`px-2 py-1 rounded-full ${ | |
| stage === i + 1 | |
| ? "bg-blue-600 text-white" | |
| : stage > i + 1 | |
| ? "bg-green-100 text-green-800" | |
| : "bg-gray-100 text-gray-600" | |
| }`} | |
| > | |
| {i + 1}. {name} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Chat Interface */} | |
| <div className="glass p-4 mb-4"> | |
| <div className="chat-container"> | |
| {conversations[stage]?.map((message, index) => ( | |
| <div | |
| key={index} | |
| className={message.type === 'ai' ? 'ai-message' : 'user-message'} | |
| > | |
| <div className="flex items-start gap-2"> | |
| <span className="text-xs opacity-75 mt-1"> | |
| {message.type === 'ai' ? '🤖' : '👤'} | |
| </span> | |
| <div className="flex-1"> | |
| {message.type === 'ai' ? ( | |
| <div dangerouslySetInnerHTML={{ __html: formatAIResponse(message.content) }} /> | |
| ) : ( | |
| message.content | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| {isAITyping && ( | |
| <div className="typing-indicator"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs">🤖</span> | |
| <div>AI is thinking...</div> | |
| <div className="flex gap-1"> | |
| <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div> | |
| <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div> | |
| <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={chatEndRef} /> | |
| </div> | |
| {/* Input Area */} | |
| <div className="mt-4 flex gap-2"> | |
| <textarea | |
| value={currentInput} | |
| onChange={e => setCurrentInput(e.target.value)} | |
| onKeyPress={handleKeyPress} | |
| placeholder="Type your response here... (Press Enter to send, Shift+Enter for new line)" | |
| className="flex-1 px-3 py-2 border rounded-lg resize-none" | |
| rows={3} | |
| disabled={isAITyping} | |
| /> | |
| <button | |
| onClick={handleSendMessage} | |
| disabled={!currentInput.trim() || isAITyping} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50" | |
| > | |
| Send | |
| </button> | |
| </div> | |
| {apiError && ( | |
| <div className="mt-2 text-red-600 text-sm">{apiError}</div> | |
| )} | |
| </div> | |
| {/* Navigation */} | |
| <div className="glass p-4"> | |
| <div className="flex justify-between items-center"> | |
| <button | |
| onClick={previousStage} | |
| disabled={stage <= 1} | |
| className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition disabled:opacity-50" | |
| > | |
| ← Previous Stage | |
| </button> | |
| <div className="text-center"> | |
| <p className="text-sm text-gray-600"> | |
| {conversations[stage]?.length || 0} messages in this stage | |
| </p> | |
| {stage === 5 && ( | |
| <p className="text-xs text-blue-600 font-semibold"> | |
| 📋 Comprehensive Development Plan | |
| </p> | |
| )} | |
| </div> | |
| {stage < 5 ? ( | |
| <button | |
| onClick={nextStage} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition" | |
| > | |
| Next Stage → | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={completeSession} | |
| className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition" | |
| > | |
| Complete Session ✓ | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Session History */} | |
| {reflectionHistory.length > 0 && ( | |
| <div className="glass p-4 mt-4"> | |
| <h3 className="font-bold text-blue-900 mb-2">Previous Sessions</h3> | |
| <div className="text-sm text-gray-600"> | |
| {reflectionHistory.length} completed reflection session{reflectionHistory.length !== 1 ? 's' : ''} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById("root")).render(<AIInteractiveMetacognitionTracker />); | |
| </script> | |
| </body> | |
| </html> | |