All 5 files reduced below 500 LOC (hard cap) by extracting sub-components: - training/page.tsx: 780→278 LOC — imports existing _components/, adds BlocksSection - control-provenance/page.tsx: 739→145 LOC — extracts provenance-data.ts, ProvenanceHelpers, LicenseMatrix, SourceRegistry - iace/[projectId]/verification/page.tsx: 673→164 LOC — extracts VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable - training/learner/page.tsx: 560→216 LOC — extracts AssignmentsList, ContentView, QuizView, CertificatesView - ControlDetail.tsx: 878→311 LOC — adds ControlSourceCitation, ControlTraceability, ControlRegulatorySection, ControlSimilarControls, ControlReviewActions siblings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
182 lines
7.7 KiB
TypeScript
182 lines
7.7 KiB
TypeScript
'use client'
|
|
|
|
import type { TrainingModule, ModuleContent, TrainingMedia } from '@/lib/sdk/training/types'
|
|
import AudioPlayer from '@/components/training/AudioPlayer'
|
|
import VideoPlayer from '@/components/training/VideoPlayer'
|
|
import ScriptPreview from '@/components/training/ScriptPreview'
|
|
|
|
interface ContentTabProps {
|
|
modules: TrainingModule[]
|
|
selectedModuleId: string
|
|
onSelectModule: (id: string) => void
|
|
generatedContent: ModuleContent | null
|
|
generating: boolean
|
|
bulkGenerating: boolean
|
|
bulkResult: { generated: number; skipped: number; errors: string[] } | null
|
|
moduleMedia: TrainingMedia[]
|
|
interactiveGenerating?: boolean
|
|
onGenerateContent: () => void
|
|
onGenerateQuiz: () => void
|
|
onBulkContent: () => void
|
|
onBulkQuiz: () => void
|
|
onPublishContent: (contentId: string) => void
|
|
onReloadMedia: () => void
|
|
onGenerateInteractiveVideo?: () => void
|
|
}
|
|
|
|
export function ContentTab({
|
|
modules,
|
|
selectedModuleId,
|
|
onSelectModule,
|
|
generatedContent,
|
|
generating,
|
|
bulkGenerating,
|
|
bulkResult,
|
|
moduleMedia,
|
|
interactiveGenerating,
|
|
onGenerateContent,
|
|
onGenerateQuiz,
|
|
onBulkContent,
|
|
onBulkQuiz,
|
|
onPublishContent,
|
|
onReloadMedia,
|
|
onGenerateInteractiveVideo,
|
|
}: ContentTabProps) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Bulk Generation */}
|
|
<div className="bg-white border rounded-lg p-4">
|
|
<h3 className="text-sm font-medium text-gray-700 mb-3">Bulk-Generierung</h3>
|
|
<p className="text-xs text-gray-500 mb-4">Generiere Inhalte und Quiz-Fragen fuer alle Module auf einmal</p>
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={onBulkContent}
|
|
disabled={bulkGenerating}
|
|
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
|
>
|
|
{bulkGenerating ? 'Generiere...' : 'Alle Inhalte generieren'}
|
|
</button>
|
|
<button
|
|
onClick={onBulkQuiz}
|
|
disabled={bulkGenerating}
|
|
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
|
|
>
|
|
{bulkGenerating ? 'Generiere...' : 'Alle Quizfragen generieren'}
|
|
</button>
|
|
</div>
|
|
{bulkResult && (
|
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-sm">
|
|
<div className="flex gap-6">
|
|
<span className="text-green-700">Generiert: {bulkResult.generated}</span>
|
|
<span className="text-gray-500">Uebersprungen: {bulkResult.skipped}</span>
|
|
{bulkResult.errors?.length > 0 && (
|
|
<span className="text-red-600">Fehler: {bulkResult.errors.length}</span>
|
|
)}
|
|
</div>
|
|
{bulkResult.errors?.length > 0 && (
|
|
<div className="mt-2 text-xs text-red-600">
|
|
{bulkResult.errors.map((err, i) => <div key={i}>{err}</div>)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-white border rounded-lg p-4">
|
|
<h3 className="text-sm font-medium text-gray-700 mb-3">LLM-Content-Generator</h3>
|
|
<p className="text-xs text-gray-500 mb-4">Generiere Schulungsinhalte und Quiz-Fragen automatisch via KI</p>
|
|
<div className="flex gap-3 items-end">
|
|
<div className="flex-1">
|
|
<label className="text-xs text-gray-600 block mb-1">Modul auswaehlen</label>
|
|
<select
|
|
value={selectedModuleId}
|
|
onChange={e => onSelectModule(e.target.value)}
|
|
className="w-full px-3 py-2 text-sm border rounded-lg bg-white"
|
|
>
|
|
<option value="">Modul waehlen...</option>
|
|
{modules.map(m => <option key={m.id} value={m.id}>{m.module_code} - {m.title}</option>)}
|
|
</select>
|
|
</div>
|
|
<button onClick={onGenerateContent} disabled={!selectedModuleId || generating} className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
|
{generating ? 'Generiere...' : 'Inhalt generieren'}
|
|
</button>
|
|
<button onClick={onGenerateQuiz} disabled={!selectedModuleId || generating} className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
|
|
{generating ? 'Generiere...' : 'Quiz generieren'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{generatedContent && (
|
|
<div className="bg-white border rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-gray-700">Generierter Inhalt (v{generatedContent.version})</h3>
|
|
<p className="text-xs text-gray-500">Generiert von: {generatedContent.generated_by} ({generatedContent.llm_model})</p>
|
|
</div>
|
|
{!generatedContent.is_published ? (
|
|
<button onClick={() => onPublishContent(generatedContent.id)} className="px-3 py-1.5 text-xs bg-green-600 text-white rounded hover:bg-green-700">Veroeffentlichen</button>
|
|
) : (
|
|
<span className="px-3 py-1.5 text-xs bg-green-100 text-green-700 rounded">Veroeffentlicht</span>
|
|
)}
|
|
</div>
|
|
<div className="prose prose-sm max-w-none border rounded p-4 bg-gray-50 max-h-96 overflow-y-auto">
|
|
<pre className="whitespace-pre-wrap text-sm text-gray-800">{generatedContent.content_body}</pre>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Audio Player */}
|
|
{selectedModuleId && generatedContent?.is_published && (
|
|
<AudioPlayer
|
|
moduleId={selectedModuleId}
|
|
audio={moduleMedia.find(m => m.media_type === 'audio') || null}
|
|
onMediaUpdate={onReloadMedia}
|
|
/>
|
|
)}
|
|
|
|
{/* Video Player */}
|
|
{selectedModuleId && generatedContent?.is_published && (
|
|
<VideoPlayer
|
|
moduleId={selectedModuleId}
|
|
video={moduleMedia.find(m => m.media_type === 'video') || null}
|
|
onMediaUpdate={onReloadMedia}
|
|
/>
|
|
)}
|
|
|
|
{/* Interactive Video */}
|
|
{selectedModuleId && generatedContent?.is_published && onGenerateInteractiveVideo && (
|
|
<div className="bg-white border rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-gray-700">Interaktives Video</h3>
|
|
<p className="text-xs text-gray-500">Video mit Narrator-Persona und Checkpoint-Quizzes</p>
|
|
</div>
|
|
{moduleMedia.some(m => m.media_type === 'interactive_video' && m.status === 'completed') ? (
|
|
<span className="px-3 py-1.5 text-xs bg-purple-100 text-purple-700 rounded-full">Interaktiv erstellt</span>
|
|
) : (
|
|
<button
|
|
onClick={onGenerateInteractiveVideo}
|
|
disabled={interactiveGenerating}
|
|
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
{interactiveGenerating ? 'Generiere interaktives Video...' : 'Interaktives Video generieren'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{moduleMedia.filter(m => m.media_type === 'interactive_video' && m.status === 'completed').map(m => (
|
|
<div key={m.id} className="text-xs text-gray-500 space-y-1 bg-gray-50 rounded p-3">
|
|
<p>Dauer: {Math.round(m.duration_seconds / 60)} Min | Groesse: {(m.file_size_bytes / 1024 / 1024).toFixed(1)} MB</p>
|
|
<p>Generiert: {new Date(m.created_at).toLocaleString('de-DE')}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Script Preview */}
|
|
{selectedModuleId && generatedContent?.is_published && (
|
|
<ScriptPreview moduleId={selectedModuleId} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|