BonelliLab commited on
Commit
f28ec30
·
1 Parent(s): b3fc910

feat: Add creative learning modes and personalization

Browse files

- Implemented 6 learning modes: Standard, Socratic, ELI5, Technical, Analogy, Code
- Added difficulty slider (1-5 scale from Beginner to Expert)
- Created tutor persona system (Friendly, Strict, Enthusiastic, Professional, Playful)
- Built complete gamification: achievements, streaks, question counter
- Added typing animation for responses
- Enhanced UI with stats dashboard, mode icons, and smooth animations
- Integrated prompt enhancement suggestions
- Added 'Surprise Me' random question generator
- Implemented share and regenerate features
- Updated Gradio app with mode/difficulty/persona controls
- All features work in DEMO_MODE for safe deployment

Files changed (3) hide show
  1. api/ask.py +50 -9
  2. app.py +75 -16
  3. public/index.html +610 -138
api/ask.py CHANGED
@@ -45,6 +45,9 @@ class AskIn(BaseModel):
45
  max_tokens: Optional[int] = 512
46
  temperature: Optional[float] = 0.7
47
  session_id: Optional[str] = None # for conversation history
 
 
 
48
 
49
 
50
  class AskOut(BaseModel):
@@ -54,19 +57,57 @@ class AskOut(BaseModel):
54
  session_id: str = "" # returned session ID
55
 
56
 
57
- def get_demo_response(prompt: str) -> str:
58
- """Generate deterministic demo responses."""
59
  p = prompt.strip().lower()
60
  if not p:
61
  return "Please enter a question for the demo tutor."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  if "explain" in p or "what is" in p:
63
- return f"**Demo Explanation:**\n\nHere's a concise explanation for your question: *\"{prompt}\"*.\n\n[Demo mode active. Configure `INFERENCE_API_URL` to use a real model.]"
 
64
  if "code" in p or "how to" in p or "implement" in p:
65
- return f"**Demo Steps:**\n\n1. Understand the problem: *\"{prompt}\"*\n2. Break it down into smaller steps\n3. Implement and test\n4. Iterate and refine\n\n[Demo-mode response]"
66
- if "compare" in p or "difference" in p:
67
- return f"**Demo Comparison:**\n\nKey differences related to *\"{prompt}\"*:\n- Point A vs Point B\n- Tradeoffs and use cases\n\n[Demo mode]"
68
- # Generic fallback
69
- return f"**Demo Response:**\n\nI understood your prompt: *\"{prompt}\"*.\n\nThis is a demo response showing how the tutor would reply. Set `INFERENCE_API_URL` to enable real model inference."
70
 
71
 
