feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export function ChatLoadingSpinner({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<span className={`chat-loading-spinner${className ? ` ${className}` : ''}`} aria-hidden="true">
|
||||
<span />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../utils/imageUpload';
|
||||
import { AvatarPicker } from './AvatarPicker';
|
||||
import { ChatSkillPicker } from './ChatSkillPicker';
|
||||
import { ChatLoadingSpinner } from './ChatLoadingSpinner';
|
||||
import { DesignSkillPanel } from './DesignSkillPanel';
|
||||
import { MessageList } from './MessageList';
|
||||
import { PageSaveDialog } from './PageSaveDialog';
|
||||
@@ -292,13 +293,15 @@ export function ChatPanel({
|
||||
|
||||
const placeholder = offlineBlocked
|
||||
? '网络断开,恢复连接后可继续输入'
|
||||
: chatState === 'connecting'
|
||||
: compact
|
||||
? '随时召唤你的小助手吧'
|
||||
: randomPrompt;
|
||||
const connectStatusText =
|
||||
chatState === 'connecting'
|
||||
? '正在连接会话…'
|
||||
: chatState === 'waiting'
|
||||
? '消息已收到,正在连接后台…'
|
||||
: compact
|
||||
? '随时召唤你的小助手吧'
|
||||
: randomPrompt;
|
||||
: null;
|
||||
|
||||
const mergeVoiceText = (spoken: string) => {
|
||||
const base = voiceBaseRef.current.trimEnd();
|
||||
@@ -609,6 +612,12 @@ export function ChatPanel({
|
||||
)}
|
||||
|
||||
<footer className={compact ? 'space-chat-panel-footer' : `footer${showHomeWelcome ? ' footer-home' : ''}`}>
|
||||
{connectStatusText && (
|
||||
<div className="chat-connect-status" role="status" aria-live="polite">
|
||||
<ChatLoadingSpinner />
|
||||
<span>{connectStatusText}</span>
|
||||
</div>
|
||||
)}
|
||||
{voiceNotice && (
|
||||
<div className="voice-notice" role="status">
|
||||
{voiceNotice}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { BalanceRing } from './BalanceRing';
|
||||
import { HistorySidebar } from './HistorySidebar';
|
||||
import { TKMindAvatar } from './TKMindAvatar';
|
||||
import { ChatPanel } from './ChatPanel';
|
||||
import { ChatLoadingSpinner } from './ChatLoadingSpinner';
|
||||
import { WechatBindPrompt } from './WechatBindPrompt';
|
||||
import { WechatAccountButton } from './WechatAccountButton';
|
||||
import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu';
|
||||
@@ -90,6 +91,7 @@ export function ChatView({
|
||||
setSidebarOpen(false);
|
||||
};
|
||||
|
||||
const isConnectingTitle = !session && chatState !== 'idle';
|
||||
const sessionTitle = session
|
||||
? getSessionDisplayName(session)
|
||||
: chatState === 'idle'
|
||||
@@ -145,7 +147,10 @@ export function ChatView({
|
||||
<TKMindAvatar size="sm" className="header-brand-avatar" />
|
||||
<div>
|
||||
<div className="header-title">{user?.displayName ?? 'TKMind'}</div>
|
||||
<div className="header-sub">{sessionTitle}</div>
|
||||
<div className="header-sub">
|
||||
{isConnectingTitle ? <ChatLoadingSpinner /> : null}
|
||||
<span>{sessionTitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-actions header-actions-desktop">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useUserAvatar } from '../hooks/useUserAvatar';
|
||||
import type { Message } from '../types';
|
||||
import { getDisplayText, getImageUrls, getRenderableImageUrls, getThinking } from '../utils/message';
|
||||
import { getDisplayText, getImageUrls, getRenderableImageUrls, getThinking, shouldShowChatMessage } from '../utils/message';
|
||||
import { getMessageSaveActions } from '../utils/messageSave';
|
||||
import { renderMarkdown } from '../utils/markdown';
|
||||
import { filterText } from '../utils/wordFilter';
|
||||
@@ -365,8 +365,8 @@ export function MessageList({
|
||||
return (
|
||||
<div className="message-list">
|
||||
{messages.length === 0 && <ChatWelcomePanel compact={compact} />}
|
||||
{messages.map((message, index) => {
|
||||
const prev = index > 0 ? messages[index - 1] : undefined;
|
||||
{messages.filter(shouldShowChatMessage).map((message, index, visibleMessages) => {
|
||||
const prev = index > 0 ? visibleMessages[index - 1] : undefined;
|
||||
const showTime = shouldShowTimestamp(message.created, prev?.created);
|
||||
const anchorId = message.id ?? `msg-${index}-${message.role}-${message.created}`;
|
||||
return (
|
||||
|
||||
@@ -35,8 +35,7 @@ import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel';
|
||||
import { MindSpaceModal } from './MindSpaceModal';
|
||||
import { MindSpaceDeletePlazaOption } from './MindSpaceDeletePlazaOption';
|
||||
import { MindSpacePublishSuccess } from './MindSpacePublishSuccess';
|
||||
import { ShareSheet } from './ShareSheet';
|
||||
import { buildPageSharePayload, type SharePayload } from '../utils/shareChannels';
|
||||
import { prepareHtmlPageBrandMarkers } from '../../mindspace-page-tag.mjs';
|
||||
|
||||
type AccessMode = MindSpacePublishCheck['accessMode'];
|
||||
|
||||
@@ -271,7 +270,6 @@ export function MindSpacePageDetail({
|
||||
const [deletePreview, setDeletePreview] = useState<MindSpacePageDeletePreview | null>(null);
|
||||
const [deletePreviewLoading, setDeletePreviewLoading] = useState(false);
|
||||
const [deleteRemoveFromPlaza, setDeleteRemoveFromPlaza] = useState(false);
|
||||
const [sharePayload, setSharePayload] = useState<SharePayload | null>(null);
|
||||
const [previewFrame, setPreviewFrame] = useState<HTMLIFrameElement | null>(null);
|
||||
const [isExportingImage, setIsExportingImage] = useState(false);
|
||||
const [exportFeedback, setExportFeedback] = useState<string | null>(null);
|
||||
@@ -315,13 +313,17 @@ export function MindSpacePageDetail({
|
||||
setError(null);
|
||||
try {
|
||||
const next = await getMindSpacePage(pageId);
|
||||
const pageContent =
|
||||
next.contentFormat === 'html'
|
||||
? prepareHtmlPageBrandMarkers(next.content ?? '')
|
||||
: (next.content ?? '');
|
||||
setPage(next);
|
||||
resetDraft({
|
||||
title: next.title,
|
||||
summary: next.summary,
|
||||
content: next.content ?? '',
|
||||
content: pageContent,
|
||||
});
|
||||
setPreviewContent(next.content ?? '');
|
||||
setPreviewContent(pageContent);
|
||||
setPreviewRefreshPending(false);
|
||||
setPreviewKey((value) => value + 1);
|
||||
setTemplateId(next.templateId);
|
||||
@@ -773,13 +775,6 @@ export function MindSpacePageDetail({
|
||||
setPreviewRefreshPending(false);
|
||||
};
|
||||
|
||||
const handleOpenShareSheet = () => {
|
||||
if (!page) return;
|
||||
const shareUrl = page.publication ? resolvePublicPageUrl(page.publication.publicUrl) : undefined;
|
||||
setSharePayload(buildPageSharePayload(page, shareUrl));
|
||||
setExportFeedback(null);
|
||||
};
|
||||
|
||||
const handleExportPageImage = async () => {
|
||||
if (!page) return;
|
||||
setIsExportingImage(true);
|
||||
@@ -897,9 +892,6 @@ export function MindSpacePageDetail({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mindspace-page-floating-actions">
|
||||
<button type="button" className="mindspace-page-floating-action" onClick={handleOpenShareSheet}>
|
||||
分享
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-page-floating-action is-secondary"
|
||||
@@ -1445,16 +1437,10 @@ export function MindSpacePageDetail({
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
{sharePayload ? (
|
||||
<ShareSheet
|
||||
payload={sharePayload}
|
||||
publication={page?.publication}
|
||||
onClose={() => setSharePayload(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{fullscreenPreviewOpen && page && page.contentFormat === 'html' ? (
|
||||
<MindSpacePageFullscreenPreview
|
||||
pageId={page.id}
|
||||
pageTitle={title || page.title}
|
||||
title={title}
|
||||
content={content}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { rewriteMindSpacePageDownloadLinks } from '../api/client';
|
||||
import { bindIframeDocumentHeightSync, measureDocumentHeight } from '../utils/iframeContentHeight';
|
||||
import {
|
||||
buildEditablePreviewDocument,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
} from '../utils/mindspaceVisualEditor';
|
||||
|
||||
export function MindSpacePageEditablePreviewFrame({
|
||||
pageId,
|
||||
title,
|
||||
content,
|
||||
reloadKey,
|
||||
@@ -18,6 +20,7 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
onFrameReady,
|
||||
onContentChange,
|
||||
}: {
|
||||
pageId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
reloadKey: number;
|
||||
@@ -34,6 +37,7 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
const frameClassName =
|
||||
className ?? (compact ? 'page-save-mini-page-frame' : 'mindspace-page-preview-frame');
|
||||
const [previewFailed, setPreviewFailed] = useState(false);
|
||||
const [displayContent, setDisplayContent] = useState(content);
|
||||
const [srcdoc, setSrcdoc] = useState(() => buildEditablePreviewDocument(content));
|
||||
const [height, setHeight] = useState(minHeight);
|
||||
const iframeContentRef = useRef(content);
|
||||
@@ -41,19 +45,30 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
const syncHeightRef = useRef<(() => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
iframeContentRef.current = content;
|
||||
setSrcdoc(buildEditablePreviewDocument(content));
|
||||
setPreviewFailed(false);
|
||||
setHeight(minHeight);
|
||||
}, [reloadKey]);
|
||||
let cancelled = false;
|
||||
const timer = window.setTimeout(() => {
|
||||
void rewriteMindSpacePageDownloadLinks(pageId, content)
|
||||
.then((html) => {
|
||||
if (cancelled) return;
|
||||
setDisplayContent(html);
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return;
|
||||
setDisplayContent(content);
|
||||
});
|
||||
}, 120);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [pageId, content, reloadKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (content === iframeContentRef.current) return;
|
||||
iframeContentRef.current = content;
|
||||
setSrcdoc(buildEditablePreviewDocument(content));
|
||||
iframeContentRef.current = displayContent;
|
||||
setSrcdoc(buildEditablePreviewDocument(displayContent));
|
||||
setPreviewFailed(false);
|
||||
setHeight(minHeight);
|
||||
}, [content, minHeight]);
|
||||
}, [displayContent, reloadKey, minHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MindSpacePageEditablePreviewFrame } from './MindSpacePageEditablePrevie
|
||||
import type { MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
||||
|
||||
export function MindSpacePageFullscreenPreview({
|
||||
pageId,
|
||||
pageTitle,
|
||||
title,
|
||||
content,
|
||||
@@ -17,6 +18,7 @@ export function MindSpacePageFullscreenPreview({
|
||||
refreshPending = false,
|
||||
onContentChange,
|
||||
}: {
|
||||
pageId: string;
|
||||
pageTitle: string;
|
||||
title: string;
|
||||
content: string;
|
||||
@@ -101,6 +103,7 @@ export function MindSpacePageFullscreenPreview({
|
||||
|
||||
<div className="mindspace-page-fullscreen-preview-body">
|
||||
<MindSpacePageEditablePreviewFrame
|
||||
pageId={pageId}
|
||||
title={title}
|
||||
content={content}
|
||||
reloadKey={reloadKey}
|
||||
|
||||
@@ -170,6 +170,7 @@ export function MindSpacePagePreviewPanel({
|
||||
</p>
|
||||
<div className="page-save-mini-page">
|
||||
<MindSpacePageEditablePreviewFrame
|
||||
pageId={pageId}
|
||||
title={title}
|
||||
content={content}
|
||||
reloadKey={reloadKey}
|
||||
|
||||
@@ -2928,20 +2928,30 @@ export function MindSpaceView({
|
||||
</>
|
||||
)}
|
||||
|
||||
{previewAsset && previewAssetFrame && (
|
||||
{previewAsset && (isImageAsset(previewAsset) || previewAssetFrame) && (
|
||||
<div className="mindspace-asset-preview-backdrop" role="presentation">
|
||||
<section className="mindspace-asset-preview-panel" role="dialog" aria-modal="true">
|
||||
<div className="mindspace-asset-preview-bar">
|
||||
<strong>文件预览</strong>
|
||||
<strong>{previewAsset.displayName || '文件预览'}</strong>
|
||||
<button type="button" onClick={() => setPreviewAssetId(null)}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<iframe
|
||||
title="资产预览"
|
||||
src={previewAssetFrame.src}
|
||||
{...(previewAssetFrame.sandbox ? { sandbox: previewAssetFrame.sandbox } : {})}
|
||||
/>
|
||||
{isImageAsset(previewAsset) ? (
|
||||
<img
|
||||
className="mindspace-asset-preview-image"
|
||||
src={buildAssetImageUrl(previewAsset)}
|
||||
alt={previewAsset.displayName}
|
||||
/>
|
||||
) : (
|
||||
previewAssetFrame && (
|
||||
<iframe
|
||||
title="资产预览"
|
||||
src={previewAssetFrame.src}
|
||||
{...(previewAssetFrame.sandbox ? { sandbox: previewAssetFrame.sandbox } : {})}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
@@ -2957,33 +2967,13 @@ export function MindSpaceView({
|
||||
</div>
|
||||
<span>先从成果开始,目录稍后再整理</span>
|
||||
</div>
|
||||
<div className="mindspace-grid">
|
||||
{space.categories
|
||||
.filter((category) => ['oa', 'public'].includes(category.code))
|
||||
.map((category) => (
|
||||
<article
|
||||
className={`mindspace-card mindspace-card-${category.code}`}
|
||||
key={category.id}
|
||||
>
|
||||
<div className="mindspace-card-top">
|
||||
<span className="mindspace-card-code">{category.code.toUpperCase()}</span>
|
||||
<span>{category.itemCount} 项</span>
|
||||
</div>
|
||||
<h3>{category.name}</h3>
|
||||
<p>{CATEGORY_DESCRIPTIONS[category.code]}</p>
|
||||
{CATEGORY_ACTIONS[category.code] && (
|
||||
<button type="button" onClick={() => void openCategory(category)}>
|
||||
{CATEGORY_ACTIONS[category.code]}
|
||||
</button>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
<div className="mindspace-grid-secondary">
|
||||
<div className="mindspace-category-layout">
|
||||
<div className="mindspace-grid-zones">
|
||||
{space.categories
|
||||
.filter((category) => category.code === 'draft')
|
||||
.filter((category) => ['oa', 'public', 'archive'].includes(category.code))
|
||||
.map((category) => (
|
||||
<article
|
||||
className={`mindspace-card mindspace-card-${category.code}`}
|
||||
className={`mindspace-card mindspace-card-compact mindspace-card-${category.code}`}
|
||||
key={category.id}
|
||||
>
|
||||
<div className="mindspace-card-top">
|
||||
@@ -2999,11 +2989,13 @@ export function MindSpaceView({
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="mindspace-grid-secondary">
|
||||
{space.categories
|
||||
.filter((category) => category.code === 'archive')
|
||||
.filter((category) => category.code === 'draft')
|
||||
.map((category) => (
|
||||
<article
|
||||
className={`mindspace-card mindspace-card-${category.code}`}
|
||||
className={`mindspace-card mindspace-card-compact mindspace-card-${category.code}`}
|
||||
key={category.id}
|
||||
>
|
||||
<div className="mindspace-card-top">
|
||||
@@ -3012,6 +3004,11 @@ export function MindSpaceView({
|
||||
</div>
|
||||
<h3>{category.name}</h3>
|
||||
<p>{CATEGORY_DESCRIPTIONS[category.code]}</p>
|
||||
{CATEGORY_ACTIONS[category.code] && (
|
||||
<button type="button" onClick={() => void openCategory(category)}>
|
||||
{CATEGORY_ACTIONS[category.code]}
|
||||
</button>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -155,6 +155,7 @@ export function PageSaveDialog({
|
||||
categoryCode,
|
||||
selectedLinkIndex,
|
||||
replacePageId,
|
||||
saveAsNew: duplicateResolved === 'new',
|
||||
});
|
||||
if (result.kind === 'page') {
|
||||
setSaveNotice(replacePageId ? '已替换原有页面' : `已保存到${CATEGORY_LABELS[categoryCode]}`);
|
||||
@@ -196,7 +197,7 @@ export function PageSaveDialog({
|
||||
|
||||
{!analysisLoading && showDuplicatePrompt && (
|
||||
<div className="page-save-duplicate">
|
||||
<p>此消息已保存为「{existingPage!.title}」,请选择操作:</p>
|
||||
<p>此页面已保存为「{existingPage!.title}」,请选择操作:</p>
|
||||
<div className="page-save-duplicate-actions">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -51,6 +51,7 @@ export function SpaceChatPanel({
|
||||
uploadChatImage,
|
||||
retryConnect,
|
||||
} = chat;
|
||||
const { capabilities, grantedSkills } = mainChat;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -134,6 +135,8 @@ export function SpaceChatPanel({
|
||||
chatState={chatState}
|
||||
pendingTool={pendingTool}
|
||||
session={session}
|
||||
capabilities={capabilities ?? undefined}
|
||||
grantedSkills={grantedSkills}
|
||||
onSubmit={(text, imageUrls, previewImageUrls) =>
|
||||
chatBridge
|
||||
? void submit(text, context, imageUrls, previewImageUrls)
|
||||
|
||||
Reference in New Issue
Block a user