feat: release chat and mindspace UI updates

This commit is contained in:
john
2026-06-27 21:57:21 +08:00
parent c924b2bb77
commit 8264e71f9e
24 changed files with 2042 additions and 209 deletions
+111
View File
@@ -0,0 +1,111 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { buildChatSavePreviewFrameUrl, quickShareFromChat } from '../api/client';
type SharePhase =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'done'; publicUrl: string }
| { kind: 'error'; message: string };
export function ChatSharePreviewModal({
sessionId,
messageId,
selectedLinkIndex = 0,
onClose,
onSave,
}: {
sessionId: string;
messageId: string;
selectedLinkIndex?: number;
onClose: () => void;
onSave?: () => void;
}) {
const [phase, setPhase] = useState<SharePhase>({ kind: 'idle' });
const [copied, setCopied] = useState(false);
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const previewUrl = buildChatSavePreviewFrameUrl({ sessionId, messageId, selectedLinkIndex });
useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}, []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
useEffect(() => () => { if (copyTimer.current) clearTimeout(copyTimer.current); }, []);
const share = async () => {
if (phase.kind === 'loading') return;
setPhase({ kind: 'loading' });
try {
const result = await quickShareFromChat({ sessionId, messageId, selectedLinkIndex });
setPhase({ kind: 'done', publicUrl: result.publicUrl });
} catch (err) {
setPhase({ kind: 'error', message: err instanceof Error ? err.message : '公开分享失败,请重试' });
}
};
const copy = async (url: string) => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
if (copyTimer.current) clearTimeout(copyTimer.current);
copyTimer.current = setTimeout(() => setCopied(false), 2000);
} catch { /* ignore */ }
};
const handleSave = () => {
onClose();
onSave?.();
};
return createPortal(
<div className="chat-share-preview-overlay" role="dialog" aria-modal="true" aria-label="页面预览">
<div className="chat-share-preview-header">
<span className="chat-share-preview-title"></span>
<button type="button" className="chat-share-preview-close" onClick={onClose} aria-label="关闭"></button>
</div>
<div className="chat-share-preview-body">
<iframe src={previewUrl} title="页面预览" sandbox="allow-same-origin allow-scripts allow-popups" className="chat-share-preview-frame" />
<div className="chat-share-side-actions">
{phase.kind === 'done' ? (
<div className="chat-share-result-card">
<p className="chat-share-result-label"> </p>
<button type="button" className="chat-share-result-copy" onClick={() => void copy(phase.publicUrl)}>
{copied ? '已复制 ✓' : '复制链接'}
</button>
<a href={phase.publicUrl} target="_blank" rel="noreferrer" className="chat-share-result-open">
</a>
</div>
) : (
<button
type="button"
className={`chat-share-side-btn chat-share-side-btn-primary${phase.kind === 'loading' ? ' is-loading' : ''}`}
disabled={phase.kind === 'loading'}
onClick={() => void share()}
title="将页面转为公开链接,私有图片自动转为公开地址"
>
{phase.kind === 'loading' ? '处理中…' : '🌐 公开分享'}
</button>
)}
{phase.kind === 'error' && <p className="chat-share-side-error">{phase.message}</p>}
{onSave && (
<button type="button" className="chat-share-side-btn" onClick={handleSave}>
📄
</button>
)}
</div>
</div>
</div>,
document.body,
);
}