Restrict AI Studio to teachers/admins only + Save to Lesson feature
Access Control: - AI Studio view checks user role: only SCHOOL_ADMIN, SUPER_ADMIN, TEACHER, VICE_PRINCIPAL - Students and parents see 'Kein Zugriff' (No Access) message - AI Studio removed from student navigation - API routes (/api/ai/snapgen, /api/ai/perchance, /api/ai/lesson-plan, /api/ai/agent) enforce role checks New Features: - 'Save to Lesson' dialog in Video and Image generation tabs - Pick Subject > Topic > Lesson from existing curriculum - Saves AI video/image URL to SubjectLesson record - Stores prompt, provider, timestamp, and teacher who generated - Students will see this content in homework and lesson materials - AI Lesson Plan API route (/api/ai/lesson-plan) - AI Agent Chat API route (/api/ai/agent) with context-aware system prompts - SubjectLesson model: Added aiVideoUrl, aiImageUrl, aiThumbnailUrl, aiPrompt, aiProvider, aiGeneratedAt, aiGeneratedBy - Homework model: Added aiLessonId, aiVideoUrl, aiImageUrl for linking AI content Navigation: - Removed duplicate AI Studio entry from teacher nav - Removed AI Studio from student nav (students can't access)
This commit is contained in:
1 parent
763ebef22b
commit
fe3eb4a272
6 files changed
+422
-10
No files matched your search
+13
-1
@@ -1376,9 +1376,13 @@ model Homework {
|
|||||||
attachments String? // JSON array of attachment URLs
|
attachments String? // JSON array of attachment URLs
|
||||||
isPublished Boolean @default(true)
|
isPublished Boolean @default(true)
|
||||||
isDemo Boolean @default(false)
|
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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
|
|
||||||
school School @relation(fields: [schoolId], references: [id])
|
school School @relation(fields: [schoolId], references: [id])
|
||||||
classGroup ClassGroup @relation(fields: [classGroupId], references: [id])
|
classGroup ClassGroup @relation(fields: [classGroupId], references: [id])
|
||||||
@@ -2181,6 +2185,14 @@ model SubjectLesson {
|
|||||||
sortOrder Int @default(0)
|
sortOrder Int @default(0)
|
||||||
estimatedMinutes Int? // estimated time to complete
|
estimatedMinutes Int? // estimated time to complete
|
||||||
isDemo Boolean @default(false)
|
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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
'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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,14 @@ const updateLessonSchema = z.object({
|
|||||||
difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
|
difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
|
||||||
sortOrder: z.number().int().optional(),
|
sortOrder: z.number().int().optional(),
|
||||||
estimatedMinutes: z.number().int().optional().nullable(),
|
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(
|
async function updateLesson(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Loader2, Download, Play, RefreshCw, Copy, Check,
|
Loader2, Download, Play, RefreshCw, Copy, Check,
|
||||||
Film, Wand2, BookOpen, Lightbulb, GraduationCap,
|
Film, Wand2, BookOpen, Lightbulb, GraduationCap,
|
||||||
Clock, CheckCircle2, AlertCircle, Settings,
|
Clock, CheckCircle2, AlertCircle, Settings,
|
||||||
|
Save, Link2, BookMarked,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -16,8 +17,10 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
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 { useAppStore } from '@/lib/store';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
|
import { apiGet, apiPost, apiPut } from '@/lib/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -44,6 +47,29 @@ export default function AIStudioView() {
|
|||||||
const { currentUser } = useAppStore();
|
const { currentUser } = useAppStore();
|
||||||
const [activeTab, setActiveTab] = useState<AITab>('video');
|
const [activeTab, setActiveTab] = useState<AITab>('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 (
|
||||||
|
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||||
|
<div className="rounded-2xl bg-rose-50 dark:bg-rose-950/30 p-6 mb-4">
|
||||||
|
<AlertCircle className="h-12 w-12 text-rose-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold">Kein Zugriff</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2 max-w-md">
|
||||||
|
Das AI Studio ist nur für Lehrkräfte und die Schulleitung verfügbar.
|
||||||
|
Schüler und Eltern haben keinen Zugriff auf diese Funktion.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-4">
|
||||||
|
AI-generated content (videos, images) wird von Lehrkräften erstellt
|
||||||
|
und kann dann in Hausaufgaben und Unterrichtsmaterialien betrachtet werden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -54,7 +80,7 @@ export default function AIStudioView() {
|
|||||||
AI Studio
|
AI Studio
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
KI-gestützte Content-Erstellung für den Unterricht
|
KI-gestützte Content-Erstellung für den Unterricht — nur für Lehrkräfte & Administration
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="bg-emerald-50 dark:bg-emerald-950/30 text-emerald-700 dark:text-emerald-400">
|
<Badge variant="outline" className="bg-emerald-50 dark:bg-emerald-950/30 text-emerald-700 dark:text-emerald-400">
|
||||||
@@ -143,6 +169,8 @@ function VideoGenerationTab() {
|
|||||||
const [status, setStatus] = useState<GenStatus>('idle');
|
const [status, setStatus] = useState<GenStatus>('idle');
|
||||||
const [result, setResult] = useState<VideoResult | null>(null);
|
const [result, setResult] = useState<VideoResult | null>(null);
|
||||||
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout | null>(null);
|
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||||
|
const { currentUser } = useAppStore();
|
||||||
|
|
||||||
const handleGenerate = useCallback(async () => {
|
const handleGenerate = useCallback(async () => {
|
||||||
if (!prompt.trim()) {
|
if (!prompt.trim()) {
|
||||||
@@ -404,12 +432,15 @@ function VideoGenerationTab() {
|
|||||||
className="w-full rounded-lg border"
|
className="w-full rounded-lg border"
|
||||||
poster={result.thumbnailUrl || undefined}
|
poster={result.thumbnailUrl || undefined}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Button size="sm" variant="outline" asChild>
|
<Button size="sm" variant="outline" asChild>
|
||||||
<a href={result.videoUrl} download="ai-video.mp4">
|
<a href={result.videoUrl} download="ai-video.mp4">
|
||||||
<Download className="h-4 w-4 mr-1" /> Herunterladen
|
<Download className="h-4 w-4 mr-1" /> Herunterladen
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="sm" variant="default" onClick={() => setSaveDialogOpen(true)}>
|
||||||
|
<Save className="h-4 w-4 mr-1" /> Zur Lektion speichern
|
||||||
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => { setStatus('idle'); setResult(null); }}>
|
<Button size="sm" variant="outline" onClick={() => { setStatus('idle'); setResult(null); }}>
|
||||||
<RefreshCw className="h-4 w-4 mr-1" /> Neu
|
<RefreshCw className="h-4 w-4 mr-1" /> Neu
|
||||||
</Button>
|
</Button>
|
||||||
@@ -435,6 +466,20 @@ function VideoGenerationTab() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Save to Lesson Dialog */}
|
||||||
|
{result?.videoUrl && currentUser && (
|
||||||
|
<SaveToLessonDialog
|
||||||
|
open={saveDialogOpen}
|
||||||
|
onOpenChange={setSaveDialogOpen}
|
||||||
|
contentType="video"
|
||||||
|
contentUrl={result.videoUrl}
|
||||||
|
prompt={prompt}
|
||||||
|
provider={result.provider || 'zai'}
|
||||||
|
userId={currentUser.id}
|
||||||
|
schoolId={currentUser.schoolId || ''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -447,6 +492,8 @@ function ImageGenerationTab() {
|
|||||||
const [status, setStatus] = useState<GenStatus>('idle');
|
const [status, setStatus] = useState<GenStatus>('idle');
|
||||||
const [result, setResult] = useState<ImageResult | null>(null);
|
const [result, setResult] = useState<ImageResult | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||||
|
const { currentUser } = useAppStore();
|
||||||
|
|
||||||
const handleGenerate = useCallback(async () => {
|
const handleGenerate = useCallback(async () => {
|
||||||
if (!prompt.trim()) {
|
if (!prompt.trim()) {
|
||||||
@@ -587,12 +634,15 @@ function ImageGenerationTab() {
|
|||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
>
|
>
|
||||||
<img src={imageSrc} alt={prompt} className="w-full rounded-lg border" />
|
<img src={imageSrc} alt={prompt} className="w-full rounded-lg border" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Button size="sm" variant="outline" asChild>
|
<Button size="sm" variant="outline" asChild>
|
||||||
<a href={imageSrc} download="ai-image.png">
|
<a href={imageSrc} download="ai-image.png">
|
||||||
<Download className="h-4 w-4 mr-1" /> Herunterladen
|
<Download className="h-4 w-4 mr-1" /> Herunterladen
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="sm" variant="default" onClick={() => setSaveDialogOpen(true)}>
|
||||||
|
<Save className="h-4 w-4 mr-1" /> Zur Lektion speichern
|
||||||
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => { navigator.clipboard.writeText(imageSrc); setCopied(true); setTimeout(() => setCopied(false), 2000); }}>
|
<Button size="sm" variant="outline" onClick={() => { navigator.clipboard.writeText(imageSrc); setCopied(true); setTimeout(() => setCopied(false), 2000); }}>
|
||||||
{copied ? <Check className="h-4 w-4 mr-1" /> : <Copy className="h-4 w-4 mr-1" />}
|
{copied ? <Check className="h-4 w-4 mr-1" /> : <Copy className="h-4 w-4 mr-1" />}
|
||||||
{copied ? 'Kopiert!' : 'Kopieren'}
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
@@ -615,6 +665,20 @@ function ImageGenerationTab() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Save to Lesson Dialog */}
|
||||||
|
{imageSrc && currentUser && (
|
||||||
|
<SaveToLessonDialog
|
||||||
|
open={saveDialogOpen}
|
||||||
|
onOpenChange={setSaveDialogOpen}
|
||||||
|
contentType="image"
|
||||||
|
contentUrl={imageSrc}
|
||||||
|
prompt={prompt}
|
||||||
|
provider={result?.provider || 'zai'}
|
||||||
|
userId={currentUser.id}
|
||||||
|
schoolId={currentUser.schoolId || ''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -637,11 +701,10 @@ function LessonPlanTab() {
|
|||||||
setStatus('generating');
|
setStatus('generating');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ai', {
|
const res = await fetch('/api/ai/lesson-plan', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'lesson-plan',
|
|
||||||
subject,
|
subject,
|
||||||
topic,
|
topic,
|
||||||
classLevel,
|
classLevel,
|
||||||
@@ -778,14 +841,14 @@ function AgentChatTab() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ai', {
|
const res = await fetch('/api/ai/agent', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'chat',
|
|
||||||
message: input,
|
message: input,
|
||||||
context: 'teacher-assistant',
|
context: 'teacher-assistant',
|
||||||
language: 'de',
|
language: 'de',
|
||||||
|
conversationHistory: messages.map(m => ({ role: m.role, content: m.content })),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -893,3 +956,153 @@ function AgentChatTab() {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 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<any[]>([]);
|
||||||
|
const [topics, setTopics] = useState<any[]>([]);
|
||||||
|
const [lessons, setLessons] = useState<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Zur Lektion speichern</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">Save AI content to a subject lesson</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm">Fach</Label>
|
||||||
|
<Select value={selectedSubject} onValueChange={setSelectedSubject}>
|
||||||
|
<SelectTrigger className="mt-1.5"><SelectValue placeholder="Fach wählen..." /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{subjects.map((s: any) => (
|
||||||
|
<SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{selectedSubject && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm">Thema</Label>
|
||||||
|
<Select value={selectedTopic} onValueChange={setSelectedTopic}>
|
||||||
|
<SelectTrigger className="mt-1.5"><SelectValue placeholder="Thema wählen..." /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{topics.map((t: any) => (
|
||||||
|
<SelectItem key={t.id} value={t.id}>{t.title}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedTopic && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm">Lektion</Label>
|
||||||
|
<Select value={selectedLesson} onValueChange={setSelectedLesson}>
|
||||||
|
<SelectTrigger className="mt-1.5"><SelectValue placeholder="Lektion wählen..." /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{lessons.map((l: any) => (
|
||||||
|
<SelectItem key={l.id} value={l.id}>{l.title}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedLesson && (
|
||||||
|
<div className="rounded-lg bg-muted p-3 text-xs">
|
||||||
|
<p className="font-medium mb-1">Vorschau:</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{contentType === 'video' ? 'Video' : 'Bild'} wird mit der Lektion verknüpft.
|
||||||
|
Schüler können es in Hausaufgaben und Unterrichtsmaterialien sehen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>Abbrechen</Button>
|
||||||
|
<Button onClick={handleSave} disabled={!selectedLesson || saving}>
|
||||||
|
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -287,7 +287,6 @@ const navSections: NavSection[] = [
|
|||||||
{ key: 'reports', icon: FileText, labelKey: 'nav.reports' },
|
{ key: 'reports', icon: FileText, labelKey: 'nav.reports' },
|
||||||
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' },
|
{ 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: 'ai-studio', icon: Sparkles, labelKey: 'nav.ai-studio' },
|
|
||||||
{ key: 'data-import-export', icon: Database, labelKey: 'nav.data-import-export' },
|
{ 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: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
||||||
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
|
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
|
||||||
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.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: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
|
||||||
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
|
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
|
||||||
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
||||||
|
|||||||
Reference in new issue
Block a user