From ab2f42bc4229f8ba97a49b44a3597e884c3cd442 Mon Sep 17 00:00:00 2001 From: baizhi958216 <1475289190@qq.com> Date: Thu, 11 Jun 2026 11:14:50 +0800 Subject: [PATCH] feat(web): merge TTS settings into ModelSettingsModal, remove from SettingsModal Signed-off-by: baizhi958216 <1475289190@qq.com> --- app/page.tsx | 16 ++- components/ModelSettingsModal.tsx | 230 +++++++++++++++++++++++++++--- components/SettingsModal.tsx | 199 ++------------------------ 3 files changed, 226 insertions(+), 219 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index b61c88f..59e69e9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1796,21 +1796,23 @@ export default function HomePage() { initialVisionClickEnabled={visionClickEnabled} onClose={() => setSettingsOpen(false)} onSaved={(settings) => { - setTtsConfigured(settings.ttsConfigured); setPlayerName(settings.playerName); setVisionClickEnabled(settings.visionClickEnabled); - if (settings.ttsConfigured && voiceRow >= 0) { - const onIdx = OPTS[voiceRow]!.items.indexOf("开启"); - if (onIdx >= 0) - setSel((s) => s.map((v, j) => (j === voiceRow ? onIdx : v))); - } }} /> )} {modelSettingsOpen && ( setModelSettingsOpen(false)} - onSaved={() => setModelSettingsOpen(false)} + onSaved={(settings) => { + setTtsConfigured(settings.ttsConfigured); + if (settings.ttsConfigured && voiceRow >= 0) { + const onIdx = OPTS[voiceRow]!.items.indexOf("开启"); + if (onIdx >= 0) + setSel((s) => s.map((v, j) => (j === voiceRow ? onIdx : v))); + } + setModelSettingsOpen(false); + }} /> )} diff --git a/components/ModelSettingsModal.tsx b/components/ModelSettingsModal.tsx index 167afb7..659d6a5 100644 --- a/components/ModelSettingsModal.tsx +++ b/components/ModelSettingsModal.tsx @@ -7,6 +7,17 @@ import { readStoredModelConfig, writeStoredModelConfig, } from "@/lib/clientModelConfig"; +import { + clearStoredTtsConfig, + readStoredTtsConfig, + writeStoredTtsConfig, +} from "@/lib/clientTtsConfig"; +import { + findTtsPreset, + PAYG_PRESET_ID, + TTS_KEY_DOC_URL, + TTS_REGION_PRESETS, +} from "@/lib/ttsPresets"; const PROVIDER_OPTIONS: { value: ProviderProtocol | ""; label: string }[] = [ { value: "", label: "自动推断(推荐)" }, @@ -32,7 +43,7 @@ export function ModelSettingsModal({ onSaved, }: { onClose: () => void; - onSaved: () => void; + onSaved: (settings: { ttsConfigured: boolean }) => void; }) { const initial = readStoredModelConfig(); @@ -69,6 +80,22 @@ export function ModelSettingsModal({ const [showKeys, setShowKeys] = useState>({}); const [shown, setShown] = useState(false); + // TTS state + const [initialTts] = useState(() => readStoredTtsConfig()); + const initialKind = findTtsPreset(initialTts?.presetId)?.kind ?? "payg"; + const [keyType, setKeyType] = useState<"token-plan" | "payg">(initialKind); + const [regionId, setRegionId] = useState( + initialKind === "token-plan" + ? (initialTts?.presetId ?? TTS_REGION_PRESETS[0]!.id) + : TTS_REGION_PRESETS[0]!.id, + ); + const [ttsApiKey, setTtsApiKey] = useState(initialTts?.apiKey ?? ""); + const [showTtsKey, setShowTtsKey] = useState(false); + + const expectedPrefix = keyType === "payg" ? "sk-" : "tp-"; + const prefixMismatch = + ttsApiKey.trim().length > 0 && !ttsApiKey.trim().startsWith(expectedPrefix); + useEffect(() => { const id = requestAnimationFrame(() => setShown(true)); return () => cancelAnimationFrame(id); @@ -99,36 +126,51 @@ export function ModelSettingsModal({ const save = () => { const [text, image, vision] = groups; - writeStoredModelConfig({ - textBaseUrl: text.baseUrl, - textApiKey: text.apiKey, - textModel: text.model, - textProvider: (text.provider as ProviderProtocol) || undefined, - imageBaseUrl: image.baseUrl, - imageApiKey: image.apiKey, - imageModel: image.model, - imageProvider: (image.provider as ProviderProtocol) || undefined, - visionBaseUrl: vision.baseUrl, - visionApiKey: vision.apiKey, - visionModel: vision.model, - visionProvider: (vision.provider as ProviderProtocol) || undefined, - }); - onSaved(); + if (text && image && vision) { + writeStoredModelConfig({ + textBaseUrl: text.baseUrl, + textApiKey: text.apiKey, + textModel: text.model, + textProvider: (text.provider as ProviderProtocol) || undefined, + imageBaseUrl: image.baseUrl, + imageApiKey: image.apiKey, + imageModel: image.model, + imageProvider: (image.provider as ProviderProtocol) || undefined, + visionBaseUrl: vision.baseUrl, + visionApiKey: vision.apiKey, + visionModel: vision.model, + visionProvider: (vision.provider as ProviderProtocol) || undefined, + }); + } + + const key = ttsApiKey.trim(); + let ttsConfigured = false; + if (key) { + const presetId = keyType === "payg" ? PAYG_PRESET_ID : regionId; + writeStoredTtsConfig({ presetId, apiKey: key }); + ttsConfigured = true; + } else { + clearStoredTtsConfig(); + } + + onSaved({ ttsConfigured }); close(); }; const clearAll = () => { clearStoredModelConfig(); - setGroups((prev) = + clearStoredTtsConfig(); + setGroups((prev) => prev.map((g) => ({ ...g, baseUrl: "", apiKey: "", model: "", provider: "" })), ); - onSaved(); + setTtsApiKey(""); + onSaved({ ttsConfigured: false }); close(); }; - const hasAnySetting = groups.some( - (g) => g.baseUrl.trim() && g.apiKey.trim() && g.model.trim(), - ); + const hasAnySetting = + groups.some((g) => g.baseUrl.trim() && g.apiKey.trim() && g.model.trim()) || + initialTts != null; return (
+ {/* ── TTS Key Section ── */} +
+
+ + + + + 自带配音 Key + + 可选 +
+

+ 填入你自己的 + 小米 MiMo API Key + ,配音将在浏览器本地合成,Key 只保存在本地、绝不经过服务器。MiMo + TTS 目前 + 限时免费 + ,申请即可使用。 +

+ +
+ + K e y · 类 型 + +
+ {( + [ + { + kind: "payg", + label: "按量付费 Pay-as-you-go", + sub: "sk- 开头", + }, + { + kind: "token-plan", + label: "套餐 Token Plan", + sub: "tp- 开头", + }, + ] as const + ).map((t) => { + const active = keyType === t.kind; + return ( + + ); + })} +
+
+ + {keyType === "token-plan" && ( +
+ + 区 域 节 点 + +
+ {TTS_REGION_PRESETS.map((p) => { + const active = p.id === regionId; + return ( + + ); + })} +
+ + 选择与你的套餐订阅地区一致的节点(通常也是延迟最低的那个)。 + +
+ )} + +
+ + A P I · K e y + +
+ setTtsApiKey(e.target.value)} + type={showTtsKey ? "text" : "password"} + autoComplete="off" + spellCheck={false} + placeholder={ + keyType === "payg" + ? "粘贴 sk- 开头的按量 Key" + : "粘贴 tp- 开头的套餐 Key" + } + className="h-11 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-11 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" + /> + +
+ {prefixMismatch && ( + + + 此 Key 不是 {expectedPrefix} 开头,可能与所选「 + {keyType === "payg" + ? "按量付费 Pay-as-you-go" + : "套餐 Token Plan"} + 」类型不符,请确认是否填错。 + + )} + + + 如何免费申请 Key?查看图文教程 + +
+
+ +
+

diff --git a/components/SettingsModal.tsx b/components/SettingsModal.tsx index 607a872..44da808 100644 --- a/components/SettingsModal.tsx +++ b/components/SettingsModal.tsx @@ -1,17 +1,6 @@ "use client"; import { type ReactNode, useEffect, useState } from "react"; -import { - clearStoredTtsConfig, - readStoredTtsConfig, - writeStoredTtsConfig, -} from "@/lib/clientTtsConfig"; -import { - findTtsPreset, - PAYG_PRESET_ID, - TTS_KEY_DOC_URL, - TTS_REGION_PRESETS, -} from "@/lib/ttsPresets"; const PLAYER_NAME_STORAGE_KEY = "infiplot:playerName"; const VISION_CLICK_STORAGE_KEY = "infiplot:visionClick"; @@ -52,30 +41,14 @@ export function SettingsModal({ }: { initialVisionClickEnabled?: boolean; onClose: () => void; - onSaved: (settings: { ttsConfigured: boolean; playerName: string; visionClickEnabled: boolean }) => void; + onSaved: (settings: { playerName: string; visionClickEnabled: boolean }) => void; footerNote?: ReactNode; }) { - const [initialTts] = useState(() => readStoredTtsConfig()); - const initialKind = findTtsPreset(initialTts?.presetId)?.kind ?? "payg"; - const [keyType, setKeyType] = useState<"token-plan" | "payg">(initialKind); - const [regionId, setRegionId] = useState( - initialKind === "token-plan" - ? (initialTts?.presetId ?? TTS_REGION_PRESETS[0]!.id) - : TTS_REGION_PRESETS[0]!.id, - ); - const [apiKey, setApiKey] = useState(initialTts?.apiKey ?? ""); - const [showKey, setShowKey] = useState(false); - const ttsAlreadyConfigured = initialTts != null; - const [playerName, setPlayerName] = useState(() => readStoredPlayerName()); const [visionClick, setVisionClick] = useState(initialVisionClickEnabled); const [shown, setShown] = useState(false); - const expectedPrefix = keyType === "payg" ? "sk-" : "tp-"; - const prefixMismatch = - apiKey.trim().length > 0 && !apiKey.trim().startsWith(expectedPrefix); - useEffect(() => { const id = requestAnimationFrame(() => setShown(true)); return () => cancelAnimationFrame(id); @@ -94,30 +67,18 @@ export function SettingsModal({ localStorage.setItem(VISION_CLICK_STORAGE_KEY, visionClick ? "1" : "0"); } catch { /* ignore */ } - const key = apiKey.trim(); - let ttsConfigured = false; - if (key) { - const presetId = keyType === "payg" ? PAYG_PRESET_ID : regionId; - writeStoredTtsConfig({ presetId, apiKey: key }); - ttsConfigured = true; - } else { - clearStoredTtsConfig(); - ttsConfigured = false; - } - - onSaved({ ttsConfigured, playerName: name, visionClickEnabled: visionClick }); + onSaved({ playerName: name, visionClickEnabled: visionClick }); close(); }; const clearAll = () => { - clearStoredTtsConfig(); writeStoredPlayerName(""); try { localStorage.removeItem(VISION_CLICK_STORAGE_KEY); } catch { /* ignore */ } - onSaved({ ttsConfigured: false, playerName: "", visionClickEnabled: true }); + onSaved({ playerName: "", visionClickEnabled: true }); close(); }; - const hasAnySetting = ttsAlreadyConfigured || readStoredPlayerName().length > 0; + const hasAnySetting = readStoredPlayerName().length > 0; return (

-
- - {/* ── TTS Key Section ── */} -
-
- - - - - 自带配音 Key - - 可选 -
-

- 填入你自己的 - 小米 MiMo API Key - ,配音将在浏览器本地合成,Key 只保存在本地、绝不经过服务器。MiMo - TTS 目前 - 限时免费 - ,申请即可使用。 -

- -
- - K e y · 类 型 - -
- {( - [ - { - kind: "payg", - label: "按量付费 Pay-as-you-go", - sub: "sk- 开头", - }, - { - kind: "token-plan", - label: "套餐 Token Plan", - sub: "tp- 开头", - }, - ] as const - ).map((t) => { - const active = keyType === t.kind; - return ( - - ); - })} -
-
- - {keyType === "token-plan" && ( -
- - 区 域 节 点 - -
- {TTS_REGION_PRESETS.map((p) => { - const active = p.id === regionId; - return ( - - ); - })} -
- - 选择与你的套餐订阅地区一致的节点(通常也是延迟最低的那个)。 - -
- )} - -
- - A P I · K e y - -
- setApiKey(e.target.value)} - type={showKey ? "text" : "password"} - autoComplete="off" - spellCheck={false} - placeholder={ - keyType === "payg" - ? "粘贴 sk- 开头的按量 Key" - : "粘贴 tp- 开头的套餐 Key" - } - className="h-11 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-11 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" - /> - -
- {prefixMismatch && ( - - - 此 Key 不是 {expectedPrefix} 开头,可能与所选「 - {keyType === "payg" - ? "按量付费 Pay-as-you-go" - : "套餐 Token Plan"} - 」类型不符,请确认是否填错。 - - )} - - - 如何免费申请 Key?查看图文教程 - -
- - {footerNote && ( + {footerNote && ( +

{footerNote}

- )} -
+
+ )}
{/* Footer */}