938 lines
38 KiB
JavaScript
938 lines
38 KiB
JavaScript
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',
|
|
'google/flan-t5-small',
|
|
'facebook/blenderbot-400M-distill',
|
|
'microsoft/DialoGPT-medium'
|
|
];
|
|
|
|
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
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for getting the actual answer after thinking process
|
|
app.post('/api/reveal-answer', 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 the actual answer from AI
|
|
const answer = await getActualAnswer(question, language);
|
|
|
|
res.json({
|
|
success: true,
|
|
answer: answer,
|
|
question: question,
|
|
language: language
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Answer API Error:', error);
|
|
|
|
// Fallback answer
|
|
const fallbackAnswer = getFallbackAnswer(question, language);
|
|
|
|
res.json({
|
|
success: true,
|
|
answer: fallbackAnswer,
|
|
question: question,
|
|
language: language,
|
|
fallback: true
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for checking math answers
|
|
app.post('/api/check-math-answer', async (req, res) => {
|
|
const { question, userAnswer, language = 'en' } = req.body;
|
|
|
|
if (!question || !userAnswer) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Question and user answer are required'
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Extract the math expression from the question
|
|
const correctAnswer = calculateMathAnswer(question);
|
|
const userAnswerNum = parseFloat(userAnswer.trim());
|
|
|
|
const isCorrect = !isNaN(userAnswerNum) && Math.abs(userAnswerNum - correctAnswer) < 0.001;
|
|
|
|
res.json({
|
|
success: true,
|
|
correct: isCorrect,
|
|
expectedAnswer: correctAnswer,
|
|
userAnswer: userAnswerNum,
|
|
hint: !isCorrect ? (language === 'de' ? 'Überprüfe deine Rechnung nochmal.' : 'Check your calculation again.') : null
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Math check error:', error);
|
|
res.json({
|
|
success: true,
|
|
correct: true, // Fallback to correct if calculation fails
|
|
expectedAnswer: null,
|
|
userAnswer: userAnswer
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for getting additional math hints
|
|
app.post('/api/get-more-hints', async (req, res) => {
|
|
const { question, language = 'en', previousHints = [] } = req.body;
|
|
|
|
if (!question) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Question is required'
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Generate additional hints based on the math problem
|
|
const additionalHints = await generateAdditionalMathHints(question, language, previousHints);
|
|
|
|
res.json({
|
|
success: true,
|
|
hints: additionalHints,
|
|
question: question,
|
|
language: language
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Additional hints error:', error);
|
|
|
|
// Fallback hints if AI fails
|
|
const fallbackHints = getFallbackMathHints(question, language);
|
|
|
|
res.json({
|
|
success: true,
|
|
hints: fallbackHints,
|
|
question: question,
|
|
language: language,
|
|
fallback: true
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for interactive thinking feedback
|
|
app.post('/api/think-response', async (req, res) => {
|
|
const { question, thought, stepNumber, language = 'en' } = req.body;
|
|
|
|
if (!question || !thought) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Question and thought are required'
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Get AI feedback on the child's thinking
|
|
const feedback = await getThinkingFeedback(question, thought, stepNumber, language);
|
|
|
|
res.json({
|
|
success: true,
|
|
feedback: feedback,
|
|
question: question,
|
|
thought: thought,
|
|
language: language
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Thinking feedback error:', error);
|
|
|
|
// Fallback positive feedback
|
|
const fallbackFeedback = getFallbackThinkingFeedback(language);
|
|
|
|
res.json({
|
|
success: true,
|
|
feedback: fallbackFeedback,
|
|
question: question,
|
|
thought: thought,
|
|
language: language,
|
|
fallback: true
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for interactive thinking feedback
|
|
app.post('/api/thinking-feedback', async (req, res) => {
|
|
const { question, stepNumber, userThought, language = 'en' } = req.body;
|
|
|
|
if (!question || !userThought || stepNumber === undefined) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Question, step number, and user thought are required'
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Get AI feedback on the child's thinking
|
|
const feedback = await getThinkingFeedback(question, userThought, stepNumber, language);
|
|
|
|
res.json({
|
|
success: true,
|
|
feedback: feedback,
|
|
question: question,
|
|
stepNumber: stepNumber,
|
|
userThought: userThought,
|
|
language: language
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Thinking feedback error:', error);
|
|
|
|
// Fallback positive feedback
|
|
const fallbackFeedback = {
|
|
response: language === 'de'
|
|
? "Interessante Gedanken! Du denkst in die richtige Richtung. 👍"
|
|
: "Interesting thoughts! You're thinking in the right direction. 👍",
|
|
encouragement: language === 'de'
|
|
? "Weiter so!"
|
|
: "Keep going!",
|
|
type: 'positive'
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
feedback: fallbackFeedback,
|
|
question: question,
|
|
stepNumber: stepNumber,
|
|
userThought: userThought,
|
|
language: language,
|
|
fallback: true
|
|
});
|
|
}
|
|
});
|
|
|
|
// Function to calculate the correct answer for simple math expressions
|
|
function calculateMathAnswer(question) {
|
|
const questionLower = question.toLowerCase().trim();
|
|
|
|
// Extract numbers and operation from common question formats
|
|
let match;
|
|
|
|
// Pattern: "what is X+Y" or "was ist X+Y"
|
|
match = questionLower.match(/(?:what\s+is\s+|was\s+ist\s+)?(\d+(?:\.\d+)?)\s*\+\s*(\d+(?:\.\d+)?)/);
|
|
if (match) {
|
|
return parseFloat(match[1]) + parseFloat(match[2]);
|
|
}
|
|
|
|
// Pattern: "what is X-Y"
|
|
match = questionLower.match(/(?:what\s+is\s+|was\s+ist\s+)?(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)/);
|
|
if (match) {
|
|
return parseFloat(match[1]) - parseFloat(match[2]);
|
|
}
|
|
|
|
// Pattern: "what is X*Y" or "X times Y"
|
|
match = questionLower.match(/(?:what\s+is\s+|was\s+ist\s+)?(\d+(?:\.\d+)?)\s*[\*x]\s*(\d+(?:\.\d+)?)/);
|
|
if (match) {
|
|
return parseFloat(match[1]) * parseFloat(match[2]);
|
|
}
|
|
|
|
// Pattern: "what is X/Y" or "X divided by Y"
|
|
match = questionLower.match(/(?:what\s+is\s+|was\s+ist\s+)?(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/);
|
|
if (match) {
|
|
return parseFloat(match[1]) / parseFloat(match[2]);
|
|
}
|
|
|
|
// If no pattern matches, throw error
|
|
throw new Error('Could not parse math expression');
|
|
}
|
|
|
|
// Function to analyze question type and determine interaction mode
|
|
function analyzeQuestionType(question) {
|
|
const questionLower = question.toLowerCase().trim();
|
|
|
|
// Mathematical questions - should guide to calculation
|
|
const mathPatterns = [
|
|
/what\s+is\s+\d+\s*[\+\-\*\/]\s*\d+/,
|
|
/\d+\s*[\+\-\*\/]\s*\d+/,
|
|
/\d+\s*plus\s*\d+/,
|
|
/\d+\s*minus\s*\d+/,
|
|
/\d+\s*times\s*\d+/,
|
|
/\d+\s*divided\s*by\s*\d+/,
|
|
/calculate/,
|
|
/was\s+ist\s+\d+\s*[\+\-\*\/]\s*\d+/,
|
|
/rechne/,
|
|
/plus|minus|mal|geteilt|add|subtract|multiply|divide/
|
|
];
|
|
|
|
// Simple factual questions - should guide to discovery
|
|
const factualPatterns = [
|
|
/what\s+color\s+is/,
|
|
/how\s+many/,
|
|
/what\s+day\s+is/,
|
|
/what\s+time\s+is/,
|
|
/welche\s+farbe/,
|
|
/wie\s+viele/,
|
|
/welcher\s+tag/,
|
|
/wie\s+spät/
|
|
];
|
|
|
|
if (mathPatterns.some(pattern => pattern.test(questionLower))) {
|
|
console.log(`🧮 Detected mathematical question: "${question}"`);
|
|
return 'mathematical';
|
|
}
|
|
|
|
if (factualPatterns.some(pattern => pattern.test(questionLower))) {
|
|
console.log(`📋 Detected simple factual question: "${question}"`);
|
|
return 'factual_simple';
|
|
}
|
|
|
|
// Default to exploratory for complex questions
|
|
console.log(`🤔 Detected exploratory question: "${question}"`);
|
|
return 'exploratory';
|
|
}
|
|
|
|
// 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 questionType = analyzeQuestionType(question);
|
|
|
|
let systemPrompt;
|
|
let userPrompt;
|
|
|
|
if (questionType === 'mathematical') {
|
|
systemPrompt = isGerman
|
|
? "Du bist ein Mathe-Tutor für Kinder. Für einfache Rechenaufgaben führst du das Kind durch konkrete mathematische Schritte, aber verrate NIEMALS die finale Antwort. Erkläre das Rechensymbol, verwende einfache Beispiele, und lass das Kind selbst rechnen. Gib 2-3 Schritte die zum Rechnen anleiten, aber sage NIE das Endergebnis."
|
|
: "You are a math tutor for children. For simple math problems, guide the child through concrete mathematical steps, but NEVER reveal the final answer. Explain the math symbol, use simple examples, and let the child calculate themselves. Provide 2-3 steps that guide calculation, but NEVER state the final result.";
|
|
|
|
userPrompt = isGerman
|
|
? `Ein Kind fragt: "${question}". Gib 2-3 konkrete Mathe-Schritte die zum Rechnen anleiten. Erkläre was das Symbol bedeutet, verwende zählbare Beispiele, aber verrate NIEMALS die finale Antwort. Das Kind soll selbst rechnen und die Antwort finden.`
|
|
: `A child asks: "${question}". Provide 2-3 concrete math steps that guide calculation. Explain what the symbol means, use countable examples, but NEVER reveal the final answer. The child should calculate and find the answer themselves.`;
|
|
} else if (questionType === 'factual_simple') {
|
|
systemPrompt = isGerman
|
|
? "Du bist ein Lernbegleiter für Kinder. Bei einfachen Faktenfragen stellst du 2-3 spezifische, logische Schritte, die das Kind zur Antwort führen. Verwende konkrete Beispiele und Beobachtungen, die das Kind selbst machen kann. Keine allgemeinen Fragen, sondern spezifische Denkschritte."
|
|
: "You are a learning companion for children. For simple factual questions, provide 2-3 specific, logical steps that lead the child to the answer. Use concrete examples and observations the child can make themselves. No general questions, but specific thinking steps.";
|
|
|
|
userPrompt = isGerman
|
|
? `Ein Kind fragt: "${question}". Gib 2-3 konkrete Denkschritte, die spezifisch zu dieser Frage passen. Verwende Beispiele und lass das Kind selbst beobachten oder überlegen.`
|
|
: `A child asks: "${question}". Provide 2-3 concrete thinking steps that are specific to this question. Use examples and have the child observe or think for themselves.`;
|
|
} else {
|
|
// For exploratory questions, provide specific thinking questions related to the topic
|
|
systemPrompt = isGerman
|
|
? "Du bist ein pädagogischer Assistent für Kinder. Für komplexere Fragen stellst du 2-3 sehr spezifische Denkfragen, die direkt mit dem Thema zusammenhängen. Analysiere die Frage und stelle konkrete, themenspezifische Fragen, die das Kind zum Nachdenken über genau dieses Thema anregen. Keine allgemeinen Fragen!"
|
|
: "You are an educational assistant for children. For complex questions, provide 2-3 very specific thinking questions that directly relate to the topic. Analyze the question and ask concrete, topic-specific questions that encourage the child to think about exactly this subject. No generic questions!";
|
|
|
|
userPrompt = isGerman
|
|
? `Ein Kind hat gefragt: "${question}". Stelle 2-3 sehr spezifische Fragen, die direkt mit diesem Thema zusammenhängen und das Kind zum Nachdenken über genau dieses Thema anregen. Zum Beispiel für "Wie entstehen Regenbogen?": 1) Was brauchst du für einen Regenbogen (Sonne und was noch?), 2) Welche Farben siehst du in einem Regenbogen?, 3) Wann hast du schon mal einen Regenbogen gesehen?`
|
|
: `A child asked: "${question}". Provide 2-3 very specific questions that directly relate to this topic and encourage the child to think about exactly this subject. For example for "How do rainbows form?": 1) What do you need for a rainbow (sun and what else?), 2) What colors do you see in a rainbow?, 3) When have you seen a rainbow before?`;
|
|
}
|
|
|
|
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',
|
|
questionType: questionType,
|
|
steps: steps,
|
|
encouragement: getRandomEncouragement(language),
|
|
source: 'OpenAI GPT-3.5',
|
|
showAnswerReveal: questionType === 'exploratory' // Only show reveal for complex questions
|
|
};
|
|
|
|
} 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 Hugging Face guidance (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}`);
|
|
|
|
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();
|
|
|
|
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}`);
|
|
|
|
let aiText = data[0]?.generated_text || '';
|
|
|
|
if (aiText.trim().length > 10) {
|
|
const steps = parseHuggingFaceResponseToSteps(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;
|
|
}
|
|
}
|
|
|
|
throw new Error('All Hugging Face models are currently unavailable');
|
|
}
|
|
|
|
// Function to parse Hugging Face response into thinking steps
|
|
function parseHuggingFaceResponseToSteps(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) {
|
|
const cleaned = trimmed.replace(/^\d+\.\s*/, '').replace(/^-\s*/, '');
|
|
if (cleaned.length > 5) {
|
|
steps.push({
|
|
id: index + 1,
|
|
text: cleaned,
|
|
type: 'question'
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (steps.length < 2) {
|
|
const fallback = getFallbackGuidance('', language);
|
|
return fallback.steps.slice(0, 3);
|
|
}
|
|
|
|
return steps.slice(0, 4);
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
|
|
// 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(),
|
|
ai_services: {
|
|
openai: !!process.env.OPENAI_API_KEY,
|
|
huggingface: !!process.env.HUGGING_FACE_TOKEN
|
|
}
|
|
});
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 KidsAI Explorer server running on port ${PORT}`);
|
|
console.log(`📖 Visit http://localhost:${PORT} to start exploring!`);
|
|
console.log(`🤖 AI Services: OpenAI=${!!process.env.OPENAI_API_KEY}, HuggingFace=${!!process.env.HUGGING_FACE_TOKEN}`);
|
|
});
|
|
|
|
module.exports = app;
|
|
|
|
// Function to get the actual answer (for answer reveal)
|
|
async function getActualAnswer(question, language) {
|
|
console.log('📝 Getting actual answer for:', question);
|
|
|
|
const isGerman = language === 'de';
|
|
|
|
const systemPrompt = isGerman
|
|
? "Du bist ein hilfreicher Assistent für Kinder. Gib eine klare, einfache und altersgerechte Antwort auf die Frage. Verwende einfache Sprache und erkläre es so, dass ein Kind es verstehen kann. Halte die Antwort kurz aber vollständig."
|
|
: "You are a helpful assistant for children. Provide a clear, simple, and age-appropriate answer to the question. Use simple language and explain it in a way a child can understand. Keep the answer concise but complete.";
|
|
|
|
const userPrompt = isGerman
|
|
? `Ein Kind möchte wissen: "${question}". Gib eine einfache, klare Antwort.`
|
|
: `A child wants to know: "${question}". Provide a simple, clear answer.`;
|
|
|
|
try {
|
|
if (process.env.OPENAI_API_KEY) {
|
|
const completion = await openai.chat.completions.create({
|
|
model: "gpt-3.5-turbo",
|
|
messages: [
|
|
{ role: "system", content: systemPrompt },
|
|
{ role: "user", content: userPrompt }
|
|
],
|
|
max_tokens: 150,
|
|
temperature: 0.3 // Lower temperature for more factual answers
|
|
});
|
|
|
|
const answer = completion.choices[0]?.message?.content || '';
|
|
console.log('✅ Answer received from OpenAI');
|
|
|
|
return {
|
|
type: 'ai-powered',
|
|
text: answer.trim(),
|
|
source: 'OpenAI GPT-3.5'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.log('❌ OpenAI answer error:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Fallback answers for common questions
|
|
function getFallbackAnswer(question, language) {
|
|
const isGerman = language === 'de';
|
|
|
|
// Simple pattern matching for common questions
|
|
const questionLower = question.toLowerCase();
|
|
|
|
if (questionLower.includes('sky') || questionLower.includes('himmel') || questionLower.includes('blue') || questionLower.includes('blau')) {
|
|
return {
|
|
type: 'rule-based',
|
|
text: isGerman
|
|
? "Der Himmel ist blau, weil winzig kleine Teilchen in der Luft das blaue Licht mehr streuen als andere Farben. Es ist wie wenn du blaues Licht durch viele kleine Glaskugeln scheinst!"
|
|
: "The sky is blue because tiny particles in the air scatter blue light more than other colors. It's like shining blue light through many tiny glass balls!",
|
|
source: 'Educational Framework'
|
|
};
|
|
}
|
|
|
|
if (questionLower.includes('bird') || questionLower.includes('fly') || questionLower.includes('vogel') || questionLower.includes('fliegen')) {
|
|
return {
|
|
type: 'rule-based',
|
|
text: isGerman
|
|
? "Vögel können fliegen, weil ihre Flügel eine besondere Form haben und sie sehr leichte, hohle Knochen haben. Sie schlagen mit den Flügeln und erzeugen Auftrieb, der sie in die Luft hebt!"
|
|
: "Birds can fly because their wings have a special shape and they have very light, hollow bones. They flap their wings to create lift that pushes them up into the air!",
|
|
source: 'Educational Framework'
|
|
};
|
|
}
|
|
|
|
if (questionLower.includes('1+1') || questionLower.includes('add') || questionLower.includes('plus')) {
|
|
return {
|
|
type: 'rule-based',
|
|
text: isGerman
|
|
? "1 + 1 = 2! Wenn du einen Apfel hast und noch einen dazu bekommst, hast du zwei Äpfel. Das ist Addition - zusammenzählen!"
|
|
: "1 + 1 = 2! When you have one apple and get another one, you have two apples. That's addition - adding together!",
|
|
source: 'Educational Framework'
|
|
};
|
|
}
|
|
|
|
// Generic fallback
|
|
return {
|
|
type: 'rule-based',
|
|
text: isGerman
|
|
? "Das ist eine tolle Frage! Die Antwort hängt von vielen Faktoren ab. Ich empfehle dir, in einem Buch nachzuschauen oder einen Erwachsenen zu fragen, der mehr darüber weiß."
|
|
: "That's a great question! The answer depends on many factors. I recommend looking it up in a book or asking an adult who knows more about this topic.",
|
|
source: 'Educational Framework'
|
|
};
|
|
}
|
|
|
|
// Generate additional math hints using AI
|
|
async function generateAdditionalMathHints(question, language, previousHints) {
|
|
const isGerman = language === 'de';
|
|
|
|
const systemPrompt = isGerman
|
|
? "Du bist ein geduldiger Mathe-Tutor für Kinder. Das Kind hat bereits Schwierigkeiten mit einer Rechenaufgabe. Gib 2-3 zusätzliche, sehr einfache und konkrete Hilfestellungen. Verwende noch einfachere Beispiele, zerlege die Aufgabe in kleinere Schritte, oder verwende visuelle Hilfsmittel wie Finger oder Gegenstände. Sei ermutigend und geduldig."
|
|
: "You are a patient math tutor for children. The child is already struggling with a math problem. Provide 2-3 additional, very simple and concrete hints. Use even simpler examples, break the problem into smaller steps, or use visual aids like fingers or objects. Be encouraging and patient.";
|
|
|
|
const userPrompt = isGerman
|
|
? `Ein Kind hat Schwierigkeiten mit: "${question}". Es braucht zusätzliche, einfachere Hilfe. Gib 2-3 sehr konkrete, einfache Schritte mit visuellen Hilfsmitteln oder noch einfacheren Beispielen.`
|
|
: `A child is struggling with: "${question}". They need additional, simpler help. Provide 2-3 very concrete, simple steps with visual aids or even simpler examples.`;
|
|
|
|
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 || '';
|
|
const steps = parseOpenAIResponseToSteps(aiResponse, language);
|
|
|
|
return {
|
|
type: 'ai-powered',
|
|
steps: steps,
|
|
encouragement: isGerman ? "Keine Sorge, wir schaffen das zusammen! 💪" : "Don't worry, we can do this together! 💪",
|
|
source: 'OpenAI GPT-3.5'
|
|
};
|
|
|
|
} catch (error) {
|
|
console.log('❌ Additional hints AI error:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Fallback hints for when AI is not available
|
|
function getFallbackMathHints(question, language) {
|
|
const isGerman = language === 'de';
|
|
const questionLower = question.toLowerCase();
|
|
|
|
// Determine operation type and generate appropriate hints
|
|
let hints = [];
|
|
|
|
if (questionLower.includes('+') || questionLower.includes('plus')) {
|
|
hints = isGerman ? [
|
|
"Verwende deine Finger zum Zählen - das macht es einfacher!",
|
|
"Nimm die erste Zahl und zähle dann die zweite Zahl dazu.",
|
|
"Du kannst auch kleine Gegenstände wie Münzen oder Stifte zum Zählen verwenden."
|
|
] : [
|
|
"Use your fingers for counting - it makes it easier!",
|
|
"Take the first number and count the second number on top of it.",
|
|
"You can also use small objects like coins or pencils for counting."
|
|
];
|
|
} else if (questionLower.includes('-') || questionLower.includes('minus')) {
|
|
hints = isGerman ? [
|
|
"Bei Minus nehmen wir etwas weg. Starte mit der ersten Zahl.",
|
|
"Zähle rückwärts um die zweite Zahl.",
|
|
"Stelle dir vor, du hast Süßigkeiten und gibst einige weg."
|
|
] : [
|
|
"With minus, we take something away. Start with the first number.",
|
|
"Count backwards by the second number.",
|
|
"Imagine you have candies and you give some away."
|
|
];
|
|
} else {
|
|
// Generic math hints
|
|
hints = isGerman ? [
|
|
"Lass uns das Problem in kleinere Teile aufteilen.",
|
|
"Verwende deine Finger oder male kleine Kreise zum Zählen.",
|
|
"Nimm dir Zeit - Mathe ist wie ein Puzzle, das wir zusammen lösen."
|
|
] : [
|
|
"Let's break the problem into smaller parts.",
|
|
"Use your fingers or draw small circles for counting.",
|
|
"Take your time - math is like a puzzle we solve together."
|
|
];
|
|
}
|
|
|
|
return {
|
|
type: 'fallback',
|
|
steps: hints.map((hint, index) => ({
|
|
id: index + 1,
|
|
text: hint,
|
|
type: 'hint'
|
|
})),
|
|
encouragement: isGerman ? "Du schaffst das! 🌟" : "You can do it! 🌟",
|
|
source: 'Fallback'
|
|
};
|
|
}
|
|
|
|
// Generate AI feedback on child's thinking
|
|
async function getThinkingFeedback(question, thought, stepNumber, language) {
|
|
const isGerman = language === 'de';
|
|
|
|
// Analyze the child's response to provide intelligent feedback
|
|
const thoughtLower = thought.toLowerCase().trim();
|
|
|
|
// Handle special cases first
|
|
if (thoughtLower === '' || thoughtLower.length < 2) {
|
|
return {
|
|
response: isGerman ? "Erzähl mir mehr über deine Gedanken!" : "Tell me more about your thoughts!",
|
|
encouragement: isGerman ? "Jede Idee zählt! 💭" : "Every idea counts! 💭",
|
|
type: 'encouraging',
|
|
source: 'Rule-based'
|
|
};
|
|
}
|
|
|
|
if (thoughtLower === 'i dont know' || thoughtLower === 'i don\'t know' || thoughtLower === 'ich weiß nicht' || thoughtLower === 'weiß nicht') {
|
|
return {
|
|
response: isGerman ?
|
|
"Das ist okay! Lass uns gemeinsam überlegen. Was fällt dir als erstes ein, wenn du an diese Frage denkst?" :
|
|
"That's okay! Let's think together. What's the first thing that comes to mind when you think about this question?",
|
|
encouragement: isGerman ? "Gemeinsam finden wir eine Antwort! 🤝" : "Together we'll find an answer! 🤝",
|
|
type: 'supportive',
|
|
source: 'Rule-based'
|
|
};
|
|
}
|
|
|
|
if (thoughtLower === 'no' || thoughtLower === 'nein' || thoughtLower === 'ja' || thoughtLower === 'yes') {
|
|
return {
|
|
response: isGerman ?
|
|
"Interessant! Kannst du mir mehr darüber erzählen, warum du so denkst?" :
|
|
"Interesting! Can you tell me more about why you think that?",
|
|
encouragement: isGerman ? "Deine Meinung ist wichtig! 💬" : "Your opinion matters! 💬",
|
|
type: 'probing',
|
|
source: 'Rule-based'
|
|
};
|
|
}
|
|
|
|
// For substantial responses, use AI with much better prompts
|
|
const systemPrompt = isGerman
|
|
? `Du bist ein intelligenter Tutor für Kinder. Ein Kind hat auf eine Denkfrage geantwortet. Deine Aufgabe:
|
|
|
|
1. ANALYSIERE den Gedanken des Kindes sorgfältig
|
|
2. ERKENNE richtige Ansätze und baue darauf auf
|
|
3. KORRIGIERE sanft Missverständnisse ohne die Antwort zu verraten
|
|
4. STELLE eine Nachfrage, die das Denken vertieft
|
|
5. SEI spezifisch, nicht generisch
|
|
|
|
Antworte in 1-2 Sätzen. Vermeide generische Phrasen wie "Gut gemacht!" oder "Weiter so!"`
|
|
: `You are an intelligent tutor for children. A child has responded to a thinking question. Your job:
|
|
|
|
1. ANALYZE the child's thought carefully
|
|
2. RECOGNIZE correct approaches and build on them
|
|
3. GENTLY correct misunderstandings without revealing the answer
|
|
4. ASK a follow-up question that deepens thinking
|
|
5. BE specific, not generic
|
|
|
|
Respond in 1-2 sentences. Avoid generic phrases like "Good job!" or "Keep going!"`;
|
|
|
|
const userPrompt = isGerman
|
|
? `Original Frage: "${question}"
|
|
Schritt ${stepNumber}
|
|
Kind antwortete: "${thought}"
|
|
|
|
Gib intelligente, spezifische Rückmeldung zu diesem Gedanken. Baue auf richtigen Ansätzen auf oder korrigiere sanft Missverständnisse. Stelle eine durchdachte Nachfrage.`
|
|
: `Original Question: "${question}"
|
|
Step ${stepNumber}
|
|
Child responded: "${thought}"
|
|
|
|
Give intelligent, specific feedback about this thought. Build on correct approaches or gently correct misunderstandings. Ask a thoughtful follow-up question.`;
|
|
|
|
try {
|
|
const completion = await openai.chat.completions.create({
|
|
model: "gpt-3.5-turbo",
|
|
messages: [
|
|
{ role: "system", content: systemPrompt },
|
|
{ role: "user", content: userPrompt }
|
|
],
|
|
max_tokens: 120,
|
|
temperature: 0.8
|
|
});
|
|
|
|
const aiResponse = completion.choices[0]?.message?.content || '';
|
|
|
|
// Determine feedback type based on content and child's response quality
|
|
let responseType = 'neutral';
|
|
if (thoughtLower.length > 10 || thoughtLower.includes('because') || thoughtLower.includes('weil')) {
|
|
responseType = 'positive';
|
|
} else if (thoughtLower.length < 5) {
|
|
responseType = 'encouraging';
|
|
}
|
|
|
|
return {
|
|
response: aiResponse.trim(),
|
|
encouragement: isGerman ? "Du denkst gut mit! 🧠" : "You're thinking well! 🧠",
|
|
type: responseType,
|
|
source: 'OpenAI GPT-3.5'
|
|
};
|
|
|
|
} catch (error) {
|
|
console.log('❌ Thinking feedback AI error:', error.message);
|
|
throw error;
|
|
}
|
|
}
|