Add skeleton loading, self-host assets, fix LLM empty response
Perceived startup speed: - index.html: animated shimmer skeleton (header + toolbar + 8 voice cards) visible immediately; fades out when loader.js finishes - WaveSurfer (57KB) and MDI icon font (394KB woff2) now served from static/vendor/ — removes 3 render-blocking external requests from <head> - Flag-icons CSS loaded async (rel=preload onload trick) — non-blocking loader.js: - Fade out skeleton + remove from DOM (300ms transition) - Reveal page-sections after JS finishes loading Conversation playground: - Qwen3 thinking mode fix: fall back to delta.reasoning_content when delta.content is empty so think-only LLM turns produce visible output - Better error message with /no-think hint when LLM returns empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a5efac5da5
commit
b33829ded5
20
CHANGELOG.md
20
CHANGELOG.md
@ -9,6 +9,26 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
|
- **Self-hosted WaveSurfer and MDI icon font** — removed two render-blocking
|
||||||
|
`<script>` and one blocking `<link>` to `unpkg.com` / `cdn.jsdelivr.net`
|
||||||
|
from `<head>`. WaveSurfer (~57 KB) and MDI font (~394 KB WOFF2 + CSS) are
|
||||||
|
now served locally from `static/vendor/`. Flag-icons CSS is loaded async
|
||||||
|
(non-blocking) via the `rel=preload` / `onload` trick.
|
||||||
|
- **Skeleton loading view** — `index.html` shows an animated shimmer
|
||||||
|
placeholder (page head + toolbar + 8 voice card outlines) immediately on
|
||||||
|
first paint, before any JS loads. The skeleton fades out once `loader.js`
|
||||||
|
finishes and real sections are revealed. No external deps — pure inline CSS.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **LLM returned empty response (Qwen3 thinking mode)** — Qwen3 models
|
||||||
|
stream thinking tokens under `delta.reasoning_content` instead of
|
||||||
|
`delta.content`. The conversation turn now falls back to
|
||||||
|
`reasoning_content` so think-only responses produce visible output.
|
||||||
|
Error message improved with a hint to add `/no-think` to the system prompt.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
- **Parallel JS module loading** — `loader.js` previously loaded all 16
|
- **Parallel JS module loading** — `loader.js` previously loaded all 16
|
||||||
modules sequentially (17 round-trips). Restructured into 4 ordered batches
|
modules sequentially (17 round-trips). Restructured into 4 ordered batches
|
||||||
with `async=false` so files are fetched in parallel but execute in the
|
with `async=false` so files are fetched in parallel but execute in the
|
||||||
|
|||||||
@ -762,7 +762,9 @@ async def conversation_turn(
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
obj = json.loads(chunk)
|
obj = json.loads(chunk)
|
||||||
delta = ((obj.get("choices") or [{}])[0].get("delta") or {}).get("content") or ""
|
d_obj = ((obj.get("choices") or [{}])[0].get("delta") or {})
|
||||||
|
# Primary: visible content. Fallback: reasoning_content (Qwen3 /think tokens)
|
||||||
|
delta = d_obj.get("content") or d_obj.get("reasoning_content") or ""
|
||||||
if not delta and isinstance(obj.get("message"), dict):
|
if not delta and isinstance(obj.get("message"), dict):
|
||||||
delta = obj["message"].get("content") or ""
|
delta = obj["message"].get("content") or ""
|
||||||
if delta:
|
if delta:
|
||||||
@ -781,7 +783,10 @@ async def conversation_turn(
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not llm_text.strip():
|
if not llm_text.strip():
|
||||||
yield sse({"type": "error", "stage": "llm", "message": "LLM returned empty response."})
|
yield sse({"type": "error", "stage": "llm",
|
||||||
|
"message": "LLM returned empty response. "
|
||||||
|
"If using a Qwen3 model, try adding /no-think to your system prompt "
|
||||||
|
"or pick a non-thinking model in the Language Model dropdown."})
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3. TTS
|
# 3. TTS
|
||||||
|
|||||||
@ -4,12 +4,59 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>TTS Voice Creator</title>
|
<title>TTS Voice Creator</title>
|
||||||
<!-- WaveSurfer must load synchronously before app.js initialises audio -->
|
|
||||||
<script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script>
|
<!-- ── Preconnect to CDN (flag SVGs loaded lazily by JS) ──────────────── -->
|
||||||
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css">
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
|
||||||
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
|
<link rel="preload" as="style"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css"
|
||||||
|
onload="this.onload=null;this.rel='stylesheet'">
|
||||||
|
<noscript>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css">
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- ── Skeleton animation (inline — zero extra request) ──────────────── -->
|
||||||
|
<style>
|
||||||
|
@keyframes sk-shimmer {
|
||||||
|
0% { background-position: -400% 0; }
|
||||||
|
100% { background-position: 400% 0; }
|
||||||
|
}
|
||||||
|
.sk {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--border, #e5e7eb) 25%,
|
||||||
|
var(--panel, #f3f4f6) 50%,
|
||||||
|
var(--border, #e5e7eb) 75%
|
||||||
|
);
|
||||||
|
background-size: 400% 100%;
|
||||||
|
animation: sk-shimmer 1.6s ease-in-out infinite;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
#app-skeleton {
|
||||||
|
padding: 28px 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .25s ease;
|
||||||
|
}
|
||||||
|
#app-skeleton.sk-hidden { opacity: 0; pointer-events: none; }
|
||||||
|
.sk-head { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.sk-icon { width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0; }
|
||||||
|
.sk-title { display: flex; flex-direction: column; gap: 7px; }
|
||||||
|
.sk-toolbar { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.sk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
|
||||||
|
.sk-card { border-radius: 10px; overflow: hidden; }
|
||||||
|
.sk-card-img { height: 56px; border-radius: 0; }
|
||||||
|
.sk-card-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.sk-row { display: flex; gap: 10px; margin-top: 4px; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@ -30,7 +77,7 @@
|
|||||||
<!-- App shell -->
|
<!-- App shell -->
|
||||||
<div id="app-shell">
|
<div id="app-shell">
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar (renders immediately — no JS needed) -->
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
<h1><span class="mdi mdi-microphone-variant"></span> Voice Creator</h1>
|
<h1><span class="mdi mdi-microphone-variant"></span> Voice Creator</h1>
|
||||||
@ -53,24 +100,12 @@
|
|||||||
<div class="nav-tree-item" data-voice-cat="hidden" onclick="navVoicesCat('hidden')">Hidden <span class="ntc" id="ntc-hidden"></span></div>
|
<div class="nav-tree-item" data-voice-cat="hidden" onclick="navVoicesCat('hidden')">Hidden <span class="ntc" id="ntc-hidden"></span></div>
|
||||||
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')">Library tools</div>
|
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')">Library tools</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-nav-section="s-clone" onclick="navTo('s-clone')">
|
<div class="nav-item" data-nav-section="s-clone" onclick="navTo('s-clone')"> <span class="nav-icon"><span class="mdi mdi-microphone-variant"></span></span> Clone a Voice</div>
|
||||||
<span class="nav-icon"><span class="mdi mdi-microphone-variant"></span></span> Clone a Voice
|
<div class="nav-item" data-nav-section="s-design" onclick="navTo('s-design')"> <span class="nav-icon"><span class="mdi mdi-auto-fix"></span></span> Design a Voice</div>
|
||||||
</div>
|
<div class="nav-item" data-nav-section="s-studio" onclick="navTo('s-studio')"> <span class="nav-icon"><span class="mdi mdi-earth"></span></span> Get Voices Online</div>
|
||||||
<div class="nav-item" data-nav-section="s-design" onclick="navTo('s-design')">
|
<div class="nav-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')"> <span class="nav-icon"><span class="mdi mdi-play"></span></span> Try It Out</div>
|
||||||
<span class="nav-icon"><span class="mdi mdi-auto-fix"></span></span> Design a Voice
|
<div class="nav-item" data-nav-section="s-conversation" onclick="navTo('s-conversation')"> <span class="nav-icon"><span class="mdi mdi-forum-outline"></span></span> Conversation</div>
|
||||||
</div>
|
<div class="nav-item" data-nav-section="s-performance" onclick="navTo('s-performance')"> <span class="nav-icon"><span class="mdi mdi-speedometer"></span></span> Benchmark</div>
|
||||||
<div class="nav-item" data-nav-section="s-studio" onclick="navTo('s-studio')">
|
|
||||||
<span class="nav-icon"><span class="mdi mdi-earth"></span></span> Get Voices Online
|
|
||||||
</div>
|
|
||||||
<div class="nav-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')">
|
|
||||||
<span class="nav-icon"><span class="mdi mdi-play"></span></span> Try It Out
|
|
||||||
</div>
|
|
||||||
<div class="nav-item" data-nav-section="s-conversation" onclick="navTo('s-conversation')">
|
|
||||||
<span class="nav-icon"><span class="mdi mdi-forum-outline"></span></span> Conversation
|
|
||||||
</div>
|
|
||||||
<div class="nav-item" data-nav-section="s-performance" onclick="navTo('s-performance')">
|
|
||||||
<span class="nav-icon"><span class="mdi mdi-speedometer"></span></span> Benchmark
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-group-label" style="margin-top:6px">Setup</div>
|
<div class="nav-group-label" style="margin-top:6px">Setup</div>
|
||||||
<div class="nav-item nav-tree-head" data-nav-section="s-llms" id="nav-engines-head" onclick="navTo('s-llms')">
|
<div class="nav-item nav-tree-head" data-nav-section="s-llms" id="nav-engines-head" onclick="navTo('s-llms')">
|
||||||
@ -83,12 +118,8 @@
|
|||||||
<div class="nav-tree-item" data-engines-cat="stt" onclick="navEnginesCat('stt')">Speech to Text</div>
|
<div class="nav-tree-item" data-engines-cat="stt" onclick="navEnginesCat('stt')">Speech to Text</div>
|
||||||
<div class="nav-tree-item" data-engines-cat="tts" onclick="navEnginesCat('tts')">Text to Speech</div>
|
<div class="nav-tree-item" data-engines-cat="tts" onclick="navEnginesCat('tts')">Text to Speech</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-nav-section="s-routing" onclick="navTo('s-routing')">
|
<div class="nav-item" data-nav-section="s-routing" onclick="navTo('s-routing')"> <span class="nav-icon"><span class="mdi mdi-swap-horizontal"></span></span> App Routing</div>
|
||||||
<span class="nav-icon"><span class="mdi mdi-swap-horizontal"></span></span> App Routing
|
<div class="nav-item" data-nav-section="s-connect" onclick="navTo('s-connect')"> <span class="nav-icon"><span class="mdi mdi-api"></span></span> Connect Apps</div>
|
||||||
</div>
|
|
||||||
<div class="nav-item" data-nav-section="s-connect" onclick="navTo('s-connect')">
|
|
||||||
<span class="nav-icon"><span class="mdi mdi-api"></span></span> Connect Apps
|
|
||||||
</div>
|
|
||||||
<div class="nav-item nav-tree-head" data-nav-section="s-settings" id="nav-settings-head" onclick="navTo('s-settings')">
|
<div class="nav-item nav-tree-head" data-nav-section="s-settings" id="nav-settings-head" onclick="navTo('s-settings')">
|
||||||
<span class="nav-icon"><span class="mdi mdi-cog-outline"></span></span>
|
<span class="nav-icon"><span class="mdi mdi-cog-outline"></span></span>
|
||||||
<span class="nav-label">Settings</span>
|
<span class="nav-label">Settings</span>
|
||||||
@ -113,19 +144,139 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main scrollable content — sections filled by loader.js -->
|
<!-- Main content — sections injected by loader.js -->
|
||||||
<main id="main-content">
|
<main id="main-content">
|
||||||
<section class="page-section" id="s-voices"></section>
|
|
||||||
<section class="page-section" id="s-clone"></section>
|
<!-- ── Skeleton loading view ─────────────────────────────────────────── -->
|
||||||
<section class="page-section" id="s-design"></section>
|
<!-- Visible immediately; loader.js fades it out when real content is ready -->
|
||||||
<section class="page-section" id="s-studio"></section>
|
<div id="app-skeleton">
|
||||||
<section class="page-section" id="s-tryout"></section>
|
|
||||||
<section class="page-section" id="s-performance"></section>
|
<!-- Page head -->
|
||||||
<section class="page-section" id="s-routing"></section>
|
<div class="sk-head">
|
||||||
<section class="page-section" id="s-connect"></section>
|
<div class="sk sk-icon"></div>
|
||||||
<section class="page-section" id="s-settings"></section>
|
<div class="sk-title">
|
||||||
<section class="page-section" id="s-llms"></section>
|
<div class="sk" style="width:160px;height:22px"></div>
|
||||||
<section class="page-section" id="s-conversation"></section>
|
<div class="sk" style="width:280px;height:13px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar row -->
|
||||||
|
<div class="sk-toolbar">
|
||||||
|
<div class="sk" style="width:48px;height:24px;border-radius:12px"></div>
|
||||||
|
<div class="sk" style="width:200px;height:30px;border-radius:6px"></div>
|
||||||
|
<div class="sk" style="width:80px;height:30px;border-radius:6px;margin-left:auto"></div>
|
||||||
|
<div class="sk" style="width:80px;height:30px;border-radius:6px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Voice card grid (matches the voice library layout) -->
|
||||||
|
<div class="sk-grid">
|
||||||
|
<!-- 8 placeholder voice cards -->
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:70%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:45%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:80%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:55%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:60%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:40%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:75%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:50%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:65%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:42%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:85%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:48%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:72%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:38%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
||||||
|
<div class="sk sk-card-img"></div>
|
||||||
|
<div class="sk-card-body">
|
||||||
|
<div class="sk" style="width:58%;height:14px"></div>
|
||||||
|
<div class="sk" style="width:44%;height:12px"></div>
|
||||||
|
<div class="sk-row">
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /app-skeleton -->
|
||||||
|
|
||||||
|
<section class="page-section" id="s-voices" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-clone" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-design" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-studio" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-tryout" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-performance" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-routing" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-connect" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-settings" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-llms" style="display:none"></section>
|
||||||
|
<section class="page-section" id="s-conversation" style="display:none"></section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div><!-- /app-shell -->
|
</div><!-- /app-shell -->
|
||||||
@ -133,7 +284,11 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<div id="status-bar">Loading…</div>
|
<div id="status-bar">Loading…</div>
|
||||||
|
|
||||||
<!-- loader.js: fetches sections → loads app.js → loads nav.js -->
|
<!-- WaveSurfer — self-hosted, loaded synchronously (voice waveform requires it) -->
|
||||||
|
<script src="/static/vendor/wavesurfer.min.js"></script>
|
||||||
|
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||||
|
|
||||||
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||||
<script src="/static/loader.js"></script>
|
<script src="/static/loader.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -93,4 +93,14 @@
|
|||||||
|
|
||||||
// F — navigation
|
// F — navigation
|
||||||
await _load('/static/nav.js');
|
await _load('/static/nav.js');
|
||||||
|
|
||||||
|
// ── Remove skeleton, reveal sections ────────────────────────────────────
|
||||||
|
var sk = document.getElementById('app-skeleton');
|
||||||
|
if (sk) {
|
||||||
|
sk.classList.add('sk-hidden');
|
||||||
|
setTimeout(function () { if (sk.parentNode) sk.parentNode.removeChild(sk); }, 300);
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.page-section').forEach(function (s) {
|
||||||
|
s.style.display = '';
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
1
static/vendor/flag-icons/flag-icons.min.css
vendored
Normal file
1
static/vendor/flag-icons/flag-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
static/vendor/flag-icons/flags.svg
vendored
Normal file
4
static/vendor/flag-icons/flags.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-xx" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" fill-rule="evenodd" stroke="#adb5bd" stroke-width="1.1" d="M.5.5h638.9v478.9H.5z"/>
|
||||||
|
<path fill="none" stroke="#adb5bd" stroke-width="1.1" d="m.5.5 639 479m0-479-639 479"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 283 B |
BIN
static/vendor/mdi/materialdesignicons-webfont.woff2
vendored
Normal file
BIN
static/vendor/mdi/materialdesignicons-webfont.woff2
vendored
Normal file
Binary file not shown.
3
static/vendor/mdi/materialdesignicons.min.css
vendored
Normal file
3
static/vendor/mdi/materialdesignicons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/vendor/wavesurfer-regions.min.js
vendored
Normal file
1
static/vendor/wavesurfer-regions.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/vendor/wavesurfer.min.js
vendored
Normal file
1
static/vendor/wavesurfer.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user