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="icon" href="/pwa/icons/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/pwa/icons/icon.svg">
|
<link rel="apple-touch-icon" href="/pwa/icons/icon.svg">
|
||||||
<script>
|
<script>
|
||||||
if ("serviceWorker" in navigator) {
|
(function () {
|
||||||
|
var deferredPrompt = null;
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener("load", function () {
|
window.addEventListener("load", function () {
|
||||||
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(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>
|
</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 = """
|
CUSTOM_CSS = """
|
||||||
/* ========== 居中布局 + 响应式 + 高对比度 ========== */
|
/* ========== 居中布局 + 响应式 + 高对比度 ========== */
|
||||||
html, body {
|
html, body {
|
||||||
@@ -348,10 +433,168 @@ gradio-app,
|
|||||||
line-height: 1.6 !important;
|
line-height: 1.6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* placeholder 高对比度 */
|
||||||
.gradio-container textarea::placeholder,
|
.gradio-container textarea::placeholder,
|
||||||
.gradio-container input::placeholder {
|
.gradio-container input::placeholder,
|
||||||
color: #9ca3af !important;
|
.gradio-container input[type="text"]::placeholder {
|
||||||
|
color: #d1d5db !important;
|
||||||
opacity: 1 !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 {
|
.gradio-container .wrap .readonly textarea {
|
||||||
@@ -512,6 +755,7 @@ def build_app() -> gr.Blocks:
|
|||||||
with gr.Blocks(
|
with gr.Blocks(
|
||||||
title="Trading Studio | 交易复盘配音中控",
|
title="Trading Studio | 交易复盘配音中控",
|
||||||
) as demo:
|
) as demo:
|
||||||
|
with gr.Row(elem_classes=["header-row"]):
|
||||||
gr.Markdown(
|
gr.Markdown(
|
||||||
f"""
|
f"""
|
||||||
# ⚡ Trading Studio
|
# ⚡ Trading Studio
|
||||||
@@ -525,11 +769,10 @@ def build_app() -> gr.Blocks:
|
|||||||
| ChatTTS | 本地 GPU 固定音色合成 |
|
| ChatTTS | 本地 GPU 固定音色合成 |
|
||||||
|
|
||||||
> 仓库: [{GIT_REPO_URL}]({GIT_REPO_URL})
|
> 仓库: [{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"]):
|
with gr.Row(elem_classes=["status-row"]):
|
||||||
ollama_status = gr.HTML(value=_status_html("Ollama 节点", "正在检测...", "warn"))
|
ollama_status = gr.HTML(value=_status_html("Ollama 节点", "正在检测...", "warn"))
|
||||||
@@ -544,9 +787,12 @@ def build_app() -> gr.Blocks:
|
|||||||
with gr.Tabs():
|
with gr.Tabs():
|
||||||
# ---- Tab 1: 音色锁定 ----
|
# ---- Tab 1: 音色锁定 ----
|
||||||
with gr.Tab("🎙️ 音色锁定"):
|
with gr.Tab("🎙️ 音色锁定"):
|
||||||
gr.Markdown(
|
gr.HTML(
|
||||||
"上传 **10-30 秒** 干净人声样本,系统将提取 Speaker Embedding "
|
f'<div class="hint-box">'
|
||||||
f"并保存至 `{SPEAKER_EMB_PATH.name}`,后续合成 100% 还原音色。"
|
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():
|
with gr.Row():
|
||||||
spk_audio = gr.Audio(
|
spk_audio = gr.Audio(
|
||||||
@@ -556,8 +802,10 @@ def build_app() -> gr.Blocks:
|
|||||||
)
|
)
|
||||||
spk_transcript = gr.Textbox(
|
spk_transcript = gr.Textbox(
|
||||||
label="参考音频精确转写(可选,提升还原度)",
|
label="参考音频精确转写(可选,提升还原度)",
|
||||||
placeholder="尽量与参考音频内容完全一致...",
|
placeholder="示例:今天开了三单,第一单手贱提前平了,第二单…",
|
||||||
|
info="请尽量与参考音频内容完全一致,可提升音色还原度",
|
||||||
lines=6,
|
lines=6,
|
||||||
|
elem_classes=["bright-input"],
|
||||||
)
|
)
|
||||||
lock_btn = gr.Button("🔒 锁定音色", variant="primary")
|
lock_btn = gr.Button("🔒 锁定音色", variant="primary")
|
||||||
lock_log = gr.Textbox(label="锁定结果", lines=4, interactive=False)
|
lock_log = gr.Textbox(label="锁定结果", lines=4, interactive=False)
|
||||||
|
|||||||
@@ -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 |
Reference in New Issue
Block a user