diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 179013e..dece4fc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1376,9 +1376,13 @@ model Homework { attachments String? // JSON array of attachment URLs isPublished Boolean @default(true) isDemo Boolean @default(false) + // AI-generated content linked from SubjectLesson + aiLessonId String? // link to SubjectLesson with AI content + aiVideoUrl String? // direct AI video URL for this homework + aiImageUrl String? // direct AI image URL for this homework createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime? + deletedAt DateTime? school School @relation(fields: [schoolId], references: [id]) classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) @@ -2181,6 +2185,14 @@ model SubjectLesson { sortOrder Int @default(0) estimatedMinutes Int? // estimated time to complete isDemo Boolean @default(false) + // AI-generated content (created by teachers/admins) + aiVideoUrl String? // AI-generated explanatory video URL + aiImageUrl String? // AI-generated illustration image URL + aiThumbnailUrl String? // Video thumbnail + aiPrompt String? // Original prompt used for generation + aiProvider String? // "snapgen" | "perchance" | "zai" + aiGeneratedAt DateTime? + aiGeneratedBy String? // userId of teacher/admin who generated createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? diff --git a/src/app/api/ai/agent/route.ts b/src/app/api/ai/agent/route.ts new file mode 100644 index 0000000..ef804db --- /dev/null +++ b/src/app/api/ai/agent/route.ts @@ -0,0 +1,85 @@ +// @ts-nocheck +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { getSession } from '@/lib/auth'; +import ZAI from 'z-ai-web-dev-sdk'; + +/** + * AI Agent Chat API (Agentic Mode) + * POST /api/ai/agent + * Body: { message, context, language, conversationHistory } + * + * Only teachers and admins can use the agentic AI. + */ + +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 use AI agent + const role = session.user.role; + if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER' && role !== 'VICE_PRINCIPAL') { + return NextResponse.json( + { error: 'Only teachers and admins can use the AI agent' }, + { status: 403 } + ); + } + + const { schoolId } = session.user; + if (!schoolId) { + return NextResponse.json({ error: 'No school assigned' }, { status: 403 }); + } + + const body = await request.json(); + const { message, context = 'teacher-assistant', language = 'de', conversationHistory = [] } = body; + + if (!message || typeof message !== 'string') { + return NextResponse.json({ error: 'Message is required' }, { status: 400 }); + } + + // Build system prompt based on context + const systemPrompts: Record = { + 'teacher-assistant': `Du bist ein KI-Assistent für Lehrkräfte an einer deutschen Schule. +Du hilfst bei Unterrichtsvorbereitung, Bewertungsvorschlägen, Kompetenzanalyse und Differenzierung. +Du kennst das deutsche Schulsystem (Grundschule, Gymnasium) und die kompetenzorientierte Beurteilung. +Antworte auf Deutsch, es sei denn es wird explizit anders verlangt. +Sei konkret, praktisch und praxisnah.`, + 'lesson-planner': `Du bist ein Experte für Unterrichtsplanung nach dem kompetenzorientierten Ansatz. +Du erstellst strukturierte Unterrichtsentwürfe mit Phasen, Lernzielen und Differenzierung.`, + 'assessment-helper': `Du bist ein Experte für Leistungsbewertung. +Du hilfst bei der Erstellung von Bewertungsrastern, Kompetenzchecklisten und Notenbegründungen.`, + }; + + const systemPrompt = systemPrompts[context] || systemPrompts['teacher-assistant']; + + // Build messages array + const messages = [ + { role: 'system', content: systemPrompt }, + ...conversationHistory.map((msg: any) => ({ + role: msg.role, + content: msg.content, + })), + { role: 'user', content: message }, + ]; + + // Generate using z-ai-web-dev-sdk + const zai = await ZAI.create(); + const response = await zai.chat.completions.create({ + messages, + thinking: { type: 'disabled' }, + }); + + const responseContent = response.choices?.[0]?.message?.content || 'Ich konnte keine Antwort generieren.'; + + return NextResponse.json({ response: responseContent }); + } catch (error) { + console.error('AI Agent error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ai/lesson-plan/route.ts b/src/app/api/ai/lesson-plan/route.ts new file mode 100644 index 0000000..aac82f7 --- /dev/null +++ b/src/app/api/ai/lesson-plan/route.ts @@ -0,0 +1,96 @@ +// @ts-nocheck +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { getSession } from '@/lib/auth'; +import ZAI from 'z-ai-web-dev-sdk'; + +/** + * AI Lesson Plan Generation API + * POST /api/ai/lesson-plan + * Body: { subject, topic, classLevel, duration, language } + * + * Only teachers and admins can generate lesson plans. + */ + +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 lesson plans + const role = session.user.role; + if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER' && role !== 'VICE_PRINCIPAL') { + return NextResponse.json( + { error: 'Only teachers and admins can generate lesson plans' }, + { 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) { + return NextResponse.json( + { error: 'AI settings not configured' }, + { status: 403 } + ); + } + + const body = await request.json(); + const { subject, topic, classLevel, duration = 45, language = 'de' } = body; + + if (!topic || typeof topic !== 'string') { + return NextResponse.json({ error: 'Topic is required' }, { status: 400 }); + } + + // Build the lesson plan prompt + const langText = language === 'de' ? 'Deutsch' : language === 'en' ? 'Englisch' : 'Türkçe'; + const prompt = `Erstelle einen detaillierten Unterrichtsentwurf auf ${langText}. + +Fach: ${subject || 'Allgemein'} +Thema: ${topic} +Klassenstufe: ${classLevel || '3'} +Dauer: ${duration} Minuten + +Struktur: +1. Lernziele (3-5 Ziele) +2. Einstieg/Motivation (5-10 Min.) +3. Erarbeitungsphase (15-20 Min.) +4. Übungsphase (10-15 Min.) +5. Sicherung/Reflexion (5-10 Min.) +6. Differenzierungsmöglichkeiten +7. Materialien/Medien +8. Hausaufgabe + +Bitte strukturiere die Antwort mit klaren Überschriften und Bulletpoints.`; + + // Generate using z-ai-web-dev-sdk + const zai = await ZAI.create(); + const response = await zai.chat.completions.create({ + messages: [ + { role: 'system', content: 'Du bist ein erfahrener Lehrer und Unterrichtsplaner. Du erstellst detaillierte, praxisnahe Unterrichtsentwürfe.' }, + { role: 'user', content: prompt }, + ], + thinking: { type: 'disabled' }, + }); + + const content = response.choices?.[0]?.message?.content || 'Keine Antwort generiert.'; + + return NextResponse.json({ content }); + } catch (error) { + console.error('AI Lesson Plan error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/subject-lessons/[id]/route.ts b/src/app/api/subject-lessons/[id]/route.ts index 17d80bd..6718373 100755 --- a/src/app/api/subject-lessons/[id]/route.ts +++ b/src/app/api/subject-lessons/[id]/route.ts @@ -70,6 +70,14 @@ const updateLessonSchema = z.object({ difficulty: z.enum(['easy', 'medium', 'hard']).optional(), sortOrder: z.number().int().optional(), estimatedMinutes: z.number().int().optional().nullable(), + // AI-generated content fields + aiVideoUrl: z.string().optional().nullable(), + aiImageUrl: z.string().optional().nullable(), + aiThumbnailUrl: z.string().optional().nullable(), + aiPrompt: z.string().optional().nullable(), + aiProvider: z.string().optional().nullable(), + aiGeneratedAt: z.date().optional().nullable(), + aiGeneratedBy: z.string().optional().nullable(), }); async function updateLesson( diff --git a/src/components/ai-studio-view.tsx b/src/components/ai-studio-view.tsx index e4f8152..7a87167 100644 --- a/src/components/ai-studio-view.tsx +++ b/src/components/ai-studio-view.tsx @@ -7,6 +7,7 @@ import { Loader2, Download, Play, RefreshCw, Copy, Check, Film, Wand2, BookOpen, Lightbulb, GraduationCap, Clock, CheckCircle2, AlertCircle, Settings, + Save, Link2, BookMarked, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -16,8 +17,10 @@ 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; +import { apiGet, apiPost, apiPut } from '@/lib/api'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; @@ -44,6 +47,29 @@ export default function AIStudioView() { const { currentUser } = useAppStore(); const [activeTab, setActiveTab] = useState('video'); + // Access control: Only teachers and admins can use AI Studio + const role = currentUser?.role; + const canAccess = role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'TEACHER' || role === 'VICE_PRINCIPAL'; + + if (!canAccess) { + return ( +
+
+ +
+

Kein Zugriff

+

+ Das AI Studio ist nur für Lehrkräfte und die Schulleitung verfügbar. + Schüler und Eltern haben keinen Zugriff auf diese Funktion. +

+

+ AI-generated content (videos, images) wird von Lehrkräften erstellt + und kann dann in Hausaufgaben und Unterrichtsmaterialien betrachtet werden. +

+
+ ); + } + return (
{/* Header */} @@ -54,7 +80,7 @@ export default function AIStudioView() { AI Studio

- KI-gestützte Content-Erstellung für den Unterricht + KI-gestützte Content-Erstellung für den Unterricht — nur für Lehrkräfte & Administration

@@ -143,6 +169,8 @@ function VideoGenerationTab() { const [status, setStatus] = useState('idle'); const [result, setResult] = useState(null); const [pollInterval, setPollInterval] = useState(null); + const [saveDialogOpen, setSaveDialogOpen] = useState(false); + const { currentUser } = useAppStore(); const handleGenerate = useCallback(async () => { if (!prompt.trim()) { @@ -404,12 +432,15 @@ function VideoGenerationTab() { className="w-full rounded-lg border" poster={result.thumbnailUrl || undefined} /> -
+
+ @@ -435,6 +466,20 @@ function VideoGenerationTab() { + + {/* Save to Lesson Dialog */} + {result?.videoUrl && currentUser && ( + + )}
); } @@ -447,6 +492,8 @@ function ImageGenerationTab() { const [status, setStatus] = useState('idle'); const [result, setResult] = useState(null); const [copied, setCopied] = useState(false); + const [saveDialogOpen, setSaveDialogOpen] = useState(false); + const { currentUser } = useAppStore(); const handleGenerate = useCallback(async () => { if (!prompt.trim()) { @@ -587,12 +634,15 @@ function ImageGenerationTab() { className="space-y-3" > {prompt} -
+
+
); } @@ -637,11 +701,10 @@ function LessonPlanTab() { setStatus('generating'); try { - const res = await fetch('/api/ai', { + const res = await fetch('/api/ai/lesson-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - action: 'lesson-plan', subject, topic, classLevel, @@ -778,14 +841,14 @@ function AgentChatTab() { setLoading(true); try { - const res = await fetch('/api/ai', { + const res = await fetch('/api/ai/agent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - action: 'chat', message: input, context: 'teacher-assistant', language: 'de', + conversationHistory: messages.map(m => ({ role: m.role, content: m.content })), }), }); @@ -893,3 +956,153 @@ function AgentChatTab() { ); } + +// ─── Save to Lesson Dialog ───────────────────────────────────────── + +interface SaveToLessonDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + contentType: 'video' | 'image'; + contentUrl: string; + prompt: string; + provider: string; + userId: string; + schoolId: string; +} + +function SaveToLessonDialog({ open, onOpenChange, contentType, contentUrl, prompt, provider, userId, schoolId }: SaveToLessonDialogProps) { + const [subjects, setSubjects] = useState([]); + const [topics, setTopics] = useState([]); + const [lessons, setLessons] = useState([]); + const [selectedSubject, setSelectedSubject] = useState(''); + const [selectedTopic, setSelectedTopic] = useState(''); + const [selectedLesson, setSelectedLesson] = useState(''); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + // Fetch subjects + useEffect(() => { + if (open && schoolId) { + apiGet(`/api/subjects?schoolId=${schoolId}`).then(data => { + setSubjects(data || []); + }).catch(() => {}); + } + }, [open, schoolId]); + + // Fetch topics when subject changes + useEffect(() => { + if (selectedSubject) { + apiGet(`/api/subject-topics?subjectId=${selectedSubject}&schoolId=${schoolId}`).then(data => { + setTopics(data || []); + }).catch(() => {}); + setSelectedTopic(''); + setLessons([]); + } + }, [selectedSubject, schoolId]); + + // Fetch lessons when topic changes + useEffect(() => { + if (selectedTopic) { + apiGet(`/api/subject-lessons?topicId=${selectedTopic}`).then(data => { + setLessons(data || []); + }).catch(() => {}); + setSelectedLesson(''); + } + }, [selectedTopic]); + + const handleSave = async () => { + if (!selectedLesson) { + toast.error('Bitte wähle eine Lektion aus'); + return; + } + setSaving(true); + try { + const updateData: any = { + aiPrompt: prompt, + aiProvider: provider, + aiGeneratedAt: new Date().toISOString(), + aiGeneratedBy: userId, + }; + + if (contentType === 'video') { + updateData.aiVideoUrl = contentUrl; + } else { + updateData.aiImageUrl = contentUrl; + } + + await apiPut(`/api/subject-lessons/${selectedLesson}`, updateData); + toast.success(contentType === 'video' ? 'Video zur Lektion gespeichert!' : 'Bild zur Lektion gespeichert!'); + onOpenChange(false); + } catch (error: any) { + toast.error('Fehler beim Speichern: ' + (error.message || 'Unbekannt')); + } finally { + setSaving(false); + } + }; + + return ( + + + + Zur Lektion speichern + Save AI content to a subject lesson + +
+
+ + +
+ {selectedSubject && ( +
+ + +
+ )} + {selectedTopic && ( +
+ + +
+ )} + {selectedLesson && ( +
+

Vorschau:

+

+ {contentType === 'video' ? 'Video' : 'Bild'} wird mit der Lektion verknüpft. + Schüler können es in Hausaufgaben und Unterrichtsmaterialien sehen. +

+
+ )} +
+ + + + +
+
+ ); +} diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx index 372f452..99f9583 100644 --- a/src/components/app-layout.tsx +++ b/src/components/app-layout.tsx @@ -287,7 +287,6 @@ const navSections: NavSection[] = [ { key: 'reports', icon: FileText, labelKey: 'nav.reports' }, { key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' }, { key: 'ai-studio', icon: Sparkles, labelKey: 'nav.ai-studio' }, - { key: 'ai-studio', icon: Sparkles, labelKey: 'nav.ai-studio' }, { key: 'data-import-export', icon: Database, labelKey: 'nav.data-import-export' }, ], }, @@ -324,7 +323,6 @@ const studentNavSections: NavSection[] = [ { key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' }, { key: 'counseling', icon: Heart, labelKey: 'counseling.title' }, { key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' }, - { key: 'ai-studio', icon: Sparkles, labelKey: 'nav.ai-studio' }, { key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' }, { key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' }, { key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },