feat(ai-core): mood state machine and affection level
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled

- 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:
Aska committed 2026-08-21 18:13:23 +08:00
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">
+12 -1
View File
@@ -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() })
+10
View File
@@ -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 }),
+3
View File
@@ -32,4 +32,7 @@ export interface WSMessage {
devices?: IotDevice[]
title?: string
body?: string
mood?: string
intensity?: number
affection?: number
}