- new emotion package: rule-based sentiment (positive/negative lexicon) drives mood transitions (开心/平静/难过) with 30min inactivity decay; affection 0-100 grows with positive interaction, with 初识→熟悉→信赖→ 羁绊 tiers - pipeline records each user message and injects a mood line into the system prompt so the persona's tone follows her mood - mood changes broadcast over WS; frontend shows a mood badge with affection tooltip; also handles the 'reminder' message type from the reminder scheduler - tests: sentiment scoring, mood shift, affection bounds, prompt line, onChange callback
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { useChatStore } from '../../store/chatStore'
|
|
import { useWebSocket } from '../../hooks/useWebSocket'
|
|
import MessageList from './MessageList'
|
|
import ChatInput from './ChatInput'
|
|
|
|
function moodEmoji(mood: string): string {
|
|
const map: Record<string, string> = { 开心: '😊', 平静: '😌', 难过: '😢', 害羞: '☺️', 感动: '🥹' }
|
|
return map[mood] || '😌'
|
|
}
|
|
|
|
export default function ChatContainer() {
|
|
const { send } = useWebSocket()
|
|
const { messages, isStreaming, currentSessionId, inputText, mood, setInput, addMessage, setStreaming } = useChatStore()
|
|
const bottomRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [messages])
|
|
|
|
const handleSend = () => {
|
|
if (!inputText.trim() || !currentSessionId) return
|
|
|
|
const userMsg = {
|
|
id: crypto.randomUUID(), session_id: currentSessionId,
|
|
role: 'user' as const, content: inputText, created_at: new Date().toISOString(),
|
|
}
|
|
addMessage(userMsg)
|
|
setStreaming(true)
|
|
|
|
// Placeholder for assistant
|
|
addMessage({
|
|
id: crypto.randomUUID(), session_id: currentSessionId,
|
|
role: 'assistant' as const, content: '', created_at: new Date().toISOString(),
|
|
})
|
|
|
|
send({ type: 'message', content: inputText, session_id: currentSessionId, timestamp: Date.now() })
|
|
setInput('')
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full relative">
|
|
{/* Mood badge */}
|
|
{mood && (
|
|
<div className="absolute top-3 right-4 z-10 px-3 py-1 rounded-full text-xs border"
|
|
style={{ background: 'var(--mx-surface)', borderColor: 'var(--mx-outline)', color: 'var(--mx-text-dim)' }}
|
|
title={`好感度 ${mood.affection}/100`}>
|
|
{moodEmoji(mood.mood)} {mood.mood}
|
|
</div>
|
|
)}
|
|
|
|
{/* Chat area */}
|
|
<div className="flex-1 overflow-auto">
|
|
<div className="max-w-2xl mx-auto px-4 py-6">
|
|
{messages.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-full text-center space-y-2 pt-32">
|
|
<div className="w-16 h-16 rounded-[20px] overflow-hidden shadow-lg mb-2" style={{ boxShadow: '0 0 24px color-mix(in srgb, var(--mx-primary) 20%, transparent)' }}>
|
|
<img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
|
|
</div>
|
|
<p className="text-[var(--mx-text-dim)] text-sm">开始新对话吧~</p>
|
|
</div>
|
|
) : (
|
|
<MessageList messages={messages} />
|
|
)}
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Input bar */}
|
|
<div className="shrink-0 border-t border-[var(--mx-divider)] bg-[var(--mx-surface)]">
|
|
<div className="max-w-2xl mx-auto px-4 py-3">
|
|
<ChatInput value={inputText} onChange={setInput} onSend={handleSend} disabled={isStreaming} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|