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
This commit is contained in:
1 parent
26d65dd6bb
commit
b8fb7feb86
8 files changed
+309
-3
No files matched your search
@@ -4,9 +4,14 @@ 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, setInput, addMessage, setStreaming } = useChatStore()
|
||||
const { messages, isStreaming, currentSessionId, inputText, mood, setInput, addMessage, setStreaming } = useChatStore()
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,7 +39,16 @@ export default function ChatContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<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">
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { WSMessage } from '../types/chat'
|
||||
export function useWebSocket() {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const reconnectRef = useRef(0)
|
||||
const { addMessage, appendToLast, setStreaming, setIotDevices, currentSessionId } = useChatStore()
|
||||
const { addMessage, appendToLast, setStreaming, setIotDevices, setMood, currentSessionId } = useChatStore()
|
||||
|
||||
const connect = useCallback(() => {
|
||||
const token = getToken()
|
||||
@@ -52,6 +52,17 @@ export function useWebSocket() {
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'mood':
|
||||
if (msg.mood) setMood({ mood: msg.mood, intensity: msg.intensity || 0, affection: msg.affection || 0 })
|
||||
break
|
||||
case 'reminder':
|
||||
if (msg.content) {
|
||||
addMessage({
|
||||
id: crypto.randomUUID(), session_id: currentSessionId || '',
|
||||
role: 'system', content: `⏰ ${msg.content}`, created_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'error':
|
||||
addMessage({ id: crypto.randomUUID(), session_id: '', role: 'system',
|
||||
content: `Error: ${msg.error}`, created_at: new Date().toISOString() })
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Message, IotDevice } from '../types/chat'
|
||||
|
||||
interface MoodState {
|
||||
mood: string
|
||||
intensity: number
|
||||
affection: number
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
messages: Message[]
|
||||
isStreaming: boolean
|
||||
currentSessionId: string | null
|
||||
iotDevices: IotDevice[]
|
||||
inputText: string
|
||||
mood: MoodState | null
|
||||
|
||||
setInput: (text: string) => void
|
||||
setSessionId: (id: string) => void
|
||||
setMessages: (msgs: Message[]) => void
|
||||
setMood: (m: MoodState) => void
|
||||
addMessage: (msg: Message) => void
|
||||
appendToLast: (content: string) => void
|
||||
setStreaming: (v: boolean) => void
|
||||
@@ -24,10 +32,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
currentSessionId: null,
|
||||
iotDevices: [],
|
||||
inputText: '',
|
||||
mood: null,
|
||||
|
||||
setInput: (text) => set({ inputText: text }),
|
||||
setSessionId: (id) => set({ currentSessionId: id }),
|
||||
setMessages: (msgs) => set({ messages: msgs }),
|
||||
setMood: (m) => set({ mood: m }),
|
||||
setStreaming: (v) => set({ isStreaming: v }),
|
||||
setIotDevices: (d) => set({ iotDevices: d }),
|
||||
|
||||
|
||||
@@ -32,4 +32,7 @@ export interface WSMessage {
|
||||
devices?: IotDevice[]
|
||||
title?: string
|
||||
body?: string
|
||||
mood?: string
|
||||
intensity?: number
|
||||
affection?: number
|
||||
}
|
||||
Reference in new issue
Block a user