feat: Introduce a VideoMarkerBlock component for interactive video questions, integrating it into the lesson editor and updating API types.
This commit is contained in:
+1
-1
@@ -130,7 +130,7 @@
|
|||||||
- [x] **Quices de Código**: Desafíos interactivos con reproductor tipo IDE (Completado)
|
- [x] **Quices de Código**: Desafíos interactivos con reproductor tipo IDE (Completado)
|
||||||
- [x] **Identificación Visual**: Quices de "Puntos Calientes" (Hotspots) en imágenes (Completado)
|
- [x] **Identificación Visual**: Quices de "Puntos Calientes" (Hotspots) en imágenes (Completado)
|
||||||
- [ ] **Tutor de IA Integrado**: Asistente basado en RAG dentro del reproductor de lecciones
|
- [ ] **Tutor de IA Integrado**: Asistente basado en RAG dentro del reproductor de lecciones
|
||||||
- [ ] **Evaluaciones por Audio**: Preguntas con respuesta oral para idiomas
|
- [x] **Evaluaciones por Audio**: Preguntas con respuesta oral para idiomas (Completado)
|
||||||
- [x] **Marcadores de Video**: Preguntas que pausan el video en timestamps específicos (Completado)
|
- [x] **Marcadores de Video**: Preguntas que pausan el video en timestamps específicos (Completado)
|
||||||
|
|
||||||
## Fase 12: Generador de Cursos "Mágico" con IA ✅
|
## Fase 12: Generador de Cursos "Mágico" con IA ✅
|
||||||
|
|||||||
Generated
+1
-14
@@ -593,7 +593,6 @@
|
|||||||
"version": "18.3.27",
|
"version": "18.3.27",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -661,7 +660,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
|
||||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.50.0",
|
"@typescript-eslint/scope-manager": "8.50.0",
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.50.0",
|
||||||
@@ -1135,7 +1133,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2168,7 +2165,6 @@
|
|||||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -2331,7 +2327,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -3743,7 +3738,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -5079,7 +5073,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -5285,7 +5278,6 @@
|
|||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -5297,7 +5289,6 @@
|
|||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -5316,7 +5307,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||||
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hast": "^3.0.0",
|
"@types/hast": "^3.0.0",
|
||||||
"@types/mdast": "^4.0.0",
|
"@types/mdast": "^4.0.0",
|
||||||
@@ -5385,8 +5375,7 @@
|
|||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
@@ -6273,7 +6262,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6451,7 +6439,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import MatchingBlock from "@/components/blocks/MatchingBlock";
|
|||||||
import OrderingBlock from "@/components/blocks/OrderingBlock";
|
import OrderingBlock from "@/components/blocks/OrderingBlock";
|
||||||
import ShortAnswerBlock from "@/components/blocks/ShortAnswerBlock";
|
import ShortAnswerBlock from "@/components/blocks/ShortAnswerBlock";
|
||||||
import DocumentBlock from "@/components/blocks/DocumentBlock";
|
import DocumentBlock from "@/components/blocks/DocumentBlock";
|
||||||
|
import VideoMarkerBlock from "@/components/blocks/VideoMarkerBlock";
|
||||||
import {
|
import {
|
||||||
Save,
|
Save,
|
||||||
X,
|
X,
|
||||||
@@ -154,7 +155,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addBlock = (type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document') => {
|
const addBlock = (type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document' | 'video_marker') => {
|
||||||
const newBlock: Block = {
|
const newBlock: Block = {
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
type,
|
type,
|
||||||
@@ -166,6 +167,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
|||||||
...(type === 'ordering' && { items: ["Item A", "Item B"] }),
|
...(type === 'ordering' && { items: ["Item A", "Item B"] }),
|
||||||
...(type === 'short-answer' && { prompt: "Question?", correctAnswers: ["Answer"] }),
|
...(type === 'short-answer' && { prompt: "Question?", correctAnswers: ["Answer"] }),
|
||||||
...(type === 'document' && { url: "", title: "" }),
|
...(type === 'document' && { url: "", title: "" }),
|
||||||
|
...(type === 'video_marker' && { url: "", title: "Video Interactivo", markers: [] }),
|
||||||
};
|
};
|
||||||
setBlocks([...blocks, newBlock]);
|
setBlocks([...blocks, newBlock]);
|
||||||
};
|
};
|
||||||
@@ -574,6 +576,15 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
|||||||
onChange={(updates) => updateBlock(block.id, updates)}
|
onChange={(updates) => updateBlock(block.id, updates)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{block.type === 'video_marker' && (
|
||||||
|
<VideoMarkerBlock
|
||||||
|
title={block.title || ""}
|
||||||
|
videoUrl={block.url || ""}
|
||||||
|
markers={block.markers || []}
|
||||||
|
editMode={editMode}
|
||||||
|
onChange={(updates) => updateBlock(block.id, updates)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -597,6 +608,13 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
|||||||
<span className="text-2xl group-hover:scale-110 transition-transform">🎬</span>
|
<span className="text-2xl group-hover:scale-110 transition-transform">🎬</span>
|
||||||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Media</span>
|
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Media</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => addBlock('video_marker')}
|
||||||
|
className="flex flex-col items-center gap-2 p-6 glass hover:border-indigo-500/50 transition-all group w-32"
|
||||||
|
>
|
||||||
|
<span className="text-2xl group-hover:scale-110 transition-transform">⏱️</span>
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Video+Q</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => addBlock('quiz')}
|
onClick={() => addBlock('quiz')}
|
||||||
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
|
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Clock, Plus, Trash2, Play, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface VideoMarker {
|
||||||
|
timestamp: number;
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
correctIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoMarkerBlockProps {
|
||||||
|
title: string;
|
||||||
|
videoUrl: string;
|
||||||
|
markers: VideoMarker[];
|
||||||
|
onChange: (updates: { title?: string; markers?: VideoMarker[] }) => void;
|
||||||
|
editMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VideoMarkerBlock({
|
||||||
|
title,
|
||||||
|
videoUrl,
|
||||||
|
markers,
|
||||||
|
onChange,
|
||||||
|
editMode
|
||||||
|
}: VideoMarkerBlockProps) {
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTime = (timeStr: string): number => {
|
||||||
|
const parts = timeStr.split(':');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return parseInt(parts[0]) * 60 + parseInt(parts[1]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMarker = () => {
|
||||||
|
const newMarker: VideoMarker = {
|
||||||
|
timestamp: currentTime,
|
||||||
|
question: "Nueva pregunta",
|
||||||
|
options: ["Opción 1", "Opción 2", "Opción 3", "Opción 4"],
|
||||||
|
correctIndex: 0
|
||||||
|
};
|
||||||
|
onChange({ markers: [...markers, newMarker] });
|
||||||
|
setEditingIndex(markers.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMarker = (index: number, updates: Partial<VideoMarker>) => {
|
||||||
|
const updated = markers.map((m, i) => i === index ? { ...m, ...updates } : m);
|
||||||
|
onChange({ markers: updated });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMarker = (index: number) => {
|
||||||
|
onChange({ markers: markers.filter((_, i) => i !== index) });
|
||||||
|
if (editingIndex === index) setEditingIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOption = (markerIndex: number, optionIndex: number, value: string) => {
|
||||||
|
const marker = markers[markerIndex];
|
||||||
|
const newOptions = [...marker.options];
|
||||||
|
newOptions[optionIndex] = value;
|
||||||
|
updateMarker(markerIndex, { options: newOptions });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!editMode) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-xs font-black uppercase tracking-widest text-gray-400">
|
||||||
|
{title || "Video con Marcadores"}
|
||||||
|
</h3>
|
||||||
|
<div className="glass-card p-6 border-indigo-500/20 bg-indigo-500/5">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
||||||
|
<Clock size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-white">Video Interactivo</p>
|
||||||
|
<p className="text-xs text-gray-500">{markers.length} marcadores configurados</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{markers.map((marker, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2 text-xs text-gray-400">
|
||||||
|
<span className="font-mono text-indigo-400">{formatTime(marker.timestamp)}</span>
|
||||||
|
<span>→</span>
|
||||||
|
<span className="truncate">{marker.question}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-in fade-in duration-300">
|
||||||
|
{/* Title Editor */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Título del Bloque</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => onChange({ title: e.target.value })}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
placeholder="Ej: Video Tutorial - Introducción"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Preview with Timeline */}
|
||||||
|
<div className="glass-card p-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<Play size={16} className="text-indigo-400" />
|
||||||
|
Vista Previa del Video
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs font-mono text-gray-500">{formatTime(currentTime)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<video
|
||||||
|
src={videoUrl}
|
||||||
|
controls
|
||||||
|
className="w-full rounded-lg"
|
||||||
|
onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={addMarker}
|
||||||
|
className="w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg font-bold flex items-center justify-center gap-2 transition-all"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
Agregar Marcador en {formatTime(currentTime)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Markers List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<Clock size={16} className="text-amber-400" />
|
||||||
|
Marcadores ({markers.length})
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{markers.length === 0 && (
|
||||||
|
<div className="glass-card p-8 text-center border-dashed">
|
||||||
|
<AlertCircle className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||||
|
<p className="text-sm text-gray-500">No hay marcadores configurados.</p>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Reproduce el video y haz clic en "Agregar Marcador"</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{markers.map((marker, idx) => (
|
||||||
|
<div key={idx} className="glass-card p-4 space-y-3 border-l-4 border-indigo-500">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xs font-mono font-bold text-indigo-400 bg-indigo-500/10 px-2 py-1 rounded">
|
||||||
|
{formatTime(marker.timestamp)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingIndex(editingIndex === idx ? null : idx)}
|
||||||
|
className="text-xs text-gray-500 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{editingIndex === idx ? "Colapsar" : "Editar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteMarker(idx)}
|
||||||
|
className="p-2 hover:bg-red-500/10 rounded-lg text-red-500 transition-all"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editingIndex === idx ? (
|
||||||
|
<div className="space-y-3 pt-2 border-t border-white/5">
|
||||||
|
{/* Timestamp Editor */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Timestamp (MM:SS)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formatTime(marker.timestamp)}
|
||||||
|
onChange={(e) => updateMarker(idx, { timestamp: parseTime(e.target.value) })}
|
||||||
|
className="w-full bg-black/20 border border-white/10 rounded px-3 py-1.5 text-sm font-mono text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Question Editor */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Pregunta</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={marker.question}
|
||||||
|
onChange={(e) => updateMarker(idx, { question: e.target.value })}
|
||||||
|
className="w-full bg-black/20 border border-white/10 rounded px-3 py-2 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
placeholder="¿Qué concepto se explicó?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Options Editor */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Opciones de Respuesta</label>
|
||||||
|
{marker.options.map((option, optIdx) => (
|
||||||
|
<div key={optIdx} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`correct-${idx}`}
|
||||||
|
checked={marker.correctIndex === optIdx}
|
||||||
|
onChange={() => updateMarker(idx, { correctIndex: optIdx })}
|
||||||
|
className="w-4 h-4 text-green-500"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={option}
|
||||||
|
onChange={(e) => updateOption(idx, optIdx, e.target.value)}
|
||||||
|
className="flex-1 bg-black/20 border border-white/10 rounded px-3 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-green-500"
|
||||||
|
placeholder={`Opción ${optIdx + 1}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<p className="text-[10px] text-gray-600 italic">Selecciona el radio button de la respuesta correcta</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-400 truncate">{marker.question}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ export interface QuizQuestion {
|
|||||||
|
|
||||||
export interface Block {
|
export interface Block {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document';
|
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document' | 'video_marker';
|
||||||
title?: string;
|
title?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -57,6 +57,12 @@ export interface Block {
|
|||||||
items?: string[];
|
items?: string[];
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
correctAnswers?: string[];
|
correctAnswers?: string[];
|
||||||
|
markers?: {
|
||||||
|
timestamp: number;
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
correctIndex: number;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Lesson {
|
export interface Lesson {
|
||||||
|
|||||||
Reference in New Issue
Block a user