oxios 1.19.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Download, FileUp, Link2, Type } from 'lucide-react'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { api } from '@/lib/api-client'
import { cn } from '@/lib/utils'
import type { Skill } from '@/types'

interface ImportDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  /** Optional initial mode (opened from a specific menu entry). */
  initialMode?: 'file' | 'text' | 'url'
}

type Mode = 'file' | 'text' | 'url'

/**
 * Skill import modal (design F2) — three modes sharing one dialog:
 *  - file: multipart upload (.md / .zip / .skill) → POST /api/skills/import
 *  - text: pasted SKILL.md (frontmatter preserved) → POST /api/skills/import/text
 *  - url:  fetch a remote SKILL.md → POST /api/skills/import/url
 */
export function ImportDialog({ open, onOpenChange, initialMode = 'file' }: ImportDialogProps) {
  const { t } = useTranslation()
  const qc = useQueryClient()
  const [mode, setMode] = useState<Mode>(initialMode)
  const [file, setFile] = useState<File | null>(null)
  const [dragging, setDragging] = useState(false)
  const [text, setText] = useState('')
  const [url, setUrl] = useState('')
  const [nameOverride, setNameOverride] = useState('')
  const fileInputRef = useRef<HTMLInputElement>(null)

  // Sync mode when the dialog re-opens from a specific menu entry.
  const [lastOpen, setLastOpen] = useState(false)
  if (open !== lastOpen) {
    if (open) setMode(initialMode)
    setLastOpen(open)
  }

  const onSuccess = (_r: unknown, name: string) => {
    toast.success(t('skills.importSuccess', { name }))
    qc.invalidateQueries({ queryKey: ['skills'] })
    onOpenChange(false)
    setFile(null)
    setText('')
    setUrl('')
    setNameOverride('')
  }

  const fileMut = useMutation({
    mutationFn: async (vars: { file: File; name?: string }) => {
      const fd = new FormData()
      fd.append('file', vars.file)
      if (vars.name) fd.append('name', vars.name)
      return api.upload<Skill>('/api/skills/import', fd)
    },
    onSuccess: (r, v) => onSuccess(r, r?.name ?? v.file.name),
    onError: (e: unknown) => toast.error(e instanceof Error ? e.message : t('skills.importFailed')),
  })

  const textMut = useMutation({
    mutationFn: (vars: { content: string; name?: string }) =>
      api.post<Skill>('/api/skills/import/text', vars),
    onSuccess: (r) => onSuccess(r, r?.name ?? 'skill'),
    onError: (e: unknown) => toast.error(e instanceof Error ? e.message : t('skills.importFailed')),
  })

  const urlMut = useMutation({
    mutationFn: (vars: { url: string; name?: string }) =>
      api.post<Skill>('/api/skills/import/url', vars),
    onSuccess: (r) => onSuccess(r, r?.name ?? 'skill'),
    onError: (e: unknown) => toast.error(e instanceof Error ? e.message : t('skills.importFailed')),
  })

  const saving = fileMut.isPending || textMut.isPending || urlMut.isPending

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault()
    setDragging(false)
    const f = e.dataTransfer.files?.[0]
    if (f) setFile(f)
  }

  const handleImport = () => {
    if (mode === 'file') {
      if (!file) return
      fileMut.mutate({ file, name: nameOverride || undefined })
    } else if (mode === 'text') {
      if (!text.trim()) return
      textMut.mutate({ content: text, name: nameOverride || undefined })
    } else {
      if (!url.trim()) return
      urlMut.mutate({ url, name: nameOverride || undefined })
    }
  }

  const canImport =
    (mode === 'file' && !!file) ||
    (mode === 'text' && text.trim().length > 0) ||
    (mode === 'url' && url.trim().length > 0)

  if (!open) return null

  const modes: { key: Mode; label: string; icon: React.ReactNode }[] = [
    { key: 'file', label: t('skills.importFromFile'), icon: <FileUp className="h-3.5 w-3.5" /> },
    { key: 'text', label: t('skills.importFromText'), icon: <Type className="h-3.5 w-3.5" /> },
    { key: 'url', label: t('skills.importFromUrl'), icon: <Link2 className="h-3.5 w-3.5" /> },
  ]

  return (
    <div
      className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-6"
      onClick={() => onOpenChange(false)}
    >
      <div
        className="bg-card border rounded-xl w-full max-w-xl max-h-[92vh] flex flex-col overflow-hidden shadow-lg"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center gap-3 px-6 py-4 border-b">
          <span className="font-mono text-[10px] uppercase tracking-wider text-primary bg-primary/10 px-2 py-0.5 rounded-full">
            import
          </span>
          <h2 className="font-semibold text-lg">{t('skills.import')}</h2>
        </div>

        <div className="p-6 overflow-y-auto flex-1 space-y-4">
          {/* Mode switcher */}
          <div className="inline-flex bg-muted rounded-lg p-1 gap-0.5">
            {modes.map((m) => (
              <button
                key={m.key}
                type="button"
                onClick={() => setMode(m.key)}
                className={cn(
                  'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all',
                  mode === m.key
                    ? 'bg-background text-foreground shadow'
                    : 'text-muted-foreground hover:text-foreground',
                )}
              >
                {m.icon}
                {m.label}
              </button>
            ))}
          </div>

          {/* Name override (optional, all modes) */}
          <div>
            <label className="block text-xs font-semibold text-muted-foreground mb-1">
              {t('skills.nameLabel')} (optional)
            </label>
            <Input
              className="font-mono text-sm max-w-xs"
              placeholder="my-skill"
              value={nameOverride}
              onChange={(e) => setNameOverride(e.target.value)}
            />
          </div>

          {mode === 'file' && (
            <div
              className={cn(
                'border-2 border-dashed rounded-lg px-6 py-10 text-center cursor-pointer transition-colors',
                dragging ? 'border-primary bg-primary/5' : 'border-input hover:border-primary/50',
              )}
              onClick={() => fileInputRef.current?.click()}
              onDragOver={(e) => {
                e.preventDefault()
                setDragging(true)
              }}
              onDragLeave={() => setDragging(false)}
              onDrop={handleDrop}
            >
              <input
                ref={fileInputRef}
                type="file"
                accept=".md,.zip,.skill"
                className="hidden"
                onChange={(e) => setFile(e.target.files?.[0] ?? null)}
              />
              <FileUp className="h-8 w-8 mx-auto mb-3 text-primary" />
              {file ? (
                <p className="text-sm font-medium">{file.name}</p>
              ) : (
                <>
                  <p className="text-sm font-medium">{t('skills.importDropzone')}</p>
                  <p className="text-xs text-muted-foreground mt-1">
                    {t('skills.importDropzoneSub')}
                  </p>
                </>
              )}
              <div className="flex gap-1.5 justify-center mt-3">
                {['.md', '.zip', '.skill'].map((f) => (
                  <span
                    key={f}
                    className="font-mono text-[10px] bg-muted border px-2 py-0.5 rounded text-muted-foreground"
                  >
                    {f}
                  </span>
                ))}
              </div>
            </div>
          )}

          {mode === 'text' && (
            <textarea
              className="font-mono text-xs w-full bg-background border rounded-md px-3 py-3 outline-none focus:border-ring focus:ring-2 focus:ring-ring/20 min-h-[240px] resize-y"
              spellCheck={false}
              placeholder={'---\nname: my-skill\ndescription: ...\n---\n\n# My Skill'}
              value={text}
              onChange={(e) => setText(e.target.value)}
            />
          )}

          {mode === 'url' && (
            <Input
              className="font-mono text-sm"
              placeholder="https://example.com/skills/my-skill/SKILL.md"
              value={url}
              onChange={(e) => setUrl(e.target.value)}
            />
          )}
        </div>

        <div className="flex items-center gap-2 px-6 py-4 border-t bg-background">
          <div className="flex-1" />
          <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={saving}>
            {t('common.cancel')}
          </Button>
          <Button onClick={handleImport} disabled={!canImport || saving}>
            <Download className="h-4 w-4" />
            {t('skills.import')}
          </Button>
        </div>
      </div>
    </div>
  )
}