Fix hint visibility and add PWA install button with one-click prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user