'use client' import dynamic from 'next/dynamic' import { useState, useEffect, useRef, useMemo } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { Download, Presentation, ExternalLink, Maximize2, ChevronLeft, ChevronRight } from 'lucide-react' import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types' import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types' import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types' import '@excalidraw/excalidraw/index.css' import type { PresentationSpec } from '@/lib/types/presentation' const Excalidraw = dynamic( async () => (await import('@excalidraw/excalidraw')).Excalidraw, { ssr: false } ) const SlidesRenderer = dynamic( () => import('./slides-renderer').then(m => ({ default: m.SlidesRenderer })), { ssr: false, loading: () =>
Chargement des slides…
} ) interface CanvasBoardProps { initialData?: string canvasId?: string name: string } type PptxPayload = { type: string; filename: string; base64: string; slideCount?: number; theme?: string } type SlidesPayload = { type: 'slides'; html: string; title: string; theme?: string; style?: string; slideCount?: number; spec?: PresentationSpec } function parseCanvasScene(initialData?: string): { slides: SlidesPayload | null pptx: PptxPayload | null elements: readonly ExcalidrawElement[] files: BinaryFiles } { if (!initialData) { return { slides: null, pptx: null, elements: [], files: {} } } try { const parsed = JSON.parse(initialData) if (parsed && parsed.type === 'slides' && parsed.html) { return { slides: parsed as SlidesPayload, pptx: null, elements: [], files: {} } } if (parsed && parsed.type === 'pptx' && parsed.base64) { return { slides: null, pptx: parsed as PptxPayload, elements: [], files: {} } } if (parsed && Array.isArray(parsed)) { return { slides: null, pptx: null, elements: parsed as ExcalidrawElement[], files: {} } } if (parsed && parsed.elements) { const files: BinaryFiles = parsed.files && typeof parsed.files === 'object' ? parsed.files : {} return { slides: null, pptx: null, elements: parsed.elements as readonly ExcalidrawElement[], files } } } catch (e) { console.error('[CanvasBoard] Failed to parse canvas data:', e) } return { slides: null, pptx: null, elements: [], files: {} } } function PptxViewer({ data, name }: { data: PptxPayload; name: string }) { const handleDownload = () => { try { const byteCharacters = atob(data.base64) const byteNumbers = new Array(byteCharacters.length) for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i) } const byteArray = new Uint8Array(byteNumbers) const blob = new Blob([byteArray], { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = data.filename || `${name}.pptx` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } catch (e) { console.error('[PptxViewer] Download failed:', e) toast.error('Failed to download presentation') } } return (

{name}

PowerPoint Presentation {data.slideCount ? ` • ${data.slideCount} slides` : ''} {data.theme ? ` • ${data.theme} theme` : ''}

This file can be opened in Microsoft PowerPoint, Google Slides, or Keynote.

) } function SlidesViewer({ data, name, canvasId }: { data: SlidesPayload; name: string; canvasId?: string | null }) { const iframeRef = useRef(null) const [currentSlide, setCurrentSlide] = useState(1) const [isLoaded, setIsLoaded] = useState(!!data.spec) // spec → React renderer, no iframe loading const totalSlides = data.slideCount || 1 // Hide the global AI floating button while slides are displayed useEffect(() => { window.dispatchEvent(new CustomEvent('contextual-ai-visibility', { detail: true })) return () => { window.dispatchEvent(new CustomEvent('contextual-ai-visibility', { detail: false })) } }, []) // Listen for slide-change events from the iframe via postMessage useEffect(() => { const handler = (e: MessageEvent) => { if (e.data?.type === 'slideChange' && typeof e.data.current === 'number') { setCurrentSlide(e.data.current) } } window.addEventListener('message', handler) return () => window.removeEventListener('message', handler) }, []) const navigate = (dir: number) => { const iframe = iframeRef.current if (!iframe) return // Direct contentWindow access (works with allow-same-origin) try { const win = iframe.contentWindow as any if (typeof win?.changeSlide === 'function') { win.changeSlide(dir) return } } catch (_) {} // Fallback: postMessage iframe.contentWindow?.postMessage({ type: 'navigate', dir }, '*') } const openFullscreen = () => { const blob = new Blob([data.html], { type: 'text/html' }) const url = URL.createObjectURL(blob) window.open(url, '_blank') setTimeout(() => URL.revokeObjectURL(url), 10000) } const downloadHtml = () => { const blob = new Blob([data.html], { type: 'text/html' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'presentation'}.html` document.body.appendChild(a) a.click() document.body.removeChild(a) setTimeout(() => URL.revokeObjectURL(url), 5000) } return (
{/* Toolbar */}
{name} {totalSlides > 1 && ( {currentSlide} / {totalSlides} )}
{canvasId && ( Export PPTX )}
{/* Slides: React renderer (spec) or iframe (HTML fallback) */}
{data.spec ? ( ) : ( <> {/* Loading overlay — visible until iframe fires onLoad */} {!isLoaded && (
Chargement de la présentation…
)}