karlexmarin Claude Opus 4.7 (1M context) commited on
Commit
ed3d534
·
1 Parent(s): 137cb0a

feat(v0.2): full i18n coverage + round flag buttons + loading progress bar

Browse files

UI improvements based on self-test feedback:

- Round flag buttons (36px circular) with tooltip-on-hover label below.
Active language gets glowing border + accent background.
- Loading progress bar: shimmering indeterminate + discrete progress
(Pyodide load → 50%, TAF load → 95%, ready → hide). Visible during
the ~10-30s initial boot so users know something is happening.
- Expanded data-i18n coverage to ALL visible text:
* Mode tab buttons (Profile/Compare/Ask/Recipe)
* Profile section: preset label, HF id label, fetch button, generate button
* Compare section: recipe label, T_eval label, models title, compare button
* Ask section: title, placeholder, analyze button, example button
* Recipe section: title, default option
* Form section: input title, preset label, HF label, fetch button, run button
* Output sections: verdict title, chain title, chain desc, answer title,
share button, TAF Card title, comparison title
- New translation keys: profile.hf_placeholder, compare.hf_placeholder
(placeholders use data-i18n-placeholder attribute)
- Removed accidentally duplicated ask-section block

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (4) hide show
  1. index.html +50 -44
  2. js/i18n.js +16 -4
  3. js/main.js +22 -3
  4. style.css +66 -8
index.html CHANGED
@@ -10,12 +10,12 @@
10
  </head>
11
  <body>
12
  <header>
13
- <!-- Language switcher (top-right) -->
14
  <div class="lang-switcher">
15
- <button class="lang-btn" data-lang="en" title="English">🇬🇧</button>
16
- <button class="lang-btn" data-lang="es" title="Español">🇪🇸</button>
17
- <button class="lang-btn" data-lang="fr" title="Français">🇫🇷</button>
18
- <button class="lang-btn" data-lang="zh" title="中文">🇨🇳</button>
19
  </div>
20
 
21
  <h1 data-i18n="hero.title">🔬 TAF Agent</h1>
@@ -141,8 +141,13 @@
141
  </div>
142
 
143
  <main>
144
- <!-- Status -->
145
- <section id="status-bar"><div id="status">⏳ Loading Python runtime...</div></section>
 
 
 
 
 
146
 
147
  <!-- Mode toggle -->
148
  <section id="mode-section">
@@ -179,23 +184,24 @@
179
  what's its full viability profile?" → paste id → Profile → done.
180
  </span></span>
181
  </h2>
182
- <p class="recipe-desc">
183
  <strong>For technicians</strong>: when you need a complete viability snapshot
184
  of a candidate model. Outputs match paper §sec:gamma_decomposition format.
185
  </p>
186
 
187
  <div class="form-row">
188
- <label for="profile-preset">Preset:</label>
189
  <select id="profile-preset" disabled>
190
- <option value="">— or pick from list —</option>
191
  </select>
192
  </div>
193
 
194
  <div class="form-row">
195
- <label for="profile-hf-id">HF model id:</label>
196
  <input type="text" id="profile-hf-id"
 
197
  placeholder="e.g. meta-llama/Meta-Llama-3-8B or Qwen/Qwen2.5-7B" style="flex:1;" />
198
- <button id="profile-fetch-btn" type="button" class="secondary">📥 Fetch</button>
199
  </div>
200
  <div id="profile-hf-status" class="subtle" style="margin: -0.5rem 0 1rem; min-height:1.2em;"></div>
201
 
@@ -241,7 +247,7 @@
241
  </div>
242
  </div>
243
 
244
- <button id="profile-btn" disabled>🚀 Generate full profile</button>
245
  </section>
246
 
247
  <!-- COMPARE mode -->
@@ -257,20 +263,20 @@
257
  best: Llama-3-8B, Mistral-7B, or Qwen-7B?" → pick 3 + X-2 + 16K → see winner.
258
  </span></span>
