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:
@@ -187,6 +187,7 @@ export function ChatView({
|
||||
sessionsHasMore,
|
||||
sessionSearchQuery,
|
||||
messages,
|
||||
memoryRecallByMessageId,
|
||||
messageHistoryLoadingMore,
|
||||
messageHistoryHasMore,
|
||||
messageHistoryTotal,
|
||||
@@ -559,6 +560,7 @@ export function ChatView({
|
||||
variant="full"
|
||||
user={user}
|
||||
messages={messages}
|
||||
memoryRecallByMessageId={memoryRecallByMessageId}
|
||||
historyLoadingMore={messageHistoryLoadingMore}
|
||||
historyHasMore={messageHistoryHasMore}
|
||||
historyTotal={messageHistoryTotal}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useChat } from '../context/ChatProvider';
|
||||
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
|
||||
import { useGoalRunBanner } from '../hooks/useGoalRunBanner';
|
||||
import type { PageEditSubChatBridge } from '../hooks/usePageEditSubChat';
|
||||
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types';
|
||||
import { formatContextChip } from '../utils/mindspaceChatContext';
|
||||
import { shouldShowChatMessage } from '../utils/message';
|
||||
import { ChatPanel } from './ChatPanel';
|
||||
import { TKMindAvatar } from './TKMindAvatar';
|
||||
import { GoalRunAwaitingBanner } from './GoalRunAwaitingBanner';
|
||||
|
||||
export function SpaceChatPanel({
|
||||
open,
|
||||
@@ -53,7 +55,15 @@ export function SpaceChatPanel({
|
||||
uploadChatAttachment,
|
||||
retryConnect,
|
||||
} = chat;
|
||||
const { capabilities, grantedSkills } = mainChat;
|
||||
const { capabilities, grantedSkills, followAgentRun, goalRunEnabled } = mainChat;
|
||||
const goalRunBanner = useGoalRunBanner({
|
||||
userId: user.id,
|
||||
sessionId: session?.id,
|
||||
chatState,
|
||||
submit: (text, options) => void submit(text, options),
|
||||
followAgentRun,
|
||||
featureEnabled: goalRunEnabled,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -130,6 +140,16 @@ export function SpaceChatPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{goalRunBanner.enabled && (
|
||||
<GoalRunAwaitingBanner
|
||||
items={goalRunBanner.items}
|
||||
busy={goalRunBanner.bannerBusy}
|
||||
onContinue={goalRunBanner.handleContinue}
|
||||
onDismiss={goalRunBanner.dismissItem}
|
||||
onRefresh={goalRunBanner.refresh}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChatPanel
|
||||
variant="compact"
|
||||
user={user}
|
||||
|
||||
Reference in New Issue
Block a user