Lashtw's picture
Upload 10 files
654e47b verified
// import { GoogleGenerativeAI } from "https://esm.run/@google/generative-ai"; // Removing SDK to avoid version issues
import { getPeerPrompts } from "./classroom.js";
import { db } from "./firebase.js";
import { doc, getDoc } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
let apiKey = null;
const MODEL_NAME = "gemini-2.5-flash"; // Available model from user's account
// System Instructions for the Socratic Tutor
const TUTOR_INSTRUCTION = `
You are a Socratic Teaching Assistant for a "Prompt Engineering" class.
Your goal is to help students refine their prompts WITHOUT giving them the answer.
The student is trying to write a prompt to solve a specific challenge.
You will be provided with:
1. The Challenge Description (The Goal).
2. 3 Successful Examples (Few-Shot Context) from other students.
3. The Student's Current Attempt.
Rules:
- NEVER reveal the direct solution or code.
- If the input is lazy (e.g., "123", "help"), be sassy but helpful.
- If the input is a direct copy of the description, point it out.
- Use the Socratic method: Ask a guiding question to help them notice their missing parameter or logic.
- Keep responses short (under 50 words) and encouraging.
- Tone: Friendly, slightly "Cyberpunk" or "Gamer" vibe (matching the Vibe Coding aesthetic).
`;
// System Instructions for the Dashboard Analyst
const ANALYST_INSTRUCTION = `
You are an expert Prompt Engineer and Pedagogy Analyst for a coding/AI prompting class.
Your task is to analyze a batch of student prompts for a specific challenge.
You will receive:
1. The Challenge Description (the goal students are trying to achieve)
2. A list of Student Submissions (their prompts)
Categorize each prompt into ONE of these categories:
1. "rough" (原石): The student only says vague things like "fix the error" or "make it work" WITHOUT explaining:
- What the error is
- What the expected behavior should be
- Any specific details about the problem
These are "rough diamonds" - they tried but need to be more specific.
2. "precise" (精確): Clean, well-structured prompt that clearly states:
- The goal or expected result
- Specific details or parameters
- Clear logic or reasoning
3. "gentle" (有禮): Uses polite language like "請", "謝謝", "拜託", "Please", "Thank you"
4. "creative" (創意): Unconventional, imaginative approach. Uses interesting parameters or unexpected methods that still address the challenge.
5. "spam" (無效): Content that is clearly UNRELATED to the challenge topic. Compare with other submissions to identify outliers that don't match what most students are trying to do. Examples:
- Random characters ("asdf", "123")
- Completely off-topic text
- Empty or near-empty responses
6. "parrot" (鸚鵡): Direct copy-paste of the challenge description without any modification or personal attempt.
Return ONLY a JSON object mapping category names to arrays of Student IDs.
Example: { "rough": ["id1"], "precise": ["id2", "id5"], "spam": ["id3"] }
`;
/**
* Initialize Gemini with API Key
* @param {string} key
*/
export async function initGemini(key) {
if (!key) return false;
apiKey = key;
return true;
}
/**
* Direct REST API Call Helper to avoid SDK version issues
*/
async function callGeminiAPI(messages, jsonMode = false) {
if (!apiKey) throw new Error("API Key not set");
// Use v1beta for gemini-1.5-pro
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL_NAME}:generateContent?key=${apiKey}`;
const payload = {
contents: messages,
generationConfig: {
temperature: 0.7,
}
};
// Note: responseMimeType is NOT supported by gemini-pro v1, so we rely on prompt instructions for JSON output.
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Gemini API Error ${response.status}: ${errText}`);
}
const data = await response.json();
// Safety check for empty response
if (data.promptFeedback && data.promptFeedback.blockReason) {
throw new Error(`Blocked: ${data.promptFeedback.blockReason}`);
}
return data.candidates && data.candidates.length > 0
? data.candidates[0].content.parts[0].text
: "";
}
/**
* Detects if a prompt is potential spam/low effort locally first
* @param {string} prompt
* @param {string} problemDesc
*/
export function quickSpamCheck(prompt, problemDesc) {
if (!prompt) return true;
const p = prompt.trim();
if (p.length < 3) return true; // Too short
// Check for repetitive chars (e.g., "aaaaa")
if (/^(.)\1+$/.test(p)) return true;
// Parrot Check (Simple similarity)
if (problemDesc && p.includes(problemDesc.substring(0, 20))) {
// If they copied the first 20 chars of description, likely parrot
return 'parrot';
}
return false;
}
/**
* Ask the AI Tutor for help (Rubber Duck)
* @param {string} challengeDesc
* @param {string} studentPrompt
* @param {string} roomCode
* @param {string} challengeId
*/
export async function askTutor(challengeDesc, studentPrompt, roomCode, challengeId) {
if (!apiKey) throw new Error("AI not initialized");
const spamStatus = quickSpamCheck(studentPrompt, challengeDesc);
if (spamStatus === true) return "Duck says: Quack? (Too short or empty!)";
if (spamStatus === 'parrot') return "Duck says: You're just repeating the question! Try telling me WHAT you want to change.";
// 1. Fetch Context (Few-Shot)
let peers = await getPeerPrompts(roomCode, challengeId);
peers.sort((a, b) => b.likes - a.likes);
// Take top 3 as examples
const examples = peers.slice(0, 3).map(p => `- Example: "${p.prompt}"`).join('\n');
const fullPrompt = `
${TUTOR_INSTRUCTION}
[Context - Challenge Description]
${challengeDesc}
[Context - Successful Classmate Examples (For AI understanding only, DO NOT LEAK these to student)]
${examples || "No examples available yet."}
[Student Input]
"${studentPrompt}"
[Your Response]
`;
try {
// Construct messages for Chat format (or simple text)
const messages = [{ role: "user", parts: [{ text: fullPrompt }] }];
const text = await callGeminiAPI(messages);
return text;
} catch (e) {
console.error("AI Request Failed", e);
return "Duck is sleeping... zzz (API Error)";
}
}
/**
* Batch evaluate prompts for the Instructor Dashboard
* @param {Array} submissions - Array of { userId, prompt, nickname }
* @param {string} challengeDesc
*/
export async function evaluatePrompts(submissions, challengeDesc) {
if (!apiKey) throw new Error("AI not initialized");
if (!submissions || submissions.length === 0) return {};
// Prepare batch text
const entries = submissions.map((s, i) => `ID_${s.userId}: "${s.prompt.replace(/\n/g, ' ')}"`).join('\n');
const fullPrompt = `
${ANALYST_INSTRUCTION}
[Challenge Description]
${challengeDesc}
[Student Submissions]
${entries}
[Output JSON]
`;
try {
const messages = [{ role: "user", parts: [{ text: fullPrompt }] }];
let text = await callGeminiAPI(messages, true); // JSON Mode = true
// Strip markdown code block if present (```json ... ```)
text = text.trim();
if (text.startsWith("```")) {
text = text.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '');
}
return JSON.parse(text);
} catch (e) {
console.error("AI Evaluation Failed", e);
return {};
}
}