actualizaciones
This commit is contained in:
@@ -21,7 +21,8 @@ import {
|
||||
Brain,
|
||||
Library,
|
||||
BookMarked,
|
||||
ArrowLeft
|
||||
ArrowLeft,
|
||||
ImageIcon
|
||||
} from 'lucide-react';
|
||||
import DescriptionBlock from "@/components/blocks/DescriptionBlock";
|
||||
import MediaBlock from "@/components/blocks/MediaBlock";
|
||||
@@ -78,6 +79,8 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
const [isAIQuizModalOpen, setIsAIQuizModalOpen] = useState(false);
|
||||
const [aiQuizContext, setAiQuizContext] = useState("");
|
||||
const [aiQuizType, setAiQuizType] = useState("multiple-choice");
|
||||
const [aiImagePrompt, setAiImagePrompt] = useState("");
|
||||
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
|
||||
|
||||
const [editValue, setEditValue] = useState("");
|
||||
|
||||
@@ -86,7 +89,10 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
if (lesson && (lesson.transcription_status === 'queued' || lesson.transcription_status === 'processing')) {
|
||||
if (lesson && (
|
||||
lesson.video_generation_status === 'queued' ||
|
||||
lesson.video_generation_status === 'processing'
|
||||
)) {
|
||||
interval = setInterval(async () => {
|
||||
try {
|
||||
const updated = await cmsApi.getLesson(params.lessonId);
|
||||
@@ -362,6 +368,32 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateImage = async () => {
|
||||
if (!lesson) return;
|
||||
setIsGeneratingImage(true);
|
||||
try {
|
||||
// Option 2: Use lesson content (blocks) as script if no prompt
|
||||
const scriptFromBlocks = blocks
|
||||
.map(b => b.content || b.description || b.title || "")
|
||||
.filter(txt => txt.trim().length > 0)
|
||||
.join(". ");
|
||||
|
||||
// Option 3: Prioritize manual prompt, then script
|
||||
const finalPrompt = aiImagePrompt.trim() || scriptFromBlocks;
|
||||
|
||||
const updated = await cmsApi.generateImage(lesson.id, {
|
||||
prompt: finalPrompt
|
||||
});
|
||||
setLesson(updated);
|
||||
setAiImagePrompt("");
|
||||
alert("Generación de imagen iniciada con el prompt/guion detectado.");
|
||||
} catch (err: any) {
|
||||
alert(err.message || "Failed to initiate image generation.");
|
||||
} finally {
|
||||
setIsGeneratingImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="py-20 text-center text-gray-500 animate-pulse font-medium">Initializing Activity Builder...</div>;
|
||||
if (!lesson) return <div className="py-20 text-center text-red-400">Activity not found.</div>;
|
||||
|
||||
@@ -872,6 +904,35 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
<div className="font-black text-xl tracking-tight">{isGeneratingQuiz ? 'Building Quiz...' : 'Generate New Test'}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="col-span-full space-y-3 mt-4">
|
||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
<ImageIcon size={14} className="text-amber-500" />
|
||||
Custom Image Prompt (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={aiImagePrompt}
|
||||
onChange={(e) => setAiImagePrompt(e.target.value)}
|
||||
placeholder="Describe what you want to see in the image, or leave blank to use the lesson script..."
|
||||
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl p-4 text-sm focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 outline-none transition-all min-h-[100px] text-slate-700 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGenerateImage}
|
||||
disabled={isGeneratingImage || lesson.video_generation_status === 'queued' || lesson.video_generation_status === 'processing'}
|
||||
className={`group/btn p-8 border rounded-[2rem] transition-all text-left flex flex-col gap-4 shadow-sm relative overflow-hidden ${isGeneratingImage || lesson.video_generation_status === 'queued' || lesson.video_generation_status === 'processing' ? 'bg-amber-50 dark:bg-amber-500/20 border-amber-200 dark:border-amber-500/50 text-amber-600 dark:text-amber-300 animate-pulse' : 'bg-white dark:bg-white/5 border-slate-100 dark:border-white/10 text-slate-400 dark:text-amber-400 hover:border-amber-500/50 hover:bg-amber-50 hover:text-amber-600 dark:hover:bg-amber-500/10'}`}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-amber-100 dark:bg-amber-500/20 flex items-center justify-center text-amber-600 dark:text-amber-400 shadow-sm group-hover/btn:scale-110 transition-transform">
|
||||
{(isGeneratingImage || lesson.video_generation_status === 'queued' || lesson.video_generation_status === 'processing') ? '⏳' : <ImageIcon />}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.2em] opacity-80">Generative AI</div>
|
||||
<div className="font-black text-xl tracking-tight">
|
||||
{(isGeneratingImage || lesson.video_generation_status === 'queued' || lesson.video_generation_status === 'processing') ? 'Generating Image...' : 'Generate AI Image'}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -281,13 +281,13 @@ export default function StudioDashboard() {
|
||||
title="AI Course Wizard"
|
||||
>
|
||||
<form onSubmit={handleAIGenerate} className="space-y-6">
|
||||
<div className="p-4 rounded-xl bg-indigo-500/5 border border-indigo-500/10 mb-2">
|
||||
<p className="text-xs text-indigo-300 leading-relaxed font-medium">
|
||||
<div className="p-4 rounded-xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 mb-2">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 leading-relaxed font-medium">
|
||||
Describe the course topic and target audience. Our AI will structure the modules and lessons for you in seconds.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
<label className="block text-sm font-bold text-slate-700 dark:text-gray-300 mb-2">
|
||||
Course Topic or Description
|
||||
</label>
|
||||
<textarea
|
||||
@@ -297,7 +297,7 @@ export default function StudioDashboard() {
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
placeholder="e.g. A comprehensive guide to building distributed systems with Rust and Axum for intermediate developers."
|
||||
className="w-full bg-black/5 dark:bg-black/40 border border-black/10 dark:border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all text-gray-900 dark:text-white resize-none"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white resize-none placeholder:text-slate-400 dark:placeholder:text-gray-600"
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
@@ -306,14 +306,14 @@ export default function StudioDashboard() {
|
||||
type="button"
|
||||
onClick={() => setIsAIModalOpen(false)}
|
||||
disabled={isGenerating}
|
||||
className="flex-1 px-4 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all text-sm font-medium"
|
||||
className="flex-1 px-4 py-3 bg-slate-100 dark:bg-white/5 hover:bg-slate-200 dark:hover:bg-white/10 border border-slate-200 dark:border-white/10 rounded-xl transition-all text-sm font-bold text-slate-700 dark:text-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isGenerating}
|
||||
className="flex-[2] px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg transition-all shadow-lg shadow-indigo-500/20 font-bold text-sm flex items-center justify-center gap-2"
|
||||
className="flex-[2] px-4 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl transition-all shadow-lg shadow-blue-500/25 font-bold text-sm flex items-center justify-center gap-2 active:scale-[0.98]"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
|
||||
@@ -32,21 +32,21 @@ export default function Modal({ isOpen, onClose, title, children }: ModalProps)
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-md animate-in fade-in duration-300">
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
className="w-full max-w-md glass-card bg-[#1a1d23] border border-white/10 rounded-2xl p-8 shadow-2xl animate-in zoom-in-95 duration-200"
|
||||
className="w-full max-w-md glass-card bg-white/90 dark:bg-slate-900/90 border border-slate-200 dark:border-white/10 rounded-2xl p-8 shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 id="modal-title" className="text-xl font-bold bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent italic">
|
||||
<h2 id="modal-title" className="text-xl font-bold bg-gradient-to-r from-slate-900 to-slate-600 dark:from-white dark:to-slate-400 bg-clip-text text-transparent">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/5 rounded-full transition-colors text-gray-400 hover:text-white"
|
||||
className="p-2 hover:bg-slate-100 dark:hover:bg-white/5 rounded-full transition-colors text-slate-400 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white"
|
||||
>
|
||||
<X size={20} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface Lesson {
|
||||
cues?: { start: number; end: number; text: string }[];
|
||||
} | null;
|
||||
transcription_status?: 'idle' | 'queued' | 'processing' | 'completed' | 'failed';
|
||||
video_generation_status?: 'idle' | 'queued' | 'processing' | 'completed' | 'failed';
|
||||
is_previewable: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -631,6 +632,7 @@ export const cmsApi = {
|
||||
updateLesson: (id: string, payload: Partial<Lesson>): Promise<Lesson> => apiFetch(`/lessons/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
||||
summarizeLesson: (id: string): Promise<Lesson> => apiFetch(`/lessons/${id}/summarize`, { method: 'POST' }),
|
||||
generateQuiz: (id: string, payload: { context?: string, quiz_type?: string }): Promise<Block[]> => apiFetch(`/lessons/${id}/generate-quiz`, { method: 'POST', body: JSON.stringify(payload) }),
|
||||
generateImage: (id: string, payload: { prompt?: string } = {}): Promise<Lesson> => apiFetch(`/lessons/${id}/generate-image`, { method: 'POST', body: JSON.stringify(payload) }),
|
||||
reviewText: (text: string): Promise<{ suggestion: string, comments: string }> => apiFetch('/api/ai/review-text', { method: 'POST', body: JSON.stringify({ text }) }),
|
||||
deleteModule: (id: string): Promise<void> => apiFetch(`/modules/${id}`, { method: 'DELETE' }),
|
||||
deleteLesson: (id: string): Promise<void> => apiFetch(`/lessons/${id}`, { method: 'DELETE' }),
|
||||
@@ -958,5 +960,6 @@ export interface BackgroundTask {
|
||||
title: string;
|
||||
course_title?: string;
|
||||
transcription_status?: 'idle' | 'queued' | 'processing' | 'failed' | 'completed';
|
||||
video_generation_status?: 'idle' | 'queued' | 'processing' | 'failed' | 'completed';
|
||||
updated_at: string;
|
||||
}
|
||||
Reference in New Issue
Block a user