hf-cli-ui / src /lib /Terminal.svelte
mishig's picture
mishig HF Staff
Revert 🤗 chrome label in source
a6baab1 verified
<script>
import { onMount, onDestroy } from 'svelte';
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const scenarios = [
{
command: 'hf download openai/gpt-oss-20b',
phase1: {
spinning: 'Preparing download...',
children: [{ text: 'Resolved main @ a1b2c3d', isLast: true }],
},
phase2: {
ready: 'Downloaded 14 files (13.5 GB)',
children: [
{ text: 'config.json', isLast: false },
{ text: 'tokenizer.json', isLast: false },
{ text: 'model.safetensors', isLast: false },
{
text: 'Cached to ~/.cache/huggingface/hub',
isLast: true,
},
],
},
},
{
command: 'hf upload username/mini-gpt .',
phase1: {
spinning: 'Preparing commit...',
children: [
{ text: '8 files hashed', isLast: false },
{ text: 'Repo created on the Hub', isLast: true },
],
},
phase2: {
ready: 'Uploaded to username/mini-gpt',
children: [
{ text: 'README.md', isLast: false },
{ text: 'model.safetensors (142 MB)', isLast: false },
{ text: 'Commit c3f4d1e pushed to main', isLast: true },
],
},
},
{
command:
'hf jobs run --flavor a10g-small python:3.12 python infer.py',
phase1: {
spinning: 'Provisioning a10g-small...',
children: [
{ text: 'GPU allocated', isLast: false },
{ text: 'Image pulled', isLast: true },
],
},
phase2: {
ready: 'Job complete',
children: [
{ text: 'Loaded weights in 4.2s', isLast: false },
{ text: 'Generated 128 tokens', isLast: false },
{ text: 'Logs at ~/.hf/jobs/job_9f2k3.log', isLast: true },
],
},
},
{
command:
'hf repos create username/diffuser-demo --type space --space-sdk gradio',
phase1: {
spinning: 'Creating Space...',
children: [
{ text: 'Initialized Gradio template', isLast: false },
{ text: 'CI build triggered', isLast: true },
],
},
phase2: {
ready: 'Space live',
children: [
{
text: 'https://huggingface.co/spaces/username/diffuser-demo',
isLast: true,
},
],
},
},
];
let spinnerFrame = $state(0);
let lines = $state([]);
let running = true;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function typeInto(idx, text, speed = 28) {
for (let i = 1; i <= text.length; i++) {
if (!running) return;
lines[idx].typed = text.slice(0, i);
await wait(speed);
}
}
function pushCommand(text) {
lines.push({ kind: 'command', text, typed: '', showCaret: true });
return lines.length - 1;
}
function pushPhase(variant, text) {
lines.push({ kind: 'phase', variant, text });
return lines.length - 1;
}
function pushChild(text, isLast) {
lines.push({ kind: 'child', text, isLast });
}
async function playScenario(scenario) {
lines = [];
await wait(500);
if (!running) return;
const c = pushCommand(scenario.command);
await typeInto(c, scenario.command);
lines[c].showCaret = false;
await wait(400);
const p1 = pushPhase('spinning', scenario.phase1.spinning);
await wait(750);
for (const child of scenario.phase1.children) {
pushChild(child.text, child.isLast);
await wait(500);
}
lines[p1].variant = 'done-hollow';
await wait(300);
pushPhase('ready', scenario.phase2.ready);
await wait(460);
for (const child of scenario.phase2.children) {
pushChild(child.text, child.isLast);
await wait(460);
}
await wait(3200);
}
async function run() {
while (running) {
for (const scenario of scenarios) {
if (!running) return;
await playScenario(scenario);
}
}
}
let spinnerInterval;
onMount(() => {
spinnerInterval = setInterval(() => {
spinnerFrame = (spinnerFrame + 1) % spinnerFrames.length;
}, 90);
run();
});
onDestroy(() => {
running = false;
clearInterval(spinnerInterval);
});
function splitChildText(text) {
const pattern = /(https?:\/\/\S+|~\/[^\s]+)/g;
const parts = [];
let last = 0;
let m;
while ((m = pattern.exec(text)) !== null) {
if (m.index > last)
parts.push({ text: text.slice(last, m.index), accent: false });
parts.push({ text: m[0], accent: true });
last = m.index + m[0].length;
}
if (last < text.length)
parts.push({ text: text.slice(last), accent: false });
return parts;
}
</script>
<div
class="frame-bg frame-shadow w-[760px] max-w-[calc(100vw-48px)] rounded-[20px] p-[3px]"
>
<div
class="w-full min-h-[540px] bg-[#fbfbf9] rounded-[17px] overflow-hidden font-mono text-[18px] leading-[1.85] text-[#232323]"
>
<div class="flex gap-2 pt-4 px-[18px] pb-[6px]">
<span class="w-3 h-3 rounded-full bg-[#d7d7d3]"></span>
<span class="w-3 h-3 rounded-full bg-[#d7d7d3]"></span>
<span class="w-3 h-3 rounded-full bg-[#d7d7d3]"></span>
</div>
<div class="pt-[18px] px-7 pb-7 min-h-[460px]" aria-live="polite">
{#each lines as line, i (i)}
<div
class="flex items-baseline gap-2.5 whitespace-pre-wrap break-words animate-fade-in {line.kind ===
'child'
? 'pl-[2.2ch]'
: ''}"
>
{#if line.kind === 'command'}
<span class="inline-block w-[1ch] text-[#6a6a66]">›</span>
<span class="text-[#222220]"
>{line.typed}{#if line.showCaret}<span
class="inline-block w-[0.55ch] ml-px text-[#222220] animate-blink"
aria-hidden="true">▎</span
>{/if}</span
>
{:else if line.kind === 'phase'}
{#if line.variant === 'spinning'}
<span class="inline-block w-[1ch] text-center text-[#5f5f5c]"
>{spinnerFrames[spinnerFrame]}</span
>
<span class="text-[#333331]">{line.text}</span>
{:else if line.variant === 'done-hollow'}
<span class="inline-block w-[1ch] text-center text-[#6b6b68]"
>○</span
>
<span class="text-[#333331]">{line.text}</span>
{:else if line.variant === 'ready'}
<span
class="inline-block w-[1ch] text-center text-[#0f7a3a] animate-ready-pulse"
>●</span
>
<span class="text-[#0f5a2a] font-semibold">{line.text}</span>
{/if}
{:else if line.kind === 'child'}
<span class="inline-block w-[1ch] text-[#b3b3ad]"
>{line.isLast ? '└' : '├'}</span
>
<span class="inline-block w-[1ch] text-[#17a34a]">✓</span>
<span class="text-[#474744]"
>{#each splitChildText(line.text) as part}{#if part.accent}<span
class="text-[#8b5cf6]">{part.text}</span
>{:else}{part.text}{/if}{/each}</span
>
{/if}
</div>
{/each}
</div>
</div>
</div>