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