Spaces:
Running
Running
File size: 12,407 Bytes
b8cc2bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | import { Component, Show, For, createEffect } from 'solid-js';
interface ModelLoadingOverlayProps {
isVisible: boolean;
progress: number;
message: string;
file?: string;
backend: 'webgpu' | 'wasm';
state: 'unloaded' | 'loading' | 'ready' | 'error';
selectedModelId: string;
onModelSelect: (id: string) => void;
onStart: () => void;
onLocalLoad: (files: FileList) => void;
onClose?: () => void;
}
export const MODELS = [
{ id: 'parakeet-tdt-0.6b-v2', name: 'Parakeet v2', desc: 'English optimized' },
{ id: 'parakeet-tdt-0.6b-v3', name: 'Parakeet v3', desc: 'Multilingual Streaming' },
];
export function getModelDisplayName(id: string): string {
return (MODELS.find((m) => m.id === id)?.name ?? id) || 'Unknown model';
}
export const ModelLoadingOverlay: Component<ModelLoadingOverlayProps> = (props) => {
const progressWidth = () => `${Math.max(0, Math.min(100, props.progress))}%`;
let fileInput: HTMLInputElement | undefined;
const handleFileChange = (e: Event) => {
const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) {
props.onLocalLoad(files);
}
};
const handleClose = () => props.onClose?.();
createEffect(() => {
if (!props.isVisible || !props.onClose) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
props.onClose?.();
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
});
return (
<Show when={props.isVisible}>
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-[var(--color-earthy-dark-brown)]/30 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="model-overlay-title"
onClick={(e) => e.target === e.currentTarget && handleClose()}
>
<input
type="file"
multiple
ref={fileInput}
class="hidden"
onChange={handleFileChange}
/>
<div class="w-full max-w-lg mx-4">
<div class="relative nm-flat rounded-[40px] overflow-hidden transition-all duration-300 animate-in fade-in slide-in-from-bottom-4">
{/* Close Button - show whenever onClose is provided so user can dismiss in any state */}
<Show when={props.onClose}>
<button
type="button"
onClick={handleClose}
class="absolute top-8 right-8 neu-square-btn text-[var(--color-earthy-soft-brown)] hover:text-[var(--color-earthy-coral)] transition-all z-10"
aria-label="Close"
>
<span class="material-symbols-outlined text-xl">close</span>
</button>
</Show>
{/* Header */}
<div class="p-10 pb-6 text-center">
<div class="w-20 h-20 mx-auto mb-8 rounded-[32px] nm-inset flex items-center justify-center">
<Show
when={props.state !== 'error'}
fallback={<span class="material-symbols-outlined text-[var(--color-earthy-coral)] text-4xl">warning</span>}
>
<span class={`material-symbols-outlined text-[var(--color-earthy-muted-green)] text-4xl ${props.state === 'loading' ? 'animate-pulse' : ''}`}>
{props.state === 'loading' ? 'downloading' : 'neurology'}
</span>
</Show>
</div>
<h2 id="model-overlay-title" class="text-3xl font-extrabold text-[var(--color-earthy-dark-brown)] tracking-tight">
{props.state === 'unloaded' ? 'Engine Selection' :
props.state === 'error' ? 'Loading Failed' : 'Model Installation'}
</h2>
<p class="text-sm text-[var(--color-earthy-soft-brown)] font-medium mt-3 px-10">
{props.state === 'unloaded' ? 'Select the AI engine for this transcription session.' : props.message}
</p>
</div>
{/* Content */}
<div class="px-10 pb-10">
<Show when={props.state === 'unloaded'}>
<div class="space-y-4">
<div class="grid gap-4">
<For each={MODELS}>
{(model) => (
<button
onClick={() => props.onModelSelect(model.id)}
class={`flex items-center text-left p-6 rounded-3xl transition-all ${props.selectedModelId === model.id
? 'nm-inset text-[var(--color-earthy-muted-green)] ring-2 ring-[var(--color-earthy-muted-green)]/20'
: 'nm-flat text-[var(--color-earthy-dark-brown)] hover:shadow-neu-btn-hover'
}`}
>
<div class={`w-6 h-6 rounded-full nm-inset mr-5 flex flex-none items-center justify-center ${props.selectedModelId === model.id ? 'text-[var(--color-earthy-muted-green)]' : 'text-[var(--color-earthy-sage)]'
}`}>
<Show when={props.selectedModelId === model.id}>
<div class="w-2.5 h-2.5 bg-[var(--color-earthy-muted-green)] rounded-full shadow-[0_0_8px_var(--color-earthy-muted-green)]" />
</Show>
</div>
<div>
<div class="font-bold text-lg leading-tight">{model.name}</div>
<div class="text-[10px] font-black opacity-40 uppercase tracking-widest mt-1">{model.desc}</div>
</div>
</button>
)}
</For>
<button
onClick={() => fileInput?.click()}
class="flex items-center text-left p-6 rounded-3xl nm-flat opacity-70 hover:opacity-100 transition-all hover:shadow-neu-btn-hover"
>
<div class="w-10 h-10 rounded-2xl nm-inset flex items-center justify-center mr-5">
<span class="material-symbols-outlined text-[var(--color-earthy-soft-brown)] text-xl">file_open</span>
</div>
<div>
<div class="font-bold text-lg leading-tight">Local Model</div>
<div class="text-[10px] font-black opacity-40 uppercase tracking-widest mt-1">Load from disk</div>
</div>
</button>
</div>
<button
onClick={() => props.onStart()}
class="w-full mt-6 py-5 bg-[var(--color-earthy-muted-green)] text-white font-extrabold rounded-3xl shadow-xl active:scale-[0.98] transition-all uppercase tracking-widest text-xs"
>
Initialize AI Engine
</button>
</div>
</Show>
{/* Progress */}
<Show when={props.state === 'loading'}>
<div class="mt-4">
<div class="h-4 nm-inset rounded-full overflow-hidden p-1">
<div
class="h-full bg-[var(--color-earthy-muted-green)] rounded-full transition-all duration-300 ease-out shadow-[0_0_12px_var(--color-earthy-muted-green)]"
style={{ width: progressWidth() }}
/>
</div>
<div class="flex justify-between items-center mt-6 px-1">
<div class="flex flex-col">
<span class="text-[10px] font-black text-[var(--color-earthy-soft-brown)] uppercase tracking-widest leading-none mb-1">Downloaded</span>
<span class="text-[var(--color-earthy-muted-green)] font-black text-2xl">{props.progress}%</span>
</div>
<div class="flex flex-col text-right">
<span class="text-[10px] font-black text-[var(--color-earthy-soft-brown)] uppercase tracking-widest leading-none mb-1">Active File</span>
<span class="text-[var(--color-earthy-soft-brown)] font-bold text-[11px] truncate max-w-[200px]">
{props.file || 'Preparing assets...'}
</span>
</div>
</div>
</div>
</Show>
<Show when={props.state === 'error'}>
<div>
<button
onClick={() => props.onStart()}
class="w-full py-5 nm-flat text-[var(--color-earthy-coral)] font-black rounded-3xl shadow-none hover:opacity-90 transition-all"
>
Retry Connection
</button>
</div>
</Show>
</div>
{/* Footer */}
<div class="px-10 py-6 border-t border-[var(--color-earthy-sage)]/30 flex items-center justify-between opacity-80">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-base text-[var(--color-earthy-soft-brown)]">offline_bolt</span>
<span class="text-[10px] font-black text-[var(--color-earthy-soft-brown)] uppercase tracking-widest">
{props.backend === 'webgpu' ? 'GPU Accelerated' : 'WASM Native'}
</span>
</div>
<span class="text-[10px] text-[var(--color-earthy-sage)] font-black tracking-widest">
PRIVACY SECURED
</span>
</div>
</div>
</div>
</div>
</Show>
);
};
|