import React, { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { installLibrary } from '../../services/libraryService'; import './InstallLibrariesModal.css'; interface InstallLibrariesModalProps { isOpen: boolean; onClose: () => void; libraries: string[]; } type ItemStatus = 'pending' | 'installing' | 'done' | 'error'; interface LibItem { /** Full spec as read from libraries.txt — may contain "@version" suffix */ spec: string; /** Parsed library name (without @version or @wokwi:hash) */ name: string; /** Version if present and valid semver, otherwise undefined */ version?: string; status: ItemStatus; error?: string; } /** Split "LibName@version" into { name, version }. * Returns version=undefined if no valid semver suffix. * Handles wokwi-hosted "LibName@wokwi:hash" — version stays undefined. */ function parseLibSpec(spec: string): { name: string; version?: string } { if (spec.includes('@wokwi:')) { return { name: spec.split('@wokwi:')[0] }; } const idx = spec.lastIndexOf('@'); if (idx > 0) { const ver = spec.slice(idx + 1); if (/^\d+\.\d+\.\d+$/.test(ver)) { return { name: spec.slice(0, idx), version: ver }; } } return { name: spec }; } const Spinner: React.FC<{ size?: number }> = ({ size = 16 }) => ( ); export const InstallLibrariesModal: React.FC = ({ isOpen, onClose, libraries, }) => { const { t } = useTranslation(); const [items, setItems] = useState(() => libraries.map((spec) => { const { name, version } = parseLibSpec(spec); return { spec, name, version, status: 'pending' as ItemStatus }; }), ); const [running, setRunning] = useState(false); const [doneCount, setDoneCount] = useState(0); // Sync items when the libraries prop changes (new import) React.useEffect(() => { setItems(libraries.map((spec) => { const { name, version } = parseLibSpec(spec); return { spec, name, version, status: 'pending' as ItemStatus }; })); setDoneCount(0); setRunning(false); }, [libraries]); const setItemStatus = useCallback( (spec: string, status: ItemStatus, error?: string) => { setItems((prev) => prev.map((it) => (it.spec === spec ? { ...it, status, error } : it)), ); }, [], ); const handleInstallAll = useCallback(async () => { setRunning(true); let completed = 0; for (const item of items) { if (item.status === 'done') { completed++; continue; } setItemStatus(item.spec, 'installing'); try { const result = await installLibrary(item.spec); if (result.success) { setItemStatus(item.spec, 'done'); } else { setItemStatus(item.spec, 'error', result.error || 'Install failed'); } } catch (e) { setItemStatus(item.spec, 'error', e instanceof Error ? e.message : 'Install failed'); } completed++; setDoneCount(completed); } setRunning(false); }, [items, setItemStatus]); if (!isOpen) return null; const pendingCount = items.filter((i) => i.status === 'pending').length; const installedCount = items.filter((i) => i.status === 'done').length; const allDone = items.length > 0 && pendingCount === 0 && !running; return (
e.stopPropagation()}> {/* Header */}
{t('editor.installLibs.title')}
{/* Subtitle */}
{running ? ( {t('editor.installLibs.installingProgress', { done: doneCount + 1, total: items.length, })} ) : allDone ? ( {t('editor.installLibs.allDone')} ) : ( {t('editor.installLibs.required', { count: items.length })} )}
{/* Library list */}
{items.map((item) => { const isWokwiLib = item.spec.includes('@wokwi:'); return (
{item.name} {item.version && ( v{item.version} )} {isWokwiLib && ( wokwi )} {item.status === 'pending' && ( {t('editor.installLibs.pending')} )} {item.status === 'installing' && ( {t('editor.installLibs.installing')} )} {item.status === 'done' && ( {t('editor.installLibs.installed')} )} {item.status === 'error' && ( {t('editor.installLibs.errorStatus')} )}
); })}
{/* Footer */}
{allDone ? ( ) : ( <> )}
); };