- Add question type classification (mathematical, factual_simple, exploratory) - Implement conditional UI: no text fields for math questions - Add math-specific step-by-step guidance without revealing answers - Enhanced AI prompts for subject-specific teaching approaches - Remove unnecessary action buttons (Research Ideas, Try Experiments, Discuss with Others) - Improve user experience with focused, educational interactions
418 lines
16 KiB
JavaScript
Executable File
418 lines
16 KiB
JavaScript
Executable File
const express = require('express');
|
||
const cors = require('cors');
|
||
const path = require('path');
|
||
const fetch = require('node-fetch');
|
||
const OpenAI = require('openai');
|
||
|
||
// Load environment variables from .env file
|
||
require('dotenv').config();
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3002;
|
||
|
||
// OpenAI Configuration (Primary AI service)
|
||
const openai = new OpenAI({
|
||
apiKey: process.env.OPENAI_API_KEY
|
||
});
|
||
|
||
// Hugging Face API configuration (Backup)
|
||
const HF_MODELS = [
|
||
'microsoft/DialoGPT-small', // Smaller, more reliable model
|
||
'google/flan-t5-small', // Good for instructions
|
||
'facebook/blenderbot-400M-distill', // Conversational AI
|
||
'microsoft/DialoGPT-medium' // Original choice
|
||
];
|
||
|
||
const HF_API_TOKEN = process.env.HUGGING_FACE_TOKEN;
|
||
|
||
// Educational prompts for AI to guide thinking instead of giving direct answers
|
||
const EDUCATIONAL_PROMPTS = {
|
||
en: {
|
||
systemPrompt: "You are an educational assistant for children. Instead of giving direct answers, ask 2-3 guiding questions that help children think through the problem themselves. Be encouraging and use simple language. Focus on the thinking process, not the answer.",
|
||
prefix: "That's a great question! Let me help you think through this step by step. Instead of telling you the answer, here are some questions to guide your thinking:"
|
||
},
|
||
de: {
|
||
systemPrompt: "Du bist ein Lernassistent für Kinder. Anstatt direkte Antworten zu geben, stelle 2-3 Leitfragen, die Kindern helfen, das Problem selbst zu durchdenken. Sei ermutigend und verwende einfache Sprache. Konzentriere dich auf den Denkprozess, nicht auf die Antwort.",
|
||
prefix: "Das ist eine tolle Frage! Lass mich dir helfen, Schritt für Schritt darüber nachzudenken. Anstatt dir die Antwort zu sagen, hier sind einige Fragen, die dein Denken leiten:"
|
||
}
|
||
};
|
||
|
||
// Middleware
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
app.use(express.static('.'));
|
||
|
||
// Serve the main page
|
||
app.get('/', (req, res) => {
|
||
res.sendFile(path.join(__dirname, 'index.html'));
|
||
});
|
||
|
||
// API endpoint for AI-powered educational guidance
|
||
app.post('/api/ask', async (req, res) => {
|
||
const { question, language = 'en' } = req.body;
|
||
|
||
if (!question || question.trim().length === 0) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'Question is required'
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Get AI-powered guidance
|
||
const aiGuidance = await getAIGuidance(question, language);
|
||
|
||
res.json({
|
||
success: true,
|
||
guidance: aiGuidance,
|
||
question: question,
|
||
language: language
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('AI API Error:', error);
|
||
|
||
// Fallback to rule-based guidance if AI fails
|
||
const fallbackGuidance = getFallbackGuidance(question, language);
|
||
|
||
res.json({
|
||
success: true,
|
||
guidance: fallbackGuidance,
|
||
question: question,
|
||
language: language,
|
||
fallback: true
|
||
});
|
||
}
|
||
});
|
||
|
||
// Function to get OpenAI-powered educational guidance (Primary)
|
||
async function getOpenAIGuidance(question, language) {
|
||
console.log('🤖 Calling OpenAI with GPT-3.5-turbo...');
|
||
|
||
const isGerman = language === 'de';
|
||
|
||
const systemPrompt = isGerman
|
||
? "Du bist ein pädagogischer Assistent für Kinder. Anstatt direkte Antworten zu geben, stellst du 3-4 durchdachte Leitfragen, die Kindern helfen, selbst über das Problem nachzudenken. Verwende einfache Sprache, sei ermutigend und konzentriere dich auf den Denkprozess. Formatiere als nummerierte Liste."
|
||
: "You are an educational assistant for children. Instead of giving direct answers, provide 3-4 thoughtful guiding questions that help children think through the problem themselves. Use simple language, be encouraging, and focus on the thinking process. Format as a numbered list.";
|
||
|
||
const userPrompt = isGerman
|
||
? `Ein Kind hat gefragt: "${question}". Hilf ihm dabei, selbst über die Antwort nachzudenken, indem du Leitfragen stellst.`
|
||
: `A child asked: "${question}". Help them think through the answer themselves by providing guiding questions.`;
|
||
|
||
try {
|
||
const completion = await openai.chat.completions.create({
|
||
model: "gpt-3.5-turbo",
|
||
messages: [
|
||
{ role: "system", content: systemPrompt },
|
||
{ role: "user", content: userPrompt }
|
||
],
|
||
max_tokens: 200,
|
||
temperature: 0.7
|
||
});
|
||
|
||
const aiResponse = completion.choices[0]?.message?.content || '';
|
||
console.log('✅ OpenAI response received:', aiResponse.substring(0, 100) + '...');
|
||
|
||
// Parse the response into steps
|
||
const steps = parseOpenAIResponseToSteps(aiResponse, language);
|
||
|
||
return {
|
||
type: 'ai-powered',
|
||
steps: steps,
|
||
encouragement: getRandomEncouragement(language),
|
||
source: 'OpenAI GPT-3.5'
|
||
};
|
||
|
||
} catch (error) {
|
||
console.log('❌ OpenAI error:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Function to parse OpenAI response into thinking steps
|
||
function parseOpenAIResponseToSteps(text, language) {
|
||
const lines = text.split('\n').filter(line => line.trim());
|
||
const steps = [];
|
||
|
||
lines.forEach((line, index) => {
|
||
const trimmed = line.trim();
|
||
// Look for numbered items or questions
|
||
if (trimmed && (trimmed.match(/^\d+\./) || trimmed.includes('?') || trimmed.length > 10)) {
|
||
// Clean up numbering and formatting
|
||
const cleaned = trimmed.replace(/^\d+\.\s*/, '').replace(/^-\s*/, '').trim();
|
||
if (cleaned.length > 5) {
|
||
steps.push({
|
||
id: steps.length + 1,
|
||
text: cleaned,
|
||
type: 'question'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Ensure we have at least 2 steps
|
||
if (steps.length < 2) {
|
||
const fallback = getFallbackGuidance('', language);
|
||
return fallback.steps.slice(0, 3);
|
||
}
|
||
|
||
return steps.slice(0, 4); // Limit to 4 steps max
|
||
}
|
||
|
||
// Function to get AI-powered educational guidance with model fallbacks (Backup)
|
||
async function getHuggingFaceGuidance(question, language) {
|
||
console.log('🤖 Trying Hugging Face as backup...');
|
||
|
||
// Try each model until one works
|
||
for (let i = 0; i < HF_MODELS.length; i++) {
|
||
const model = HF_MODELS[i];
|
||
const apiUrl = `https://api-inference.huggingface.co/models/${model}`;
|
||
|
||
try {
|
||
console.log(`📡 Trying model ${i + 1}/${HF_MODELS.length}: ${model}`);
|
||
|
||
// Create educational prompt
|
||
const educationalPrompt = `Help a child think about this question: "${question}".
|
||
Don't give the answer. Instead, ask 3 guiding questions that help them discover it themselves.
|
||
Format: 1. [question] 2. [question] 3. [question]`;
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': HF_API_TOKEN ? `Bearer ${HF_API_TOKEN}` : undefined,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
inputs: educationalPrompt,
|
||
parameters: {
|
||
max_length: 120,
|
||
temperature: 0.8,
|
||
do_sample: true,
|
||
pad_token_id: 50256
|
||
}
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
// Check if the model is still loading
|
||
if (data.error && data.error.includes('loading')) {
|
||
console.log(`⏳ Model ${model} is loading, trying next...`);
|
||
continue;
|
||
}
|
||
|
||
if (data.error) {
|
||
console.log(`❌ Model ${model} error:`, data.error);
|
||
continue;
|
||
}
|
||
|
||
console.log(`✅ Success with model ${model}`);
|
||
|
||
// Extract and format the AI response
|
||
let aiText = data[0]?.generated_text || '';
|
||
|
||
if (aiText.trim().length > 10) {
|
||
const steps = parseAIResponseToSteps(aiText, language);
|
||
|
||
return {
|
||
type: 'ai-powered',
|
||
steps: steps,
|
||
encouragement: getRandomEncouragement(language),
|
||
source: `Hugging Face (${model.split('/')[1]})`
|
||
};
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.log(`⚠️ Model ${model} failed:`, error.message);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// If all models fail, throw error to trigger fallback
|
||
throw new Error('All Hugging Face models are currently unavailable');
|
||
}
|
||
|
||
// Updated main function that tries OpenAI first, then Hugging Face, then local fallback
|
||
async function getAIGuidance(question, language) {
|
||
// Try OpenAI first (most reliable)
|
||
if (process.env.OPENAI_API_KEY) {
|
||
try {
|
||
return await getOpenAIGuidance(question, language);
|
||
} catch (error) {
|
||
console.log('⚠️ OpenAI failed, trying Hugging Face...');
|
||
}
|
||
}
|
||
|
||
// Try Hugging Face as backup
|
||
if (process.env.HUGGING_FACE_TOKEN) {
|
||
try {
|
||
return await getHuggingFaceGuidance(question, language);
|
||
} catch (error) {
|
||
console.log('⚠️ Hugging Face failed, using local fallback...');
|
||
}
|
||
}
|
||
|
||
// Final fallback - this will throw error to trigger local guidance
|
||
throw new Error('All AI services are currently unavailable');
|
||
}
|
||
|
||
// Try each model until one works
|
||
for (let i = 0; i < HF_MODELS.length; i++) {
|
||
const model = HF_MODELS[i];
|
||
const apiUrl = `https://api-inference.huggingface.co/models/${model}`;
|
||
|
||
try {
|
||
console.log(`📡 Trying model ${i + 1}/${HF_MODELS.length}: ${model}`);
|
||
|
||
// Create educational prompt
|
||
const educationalPrompt = `Help a child think about this question: "${question}".
|
||
Don't give the answer. Instead, ask 3 guiding questions that help them discover it themselves.
|
||
Format: 1. [question] 2. [question] 3. [question]`;
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': HF_API_TOKEN ? `Bearer ${HF_API_TOKEN}` : undefined,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
inputs: educationalPrompt,
|
||
parameters: {
|
||
max_length: 120,
|
||
temperature: 0.8,
|
||
do_sample: true,
|
||
pad_token_id: 50256
|
||
}
|
||
})
|
||
});
|
||
|
||
console.log(`<EFBFBD> Model ${model} response status:`, response.status);
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
// Check if the model is still loading
|
||
if (data.error && data.error.includes('loading')) {
|
||
console.log(`⏳ Model ${model} is loading, trying next...`);
|
||
continue;
|
||
}
|
||
|
||
if (data.error) {
|
||
console.log(`❌ Model ${model} error:`, data.error);
|
||
continue;
|
||
}
|
||
|
||
console.log(`✅ Success with model ${model}:`, JSON.stringify(data, null, 2));
|
||
|
||
// Extract and format the AI response
|
||
let aiText = data[0]?.generated_text || '';
|
||
|
||
if (aiText.trim().length > 10) {
|
||
const steps = parseAIResponseToSteps(aiText, language);
|
||
|
||
return {
|
||
type: 'ai-powered',
|
||
steps: steps,
|
||
encouragement: getRandomEncouragement(language),
|
||
source: `Hugging Face AI (${model.split('/')[1]})`
|
||
};
|
||
}
|
||
} else {
|
||
const errorText = await response.text();
|
||
console.log(`❌ Model ${model} HTTP error ${response.status}:`, errorText);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.log(`⚠️ Model ${model} failed:`, error.message);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// If all models fail, throw error to trigger fallback
|
||
throw new Error('All AI models are currently unavailable');
|
||
}
|
||
|
||
// Function to parse AI response into thinking steps
|
||
function parseAIResponseToSteps(text, language) {
|
||
const lines = text.split('\n').filter(line => line.trim());
|
||
const steps = [];
|
||
|
||
lines.forEach((line, index) => {
|
||
const trimmed = line.trim();
|
||
if (trimmed && trimmed.length > 10) {
|
||
// Remove numbering if present
|
||
const cleaned = trimmed.replace(/^\d+\.\s*/, '').replace(/^-\s*/, '');
|
||
if (cleaned.length > 5) {
|
||
steps.push({
|
||
id: index + 1,
|
||
text: cleaned,
|
||
type: 'question'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Ensure we have at least 2 steps
|
||
if (steps.length < 2) {
|
||
const fallback = getFallbackGuidance('', language);
|
||
return fallback.steps.slice(0, 3);
|
||
}
|
||
|
||
return steps.slice(0, 4); // Limit to 4 steps max
|
||
}
|
||
|
||
// Fallback guidance for when AI is unavailable
|
||
function getFallbackGuidance(question, language) {
|
||
const isGerman = language === 'de';
|
||
|
||
const fallbackSteps = isGerman ? [
|
||
{ id: 1, text: "Was weißt du bereits über dieses Thema?", type: 'question' },
|
||
{ id: 2, text: "Welche Teile der Frage verstehst du, und welche sind unklar?", type: 'question' },
|
||
{ id: 3, text: "Wo könntest du mehr Informationen finden?", type: 'question' },
|
||
{ id: 4, text: "Kannst du das Problem in kleinere Teile aufteilen?", type: 'question' }
|
||
] : [
|
||
{ id: 1, text: "What do you already know about this topic?", type: 'question' },
|
||
{ id: 2, text: "Which parts of the question do you understand, and which are unclear?", type: 'question' },
|
||
{ id: 3, text: "Where could you find more information about this?", type: 'question' },
|
||
{ id: 4, text: "Can you break this problem down into smaller parts?", type: 'question' }
|
||
];
|
||
|
||
return {
|
||
type: 'rule-based',
|
||
steps: fallbackSteps,
|
||
encouragement: getRandomEncouragement(language),
|
||
source: 'Educational Framework'
|
||
};
|
||
}
|
||
|
||
// Get random encouragement
|
||
function getRandomEncouragement(language) {
|
||
const encouragements = language === 'de' ? [
|
||
"Großartige Frage! Du denkst wie ein echter Forscher! 🔬",
|
||
"Super! Lass uns das zusammen herausfinden! 🚀",
|
||
"Wow, das ist eine kluge Frage! 🤔",
|
||
"Du bist auf dem richtigen Weg! 🌟"
|
||
] : [
|
||
"Great question! You're thinking like a real scientist! 🔬",
|
||
"Awesome! Let's figure this out together! 🚀",
|
||
"Wow, that's a smart question! 🤔",
|
||
"You're on the right track! 🌟"
|
||
];
|
||
|
||
return encouragements[Math.floor(Math.random() * encouragements.length)];
|
||
}
|
||
|
||
// Health check endpoint
|
||
app.get('/api/health', (req, res) => {
|
||
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
||
});
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`🚀 KidsAI Explorer server running on port ${PORT}`);
|
||
console.log(`📖 Visit http://localhost:${PORT} to start exploring!`);
|
||
});
|
||
|
||
module.exports = app;
|