Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+128
-12
@@ -8,6 +8,8 @@ import {
|
||||
deleteChatSession,
|
||||
getMindSpace,
|
||||
getMe,
|
||||
listNotifications,
|
||||
markNotificationRead,
|
||||
getSession,
|
||||
listSessions,
|
||||
loadSessionDetail,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
resumeSession,
|
||||
sendReply,
|
||||
startSession,
|
||||
subscribeNotificationEvents,
|
||||
subscribeSessionEvents,
|
||||
updateProvider,
|
||||
} from '../api/client';
|
||||
@@ -30,6 +33,7 @@ import type {
|
||||
Session,
|
||||
SessionEvent,
|
||||
ToolConfirmation,
|
||||
UserNotification,
|
||||
} from '../types';
|
||||
import { buildContextPrefix } from '../utils/mindspaceChatContext';
|
||||
import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
@@ -105,12 +109,16 @@ export function useTKMindChat(
|
||||
const [memoryLoading, setMemoryLoading] = useState(false);
|
||||
const [rechargePrompt, setRechargePrompt] = useState(false);
|
||||
const [rechargeForced, setRechargeForced] = useState(false);
|
||||
const [subscribePrompt, setSubscribePrompt] = useState(false);
|
||||
const [activeNotification, setActiveNotification] = useState<UserNotification | null>(null);
|
||||
const seenNotificationIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const activeRequestId = useRef<string | null>(null);
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
const connectTokenRef = useRef(0);
|
||||
const messagesRef = useRef<Message[]>([]);
|
||||
const sessionRef = useRef<Session | null>(null);
|
||||
const sessionsRef = useRef<Session[]>([]);
|
||||
const rememberedContextRef = useRef<string | null>(null);
|
||||
const rememberInFlightRef = useRef(false);
|
||||
const fallbackRetriedRef = useRef(new Set<string>());
|
||||
@@ -118,7 +126,14 @@ export function useTKMindChat(
|
||||
const onUserUpdateRef = useRef(onUserUpdate);
|
||||
const chatImageCategoryIdRef = useRef<string | null>(null);
|
||||
|
||||
const dismissNotice = useCallback(() => setNotice(null), []);
|
||||
const dismissNotice = useCallback(() => {
|
||||
const currentNotification = activeNotification;
|
||||
setNotice(null);
|
||||
setActiveNotification(null);
|
||||
if (currentNotification) {
|
||||
void markNotificationRead(currentNotification.id).catch(() => {});
|
||||
}
|
||||
}, [activeNotification]);
|
||||
|
||||
const notifyInsufficientBalance = useCallback(() => {
|
||||
setNotice(INSUFFICIENT_BALANCE_NOTICE);
|
||||
@@ -152,6 +167,28 @@ export function useTKMindChat(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openSubscribe = useCallback(() => {
|
||||
setSubscribePrompt(true);
|
||||
}, []);
|
||||
|
||||
const dismissSubscribe = useCallback(() => {
|
||||
setSubscribePrompt(false);
|
||||
}, []);
|
||||
|
||||
const completeSubscribe = useCallback((nextBalanceCents: number, subscription: import('../types').ActiveSubscription) => {
|
||||
setSubscribePrompt(false);
|
||||
const currentUser = userRef.current;
|
||||
const updateUser = onUserUpdateRef.current;
|
||||
if (currentUser && updateUser) {
|
||||
updateUser({
|
||||
...currentUser,
|
||||
balanceCents: nextBalanceCents,
|
||||
subscription,
|
||||
planType: subscription.planType,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
userRef.current = user;
|
||||
}, [user]);
|
||||
@@ -168,6 +205,63 @@ export function useTKMindChat(
|
||||
sessionRef.current = session;
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionsRef.current = sessions;
|
||||
}, [sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id) return;
|
||||
let cancelled = false;
|
||||
|
||||
const showNotification = (notification: UserNotification) => {
|
||||
if (cancelled) return;
|
||||
const hasSeen = seenNotificationIdsRef.current.has(notification.id);
|
||||
if (activeNotification?.id === notification.id && hasSeen) return;
|
||||
seenNotificationIdsRef.current.add(notification.id);
|
||||
setActiveNotification(notification);
|
||||
setNotice(`${notification.title}\n${notification.body}`.trim());
|
||||
};
|
||||
|
||||
const pullNotifications = async () => {
|
||||
try {
|
||||
const notifications = await listNotifications('unread', 10);
|
||||
if (cancelled) return;
|
||||
if (notifications.length === 0) {
|
||||
seenNotificationIdsRef.current.clear();
|
||||
return;
|
||||
}
|
||||
const currentIds = new Set(notifications.map((item) => item.id));
|
||||
seenNotificationIdsRef.current.forEach((id) => {
|
||||
if (!currentIds.has(id)) seenNotificationIdsRef.current.delete(id);
|
||||
});
|
||||
showNotification(notifications[0]);
|
||||
} catch {
|
||||
// Ignore notification polling failures to avoid disrupting chat.
|
||||
}
|
||||
};
|
||||
|
||||
const handleForegroundSync = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void pullNotifications();
|
||||
}
|
||||
};
|
||||
|
||||
void pullNotifications();
|
||||
const unsubscribe = subscribeNotificationEvents({
|
||||
onNotification: showNotification,
|
||||
onSync: () => void pullNotifications(),
|
||||
});
|
||||
window.addEventListener('focus', handleForegroundSync);
|
||||
document.addEventListener('visibilitychange', handleForegroundSync);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe();
|
||||
window.removeEventListener('focus', handleForegroundSync);
|
||||
document.removeEventListener('visibilitychange', handleForegroundSync);
|
||||
};
|
||||
}, [user?.id, activeNotification?.id]);
|
||||
|
||||
const loadProjectMemory = useCallback(async (sessionId: string, force: boolean) => {
|
||||
if (!canUseProjectMemory) return null;
|
||||
setMemoryLoading(true);
|
||||
@@ -313,6 +407,18 @@ export function useTKMindChat(
|
||||
});
|
||||
}, [resolveChatImageUploadCategoryId]);
|
||||
|
||||
const syncSessionMessages = useCallback(async (sessionId: string) => {
|
||||
try {
|
||||
const detail = await loadSessionDetail(sessionId);
|
||||
if (sessionRef.current?.id !== sessionId) return;
|
||||
messagesRef.current = detail.messages;
|
||||
setMessages(detail.messages);
|
||||
setSession((current) => (current?.id === sessionId ? detail.session : current));
|
||||
} catch {
|
||||
// Keep the optimistic streamed state if the follow-up sync fails.
|
||||
}
|
||||
}, []);
|
||||
|
||||
const processEvent = useCallback(
|
||||
(event: SessionEvent, requestId: string, sessionId: string) => {
|
||||
const raw = event as SessionEvent & { chat_request_id?: string; request_id?: string };
|
||||
@@ -407,27 +513,28 @@ export function useTKMindChat(
|
||||
.then(({ user: fresh }) => onUserUpdateRef.current?.(fresh))
|
||||
.catch(() => {});
|
||||
}
|
||||
const recentContext = messagesRef.current
|
||||
.filter((message) => getDisplayText(message).trim())
|
||||
.slice(-6)
|
||||
.map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${getDisplayText(message)}`)
|
||||
.join('\n\n')
|
||||
.slice(0, 6_000);
|
||||
const finishedSession = sessionRef.current;
|
||||
window.setTimeout(() => {
|
||||
void (async () => {
|
||||
await syncSessionMessages(sessionId);
|
||||
const recentContext = messagesRef.current
|
||||
.filter((message) => getDisplayText(message).trim())
|
||||
.slice(-6)
|
||||
.map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${getDisplayText(message)}`)
|
||||
.join('\n\n')
|
||||
.slice(0, 6_000);
|
||||
const finishedSession = sessionRef.current;
|
||||
void rememberRecentContext({
|
||||
silent: true,
|
||||
sessionId,
|
||||
sessionName: finishedSession?.name,
|
||||
recentContext,
|
||||
});
|
||||
}, 0);
|
||||
})();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
[rememberRecentContext],
|
||||
[rememberRecentContext, syncSessionMessages],
|
||||
);
|
||||
|
||||
const subscribeToSession = useCallback(
|
||||
@@ -506,7 +613,11 @@ export function useTKMindChat(
|
||||
: await resumeSession(sessionId);
|
||||
if (token !== connectTokenRef.current) return;
|
||||
|
||||
const { session: detail, messages: history } = await loadSessionDetail(sessionId);
|
||||
const knownSession = sessionsRef.current.find((s) => s.id === sessionId);
|
||||
const hints = knownSession
|
||||
? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
|
||||
: undefined;
|
||||
const { session: detail, messages: history } = await loadSessionDetail(sessionId, hints);
|
||||
if (token !== connectTokenRef.current) return;
|
||||
|
||||
writeStoredSessionId(userRef.current?.id, sessionId);
|
||||
@@ -826,10 +937,15 @@ export function useTKMindChat(
|
||||
balanceCents: user?.balanceCents,
|
||||
totalCreditCents: user?.totalCreditCents,
|
||||
tokensUsed: user?.tokensUsed ?? 0,
|
||||
subscription: user?.subscription,
|
||||
rechargePrompt,
|
||||
rechargeForced,
|
||||
openRecharge,
|
||||
dismissRecharge,
|
||||
completeRecharge,
|
||||
subscribePrompt,
|
||||
openSubscribe,
|
||||
dismissSubscribe,
|
||||
completeSubscribe,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user