"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { lmsApi, getLmsApiUrl, getToken, CollaborativeDoc, UpdateCollaborativeDocResponse } from "@/lib/api"; import { AlertTriangle, CheckCircle, Loader2, Save, Users, } from "lucide-react"; type ConflictInfo = { localContent: string; serverContent: string; serverRevision: number; }; type Props = { lessonId: string; }; export default function CollaborativeDocEditor({ lessonId }: Props) { const [content, setContent] = useState(""); const [revision, setRevision] = useState(0); const [saving, setSaving] = useState(false); const [status, setStatus] = useState<"idle" | "saved" | "conflict" | "error">("idle"); const [conflict, setConflict] = useState(null); const [activeEditors, setActiveEditors] = useState(0); const debounceRef = useRef | null>(null); const sseRef = useRef(null); const localDirtyRef = useRef(false); const revisionRef = useRef(0); const contentRef = useRef(""); // Cargar documento inicial useEffect(() => { void (async () => { try { const doc: CollaborativeDoc = await lmsApi.getLessonCollaborativeDoc(lessonId); setContent(doc.content); setRevision(doc.revision); revisionRef.current = doc.revision; contentRef.current = doc.content; } catch { setStatus("error"); } })(); }, [lessonId]); // SSE: escuchar cambios remotos useEffect(() => { const token = getToken() || ""; const baseUrl = getLmsApiUrl(); const url = `${baseUrl}/lessons/${lessonId}/collaborative-doc/stream${token ? `?preview_token=${encodeURIComponent(token)}` : ""}`; const es = new EventSource(url); sseRef.current = es; es.onmessage = (e) => { try { const data = JSON.parse(e.data as string) as { content: string; revision: number; }; // Solo actualizar si no tenemos cambios locales pendientes if (!localDirtyRef.current && data.revision !== revisionRef.current) { setContent(data.content); setRevision(data.revision); revisionRef.current = data.revision; contentRef.current = data.content; } // Contar "editores activos" como señal de conexión SSE viva setActiveEditors((n) => Math.max(1, n)); } catch { /* ignore */ } }; es.onerror = () => { setActiveEditors(0); }; return () => { es.close(); sseRef.current = null; }; }, [lessonId]); // Guardar con debounce 1.5s const save = useCallback(async (text: string, baseRevision: number) => { setSaving(true); setStatus("idle"); try { const res: UpdateCollaborativeDocResponse = await lmsApi.updateLessonCollaborativeDoc(lessonId, { content: text, base_revision: baseRevision, }); if (res.conflict) { setConflict({ localContent: text, serverContent: res.server_content ?? "", serverRevision: res.server_revision ?? baseRevision, }); setStatus("conflict"); } else { setRevision(res.revision); revisionRef.current = res.revision; contentRef.current = text; localDirtyRef.current = false; setStatus("saved"); setTimeout(() => setStatus("idle"), 2000); } } catch { setStatus("error"); } finally { setSaving(false); } }, [lessonId]); const handleChange = (value: string) => { setContent(value); contentRef.current = value; localDirtyRef.current = true; setStatus("idle"); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { void save(contentRef.current, revisionRef.current); }, 1500); }; // Resolución de conflictos const resolveKeepMine = () => { if (!conflict) return; setConflict(null); setStatus("idle"); // Forzar guardado con la revisión del servidor (sobreescribir) void save(conflict.localContent, conflict.serverRevision); }; const resolveKeepServer = () => { if (!conflict) return; setContent(conflict.serverContent); setRevision(conflict.serverRevision); revisionRef.current = conflict.serverRevision; contentRef.current = conflict.serverContent; localDirtyRef.current = false; setConflict(null); setStatus("idle"); }; // Toolbar: aplicar formato HTML básico via execCommand (compatible con contenteditable) // Para mantener simpleza y no depender de librerías externas usamos un