feat(desktop): JARVIS 设置面板——名字/人设/自带豆包配置(用户级)
- lib/api.ts: getMyJarvis/saveMyJarvis + JarvisConfig 类型 - shell/JarvisSettings.tsx: 弹窗——名字 + 语气人设 + 折叠「高级:我的豆包配置」 (api_key/asr/tts resource-id/音色;掩码不回传=沿用已存;填齐才覆盖系统) - VoiceDock: 麦克风上方加设置齿轮,打开面板 真机验证:面板正确加载后端配置(名字=星期五、人设),高级区四项占位清晰,存档下次说话生效。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dialog } from "../ui/Dialog";
|
||||
import { Button } from "../ui/Button";
|
||||
import { useToast } from "../ui/Toast";
|
||||
import { getMyJarvis, saveMyJarvis, type JarvisConfig } from "../lib/api";
|
||||
|
||||
// 每用户 JARVIS 设置:名字 / 人设 / (高级)自带豆包配置。
|
||||
// 名字与人设归用户自己;豆包配置齐全则语音走用户的账号,否则走系统兜底。
|
||||
export function JarvisSettings({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const toast = useToast();
|
||||
const [cfg, setCfg] = useState<JarvisConfig | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setCfg(null);
|
||||
getMyJarvis()
|
||||
.then(setCfg)
|
||||
.catch((e) => toast.push("error", (e as Error).message));
|
||||
}, [open, toast]);
|
||||
|
||||
const set = (k: keyof JarvisConfig, v: string) => setCfg((c) => (c ? { ...c, [k]: v } : c));
|
||||
|
||||
const onSave = async () => {
|
||||
if (!cfg) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveMyJarvis({
|
||||
name: cfg.name,
|
||||
persona: cfg.persona,
|
||||
api_key: cfg.api_key.includes("•") ? "" : cfg.api_key, // 掩码=没改动,留空沿用已存
|
||||
asr_resource_id: cfg.asr_resource_id,
|
||||
tts_resource_id: cfg.tts_resource_id,
|
||||
tts_voice_type: cfg.tts_voice_type,
|
||||
});
|
||||
toast.push("success", "已保存,下次说话即生效");
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast.push("error", (e as Error).message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title="JARVIS 设置"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={onSave} disabled={saving || !cfg}>
|
||||
{saving ? "保存中…" : "保存"}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!cfg ? (
|
||||
<div className="text-slate-500">加载中…</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<label className="block">
|
||||
<span className="text-xs text-slate-400">助手名字</span>
|
||||
<input
|
||||
className="mt-1 w-full rounded-md border border-line bg-ink-850 px-3 py-2 text-slate-100 focus:border-brand focus:outline-none"
|
||||
value={cfg.name}
|
||||
onChange={(e) => set("name", e.target.value)}
|
||||
placeholder="JARVIS(留空用默认;可改成星期五、小助手…)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs text-slate-400">语气 / 人设</span>
|
||||
<textarea
|
||||
rows={3}
|
||||
className="mt-1 w-full resize-none rounded-md border border-line bg-ink-850 px-3 py-2 text-slate-100 focus:border-brand focus:outline-none"
|
||||
value={cfg.persona}
|
||||
onChange={(e) => set("persona", e.target.value)}
|
||||
placeholder="例:简洁专业、平和有礼;或活泼幽默一点。留空用默认平和语气。"
|
||||
/>
|
||||
<span className="mt-1 block text-[11px] text-slate-500">只作用于语音助手,和你的主偏好记忆分开、互不影响。</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-slate-400 transition hover:text-slate-200"
|
||||
onClick={() => setAdvanced((v) => !v)}
|
||||
>
|
||||
{advanced ? "▾" : "▸"} 高级:用我自己的豆包(火山)配置
|
||||
<span className="ml-1 text-slate-500">{cfg.has_own_voice ? "(已启用)" : "(默认走系统)"}</span>
|
||||
</button>
|
||||
|
||||
{advanced && (
|
||||
<div className="space-y-3 rounded-md border border-line bg-ink-900/50 p-3">
|
||||
<p className="text-[11px] leading-relaxed text-slate-500">
|
||||
填齐这四项就用你自己的豆包账号跑语音(识别 + 合成);任一留空则整体回落系统配置。
|
||||
</p>
|
||||
<Field label="API Key" type="password" value={cfg.api_key} onChange={(v) => set("api_key", v)} placeholder="留空 = 沿用已存 / 系统" />
|
||||
<Field label="ASR Resource-Id" value={cfg.asr_resource_id} onChange={(v) => set("asr_resource_id", v)} placeholder="volc.bigasr.sauc.duration" />
|
||||
<Field label="TTS Resource-Id" value={cfg.tts_resource_id} onChange={(v) => set("tts_resource_id", v)} placeholder="seed-tts-2.0" />
|
||||
<Field label="音色 voice_type" value={cfg.tts_voice_type} onChange={(v) => set("tts_voice_type", v)} placeholder="zh_male_m191_uranus_bigtts" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
type,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="text-xs text-slate-400">{label}</span>
|
||||
<input
|
||||
type={type}
|
||||
className="mt-1 w-full rounded-md border border-line bg-ink-850 px-3 py-1.5 font-mono text-sm text-slate-100 focus:border-brand focus:outline-none"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user