fix: restore frontend Chinese encoding

This commit is contained in:
millionart committed 2026-05-07 12:41:24 +08:00
1 parent 70455aa2a1
commit e0f7ca2d78
2 files changed
+54 -54

No files matched your search

+9 -9
View File
@@ -1,9 +1,9 @@
// SelectionAsk.tsx 鈥?鐙珛鐨?鍒掕瘝杩介棶"椤碉紙issue #118 / PR #119 閰嶇疆 UI 鎷嗗垎鐗堬級銆?
// 鍔熻兘锛氱敤鎴峰湪浠绘剰 app 閫変腑涓€娈垫枃瀛?鈫?鎸?hotkey 鈫?娴獥寮瑰嚭 + 杩涘叆璇煶褰曢煶 鈫?
// 鐢ㄦ埛鍙h堪鎻愰棶 鈫?ASR + 閫夊尯 + 鎻愰棶 涓€璧烽€?LLM 鈫?绛旀浠?markdown 鏄剧ず鍦ㄦ诞绐椼€?
// SelectionAsk.tsx — 独立的"划词追问"页(issue #118 / PR #119 配置 UI 拆分版)。
// 功能:用户在任意 app 选中一段文字 → 按 hotkey → 浮窗弹出 + 进入语音录音 →
// 用户口述提问 → ASR + 选区 + 提问 一起送 LLM → 答案以 markdown 显示在浮窗。
//
// 杩欎竴椤垫妸鍘熸湰鏁e湪 Settings 鈫?褰曢煶 閲岀殑涓ゆ潯閰嶇疆锛坔otkey 棰勮 / 淇濆瓨 Q&A 鍘嗗彶锛?
// 闆嗕腑璧锋潵 + 鍔犲畬鏁翠娇鐢ㄦ寚鍗楋紝璺?缈昏瘧"椤靛钩绾с€?
// 这一页把原本散在 Settings → 录音 里的两条配置(hotkey 预设 / 保存 Q&A 历史)
// 集中起来 + 加完整使用指南,跟"翻译"页平级。
import { useTranslation } from 'react-i18next';
import { Card, PageHeader } from './_atoms';
@@ -55,7 +55,7 @@ export function SelectionAsk() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 1. 瑙﹀彂蹇嵎閿?*/}
{/* 1. 触发快捷键 */}
<Card>
<CardHeaderToggle
title={t('selectionAsk.hotkey.title')}
@@ -80,7 +80,7 @@ export function SelectionAsk() {
)}
</Card>
{/* 2. 鍘嗗彶淇濆瓨 */}
{/* 2. 历史保存 */}
<Card>
<CardHeaderToggle
title={t('selectionAsk.history.title')}
@@ -92,7 +92,7 @@ export function SelectionAsk() {
</div>
</Card>
{/* 3. 浣跨敤鏂规硶 */}
{/* 3. 使用方法 */}
<Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 10 }}>{t('selectionAsk.howto.title')}</div>
<ol style={{ margin: 0, paddingLeft: 18, fontSize: 12.5, color: 'var(--ol-ink-2)', lineHeight: 1.7 }}>
@@ -138,7 +138,7 @@ export function SelectionAsk() {
</>
);
}
// 鍗$墖鏍囬琛屽彸渚у紑鍏筹細涓?Style 椤甸潰椤舵爮鐨?36脳20 toggle 鍚屾锛屼繚鎸佸叏灞€瑙嗚涓€鑷淬€?
// 卡片标题行右侧开关:与 Style 页面顶栏的 36×20 toggle 同款,保持全局视觉一致。
function CardHeaderToggle({
title,
checked,
+45 -45
View File
@@ -1,4 +1,4 @@
// Settings.tsx 鈥?ported verbatim from design_handoff_openless/pages.jsx::Settings.
// Settings.tsx — ported verbatim from design_handoff_openless/pages.jsx::Settings.
// Internal sub-sections (Recording / Providers / Shortcuts / Permissions / Language / About)
// keep their inline-style literals 1:1 with the source JSX.
@@ -69,15 +69,15 @@ import {
type LocalAsrSettings,
} from '../lib/localAsr';
/// Settings 鈫?ASR 閫変簡 local-qwen3 鏃惰Е鍙戣烦鍒般€屾ā鍨嬭缃€嶉〉 + 鍏?Settings modal銆?
/// FloatingShell 鐩戝惉鍚屽悕浜嬩欢鍋?setCurrentTab('localAsr') + setSettingsOpen(false)銆?
/// Settings → ASR 选了 local-qwen3 时触发跳到「模型设置」页 + 关 Settings modal。
/// FloatingShell 监听同名事件做 setCurrentTab('localAsr') + setSettingsOpen(false)。
export const NAVIGATE_LOCAL_ASR_EVENT = 'openless:navigate-local-asr';
interface SettingsProps {
embedded?: boolean;
initialSection?: SettingsSectionId;
}
// "鍏充簬" tab 宸茬Щ闄わ紙鍐呭骞跺叆澶栧眰 SettingsModal 鐨?About 椤碉紝閬垮厤璁剧疆鍐呭閲嶅鍏ュ彛锛夈€?
// "关于" tab 已移除(内容并入外层 SettingsModal 的 About 页,避免设置内外重复入口)。
export type SettingsSectionId = 'recording' | 'providers' | 'shortcuts' | 'permissions' | 'language';
const SECTION_ORDER: SettingsSectionId[] = ['recording', 'providers', 'shortcuts', 'permissions', 'language'];
@@ -114,10 +114,10 @@ export function Settings({ embedded = false, initialSection = 'recording' }: Set
desc={t('settings.desc')}
/>
)}
{/* embedded锛堝湪 SettingsModal 閲岋級妯″紡涓嬶細mini-sidebar 鍥哄畾锛屼粎鍙虫爮 scroll銆?
澶栧眰 flex:1 minHeight:0 璁?grid 鎷垮埌纭畾楂樺害锛沢ridTemplateRows: minmax(0, 1fr)
寮哄埗琛岄珮绛変簬瀹瑰櫒楂樺害锛屽惁鍒?grid 榛樿 auto rows 浼氳窡鍐呭闀匡紝鍙虫爮 overflow:auto
灏遍€€鍖栨垚"娌′笢瑗块渶瑕?scroll"锛屼簬鏄ぇ瀹剁収鏃т竴璧烽銆?*/}
{/* embedded(在 SettingsModal 里)模式下:mini-sidebar 固定,仅右栏 scroll。
外层 flex:1 minHeight:0 让 grid 拿到确定高度;gridTemplateRows: minmax(0, 1fr)
强制行高等于容器高度,否则 grid 默认 auto rows 会跟内容长,右栏 overflow:auto
就退化成"没东西需要 scroll",于是大家照旧一起飘。 */}
<div
style={{
display: 'grid',
@@ -952,9 +952,9 @@ function AutostartRow() {
const { t } = useTranslation();
const [enabled, setEnabled] = useState(false);
const [loaded, setLoaded] = useState(false);
// 鍒?plist / 娉ㄥ唽琛ㄥけ璐ユ椂缁欑敤鎴风湅鐨勯敊璇€俷ull = 娌℃湁澶辫触/涓婃鎿嶄綔宸叉垚鍔熴€?
// 涓嶆覆鏌撶瓑浜庢妸澶辫触鍚炴帀 鈥斺€?Windows 鍐?HKCU\Run 琚粍绛栫暐鎷︺€乵acOS 鍐?
// LaunchAgent plist 鏉冮檺涓嶅 閮芥槸鐪熷疄鍙兘銆俰ssue #194銆?
// 切 plist / 注册表失败时给用户看的错误。null = 没有失败/上次操作已成功。
// 不渲染等于把失败吞掉 —— Windows 写 HKCU\Run 被组策略拦、macOS 写
// LaunchAgent plist 权限不够 都是真实可能。issue #194。
const [error, setError] = useState<string | null>(null);
useEffect(() => {
@@ -1044,11 +1044,11 @@ type LlmPresetId = typeof LLM_PRESETS[number]['id'];
const ASR_DEFAULT_RESOURCE_ID = 'volc.seedasr.sauc.duration';
// `volcengine` 璧拌嚜寤烘祦寮忓鎴风锛涘叾浣欒蛋 OpenAI 鍏煎 `/audio/transcriptions`
// 锛坄coordinator.rs::is_whisper_compatible_provider`锛夈€傛柊澧炲吋瀹瑰巶鍟嗭細
// 1. 鍦ㄨ繖閲屽姞涓€椤?`{ id, nameKey, baseUrl, model }`锛?
// 2. `coordinator.rs::is_whisper_compatible_provider` 鍔犲悓鍚?id锛?
// 3. 鍦?i18n 鐨?`settings.providers.presets.<nameKey>` 鍔犳枃妗堛€?
// `volcengine` 走自建流式客户端;其余走 OpenAI 兼容 `/audio/transcriptions`
// (`coordinator.rs::is_whisper_compatible_provider`)。新增兼容厂商:
// 1. 在这里加一项 `{ id, nameKey, baseUrl, model }`;
// 2. `coordinator.rs::is_whisper_compatible_provider` 加同名 id;
// 3. 在 i18n 的 `settings.providers.presets.<nameKey>` 加文案。
const ASR_PRESETS = [
{ id: 'volcengine', nameKey: 'asrVolcengine', baseUrl: '', model: '' },
{ id: 'siliconflow', nameKey: 'asrSiliconflow', baseUrl: 'https://api.siliconflow.cn/v1', model: 'FunAudioLLM/SenseVoiceSmall' },
@@ -1056,7 +1056,7 @@ const ASR_PRESETS = [
{ id: 'groq', nameKey: 'asrGroq', baseUrl: 'https://api.groq.com/openai/v1', model: 'whisper-large-v3-turbo' },
{ id: 'whisper', nameKey: 'asrWhisper', baseUrl: 'https://api.openai.com/v1', model: 'whisper-1' },
{ id: 'foundry-local-whisper', nameKey: 'asrFoundryLocalWhisper', baseUrl: '', model: '' },
// 鏈湴 Qwen3-ASR锛氭棤 baseUrl/model 閰嶇疆锛屾ā鍨嬪湪銆屾ā鍨嬭缃€嶉〉涓嬭浇涓庡垏鎹€?
// 本地 Qwen3-ASR:无 baseUrl/model 配置,模型在「模型设置」页下载与切换。
{ id: 'local-qwen3', nameKey: 'asrLocalQwen3', baseUrl: '', model: '' },
] as const;
@@ -1065,13 +1065,13 @@ type AsrPresetId = typeof ASR_PRESETS[number]['id'];
function ProvidersSection() {
const { t } = useTranslation();
const { prefs, updatePrefs } = useHotkeySettings();
// `*Provider` 绔嬪嵆璺熼殢 <select> 鏀瑰姩锛堝彈鎺х粍浠跺繀椤诲疄鏃跺弽鏄犵敤鎴疯緭鍏ワ級锛?
// `committed*Provider` 鎵嶅喅瀹?CredentialField 鐨?key锛屼粎鍦ㄥ悗绔?active
// 鍒囨崲 + 榛樿鍊煎啓瀹屽悗鍐?commit銆備袱鑰呮媶寮€鏄负浜嗗悓鏃舵弧瓒筹細
// - <select> 绔嬪埢鏄剧ず鐢ㄦ埛鐨勯€夋嫨锛坕ssue #220 P2锛歝odex 鎸囧嚭鍙楁帶閫変笉搴旂瓑 await锛?
// - CredentialField 涓嶈鍦ㄥ悗绔?active 鍒囧畬鍓?remount锛坕ssue #219锛氶伩鍏嶈鍒版棫 entry锛?
// `*SwitchSeq` 鏄?stale-write 瀹堝崼锛氱敤鎴?100ms 鍐呰繛鐐逛袱娆℃椂锛屽厛鍙戠殑璇锋眰鏅氬埌涓?
// 浼氳鐩栧悗鍙戠殑 commit銆?
// `*Provider` 立即跟随 <select> 改动(受控组件必须实时反映用户输入);
// `committed*Provider` 才决定 CredentialField 的 key,仅在后端 active
// 切换 + 默认值写完后再 commit。两者拆开是为了同时满足:
// - <select> 立刻显示用户的选择(issue #220 P2:codex 指出受控选不应等 await)
// - CredentialField 不要在后端 active 切完前 remount(issue #219:避免读到旧 entry)
// `*SwitchSeq` 是 stale-write 守卫:用户 100ms 内连点两次时,先发的请求晚到不
// 会覆盖后发的 commit。
const [llmProvider, setLlmProvider] = useState<LlmPresetId>('ark');
const [asrProvider, setAsrProvider] = useState<AsrPresetId>('volcengine');
const [committedLlmProvider, setCommittedLlmProvider] = useState<LlmPresetId>('ark');
@@ -1097,12 +1097,12 @@ function ProvidersSection() {
setCommittedAsrProvider(asrId);
}, [prefs, os]);
// issue #219 / #220 P2锛?
// 1. 绔嬪埢 setLlmProvider 鈥斺€?鍙楁帶 <select> 蹇呴』鍙嶆槧鐢ㄦ埛鏈€鏂伴€夋嫨銆?
// 2. 鐢?seq 瀹堝崼姣忎釜 await锛氱敤鎴疯繛鐐逛袱娆℃椂鏃ц姹傛櫄鍒颁篃涓嶄細鐩栨帀鏂伴€夋嫨銆?
// 3. 浠?setCommittedLlmProvider 涔嬪悗 CredentialField 鎵?remount 璇绘柊 entry锛?
// 姝ゆ椂鍚庣 root.active.llm 宸茬粡鏄?id锛宭ookup_account 钀藉埌姝g‘ entry銆?
// 4. endpoint/model 榛樿鍊间粎鍦ㄨ provider entry 璇ュ瓧娈典负绌烘椂鎵嶅~锛屼笉瑕嗙洊鐢ㄦ埛鑷畾涔夈€?
// issue #219 / #220 P2:
// 1. 立刻 setLlmProvider —— 受控 <select> 必须反映用户最新选择。
// 2. 用 seq 守卫每个 await:用户连点两次时旧请求晚到也不会盖掉新选择。
// 3. 仅 setCommittedLlmProvider 之后 CredentialField 才 remount 读新 entry,
// 此时后端 root.active.llm 已经是 id,lookup_account 落到正确 entry。
// 4. endpoint/model 默认值仅在该 provider entry 该字段为空时才填,不覆盖用户自定义。
const onLlmProviderChange = async (id: LlmPresetId) => {
setLlmProvider(id);
const seq = ++llmSwitchSeqRef.current;
@@ -1135,9 +1135,9 @@ function ProvidersSection() {
await updatePrefs(next);
if (seq !== asrSwitchSeqRef.current) return;
}
// OpenAI 鍏煎鍘傚晢棣栨鍒囨崲鏃堕濉?baseUrl / model 榛樿鍊硷紝鐪佸緱鐢ㄦ埛蹇呰俯
// 銆岃法鍘傚晢 model 鍚嶆牴鏈笉涓€鏍枫€嶇殑鍧戯紱浣嗙敤鎴峰凡鑷畾涔夊悗灏变笉鍐嶈鐩栥€?
// volcengine 璧板彟涓€濂楀嚟鎹紝璺宠繃銆?
// OpenAI 兼容厂商首次切换时预填 baseUrl / model 默认值,省得用户必踩
// 「跨厂商 model 名根本不一样」的坑;但用户已自定义后就不再覆盖。
// volcengine 走另一套凭据,跳过。
const preset = ASR_PRESETS.find(p => p.id === id);
if (preset && preset.baseUrl) {
const existing = await readCredential('asr.endpoint');
@@ -1158,9 +1158,9 @@ function ProvidersSection() {
setCommittedAsrProvider(id);
};
// preset 鍐冲畾 placeholder 涓?default 鈥斺€?蹇呴』璺熺潃 committed*Provider 璧帮紝
// 鍚﹀垯鍙楁帶 <select> 绔嬪埢鍒囧埌鏂板巶鍟嗭紝浣嗗嚟鎹瓧娈佃繕鍦ㄦ樉绀烘棫 entry锛宲laceholder
// 浼氬厛浜庡疄闄呮暟鎹垏鎹€佽瑙変笂瀵逛笉涓娿€?
// preset 决定 placeholder 与 default —— 必须跟着 committed*Provider 走,
// 否则受控 <select> 立刻切到新厂商,但凭据字段还在显示旧 entry,placeholder
// 会先于实际数据切换、视觉上对不上。
const preset = LLM_PRESETS.find(p => p.id === committedLlmProvider) ?? LLM_PRESETS[LLM_PRESETS.length - 1];
const asrPreset = visibleAsrPresets.find(p => p.id === committedAsrProvider);
@@ -1779,7 +1779,7 @@ function PermissionsSection() {
refreshHotkey();
refreshWindowsIme();
const hotkeyId = window.setInterval(refreshHotkey, 1000);
// 楹﹀厠椋庢鏌ヤ細鐭殏鎵撳紑杈撳叆娴侊紝閬垮厤姣忕鎺㈡祴瀵艰嚧闅愮鎸囩ず鍣ㄩ绻侀棯鐑併€?
// 麦克风检查会短暂打开输入流,避免每秒探测导致隐私指示器频繁闪烁。
const permissionId = window.setInterval(refreshPermissions, 10000);
const onFocus = () => {
refreshPermissions();
@@ -1942,7 +1942,7 @@ function LanguageSection() {
);
}
// AboutSection 宸茬Щ闄わ細鍐呭骞跺叆 SettingsModal 鐨?AboutMini锛岄伩鍏嶈缃唴澶栦袱涓?鍏充簬"閲嶅鍏ュ彛銆?
// AboutSection 已移除:内容并入 SettingsModal 的 AboutMini,避免设置内外两个"关于"重复入口。
export function AboutUpdateControl({ tagline }: { tagline: string }) {
const { t } = useTranslation();
@@ -2006,9 +2006,9 @@ function adapterDisplayName(adapter: HotkeyCapability['adapter'] | HotkeyStatus[
return i18n.t('hotkey.adapter.rdev');
}
/// 鏈湴 Qwen3-ASR 鍦?Settings 鈫?鏈嶅姟鍟嗗尯閲?*涓?*璁╃敤鎴峰~绌衡€斺€斿睍绀哄綋鍓嶆縺娲绘ā鍨?
/// 鏄惁宸蹭笅杞姐€佸垪鍑烘墍鏈夊凡涓嬭浇妯″瀷 + 鍒犻櫎鎸夐挳锛屽苟鎻愮ず鎬ц兘/璐ㄩ噺棰勬湡锛屽紩瀵艰烦鍒?
/// 銆屾ā鍨嬭缃€嶉〉鍋氫笅杞姐€?
/// 本地 Qwen3-ASR 在 Settings → 服务商区里**不**让用户填空——展示当前激活模型
/// 是否已下载、列出所有已下载模型 + 删除按钮,并提示性能/质量预期,引导跳到
/// 「模型设置」页做下载。
function LocalAsrProviderHint({
provider,
selectedProvider,
@@ -2132,7 +2132,7 @@ function LocalAsrProviderHint({
return (
<div style={{ padding: '8px 0 4px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 鎬ц兘/璐ㄩ噺棰勬湡璀﹀憡 鈥斺€?鐢ㄦ埛纭姹傝鍐欐竻妤?*/}
{/* 性能/质量预期警告 —— 用户硬要求要写清楚 */}
<div
style={{
padding: '10px 12px',
@@ -2142,14 +2142,14 @@ function LocalAsrProviderHint({
color: 'var(--ol-ink-2)',
lineHeight: 1.6,
}}>
鈿狅笍 {t('settings.providers.localAsrPerformanceWarning')}
⚠️ {t('settings.providers.localAsrPerformanceWarning')}
</div>
<div style={{ fontSize: 12.5, color: 'var(--ol-ink-3)', lineHeight: 1.6 }}>
{t(hintKey)}
</div>
{/* 褰撳墠婵€娲绘ā鍨嬬姸鎬?+ 璺宠浆鎸夐挳 */}
{/* 当前激活模型状态 + 跳转按钮 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<Pill tone={isReady ? 'ok' : 'outline'} size="sm">
{isReady
@@ -2163,7 +2163,7 @@ function LocalAsrProviderHint({
</Btn>
</div>
{/* 宸蹭笅杞芥ā鍨嬪垪琛?+ 鍒犻櫎鎸夐挳锛堢敤鎴凤細宸蹭笅杞界殑椤圭洰瑕佸湪鏃佽竟鏄剧ず + 鎻愪緵鍒犻櫎锛?*/}
{/* 已下载模型列表 + 删除按钮(用户:已下载的项目要在旁边显示 + 提供删除) */}
{downloaded.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ol-ink-4)', letterSpacing: '.04em', textTransform: 'uppercase' }}>