feat(memory-v2): close Phase A with auto-review, product events, and H5 recall UI.

Add candidate auto-review pipeline, shadow audit tooling, admin metrics page,
and user-visible memory recall hints in chat with phase-a readiness checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 17:14:06 +08:00
parent 666db0b939
commit 6f3e53a56a
50 changed files with 3730 additions and 38 deletions
+47 -1
View File
@@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
import { useUserAvatar } from '../hooks/useUserAvatar';
import type { Message } from '../types';
import { getDisplayText, getFileAttachments, getImageUrls, getRenderableImageUrls, getThinking, shouldShowChatMessage } from '../utils/message';
import { resolveMessageRecallKey } from '../utils/memoryFeedback';
import { getMessageSaveActions } from '../utils/messageSave';
import { renderMarkdown } from '../utils/markdown';
import { filterText } from '../utils/wordFilter';
@@ -169,6 +170,38 @@ function shouldShowEndTypingIndicator(messages: Message[], streaming: boolean) {
return true;
}
function MemoryRecallBadgeItem({
recall,
}: {
recall: { count: number; previews: string[] };
}) {
const [open, setOpen] = useState(false);
const hasPreviews = recall.previews.length > 0;
return (
<div className="memory-recall-badge-wrap">
<button
type="button"
className={`memory-recall-badge${open ? ' is-open' : ''}`}
aria-expanded={hasPreviews ? open : undefined}
onClick={() => {
if (hasPreviews) setOpen((current) => !current);
}}
>
<span> {recall.count} </span>
{hasPreviews && <ChevronIcon open={open} />}
</button>
{open && hasPreviews && (
<ul className="memory-recall-previews">
{recall.previews.map((preview, index) => (
<li key={`${index}-${preview.slice(0, 12)}`}>{preview}</li>
))}
</ul>
)}
</div>
);
}
function ToolBadge({ message, active }: { message: Message; active: boolean }) {
if (message.role !== 'assistant') return null;
const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
@@ -291,6 +324,8 @@ function MessageRow({
compact = false,
activeToolMessage = false,
showInlineTyping = false,
memoryRecallByMessageId = {},
messageIndex = 0,
}: {
message: Message;
avatarUrl: string | null;
@@ -306,10 +341,13 @@ function MessageRow({
compact?: boolean;
activeToolMessage?: boolean;
showInlineTyping?: boolean;
memoryRecallByMessageId?: Record<string, { count: number; previews: string[] }>;
messageIndex?: number;
}) {
const [actionsOpen, setActionsOpen] = useState(false);
const rawText = getDisplayText(message);
const text = filterText(rawText);
const memoryRecall = memoryRecallByMessageId[resolveMessageRecallKey(message, messageIndex)];
const saveActions = getMessageSaveActions(text, { userId: publishUserId, username: publishUsername });
const thinking = getThinking(message);
const isUser = message.role === 'user';
@@ -326,7 +364,10 @@ function MessageRow({
{showInlineTyping ? (
<TypingBubble />
) : (
<ToolBadge message={message} active={activeToolMessage} />
<>
<ToolBadge message={message} active={activeToolMessage} />
{memoryRecall && <MemoryRecallBadgeItem recall={memoryRecall} />}
</>
)}
</div>
</div>
@@ -444,6 +485,7 @@ function MessageRow({
/>
)}
<ToolBadge message={message} active={activeToolMessage} />
{memoryRecall && <MemoryRecallBadgeItem recall={memoryRecall} />}
</div>
{showActionsToggle && (
<button
@@ -494,6 +536,7 @@ export function MessageList({
publishUserId,
publishUsername,
compact = false,
memoryRecallByMessageId = {},
}: {
messages: Message[];
streaming: boolean;
@@ -505,6 +548,7 @@ export function MessageList({
publishUserId?: string;
publishUsername?: string;
compact?: boolean;
memoryRecallByMessageId?: Record<string, { count: number; previews: string[] }>;
}) {
const { avatarUrl } = useUserAvatar();
const renderableMessages = messages.filter(shouldShowChatMessage);
@@ -561,6 +605,8 @@ export function MessageList({
compact={compact}
activeToolMessage={message === activeToolMessage}
showInlineTyping={showEndTyping && message === inlineTypingMessage}
memoryRecallByMessageId={memoryRecallByMessageId}
messageIndex={index}
/>
</div>
);