Fix hint visibility and add PWA install button with one-click prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-12 14:30:57 +08:00
parent 3a0dff87bf
commit f0bb40c605
2 changed files with 272 additions and 17 deletions
+265 -17
View File
@@ -193,14 +193,99 @@ PWA_HEAD = """
<link rel="icon" href="/pwa/icons/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/pwa/icons/icon.svg">
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(function () {});
(function () {
var deferredPrompt = null;
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(function () {});
});
}
function isStandalone() {
return window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
}
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
}
function closeModal() {
var m = document.getElementById("pwa-install-modal");
if (m) m.remove();
}
function showModal(html) {
closeModal();
var overlay = document.createElement("div");
overlay.id = "pwa-install-modal";
overlay.className = "pwa-modal-overlay";
overlay.innerHTML = '<div class="pwa-modal">' + html + '</div>';
overlay.addEventListener("click", function (e) {
if (e.target === overlay || e.target.classList.contains("pwa-modal-close")) closeModal();
});
document.body.appendChild(overlay);
}
function manualInstallGuide() {
var steps = isIOS()
? "<ol><li>点击 Safari 底部分享按钮 <strong>□↑</strong></li><li>选择 <strong>「添加到主屏幕」</strong></li><li>点击 <strong>添加</strong> 即可</li></ol>"
: "<ol><li>点击浏览器右上角 <strong>⋮</strong> 菜单</li><li>选择 <strong>「安装应用」</strong> 或 <strong>「添加到主屏幕」</strong></li><li>确认安装即可</li></ol>";
showModal(
'<button class="pwa-modal-close" type="button">✕</button>' +
'<h3>📲 安装 Trading Studio</h3>' +
'<p>当前环境需手动安装,按以下步骤操作:</p>' + steps +
'<p class="pwa-modal-tip">安装后可像原生 App 一样从桌面/icon 启动。</p>'
);
}
window.addEventListener("beforeinstallprompt", function (e) {
e.preventDefault();
deferredPrompt = e;
var btn = document.getElementById("pwa-install-btn");
if (btn) btn.classList.add("pwa-ready");
});
}
window.addEventListener("appinstalled", function () {
deferredPrompt = null;
var btn = document.getElementById("pwa-install-btn");
if (btn) btn.style.display = "none";
});
document.addEventListener("click", function (e) {
var btn = e.target.closest("#pwa-install-btn");
if (!btn) return;
e.preventDefault();
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(function (choice) {
if (choice.outcome === "accepted") btn.style.display = "none";
deferredPrompt = null;
});
} else {
manualInstallGuide();
}
});
window.addEventListener("load", function () {
var btn = document.getElementById("pwa-install-btn");
if (!btn || isStandalone()) {
if (btn) btn.style.display = "none";
return;
}
btn.style.display = "inline-flex";
});
})();
</script>
"""
INSTALL_APP_BUTTON_HTML = """
<button id="pwa-install-btn" class="pwa-install-btn" type="button" title="安装到手机/平板/电脑桌面">
<img src="/pwa/icons/install.svg" alt="" class="pwa-install-icon" width="28" height="28"/>
<span class="pwa-install-text">安装 App</span>
</button>
"""
CUSTOM_CSS = """
/* ========== 居中布局 + 响应式 + 高对比度 ========== */
html, body {
@@ -348,10 +433,168 @@ gradio-app,
line-height: 1.6 !important;
}
/* placeholder 高对比度 */
.gradio-container textarea::placeholder,
.gradio-container input::placeholder {
color: #9ca3af !important;
.gradio-container input::placeholder,
.gradio-container input[type="text"]::placeholder {
color: #d1d5db !important;
opacity: 1 !important;
-webkit-text-fill-color: #d1d5db !important;
}
.gradio-container .bright-input textarea,
.gradio-container .bright-input input {
background: #1e293b !important;
border: 1px solid #64748b !important;
}
/* 输入框下方 info 提示文字 */
.gradio-container .hint,
.gradio-container .info,
.gradio-container .form .secondary-wrap,
.gradio-container span[data-testid="block-info"] {
color: #94a3b8 !important;
font-size: 0.88rem !important;
}
/* Markdown 内联代码 — 修复白底看不见 */
.gradio-container code,
.gradio-container .prose code,
.gradio-container .markdown-text code,
.gradio-container pre code {
background: #1e3a5f !important;
color: #bfdbfe !important;
border: 1px solid #3b82f6 !important;
padding: 2px 10px !important;
border-radius: 6px !important;
font-size: 0.9em !important;
}
.gradio-container pre {
background: #111827 !important;
border: 1px solid #374151 !important;
padding: 12px !important;
border-radius: 8px !important;
}
/* 组件标签 — 修复白底蓝字 */
.gradio-container .block-label,
.gradio-container .label-wrap,
.gradio-container .label-wrap span,
.gradio-container span.label {
background: #1e293b !important;
background-color: #1e293b !important;
color: #93c5fd !important;
font-weight: 600 !important;
font-size: 0.95rem !important;
border: 1px solid #475569 !important;
border-radius: 6px !important;
padding: 4px 10px !important;
}
/* 说明文字块 */
.hint-box {
background: #1e293b !important;
border: 1px solid #475569 !important;
border-radius: 10px !important;
padding: 14px 18px !important;
color: #e2e8f0 !important;
font-size: 0.95rem !important;
line-height: 1.7 !important;
margin-bottom: 12px !important;
}
.hint-box strong { color: #ffffff !important; }
.file-tag {
display: inline-block;
background: #1e3a5f !important;
color: #bfdbfe !important;
border: 1px solid #3b82f6 !important;
padding: 3px 12px !important;
border-radius: 6px !important;
font-family: "JetBrains Mono", Consolas, monospace !important;
font-weight: 700 !important;
font-size: 0.92em !important;
}
/* 页头 + 安装 App 按钮 */
.header-row {
align-items: flex-start !important;
width: 100% !important;
}
.header-row > div { flex: 1 1 auto !important; }
.pwa-install-btn {
display: none;
align-items: center;
gap: 10px;
padding: 12px 20px;
margin-top: 8px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: #ffffff !important;
border: 2px solid #60a5fa;
border-radius: 12px;
cursor: pointer;
font-weight: 700;
font-size: 1rem;
font-family: inherit;
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.45);
transition: transform 0.15s, box-shadow 0.15s;
white-space: nowrap;
}
.pwa-install-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(37, 99, 235, 0.55);
}
.pwa-install-btn.pwa-ready {
animation: pwa-pulse 2s infinite;
}
@keyframes pwa-pulse {
0%, 100% { box-shadow: 0 4px 20px rgba(37, 99, 235, 0.45); }
50% { box-shadow: 0 4px 28px rgba(96, 165, 250, 0.8); }
}
.pwa-install-icon { flex-shrink: 0; border-radius: 6px; }
.pwa-install-text { color: #ffffff !important; }
/* 安装引导弹窗 */
.pwa-modal-overlay {
position: fixed;
inset: 0;
z-index: 99999;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.pwa-modal {
position: relative;
background: #1e293b;
border: 2px solid #3b82f6;
border-radius: 16px;
padding: 28px 32px;
max-width: 420px;
width: 100%;
color: #e2e8f0;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.pwa-modal h3 { color: #ffffff !important; margin: 0 0 12px !important; font-size: 1.25rem !important; }
.pwa-modal p { color: #cbd5e1 !important; line-height: 1.6 !important; }
.pwa-modal ol { color: #e2e8f0 !important; padding-left: 20px !important; line-height: 1.8 !important; }
.pwa-modal-tip { font-size: 0.85rem !important; color: #93c5fd !important; margin-top: 16px !important; }
.pwa-modal-close {
position: absolute;
top: 12px;
right: 14px;
background: #374151;
border: none;
color: #fff;
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
}
@media (max-width: 640px) {
.header-row { flex-direction: column !important; }
.pwa-install-btn { width: 100% !important; justify-content: center !important; margin-top: 12px !important; }
}
.gradio-container .wrap .readonly textarea {
@@ -512,8 +755,9 @@ def build_app() -> gr.Blocks:
with gr.Blocks(
title="Trading Studio | 交易复盘配音中控",
) as demo:
gr.Markdown(
f"""
with gr.Row(elem_classes=["header-row"]):
gr.Markdown(
f"""
# ⚡ Trading Studio
**本地量化交易复盘 → B 站配音生产流水线**
@@ -525,11 +769,10 @@ def build_app() -> gr.Blocks:
| ChatTTS | 本地 GPU 固定音色合成 |
> 仓库: [{GIT_REPO_URL}]({GIT_REPO_URL})
<div class="install-hint">📱 <strong>安装为 App</strong>手机/平板用浏览器菜单「添加到主屏幕」;电脑 Chrome/Edge 地址栏点击「安装 Trading Studio」图标即可。</div>
""",
elem_classes=["dark-panel"],
)
""",
elem_classes=["dark-panel"],
)
gr.HTML(INSTALL_APP_BUTTON_HTML)
with gr.Row(elem_classes=["status-row"]):
ollama_status = gr.HTML(value=_status_html("Ollama 节点", "正在检测...", "warn"))
@@ -544,9 +787,12 @@ def build_app() -> gr.Blocks:
with gr.Tabs():
# ---- Tab 1: 音色锁定 ----
with gr.Tab("🎙️ 音色锁定"):
gr.Markdown(
"上传 **10-30 秒** 干净人声样本,系统将提取 Speaker Embedding "
f"并保存至 `{SPEAKER_EMB_PATH.name}`,后续合成 100% 还原音色。"
gr.HTML(
f'<div class="hint-box">'
f'上传 <strong>10-30 秒</strong> 干净人声样本,系统将提取 Speaker Embedding '
f'并保存至 <span class="file-tag">{SPEAKER_EMB_PATH.name}</span>'
f'后续合成 <strong>100% 还原音色</strong>。'
f"</div>"
)
with gr.Row():
spk_audio = gr.Audio(
@@ -556,8 +802,10 @@ def build_app() -> gr.Blocks:
)
spk_transcript = gr.Textbox(
label="参考音频精确转写(可选,提升还原度)",
placeholder="尽量与参考音频内容完全一致...",
placeholder="示例:今天开了三单,第一单手贱提前平了,第二单…",
info="请尽量与参考音频内容完全一致,可提升音色还原度",
lines=6,
elem_classes=["bright-input"],
)
lock_btn = gr.Button("🔒 锁定音色", variant="primary")
lock_log = gr.Textbox(label="锁定结果", lines=4, interactive=False)
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="8" y="8" width="48" height="48" rx="12" fill="#2563eb"/>
<path d="M32 18v22M32 40v4" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
<path d="M22 32h20" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
<path d="M32 18l-8 8h16l-8-8z" fill="#93c5fd"/>
<rect x="14" y="46" width="36" height="6" rx="3" fill="#1e40af"/>
</svg>

After

Width:  |  Height:  |  Size: 430 B