- Use a thinking/running face while speech audio is being generated
- Switch to speaking only when playback actually begins
- Add quota-safe history persistence that strips audio from older entries
instead of crashing localStorage writes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Show a dedicated '生成语音中…' state while TTS is being generated
- Only switch to '播放中…' once audio is actually ready/playing
- Strip markdown formatting from the spoken text while preserving the raw
assistant message in the chat UI
- Render assistant messages as lightweight markdown in the bubble view
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes:
1. Race condition: after await startRec(), check isSpaceDownRef again —
fast-tap released before mic ready was leaving phase stuck at 'listening'
2. Global window mouseup/touchend/touchcancel listeners as fallback so
releasing outside the button boundary always triggers handleSpaceUp
3. Show ✕ cancel button during ALL non-idle phases (was only thinking/speaking)
4. handleCancel now stops the recorder if called during listening/acquiring
5. Safety auto-cancel timeout: if stuck in 'listening' > 60s, auto-stop
6. Add stopRec to handleSpaceDown dep array
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- VoiceButton gets a red ✕ cancel button that appears during thinking/speaking
- AbortController per pipeline run — ✕ aborts the in-flight apiChat fetch,
stops TTS playback, and returns to idle cleanly
- Hermes connection-refused error shows readable message instead of raw stack
- apiChat() accepts optional AbortSignal (4th arg), passed from App.jsx
- handleSpaceUp dep array updated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In Hermes mode JBOT is a pure voice UI — memory is owned by the Agent:
- Backend: sys prompt excludes local memories when backend_mode=hermes
- Backend: config_view now returns backendMode so the frontend can adapt
- Frontend: App.jsx loads backendMode on mount via apiConfig()
- Frontend: memories not passed to apiChat() when isHermesMode
- Frontend: memory extraction (apiExtractMemory) skipped when isHermesMode
- Frontend: MemoryPanel hidden in Hermes mode; replaced by subtle
'🧠 Hermes Agent 模式 · 记忆由 Agent 管理' badge
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- New DB fields: hermes_url, hermes_api_key, backend_mode (openrouter/hermes)
- Migration 0002 adds the three fields to JbotApiConfig
- _do_hermes_chat(): streams /v1/chat/completions from Hermes Agent,
collects hermes.tool.progress SSE events, returns (final_text, tool_calls)
- chat_view branches on backend_mode; Hermes response includes toolCalls list
- Settings page: new '🧠 AI 后端模式' section — radio to toggle mode,
conditional Hermes URL + API key fields, LLM model field moved under
OpenRouter mode
- ChatPanel: ToolCallsList component renders collapsible '🔧 Hermes 使用了
N 个工具' widget before the bot reply — tool calls are NOT spoken via TTS
- CSS: .tool-calls, .tool-calls-* styles for the tool progress widget
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
right-footer served no purpose after hint text was removed. vbtn-wrap
is now a direct flex child of right-col: white bar at the bottom on
desktop, position:fixed on mobile — no stray white boxes anywhere.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merged hint text + VoiceButton into a single .right-footer div
(flex column, white bg, border-top) so they form one solid footer bar
visually anchored to the bottom of the right column — no more floating
- .right-col gets min-height:0 so flex children can shrink properly
- .vbtn-wrap is now a transparent width:100% wrapper inside right-footer
(desktop); mobile keeps its own fixed-position overlay
- .right-footer { display: none } on mobile (fixed vbtn-wrap takes over)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove 'overflow:hidden' from html/body in index.css — it was blocking
scroll on all pages (Settings, History). The chat page is self-contained
via .app {height:100dvh; overflow:hidden} so body lock is unnecessary.
.app reverted back to height:100dvh (self-sufficient, not body-relative)
- Remove duplicate 历史记录 link from the right-column hint bar; it
already appears in the ChatPanel header, so three links was redundant
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move MemoryPanel from bottom of left column to top of right column
(above chat messages, with a clean border-bottom separator)
- Move hint text ('按住 Space 或按钮说话 · 音色') from left column
to a compact bar on the right column between chat and voice button;
hidden on mobile to save space
- Remove 切换间隔 slider from left column; add it to Settings page
under 🤖 机器人身份 section with proper slider+value layout
- Add switchIntervalMs:10000 to DEFAULT_SETTINGS in settingsStore so
the value persists across sessions; App.jsx reads it on mount
- Left column is now much leaner: face + interaction buttons + micro
expression grid + auto-rotate toggle only
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Left column: scrollbar-width:none + ::-webkit-scrollbar:none to
hide the scrollbar chrome while keeping overflow-y:auto for tall
viewports; tightened padding (14→10px top, 20→14px bottom) and
panel/section gaps (14→10px, 12→8px, h3 margin 7→5px) to help
content fit without scrolling on typical laptop heights
- Header: added 🏠 home anchor linking to '/' (Links homepage) next
to the JBOT logo; styled as a small icon button
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- index.css targeted #root but app mounts to #jbot-root — viewport
height was never constrained, causing full page scroll
- Fix: rename #root → #jbot-root, add html/body {height:100%;overflow:hidden}
- .app now uses height:100% relative to #jbot-root instead of
duplicate 100dvh, so the layout is a true single-screen SPA
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
.chat-panel was using height:100% which consumed all right-col space,
pushing VoiceButton out of view (clipped by overflow:hidden).
Fix: switch .chat-panel to flex:1 + min-height:0 so it participates
in flex layout and leaves room for the VoiceButton below it.
Also moved VoiceButton component outside App() to avoid React
component-identity churn on every render.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Added VoiceButton component (inline in App.jsx) with full
mousedown/mouseup/mouseleave + touchstart/touchend/touchcancel support
- Shows context-aware label: 🎙 按住说话 / ⏳ 准备中 / 🔴 松开发送 / ⚙ 思考中 / 🔊 播放中
- Pulsing red animation while recording, amber pulse while acquiring mic
- Disabled (gray) while AI is thinking or speaking
- Desktop: pill button docked at bottom of chat panel (max-width 380px)
- Mobile: fixed to bottom of viewport with safe-area-inset support,
height 60px for easy thumb tap; right-col padded so messages aren't hidden
- Updated hint text and listening status to be input-agnostic
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add JbotApiConfig Django model (DB singleton) for storing
Volcengine/OpenRouter credentials set via Settings UI
- New endpoint GET/POST /api/jbot/api-config/ — tokens masked as ***
on read; only non-placeholder values are updated on write
- views.py: _get_cfg() helper reads DB first, env vars as fallback
- Settings.jsx: new 🔑 API 配置 section with show/hide token fields
for VOLC App ID, Access Token, OpenRouter API Key, LLM Model, ASR Resource
- api.js: apiGetApiConfig() / apiSaveApiConfig() client helpers
- Dockerfile: COPY jbot/ in both builder + production stages (was missing)
- entrypoint.sh: runs migrate --noinput before gunicorn (auto-creates table)
- k8s/manifest.yaml: remove jbot-credentials secretKeyRef (no Secret needed);
keep LLM_MODEL/VOLC_TTS_VOICE/VOLC_ASR_RESOURCE as optional env defaults
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>