feat: agent proactively re-engages users on returning sessions
Two complementary changes to reduce conversation cold-start friction: A. Returning-user re-greeting (backend) - When WS reconnects to a non-empty conversation and last_message_at is older than chat_re_greeting_idle_hours (default 6h), the agent emits a warm continuation message that references prior history instead of staying silent. - Self-debouncing: the AI message updates last_message_at, so reconnects within the window will not re-trigger. - Skipped while profile collection is still pending. D. Topic suggestion chips (backend + Expo) - New WS message type topic_suggestions carries 3-4 quick-start chips derived from the current memoir stage's empty slots (deterministic, no extra LLM cost). Sent alongside opening / re-greeting / resume. - Expo chat screen renders a horizontally-scrollable chip row above the input bar; tapping a chip sends the chip's text as a user message and clears the row. Sending any text/voice also clears the chips.
This commit is contained in:
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { File, Paths } from 'expo-file-system';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { WsConnectionState } from '@/core/ws/types';
|
||||
import type { TopicSuggestion, WsConnectionState } from '@/core/ws/types';
|
||||
|
||||
import { conversationApi } from './api';
|
||||
import { conversationMessagesRepository } from './conversation-messages-repository';
|
||||
@@ -183,6 +183,9 @@ interface RealtimeSessionState {
|
||||
/** 已发出用户消息,尚未收到助手首段流式文本(用于「正在回复」气泡) */
|
||||
awaitingAssistantReply: boolean;
|
||||
error: string | null;
|
||||
/** 服务端下发的 quick-start 话题 chips;用户首次发文本/语音后清空 */
|
||||
topicSuggestions: TopicSuggestion[];
|
||||
dismissTopicSuggestions: () => void;
|
||||
sendText: (text: string) => void;
|
||||
sendVoiceMessage: (uri: string, durationMs: number) => Promise<boolean>;
|
||||
sendEndConversation: () => void;
|
||||
@@ -204,6 +207,9 @@ export function useRealtimeSession({
|
||||
useState<StreamingAgentMessage | null>(null);
|
||||
const [awaitingAssistantReply, setAwaitingAssistantReply] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [topicSuggestions, setTopicSuggestions] = useState<TopicSuggestion[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const handleStreamingText: StreamingTextCallback = useCallback(
|
||||
(text, isComplete) => {
|
||||
@@ -225,6 +231,17 @@ export function useRealtimeSession({
|
||||
setError(message);
|
||||
}, []);
|
||||
|
||||
const handleTopicSuggestions = useCallback(
|
||||
(payload: { suggestions: TopicSuggestion[] }) => {
|
||||
setTopicSuggestions(payload.suggestions);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const dismissTopicSuggestions = useCallback(() => {
|
||||
setTopicSuggestions([]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !conversationId) return;
|
||||
|
||||
@@ -233,6 +250,7 @@ export function useRealtimeSession({
|
||||
queryClient,
|
||||
onStreamingText: handleStreamingText,
|
||||
onTtsSegment,
|
||||
onTopicSuggestions: handleTopicSuggestions,
|
||||
onError: handleError,
|
||||
onStateChange: setConnectionState,
|
||||
});
|
||||
@@ -246,6 +264,7 @@ export function useRealtimeSession({
|
||||
setConnectionState('disconnected');
|
||||
setStreamingMessage(null);
|
||||
setAwaitingAssistantReply(false);
|
||||
setTopicSuggestions([]);
|
||||
};
|
||||
}, [
|
||||
conversationId,
|
||||
@@ -253,6 +272,7 @@ export function useRealtimeSession({
|
||||
queryClient,
|
||||
handleStreamingText,
|
||||
handleError,
|
||||
handleTopicSuggestions,
|
||||
onTtsSegment,
|
||||
]);
|
||||
|
||||
@@ -267,6 +287,7 @@ export function useRealtimeSession({
|
||||
}
|
||||
|
||||
setAwaitingAssistantReply(true);
|
||||
setTopicSuggestions([]);
|
||||
onTtsPlaybackResume?.();
|
||||
|
||||
const localId = `pending_${Date.now()}`;
|
||||
@@ -326,6 +347,7 @@ export function useRealtimeSession({
|
||||
}
|
||||
|
||||
setAwaitingAssistantReply(true);
|
||||
setTopicSuggestions([]);
|
||||
const localId = `pending_voice_${Date.now()}`;
|
||||
await voiceSegmentStore.recordSentSegment({
|
||||
voiceSessionId,
|
||||
@@ -385,6 +407,8 @@ export function useRealtimeSession({
|
||||
streamingMessage,
|
||||
awaitingAssistantReply,
|
||||
error,
|
||||
topicSuggestions,
|
||||
dismissTopicSuggestions,
|
||||
sendText,
|
||||
sendVoiceMessage,
|
||||
sendEndConversation,
|
||||
|
||||
Reference in New Issue
Block a user