Files
memind/src/components/MindSpacePagePreviewPanel.tsx
T
john 722b18326f feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:08:06 +08:00

227 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import {
regenerateMindSpacePageThumbnail,
uploadMindSpacePageThumbnail,
} from '../api/client';
import { compressImageForUpload } from '../utils/imageUpload';
import { MindSpacePageEditablePreviewFrame } from './MindSpacePageEditablePreviewFrame';
async function readCompressedImageAsBase64(file: File): Promise<{ base64: string; mimeType: string }> {
const compressed = await compressImageForUpload(file, {
maxInputBytes: 8 * 1024 * 1024,
maxOutputBytes: 900 * 1024,
maxDimension: 1200,
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = String(reader.result ?? '');
const match = result.match(/^data:([^;]+);base64,(.+)$/);
if (!match) {
reject(new Error('无法读取图片'));
return;
}
resolve({ mimeType: match[1], base64: match[2] });
};
reader.onerror = () => reject(new Error('无法读取图片'));
reader.readAsDataURL(compressed);
});
}
export function MindSpacePagePreviewPanel({
pageId,
title,
summary,
content,
reloadKey,
onFrameReady,
onContentChange,
}: {
pageId: string;
title: string;
summary: string;
content: string;
reloadKey: number;
onFrameReady?: (iframe: HTMLIFrameElement | null) => void;
onContentChange: (html: string) => void;
}) {
const [thumbnailFailed, setThumbnailFailed] = useState(false);
const [thumbnailVersion, setThumbnailVersion] = useState(reloadKey);
const [thumbBusy, setThumbBusy] = useState<'upload' | 'ai' | null>(null);
const [thumbNotice, setThumbNotice] = useState<string | null>(null);
const [thumbError, setThumbError] = useState<string | null>(null);
const uploadInputRef = useRef<HTMLInputElement>(null);
const thumbnailRefreshTokenRef = useRef(0);
const latestDraftRef = useRef({ title, summary, content });
const thumbnailUrl = `/api/mindspace/v1/pages/${pageId}/thumbnail?v=${thumbnailVersion}`;
useEffect(() => {
setThumbnailVersion(reloadKey);
}, [reloadKey]);
useEffect(() => {
setThumbnailFailed(false);
}, [thumbnailUrl]);
useEffect(() => {
latestDraftRef.current = { title, summary, content };
}, [content, summary, title]);
useEffect(() => {
let cancelled = false;
const refreshToken = ++thumbnailRefreshTokenRef.current;
const { title: draftTitle, summary: draftSummary, content: draftContent } = latestDraftRef.current;
const syncThumbnail = async () => {
try {
const result = await regenerateMindSpacePageThumbnail(pageId, {
title: draftTitle,
summary: draftSummary,
content: draftContent,
});
if (cancelled || thumbnailRefreshTokenRef.current !== refreshToken) return;
setThumbnailVersion(result.updatedAt ?? Date.now());
setThumbnailFailed(false);
} catch {
if (cancelled || thumbnailRefreshTokenRef.current !== refreshToken) return;
setThumbnailFailed(false);
}
};
void syncThumbnail();
return () => {
cancelled = true;
};
}, [pageId, reloadKey]);
const bumpThumbnail = () => {
setThumbnailVersion(Date.now());
setThumbnailFailed(false);
};
const handleUploadClick = () => {
uploadInputRef.current?.click();
};
const handleUploadFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
if (!file.type.startsWith('image/')) {
setThumbError('请选择图片文件');
return;
}
setThumbBusy('upload');
setThumbError(null);
setThumbNotice(null);
try {
const { base64, mimeType } = await readCompressedImageAsBase64(file);
await uploadMindSpacePageThumbnail(pageId, {
imageBase64: base64,
mimeType,
title,
summary,
content,
});
bumpThumbnail();
setThumbNotice('封面已替换,保存页面后仍会保留');
} catch (err) {
setThumbError(err instanceof Error ? err.message : '上传封面失败');
} finally {
setThumbBusy(null);
}
};
const handleAiRegenerate = async () => {
setThumbBusy('ai');
setThumbError(null);
setThumbNotice(null);
try {
const result = await regenerateMindSpacePageThumbnail(pageId, {
title,
summary,
content,
useAi: true,
instruction: '根据当前页面内容与标题,生成更精美、可在信息流中直接展示的中文封面。',
});
if (result.content) {
onContentChange(result.content);
}
bumpThumbnail();
setThumbNotice('AI 已重新生成封面');
} catch (err) {
setThumbError(err instanceof Error ? err.message : 'AI 封面生成失败');
} finally {
setThumbBusy(null);
}
};
return (
<div className="page-save-preview-dual mindspace-page-preview-dual">
<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>
<div className="page-save-mini-page">
<MindSpacePageEditablePreviewFrame
pageId={pageId}
title={title}
content={content}
reloadKey={reloadKey}
compact
showHint={false}
onFrameReady={onFrameReady}
onContentChange={onContentChange}
/>
</div>
</div>
<div className="page-save-preview-pane page-save-preview-pane-thumb">
<span className="page-save-preview-label"></span>
<p className="page-save-preview-hint"> 3:4 </p>
<div className="page-save-card-thumb">
{!thumbnailFailed ? (
<img
src={thumbnailUrl}
alt={`${title || '页面'} 卡片预览图`}
onError={() => setThumbnailFailed(true)}
/>
) : (
<span className="page-save-card-thumb-placeholder"></span>
)}
</div>
<div className="mindspace-page-thumb-actions">
<input
ref={uploadInputRef}
type="file"
accept="image/*"
hidden
onChange={(event) => void handleUploadFile(event)}
/>
<button
type="button"
onClick={handleUploadClick}
disabled={thumbBusy !== null}
>
{thumbBusy === 'upload' ? '上传中…' : '上传替换'}
</button>
<button
type="button"
onClick={() => void handleAiRegenerate()}
disabled={thumbBusy !== null}
>
{thumbBusy === 'ai' ? '生成中…' : 'AI 重新生成'}
</button>
</div>
{thumbNotice ? <p className="mindspace-page-thumb-note">{thumbNotice}</p> : null}
{thumbError ? <p className="mindspace-page-thumb-error">{thumbError}</p> : null}
</div>
</div>
);
}