skill-install-dialog.tsx
1 'use client' 2 3 import { useState } from 'react' 4 import type { SkillInstallOption } from '@/types' 5 import { api } from '@/lib/app/api-client' 6 import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' 7 8 interface Props { 9 open: boolean 10 onClose: () => void 11 skillName: string 12 installOptions?: SkillInstallOption[] 13 onInstalled: () => void 14 } 15 16 export function SkillInstallDialog({ open, onClose, skillName, installOptions = [], onInstalled }: Props) { 17 const [selectedMethod, setSelectedMethod] = useState<string>(installOptions[0]?.kind || 'brew') 18 const [installing, setInstalling] = useState(false) 19 const [error, setError] = useState('') 20 const [progress, setProgress] = useState('') 21 22 const handleInstall = async () => { 23 setInstalling(true) 24 setError('') 25 setProgress('Installing...') 26 try { 27 await api('POST', '/openclaw/skills/install', { 28 name: skillName, 29 installId: selectedMethod, 30 timeoutMs: 120_000, 31 }) 32 setProgress('Installed successfully!') 33 onInstalled() 34 setTimeout(onClose, 1000) 35 } catch (err: unknown) { 36 setError(err instanceof Error ? err.message : 'Installation failed') 37 setProgress('') 38 } finally { 39 setInstalling(false) 40 } 41 } 42 43 const methods = installOptions.length > 0 44 ? installOptions 45 : [ 46 { kind: 'brew' as const, label: 'Homebrew' }, 47 { kind: 'node' as const, label: 'npm/Node' }, 48 { kind: 'go' as const, label: 'Go install' }, 49 { kind: 'uv' as const, label: 'UV (Python)' }, 50 { kind: 'download' as const, label: 'Direct download' }, 51 ] 52 53 return ( 54 <Dialog open={open} onOpenChange={(v) => !v && onClose()}> 55 <DialogContent className="sm:max-w-[400px]"> 56 <DialogHeader> 57 <DialogTitle>Install {skillName}</DialogTitle> 58 </DialogHeader> 59 <div className="py-3 flex flex-col gap-3"> 60 <label className="text-[12px] font-600 text-text-3">Install Method</label> 61 <div className="flex flex-wrap gap-2"> 62 {methods.map((m) => ( 63 <button 64 key={m.kind} 65 onClick={() => setSelectedMethod(m.kind)} 66 disabled={installing} 67 className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border 68 ${selectedMethod === m.kind 69 ? 'bg-accent-soft text-accent-bright border-accent-bright/30' 70 : 'bg-transparent text-text-3 border-white/[0.08] hover:border-white/[0.15]' 71 }`} 72 style={{ fontFamily: 'inherit' }} 73 > 74 {m.label} 75 </button> 76 ))} 77 </div> 78 {progress && <p className="text-[12px] text-emerald-400">{progress}</p>} 79 {error && <p className="text-[12px] text-red-400">{error}</p>} 80 </div> 81 <DialogFooter> 82 <button 83 onClick={onClose} 84 disabled={installing} 85 className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[13px] font-600 cursor-pointer hover:bg-surface-2 transition-all" 86 style={{ fontFamily: 'inherit' }} 87 > 88 Cancel 89 </button> 90 <button 91 onClick={handleInstall} 92 disabled={installing} 93 className="px-4 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[13px] font-600 cursor-pointer disabled:opacity-40 transition-all hover:brightness-110" 94 style={{ fontFamily: 'inherit' }} 95 > 96 {installing ? 'Installing...' : 'Install'} 97 </button> 98 </DialogFooter> 99 </DialogContent> 100 </Dialog> 101 ) 102 }