(() => { const MAX_VISIBLE = 6; const blocks = []; let showAll = false; let idSeed = 1; let running = false; let timers = []; const stackShell = document.getElementById('stackShell'); const stackInner = document.getElementById('stackInner'); const btnPlay = document.getElementById('playLoop'); const btnStep = document.getElementById('stepOnce'); const btnPause = document.getElementById('pauseLoop'); const btnClear = document.getElementById('clearAll'); const btnToggleAll = document.getElementById('toggleAll'); const samples = [ '评估用户意图,先汇总检索关键字,再推演候选工具。', '正在调用 web_search,拼接 query 并去重关键词。', '校验上下文有效性,准备输出草稿。', '工具结果将写入下一个文本块,留意引用。', '自动回溯最近 3 条工具调用,避免重复请求。' ]; const pick = () => samples[Math.floor(Math.random() * samples.length)]; const createBlock = type => ({ id: `${type}-${idSeed++}`, type, title: type === 'thinking' ? '思考过程' : '搜索完成', detail: pick(), streaming: true, expanded: type === 'thinking', timestamp: Date.now() }); const computeVisible = () => { if (showAll || blocks.length <= MAX_VISIBLE) return [...blocks]; const hidden = blocks.length - MAX_VISIBLE; return [{ id: `more-${hidden}`, type: 'more', hidden }, ...blocks.slice(-MAX_VISIBLE)]; }; const updateButtons = () => { const overLimit = blocks.length > MAX_VISIBLE; btnToggleAll.disabled = !overLimit; btnToggleAll.textContent = showAll ? '收起到最近 6 个' : '展开全部'; }; const measurePositions = () => { const map = {}; stackInner.querySelectorAll('[data-id]').forEach(el => { map[el.dataset.id] = el.getBoundingClientRect(); }); return map; }; const animateHeight = (prev, next) => { if (prev === next) return; stackShell.style.height = prev + 'px'; // force reflow stackShell.getBoundingClientRect(); stackShell.style.transition = 'height 280ms cubic-bezier(0.25, 0.9, 0.3, 1)'; stackShell.style.height = next + 'px'; const clear = () => { stackShell.style.transition = ''; stackShell.removeEventListener('transitionend', clear); }; stackShell.addEventListener('transitionend', clear); }; const applyFLIP = (prev, isGrowMode, addedId) => { stackInner.querySelectorAll('[data-id]').forEach(el => { const id = el.dataset.id; const isMoreRow = id.startsWith('more'); const now = el.getBoundingClientRect(); const old = prev[id]; if (old) { const dy = old.top - now.top; if (Math.abs(dy) > 0.5) { el.style.transform = `translateY(${dy}px)`; el.style.transition = 'transform 220ms ease, opacity 220ms ease'; requestAnimationFrame(() => { el.style.transform = 'translateY(0)'; }); el.addEventListener( 'transitionend', () => { el.style.transform = ''; el.style.transition = ''; }, { once: true } ); } } else { const usePeek = !showAll && blocks.length <= MAX_VISIBLE && id === addedId; if (usePeek) { el.classList.add('peek'); requestAnimationFrame(() => el.classList.remove('peek')); } else if (isGrowMode || isMoreRow) { el.classList.add('flow-enter'); requestAnimationFrame(() => el.classList.add('flow-enter-active')); el.addEventListener( 'transitionend', () => { el.classList.remove('flow-enter', 'flow-enter-active'); }, { once: true } ); } else if (id === addedId) { el.classList.add('peek'); requestAnimationFrame(() => el.classList.remove('peek')); } } }); }; const createBlockRow = block => { const row = document.createElement('div'); row.className = `row block-row type-${block.type}`; row.dataset.id = block.id; const main = document.createElement('div'); main.className = 'row-main'; main.addEventListener('click', () => { block.expanded = !block.expanded; render(); }); const arrow = document.createElement('div'); arrow.className = `arrow ${block.expanded ? 'open' : ''}`; arrow.textContent = '›'; const icon = document.createElement('div'); icon.className = 'row-icon'; icon.textContent = block.type === 'thinking' ? '🧠' : '🔍'; const text = document.createElement('div'); text.className = 'row-text'; const title = document.createElement('div'); title.className = 'row-title'; title.textContent = block.title; const sub = document.createElement('div'); sub.className = 'row-sub'; if (block.type === 'thinking') { sub.textContent = block.streaming ? '思考中 · 默认展开' : '思考完成 · 点击查看详情'; } else { sub.textContent = block.streaming ? '工具调用中 · 运行后自动收起' : '工具完成 · 点击展开结果'; } text.appendChild(title); text.appendChild(sub); const pill = document.createElement('div'); pill.className = `pill ${block.streaming ? '' : 'green'}`; pill.textContent = block.streaming ? '进行中' : '已完成'; main.appendChild(arrow); main.appendChild(icon); main.appendChild(text); main.appendChild(pill); const detailWrap = document.createElement('div'); detailWrap.className = 'row-detail'; detailWrap.style.display = block.expanded ? 'block' : 'none'; const detailInner = document.createElement('div'); detailInner.className = 'row-detail-inner'; detailInner.textContent = block.detail; detailWrap.appendChild(detailInner); row.appendChild(main); row.appendChild(detailWrap); if (block.streaming) { const progress = document.createElement('div'); progress.className = 'progress'; row.appendChild(progress); } return row; }; const createMoreRow = (id, hidden) => { const row = document.createElement('div'); row.className = 'row more-row'; row.dataset.id = id; row.addEventListener('click', () => { showAll = true; render(); }); const dot = document.createElement('div'); dot.className = 'more-dot'; dot.textContent = '···'; const text = document.createElement('div'); const title = document.createElement('div'); title.className = 'row-title'; title.textContent = '更多'; const tip = document.createElement('div'); tip.className = 'more-tip'; tip.textContent = `已折叠 ${hidden} 个更早的块,点击展开查看`; text.appendChild(title); text.appendChild(tip); const btn = document.createElement('button'); btn.className = 'more-btn'; btn.textContent = '展开'; row.appendChild(dot); row.appendChild(text); row.appendChild(btn); return row; }; const render = (options = {}) => { const prev = measurePositions(); const prevHeight = stackShell.getBoundingClientRect().height || 0; const visible = computeVisible(); stackInner.innerHTML = ''; if (!visible.length) { stackShell.classList.add('empty'); updateButtons(); animateHeight(prevHeight, 140); return; } stackShell.classList.remove('empty'); visible.forEach(block => { if (block.type === 'more') { stackInner.appendChild(createMoreRow(block.id, block.hidden)); } else { stackInner.appendChild(createBlockRow(block)); } }); const isGrowMode = showAll || blocks.length <= MAX_VISIBLE; requestAnimationFrame(() => applyFLIP(prev, isGrowMode, options.newlyAddedId)); updateButtons(); const nextHeight = stackInner.scrollHeight; animateHeight(prevHeight || nextHeight, nextHeight); }; const addBlock = type => { const block = createBlock(type); blocks.push(block); render({ newlyAddedId: block.id }); // 返回 Promise,结束时机取决于类型 return new Promise(resolve => { if (type === 'thinking') { timers.push( setTimeout(() => { block.streaming = false; block.detail += ' · 已产出结论,等待工具。'; render(); }, 1200) ); timers.push( setTimeout(() => { block.expanded = false; render(); resolve(); }, 2000) ); } else { timers.push( setTimeout(() => { block.streaming = false; block.detail = '检索到 3 条结果,已写入后续回复。'; render(); resolve(); }, 1400) ); } }); }; const wait = ms => new Promise(r => timers.push(setTimeout(r, ms))); const runCycle = async () => { const thinkingDone = await addBlock('thinking'); await wait(200); // 给折叠留一帧 await addBlock('tool'); await wait(500); // 循环间隔 }; const playLoop = async () => { if (running) return; running = true; btnPlay.disabled = true; btnStep.disabled = true; btnPause.disabled = false; while (running) { await runCycle(); } btnPlay.disabled = false; btnStep.disabled = false; btnPause.disabled = true; }; const stepOnce = async () => { if (running) return; btnStep.disabled = true; await runCycle(); btnStep.disabled = false; }; const pauseLoop = () => { running = false; }; const clearAll = () => { timers.forEach(t => clearTimeout(t)); timers = []; running = false; blocks.length = 0; showAll = false; render(); btnPlay.disabled = false; btnStep.disabled = false; btnPause.disabled = true; }; btnPlay.addEventListener('click', playLoop); btnStep.addEventListener('click', stepOnce); btnPause.addEventListener('click', pauseLoop); btnClear.addEventListener('click', clearAll); btnToggleAll.addEventListener('click', () => { if (blocks.length <= MAX_VISIBLE) return; showAll = !showAll; render(); }); // 初始播放一轮示例,演示“先思考后工具”的顺序 stepOnce(); })();