From 21b2f12485373a34a14e83f856faab12477a638f Mon Sep 17 00:00:00 2001 From: Nurfog Date: Mon, 19 Jan 2026 10:01:05 -0300 Subject: [PATCH] feat: Introduce a VideoMarkerBlock component for interactive video questions, integrating it into the lesson editor and updating API types. --- roadmap.md | 2 +- web/studio/package-lock.json | 15 +- .../courses/[id]/lessons/[lessonId]/page.tsx | 20 +- .../components/blocks/VideoMarkerBlock.tsx | 239 ++++++++++++++++++ web/studio/src/lib/api.ts | 8 +- 5 files changed, 267 insertions(+), 17 deletions(-) create mode 100644 web/studio/src/components/blocks/VideoMarkerBlock.tsx diff --git a/roadmap.md b/roadmap.md index 539329b..e21839f 100644 --- a/roadmap.md +++ b/roadmap.md @@ -130,7 +130,7 @@ - [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) - [ ] **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) ## Fase 12: Generador de Cursos "Mágico" con IA ✅ diff --git a/web/studio/package-lock.json b/web/studio/package-lock.json index 5176251..6fab93d 100644 --- a/web/studio/package-lock.json +++ b/web/studio/package-lock.json @@ -593,7 +593,6 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -661,7 +660,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -1135,7 +1133,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2168,7 +2165,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@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", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3743,7 +3738,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5079,7 +5073,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5285,7 +5278,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5297,7 +5289,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5316,7 +5307,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", - "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -5385,8 +5375,7 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "peer": true + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -6273,7 +6262,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -6451,7 +6439,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx b/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx index a1e27e9..78835e4 100644 --- a/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx +++ b/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx @@ -11,6 +11,7 @@ import MatchingBlock from "@/components/blocks/MatchingBlock"; import OrderingBlock from "@/components/blocks/OrderingBlock"; import ShortAnswerBlock from "@/components/blocks/ShortAnswerBlock"; import DocumentBlock from "@/components/blocks/DocumentBlock"; +import VideoMarkerBlock from "@/components/blocks/VideoMarkerBlock"; import { Save, 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 = { id: Math.random().toString(36).substr(2, 9), type, @@ -166,6 +167,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI ...(type === 'ordering' && { items: ["Item A", "Item B"] }), ...(type === 'short-answer' && { prompt: "Question?", correctAnswers: ["Answer"] }), ...(type === 'document' && { url: "", title: "" }), + ...(type === 'video_marker' && { url: "", title: "Video Interactivo", markers: [] }), }; setBlocks([...blocks, newBlock]); }; @@ -574,6 +576,15 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI onChange={(updates) => updateBlock(block.id, updates)} /> )} + {block.type === 'video_marker' && ( + updateBlock(block.id, updates)} + /> + )} ))} @@ -597,6 +608,13 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI 🎬 Media + + + + {/* Markers List */} +
+
+

+ + Marcadores ({markers.length}) +

+
+ + {markers.length === 0 && ( +
+ +

No hay marcadores configurados.

+

Reproduce el video y haz clic en "Agregar Marcador"

+
+ )} + + {markers.map((marker, idx) => ( +
+
+
+ + {formatTime(marker.timestamp)} + + +
+ +
+ + {editingIndex === idx ? ( +
+ {/* Timestamp Editor */} +
+ + 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" + /> +
+ + {/* Question Editor */} +
+ + 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ó?" + /> +
+ + {/* Options Editor */} +
+ + {marker.options.map((option, optIdx) => ( +
+ updateMarker(idx, { correctIndex: optIdx })} + className="w-4 h-4 text-green-500" + /> + 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}`} + /> +
+ ))} +

Selecciona el radio button de la respuesta correcta

+
+
+ ) : ( +

{marker.question}

+ )} +
+ ))} +
+ + ); +} diff --git a/web/studio/src/lib/api.ts b/web/studio/src/lib/api.ts index 7998e7c..899ac3c 100644 --- a/web/studio/src/lib/api.ts +++ b/web/studio/src/lib/api.ts @@ -44,7 +44,7 @@ export interface QuizQuestion { export interface Block { 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; content?: string; url?: string; @@ -57,6 +57,12 @@ export interface Block { items?: string[]; prompt?: string; correctAnswers?: string[]; + markers?: { + timestamp: number; + question: string; + options: string[]; + correctIndex: number; + }[]; } export interface Lesson {