From 763ebef22bb3d0dca8c8d68348ebf4c324bb4d91 Mon Sep 17 00:00:00 2001 From: Z User Date: Mon, 10 Aug 2026 16:33:23 +0000 Subject: [PATCH] Add AI Studio with agentic system, SnapGen video gen, Perchance image gen Features: - AI Studio module with 4 tabs: Video Generation, Image Generation, Lesson Plan, AI Agent Chat - SnapGen AI API integration for video lesson generation (https://api.snapgen.ai/uapi/v1/generate) - Educational prompt enhancement with subject/topic/language context - Async polling for video generation status - Fallback to z-ai-web-dev-sdk if SnapGen not configured - Perchance AI API integration for image generation (https://perchance.org/text-to-image-plugin) - Multiple styles: realistic, cartoon, watercolor, pixel, sketch, 3D - Fallback to z-ai-web-dev-sdk if Perchance not configured - Agentic AI chat with teacher assistant context - AI lesson plan generation Fixes: - Fix duplicate close button in onboarding tour (removed default Dialog close, kept custom one) - Add SnapGen and Perchance fields to AISettings model Navigation: - AI Studio added to Insights section (teachers) and Student nav - Digital Notebook confirmed available for both students and teachers --- prisma/schema.prisma | 6 + src/app/api/ai/perchance/route.ts | 144 +++++ src/app/api/ai/snapgen/route.ts | 223 +++++++ src/components/ai-studio-view.tsx | 895 +++++++++++++++++++++++++++++ src/components/app-layout.tsx | 5 + src/components/onboarding-tour.tsx | 2 +- src/lib/i18n.ts | 2 + src/lib/store.ts | 3 +- 8 files changed, 1278 insertions(+), 2 deletions(-) create mode 100644 src/app/api/ai/perchance/route.ts create mode 100644 src/app/api/ai/snapgen/route.ts create mode 100644 src/components/ai-studio-view.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e5ee15b..179013e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2430,6 +2430,12 @@ model AISettings { aiMaxRequestsPerDay Int @default(50) // Student daily limit aiHelperMode String @default("guided") // guided, full, restricted aiSystemPrompt String? // Custom system prompt for AI chat + // SnapGen AI (Video generation) + snapgenEnabled Boolean @default(false) + snapgenApiKey String? + // Perchance AI (Image generation) + perchanceEnabled Boolean @default(false) + perchanceApiKey String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/app/api/ai/perchance/route.ts b/src/app/api/ai/perchance/route.ts new file mode 100644 index 0000000..d8c4d1e --- /dev/null +++ b/src/app/api/ai/perchance/route.ts @@ -0,0 +1,144 @@ +// @ts-nocheck +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { getSession } from '@/lib/auth'; + +/** + * Perchance AI Image Generation API + * POST /api/ai/perchance + * Body: { prompt, style?, size? } + * + * Uses Perchance AI: https://perchance.org/text-to-image-plugin + * Falls back to z-ai-web-dev-sdk if Perchance is not configured + */ + +export async function POST(request: Request) { + try { + const session = await getSession(); + if (!session?.user) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + + // Only admin/teacher can generate images (students can if enabled) + const role = session.user.role; + const isStaff = role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'TEACHER'; + + if (!isStaff && role !== 'STUDENT') { + return NextResponse.json( + { error: 'Only teachers, admins, and students can generate images' }, + { status: 403 } + ); + } + + const { schoolId } = session.user; + if (!schoolId) { + return NextResponse.json({ error: 'No school assigned' }, { status: 403 }); + } + + // Check AI settings + const aiSettings = await db.aISettings.findUnique({ + where: { schoolId }, + }); + + if (!aiSettings || (!aiSettings.perchanceEnabled && !aiSettings.aiImageGenEnabled)) { + return NextResponse.json( + { error: 'Image generation is disabled for your school. Enable it in Settings > AI.' }, + { status: 403 } + ); + } + + const body = await request.json(); + const { + prompt, + style = 'realistic', + size = '1024x1024', + negativePrompt, + seed, + } = body; + + if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { + return NextResponse.json( + { error: 'Prompt is required' }, + { status: 400 } + ); + } + + // Try Perchance API first if enabled + if (aiSettings.perchanceEnabled) { + try { + // Perchance uses a web-based generator. We'll use their API endpoint. + const perchanceResponse = await fetch('https://image-generation.perchance.org/api/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt, + style, + size, + negativePrompt: negativePrompt || '', + seed: seed || Math.floor(Math.random() * 1000000), + }), + }); + + if (perchanceResponse.ok) { + const data = await perchanceResponse.json(); + return NextResponse.json({ + success: true, + provider: 'perchance', + image: { + url: data.imageUrl || data.url || null, + b64_json: data.b64_json || null, + revised_prompt: prompt, + seed: data.seed || seed, + }, + }); + } + } catch (perchanceError) { + console.error('Perchance API error:', perchanceError); + // Fall through to z-ai-web-dev-sdk + } + } + + // Fallback to z-ai-web-dev-sdk + try { + const ZAI = (await import('z-ai-web-dev-sdk')).default; + const zai = await ZAI.create(); + const response = await zai.images.generations.create({ + prompt: `${prompt}. Style: ${style}`, + size, + }); + + const imageData = response.data?.[0]; + + if (!imageData) { + return NextResponse.json( + { error: 'Failed to generate image' }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + provider: 'zai', + image: { + url: imageData.url || null, + b64_json: imageData.b64_json || null, + revised_prompt: imageData.revised_prompt || prompt, + }, + }); + } catch (zaiError) { + console.error('ZAI Image Generation error:', zaiError); + return NextResponse.json( + { error: 'Image generation failed. Please try again later.' }, + { status: 500 } + ); + } + } catch (error) { + console.error('Perchance Image API error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ai/snapgen/route.ts b/src/app/api/ai/snapgen/route.ts new file mode 100644 index 0000000..644497b --- /dev/null +++ b/src/app/api/ai/snapgen/route.ts @@ -0,0 +1,223 @@ +// @ts-nocheck +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { getSession } from '@/lib/auth'; + +/** + * SnapGen AI Video Generation API + * POST /api/ai/snapgen + * Body: { prompt, duration?, aspectRatio?, style? } + * + * Uses SnapGen API: https://api.snapgen.ai/uapi/v1/generate + */ + +export async function POST(request: Request) { + try { + const session = await getSession(); + if (!session?.user) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + + // Only admin/teacher can generate videos + const role = session.user.role; + if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER') { + return NextResponse.json( + { error: 'Only teachers and admins can generate videos' }, + { status: 403 } + ); + } + + const { schoolId } = session.user; + if (!schoolId) { + return NextResponse.json({ error: 'No school assigned' }, { status: 403 }); + } + + // Check AI settings + const aiSettings = await db.aISettings.findUnique({ + where: { schoolId }, + }); + + if (!aiSettings || (!aiSettings.snapgenEnabled && !aiSettings.aiVideoGenEnabled)) { + return NextResponse.json( + { error: 'Video generation is disabled for your school. Enable it in Settings > AI.' }, + { status: 403 } + ); + } + + const body = await request.json(); + const { + prompt, + duration = 10, + aspectRatio = '16:9', + style = 'realistic', + language = 'de', + subject, + topic, + } = body; + + if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { + return NextResponse.json( + { error: 'Prompt is required' }, + { status: 400 } + ); + } + + // Build the enhanced prompt for educational content + const enhancedPrompt = buildEducationalPrompt(prompt, { + duration, + aspectRatio, + style, + language, + subject, + topic, + }); + + // Try SnapGen API first if enabled + if (aiSettings.snapgenEnabled && aiSettings.snapgenApiKey) { + try { + const snapgenResponse = await fetch('https://api.snapgen.ai/uapi/v1/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${aiSettings.snapgenApiKey}`, + }, + body: JSON.stringify({ + prompt: enhancedPrompt, + duration: `${duration}s`, + aspect_ratio: aspectRatio, + style, + language, + type: 'video', + }), + }); + + if (snapgenResponse.ok) { + const data = await snapgenResponse.json(); + return NextResponse.json({ + success: true, + provider: 'snapgen', + taskId: data.task_id || data.id || data.taskId, + status: data.status || 'processing', + videoUrl: data.video_url || data.url || null, + thumbnailUrl: data.thumbnail_url || null, + message: 'Video generation started with SnapGen AI.', + }); + } + // Fall through to z-ai-web-dev-sdk if SnapGen fails + console.error('SnapGen API error:', snapgenResponse.status); + } catch (snapgenError) { + console.error('SnapGen API error:', snapgenError); + // Fall through to z-ai-web-dev-sdk + } + } + + // Fallback to z-ai-web-dev-sdk + try { + const ZAI = (await import('z-ai-web-dev-sdk')).default; + const zai = await ZAI.create(); + const task = await zai.video.generations.create({ + prompt: enhancedPrompt, + quality: 'standard', + size: aspectRatio === '9:16' ? '720x1280' : '1280x720', + }); + + return NextResponse.json({ + success: true, + provider: 'zai', + taskId: task.id || task.taskId || 'pending', + status: task.status || 'processing', + message: 'Video generation started. Check status for updates.', + }); + } catch (zaiError) { + console.error('ZAI Video Generation error:', zaiError); + return NextResponse.json( + { error: 'Video generation failed. Please try again later.' }, + { status: 500 } + ); + } + } catch (error) { + console.error('SnapGen Video API error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +/** + * GET /api/ai/snapgen?taskId=xxx + * Check video generation task status + */ +export async function GET(request: Request) { + try { + const session = await getSession(); + if (!session?.user) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const taskId = searchParams.get('taskId'); + const provider = searchParams.get('provider') || 'zai'; + + if (!taskId) { + return NextResponse.json({ error: 'taskId is required' }, { status: 400 }); + } + + if (provider === 'snapgen') { + const aiSettings = await db.aISettings.findUnique({ + where: { schoolId: session.user.schoolId || '' }, + }); + + if (aiSettings?.snapgenApiKey) { + const statusResponse = await fetch(`https://api.snapgen.ai/uapi/v1/generate/${taskId}`, { + headers: { + 'Authorization': `Bearer ${aiSettings.snapgenApiKey}`, + }, + }); + + if (statusResponse.ok) { + const data = await statusResponse.json(); + return NextResponse.json({ + status: data.status || 'processing', + videoUrl: data.video_url || data.url || null, + thumbnailUrl: data.thumbnail_url || null, + progress: data.progress || 0, + }); + } + } + } + + // Fallback to z-ai-web-dev-sdk + try { + const ZAI = (await import('z-ai-web-dev-sdk')).default; + const zai = await ZAI.create(); + const status = await zai.video.generations.retrieve(taskId); + return NextResponse.json({ + status: status.status || 'processing', + videoUrl: status.video_url || status.url || null, + }); + } catch (error) { + return NextResponse.json({ + status: 'processing', + message: 'Task is still being processed.', + }); + } + } catch (error) { + console.error('SnapGen status check error:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +function buildEducationalPrompt( + prompt: string, + opts: { duration: number; aspectRatio: string; style: string; language: string; subject?: string; topic?: string } +): string { + const langInstruction = opts.language === 'de' + ? 'Erkläre auf Deutsch mit klarer, freundlicher Stimme.' + : 'Explain in English with a clear, friendly voice.'; + + const subjectContext = opts.subject ? `Fach: ${opts.subject}. ` : ''; + const topicContext = opts.topic ? `Thema: ${opts.topic}. ` : ''; + + return `${subjectContext}${topicContext}${langInstruction} Inhalt: ${prompt}. Stil: ${opts.style}. Dauer: ${opts.duration} Sekunden. Aspect Ratio: ${opts.aspectRatio}.`; +} diff --git a/src/components/ai-studio-view.tsx b/src/components/ai-studio-view.tsx new file mode 100644 index 0000000..e4f8152 --- /dev/null +++ b/src/components/ai-studio-view.tsx @@ -0,0 +1,895 @@ +'use client'; + +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Sparkles, Video, Image as ImageIcon, FileText, Send, + Loader2, Download, Play, RefreshCw, Copy, Check, + Film, Wand2, BookOpen, Lightbulb, GraduationCap, + Clock, CheckCircle2, AlertCircle, Settings, +} from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useAppStore } from '@/lib/store'; +import { t } from '@/lib/i18n'; +import { toast } from 'sonner'; +import { cn } from '@/lib/utils'; + +type AITab = 'video' | 'image' | 'lesson' | 'agent'; +type GenStatus = 'idle' | 'generating' | 'success' | 'error'; + +interface VideoResult { + taskId?: string; + status: string; + videoUrl?: string | null; + thumbnailUrl?: string | null; + message?: string; + provider?: string; +} + +interface ImageResult { + url?: string | null; + b64_json?: string | null; + revised_prompt?: string; + provider?: string; +} + +export default function AIStudioView() { + const { currentUser } = useAppStore(); + const [activeTab, setActiveTab] = useState('video'); + + return ( +
+ {/* Header */} +
+
+

+ + AI Studio +

+

+ KI-gestützte Content-Erstellung für den Unterricht +

+
+ + + Agentic AI + +
+ + {/* Stats cards */} +
+ {[ + { label: 'Video-Gen', icon: Video, color: 'text-sky-600 bg-sky-50 dark:bg-sky-950/30' }, + { label: 'Bild-Gen', icon: ImageIcon, color: 'text-violet-600 bg-violet-50 dark:bg-violet-950/30' }, + { label: 'Unterricht', icon: BookOpen, color: 'text-emerald-600 bg-emerald-50 dark:bg-emerald-950/30' }, + { label: 'KI-Agent', icon: Lightbulb, color: 'text-amber-600 bg-amber-50 dark:bg-amber-950/30' }, + ].map((s, i) => { + const Icon = s.icon; + return ( + + +
+ +
+
+

{s.label}

+

Bereit

+
+
+
+ ); + })} +
+ + {/* Main content tabs */} + setActiveTab(v as AITab)}> + + + + + + Bild generieren + Bild + + + + Unterrichtsentwurf + Plan + + + + KI-Agent + Agent + + + + + + + + + + + + + + + + +
+ ); +} + +// ─── Video Generation Tab ────────────────────────────────────────── + +function VideoGenerationTab() { + const [prompt, setPrompt] = useState(''); + const [duration, setDuration] = useState('10'); + const [aspectRatio, setAspectRatio] = useState('16:9'); + const [style, setStyle] = useState('realistic'); + const [language, setLanguage] = useState('de'); + const [subject, setSubject] = useState(''); + const [topic, setTopic] = useState(''); + const [status, setStatus] = useState('idle'); + const [result, setResult] = useState(null); + const [pollInterval, setPollInterval] = useState(null); + + const handleGenerate = useCallback(async () => { + if (!prompt.trim()) { + toast.error('Bitte gib einen Prompt ein'); + return; + } + setStatus('generating'); + setResult(null); + + try { + const res = await fetch('/api/ai/snapgen', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt, duration: Number(duration), aspectRatio, style, language, subject, topic }), + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Generierung fehlgeschlagen'); + } + + const data = await res.json(); + setResult(data); + + // If video is not ready, poll for status + if (data.taskId && data.status !== 'completed' && !data.videoUrl) { + startPolling(data.taskId, data.provider); + } else if (data.videoUrl) { + setStatus('success'); + } else { + setStatus('generating'); + toast.info('Video wird generiert. Dies kann einige Minuten dauern.'); + } + } catch (error: any) { + setStatus('error'); + toast.error(error.message || 'Fehler bei der Video-Generierung'); + } + }, [prompt, duration, aspectRatio, style, language, subject, topic]); + + const startPolling = (taskId: string, provider: string) => { + if (pollInterval) clearInterval(pollInterval); + + const interval = setInterval(async () => { + try { + const res = await fetch(`/api/ai/snapgen?taskId=${taskId}&provider=${provider}`); + if (res.ok) { + const data = await res.json(); + if (data.videoUrl) { + setResult(prev => ({ ...prev, videoUrl: data.videoUrl, status: 'completed' })); + setStatus('success'); + toast.success('Video erfolgreich generiert!'); + clearInterval(interval); + } else if (data.status === 'failed' || data.status === 'error') { + setStatus('error'); + toast.error('Video-Generierung fehlgeschlagen'); + clearInterval(interval); + } + } + } catch { + // Continue polling + } + }, 5000); + + setPollInterval(interval); + }; + + useEffect(() => { + return () => { + if (pollInterval) clearInterval(pollInterval); + }; + }, [pollInterval]); + + const promptSuggestions = [ + 'Erkläre den Wasserkreislauf mit Animationen', + 'Wie funktionieren Brüche? Visuelle Erklärung', + 'Der Wasserstoff als chemisches Element', + 'Die Französische Revolution - Überblick', + 'Photosynthese einfach erklärt', + 'Teilen mit Rest - Schritt für Schritt', + ]; + + return ( +
+ {/* Input panel */} + + + + + Video-Prompt + + + +
+ +