尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

一键重新生成与多版本对比滑动交互设计

一键重新生成与多版本对比滑动交互设计 一键重新生成与多版本对比滑动交互设计在大模型落地于内容生成、代码重构与文案润色的前端场景中“重新生成”绝不是一个简单的覆盖替换按钮。业务一线经常遭遇两个极端要么直接抹掉上一轮生成导致用户遗失了某个闪光片段要么粗暴地在下方追加卡片造成信息流无限膨胀眼花缭乱。如何在单一视口内承载同一上下文的多个生成版本并提供如拉开卷轴般丝滑的对比交互是交互架构师必须攻克的细节。版本树的拓扑与状态切片大模型的多版本并非线性数组而是一个轻量级的版本分支树。每一次点击“重新生成”本质上是在父节点下挂载新的兄弟节点。前端状态管理器必须精确记录激活路径同时保留各分支的流式生成元数据与滚动锚点。export interface GenerationNode { id: string; parentId: string | null; version: number; content: string; status: streaming | complete | failed; metrics?: { latency: number; tokenCount: number; }; createdAt: number; } export interface ConversationTurn { turnId: string; prompt: string; versions: GenerationNode[]; activeVersionId: string; }在状态树中我们维护activeVersionId作为默认展示态。当用户触发对比模式时状态派生出compareVersionId将单一渲染管道扩展为双轨对比通道。幕布滑动式Split-Curtain交互机制传统的左右双栏排版在移动端或窄屏容器中会极度压缩排版宽度造成长文本折行紊乱。幕布滑动对比Curtain Comparison采用双层绝对定位叠加渲染上层通过clip-path: inset()动态裁切配合中间的物理手柄让用户通过拖拽直观探查两版之间的微妙差异。import React, { useState, useRef, useCallback, useEffect } from react; interface DiffSliderProps { leftContent: string; rightContent: string; leftVersionLabel: string; rightVersionLabel: string; } export const DiffSlider: React.FCDiffSliderProps ({ leftContent, rightContent, leftVersionLabel, rightVersionLabel, }) { const [sliderPos, setSliderPos] useStatenumber(50); // 百分比 const [isDragging, setIsDragging] useStateboolean(false); const containerRef useRefHTMLDivElement(null); const leftScrollRef useRefHTMLDivElement(null); const rightScrollRef useRefHTMLDivElement(null); const isSyncingScroll useRefboolean(false); // 双栏滚动联动锁 const handleScroll useCallback((source: left | right) { if (isSyncingScroll.current) return; isSyncingScroll.current true; const srcEl source left ? leftScrollRef.current : rightScrollRef.current; const tgtEl source left ? rightScrollRef.current : leftScrollRef.current; if (srcEl tgtEl) { const scrollPercentage srcEl.scrollTop / (srcEl.scrollHeight - srcEl.clientHeight); tgtEl.scrollTop scrollPercentage * (tgtEl.scrollHeight - tgtEl.clientHeight); } requestAnimationFrame(() { isSyncingScroll.current false; }); }, []); const handlePointerMove useCallback( (e: PointerEvent) { if (!isDragging || !containerRef.current) return; const rect containerRef.current.getBoundingClientRect(); const x Math.max(0, Math.min(e.clientX - rect.left, rect.width)); const percentage (x / rect.width) * 100; setSliderPos(percentage); }, [isDragging] ); const handlePointerUp useCallback(() { setIsDragging(false); }, []); useEffect(() { if (isDragging) { window.addEventListener(pointermove, handlePointerMove); window.addEventListener(pointerup, handlePointerUp); } return () { window.removeEventListener(pointermove, handlePointerMove); window.removeEventListener(pointerup, handlePointerUp); }; }, [isDragging, handlePointerMove, handlePointerUp]); return ( div ref{containerRef} classNamerelative w-full h-[520px] overflow-hidden rounded-xl border border-slate-700 bg-slate-900 select-none {/* 底层版本 B (右侧/最新版) */} div ref{rightScrollRef} onScroll{() handleScroll(right)} classNameabsolute inset-0 p-6 overflow-y-auto font-mono text-sm leading-relaxed text-slate-200 bg-slate-950 div classNameinline-block px-2.5 py-1 mb-4 text-xs font-semibold text-emerald-400 bg-emerald-950/60 rounded border border-emerald-800/50 {rightVersionLabel} /div div classNamewhitespace-pre-wrap{rightContent}/div /div {/* 顶层版本 A (左侧/历史版) 配合 clip-path */} div ref{leftScrollRef} onScroll{() handleScroll(left)} style{{ clipPath: inset(0 ${100 - sliderPos}% 0 0) }} classNameabsolute inset-0 p-6 overflow-y-auto font-mono text-sm leading-relaxed text-slate-200 bg-slate-900 z-10 div classNameinline-block px-2.5 py-1 mb-4 text-xs font-semibold text-indigo-400 bg-indigo-950/60 rounded border border-indigo-800/50 {leftVersionLabel} /div div classNamewhitespace-pre-wrap{leftContent}/div /div {/* 中间拖拽手柄分割线 */} div style{{ left: ${sliderPos}% }} onPointerDown{() setIsDragging(true)} classNameabsolute top-0 bottom-0 w-1 bg-indigo-500/80 cursor-ew-resize z-20 transition-shadow hover:shadow-[0_0_12px_rgba(99,102,241,0.8)] div classNameabsolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-7 h-7 rounded-full bg-indigo-600 border-2 border-white flex items-center justify-center shadow-lg svg classNamew-4 h-4 text-white viewBox0 0 24 24 fillnone strokecurrentColor strokeWidth2.5 path dM8 7l-5 5 5 5M16 7l5 5-5 5 / /svg /div /div /div ); };文本粒度的 Diff 映射与滚动防抖当两版文本长度悬殊时简单的像素等比滚动会导致视口内对齐错位。在专业级工程实现中需借助轻量分词与 Myers 算法生成对齐标记点Anchor Anchoring在组件初始化时使用 Web Worker 将两版内容解析为 Token 序列计算出新增、删除与保留段落的行索引映射表。双栏滚动联动不再直接同步scrollTop而是计算当前视口顶部的 Token 序号通过二分查找锁定目标栏对应的 Token 偏移再平滑插值到物理像素。对流式生成中的版本由于内容实时增长渲染层采用动态挂起的防抖策略在打字机吐字阶段禁用重度 Diff 计算仅维持底部的滚动跟随待响应结束进入complete态后统一计算行级映射。交互细节中的动效克制多版本切换时的生硬切屏是破坏沉浸感的主因。我们采用弹性过渡曲线为手柄赋予微量惯性阻尼。在点击版本号胶囊如1/3、2/3时手柄并非瞬移而是以形如cubic-bezier(0.22, 1, 0.36, 1)的山水回锋姿态滑行至对侧将视觉过渡的主动权自然还给读者。这种兼具工程严密与视觉呼吸感的交互才是生产级 AI 工具应有的质感。
返回列表