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
+421
-9
No files matched your search
@@ -1376,6 +1376,10 @@ 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?
|
||||
@@ -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?
|
||||
|
||||
@@ -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(),
|
||||
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(
|
||||
|
||||
@@ -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<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 (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -54,7 +80,7 @@ export default function AIStudioView() {
|
||||
AI Studio
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
<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 [result, setResult] = useState<VideoResult | null>(null);
|
||||
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout | null>(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}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<a href={result.videoUrl} download="ai-video.mp4">
|
||||
<Download className="h-4 w-4 mr-1" /> Herunterladen
|
||||
</a>
|
||||
</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); }}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" /> Neu
|
||||
</Button>
|
||||
@@ -435,6 +466,20 @@ function VideoGenerationTab() {
|
||||
</AnimatePresence>
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -447,6 +492,8 @@ function ImageGenerationTab() {
|
||||
const [status, setStatus] = useState<GenStatus>('idle');
|
||||
const [result, setResult] = useState<ImageResult | null>(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"
|
||||
>
|
||||
<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>
|
||||
<a href={imageSrc} download="ai-image.png">
|
||||
<Download className="h-4 w-4 mr-1" /> Herunterladen
|
||||
</a>
|
||||
</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); }}>
|
||||
{copied ? <Check className="h-4 w-4 mr-1" /> : <Copy className="h-4 w-4 mr-1" />}
|
||||
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||
@@ -615,6 +665,20 @@ function ImageGenerationTab() {
|
||||
</AnimatePresence>
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
</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: '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' },
|
||||
|
||||
Reference in new issue
Block a user