import { useEffect, useRef, useState, type ReactNode } from 'react' import { createPortal } from 'react-dom' import type { CanvasAssetRef, CanvasSkillCatalog, CanvasSkillTask } from '../protocol.ts' import type { ImageGenApi } from './api.ts' import { errorMessage, tt } from './helpers.ts' import { useImageGenLanguageTick } from './use-language.ts' import { PptProgressCard } from './PptProgressCard.tsx' import { PptDeckPreview } from './PptDeckPreview.tsx' import { PptSetup, PPT_SKILL_NAME } from './PptSetup.tsx' import css from './ppt-workspace.module.css' export interface PptImport { id: number; file: File } const running = (task: CanvasSkillTask): boolean => task.status === 'queued' || task.status === 'running' function PptDialog({ title, close, children }: { title: string; close: () => void; children: ReactNode }) { const root = useRef(null) useEffect(() => { const previous = document.activeElement as HTMLElement | null root.current?.querySelector('button')?.focus() const onKey = (event: KeyboardEvent): void => { event.stopPropagation() if (event.key === 'Escape') { event.preventDefault(); close() } if (event.key === 'Tab') { const focusable = Array.from(root.current?.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href]') ?? []) const first = focusable[0], last = focusable.at(-1) if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus() } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() } } } root.current?.addEventListener('keydown', onKey) const element = root.current return () => { element?.removeEventListener('keydown', onKey); previous?.focus() } }, []) return createPortal(
{ if (event.target === event.currentTarget) close() }}>

{title}

{children}
, document.body) } export function PptWorkspace({ api, active, importRequest, onImported }: { api: ImageGenApi; active: boolean; importRequest?: PptImport; onImported: () => void }) { useImageGenLanguageTick() const [images, setImages] = useState([]) const current = useRef(images) const [tasks, setTasks] = useState([]) const [catalog, setCatalog] = useState() const [busy, setBusy] = useState(false) const [uploading, setUploading] = useState(false) const [offline, setOffline] = useState(true) const ocrChosen = useRef(false) const [error, setError] = useState('') const [settingsOpen, setSettingsOpen] = useState(false) const [sourcePreview, setSourcePreview] = useState() const [pptPreview, setPptPreview] = useState() const [diagnostic, setDiagnostic] = useState() useEffect(() => { if (!active) { setSettingsOpen(false); setSourcePreview(undefined); setPptPreview(undefined); setDiagnostic(undefined) } }, [active]) const [selected, setSelected] = useState() const fileInput = useRef(null) const uploadQueue = useRef(Promise.resolve()) const uploadingCount = useRef(0) const mounted = useRef(true) const consumed = useRef() const [clock, setClock] = useState(Date.now()) useEffect(() => { mounted.current = true; return () => { mounted.current = false } }, []) const commit = (next: CanvasAssetRef[]): void => { current.current = next; setImages(next) } const accept = (files: File[]): void => { if (files.length === 0) return uploadingCount.current += 1; setUploading(true); setError('') uploadQueue.current = uploadQueue.current.then(async () => { for (const file of files) { if (!mounted.current) return if (current.current.length >= 12) { setError(tt('ppt.inputLimit')); break } if (!['image/png', 'image/jpeg', 'image/webp'].includes(file.type) || file.size === 0 || file.size > 20 * 1024 * 1024) { setError(tt('ppt.invalidImage')); continue } try { const asset = await api.canvasFileUpload(file) if (mounted.current) commit([...current.current, asset]) } catch (error) { if (mounted.current) setError(errorMessage(error)) } } }).finally(() => { uploadingCount.current -= 1 if (mounted.current) setUploading(uploadingCount.current > 0) }) } useEffect(() => { if (importRequest === undefined || consumed.current === importRequest.id) return consumed.current = importRequest.id accept([importRequest.file]); onImported() }, [importRequest]) useEffect(() => { if (!active) return let alive = true const load = async (): Promise => { try { const [nextTasks, nextCatalog] = await Promise.all([api.pptTasks(), api.canvasSkillsList()]) if (alive) { setTasks(nextTasks); setCatalog(nextCatalog); setClock(Date.now()) } } catch (error) { if (alive) setError(errorMessage(error)) } } void api.canvasSkillLibrary().then(library => { const token = library.entries.find(entry => entry.name === PPT_SKILL_NAME)?.config?.values.find(field => field.id === 'paddle-ocr-token') if (alive && !ocrChosen.current && token?.set) setOffline(false) }).catch(() => { /* The catalog reports any setup problem. */ }) void load() const timer = window.setInterval(() => { void load() }, 2000) return () => { alive = false; window.clearInterval(timer) } }, [active, api]) const ready = catalog?.agentAvailable && catalog.installed.includes(PPT_SKILL_NAME) const focused = tasks.find(task => task.id === selected) ?? tasks[0] const submit = async (source = current.current): Promise => { if (busy || uploading || source.length === 0) return setBusy(true); setError('') try { const task = await api.pptRun({ images: source, offlineOcr: offline }) setTasks(previous => [task, ...previous.filter(item => item.id !== task.id)]) setSelected(task.id) } catch (error) { setError(errorMessage(error)) } finally { setBusy(false) } } return