259
  </h2>
260
- <p class="recipe-desc">
261
  <strong>For technicians</strong>: when choosing between 2-3 candidate models for
262
  a specific deployment scenario. Compare their verdicts on the same recipe.
263
  </p>
264
 
265
  <div class="form-row">
266
- <label for="compare-recipe">Recipe:</label>
267
  <select id="compare-recipe" disabled>
268
- <option value="">— pick a recipe —</option>
269
  </select>
270
  </div>
271
 
272
  <div class="form-row">
273
- <label for="compare-T_eval">T_eval (target context):</label>
274
  <input type="number" id="compare-T_eval" value="16000" style="flex:1;" />
275
  <span class="info" style="margin-top:0.5rem;"><span class="tooltip">
276
  For X-2 / X-19 only. The context length all compared models will be
@@ -279,7 +285,7 @@
279
  </div>
280
 
281
  <div id="compare-models">
282
- <h3 style="margin-top:1rem;">Models to compare (add up to 3)</h3>
283
  <div class="compare-slot" data-slot="1">
284
  <input type="text" class="compare-hf-id" placeholder="HF model id (e.g. meta-llama/Meta-Llama-3-8B)" />
285
  <select class="compare-preset">
@@ -300,82 +306,82 @@
300
  </div>
301
  </div>
302
 
303
- <button id="compare-btn" disabled style="margin-top:1rem;">🚀 Compare</button>
304
  </section>
305
 
306
- <!-- ASK mode -->
307
  <section id="ask-section" style="display:none;">
308
-
309
- <!-- Free-form question (mode=ask) -->
310
- <section id="ask-section">
311
- <h2>❓ Your question</h2>
312
- <textarea id="question" rows="3" placeholder="e.g. Will Mistral-7B handle 16K NIAH retrieval? Or: I have $5,000, what model can I train? Or: Cheapest GPU to serve Llama-70B at 100M tokens/day?"></textarea>
313
  <div style="display:flex; gap:0.5rem; margin-top:0.5rem; flex-wrap:wrap;">
314
- <button id="ask-btn" disabled>🚀 Analyze</button>
315
- <button id="example-btn" type="button" class="secondary">💡 Try an example</button>
316
  </div>
317
  </section>
318
 
319
  <!-- Recipe selector (mode=recipe) -->
320
  <section id="recipe-section" style="display:none;">
321
- <h2>📋 Recipe</h2>
322
  <select id="recipe-select" disabled>
323
- <option value="">— select a recipe —</option>
324
  </select>
325
  <p id="recipe-desc-display" class="recipe-desc"></p>
326
  </section>
327
 
328
  <!-- Form (mode=recipe) -->
329
  <section id="form-section" style="display:none;">
330
- <h2>🎯 Inputs</h2>
331
 
332
  <div class="form-row">
333
- <label for="preset">Preset model:</label>
334
  <select id="preset" disabled>
335
- <option value="">— select to autofill —</option>
336
  </select>
337
  </div>
338
 
339
  <div class="form-row">
340
- <label for="hf-id">Or any HF model:</label>
341
- <input type="text" id="hf-id" placeholder="e.g. Qwen/Qwen2.5-32B-Instruct" style="flex:1;" />
342
- <button id="hf-fetch-btn" type="button" class="secondary">📥 Fetch</button>
 
 
343
  </div>
344
  <div id="hf-status" class="subtle" style="margin: -0.5rem 0 1rem; min-height:1.2em;"></div>
345
 
346
- <!-- Dynamic form fields based on recipe -->
347
  <div id="dynamic-form" class="form-grid"></div>
348
 
349
- <button id="run-btn" disabled>🚀 Analyze</button>
350
  </section>
351
 
352
  <!-- Output (single-recipe verdict + chain) -->
353
  <section id="output-section" style="display:none;">
354
- <h2>📊 Verdict</h2>
355
  <div id="verdict-box"></div>
356
 
357
  <div style="margin: 0.75rem 0;">