72
  async def call_inference_api(
@@ -137,7 +178,7 @@ async def ask(in_data: AskIn, request: Request):
137
 
138
  # Demo mode
139
  if demo_mode or not api_url:
140
- result_text = get_demo_response(in_data.prompt)
141
  save_conversation(session_id, in_data.prompt, result_text, "demo")
142
  return AskOut(result=result_text, source="demo", session_id=session_id)
143
 
 
45
  max_tokens: Optional[int] = 512
46
  temperature: Optional[float] = 0.7
47
  session_id: Optional[str] = None # for conversation history
48
+ mode: Optional[str] = "standard" # learning mode: standard, socratic, eli5, technical, analogy, code
49
+ difficulty: Optional[int] = 3 # 1-5 difficulty scale
50
+ persona: Optional[str] = "friendly" # friendly, strict, enthusiastic, professional
51
 
52
 
53
  class AskOut(BaseModel):
 
57
  session_id: str = "" # returned session ID
58
 
59
 
60
+ def get_demo_response(prompt: str, mode: str = "standard", difficulty: int = 3, persona: str = "friendly") -> str:
61
+ """Generate deterministic demo responses with learning modes and personalization."""
62
  p = prompt.strip().lower()
63
  if not p:
64
  return "Please enter a question for the demo tutor."
65
+
66
+ # Persona prefixes
67
+ persona_styles = {
68
+ "friendly": "😊 ",
69
+ "strict": "📚 ",
70
+ "enthusiastic": "🎉 ",
71
+ "professional": "🎓 ",
72
+ "playful": "🎮 "
73
+ }
74
+ prefix = persona_styles.get(persona, "")
75
+
76
+ # Mode-specific responses
77
+ if mode == "socratic":
78
+ return f"{prefix}**Socratic Mode** 🤔\n\nGreat question! Let me guide you with some questions:\n\n1. What do you already know about *\"{prompt}\"*?\n2. Can you think of a similar concept you're familiar with?\n3. What would happen if we changed one key variable?\n4. How would you explain this to someone younger?\n\n[Demo mode - these questions would adapt based on your actual responses]"
79
+
80
+ elif mode == "eli5":
81
+ return f"{prefix}**ELI5 Mode** 👶\n\nOkay, imagine *\"{prompt}\"* like this:\n\nThink of it like building with LEGO blocks. Each block is a simple piece, but when you put them together in the right way, you can build amazing things!\n\n[Demo mode - real responses would use age-appropriate analogies]"
82
+
83
+ elif mode == "technical":
84
+ difficulty_markers = ["Beginner", "Intermediate", "Advanced", "Expert", "Research-Level"]
85
+ level = difficulty_markers[min(difficulty - 1, 4)]
86
+ return f"{prefix}**Technical Deep-Dive** 🔬 (Level: {level})\n\n**Topic:** {prompt}\n\n**Core Concepts:**\n- Fundamental principles and definitions\n- Mathematical/logical foundations\n- Implementation details and edge cases\n- Performance considerations\n- Common pitfalls and best practices\n\n[Demo mode - depth would match difficulty level {difficulty}/5]"
87
+
88
+ elif mode == "analogy":
89
+ analogies = [
90
+ "a restaurant kitchen (preparation → cooking → serving)",
91
+ "a postal system (sending → routing → delivery)",
92
+ "a factory assembly line (input → processing → output)",
93
+ "a team sport (strategy → execution → scoring)"
94
+ ]
95
+ import random
96
+ random.seed(len(prompt)) # deterministic
97
+ analogy = random.choice(analogies)
98
+ return f"{prefix}**Analogy Master** 🎭\n\nLet me explain *\"{prompt}\"* using an analogy:\n\nIt's like {analogy}.\n\nEach step has a purpose, and when they work together, magic happens!\n\n[Demo mode - analogies would be carefully crafted for each topic]"
99
+
100
+ elif mode == "code":
101
+ return f"{prefix}**Code Mentor** 💻\n\n```python\n# Pseudocode for: {prompt}\n\nclass Solution:\n def solve(self, problem):\n # Step 1: Understand the requirements\n requirements = self.analyze(problem)\n \n # Step 2: Break down into smaller pieces\n components = self.decompose(requirements)\n \n # Step 3: Implement each piece\n for component in components:\n self.implement(component)\n \n # Step 4: Test and refine\n return self.test_and_validate()\n```\n\n[Demo mode - would provide working code examples]"
102
+
103
+ # Standard mode (fallback)
104
  if "explain" in p or "what is" in p:
105
+ return f"{prefix}**Standard Explanation:**\n\nHere's a concise explanation for *\"{prompt}\"*:\n\n• **Key Point 1:** Main concept overview\n• **Key Point 2:** Why it matters\n• **Key Point 3:** How it's used in practice\n\n[Demo mode - set DEMO_MODE=1 or configure INFERENCE_API_URL]"
106
+
107
  if "code" in p or "how to" in p or "implement" in p:
108
+ return f"{prefix}**Implementation Guide:**\n\n**Problem:** {prompt}\n\n**Approach:**\n1. Define the requirements clearly\n2. Choose the right data structures\n3. Write clean, testable code\n4. Handle edge cases\n\n[Demo mode]"
109
+
110
+ return f"{prefix}**Response:**\n\nI understood your prompt: *\"{prompt}\"*.\n\nThis is a demo response. Try different **learning modes** (Socratic, ELI5, Technical, Analogy, Code) for varied approaches!\n\n[Demo mode]"
 
 
111
 
112
 
113
  async def call_inference_api(
 
178
 
179
  # Demo mode
180
  if demo_mode or not api_url:
181
+ result_text = get_demo_response(in_data.prompt, in_data.mode, in_data.difficulty, in_data.persona)
182
  save_conversation(session_id, in_data.prompt, result_text, "demo")
183
  return AskOut(result=result_text, source="demo", session_id=session_id)
184
 
app.py CHANGED
@@ -26,8 +26,8 @@ def _demo_reply(prompt: str) -> str:
26
  return f"**Demo Response:**\n\nI understood your prompt: *\"{p}\"*.\n\nThis is a demo response showing how the tutor would reply. Set `INFERENCE_API_URL` to enable real model inference."
27
 
28
 
29
- def ask_sync(question: str) -> str:
30
- """Handle question answering with demo mode, inference API, or local model fallback."""
31
  if not question or not question.strip():
32
  return "Please enter a question for the tutor."
33
 
@@ -42,7 +42,12 @@ def ask_sync(question: str) -> str:
42
 
43
  resp = httpx.post(
44
  INFERENCE_API_URL,
45
- json={"inputs": question},
 
 
 
 
 
46
  headers=headers,
47
  timeout=60.0
48
  )
@@ -50,6 +55,8 @@ def ask_sync(question: str) -> str:
50
  data = resp.json()
51
 
52
  # Normalize response
 
 
53
  if isinstance(data, list) and len(data) > 0:
54
  first = data[0]
55
  if isinstance(first, dict) and "generated_text" in first:
@@ -62,9 +69,34 @@ def ask_sync(question: str) -> str:
62
  except Exception as e:
63
  return f"⚠️ Inference API error: {e}\n\nFalling back to demo mode..."
64
 
65
- # Demo mode
66
  if DEMO_MODE:
67
- return _demo_reply(question)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  # Fallback to local model (only for developers with model weights)
70
  try:
@@ -77,19 +109,46 @@ def ask_sync(question: str) -> str:
77
 
78
  iface = gr.Interface(
79
  fn=ask_sync,
80
- inputs=gr.Textbox(
81
- label="Ask the tutor",
82
- placeholder="Enter your question here (e.g., 'Explain Newton's laws')",
83
- lines=3
84
- ),
85
- outputs=gr.Textbox(label="Tutor response", lines=10),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  title="🧠 Eidolon Cognitive Tutor",
87
- description="Interactive tutor demo. Running in **demo mode** by default (set `DEMO_MODE=1` or configure `INFERENCE_API_URL` for real inference).",
 
 
 
 
 
88
  examples=[
89
- ["Explain Newton's laws in simple terms"],
90
- ["How do I implement a binary search in Python?"],
91
- ["Compare supervised vs unsupervised learning"],
92
- ["What is the difference between HTTP and HTTPS?"]
93
  ],
94
  theme="soft"
95
  )
 
26
  return f"**Demo Response:**\n\nI understood your prompt: *\"{p}\"*.\n\nThis is a demo response showing how the tutor would reply. Set `INFERENCE_API_URL` to enable real model inference."
27
 
28
 
29
+ def ask_sync(question: str, mode: str = "standard", difficulty: int = 3, persona: str = "friendly") -> str:
30
+ """Handle question answering with learning modes, demo mode, inference API, or local model fallback."""
31
  if not question or not question.strip():
32
  return "Please enter a question for the tutor."
33
 
 
42
 
43
  resp = httpx.post(
44
  INFERENCE_API_URL,
45
+ json={
46
+ "prompt": question,
47
+ "mode": mode,
48
+ "difficulty": difficulty,
49
+ "persona": persona
50
+ },
51
  headers=headers,
52
  timeout=60.0
53
  )
 
55
  data = resp.json()
56
 
57
  # Normalize response
58
+ if isinstance(data, dict) and "result" in data:
59
+ return data["result"]
60
  if isinstance(data, list) and len(data) > 0:
61
  first = data[0]
62
  if isinstance(first, dict) and "generated_text" in first:
 
69
  except Exception as e:
70
  return f"⚠️ Inference API error: {e}\n\nFalling back to demo mode..."
71
 
72
+ # Demo mode with learning modes support
73
  if DEMO_MODE:
74
+ mode_emoji = {
75
+ "socratic": "🤔",
76
+ "eli5": "👶",
77
+ "technical": "🔬",
78
+ "analogy": "🎭",
79
+ "code": "💻"
80
+ }.get(mode, "📚")
81
+
82
+ persona_prefix = {
83
+ "friendly": "Hey there! 😊",
84
+ "strict": "Attention, student.",
85
+ "enthusiastic": "OMG this is so cool! 🎉",
86
+ "professional": "Greetings.",
87
+ "playful": "Hehe, ready to learn? 😄"
88
+ }.get(persona, "Hello!")
89
+
90
+ base_reply = _demo_reply(question)
91
+
92
+ return (
93
+ f"{mode_emoji} **{mode.upper()} Mode** | Difficulty: {difficulty}/5 | Persona: {persona}\n\n"
94
+ f"{persona_prefix}\n\n"
95
+ f"{base_reply}\n\n"
96
+ "---\n"
97
+ "💡 **Learning Mode Active**: This demo shows how different modes adapt content!\n"
98
+ "Try other modes like Socratic (questions), ELI5 (simple), Technical (deep), Analogy (metaphors), or Code (examples)."
99
+ )
100
 
101
  # Fallback to local model (only for developers with model weights)
102
  try:
 
109
 
110
  iface = gr.Interface(
111
  fn=ask_sync,
112
+ inputs=[
113
+ gr.Textbox(
114
+ label="Ask the tutor",
115
+ placeholder="Enter your question here (e.g., 'Explain Newton's laws')",
116
+ lines=3
117
+ ),
118
+ gr.Radio(
119
+ choices=["standard", "socratic", "eli5", "technical", "analogy", "code"],
120
+ label="📚 Learning Mode",
121
+ value="standard",
122
+ info="Choose how you want to learn: Socratic (questions), ELI5 (simple), Technical (deep), Analogy (metaphors), Code (examples)"
123
+ ),
124
+ gr.Slider(
125
+ minimum=1,
126
+ maximum=5,
127
+ step=1,
128
+ value=3,
129
+ label="🎯 Difficulty Level",
130
+ info="1 = Beginner, 5 = Expert"
131
+ ),
132
+ gr.Radio(
133
+ choices=["friendly", "strict", "enthusiastic", "professional", "playful"],
134
+ label="🎭 Tutor Persona",
135
+ value="friendly",
136
+ info="Pick your tutor's personality style"
137
+ )
138
+ ],
139
+ outputs=gr.Textbox(label="Tutor response", lines=12),
140
  title="🧠 Eidolon Cognitive Tutor",
141
+ description="""
142
+ **Interactive AI Tutor with Multiple Learning Modes**
143
+
144
+ Choose your learning style, adjust difficulty, and pick your tutor's personality!
145
+ Running in demo mode by default (set `DEMO_MODE=1` or configure `INFERENCE_API_URL` for real inference).
146
+ """,
147
  examples=[
148
+ ["Explain Newton's laws in simple terms", "eli5", 2, "friendly"],
149
+ ["How do I implement a binary search in Python?", "code", 3, "professional"],
150
+ ["Compare supervised vs unsupervised learning", "technical", 4, "enthusiastic"],
151
+ ["What is the difference between HTTP and HTTPS?", "analogy", 2, "playful"]
152
  ],
153
  theme="soft"
154
  )
public/index.html CHANGED
@@ -3,198 +3,670 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Eidolon Cognitive Tutor</title>
 
 
7
  <style>
8
- * { box-sizing: border-box; }
9
- body { font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background:#f7f8fb; color:#0f1724; display:flex; align-items:center; justify-content:center; min-height:100vh; margin:0; padding:20px }
10
- .card { background:#fff; padding:32px; border-radius:16px; box-shadow:0 6px 40px rgba(20,20,40,0.1); width:820px; max-width:100% }
11
- h1 { margin:0 0 8px 0; font-size:24px; font-weight:700 }
12
- p.lead { margin:0 0 24px 0; color:#475569; line-height:1.5 }
13
- .examples { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px }
14
- .example-btn { background:#e0e7ff; color:#3730a3; border:none; padding:8px 14px; border-radius:8px; font-size:13px; cursor:pointer; transition:all 0.2s }
15
- .example-btn:hover { background:#c7d2fe; transform:translateY(-1px) }
16
- textarea { width:100%; height:140px; padding:14px; border-radius:10px; border:1px solid #e6e9ef; resize:vertical; font-size:15px; font-family:inherit; transition:border 0.2s }
17
- textarea:focus { outline:none; border-color:#0b84ff }
18
- .controls { display:flex; gap:10px; margin-top:14px; align-items:center }
19
- button.primary { background:#0b84ff; color:white; border:none; padding:12px 20px; border-radius:10px; font-weight:600; cursor:pointer; font-size:15px; transition:all 0.2s }
20
- button.primary:hover { background:#0070e0; transform:translateY(-1px); box-shadow:0 4px 12px rgba(11,132,255,0.3) }
21
- button.primary:disabled { background:#cbd5e1; cursor:not-allowed; transform:none }
22
- button.secondary { background:#f1f5f9; color:#334155; border:none; padding:10px 16px; border-radius:8px; cursor:pointer; font-size:14px; transition:all 0.2s }
23
- button.secondary:hover { background:#e2e8f0 }
24
- .out { margin-top:20px; padding:16px; border-radius:10px; background:#f8fafc; min-height:100px; white-space:pre-wrap; border:1px solid #e2e8f0; position:relative }
25
- .out.loading { background:#fef3c7; border-color:#fde047 }
26
- .out.error { background:#fee; border-color:#fca5a5 }
27
- .spinner { display:inline-block; width:16px; height:16px; border:2px solid #cbd5e1; border-top-color:#0b84ff; border-radius:50%; animation:spin 0.7s linear infinite; margin-right:8px }
28
- @keyframes spin { to { transform: rotate(360deg) } }
29
- .copy-btn { position:absolute; top:12px; right:12px; background:#fff; border:1px solid #e2e8f0; padding:6px 12px; border-radius:6px; font-size:12px; cursor:pointer; transition:all 0.2s }
30
- .copy-btn:hover { background:#f1f5f9; border-color:#cbd5e1 }
31
- .history { margin-top:24px; padding-top:24px; border-top:1px solid #e2e8f0 }
32
- .history h3 { margin:0 0 12px 0; font-size:16px; color:#64748b }
33
- .history-item { background:#f8fafc; padding:10px; border-radius:8px; margin-bottom:8px; font-size:13px; border-left:3px solid #0b84ff }
34
- .meta { font-size:13px; color:#64748b; margin-top:8px }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  </style>
36
  </head>
37
  <body>
38
- <div class="card">
39
- <h1>🧠 Eidolon Cognitive Tutor</h1>
40
- <p class="lead">Interactive tutor demo powered by adaptive responses. Try the examples below or ask your own question.</p>
41
-
42
- <div class="examples">
43
- <button class="example-btn" data-prompt="Explain Newton's laws in simple terms">📐 Newton's Laws</button>
44
- <button class="example-btn" data-prompt="How do I implement a binary search in Python?">💻 Binary Search</button>
45
- <button class="example-btn" data-prompt="Compare supervised vs unsupervised learning">🤖 ML Comparison</button>
46
- <button class="example-btn" data-prompt="What is the difference between HTTP and HTTPS?">🔒 HTTP vs HTTPS</button>
47
  </div>
48
 
49
- <textarea id="prompt" placeholder="Type your question here..."></textarea>
50
-
51
- <div class="controls">
52
- <button id="ask" class="primary">Ask Tutor</button>
53
- <button id="clear" class="secondary">Clear</button>
54
- <button id="history-btn" class="secondary">View History</button>
 
 
 
 
 
 
 
55
  </div>
56
 
57
- <div class="out" id="out">Awaiting your question...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- <div class="history" id="history" style="display:none">
60
- <h3>Recent Conversations</h3>
61
- <div id="history-list"></div>
62
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- <div class="meta">
65
- <span id="status">Demo mode active</span> |
66
- <a href="https://github.com/Zwin-ux/Eidolon-Cognitive-Tutor" target="_blank" style="color:#0b84ff; text-decoration:none">View on GitHub</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  </div>
68
  </div>
69
 
 
 
 
 
 
 
70
  <script>
71
- const btn = document.getElementById('ask');
72
- const clearBtn = document.getElementById('clear');
73
- const historyBtn = document.getElementById('history-btn');
74
- const out = document.getElementById('out');
 
 
 
 
 
 
75
  const promptEl = document.getElementById('prompt');
76
- const statusEl = document.getElementById('status');
77
- const historyEl = document.getElementById('history');
78
- const historyList = document.getElementById('history-list');
 
 
 
 
 
 
79
 
80
- let sessionId = localStorage.getItem('session_id') || '';
81
- let conversationHistory = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- // Example button handlers
84
- document.querySelectorAll('.example-btn').forEach(btn => {
85
  btn.addEventListener('click', () => {
86
- promptEl.value = btn.dataset.prompt;
87
- promptEl.focus();
 
88
  });
89
  });
90
 
91
- // Copy button
92
- function addCopyButton() {
93
- if (document.querySelector('.copy-btn')) return;
94
- const copyBtn = document.createElement('button');
95
- copyBtn.className = 'copy-btn';
96
- copyBtn.textContent = 'Copy';
97
- copyBtn.onclick = () => {
98
- navigator.clipboard.writeText(out.textContent);
99
- copyBtn.textContent = 'Copied!';
100
- setTimeout(() => copyBtn.textContent = 'Copy', 2000);
101
- };
102
- out.appendChild(copyBtn);
103
- }
104
-
105
- // Clear functionality
106
- clearBtn.addEventListener('click', () => {
107
- promptEl.value = '';
108
- out.textContent = 'Awaiting your question...';
109
- out.className = 'out';
110
- const copyBtn = out.querySelector('.copy-btn');
111
- if (copyBtn) copyBtn.remove();
112
  });
113
 
114
- // History toggle
115
- historyBtn.addEventListener('click', () => {
116
- if (historyEl.style.display === 'none') {
117
- loadHistory();
118
- historyEl.style.display = 'block';
119
- historyBtn.textContent = 'Hide History';
120
- } else {
121
- historyEl.style.display = 'none';
122
- historyBtn.textContent = 'View History';
123
- }
124
  });
125
 
126
- // Load history from server
127
- async function loadHistory() {
128
- if (!sessionId) return;
129
- try {
130
- const resp = await fetch(`/api/history/${sessionId}`);
131
- const data = await resp.json();
132
- if (data.history && data.history.length > 0) {
133
- historyList.innerHTML = data.history.map(item =>
134
- `<div class="history-item"><strong>Q:</strong> ${item.prompt.substring(0, 60)}...<br><strong>A:</strong> ${item.response.substring(0, 80)}...</div>`
135
- ).join('');
136
- } else {
137
- historyList.innerHTML = '<div style="color:#94a3b8">No conversation history yet.</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }
139
- } catch (e) {
140
- historyList.innerHTML = '<div style="color:#94a3b8">Could not load history.</div>';
141
- }
142
- }
143
 
144
- // Main ask functionality
145
- btn.addEventListener('click', async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  const prompt = promptEl.value.trim();
147
- if (!prompt) {
148
- out.textContent = 'Please enter a question.';
149
- out.className = 'out error';
150
  return;
151
  }
152
-
153
- out.innerHTML = '<span class="spinner"></span>Thinking...';
154
- out.className = 'out loading';
155
- btn.disabled = true;
156
-
 
 
 
157
  try {
158
  const resp = await fetch('/api/ask', {
159
  method: 'POST',
160
  headers: { 'Content-Type': 'application/json' },
161
- body: JSON.stringify({
162
  prompt,
 
 
 
163
  session_id: sessionId || undefined
164
  })
165
  });
166
-
167
  const data = await resp.json();
168
-
169
  if (data.session_id && !sessionId) {
170
  sessionId = data.session_id;
171
  localStorage.setItem('session_id', sessionId);
172
  }
173
-
 
 
174
  if (data.error) {
175
- out.textContent = '❌ Error: ' + (data.detail || data.error);
176
- out.className = 'out error';
177
  } else {
178
- out.textContent = data.result || JSON.stringify(data, null, 2);
179
- out.className = 'out';
180
- addCopyButton();
181
- statusEl.textContent = `Response from: ${data.source}`;
182
- conversationHistory.push({ prompt, response: data.result });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
  } catch (e) {
185
- out.textContent = '❌ Request failed: ' + e.message;
186
- out.className = 'out error';
187
  } finally {
188
- btn.disabled = false;
 
 
189
  }
190
- });
191
 
192
- // Enter key to submit
193
  promptEl.addEventListener('keydown', (e) => {
194
  if (e.key === 'Enter' && e.ctrlKey) {
195
- btn.click();
196
  }
197
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  </script>
199
  </body>
200
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Eidolon Cognitive Tutor - Learn Anything, Your Way</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  <style>
10
+ * { box-sizing: border-box; margin: 0; padding: 0; }
11
+ body {
12
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
13
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
14
+ min-height: 100vh;
15
+ padding: 20px;
16
+ color: #1a202c;
17
+ }
18
+
19
+ .container { max-width: 1200px; margin: 0 auto; }
20
+
21
+ .header { text-align: center; margin-bottom: 40px; color: white; }
22
+ .header h1 { font-size: 42px; font-weight: 700; margin-bottom: 12px; text-shadow: 0 2px 10px rgba(0,0,0,0.2); }
23
+ .header .tagline { font-size: 18px; opacity: 0.95; }
24
+
25
+ .stats-bar {
26
+ display: flex;
27
+ gap: 20px;
28
+ justify-content: center;
29
+ margin-bottom: 30px;
30
+ flex-wrap: wrap;
31
+ }
32
+ .stat-card {
33
+ background: rgba(255,255,255,0.2);
34
+ backdrop-filter: blur(10px);
35
+ padding: 15px 25px;
36
+ border-radius: 12px;
37
+ color: white;
38
+ border: 1px solid rgba(255,255,255,0.3);
39
+ }
40
+ .stat-value { font-size: 24px; font-weight: 700; }
41
+ .stat-label { font-size: 13px; opacity: 0.9; margin-top: 4px; }
42
+
43
+ .main-card {
44
+ background: white;
45
+ border-radius: 20px;
46
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
47
+ overflow: hidden;
48
+ }
49
+
50
+ .mode-selector {
51
+ display: flex;
52
+ gap: 10px;
53
+ padding: 20px;
54
+ background: linear-gradient(to right, #f7fafc, #edf2f7);
55
+ border-bottom: 2px solid #e2e8f0;
56
+ overflow-x: auto;
57
+ }
58
+ .mode-btn {
59
+ padding: 12px 20px;
60
+ border: 2px solid #cbd5e1;
61
+ background: white;
62
+ border-radius: 10px;
63
+ cursor: pointer;
64
+ transition: all 0.2s;
65
+ font-size: 14px;
66
+ font-weight: 600;
67
+ white-space: nowrap;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 6px;
71
+ }
72
+ .mode-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
73
+ .mode-btn.active {
74
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
75
+ color: white;
76
+ border-color: #667eea;
77
+ }
78
+
79
+ .controls-panel {
80
+ padding: 20px;
81
+ background: #f8fafc;
82
+ border-bottom: 1px solid #e2e8f0;
83
+ }
84
+ .control-group {
85
+ display: grid;
86
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
87
+ gap: 20px;
88
+ }
89
+ .control-item label {
90
+ display: block;
91
+ font-size: 13px;
92
+ font-weight: 600;
93
+ color: #475569;
94
+ margin-bottom: 8px;
95
+ }
96
+ .persona-selector {
97
+ display: flex;
98
+ gap: 8px;
99
+ flex-wrap: wrap;
100
+ }
101
+ .persona-btn {
102
+ padding: 8px 16px;
103
+ border: 2px solid #e2e8f0;
104
+ background: white;
105
+ border-radius: 8px;
106
+ cursor: pointer;
107
+ font-size: 13px;
108
+ transition: all 0.2s;
109
+ }
110
+ .persona-btn:hover { border-color: #cbd5e1; }
111
+ .persona-btn.active { background: #667eea; color: white; border-color: #667eea; }
112
+
113
+ .slider-container { position: relative; }
114
+ .slider {
115
+ width: 100%;
116
+ height: 8px;
117
+ border-radius: 5px;
118
+ background: linear-gradient(to right, #48bb78, #f6ad55, #f56565);
119
+ outline: none;
120
+ -webkit-appearance: none;
121
+ }
122
+ .slider::-webkit-slider-thumb {
123
+ -webkit-appearance: none;
124
+ appearance: none;
125
+ width: 24px;
126
+ height: 24px;
127
+ border-radius: 50%;
128
+ background: white;
129
+ cursor: pointer;
130
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
131
+ border: 3px solid #667eea;
132
+ }
133
+ .difficulty-labels {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ font-size: 11px;
137
+ color: #64748b;
138
+ margin-top: 6px;
139
+ }
140
+
141
+ .chat-area {
142
+ padding: 30px;
143
+ min-height: 400px;
144
+ }
145
+
146
+ .prompt-enhance {
147
+ background: #fef3c7;
148
+ padding: 12px;
149
+ border-radius: 8px;
150
+ margin-bottom: 16px;
151
+ font-size: 13px;
152
+ border-left: 4px solid #f59e0b;
153
+ display: none;
154
+ }
155
+ .prompt-enhance.show { display: block; }
156
+
157
+ textarea {
158
+ width: 100%;
159
+ min-height: 120px;
160
+ padding: 16px;
161
+ border: 2px solid #e2e8f0;
162
+ border-radius: 12px;
163
+ font-size: 15px;
164
+ font-family: inherit;
165
+ resize: vertical;
166
+ transition: border 0.2s;
167
+ }
168
+ textarea:focus { outline: none; border-color: #667eea; }
169
+
170
+ .action-bar {
171
+ display: flex;
172
+ gap: 12px;
173
+ margin-top: 16px;
174
+ align-items: center;
175
+ flex-wrap: wrap;
176
+ }
177
+ .btn-primary {
178
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
179
+ color: white;
180
+ border: none;
181
+ padding: 14px 28px;
182
+ border-radius: 10px;
183
+ font-weight: 600;
184
+ cursor: pointer;
185
+ font-size: 15px;
186
+ transition: all 0.2s;
187
+ box-shadow: 0 4px 14px rgba(102, 126, 234, 0.4);
188
+ }
189
+ .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); }
190
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
191
+
192
+ .btn-secondary {
193
+ background: #f1f5f9;
194
+ color: #475569;
195
+ border: 2px solid #e2e8f0;
196
+ padding: 12px 20px;
197
+ border-radius: 10px;
198
+ font-weight: 600;
199
+ cursor: pointer;
200
+ font-size: 14px;
201
+ transition: all 0.2s;
202
+ }
203
+ .btn-secondary:hover { background: #e2e8f0; }
204
+
205
+ .response-area {
206
+ margin-top: 24px;
207
+ padding: 20px;
208
+ background: #f8fafc;
209
+ border-radius: 12px;
210
+ min-height: 150px;
211
+ border: 2px solid #e2e8f0;
212
+ position: relative;
213
+ }
214
+ .response-area.loading { background: linear-gradient(90deg, #f8fafc 0%, #edf2f7 50%, #f8fafc 100%); background-size: 200% 100%; animation: shimmer 1.5s infinite; }
215
+ @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
216
+
217
+ .typing-indicator { display: none; align-items: center; gap: 6px; color: #64748b; }
218
+ .typing-indicator.show { display: flex; }
219
+ .typing-dot { width: 8px; height: 8px; background: #94a3b8; border-radius: 50%; animation: bounce 1.4s infinite; }
220
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
221
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
222
+ @keyframes bounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-10px); } }
223
+
224
+ .response-text { line-height: 1.7; white-space: pre-wrap; }
225
+ .response-text code { background: #e2e8f0; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
226
+
227
+ .response-actions {
228
+ position: absolute;
229
+ top: 16px;
230
+ right: 16px;
231
+ display: flex;
232
+ gap: 8px;
233
+ }
234
+ .icon-btn {
235
+ background: white;
236
+ border: 1px solid #e2e8f0;
237
+ padding: 8px 12px;
238
+ border-radius: 6px;
239
+ cursor: pointer;
240
+ font-size: 12px;
241
+ transition: all 0.2s;
242
+ }
243
+ .icon-btn:hover { background: #f8fafc; border-color: #cbd5e1; }
244
+
245
+ .examples-grid {
246
+ display: grid;
247
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
248
+ gap: 12px;
249
+ margin-top: 20px;
250
+ }
251
+ .example-card {
252
+ background: white;
253
+ border: 2px solid #e2e8f0;
254
+ padding: 14px;
255
+ border-radius: 10px;
256
+ cursor: pointer;
257
+ transition: all 0.2s;
258
+ font-size: 14px;
259
+ }
260
+ .example-card:hover {
261
+ border-color: #667eea;
262
+ transform: translateY(-2px);
263
+ box-shadow: 0 4px 12px rgba(102,126,234,0.2);
264
+ }
265
+
266
+ .achievement-popup {
267
+ position: fixed;
268
+ top: 20px;
269
+ right: 20px;
270
+ background: white;
271
+ padding: 20px;
272
+ border-radius: 12px;
273
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
274
+ border: 2px solid #48bb78;
275
+ transform: translateX(400px);
276
+ transition: transform 0.4s;
277
+ z-index: 1000;
278
+ max-width: 300px;
279
+ }
280
+ .achievement-popup.show { transform: translateX(0); }
281
+ .achievement-icon { font-size: 48px; text-align: center; margin-bottom: 10px; }
282
+ .achievement-title { font-weight: 700; font-size: 18px; margin-bottom: 6px; }
283
+ .achievement-desc { font-size: 14px; color: #64748b; }
284
+
285
+ @media (max-width: 768px) {
286
+ .header h1 { font-size: 32px; }
287
+ .mode-selector { flex-wrap: nowrap; padding: 15px; }
288
+ .stat-card { padding: 10px 15px; }
289
+ }
290
  </style>
291
  </head>
292
  <body>
293
+ <div class="container">
294
+ <div class="header">
295
+ <h1>🧠 Eidolon Cognitive Tutor</h1>
296
+ <div class="tagline">Learn Anything, Your Way — Personalized, Interactive, Engaging</div>
 
 
 
 
 
297
  </div>
298
 
299
+ <div class="stats-bar">
300
+ <div class="stat-card">
301
+ <div class="stat-value" id="question-count">0</div>
302
+ <div class="stat-label">Questions Asked</div>
303
+ </div>
304
+ <div class="stat-card">
305
+ <div class="stat-value" id="streak-count">0🔥</div>
306
+ <div class="stat-label">Learning Streak</div>
307
+ </div>
308
+ <div class="stat-card">
309
+ <div class="stat-value" id="achievement-count">0🏆</div>
310
+ <div class="stat-label">Achievements</div>
311
+ </div>
312
  </div>
313
 
314
+ <div class="main-card">
315
+ <div class="mode-selector" id="mode-selector">
316
+ <button class="mode-btn active" data-mode="standard">
317
+ <span>📚</span> Standard
318
+ </button>
319
+ <button class="mode-btn" data-mode="socratic">
320
+ <span>🤔</span> Socratic
321
+ </button>
322
+ <button class="mode-btn" data-mode="eli5">
323
+ <span>👶</span> ELI5
324
+ </button>
325
+ <button class="mode-btn" data-mode="technical">
326
+ <span>🔬</span> Technical
327
+ </button>
328
+ <button class="mode-btn" data-mode="analogy">
329
+ <span>🎭</span> Analogy
330
+ </button>
331
+ <button class="mode-btn" data-mode="code">
332
+ <span>💻</span> Code
333
+ </button>
334
+ </div>
335
 
336
+ <div class="controls-panel">
337
+ <div class="control-group">
338
+ <div class="control-item">
339
+ <label>Difficulty Level</label>
340
+ <div class="slider-container">
341
+ <input type="range" min="1" max="5" value="3" class="slider" id="difficulty-slider">
342
+ <div class="difficulty-labels">
343
+ <span>Beginner</span>
344
+ <span>Intermediate</span>
345
+ <span>Expert</span>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <div class="control-item">
351
+ <label>Tutor Persona</label>
352
+ <div class="persona-selector">
353
+ <button class="persona-btn active" data-persona="friendly">😊 Friendly</button>
354
+ <button class="persona-btn" data-persona="strict">📚 Strict</button>
355
+ <button class="persona-btn" data-persona="enthusiastic">🎉 Hyped</button>
356
+ <button class="persona-btn" data-persona="professional">🎓 Pro</button>
357
+ </div>
358
+ </div>
359
+ </div>
360
+ </div>
361
+
362
+ <div class="chat-area">
363
+ <div class="prompt-enhance" id="prompt-enhance">
364
+ 💡 <strong>Suggestion:</strong> <span id="enhance-text"></span>
365
+ </div>
366
+
367
+ <textarea
368
+ id="prompt"
369
+ placeholder="Ask anything... Try 'Explain quantum computing' or 'How do I build a REST API?'"
370
+ ></textarea>
371
+
372
+ <div class="action-bar">
373
+ <button class="btn-primary" id="ask-btn">
374
+ <span id="ask-text">Ask Tutor</span>
375
+ </button>
376
+ <button class="btn-secondary" id="enhance-btn">✨ Enhance Prompt</button>
377
+ <button class="btn-secondary" id="clear-btn">🗑️ Clear</button>
378
+ <button class="btn-secondary" id="surprise-btn">🎲 Surprise Me</button>
379
+ </div>
380
 
381
+ <div class="response-area" id="response-area">
382
+ <div class="typing-indicator" id="typing-indicator">
383
+ <div class="typing-dot"></div>
384
+ <div class="typing-dot"></div>
385
+ <div class="typing-dot"></div>
386
+ <span style="margin-left: 8px;">Thinking...</span>
387
+ </div>
388
+ <div class="response-text" id="response-text">Your tutor is ready! Pick a learning mode and ask away 🚀</div>
389
+ <div class="response-actions" style="display:none" id="response-actions">
390
+ <button class="icon-btn" id="copy-btn">📋 Copy</button>
391
+ <button class="icon-btn" id="share-btn">🔗 Share</button>
392
+ <button class="icon-btn" id="regenerate-btn">🔄 Regenerate</button>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="examples-grid">
397
+ <div class="example-card" data-example="Explain Newton's laws in simple terms">
398
+ 📐 Newton's Laws
399
+ </div>
400
+ <div class="example-card" data-example="How do I implement binary search?">
401
+ 💻 Binary Search
402
+ </div>
403
+ <div class="example-card" data-example="Compare supervised vs unsupervised learning">
404
+ 🤖 ML Comparison
405
+ </div>
406
+ <div class="example-card" data-example="What's the difference between HTTP and HTTPS?">
407
+ 🔒 HTTP vs HTTPS
408
+ </div>
409
+ </div>
410
+ </div>
411
  </div>
412
  </div>
413
 
414
+ <div class="achievement-popup" id="achievement-popup">
415
+ <div class="achievement-icon">🏆</div>
416
+ <div class="achievement-title" id="achievement-title">Achievement Unlocked!</div>
417
+ <div class="achievement-desc" id="achievement-desc">You've earned a new badge</div>
418
+ </div>
419
+
420
  <script>
421
+ // State
422
+ let selectedMode = 'standard';
423
+ let selectedPersona = 'friendly';
424
+ let difficulty = 3;
425
+ let sessionId = localStorage.getItem('session_id') || '';
426
+ let questionCount = parseInt(localStorage.getItem('question_count') || '0');
427
+ let achievements = JSON.parse(localStorage.getItem('achievements') || '[]');
428
+ let lastQuestionTime = Date.now();
429
+
430
+ // Elements
431
  const promptEl = document.getElementById('prompt');
432
+ const askBtn = document.getElementById('ask-btn');
433
+ const responseArea = document.getElementById('response-area');
434
+ const responseText = document.getElementById('response-text');
435
+ const typingIndicator = document.getElementById('typing-indicator');
436
+ const responseActions = document.getElementById('response-actions');
437
+
438
+ // Update stats
439
+ document.getElementById('question-count').textContent = questionCount;
440
+ document.getElementById('achievement-count').textContent = achievements.length + '🏆';
441
 
442
+ // Calculate streak
443
+ function updateStreak() {
444
+ const now = Date.now();
445
+ const hoursSinceLastQuestion = (now - lastQuestionTime) / (1000 * 60 * 60);
446
+ let streak = parseInt(localStorage.getItem('streak') || '1');
447
+ if (hoursSinceLastQuestion < 24) {
448
+ streak++;
449
+ localStorage.setItem('streak', streak);
450
+ } else if (hoursSinceLastQuestion > 48) {
451
+ streak = 1;
452
+ localStorage.setItem('streak', '1');
453
+ }
454
+ document.getElementById('streak-count').textContent = streak + '🔥';
455
+ }
456
+ updateStreak();
457
 
458
+ // Mode selection
459
+ document.querySelectorAll('.mode-btn').forEach(btn => {
460
  btn.addEventListener('click', () => {
461
+ document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
462
+ btn.classList.add('active');
463
+ selectedMode = btn.dataset.mode;
464
  });
465
  });
466
 
467
+ // Persona selection
468
+ document.querySelectorAll('.persona-btn').forEach(btn => {
469
+ btn.addEventListener('click', () => {
470
+ document.querySelectorAll('.persona-btn').forEach(b => b.classList.remove('active'));
471
+ btn.classList.add('active');
472
+ selectedPersona = btn.dataset.persona;
473
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  });
475
 
476
+ // Difficulty slider
477
+ document.getElementById('difficulty-slider').addEventListener('input', (e) => {
478
+ difficulty = parseInt(e.target.value);
 
 
 
 
 
 
 
479
  });
480
 
481
+ // Example cards
482
+ document.querySelectorAll('.example-card').forEach(card => {
483
+ card.addEventListener('click', () => {
484
+ promptEl.value = card.dataset.example;
485
+ promptEl.focus();
486
+ });
487
+ });
488
+
489
+ // Enhance prompt
490
+ document.getElementById('enhance-btn').addEventListener('click', () => {
491
+ const prompt = promptEl.value.trim();
492
+ if (!prompt) return;
493
+
494
+ const enhancements = [
495
+ `Can you explain ${prompt} with examples and use cases?`,
496
+ `Please provide a detailed explanation of ${prompt}, including practical applications.`,
497
+ `I'd like to understand ${prompt} - can you break it down step by step?`,
498
+ `What are the key concepts behind ${prompt} and how do they relate to each other?`
499
+ ];
500
+ const enhanced = enhancements[Math.floor(Math.random() * enhancements.length)];
501
+
502
+ document.getElementById('enhance-text').textContent = enhanced;
503
+ document.getElementById('prompt-enhance').classList.add('show');
504
+
505
+ setTimeout(() => {
506
+ if (confirm('Apply this enhanced prompt?')) {
507
+ promptEl.value = enhanced;
508
  }
509
+ document.getElementById('prompt-enhance').classList.remove('show');
510
+ }, 3000);
511
+ });
 
512
 
513
+ // Surprise me
514
+ document.getElementById('surprise-btn').addEventListener('click', () => {
515
+ const surprises = [
516
+ "Explain the concept of emergence in complex systems",
517
+ "How does the human brain process and store memories?",
518
+ "What would happen if we could travel faster than light?",
519
+ "Explain blockchain technology using an everyday analogy",
520
+ "How do neural networks learn from data?",
521
+ "What is the halting problem and why is it unsolvable?"
522
+ ];
523
+ promptEl.value = surprises[Math.floor(Math.random() * surprises.length)];
524
+ });
525
+
526
+ // Clear
527
+ document.getElementById('clear-btn').addEventListener('click', () => {
528
+ promptEl.value = '';
529
+ responseText.textContent = 'Your tutor is ready! Pick a learning mode and ask away 🚀';
530
+ responseActions.style.display = 'none';
531
+ });
532
+
533
+ // Copy
534
+ document.getElementById('copy-btn').addEventListener('click', () => {
535
+ navigator.clipboard.writeText(responseText.textContent);
536
+ document.getElementById('copy-btn').textContent = '✅ Copied!';
537
+ setTimeout(() => {
538
+ document.getElementById('copy-btn').textContent = '📋 Copy';
539
+ }, 2000);
540
+ });
541
+
542
+ // Main ask function with typing animation
543
+ async function askQuestion() {
544
  const prompt = promptEl.value.trim();
545
+ if (!prompt) {
546
+ alert('Please enter a question!');
 
547
  return;
548
  }
549
+
550
+ askBtn.disabled = true;
551
+ document.getElementById('ask-text').textContent = 'Thinking...';
552
+ responseArea.classList.add('loading');
553
+ typingIndicator.classList.add('show');
554
+ responseText.textContent = '';
555
+ responseActions.style.display = 'none';
556
+
557
  try {
558
  const resp = await fetch('/api/ask', {
559
  method: 'POST',
560
  headers: { 'Content-Type': 'application/json' },
561
+ body: JSON.stringify({
562
  prompt,
563
+ mode: selectedMode,
564
+ difficulty,
565
+ persona: selectedPersona,
566
  session_id: sessionId || undefined
567
  })
568
  });
569
+
570
  const data = await resp.json();
571
+
572
  if (data.session_id && !sessionId) {
573
  sessionId = data.session_id;
574
  localStorage.setItem('session_id', sessionId);
575
  }
576
+
577
+ typingIndicator.classList.remove('show');
578
+
579
  if (data.error) {
580
+ responseText.textContent = '❌ ' + data.error;
 
581
  } else {
582
+ // Typing animation
583
+ const fullText = data.result;
584
+ let index = 0;
585
+ responseText.textContent = '';
586
+
587
+ const typeInterval = setInterval(() => {
588
+ if (index < fullText.length) {
589
+ responseText.textContent += fullText[index];
590
+ index++;
591
+ } else {
592
+ clearInterval(typeInterval);
593
+ responseActions.style.display = 'flex';
594
+ }
595
+ }, 15);
596
+
597
+ // Update stats
598
+ questionCount++;
599
+ localStorage.setItem('question_count', questionCount);
600
+ document.getElementById('question-count').textContent = questionCount;
601
+ lastQuestionTime = Date.now();
602
+ updateStreak();
603
+
604
+ // Check achievements
605
+ checkAchievements();
606
  }
607
  } catch (e) {
608
+ typingIndicator.classList.remove('show');
609
+ responseText.textContent = ' Request failed: ' + e.message;
610
  } finally {
611
+ askBtn.disabled = false;
612
+ document.getElementById('ask-text').textContent = 'Ask Tutor';
613
+ responseArea.classList.remove('loading');
614
  }
615
+ }
616
 
617
+ askBtn.addEventListener('click', askQuestion);
618
  promptEl.addEventListener('keydown', (e) => {
619
  if (e.key === 'Enter' && e.ctrlKey) {
620
+ askQuestion();
621
  }
622
  });
623
+
624
+ // Achievements system
625
+ function checkAchievements() {
626
+ const newAchievements = [];
627
+
628
+ if (questionCount === 1 && !achievements.includes('first-question')) {
629
+ newAchievements.push({ id: 'first-question', title: 'Getting Started!', desc: 'Asked your first question', icon: '🌟' });
630
+ }
631
+ if (questionCount === 10 && !achievements.includes('curious-learner')) {
632
+ newAchievements.push({ id: 'curious-learner', title: 'Curious Learner', desc: '10 questions asked!', icon: '🎓' });
633
+ }
634
+ if (questionCount === 50 && !achievements.includes('knowledge-seeker')) {
635
+ newAchievements.push({ id: 'knowledge-seeker', title: 'Knowledge Seeker', desc: '50 questions - you\'re on fire!', icon: '🔥' });
636
+ }
637
+
638
+ // Mode explorer
639
+ const modes = JSON.parse(localStorage.getItem('modes_used') || '[]');
640
+ if (!modes.includes(selectedMode)) {
641
+ modes.push(selectedMode);
642
+ localStorage.setItem('modes_used', JSON.stringify(modes));
643
+ if (modes.length === 6 && !achievements.includes('mode-master')) {
644
+ newAchievements.push({ id: 'mode-master', title: 'Mode Master', desc: 'Tried all learning modes!', icon: '🎨' });
645
+ }
646
+ }
647
+
648
+ newAchievements.forEach(achievement => {
649
+ if (!achievements.includes(achievement.id)) {
650
+ achievements.push(achievement.id);
651
+ showAchievement(achievement);
652
+ }
653
+ });
654
+
655
+ localStorage.setItem('achievements', JSON.stringify(achievements));
656
+ document.getElementById('achievement-count').textContent = achievements.length + '🏆';
657
+ }
658
+
659
+ function showAchievement(achievement) {
660
+ const popup = document.getElementById('achievement-popup');
661
+ document.getElementById('achievement-title').textContent = achievement.title;
662
+ document.getElementById('achievement-desc').textContent = achievement.desc;
663
+ document.querySelector('.achievement-icon').textContent = achievement.icon;
664
+
665
+ popup.classList.add('show');
666
+ setTimeout(() => {
667
+ popup.classList.remove('show');
668
+ }, 4000);
669
+ }
670
  </script>
671
  </body>
672
  </html>