Files
links/static_src/jbot/pages/Settings.jsx
T
junvandCopilot a87b4643fd feat: port JBOT AI robot face into links as Django app
- Add jbot Django app (jbot/__init__.py, apps.py, urls.py, api_urls.py)
- Python backend (jbot/views.py): TTS 2.0, ASR 1.0 BigASR, LLM/memory via OpenRouter
- React SPA frontend at /jbot/ (static_src/jbot/): RobotFace, ChatPanel, expressions,
  voices, push-to-talk, history, memory, settings pages
- Vite entry jbot: static_src/jbot/main.jsx → dist/jbot.js + dist/jbot.css
- react-router-dom added; BrowserRouter basename=/jbot for SPA routing
- core/settings.py: added jbot to INSTALLED_APPS
- core/urls.py: /jbot/ + /api/jbot/ URL includes
- pyproject.toml: websocket-client>=1.9.0 for BigASR binary WS protocol
- Dockerfile: ffmpeg added to production apt-get for WebM→PCM audio conversion
- k8s/manifest.yaml: VOLC_APP_ID, VOLC_ACCESS_TOKEN, OPENROUTER_API_KEY env vars
  via jbot-credentials Secret; LLM_MODEL, VOLC_TTS_VOICE, VOLC_ASR_RESOURCE defaults

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-17 14:13:37 +10:00

153 lines
5.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getSettings, saveSettings, resetSettings } from '../store/settingsStore';
import { VOICES, DEFAULT_VOICE } from '../data/voices';
export default function Settings() {
const navigate = useNavigate();
const [form, setForm] = useState(() => getSettings());
const [saved, setSaved] = useState(false);
const set = (key, val) => {
setForm(prev => ({ ...prev, [key]: val }));
setSaved(false);
};
const handleSave = () => {
saveSettings(form);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const handleReset = () => {
if (window.confirm('重置所有设置为默认值?')) {
const defaults = resetSettings();
setForm(defaults);
setSaved(false);
}
};
return (
<div className="settings-page">
<div className="settings-toolbar">
<button className="settings-back" onClick={() => navigate('/')}> 返回</button>
<h1 className="settings-heading">全局设置</h1>
<button className="settings-save" onClick={handleSave}>
{saved ? '✓ 已保存' : '保存'}
</button>
</div>
<div className="settings-body">
{/* ── Robot identity ── */}
<section className="settings-section">
<h2 className="settings-section-title">🤖 机器人身份</h2>
<label className="settings-label">
名字
<input
className="settings-input"
value={form.robotName}
onChange={e => set('robotName', e.target.value)}
placeholder="JBOT"
/>
</label>
<label className="settings-label">
人格描述 <span className="settings-hint">会注入到每轮对话的系统提示</span>
<textarea
className="settings-textarea"
rows={3}
value={form.robotPersonality}
onChange={e => set('robotPersonality', e.target.value)}
placeholder="你是一个友好、简洁、偶尔幽默的AI机器人助手。回复控制在80字以内。"
/>
</label>
</section>
{/* ── User context ── */}
<section className="settings-section">
<h2 className="settings-section-title">👤 用户背景</h2>
<label className="settings-label">
关于你 <span className="settings-hint">机器人会了解你的背景更好地回答</span>
<textarea
className="settings-textarea"
rows={3}
value={form.extraContext || ''}
onChange={e => set('extraContext', e.target.value)}
placeholder="例如:我叫小明,是一名软件工程师,在上海工作,对AI和嵌入式系统感兴趣。"
/>
</label>
</section>
{/* ── Language ── */}
<section className="settings-section">
<h2 className="settings-section-title">🌐 语言</h2>
<div className="settings-radio-group">
{[
{ value: 'auto', label: '自动(跟随用户语言)' },
{ value: 'zh-CN', label: '中文' },
{ value: 'en-US', label: 'English' },
].map(opt => (
<label key={opt.value} className="settings-radio-label">
<input
type="radio"
name="language"
value={opt.value}
checked={form.language === opt.value}
onChange={() => set('language', opt.value)}
/>
{opt.label}
</label>
))}
</div>
</section>
{/* ── TTS Voice ── */}
<section className="settings-section">
<h2 className="settings-section-title">🔊 语音音色</h2>
<div className="settings-voice-grid">
{VOICES.map(v => (
<button
key={v.id}
className={`settings-voice-btn ${!form.customVoiceId && form.voiceId === v.id ? 'active' : ''}`}
onClick={() => { set('voiceId', v.id); set('customVoiceId', ''); }}
>
<span className="voice-btn-label">{v.label}</span>
<span className="voice-btn-meta">{v.lang} · {v.gender} · {v.style}</span>
</button>
))}
</div>
<label className="settings-label settings-label-mt">
自定义音色 ID <span className="settings-hint">填写后会覆盖上方选择</span>
<input
className="settings-input"
value={form.customVoiceId || ''}
onChange={e => set('customVoiceId', e.target.value)}
placeholder="例如 zh_female_vv_uranus_bigtts"
/>
</label>
<p className="settings-hint-block">
完整音色列表见{' '}
<a href="https://www.volcengine.com/docs/6561/1257544" target="_blank" rel="noreferrer">
火山引擎 TTS 音色文档
</a>
</p>
</section>
{/* ── Danger zone ── */}
<section className="settings-section settings-section-danger">
<h2 className="settings-section-title">⚠️ 重置</h2>
<button className="settings-reset-btn" onClick={handleReset}>
重置为默认设置
</button>
</section>
</div>
</div>
);
}