358
- <button id="share-btn" class="secondary" type="button">🔗 Copy share link</button>
359
  <span id="share-status" class="subtle" style="margin-left:0.75rem;"></span>
360
  </div>
361
 
362
- <h2>🔍 Computation Chain</h2>
363
- <p class="subtle">Every number below is deterministic Python. Click a step to expand.</p>
364
  <div id="chain-box"></div>
365
 
366
- <h2 id="answer-header" style="display:none;">💬 Plain-English Answer</h2>
367
  <div id="answer-box" style="display:none;"></div>
368
  </section>
369
 
370
  <!-- Profile output -->
371
  <section id="profile-output" style="display:none;">
372
- <h2>📇 TAF Card — full model profile</h2>
373
  <div id="profile-box"></div>
374
  </section>
375
 
376
  <!-- Compare output -->
377
  <section id="compare-output" style="display:none;">
378
- <h2>🆚 Comparison Table</h2>
379
  <div id="compare-box"></div>
380
  </section>
381
  </main>
 
10
  </head>
11
  <body>
12
  <header>
13
+ <!-- Language switcher (top-right, round flags) -->
14
  <div class="lang-switcher">
15
+ <button class="lang-btn" data-lang="en" data-label="English" title="English">🇬🇧</button>
16
+ <button class="lang-btn" data-lang="es" data-label="Español" title="Español">🇪🇸</button>
17
+ <button class="lang-btn" data-lang="fr" data-label="Français" title="Français">🇫🇷</button>
18
+ <button class="lang-btn" data-lang="zh" data-label="中文" title="中文">🇨🇳</button>
19
  </div>
20
 
21
  <h1 data-i18n="hero.title">🔬 TAF Agent</h1>
 
141
  </div>
142
 
143
  <main>
144
+ <!-- Status with loading bar -->
145
+ <section id="status-bar">
146
+ <div id="status" data-i18n="status.loading_pyodide">⏳ Loading Python runtime...</div>
147
+ <div id="loading-bar-wrap" style="display:none;">
148
+ <div id="loading-bar"></div>
149
+ </div>
150
+ </section>
151
 
152
  <!-- Mode toggle -->
153
  <section id="mode-section">
 
184
  what's its full viability profile?" → paste id → Profile → done.
185
  </span></span>
186
  </h2>
187
+ <p class="recipe-desc" data-i18n="profile.desc">
188
  <strong>For technicians</strong>: when you need a complete viability snapshot
189
  of a candidate model. Outputs match paper §sec:gamma_decomposition format.
190
  </p>
191
 
192
  <div class="form-row">
193
+ <label for="profile-preset" data-i18n="profile.preset_label">Preset:</label>
194
  <select id="profile-preset" disabled>
195
+ <option value="" data-i18n="profile.preset_default">— or pick from list —</option>
196
  </select>
197
  </div>
198
 
199
  <div class="form-row">
200
+ <label for="profile-hf-id" data-i18n="profile.hf_label">HF model id:</label>
201
  <input type="text" id="profile-hf-id"
202
+ data-i18n-placeholder="profile.hf_placeholder"
203
  placeholder="e.g. meta-llama/Meta-Llama-3-8B or Qwen/Qwen2.5-7B" style="flex:1;" />
204
+ <button id="profile-fetch-btn" type="button" class="secondary" data-i18n="profile.fetch_btn">📥 Fetch</button>
205
  </div>
206
  <div id="profile-hf-status" class="subtle" style="margin: -0.5rem 0 1rem; min-height:1.2em;"></div>
207
 
 
247
  </div>
248
  </div>
249
 
250
+ <button id="profile-btn" disabled data-i18n="profile.btn">🚀 Generate full profile</button>
251
  </section>
252
 
253
  <!-- COMPARE mode -->
 
263
  best: Llama-3-8B, Mistral-7B, or Qwen-7B?" → pick 3 + X-2 + 16K → see winner.
264
  </span></span>
