Workshop / src /components /workshop /UrlQuickPanel.tsx
00000tt's picture
Workshop: bowl UI + /api/bowl + /api/hints + URL flow
5dc6970 verified
Raw
History Blame Contribute Delete
5.08 kB
import { useState } from 'react'
import { Link2, Search, Package, Loader2, CheckCircle2, XCircle } from 'lucide-react'
interface UrlFlowResp {
repo?: string
type?: string
found_model?: string | null
workflow?: { pipeline?: string; steps?: number | null; guidance?: number | null; fps?: number | null; loras?: boolean; quantization?: boolean }
examples?: Array<{ images?: string[]; prompt?: string; lora?: string }>
installed?: boolean
packet_space?: string
url?: string
imported_examples?: Array<{ images?: string[]; prompt?: string; lora?: string }>
status?: string
}
export default function UrlQuickPanel({ onLog }: { onLog: (msg: string) => void }) {
const [url, setUrl] = useState('')
const [answer, setAnswer] = useState('')
const [busy, setBusy] = useState<string | null>(null)
const run = async (mode: 'find' | 'install') => {
if (!url.trim()) { setAnswer('Paste a URL first, e.g. https://huggingface.co/spaces/org/name'); return }
setBusy(mode)
setAnswer('')
try {
const res = await fetch('/api/urlflow', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url.trim(), mode }),
})
const data: UrlFlowResp = await res.json()
if (!res.ok) {
setAnswer(`✗ ${(data as unknown as { detail?: string }).detail ?? 'request failed'}`)
onLog(`URL flow ${mode} failed: ${(data as unknown as { detail?: string }).detail ?? res.status}`)
return
}
const lines: string[] = []
lines.push(`REPO ${data.repo} (${data.type})`)
lines.push(`MODEL ${data.found_model ?? 'no recipe match (found nothing usable yet)'}`)
const wf = data.workflow ?? {}
lines.push(`WORKFLOW pipeline=${wf.pipeline ?? '?'} steps=${wf.steps ?? '?'} guidance=${wf.guidance ?? '?'} fps=${wf.fps ?? '?'} lora=${wf.loras ? 'yes' : 'no'} quant=${wf.quantization ? 'yes' : 'no'}`)
const exs = data.examples ?? []
lines.push(`EXAMPLES found ${exs.length}`)
exs.slice(0, 6).forEach(e => lines.push(` - ${(e.images ?? []).join(', ')} ${e.prompt ? '| ' + e.prompt.slice(0, 80) : ''}`))
if (data.installed) {
const imp = data.imported_examples ?? []
lines.push(`PACKET INSTALLED ${imp.length} example(s)`)
lines.push(`SPACE ${data.packet_space ?? ''} ${data.url ?? ''}`)
} else if (mode === 'find') {
lines.push('')
lines.push('Then press "Install as whole packet" to package all found examples into one private Space.')
}
setAnswer(lines.join('\n'))
if (data.installed) onLog(`Whole packet installed -> ${data.url}`)
} catch (e) {
setAnswer('✗ ' + String(e))
onLog('URL flow error: ' + e)
} finally { setBusy(null) }
}
return (
<div className="bg-[#191919] border border-[#2E2E2E] rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2">
<Link2 size={16} className="text-[#FF9D00]" />
<span className="text-sm font-semibold">URL Quick · 2 windows</span>
</div>
<div className="grid grid-cols-1 gap-3">
<div className="bg-[#0F0F0F] border border-[#2E2E2E] rounded-lg p-3 space-y-2">
<label className="text-[10px] font-semibold uppercase tracking-wide text-[#8B8B8B]">Window 1 · input URL</label>
<input
value={url}
onChange={e => setUrl(e.target.value)}
placeholder="https://huggingface.co/spaces/org/name"
className="w-full bg-[#0A0A0A] border border-[#2E2E2E] rounded-lg px-3 py-2 text-xs text-white placeholder:text-[#5A5A5A] outline-none focus:border-[#FF9D00]"
/>
<div className="flex gap-2">
<button
onClick={() => run('find')}
disabled={busy !== null}
className="flex items-center gap-1.5 bg-[#0F0F0F] hover:bg-[#242424] disabled:opacity-40 border border-[#2E2E2E] text-xs px-3 py-2 rounded-lg"
>
{busy === 'find' ? <Loader2 size={12} className="animate-spin" /> : <Search size={12} />}
Find same model
</button>
<button
onClick={() => run('install')}
disabled={busy !== null}
className="flex-1 flex items-center justify-center gap-1.5 bg-[#FF9D00] hover:bg-[#FFB93E] disabled:opacity-40 text-black text-xs font-semibold px-3 py-2 rounded-lg"
>
{busy === 'install' ? <Loader2 size={12} className="animate-spin" /> : <Package size={12} />}
Install as whole packet
</button>
</div>
</div>
<div className="bg-[#0F0F0F] border border-[#2E2E2E] rounded-lg p-3 space-y-2">
<label className="text-[10px] font-semibold uppercase tracking-wide text-[#8B8B8B]">Window 2 · answer / found</label>
<pre className="whitespace-pre-wrap text-[11px] leading-relaxed text-[#E0E0E0] font-mono min-h-[110px] max-h-56 overflow-y-auto">
{answer || '…'}
</pre>
</div>
</div>
</div>
)
}