import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { BookOpen, FileArchive, Loader2, PackageOpen } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { httpClient } from '@/app/infra/http/HttpClient'; import type { Skill } from '@/app/infra/entities/api'; import { cn } from '@/lib/utils'; import { WorkspaceQuotaTooltip } from '@/app/home/components/workspace-quota/WorkspaceQuotaTooltip'; import type { WorkspaceQuotaItem } from '@/app/home/components/workspace-quota/useWorkspaceQuotaStatus'; interface PreviewSkill extends Skill { source_path?: string; } interface SkillZipPreviewPanelProps { file: File; onImported: (skillNames: string[]) => void; onCancel?: () => void; quota?: WorkspaceQuotaItem; quotaResource?: string; } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; } function previewPath(skill: PreviewSkill): string { return skill.source_path ?? ''; } function displayPreviewPath(skill: PreviewSkill): string { return previewPath(skill) || skill.name; } function truncateInstructions(instructions?: string): string { if (!instructions) return ''; const trimmed = instructions.trim(); if (trimmed.length <= 900) return trimmed; return trimmed.slice(0, 900).trimEnd() + '\n...'; } export default function SkillZipPreviewPanel({ file, onImported, onCancel, quota, quotaResource = '', }: SkillZipPreviewPanelProps) { const { t } = useTranslation(); const [previewSkills, setPreviewSkills] = useState([]); const [selectedPaths, setSelectedPaths] = useState([]); const [activePath, setActivePath] = useState(''); const [previewing, setPreviewing] = useState(false); const [installing, setInstalling] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const lastPreviewSignatureRef = useRef(''); const previewFileSignature = `${file.name}:${file.size}:${file.lastModified}`; const activeSkill = useMemo( () => previewSkills.find((skill) => previewPath(skill) === activePath) || previewSkills[0] || null, [activePath, previewSkills], ); const loadPreview = useCallback(async () => { setPreviewing(true); setPreviewSkills([]); setSelectedPaths([]); setActivePath(''); setErrorMessage(null); try { const resp = await httpClient.previewSkillInstallFromUpload(file); const skills = (resp.skills || []) as PreviewSkill[]; setPreviewSkills(skills); const paths = skills.map(previewPath); setSelectedPaths(paths); setActivePath(paths[0] || ''); if (skills.length === 0) { setErrorMessage(t('skills.noSkillMdInDirectory')); } else { setErrorMessage(null); } } catch (error: unknown) { const message = error instanceof Error ? error.message : typeof error === 'object' && error && 'msg' in error ? String((error as { msg?: string }).msg || '') : String(error); setErrorMessage(message || t('skills.previewLoadError')); } finally { setPreviewing(false); } }, [file, t]); useEffect(() => { if (lastPreviewSignatureRef.current === previewFileSignature) return; lastPreviewSignatureRef.current = previewFileSignature; void loadPreview(); }, [loadPreview, previewFileSignature]); function toggleSelection(path: string) { setSelectedPaths((current) => { if (current.includes(path)) { const next = current.filter((item) => item !== path); if (activePath === path) { setActivePath(next[0] || path); } return next; } setActivePath(path); return [...current, path]; }); } async function handleInstall() { if (quota?.disabled) return; if (selectedPaths.length === 0) return; setInstalling(true); setErrorMessage(null); try { const resp = await httpClient.installSkillFromUpload(file, selectedPaths); toast.success(t('skills.installSuccess')); onImported(resp.skills.map((skill) => skill.name)); } catch (error: unknown) { const message = error instanceof Error ? error.message : typeof error === 'object' && error && 'msg' in error ? String((error as { msg?: string }).msg || '') : String(error); setErrorMessage(message || t('skills.installError')); } finally { setInstalling(false); } } const activeInstructions = truncateInstructions(activeSkill?.instructions); return (
{previewing ? ( ) : ( )}
{previewing ? t('skills.loading') : t('skills.preview')}
{file.name} ยท {formatFileSize(file.size)}
{previewSkills.length > 0 && (
1 && 'md:grid-cols-[240px_minmax(0,1fr)]', )} > {previewSkills.length > 1 && (
{previewSkills.map((skill) => { const path = previewPath(skill); const displayPath = displayPreviewPath(skill); const selected = selectedPaths.includes(path); const active = activePath === path; return ( ); })}
)} {activeSkill && (

{activeSkill.display_name || activeSkill.name}

{activeSkill.description && (

{activeSkill.description}

)} {activeInstructions && (
{t('skills.previewInstructions')}
{activeInstructions}
)}
)}
)} {errorMessage && (
{errorMessage}
)}
{onCancel && ( )} {quota ? ( ) : ( )}
); }