265
  </h2>
266
+ <p class="recipe-desc" data-i18n="compare.desc">
267
  <strong>For technicians</strong>: when choosing between 2-3 candidate models for
268
  a specific deployment scenario. Compare their verdicts on the same recipe.
269
  </p>
270
 
271
  <div class="form-row">
272
+ <label for="compare-recipe" data-i18n="compare.recipe_label">Recipe:</label>
273
  <select id="compare-recipe" disabled>
274
+ <option value="" data-i18n="recipe.default">— pick a recipe —</option>
275
  </select>
276
  </div>
277
 
278
  <div class="form-row">
279
+ <label for="compare-T_eval" data-i18n="compare.T_eval_label">T_eval (target context):</label>
280
  <input type="number" id="compare-T_eval" value="16000" style="flex:1;" />
281
  <span class="info" style="margin-top:0.5rem;"><span class="tooltip">
282
  For X-2 / X-19 only. The context length all compared models will be
 
285
  </div>
286
 
287
  <div id="compare-models">
288
+ <h3 style="margin-top:1rem;" data-i18n="compare.models_title">Models to compare (add up to 3)</h3>
289
  <div class="compare-slot" data-slot="1">
290
  <input type="text" class="compare-hf-id" placeholder="HF model id (e.g. meta-llama/Meta-Llama-3-8B)" />
291
  <select class="compare-preset">
 
306
  </div>
307
  </div>
308
 
309
+ <button id="compare-btn" disabled style="margin-top:1rem;" data-i18n="compare.btn">🚀 Compare</button>
310
  </section>
311
 
312
+ <!-- ASK mode (free-form question) -->
313
  <section id="ask-section" style="display:none;">
314
+ <h2 data-i18n="ask.title">❓ Your question</h2>
315
+ <textarea id="question" rows="3"
316
+ data-i18n-placeholder="ask.placeholder"
317
+ placeholder="e.g. Will Mistral-7B handle 16K NIAH retrieval? Or: I have $5,000, what model can I train? Or: Cheapest GPU to serve Llama-70B at 100M tokens/day?"></textarea>
 
318
  <div style="display:flex; gap:0.5rem; margin-top:0.5rem; flex-wrap:wrap;">
319
+ <button id="ask-btn" disabled data-i18n="ask.btn">🚀 Analyze</button>
320
+ <button id="example-btn" type="button" class="secondary" data-i18n="ask.example_btn">💡 Try an example</button>
321
  </div>
322
  </section>
323
 
324
  <!-- Recipe selector (mode=recipe) -->
325
  <section id="recipe-section" style="display:none;">
326
+ <h2 data-i18n="recipe.title">📋 Recipe</h2>
327
  <select id="recipe-select" disabled>
328
+ <option value="" data-i18n="recipe.default">— select a recipe —</option>
329
  </select>
330
  <p id="recipe-desc-display" class="recipe-desc"></p>
331
  </section>
332
 
333
  <!-- Form (mode=recipe) -->
334
  <section id="form-section" style="display:none;">
335
+ <h2 data-i18n="recipe.input_title">🎯 Inputs</h2>
336
 
337
  <div class="form-row">
338
+ <label for="preset" data-i18n="profile.preset_label">Preset model:</label>
339
  <select id="preset" disabled>
340
+ <option value="" data-i18n="profile.preset_default">— select to autofill —</option>
341
  </select>
342
  </div>
343
 
344
  <div class="form-row">
345
+ <label for="hf-id" data-i18n="profile.hf_label">Or any HF model:</label>
346
+ <input type="text" id="hf-id"
347
+ data-i18n-placeholder="profile.hf_placeholder"
348
+ placeholder="e.g. Qwen/Qwen2.5-32B-Instruct" style="flex:1;" />
349
+ <button id="hf-fetch-btn" type="button" class="secondary" data-i18n="profile.fetch_btn">📥 Fetch</button>
350
  </div>
