/ src / components / agents / skill-install-dialog.tsx
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  }