Commit 9dc2093f by luoqi

feat(web): artifact 放大改本屏居中大窗 — 流式生成时大窗同步实时"长"

- iframe 渲染抽成 ArtifactFrame(持久壳 + postMessage 注入)可多实例:
  内联卡 + 居中大窗各挂一个,同吃一份流式 html → 放大后内容继续实时增长;
  生成中大窗头部带 ⟳ 提示。
- 放大键不再开新 tab → 居中大窗(min(980px,94vw) × 86vh,遮罩/✕ 关闭,z-[70]);
  大窗内保留「新窗口打开」小按钮;内联卡 autoHeight 原行为,大窗锁高内部滚动。

web tsc 0 + Next 生产构建过。仅本地,未部署。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 74735286
......@@ -16,6 +16,8 @@ import {
Sparkles,
Square,
Wrench,
X,
ExternalLink,
} from 'lucide-react';
import { io, type Socket } from 'socket.io-client';
import { cn } from '@/lib/utils';
......@@ -248,7 +250,18 @@ function buildStandaloneDoc(inner: string): string {
return `<!DOCTYPE html><html lang="zh"><head>${ARTIFACT_HEAD}</head><body class="p-4">${inner}</body></html>`;
}
function ArtifactView({ artifact }: { artifact: Artifact }) {
/// 单个 artifact 的沙箱 iframe 渲染器(持久壳 + postMessage 增量注入;可多实例:
/// 内联卡 + 居中放大窗同时挂,各自吃同一份流式 html → 放大窗也实时"长")。
/// autoHeight:内联卡随内容增高;放大窗 false(锁满容器,内部滚动)。
function ArtifactFrame({
artifact,
autoHeight,
className,
}: {
artifact: Artifact;
autoHeight: boolean;
className?: string;
}) {
const ref = useRef<HTMLIFrameElement>(null);
const [height, setHeight] = useState(120);
const shell = useMemo(() => artifactShell(), []);
......@@ -264,7 +277,6 @@ function ArtifactView({ artifact }: { artifact: Artifact }) {
lastPostRef.current = Date.now();
};
// 内容/状态变化 → postMessage 注入(流式 500ms 节流;final 立即,触发图表脚本)。
useEffect(() => {
htmlRef.current = artifact.html;
finalRef.current = !artifact.streaming;
......@@ -291,14 +303,31 @@ function ArtifactView({ artifact }: { artifact: Artifact }) {
readyRef.current = true;
post();
}
if (typeof d?.__artifactHeight === 'number') setHeight(Math.min(Math.max(d.__artifactHeight, 60), 1600));
if (autoHeight && typeof d?.__artifactHeight === 'number') {
setHeight(Math.min(Math.max(d.__artifactHeight, 60), 1600));
}
};
window.addEventListener('message', onMsg);
return () => window.removeEventListener('message', onMsg);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [autoHeight]);
return (
<iframe
ref={ref}
title={artifact.title || 'artifact'}
sandbox="allow-scripts"
srcDoc={shell}
className={cn('w-full', className)}
style={autoHeight ? { height, border: 0 } : { border: 0 }}
/>
);
}
const openFull = () => {
function ArtifactView({ artifact }: { artifact: Artifact }) {
const [expanded, setExpanded] = useState(false);
const openTab = () => {
const w = window.open('', '_blank');
w?.document.write(buildStandaloneDoc(artifact.html));
w?.document.close();
......@@ -311,21 +340,50 @@ function ArtifactView({ artifact }: { artifact: Artifact }) {
<span className="text-[11.5px] font-medium text-slate-500">{artifact.title || '可视化卡片'}</span>
<button
type="button"
onClick={openFull}
title="新窗口打开"
onClick={() => setExpanded(true)}
title="放大查看"
className="ml-auto inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10.5px] text-slate-400 hover:bg-slate-100 hover:text-slate-600"
>
<Maximize2 className="h-3 w-3" />
</button>
</div>
<iframe
ref={ref}
title={artifact.title || 'artifact'}
sandbox="allow-scripts"
srcDoc={shell}
className="w-full"
style={{ height, border: 0 }}
/>
<ArtifactFrame artifact={artifact} autoHeight />
{/* 居中放大窗:与内联卡同源同流(streaming 时这里同步在"长") */}
{expanded && (
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/40" onClick={() => setExpanded(false)} />
<div className="relative z-10 flex h-[min(86vh,920px)] w-[min(980px,94vw)] flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl">
<div className="flex flex-none items-center gap-1.5 border-b border-slate-100 bg-slate-50/60 px-3 py-2">
<LayoutTemplate className="h-4 w-4 text-teal-600" />
<span className="text-[12.5px] font-medium text-slate-600">{artifact.title || '可视化卡片'}</span>
{artifact.streaming && (
<span className="ml-1 inline-flex items-center gap-1 text-[10.5px] text-teal-600">
<Loader2 className="h-3 w-3 animate-spin" />
生成中
</span>
)}
<button
type="button"
onClick={openTab}
title="新窗口打开"
className="ml-auto inline-flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-600"
>
<ExternalLink className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={() => setExpanded(false)}
title="关闭"
className="inline-flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-600"
>
<X className="h-4 w-4" />
</button>
</div>
<ArtifactFrame artifact={artifact} autoHeight={false} className="min-h-0 flex-1" />
</div>
</div>
)}
</div>
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment