的动态流式弹出)
AI 提问建议Prompt Suggestions的动态流式弹出在对话交互系统中用户很多时候并不知道下一句该问什么。大语言模型输出完一长串技术分析或操作方案后页面若只是死寂地停留在光标闪烁状态交互回路就出现了断崖式的冷场。动态跟随上下文生成“下一步追问建议Follow-up Suggestions”或“快捷操作药丸Action Chips”已经成为新一代智能助理的标配能力。然而如果等模型完整吐出全部回答、后端再开启一次耗时两三秒的独立请求去获取推荐问题用户早就视线转移或直接关闭了窗口。高水准的交互实现是将追问建议作为主流式响应的“尾随数据流”或者通过独立的轻量旁路流与主回答近乎并行输出并以前端细腻的交错动效Stagger Animation平滑浮现。数据协议设计主副数据流拆分与协议隔离在基于 Server-Sent Events (SSE) 或 WebSocket 的传输通道中推荐问题的下发有两种常见策略带外事件Out-of-band Events在同一个 SSE 流中以不同的event类型发送数据。例如主文本使用event: text_delta尾随的推荐词使用event: suggestions_delta或整包event: suggestions_complete。结构化标记定界Delimiter Delimited模型在正文结束后输出特定定界符如SUGGESTIONS后续内容解析为 JSON 或每行一条的纯文本数组。在生产环境中带外事件是更稳妥的选择能彻底避免模型在正文里偶然出现定界符而导致前端解析错乱的问题。// sse-parser.ts export interface SuggestionItem { id: string; text: string; category?: deepen | expand | action; } export interface StreamEventHandlers { onTextDelta: (chunk: string) void; onSuggestionReceived: (suggestions: SuggestionItem[]) void; onComplete: () void; onError: (err: Error) void; } export async function consumeChatStream( url: string, payload: Recordstring, unknown, handlers: StreamEventHandlers, signal: AbortSignal ) { const response await fetch(url, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), signal }); if (!response.ok || !response.body) { throw new Error(HTTP error: ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; try { while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n\n); buffer lines.pop() || ; for (const block of lines) { const rawEvent block.trim(); if (!rawEvent) continue; let eventType message; let dataStr ; for (const line of rawEvent.split(\n)) { if (line.startsWith(event:)) { eventType line.slice(6).trim(); } else if (line.startsWith(data:)) { dataStr line.slice(5).trim(); } } if (eventType text_delta) { handlers.onTextDelta(dataStr); } else if (eventType suggestions_complete) { try { const parsed JSON.parse(dataStr) as SuggestionItem[]; handlers.onSuggestionReceived(parsed); } catch (e) { console.warn(Failed to parse suggestions payload, e); } } } } handlers.onComplete(); } catch (error) { if ((error as DOMException).name ! AbortError) { handlers.onError(error as Error); } } }空间位移与交错微动效Stagger Motion当建议数据包到达前端时最忌讳的是瞬间将一组 DOM 节点突兀地插在对话气泡下方。这种硬切换会造成页面高度瞬时跳变Layout Shift如果视口恰好固定在底部还会引发滚动条剧烈抖动。优雅的处理手法包含以下几个细节占位与平滑高度过渡利用 CSS Grid 或max-height配合requestAnimationFrame让建议容器的高度像展开画卷般平滑张开避免视口猛烈下移。交错延时入场Staggered Fade-in每个建议药丸以 60ms 到 80ms 的微小延迟依次向上滑入并淡入给用户视觉引导。触控与按键适配每个药丸不仅支持点击还带有数字角标快捷键如Alt 1、Alt 2按下即可直接把文本推入输入框并触发发送。// PromptSuggestions.tsx import React, { FC } from react; import ./PromptSuggestions.css; export interface Props { suggestions: SuggestionItem[]; onSelect: (item: SuggestionItem) void; disabled?: boolean; } export const PromptSuggestions: FCProps ({ suggestions, onSelect, disabled }) { if (!suggestions || suggestions.length 0) return null; return ( div classNamesuggestions-wrapper roleregion aria-label相关追问建议 div classNamesuggestions-header span classNamesuggestions-icon✨/span span classNamesuggestions-title建议继续探讨/span /div div classNamesuggestions-list {suggestions.map((item, index) ( button key{item.id || index} typebutton classNamesuggestion-chip style{{ animationDelay: ${index * 70}ms }} onClick{() onSelect(item)} disabled{disabled} aria-keyshortcuts{Alt${index 1}} span classNameshortcut-badge{index 1}/span span classNamechip-text{item.text}/span /button ))} /div /div ); };配合 CSS 的硬件加速变换属性让药丸从下方 8 像素处平稳归位/* PromptSuggestions.css */ .suggestions-wrapper { margin-top: 14px; padding: 12px 16px; background: rgba(246, 248, 250, 0.85); border-radius: 12px; border: 1px solid rgba(220, 225, 230, 0.6); backdrop-filter: blur(8px); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); } .suggestions-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; } .suggestion-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: #ffffff; border: 1px solid #e1e4e8; border-radius: 20px; font-size: 13px; color: #24292e; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); opacity: 0; transform: translateY(8px); animation: chipSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; } .suggestion-chip:hover:not(:disabled) { transform: translateY(-1px); border-color: #0969da; box-shadow: 0 3px 8px rgba(9, 105, 218, 0.12); color: #0969da; } .shortcut-badge { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; font-size: 10px; background: #f1f3f5; color: #57606a; border-radius: 50%; } keyframes chipSlideIn { to { opacity: 1; transform: translateY(0); } }竞态条件与中断取消控制当用户在模型还在输出正文或建议刚刚开始浮现的瞬间手动输入了新的问题并敲下回车必须具备清晰的状态归零逻辑主动中断未决的流利用AbortController立即断开当前的流式通道丢弃任何正在传输或待渲染的建议列表。历史卡片的交互禁用旧消息下方的建议药丸在用户发出下一轮提问后应自动置为只读Disabled或降为次级透明度防止用户误触旧上下文引发意图分裂。滚动锚定协同在建议弹出触发容器高度扩增时如果当前滚动条已经贴底Pinned to Bottom需利用ResizeObserver配合平滑滚动继续锚定在可视区域最下端确保用户视线不会被突然顶出屏幕。将建议弹出的时机、流式协议的边界与动效渲染严丝合缝地组合在一起AI 界面才能褪去机械生成的冰冷感呈现出行云流水般的连续交互体验。