351
  <div id="hf-status" class="subtle" style="margin: -0.5rem 0 1rem; min-height:1.2em;"></div>
352
 
 
353
  <div id="dynamic-form" class="form-grid"></div>
354
 
355
+ <button id="run-btn" disabled data-i18n="ask.btn">🚀 Analyze</button>
356
  </section>
357
 
358
  <!-- Output (single-recipe verdict + chain) -->
359
  <section id="output-section" style="display:none;">
360
+ <h2 data-i18n="verdict.title">📊 Verdict</h2>
361
  <div id="verdict-box"></div>
362
 
363
  <div style="margin: 0.75rem 0;">
364
+ <button id="share-btn" class="secondary" type="button" data-i18n="share.btn">🔗 Copy share link</button>
365
  <span id="share-status" class="subtle" style="margin-left:0.75rem;"></span>
366
  </div>
367
 
368
+ <h2 data-i18n="chain.title">🔍 Computation Chain</h2>
369
+ <p class="subtle" data-i18n="chain.desc">Every number below is deterministic Python. Click a step to expand.</p>
370
  <div id="chain-box"></div>
371
 
372
+ <h2 id="answer-header" style="display:none;" data-i18n="answer.title">💬 Plain-English Answer</h2>
373
  <div id="answer-box" style="display:none;"></div>
374
  </section>
375
 
376
  <!-- Profile output -->
377
  <section id="profile-output" style="display:none;">
378
+ <h2 data-i18n="tafcard.title">📇 TAF Card — full model profile</h2>
379
  <div id="profile-box"></div>
380
  </section>
381
 
382
  <!-- Compare output -->
383
  <section id="compare-output" style="display:none;">
384
+ <h2 data-i18n="compare.title_out">🆚 Comparison Table</h2>
385
  <div id="compare-box"></div>
386
  </section>
387
  </main>
js/i18n.js CHANGED
@@ -64,12 +64,15 @@ export const TRANSLATIONS = {
64
 
65
  "compare.title_out": "🆚 Comparison Table",
66
 
67
- "status.loading_pyodide": "⏳ Loading Python runtime...",
68
  "status.loading_taf": "⏳ Loading TAF formulas + recipes...",
69
  "status.ready": "✅ Ready. Pick a model and click Profile to start.",
70
  "status.computing": "🧮 Computing TAF chain...",
71
  "status.done": "✅ Done.",
72
 
 
 
 
73
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · independent research · the tool that closes the loop of the paper.",
74
  },
75
 
@@ -127,12 +130,15 @@ export const TRANSLATIONS = {
127
 
128
  "compare.title_out": "🆚 Tabla comparativa",
129
 
130
- "status.loading_pyodide": "⏳ Cargando runtime Python...",
131
  "status.loading_taf": "⏳ Cargando fórmulas TAF + recetas...",
132
  "status.ready": "✅ Listo. Elige un modelo y click Perfilar para empezar.",
133
  "status.computing": "🧮 Calculando cadena TAF...",
134
  "status.done": "✅ Hecho.",
135
 
 
 
 
136
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · investigación independiente · la herramienta que cierra el círculo del paper.",
137
  },
138
 
@@ -190,12 +196,15 @@ export const TRANSLATIONS = {
190
 
191
  "compare.title_out": "🆚 Tableau comparatif",
192
 
193
- "status.loading_pyodide": "⏳ Chargement du runtime Python...",
194
  "status.loading_taf": "⏳ Chargement des formules TAF + recettes...",
195
  "status.ready": "✅ Prêt. Choisissez un modèle et cliquez Profiler pour commencer.",
196
  "status.computing": "🧮 Calcul de la chaîne TAF...",
197
  "status.done": "✅ Terminé.",
198
 
 
 
 
199
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · recherche indépendante · l'outil qui ferme la boucle du paper.",
200
  },
201
 
