managarten/manadeck/supabase/functions/generate-deck/index.ts
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

273 lines
8.3 KiB
TypeScript

import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
import * as jose from "https://deno.land/x/jose@v5.9.6/index.ts";
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
};
// System prompt for consistent card generation
const SYSTEM_PROMPT = `Du bist ein Experte für die Erstellung von Lernkarten. Erstelle strukturierte Lernkarten basierend auf dem gegebenen Thema.
Regeln für die Kartenerstellung:
1. Erstelle abwechslungsreiche Karten (Flashcards und Quiz-Karten)
2. Formuliere klare, präzise Fragen und Antworten
3. Verwende die deutsche Sprache
4. Stelle sicher, dass die Karten aufeinander aufbauen
5. Füge hilfreiche Hinweise und Erklärungen hinzu
Du musst die Karten als JSON-Array zurückgeben mit folgendem Format:
[
{
"card_type": "flashcard" | "quiz",
"content": {
// Für flashcard:
"front": "Frage oder Begriff",
"back": "Antwort oder Definition",
"hint": "Optionaler Hinweis"
// Für quiz:
"question": "Die Frage",
"options": ["Option 1", "Option 2", "Option 3", "Option 4"],
"correct_answer": 0, // Index der richtigen Antwort (0-3)
"explanation": "Erklärung zur richtigen Antwort"
},
"position": 1, // Position in der Reihenfolge
"title": "Kurzer Titel für die Karte"
}
]
Erstelle GENAU die angeforderte Anzahl von Karten.`;
Deno.serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// Get the authorization header
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
throw new Error('No authorization header');
}
// Extract the Mana app token
const appToken = authHeader.replace('Bearer ', '');
// Get Mana Core JWKS URL from environment variable
const manaJwksUrl = Deno.env.get('JWKS_URL');
if (!manaJwksUrl) {
throw new Error('JWKS_URL not configured');
}
// Verify the Mana token using JWKS
const JWKS = jose.createRemoteJWKSet(new URL(manaJwksUrl));
const { payload } = await jose.jwtVerify(appToken, JWKS);
const userId = payload.sub as string;
if (!userId) {
throw new Error('Invalid token: no user ID');
}
console.log(`Authenticated user: ${userId}`);
// Initialize Supabase client with service role
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// Use the userId from the Mana token
const user = { id: userId };
// Parse request body
const requestData = await req.json();
const {
prompt: userPrompt,
deckTitle,
deckDescription = '',
cardCount = 10,
cardTypes = ['flashcard', 'quiz'],
difficulty = 'intermediate',
tags = []
} = requestData;
// Validate input
if (!userPrompt || !deckTitle) {
throw new Error('userPrompt and deckTitle are required');
}
if (cardCount < 1 || cardCount > 50) {
throw new Error('cardCount must be between 1 and 50');
}
// Get OpenAI API key from environment
const openAIApiKey = Deno.env.get('OPENAI_API_KEY');
if (!openAIApiKey) {
throw new Error('OpenAI API key not configured');
}
// Prepare the user message with specific instructions
const userMessage = `
Thema: ${userPrompt}
Anzahl Karten: ${cardCount}
Kartentypen: ${cardTypes.join(', ')}
Schwierigkeit: ${difficulty}
Tags: ${tags.length > 0 ? tags.join(', ') : 'keine spezifischen Tags'}
Erstelle ${cardCount} Lernkarten zum obigen Thema. Mische die Kartentypen ab und stelle sicher, dass die Karten progressiv aufeinander aufbauen.`;
console.log('Generating cards with OpenAI...');
// Call OpenAI API
const openAIResponse = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openAIApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userMessage }
],
temperature: 0.7,
max_tokens: 4000,
response_format: { type: "json_object" }
})
});
if (!openAIResponse.ok) {
const error = await openAIResponse.text();
console.error('OpenAI API error:', error);
throw new Error(`OpenAI API error: ${error}`);
}
const aiData = await openAIResponse.json();
const content = aiData.choices[0]?.message?.content;
if (!content) {
throw new Error('No content received from OpenAI');
}
// Parse the JSON response
let generatedCards;
try {
const parsed = JSON.parse(content);
generatedCards = Array.isArray(parsed) ? parsed : parsed.cards || parsed.karten || [];
} catch (parseError) {
console.error('Failed to parse OpenAI response:', content);
throw new Error('Invalid response format from AI');
}
if (!Array.isArray(generatedCards) || generatedCards.length === 0) {
throw new Error('No cards generated');
}
console.log(`Generated ${generatedCards.length} cards`);
// Create the deck in the database
const { data: deck, error: deckError } = await supabase
.from('decks')
.insert({
user_id: user.id,
title: deckTitle,
description: deckDescription || `KI-generiertes Deck zum Thema: ${userPrompt}`,
is_public: false,
tags: tags,
metadata: {
ai_generated: true,
generation_prompt: userPrompt,
generation_date: new Date().toISOString(),
model: 'gpt-4o-mini'
}
})
.select()
.single();
if (deckError || !deck) {
console.error('Failed to create deck:', deckError);
throw new Error('Failed to create deck');
}
// Prepare cards for insertion
const cardsToInsert = generatedCards.map((card, index) => ({
deck_id: deck.id,
card_type: card.card_type,
content: card.content,
position: card.position || index + 1,
title: card.title || `Karte ${index + 1}`,
ai_model: 'gpt-4o-mini',
ai_prompt: userPrompt,
version: 1,
is_favorite: false
}));
// Insert all cards
const { data: cards, error: cardsError } = await supabase
.from('cards')
.insert(cardsToInsert)
.select();
if (cardsError) {
console.error('Failed to create cards:', cardsError);
await supabase.from('decks').delete().eq('id', deck.id);
throw new Error('Failed to create cards: ' + JSON.stringify(cardsError));
}
console.log(`Successfully created deck with ${cards?.length} cards`);
// Track the generation (optional)
try {
await supabase.from('ai_generations').insert({
user_id: user.id,
deck_id: deck.id,
function_name: 'generate-deck',
prompt: userPrompt,
model: 'gpt-4o-mini',
status: 'completed',
metadata: {
card_count: cards?.length,
card_types: cardTypes,
difficulty: difficulty
},
completed_at: new Date().toISOString()
});
} catch (trackingError) {
console.log('Could not track generation:', trackingError);
}
// Return success response
return new Response(JSON.stringify({
success: true,
deck: {
id: deck.id,
title: deck.title,
description: deck.description,
card_count: cards?.length || 0
},
cards: cards,
message: `Deck "${deckTitle}" mit ${cards?.length} Karten erfolgreich erstellt!`
}), {
headers: {
...corsHeaders,
'Content-Type': 'application/json'
},
status: 200
});
} catch (error) {
console.error('Error in generate-deck function:', error);
return new Response(JSON.stringify({
success: false,
error: error.message || 'Ein unerwarteter Fehler ist aufgetreten'
}), {
headers: {
...corsHeaders,
'Content-Type': 'application/json'
},
status: error.message?.includes('authorization') ? 401 : 400
});
}
});