"use client"; // Bring-your-own Xiaomi MiMo TTS key modal — shared by the homepage and the // play page. Two-step picker (key family → region for Token Plan only), key // stored CLIENT-SIDE ONLY (see lib/clientTtsConfig). `onSaved(configured)` // fires after a save/disable so each host can react (homepage flips the // 语音配音 toggle; the play page re-synthesizes the current scene in-browser). // `footerNote` lets the host tailor the closing hint to its own context. 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 DEFAULT_FOOTER_NOTE: ReactNode = "提示:需将上方「语音配音」设为「开启」配音才会生效。保存后本设备后续游玩会自动使用此 Key。"; export function TtsKeyModal({ onClose, onSaved, footerNote = DEFAULT_FOOTER_NOTE, }: { onClose: () => void; onSaved: (configured: boolean) => void; footerNote?: ReactNode; }) { // Read storage once; useState initializers ignore later renders, so local // edits aren't clobbered and we don't re-hit localStorage every render. const [initial] = useState(() => readStoredTtsConfig()); // Two-step picker: choose key family first, then — only for Token Plan — a // region. Pay-as-you-go (`sk-`) keys hit one fixed endpoint, so no region. const initialKind = findTtsPreset(initial?.presetId)?.kind ?? "token-plan"; const [keyType, setKeyType] = useState<"token-plan" | "payg">(initialKind); const [regionId, setRegionId] = useState( initialKind === "token-plan" ? (initial?.presetId ?? TTS_REGION_PRESETS[0]!.id) : TTS_REGION_PRESETS[0]!.id, ); const [apiKey, setApiKey] = useState(initial?.apiKey ?? ""); const [showKey, setShowKey] = useState(false); const [shown, setShown] = useState(false); const alreadyConfigured = initial != null; // Soft guard: tp- keys belong to Token Plan, sk- to pay-as-you-go. A // mismatched pairing hits the wrong endpoint → guaranteed auth failure → // silent playback (the very symptom BYO exists to kill). Warn, but never // block: prefix conventions could change and a hard gate would lock out an // otherwise-valid key. 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); }, []); const close = () => { setShown(false); setTimeout(onClose, 280); }; const save = () => { const key = apiKey.trim(); if (!key) return; const presetId = keyType === "payg" ? PAYG_PRESET_ID : regionId; writeStoredTtsConfig({ presetId, apiKey: key }); onSaved(true); close(); }; const disable = () => { clearStoredTtsConfig(); onSaved(false); close(); }; return (
e.stopPropagation()} className={ "flex w-[560px] max-w-[94vw] max-h-[88vh] flex-col overflow-hidden rounded-sm border border-clay-900/15 bg-cream-50 shadow-2xl shadow-clay-900/25 transition-all duration-300 " + (shown ? "opacity-100 scale-100" : "opacity-0 scale-95") } >
自带配音 Key 可选 · 用你自己的小米 MiMo 免费额度,配音更稳定、延迟更低

经常没有声音?公共语音模型有调用频率限额(RPM / TPM),同时游玩的人多时很容易撞到限额而静音。填入你自己的小米 MiMo API Key 后,配音将 直接在你的浏览器里合成 、使用你自己的免费额度 ——{" "} Key 只保存在本地浏览器、绝不经过我们的服务器

K e y · 类 型
{( [ { kind: "token-plan", label: "套餐 Token Plan", sub: "tp- 开头" }, { kind: "payg", label: "按量付费 Pay-as-you-go", sub: "sk- 开头" }, ] as const ).map((t) => { const active = keyType === t.kind; return ( ); })}
{keyType === "token-plan" ? (
区 域 节 点
{TTS_REGION_PRESETS.map((p) => { const active = p.id === regionId; return ( ); })}
选择与你的套餐订阅地区一致的节点(通常也是延迟最低的那个)。
) : (
按量付费使用统一端点{" "} api.xiaomimimo.com ,无需选择区域。
)}
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}

{alreadyConfigured && ( )}
); }