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:
Z User committed 2026-08-10 16:47:05 +00:00
1 parent 763ebef22b
commit fe3eb4a272
6 files changed
+422 -10

No files matched your search

+13 -1
View File
@@ -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?
+85
View File
@@ -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 }
);
}
}
+96
View File
@@ -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(
+220 -7
View File
@@ -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>
);
}
-2
View File
@@ -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' },