@@ -253,12 +262,15 @@ export const TRANSLATIONS = {
253
 
254
  "compare.title_out": "🆚 比较表",
255
 
256
- "status.loading_pyodide": "⏳ 加载 Python 运行时...",
257
  "status.loading_taf": "⏳ 加载 TAF 公式 + 配方...",
258
  "status.ready": "✅ 就绪。选择一个模型并点击画像开始。",
259
  "status.computing": "🧮 计算 TAF 链...",
260
  "status.done": "✅ 完成。",
261
 
 
 
 
262
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · 独立研究 · 闭合论文回路的工具。",
263
  },
264
  };
 
64
 
65
  "compare.title_out": "🆚 Comparison Table",
66
 
67
+ "status.loading_pyodide": "⏳ Loading Python runtime (~10MB, first time only)...",
68
  "status.loading_taf": "⏳ Loading TAF formulas + recipes...",
69
  "status.ready": "✅ Ready. Pick a model and click Profile to start.",
70
  "status.computing": "🧮 Computing TAF chain...",
71
  "status.done": "✅ Done.",
72
 
73
+ "profile.hf_placeholder": "e.g. meta-llama/Meta-Llama-3-8B or Qwen/Qwen2.5-7B",
74
+ "compare.hf_placeholder": "HF model id (e.g. meta-llama/Meta-Llama-3-8B)",
75
+
76
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · independent research · the tool that closes the loop of the paper.",
77
  },
78
 
 
130
 
131
  "compare.title_out": "🆚 Tabla comparativa",
132
 
133
+ "status.loading_pyodide": "⏳ Cargando runtime Python (~10MB, solo primera vez)...",
134
  "status.loading_taf": "⏳ Cargando fórmulas TAF + recetas...",
135
  "status.ready": "✅ Listo. Elige un modelo y click Perfilar para empezar.",
136
  "status.computing": "🧮 Calculando cadena TAF...",
137
  "status.done": "✅ Hecho.",
138
 
139
+ "profile.hf_placeholder": "ej. meta-llama/Meta-Llama-3-8B o Qwen/Qwen2.5-7B",
140
+ "compare.hf_placeholder": "ID modelo HF (ej. meta-llama/Meta-Llama-3-8B)",
141
+
142
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · investigación independiente · la herramienta que cierra el círculo del paper.",
143
  },
144
 
 
196
 
197
  "compare.title_out": "🆚 Tableau comparatif",
198
 
199
+ "status.loading_pyodide": "⏳ Chargement du runtime Python (~10MB, première fois)...",
200
  "status.loading_taf": "⏳ Chargement des formules TAF + recettes...",
201
  "status.ready": "✅ Prêt. Choisissez un modèle et cliquez Profiler pour commencer.",
202
  "status.computing": "🧮 Calcul de la chaîne TAF...",
203
  "status.done": "✅ Terminé.",
204
 
205
+ "profile.hf_placeholder": "ex. meta-llama/Meta-Llama-3-8B ou Qwen/Qwen2.5-7B",
206
+ "compare.hf_placeholder": "ID modèle HF (ex. meta-llama/Meta-Llama-3-8B)",
207
+
208
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · recherche indépendante · l'outil qui ferme la boucle du paper.",
209
  },
210
 
 
262
 
263
  "compare.title_out": "🆚 比较表",
264
 
265
+ "status.loading_pyodide": "⏳ 加载 Python 运行时 (~10MB,首次加载)...",
266
  "status.loading_taf": "⏳ 加载 TAF 公式 + 配方...",
267
  "status.ready": "✅ 就绪。选择一个模型并点击画像开始。",
268
  "status.computing": "🧮 计算 TAF 链...",
269
  "status.done": "✅ 完成。",
270
 
271
+ "profile.hf_placeholder": "例如: meta-llama/Meta-Llama-3-8B 或 Qwen/Qwen2.5-7B",
272
+ "compare.hf_placeholder": "HF 模型 id (例如: meta-llama/Meta-Llama-3-8B)",
273
+
274
  "footer.text": "© 2026 Carles Marin · Apache-2.0 · 独立研究 · 闭合论文回路的工具。",
