jbot: reorganize layout — memory+hint to right, interval to settings

- 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>
This commit is contained in:
2026-05-17 14:13:37 +10:00
co-authored by Copilot
parent 4870e1a6f4
commit 3cbbaac083
6 changed files with 67 additions and 31 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+38 -2
View File
@@ -597,13 +597,29 @@ kbd {
/* Chat padding tighter on mobile */
.chat-panel { padding: 10px 12px; }
/* Right hint hidden on mobile — space is tight */
.right-hint { display: none; }
}
/* ── Right column hint bar ────────────────────────────────── */
.right-hint {
flex: 0 0 auto;
font-size: 0.72rem;
color: var(--text3);
text-align: center;
padding: 3px 16px 2px;
line-height: 1.7;
border-top: 1px solid var(--border);
background: var(--surface);
}
/* ── Memory panel ─────────────────────────────────────────── */
.memory-panel {
border: 1px solid var(--border);
border-radius: 8px;
flex: 0 0 auto;
border-bottom: 1px solid var(--border);
overflow: hidden;
}
.memory-toggle {
@@ -992,3 +1008,23 @@ kbd {
padding-bottom: 100px;
}
}
/* ── Settings slider row ──────────────────────────────────── */
.settings-slider-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 4px;
}
.settings-slider {
flex: 1;
accent-color: var(--accent);
cursor: pointer;
}
.settings-slider-val {
font-size: 0.82rem;
font-weight: 700;
color: var(--accent);
min-width: 36px;
text-align: right;
}
+11 -25
View File
@@ -82,17 +82,17 @@ export default function App() {
const [exprId, setExprId] = useState(ALL_AUTO_IDS[0]);
const [autoRotate, setAutoRotate] = useState(true);
const [scale, setScale] = useState(1);
const [switchMs, setSwitchMs] = useState(10000);
// switchMs is read from settings on mount (changed only via Settings page)
const [switchMs] = useState(() => getSettings().switchIntervalMs ?? 10000);
const autoTimerRef = useRef(null);
const autoRotateRef = useRef(true);
const exprIdRef = useRef(ALL_AUTO_IDS[0]);
const scheduleRef = useRef(null);
const switchMsRef = useRef(10000);
const switchMsRef = useRef(switchMs);
useEffect(() => { autoRotateRef.current = autoRotate; }, [autoRotate]);
useEffect(() => { exprIdRef.current = exprId; }, [exprId]);
useEffect(() => { switchMsRef.current = switchMs; }, [switchMs]);
const clearAuto = useCallback(() => {
clearTimeout(autoTimerRef.current);
@@ -373,33 +373,19 @@ export default function App() {
<input type="checkbox" checked={autoRotate} onChange={ev => setAutoRotate(ev.target.checked)} />
<span>自动轮换微表情</span>
</label>
<div className="switch-interval">
<div className="switch-interval-label">
切换间隔 <span className="interval-val">{Math.round(switchMs / 1000)} </span>
</div>
<input
type="range" min="3" max="30" step="1"
value={Math.round(switchMs / 1000)}
onChange={ev => setSwitchMs(Number(ev.target.value) * 1000)}
/>
<div className="interval-ticks"><span>3s</span><span>30s</span></div>
</div>
<p className="hint">
按住 <kbd>Space</kbd> 或按钮说话 · 松开发送 · <button className="hist-inline-link" onClick={() => navigate('/history')}>历史记录</button>
</p>
<p className="hint voice-hint">
🔊 音色: <button className="hist-inline-link" onClick={() => navigate('/settings')}>{currentVoiceLabel}</button>
</p>
<MemoryPanel memories={memories} onMemoriesChange={refreshMemories} />
</section>
</aside>
{/* ── Right column: chat + voice button ── */}
{/* ── Right column: memory + chat + hint + voice button ── */}
<div className="right-col">
<MemoryPanel memories={memories} onMemoriesChange={refreshMemories} />
<ChatPanel messages={chatMessages} voiceStatus={voiceStatus} />
<p className="right-hint">
按住 <kbd>Space</kbd> 或按钮说话 · 松开发送 ·{' '}
<button className="hist-inline-link" onClick={() => navigate('/history')}>历史记录</button>
{' '}· 🔊{' '}
<button className="hist-inline-link" onClick={() => navigate('/settings')}>{currentVoiceLabel}</button>
</p>
<VoiceButton phase={voicePhase} onStart={handleSpaceDown} onEnd={handleSpaceUp} />
</div>
</div>
+13
View File
@@ -163,6 +163,19 @@ export default function Settings() {
placeholder="你是一个友好、简洁、偶尔幽默的AI机器人助手。回复控制在80字以内。"
/>
</label>
<div className="settings-label">
微表情切换间隔
<div className="settings-slider-row">
<input
type="range" min="3" max="30" step="1"
value={Math.round((form.switchIntervalMs ?? 10000) / 1000)}
onChange={e => set('switchIntervalMs', Number(e.target.value) * 1000)}
className="settings-slider"
/>
<span className="settings-slider-val">{Math.round((form.switchIntervalMs ?? 10000) / 1000)} </span>
</div>
</div>
</section>
{/* ── User context ── */}
+1
View File
@@ -9,6 +9,7 @@ export const DEFAULT_SETTINGS = {
voiceId: DEFAULT_VOICE,
customVoiceId: '', // overrides voiceId if non-empty
responseMaxWords: 80,
switchIntervalMs: 10000, // micro-expression auto-rotate interval
};
export function getSettings() {