Improve MindSpace visual editor UX and default RMB billing.
Add color-coded edit highlights with an in-preview legend, strip CSP meta for srcdoc editing, and simplify fullscreen preview to immersive visual edit only. Default billing to CNY token rates unless H5_USE_BACKEND_COST=1. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,7 +27,6 @@ import { resolvePlazaPostUrl, resolvePublicPageUrl } from '../utils/publicUrl';
|
||||
import { MindSpacePageDraftPreviewFrame, type MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
||||
import {
|
||||
MindSpacePageFullscreenPreview,
|
||||
type MindSpacePageFullscreenPreviewChat,
|
||||
} from './MindSpacePageFullscreenPreview';
|
||||
import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel';
|
||||
import { MindSpaceModal } from './MindSpaceModal';
|
||||
@@ -96,7 +95,6 @@ export function MindSpacePageDetail({
|
||||
onContextUpdate,
|
||||
autoOpenPlaza = false,
|
||||
refreshTrigger = 0,
|
||||
overlayChat = null,
|
||||
onFullscreenPreviewChange,
|
||||
}: {
|
||||
pageId: string;
|
||||
@@ -110,7 +108,6 @@ export function MindSpacePageDetail({
|
||||
}) => void;
|
||||
autoOpenPlaza?: boolean;
|
||||
refreshTrigger?: number;
|
||||
overlayChat?: MindSpacePageFullscreenPreviewChat | null;
|
||||
onFullscreenPreviewChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const [page, setPage] = useState<MindSpacePage | null>(null);
|
||||
@@ -550,6 +547,9 @@ export function MindSpacePageDetail({
|
||||
|
||||
const openFullscreenPreview = () => {
|
||||
if (!page || page.contentFormat !== 'html' || !content.trim()) return;
|
||||
setPreviewContent(content);
|
||||
setPreviewRefreshPending(false);
|
||||
setPreviewKey((value) => value + 1);
|
||||
setFullscreenPreviewOpen(true);
|
||||
onFullscreenPreviewChange?.(true);
|
||||
};
|
||||
@@ -1071,7 +1071,7 @@ export function MindSpacePageDetail({
|
||||
type="button"
|
||||
onClick={openFullscreenPreview}
|
||||
disabled={page.contentFormat !== 'html' || !content.trim()}
|
||||
title={page.contentFormat === 'html' ? '全屏可视化编辑,可配合 Agent 改页' : '仅 HTML 页面支持全屏编辑'}
|
||||
title={page.contentFormat === 'html' ? '进入沉浸式可视化编辑' : '仅 HTML 页面支持全屏编辑'}
|
||||
>
|
||||
全屏预览编辑
|
||||
</button>
|
||||
@@ -1156,12 +1156,9 @@ export function MindSpacePageDetail({
|
||||
|
||||
{fullscreenPreviewOpen && page && page.contentFormat === 'html' ? (
|
||||
<MindSpacePageFullscreenPreview
|
||||
pageId={page.id}
|
||||
pageTitle={title || page.title}
|
||||
title={title}
|
||||
summary={summary}
|
||||
content={previewContent}
|
||||
templateId={templateId}
|
||||
content={content}
|
||||
reloadKey={previewKey}
|
||||
changeHighlight={changeHighlight}
|
||||
canUndo={canUndo}
|
||||
@@ -1172,7 +1169,6 @@ export function MindSpacePageDetail({
|
||||
onRefresh={() => handleManualPreviewRefresh()}
|
||||
refreshPending={previewRefreshPending}
|
||||
onContentChange={handlePreviewContentChange}
|
||||
chat={overlayChat}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -11,7 +11,9 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
reloadKey,
|
||||
minHeight = 680,
|
||||
maxHeight = 12000,
|
||||
className = 'mindspace-page-preview-frame',
|
||||
className,
|
||||
compact = false,
|
||||
fillViewport = false,
|
||||
showHint = true,
|
||||
onContentChange,
|
||||
}: {
|
||||
@@ -21,9 +23,14 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
className?: string;
|
||||
compact?: boolean;
|
||||
fillViewport?: boolean;
|
||||
showHint?: boolean;
|
||||
onContentChange: (html: string) => void;
|
||||
}) {
|
||||
const scrollInsideFrame = compact || fillViewport;
|
||||
const frameClassName =
|
||||
className ?? (compact ? 'page-save-mini-page-frame' : 'mindspace-page-preview-frame');
|
||||
const [previewFailed, setPreviewFailed] = useState(false);
|
||||
const [srcdoc, setSrcdoc] = useState(() => buildEditablePreviewDocument(content));
|
||||
const [height, setHeight] = useState(minHeight);
|
||||
@@ -62,6 +69,11 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
}, [onContentChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollInsideFrame) {
|
||||
syncHeightRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe || !srcdoc) return;
|
||||
|
||||
@@ -85,7 +97,7 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
syncHeightRef.current = null;
|
||||
unbind();
|
||||
};
|
||||
}, [maxHeight, minHeight, srcdoc]);
|
||||
}, [maxHeight, minHeight, scrollInsideFrame, srcdoc]);
|
||||
|
||||
if (previewFailed) {
|
||||
return <div className="mindspace-state mindspace-error">页面预览加载失败</div>;
|
||||
@@ -95,7 +107,14 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
<>
|
||||
{showHint ? (
|
||||
<p className="mindspace-page-editable-hint">
|
||||
文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址
|
||||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-text" />
|
||||
文字
|
||||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-media" />
|
||||
图片/媒体
|
||||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-link" />
|
||||
链接
|
||||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-bg" />
|
||||
背景 — 预览内左上角有完整图例,虚线框即可直接修改
|
||||
</p>
|
||||
) : null}
|
||||
<iframe
|
||||
@@ -103,9 +122,9 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
title={`${title || '页面'} 预览`}
|
||||
srcDoc={srcdoc}
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
scrolling="no"
|
||||
className={className}
|
||||
style={{ height: `${height}px` }}
|
||||
scrolling={scrollInsideFrame ? 'yes' : 'no'}
|
||||
className={frameClassName}
|
||||
style={scrollInsideFrame ? { height: '100%' } : { height: `${height}px` }}
|
||||
onError={() => setPreviewFailed(true)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,32 +1,11 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { usePageEditSubChat } from '../hooks/usePageEditSubChat';
|
||||
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
|
||||
import { useEffect } from 'react';
|
||||
import { MindSpacePageEditablePreviewFrame } from './MindSpacePageEditablePreviewFrame';
|
||||
import type { MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
||||
import { MindSpaceSpaceChat } from './MindSpaceSpaceChat';
|
||||
|
||||
export type MindSpacePageFullscreenPreviewChat = {
|
||||
context: MindSpaceChatContext;
|
||||
user: PortalUser;
|
||||
parentSessionId?: string | null;
|
||||
h5ApiBase?: string | null;
|
||||
onOpenFullChat: () => void;
|
||||
onPageSaved?: (result: {
|
||||
kind: 'page' | 'category';
|
||||
pageId?: string;
|
||||
categoryCode?: MindSpaceSaveCategory;
|
||||
}) => void;
|
||||
onSubSessionChange?: (sessionId: string | null) => void;
|
||||
onForkUnavailable?: () => void;
|
||||
};
|
||||
|
||||
export function MindSpacePageFullscreenPreview({
|
||||
pageId,
|
||||
pageTitle,
|
||||
title,
|
||||
summary: _summary,
|
||||
content,
|
||||
templateId: _templateId,
|
||||
reloadKey,
|
||||
changeHighlight: _changeHighlight = null,
|
||||
canUndo = false,
|
||||
@@ -37,14 +16,10 @@ export function MindSpacePageFullscreenPreview({
|
||||
onRefresh,
|
||||
refreshPending = false,
|
||||
onContentChange,
|
||||
chat,
|
||||
}: {
|
||||
pageId: string;
|
||||
pageTitle: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
reloadKey: number;
|
||||
changeHighlight?: MindSpacePreviewChangeHighlight | null;
|
||||
canUndo?: boolean;
|
||||
@@ -55,40 +30,7 @@ export function MindSpacePageFullscreenPreview({
|
||||
onRefresh: () => void;
|
||||
refreshPending?: boolean;
|
||||
onContentChange: (html: string) => void;
|
||||
chat?: MindSpacePageFullscreenPreviewChat | null;
|
||||
}) {
|
||||
const [chatOpen, setChatOpen] = useState(true);
|
||||
const [subChatFallback, setSubChatFallback] = useState(false);
|
||||
const subChatEnabled = Boolean(chat?.parentSessionId);
|
||||
|
||||
useEffect(() => {
|
||||
setSubChatFallback(false);
|
||||
}, [pageId, chat?.parentSessionId]);
|
||||
|
||||
const subChat = usePageEditSubChat({
|
||||
pageId,
|
||||
pageTitle: pageTitle || title,
|
||||
parentSessionId: chat?.parentSessionId,
|
||||
h5ApiBase: chat?.h5ApiBase,
|
||||
user: chat?.user,
|
||||
enabled: subChatEnabled && !subChatFallback,
|
||||
onSessionChange: chat?.onSubSessionChange,
|
||||
onForkUnavailable: () => {
|
||||
setSubChatFallback(true);
|
||||
chat?.onForkUnavailable?.();
|
||||
},
|
||||
});
|
||||
|
||||
const subChatContext = useMemo(() => {
|
||||
if (!chat?.context) return null;
|
||||
return {
|
||||
...chat.context,
|
||||
pageEditMode: true,
|
||||
parentAgentSessionId: chat.parentSessionId ?? chat.context.parentAgentSessionId,
|
||||
agentSessionId: subChat.session?.id ?? chat.context.agentSessionId,
|
||||
} satisfies MindSpaceChatContext;
|
||||
}, [chat, subChat.session?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -118,13 +60,13 @@ export function MindSpacePageFullscreenPreview({
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== 'Escape') return;
|
||||
if (chatOpen) return;
|
||||
onClose();
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [canRedo, canUndo, chatOpen, onClose, onRedo, onUndo]);
|
||||
}, [canRedo, canUndo, onClose, onRedo, onUndo]);
|
||||
|
||||
return (
|
||||
<div className="mindspace-page-fullscreen-preview" role="dialog" aria-modal="true" aria-label="页面全屏预览编辑">
|
||||
@@ -133,16 +75,10 @@ export function MindSpacePageFullscreenPreview({
|
||||
<span className="mindspace-eyebrow">VISUAL EDIT</span>
|
||||
<strong>{pageTitle || '全屏预览编辑'}</strong>
|
||||
<span className="mindspace-page-fullscreen-preview-hint">
|
||||
文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址
|
||||
蓝框=文字 · 橙框=图片/媒体 · 紫框=链接 · 绿框=背景(预览内左上角可看完整图例)
|
||||
</span>
|
||||
</div>
|
||||
<div className="mindspace-page-fullscreen-preview-actions">
|
||||
{subChatEnabled && !subChatFallback && subChat.chatState === 'loading' ? (
|
||||
<span className="mindspace-page-preview-sync">启动编辑 Agent…</span>
|
||||
) : null}
|
||||
{subChatFallback ? (
|
||||
<span className="mindspace-page-preview-sync">使用主对话编辑(子 Agent 未就绪)</span>
|
||||
) : null}
|
||||
<button type="button" onClick={onUndo} disabled={!canUndo} title="撤销 (⌘Z)">
|
||||
撤销
|
||||
</button>
|
||||
@@ -168,28 +104,12 @@ export function MindSpacePageFullscreenPreview({
|
||||
title={title}
|
||||
content={content}
|
||||
reloadKey={reloadKey}
|
||||
minHeight={720}
|
||||
maxHeight={12000}
|
||||
fillViewport
|
||||
className="mindspace-page-fullscreen-preview-frame"
|
||||
showHint={false}
|
||||
onContentChange={onContentChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{chat && subChatContext ? (
|
||||
<MindSpaceSpaceChat
|
||||
context={subChatContext}
|
||||
user={chat.user}
|
||||
onOpenFullChat={chat.onOpenFullChat}
|
||||
onPageSaved={chat.onPageSaved}
|
||||
open={chatOpen}
|
||||
onOpenChange={setChatOpen}
|
||||
variant="overlay"
|
||||
chatBridge={subChatEnabled && !subChatFallback ? subChat : undefined}
|
||||
hideOpenFullChat={!subChatFallback}
|
||||
title={subChatFallback ? 'TKMind' : '页面编辑 Agent'}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,19 +118,24 @@ export function MindSpacePagePreviewPanel({
|
||||
|
||||
return (
|
||||
<div className="page-save-preview-dual mindspace-page-preview-dual">
|
||||
<div className="page-save-preview-pane page-save-preview-pane-page">
|
||||
<div
|
||||
className="page-save-preview-pane page-save-preview-pane-page"
|
||||
onWheelCapture={(event) => event.stopPropagation()}
|
||||
>
|
||||
<span className="page-save-preview-label">页面预览编辑</span>
|
||||
<p className="page-save-preview-hint">
|
||||
文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址
|
||||
微型页面,可上下滑动查看完整效果 · 虚线框:蓝=文字 · 橙=图片 · 紫=链接 · 绿=背景
|
||||
</p>
|
||||
<MindSpacePageEditablePreviewFrame
|
||||
title={title}
|
||||
content={content}
|
||||
reloadKey={reloadKey}
|
||||
showHint={false}
|
||||
className="mindspace-page-preview-frame"
|
||||
onContentChange={onContentChange}
|
||||
/>
|
||||
<div className="page-save-mini-page">
|
||||
<MindSpacePageEditablePreviewFrame
|
||||
title={title}
|
||||
content={content}
|
||||
reloadKey={reloadKey}
|
||||
compact
|
||||
showHint={false}
|
||||
onContentChange={onContentChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-save-preview-pane page-save-preview-pane-thumb">
|
||||
<span className="page-save-preview-label">卡片预览图</span>
|
||||
|
||||
@@ -199,8 +199,6 @@ export function MindSpaceView({
|
||||
} | null>(null);
|
||||
const [pageRefreshTrigger, setPageRefreshTrigger] = useState(0);
|
||||
const [pageFullscreenPreviewOpen, setPageFullscreenPreviewOpen] = useState(false);
|
||||
const [pageEditSubSessionId, setPageEditSubSessionId] = useState<string | null>(null);
|
||||
const [pageEditSubSessionActive, setPageEditSubSessionActive] = useState(false);
|
||||
const { chatState, messages, session } = useChat();
|
||||
const prevChatStateRef = useRef(chatState);
|
||||
const h5ApiBase = useMemo(() => resolveH5ApiBase(), []);
|
||||
@@ -218,22 +216,13 @@ export function MindSpaceView({
|
||||
if (!selectedPageId) {
|
||||
setPageLiveContext(null);
|
||||
setPageFullscreenPreviewOpen(false);
|
||||
setPageEditSubSessionId(null);
|
||||
setPageEditSubSessionActive(false);
|
||||
}
|
||||
}, [selectedPageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPageId || !session?.id || previewMode) return;
|
||||
if (pageFullscreenPreviewOpen && pageEditSubSessionActive) return;
|
||||
if (!selectedPageId || !session?.id || previewMode || pageFullscreenPreviewOpen) return;
|
||||
void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(() => {});
|
||||
}, [
|
||||
selectedPageId,
|
||||
session?.id,
|
||||
previewMode,
|
||||
pageFullscreenPreviewOpen,
|
||||
pageEditSubSessionActive,
|
||||
]);
|
||||
}, [selectedPageId, session?.id, previewMode, pageFullscreenPreviewOpen]);
|
||||
|
||||
const mindspaceChatContext = useMemo(() => {
|
||||
if (!space) return null;
|
||||
@@ -247,10 +236,8 @@ export function MindSpaceView({
|
||||
assets,
|
||||
focusedAsset: agentAsset,
|
||||
route: `${location.pathname}${location.search}`,
|
||||
agentSessionId: pageEditSubSessionId ?? session?.id ?? null,
|
||||
agentSessionId: session?.id ?? null,
|
||||
h5ApiBase,
|
||||
pageEditMode: pageFullscreenPreviewOpen,
|
||||
parentAgentSessionId: pageFullscreenPreviewOpen ? session?.id ?? null : null,
|
||||
});
|
||||
}, [
|
||||
space,
|
||||
@@ -265,8 +252,6 @@ export function MindSpaceView({
|
||||
location.search,
|
||||
session?.id,
|
||||
h5ApiBase,
|
||||
pageFullscreenPreviewOpen,
|
||||
pageEditSubSessionId,
|
||||
]);
|
||||
|
||||
const truncateLabel = (text: string, max = 48) => {
|
||||
@@ -537,10 +522,10 @@ export function MindSpaceView({
|
||||
const previous = prevChatStateRef.current;
|
||||
prevChatStateRef.current = chatState;
|
||||
const wasBusy = previous === 'streaming' || previous === 'waiting';
|
||||
if (!wasBusy || chatState !== 'idle' || !selectedPageId || previewMode) return;
|
||||
if (!wasBusy || chatState !== 'idle' || !selectedPageId || previewMode || pageFullscreenPreviewOpen) return;
|
||||
setPageRefreshTrigger((value) => value + 1);
|
||||
void refreshPagesSilently();
|
||||
}, [chatState, selectedPageId, previewMode]);
|
||||
}, [chatState, pageFullscreenPreviewOpen, previewMode, selectedPageId]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
@@ -1174,47 +1159,7 @@ export function MindSpaceView({
|
||||
onContextUpdate={setPageLiveContext}
|
||||
autoOpenPlaza={selectedPageAutoPlaza}
|
||||
refreshTrigger={pageRefreshTrigger}
|
||||
onFullscreenPreviewChange={(open) => {
|
||||
setPageFullscreenPreviewOpen(open);
|
||||
if (!open) {
|
||||
setPageEditSubSessionId(null);
|
||||
setPageEditSubSessionActive(false);
|
||||
}
|
||||
}}
|
||||
overlayChat={
|
||||
mindspaceChatContext && session?.id
|
||||
? {
|
||||
context: mindspaceChatContext,
|
||||
user,
|
||||
parentSessionId: session.id,
|
||||
h5ApiBase,
|
||||
onOpenFullChat: onBack,
|
||||
onSubSessionChange: (subSessionId) => {
|
||||
setPageEditSubSessionId(subSessionId);
|
||||
setPageEditSubSessionActive(Boolean(subSessionId));
|
||||
},
|
||||
onForkUnavailable: () => {
|
||||
setPageEditSubSessionId(null);
|
||||
setPageEditSubSessionActive(false);
|
||||
if (selectedPageId && session?.id) {
|
||||
void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
},
|
||||
onPageSaved: (result) => {
|
||||
if (result.kind === 'page' && result.pageId) {
|
||||
void refreshPagesSilently();
|
||||
if (result.pageId === selectedPageId) {
|
||||
setPageRefreshTrigger((value) => value + 1);
|
||||
} else {
|
||||
showPage(result.pageId);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
: null
|
||||
}
|
||||
onFullscreenPreviewChange={setPageFullscreenPreviewOpen}
|
||||
/>
|
||||
)
|
||||
) : newPageOpen ? (
|
||||
|
||||
Reference in New Issue
Block a user