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>
89 lines
2.5 KiB
React
89 lines
2.5 KiB
React
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { getHistory, clearHistory, deleteEntry } from '../store/historyStore';
|
|
|
|
function AudioButton({ base64, mimeType = 'audio/mp3' }) {
|
|
if (!base64) return null;
|
|
return (
|
|
<audio
|
|
className="hist-audio"
|
|
controls
|
|
src={`data:${mimeType};base64,${base64}`}
|
|
preload="none"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function HistoryEntry({ entry, onDelete }) {
|
|
const ts = new Date(entry.timestamp);
|
|
const timeStr = ts.toLocaleString('zh-CN', {
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
});
|
|
return (
|
|
<article className="hist-entry">
|
|
<div className="hist-meta">
|
|
<span className="hist-time">{timeStr}</span>
|
|
<button className="hist-del" onClick={() => onDelete(entry.id)} title="删除">✕</button>
|
|
</div>
|
|
|
|
<div className="hist-row hist-row-user">
|
|
<div className="hist-role">你</div>
|
|
<div className="hist-body">
|
|
<p className="hist-text">{entry.userText || '(语音消息)'}</p>
|
|
<AudioButton base64={entry.userAudioBase64} mimeType={entry.userAudioMime || 'audio/webm'} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="hist-row hist-row-bot">
|
|
<div className="hist-role">JBOT</div>
|
|
<div className="hist-body">
|
|
<p className="hist-text">{entry.botText || ''}</p>
|
|
<AudioButton base64={entry.botAudioBase64} mimeType="audio/mp3" />
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
export default function History() {
|
|
const [entries, setEntries] = useState(() => getHistory());
|
|
const navigate = useNavigate();
|
|
|
|
const handleDelete = (id) => {
|
|
deleteEntry(id);
|
|
setEntries(getHistory());
|
|
};
|
|
|
|
const handleClear = () => {
|
|
if (window.confirm('确认清空所有历史记录?')) {
|
|
clearHistory();
|
|
setEntries([]);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="hist-page">
|
|
<div className="hist-toolbar">
|
|
<button className="hist-back" onClick={() => navigate('/')}>← 返回</button>
|
|
<h1 className="hist-heading">历史对话</h1>
|
|
<button
|
|
className="hist-clear"
|
|
onClick={handleClear}
|
|
disabled={entries.length === 0}
|
|
>清空</button>
|
|
</div>
|
|
|
|
{entries.length === 0 ? (
|
|
<p className="hist-empty">暂无历史记录</p>
|
|
) : (
|
|
<div className="hist-list">
|
|
{entries.map(e => (
|
|
<HistoryEntry key={e.id} entry={e} onDelete={handleDelete} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|