Files
Cyrene_v2/frontend/web/src/components/chat/ChatContainer.tsx
T
Aska b8fb7feb86
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled
feat(ai-core): mood state machine and affection level
- 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
2026-08-21 18:13:23 +08:00

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>
)
}