mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +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>
76 lines
1.9 KiB
React
76 lines
1.9 KiB
React
import { useRef, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
function AudioButton({ base64, mimeType = 'audio/mp3', label = '🔊' }) {
|
|
const src = base64
|
|
? `data:${mimeType};base64,${base64}`
|
|
: null;
|
|
|
|
if (!src) return null;
|
|
return (
|
|
<audio
|
|
className="audio-btn"
|
|
controls
|
|
src={src}
|
|
title={label}
|
|
preload="none"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function Bubble({ role, text, audioBase64, audioMime }) {
|
|
const isUser = role === 'user';
|
|
return (
|
|
<div className={`bubble ${isUser ? 'bubble-user' : 'bubble-bot'}`}>
|
|
<div className="bubble-label">{isUser ? '你' : 'JBOT'}</div>
|
|
<div className="bubble-text">{text}</div>
|
|
{audioBase64 && (
|
|
<AudioButton base64={audioBase64} mimeType={audioMime || 'audio/mp3'} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ChatPanel({ messages, voiceStatus }) {
|
|
const bottomRef = useRef(null);
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages]);
|
|
|
|
return (
|
|
<section className="chat-panel">
|
|
<div className="chat-header">
|
|
<span className="chat-title">对话</span>
|
|
<button className="history-link" onClick={() => navigate('/history')}>
|
|
历史记录 →
|
|
</button>
|
|
</div>
|
|
|
|
{voiceStatus && (
|
|
<div className={`voice-status voice-status-${voiceStatus.type}`}>
|
|
{voiceStatus.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="chat-messages">
|
|
{messages.length === 0 ? (
|
|
<p className="chat-empty">按住 Space 开始说话…</p>
|
|
) : (
|
|
messages.map((m) => (
|
|
<Bubble
|
|
key={m.id}
|
|
role={m.role}
|
|
text={m.text}
|
|
audioBase64={m.audioBase64}
|
|
audioMime={m.audioMime}
|
|
/>
|
|
))
|
|
)}
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|