275
  },
276
  };
js/main.js CHANGED
@@ -39,12 +39,29 @@ const EXAMPLES = [
39
  // ════════════════════════════════════════════════════════════════════
40
  // Bootstrap
41
  // ════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  async function loadPyodideAndTaf() {
43
- setStatus("⏳ Loading Pyodide (Python runtime ~10MB)...");
 
44
  state.pyodide = await loadPyodide({
45
  indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/",
46
  });
47
- setStatus("⏳ Loading TAF formulas + recipes...");
 
48
  const tafCode = await fetch(TAF_BROWSER_URL).then(r => r.text());
49
  await state.pyodide.runPythonAsync(tafCode);
50
 
@@ -52,10 +69,12 @@ async function loadPyodideAndTaf() {
52
  state.recipes = JSON.parse(state.pyodide.runPython("list_recipes()"));
53
  state.recipesById = Object.fromEntries(state.recipes.map(r => [r.id, r]));
54
 
 
55
  populatePresets();
56
  populateRecipes();
57
  enableUI();
58
- setStatus("✅ Ready. Ask a question or pick a recipe.");
 
59
  }
60
 
61
  function populatePresets() {
 
39
  // ════════════════════════════════════════════════════════════════════
40
  // Bootstrap
41
  // ════════════════════════════════════════════════════════════════════
42
+ function showLoadingBar(show, progress=null) {
43
+ const wrap = $("loading-bar-wrap");
44
+ const bar = $("loading-bar");
45
+ if (!wrap || !bar) return;
46
+ if (!show) { wrap.style.display = "none"; return; }
47
+ wrap.style.display = "block";
48
+ if (progress === null) {
49
+ bar.classList.add("indeterminate");
50
+ bar.style.width = "100%";
51
+ } else {
52
+ bar.classList.remove("indeterminate");
53
+ bar.style.width = `${Math.min(100, Math.max(0, progress * 100))}%`;
54
+ }
55
+ }
56
+
57
  async function loadPyodideAndTaf() {
58
+ showLoadingBar(true, null);
59
+ setStatus(t("status.loading_pyodide"));
60
  state.pyodide = await loadPyodide({
61
  indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/",
62
  });
63
+ showLoadingBar(true, 0.5);
64
+ setStatus(t("status.loading_taf"));
65
  const tafCode = await fetch(TAF_BROWSER_URL).then(r => r.text());
66
  await state.pyodide.runPythonAsync(tafCode);
67
 
 
69
  state.recipes = JSON.parse(state.pyodide.runPython("list_recipes()"));
70
  state.recipesById = Object.fromEntries(state.recipes.map(r => [r.id, r]));
71
 
72
+ showLoadingBar(true, 0.95);
73
  populatePresets();
74
  populateRecipes();
75
  enableUI();
76
+ showLoadingBar(false);
77
+ setStatus(t("status.ready"));
78
  }
79
 
80
  function populatePresets() {
style.css CHANGED
@@ -33,29 +33,53 @@ header {
33
  }
34
  header h1 { margin: 0 0 0.5rem 0; font-size: 2rem; }
35
 
36
- /* Language switcher (top-right) */
37
  .lang-switcher {
38
  position: absolute;
39
  top: 1rem; right: 1rem;
40
- display: flex; gap: 0.25rem;
41
  z-index: 50;
42
  }
43
  .lang-btn {
44
  background: var(--bg-input);
45
- border: 1px solid var(--border);
46
  color: var(--fg);
47
- border-radius: 4px;
48
- padding: 0.25rem 0.5rem;
 
 
 
 
49
  font-size: 1.1rem;
50
  cursor: pointer;
51
  line-height: 1;
52
- transition: border-color 0.15s, background 0.15s;
 
 
 
 
 
53
  }
54
- .lang-btn:hover { border-color: var(--accent); }
55
  .lang-btn.lang-active {
56
  border-color: var(--accent);
57
  background: var(--accent-dim);
 
58
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  /* Quickstart banner */
61
  .quickstart-banner {
@@ -97,7 +121,41 @@ section {
97
  h2 { margin-top: 0; font-size: 1.2rem; color: var(--accent); }
98
 
99
  #status-bar { padding: 0.75rem 1.25rem; }
100
- #status { font-family: monospace; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  .recipe-desc { color: var(--fg-dim); margin: 0.5rem 0 0 0; }
103
 
 
33
  }
34
  header h1 { margin: 0 0 0.5rem 0; font-size: 2rem; }
35
 
36
+ /* Language switcher (top-right) — round flags */
37
  .lang-switcher {
38
  position: absolute;
39
  top: 1rem; right: 1rem;
40
+ display: flex; gap: 0.4rem;
41
  z-index: 50;
42
  }
43
  .lang-btn {
44
  background: var(--bg-input);
45
+ border: 2px solid var(--border);
46
  color: var(--fg);
47
+ border-radius: 50%;
48
+ width: 36px; height: 36px;
49
+ padding: 0;
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
  font-size: 1.1rem;
54
  cursor: pointer;
55
  line-height: 1;
56
+ transition: all 0.2s ease;
57
+ position: relative;
58
+ }
59
+ .lang-btn:hover {
60
+ border-color: var(--accent);
61
+ transform: scale(1.1);
62
  }
 
63
  .lang-btn.lang-active {
64
  border-color: var(--accent);
65
  background: var(--accent-dim);
66
+ box-shadow: 0 0 12px rgba(88, 166, 255, 0.4);
67
  }
68
+ .lang-btn::after {
69
+ content: attr(data-label);
70
+ position: absolute;
71
+ top: 100%;
72
+ left: 50%;
73
+ transform: translateX(-50%);
74
+ margin-top: 4px;
75
+ font-size: 0.65rem;
76
+ color: var(--fg-dim);
77
+ opacity: 0;
78
+ transition: opacity 0.15s;
79
+ white-space: nowrap;
80
+ pointer-events: none;
81
+ }
82
+ .lang-btn:hover::after { opacity: 1; }
83
 
84
  /* Quickstart banner */
85
  .quickstart-banner {
 
121
  h2 { margin-top: 0; font-size: 1.2rem; color: var(--accent); }
122
 
123
  #status-bar { padding: 0.75rem 1.25rem; }
124
+ #status { font-family: monospace; margin-bottom: 0.4rem; }
125
+
126
+ /* Loading progress bar */
127
+ #loading-bar-wrap {
128
+ height: 6px;
129
+ background: var(--bg-input);
130
+ border-radius: 3px;
131
+ overflow: hidden;
132
+ position: relative;
133
+ }
134
+ #loading-bar {
135
+ height: 100%;
136
+ background: linear-gradient(90deg, var(--accent-dim), var(--accent), var(--accent-dim));
137
+ background-size: 200% 100%;
138
+ width: 0%;
139
+ transition: width 0.3s ease-out;
140
+ border-radius: 3px;
141
+ animation: loading-shimmer 1.5s linear infinite;
142
+ }
143
+ @keyframes loading-shimmer {
144
+ 0% { background-position: 200% 0; }
145
+ 100% { background-position: -200% 0; }
146
+ }
147
+ #loading-bar.indeterminate {
148
+ width: 100%;
149
+ background: linear-gradient(90deg,
150
+ var(--bg-input) 0%, var(--accent) 50%, var(--bg-input) 100%);
151
+ background-size: 50% 100%;
152
+ background-repeat: no-repeat;
153
+ animation: loading-indeterminate 1.5s linear infinite;
154
+ }
155
+ @keyframes loading-indeterminate {
156
+ 0% { background-position: -50% 0; }
157
+ 100% { background-position: 150% 0; }
158
+ }
159
 
160
  .recipe-desc { color: var(--fg-dim); margin: 0.5rem 0 0 0; }
161