mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
- 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>
153 lines
5.5 KiB
React
153 lines
5.5 KiB
React
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>
|
||
);
|
||
}
|