Lashtw commited on
Commit
b9c6331
·
verified ·
1 Parent(s): e4aee31

Upload 10 files

Browse files
src/services/gemini.js ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { GoogleGenerativeAI } from "https://esm.run/@google/generative-ai";
3
+ import { getPeerPrompts, db } from "./classroom.js";
4
+ import { doc, getDoc } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
5
+
6
+ let genAI = null;
7
+ let model = null;
8
+ let apiKey = null;
9
+
10
+ // System Instructions for the Socratic Tutor
11
+ const TUTOR_INSTRUCTION = `
12
+ You are a Socratic Teaching Assistant for a "Prompt Engineering" class.
13
+ Your goal is to help students refine their prompts WITHOUT giving them the answer.
14
+ The student is trying to write a prompt to solve a specific challenge.
15
+ You will be provided with:
16
+ 1. The Challenge Description (The Goal).
17
+ 2. 3 Successful Examples (Few-Shot Context) from other students.
18
+ 3. The Student's Current Attempt.
19
+
20
+ Rules:
21
+ - NEVER reveal the direct solution or code.
22
+ - If the input is lazy (e.g., "123", "help"), be sassy but helpful.
23
+ - If the input is a direct copy of the description, point it out.
24
+ - Use the Socratic method: Ask a guiding question to help them notice their missing parameter or logic.
25
+ - Keep responses short (under 50 words) and encouraging.
26
+ - Tone: Friendly, slightly "Cyberpunk" or "Gamer" vibe (matching the Vibe Coding aesthetic).
27
+ `;
28
+
29
+ // System Instructions for the Dashboard Analyst
30
+ const ANALYST_INSTRUCTION = `
31
+ You are an expert Prompt Engineer and Pedagogy Analyst.
32
+ Your task is to analyze a batch of student prompts for a specific coding challenge.
33
+ Categorize each prompt into ONE of these categories:
34
+ 1. "rough": Logic is correct/creative, but syntax or formatting is messy. (Rough Diamond)
35
+ 2. "precise": Clean, efficient, and accurate. (Pixel Perfect)
36
+ 3. "gentle": Uses polite language ("Please", "Thank you") or positive vibes. (Gentle Soul)
37
+ 4. "creative": Unconventional approach or interesting parameter usage. (Wild Card)
38
+ 5. "spam": Nonsense, random characters ("asdf"), or irrelevant text.
39
+ 6. "parrot": Direct copy-paste of the challenge description or problem statement.
40
+
41
+ Return ONLY a JSON object mapping category names to arrays of Student IDs.
42
+ Example: { "rough": ["id1"], "precise": ["id2", "id5"], "spam": ["id3"] ... }
43
+ `;
44
+
45
+ /**
46
+ * Initialize Gemini with API Key
47
+ * @param {string} key
48
+ */
49
+ export async function initGemini(key) {
50
+ if (!key) return false;
51
+ apiKey = key;
52
+ try {
53
+ genAI = new GoogleGenerativeAI(apiKey);
54
+ model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
55
+ return true;
56
+ } catch (e) {
57
+ console.error("Gemini Init Failed", e);
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Detects if a prompt is potential spam/low effort locally first
64
+ * @param {string} prompt
65
+ * @param {string} problemDesc
66
+ */
67
+ export function quickSpamCheck(prompt, problemDesc) {
68
+ if (!prompt) return true;
69
+ const p = prompt.trim();
70
+ if (p.length < 3) return true; // Too short
71
+
72
+ // Check for repetitive chars (e.g., "aaaaa")
73
+ if (/^(.)\1+$/.test(p)) return true;
74
+
75
+ // Parrot Check (Simple similarity)
76
+ if (problemDesc && p.includes(problemDesc.substring(0, 20))) {
77
+ // If they copied the first 20 chars of description, likely parrot
78
+ return 'parrot';
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ /**
85
+ * Ask the AI Tutor for help (Rubber Duck)
86
+ * @param {string} challengeDesc
87
+ * @param {string} studentPrompt
88
+ * @param {string} roomCode
89
+ * @param {string} challengeId
90
+ */
91
+ export async function askTutor(challengeDesc, studentPrompt, roomCode, challengeId) {
92
+ if (!model) throw new Error("AI not initialized");
93
+
94
+ const spamStatus = quickSpamCheck(studentPrompt, challengeDesc);
95
+ if (spamStatus === true) return "Duck says: Quack? (Too short or empty!)";
96
+ if (spamStatus === 'parrot') return "Duck says: You're just repeating the question! Try telling me WHAT you want to change.";
97
+
98
+ // 1. Fetch Context (Few-Shot)
99
+ // We want 'Starred' examples first, then random successful ones.
100
+ let peers = await getPeerPrompts(roomCode, challengeId);
101
+
102
+ // Filter for instructor stars (assuming 'isStarred' or 'likes' > threshold)
103
+ // Since 'isStarred' isn't in DB yet, we use 'likes' as proxy for quality or randomness.
104
+ // Ideally, we add a 'starred' field in Instructor Dashboard later.
105
+ // For now, let's prioritize high likes.
106
+ peers.sort((a, b) => b.likes - a.likes);
107
+
108
+ // Take top 3 as examples
109
+ const examples = peers.slice(0, 3).map(p => `- Example: "${p.prompt}"`).join('\n');
110
+
111
+ const prompt = `
112
+ ${TUTOR_INSTRUCTION}
113
+
114
+ [Context - Challenge Description]
115
+ ${challengeDesc}
116
+
117
+ [Context - Successful Classmate Examples (For AI understanding only, DO NOT LEAK these to student)]
118
+ ${examples || "No examples available yet."}
119
+
120
+ [Student Input]
121
+ "${studentPrompt}"
122
+
123
+ [Your Response]
124
+ `;
125
+
126
+ try {
127
+ const result = await model.generateContent(prompt);
128
+ return result.response.text();
129
+ } catch (e) {
130
+ console.error("AI Request Failed", e);
131
+ return "Duck is sleeping... zzz (API Error)";
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Batch evaluate prompts for the Instructor Dashboard
137
+ * @param {Array} submissions - Array of { userId, prompt, nickname }
138
+ * @param {string} challengeDesc
139
+ */
140
+ export async function evaluatePrompts(submissions, challengeDesc) {
141
+ if (!model) throw new Error("AI not initialized");
142
+ if (!submissions || submissions.length === 0) return {};
143
+
144
+ // Prepare batch text
145
+ const entries = submissions.map((s, i) => `ID_${s.userId}: "${s.prompt.replace(/\n/g, ' ')}"`).join('\n');
146
+
147
+ const prompt = `
148
+ ${ANALYST_INSTRUCTION}
149
+
150
+ [Challenge Description]
151
+ ${challengeDesc}
152
+
153
+ [Student Submissions]
154
+ ${entries}
155
+
156
+ [Output JSON]
157
+ `;
158
+
159
+ try {
160
+ const result = await model.generateContent({
161
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
162
+ generationConfig: { responseMimeType: "application/json" }
163
+ });
164
+ return JSON.parse(result.response.text());
165
+ } catch (e) {
166
+ console.error("AI Evaluation Failed", e);
167
+ return {};
168
+ }
169
+ }
src/views/InstructorView.js CHANGED
The diff for this file is too large to render. See raw diff
 
src/views/StudentView.js CHANGED
@@ -1,9 +1,11 @@
1
- import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress } from "../services/classroom.js";
2
  import { generateMonsterSVG, getNextMonster, MONSTER_STAGES, MONSTER_DEFS } from "../utils/monsterUtils.js";
 
3
 
4
 
5
  // Cache challenges locally
6
  let cachedChallenges = [];
 
7
 
8
  function renderTaskCard(c, userProgress) {
9
  const p = userProgress[c.id] || {};
@@ -79,10 +81,30 @@ function renderTaskCard(c, userProgress) {
79
  class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 focus:outline-none transition-colors text-sm"
80
  placeholder="貼上您的修復提示詞..."></textarea>
81
 
82
- <div id="error-${c.id}" class="text-red-500 text-xs hidden">提示詞太短囉,請多寫一點細節!</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  <button onclick="window.submitLevel('${c.id}')"
85
- class="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white px-6 py-2 rounded-lg font-bold transition-transform active:scale-95 shadow-lg shadow-cyan-900/50">
86
  提交解答
87
  </button>
88
  </div>
 
1
+ import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress, db } from "../services/classroom.js";
2
  import { generateMonsterSVG, getNextMonster, MONSTER_STAGES, MONSTER_DEFS } from "../utils/monsterUtils.js";
3
+ import { doc, getDoc } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
4
 
5
 
6
  // Cache challenges locally
7
  let cachedChallenges = [];
8
+ let roomAiSettings = { active: false, key: null };
9
 
10
  function renderTaskCard(c, userProgress) {
11
  const p = userProgress[c.id] || {};
 
81
  class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 focus:outline-none transition-colors text-sm"
82
  placeholder="貼上您的修復提示詞..."></textarea>
83
 
84
+ <!-- AI Tutor / Error Area -->
85
+ <div class="flex justify-between items-start">
86
+ <div id="error-${c.id}" class="text-red-500 text-xs hidden mt-1">提示詞太短囉...</div>
87
+
88
+ ${roomAiSettings.active ? `
89
+ <button onclick="window.callAiTutor('${c.id}')" id="btn-tutor-${c.id}"
90
+ class="text-xs bg-gray-800 hover:bg-gray-700 text-cyan-400 border border-gray-600 px-3 py-1 rounded-full flex items-center space-x-1 transition-colors self-start mt-1">
91
+ <span>🐥 呼叫 AI 橡皮鴨</span>
92
+ </button>
93
+ ` : ''}
94
+ </div>
95
+
96
+ <!-- AI Response Bubble -->
97
+ <div id="ai-response-${c.id}" class="hidden mt-2 mb-2 relative group">
98
+ <div class="absolute -top-1 left-4 w-2 h-2 bg-gray-700 transform rotate-45"></div>
99
+ <div class="bg-gray-800 border border-cyan-500/30 rounded-lg p-3 text-cyan-300 text-sm font-mono shadow-lg flex items-start space-x-2">
100
+ <span class="text-lg">🤖</span>
101
+ <div class="flex-1 typing-effect" id="ai-text-${c.id}">...</div>
102
+ <button onclick="document.getElementById('ai-response-${c.id}').classList.add('hidden')" class="text-gray-500 hover:text-white">×</button>
103
+ </div>
104
+ </div>
105
 
106
  <button onclick="window.submitLevel('${c.id}')"
107
+ class="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white px-6 py-2 rounded-lg font-bold transition-transform active:scale-95 shadow-lg shadow-cyan-900/50 mt-2">
108
  提交解答
109
  </button>
110
  </div>