| 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> |
| ) |
| } |