Files
memind/src/components/MindSpacePagePreviewPanel.tsx
T
john 9b4a25799f Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 15:19:03 +08:00

226 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
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>
);
}