agent-Specialization/demo/stacked-blocks/stack7.html

842 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>≥7 块传送带 Demo</title>
<style>
:root {
--bg: #f6f1e6;
--card: rgba(255, 255, 255, 0.86);
--card-solid: #ffffff;
--border: #e0d7c7;
--shadow: 0 16px 44px rgba(91, 74, 54, 0.12);
--text: #4b3e2f;
--text-muted: #9a8d7d;
--accent: #b98a59;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 24px;
background: radial-gradient(circle at 18% 18%, rgba(185, 138, 89, 0.08), transparent 28%), linear-gradient(180deg, #faf7f0, #f5f1e7);
font-family: "Inter", "PingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
}
.page { max-width: 960px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: var(--shadow);
padding: 12px 16px;
}
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
button {
border: 1px solid var(--border);
background: #fff;
color: var(--text);
border-radius: 12px;
padding: 8px 12px;
font-size: 13px;
cursor: pointer;
transition: all 150ms ease;
box-shadow: 0 6px 18px rgba(91, 74, 54, 0.08);
}
button.primary { background: linear-gradient(135deg, #cfa976, #b98957); border-color: #c49556; color: #fffdf8; }
button:active { transform: translateY(1px); }
button:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
.hint { color: var(--text-muted); font-size: 13px; }
.stack-shell {
position: relative;
border: 1px solid var(--border);
border-radius: 16px;
background: var(--card);
box-shadow: var(--shadow);
overflow: hidden;
transition:
height 280ms cubic-bezier(0.4, 0, 0.2, 1),
padding-top 280ms cubic-bezier(0.4, 0, 0.2, 1);
min-height: 0;
padding-top: 0;
}
.stack-inner { width: 100%; position: relative; z-index: 1; }
.more-block {
position: absolute;
inset: 0 0 auto 0;
background: var(--card-solid);
border-bottom: 0 solid var(--border);
display: flex;
align-items: center;
gap: 10px;
padding: 0 16px;
height: 0;
opacity: 0;
overflow: hidden;
z-index: 20;
cursor: pointer;
transition:
height 280ms cubic-bezier(0.4,0,0.2,1),
padding 280ms cubic-bezier(0.4,0,0.2,1),
border-bottom-width 280ms cubic-bezier(0.4,0,0.2,1),
opacity 200ms ease;
}
.more-block.visible {
opacity: 1;
padding: 14px 16px;
border-bottom-width: 1px;
}
.more-label { font-weight: 700; color: var(--text); }
.more-desc { color: var(--text-muted); font-size: 12px; }
.more-expand { margin-left: auto; font-size: 12px; color: var(--accent); cursor: pointer; user-select: none; }
.block {
background: transparent;
border-bottom: 1px solid var(--border);
}
.block:last-child { border-bottom: none; }
.collapsible-block {
margin: 0;
border: none;
border-radius: 0;
box-shadow: none;
position: relative;
overflow: hidden;
}
.collapsible-header {
padding: 14px 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
user-select: none;
background: rgba(255, 255, 255, 0.78);
transition: background-color 0.2s ease;
position: relative;
}
.arrow { width: 20px; height: 20px; display: grid; place-items: center; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); color: var(--text-muted); font-size: 18px; }
.collapsible-block.expanded .arrow { transform: rotate(90deg); }
.status-icon { width: 24px; height: 24px; display: grid; place-items: center; font-size: 18px; }
.status-text { font-size: 14px; font-weight: 600; color: var(--text); }
.tool-desc { color: var(--text-muted); font-size: 12px; margin-left: 6px; }
.collapsible-content {
max-height: 0;
overflow: hidden;
opacity: 0;
transition:
max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.collapsible-block.expanded .collapsible-content {
max-height: 600px;
opacity: 1;
}
.content-inner {
padding: 16px 20px 24px 56px;
font-size: 14px;
line-height: 1.6;
color: var(--text-muted);
white-space: pre-wrap;
}
.content-inner::after {
content: '';
display: block;
height: 12px;
}
.progress-indicator {
position: absolute; bottom: 0; left: 0; height: 2px;
background: var(--accent);
animation: progress-bar 2s ease-in-out infinite;
opacity: 1;
}
@keyframes progress-bar {
0% { width: 0%; left: 0%; }
50% { width: 40%; left: 30%; }
100% { width: 0%; left: 100%; }
}
.footer-note { color: var(--text-muted); font-size: 12px; text-align: right; }
</style>
</head>
<body>
<div class="page">
<div class="panel">
<div class="controls">
<button id="play" class="primary">开始演示 (0→10块)</button>
<button id="reset">重置</button>
<span class="hint">前 6 块动画与 stack6 完全一致;第 7 块起触发“更多”+传送带。</span>
</div>
</div>
<div class="stack-shell" id="shell">
<div class="more-block" id="moreBlock">
<span class="more-label">⋯ 更多</span>
<span class="more-desc" id="moreDesc">0 条折叠</span>
</div>
<div class="stack-inner" id="inner"></div>
</div>
<div class="footer-note">demo/stacked-blocks/stack7.html · 7+ 块传送带示例</div>
</div>
<script>
const shell = document.getElementById('shell');
const inner = document.getElementById('inner');
const moreBlock = document.getElementById('moreBlock');
const moreDesc = document.getElementById('moreDesc');
const btnPlay = document.getElementById('play');
const btnReset = document.getElementById('reset');
let running = false;
let timers = [];
let idSeed = 1;
let shellHeight = 0;
let blocks = [];
let showAll = false;
let toggling = false;
// 为了便于快速查看 demo前 6 个阶段缩短时长(参考 stack6
const THINKING_DURATION = 2400; // 原 4800
const THINKING_COLLAPSE_DELAY = 120; // 原 200
const TOOL_DURATION = 2000; // 原 4200
const STREAM_INTERVAL = 90;
const VISIBLE = 6;
const pick = () => {
const samples = [
'评估用户意图,列出检索关键词。',
'构造搜索查询,并准备调用工具。',
'生成中间结论,等待工具补充。',
'对结果做归因与去重。',
'落盘前再次校验上下文。'
];
return samples[Math.floor(Math.random() * samples.length)];
};
const randomLetters = (len = 2) => {
const chars = 'abcdefghijklmnopqrstuvwxyz';
let out = '';
for (let i = 0; i < len; i++) out += chars[Math.floor(Math.random() * chars.length)];
return out;
};
const wait = (ms) => new Promise((r) => timers.push(setTimeout(r, ms)));
const createBlockEl = (type) => {
const block = document.createElement('div');
block.className = 'block';
block.dataset.id = `blk-${idSeed++}`;
const wrap = document.createElement('div');
wrap.className = 'collapsible-block ' + (type === 'thinking' ? 'thinking-block' : 'tool-block');
const header = document.createElement('div');
header.className = 'collapsible-header';
const arrow = document.createElement('div');
arrow.className = 'arrow';
arrow.textContent = '';
const icon = document.createElement('div');
icon.className = 'status-icon';
icon.textContent = type === 'thinking' ? '🧠' : '🔍';
const text = document.createElement('span');
text.className = 'status-text';
text.textContent = type === 'thinking' ? '思考过程' : '搜索完成';
const desc = document.createElement('span');
desc.className = 'tool-desc';
desc.textContent = type === 'thinking' ? '正在思考...' : '“RAG 模型 2024 2025 常用”';
header.appendChild(arrow);
header.appendChild(icon);
header.appendChild(text);
header.appendChild(desc);
const contentWrap = document.createElement('div');
contentWrap.className = 'collapsible-content';
const innerContent = document.createElement('div');
innerContent.className = 'content-inner';
innerContent.textContent = pick();
contentWrap.appendChild(innerContent);
wrap.appendChild(header);
wrap.appendChild(contentWrap);
block.appendChild(wrap);
header.addEventListener('click', () => toggleBlock(wrap));
return { block, wrap, contentWrap, innerContent, desc };
};
const animateHeight = (from, to) => {
if (from === to) return Promise.resolve();
return new Promise((resolve) => {
shell.style.height = from + 'px';
shell.getBoundingClientRect();
shell.style.height = to + 'px';
const onEnd = () => {
shell.removeEventListener('transitionend', onEnd);
resolve();
};
shell.addEventListener('transitionend', onEnd, { once: true });
});
};
const measureBlockHeight = (block, isLast) => {
const header = block.querySelector('.collapsible-header');
const content = block.querySelector('.collapsible-content');
const progress = block.querySelector('.progress-indicator');
const wrap = block.firstElementChild;
const expanded = wrap?.classList.contains('expanded');
const headerH = header ? Math.ceil(header.getBoundingClientRect().height) : 0;
const contentH = content && expanded ? Math.ceil(content.scrollHeight) : 0;
const progressH = progress ? Math.ceil(progress.getBoundingClientRect().height) : 0;
const borderH = !isLast ? parseFloat(getComputedStyle(block).borderBottomWidth) || 0 : 0;
return headerH + contentH + progressH + borderH;
};
const measureListHeight = (listEls) => {
return listEls.reduce((acc, b, idx) => acc + measureBlockHeight(b, idx === listEls.length - 1), 0);
};
const capturePositions = () => {
const map = {};
inner.querySelectorAll('.block').forEach((b) => {
map[b.dataset.id] = { top: b.offsetTop, height: b.offsetHeight };
});
return map;
};
const playFLIP = (prevMap, nextMap) => {
if (!prevMap || !nextMap) return;
inner.querySelectorAll('.block').forEach((b) => {
const id = b.dataset.id;
const old = prevMap[id];
const next = nextMap[id];
if (!old || !next) return;
const dy = old.top - next.top;
if (Math.abs(dy) < 0.5) return;
b.style.transform = `translateY(${dy}px)`;
b.style.transition = 'transform 220ms ease';
requestAnimationFrame(() => {
b.style.transform = '';
});
b.addEventListener('transitionend', () => {
b.style.transition = '';
}, { once: true });
});
};
const waitForContentTransition = (content) =>
new Promise((resolve) => {
if (!content) return resolve();
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
resolve();
}
}, 500);
const handler = (e) => {
if (e.target === content && e.propertyName === 'max-height') {
if (!settled) {
settled = true;
clearTimeout(timer);
content.removeEventListener('transitionend', handler);
resolve();
}
}
};
content.addEventListener('transitionend', handler);
});
const applyContentTransition = (content, toHeight, toOpacity) => {
if (!content) return;
const current = content.style.maxHeight;
if (!current || current === '0px') {
content.style.maxHeight = content.scrollHeight + 'px';
}
content.getBoundingClientRect();
content.style.maxHeight = toHeight;
content.style.opacity = toOpacity;
};
const setExpandedState = (wrap, expanded) => {
const content = wrap.querySelector('.collapsible-content');
wrap.classList.toggle('expanded', expanded);
if (content) {
const target = expanded ? content.scrollHeight : 0;
content.style.maxHeight = target + 'px';
content.style.opacity = expanded ? '1' : '0';
}
};
const normalizeBlockState = (block) => {
const wrap = block?.firstElementChild;
if (!wrap) return;
const content = wrap.querySelector('.collapsible-content');
const expanded = wrap.classList.contains('expanded');
if (content) {
content.style.maxHeight = expanded ? content.scrollHeight + 'px' : '0px';
content.style.opacity = expanded ? '1' : '0';
}
block.style.transition = '';
block.style.transform = '';
};
const toggleBlock = async (wrap) => {
const content = wrap.querySelector('.collapsible-content');
const prev = capturePositions();
const next = !wrap.classList.contains('expanded');
setExpandedState(wrap, next);
const nextPos = capturePositions();
await Promise.all([updateHeight(prev, nextPos), waitForContentTransition(content)]);
};
const streamThinkingContent = (contentEl, wrap) => {
if (!contentEl) return;
let charInLine = 0;
const start = performance.now();
const tick = () => {
if (!running) return;
const elapsed = performance.now() - start;
if (elapsed >= THINKING_DURATION - STREAM_INTERVAL) return;
const chunk = randomLetters(2);
chunk.split('').forEach((ch) => {
if (charInLine >= 10) {
contentEl.textContent += '\n';
charInLine = 0;
}
contentEl.textContent += ch;
charInLine += 1;
});
const contentWrap = wrap.querySelector('.collapsible-content');
contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
if (contentWrap) contentWrap.style.maxHeight = contentWrap.scrollHeight + 'px';
updateHeight();
const t = setTimeout(tick, STREAM_INTERVAL);
timers.push(t);
};
tick();
};
const updateMore = (hiddenCount, refHeight, forceVisible = false, labelOverride = null) => {
const shouldShow = hiddenCount > 0 || forceVisible;
if (shouldShow) {
moreBlock.classList.add('visible');
moreBlock.style.height = refHeight + 'px';
shell.style.paddingTop = refHeight + 'px';
moreDesc.textContent = labelOverride ?? `${hiddenCount} 条折叠`;
} else {
moreBlock.classList.remove('visible');
moreBlock.style.height = '0px';
shell.style.paddingTop = '0px';
moreDesc.textContent = '无折叠';
}
};
const updateHeight = async (prevPositions, nextPositions) => {
const children = Array.from(inner.children);
const visibleH = measureListHeight(children);
const moreH = moreBlock.classList.contains('visible') ? parseFloat(moreBlock.style.height) || 0 : 0;
const target = Math.max(0, moreH + visibleH);
const anim = animateHeight(shellHeight, target);
shellHeight = target;
if (prevPositions && nextPositions) {
playFLIP(prevPositions, nextPositions);
}
await anim;
};
const conveyorShift = async (firstHeight, firstOverflow) => {
const items = Array.from(inner.children);
if (!items.length) return;
const targetShell = shellHeight + (firstOverflow ? firstHeight : 0);
if (firstOverflow) {
moreBlock.classList.add('visible');
moreBlock.style.height = firstHeight + 'px';
moreDesc.textContent = `${blocks.length - VISIBLE} 条折叠`;
shell.style.paddingTop = firstHeight + 'px';
}
// 传送带上移
const movePromise = new Promise((resolve) => {
let done = 0;
items.forEach((el) => {
el.style.transition = 'transform 260ms cubic-bezier(0.4,0,0.2,1)';
el.style.transform = `translateY(-${firstHeight}px)`;
el.addEventListener(
'transitionend',
() => {
done += 1;
if (done === items.length) resolve();
},
{ once: true }
);
});
requestAnimationFrame(() => {});
});
const heightPromise = animateHeight(shellHeight, targetShell).then(() => {
shellHeight = targetShell;
});
await Promise.all([movePromise, heightPromise]);
// 清理
const first = inner.firstElementChild;
if (first) inner.removeChild(first);
Array.from(inner.children).forEach((el) => {
el.style.transition = '';
el.style.transform = '';
});
};
// 展开抽屉动画:把折叠的块从顶部“吐”出来,现有 6 个块整体下移
const expandDrawer = async (hiddenBlocks, visibleBlocks) => {
if (!hiddenBlocks.length) return render({ mode: 'collapse' }); // nothing hidden, fallback
// 先放入 DOM 便于测量
inner.innerHTML = '';
const full = [...hiddenBlocks, ...visibleBlocks];
full.forEach((b) => {
normalizeBlockState(b);
b.style.transition = 'none';
b.style.transform = '';
b.style.opacity = '1';
inner.appendChild(b);
});
const totalH = measureListHeight(full);
const ref = full[0] || null;
const refH = ref ? measureBlockHeight(ref) : 0;
const firstVisible = visibleBlocks[0] || null;
const hiddenH = firstVisible ? firstVisible.offsetTop : measureListHeight(hiddenBlocks); // 精确到分割线
// 顶部展开块保持占位
updateMore(hiddenBlocks.length, refH, true, '已展开全部');
// 初始状态:整体向上移 hiddenH使当前 6 个保持原位,隐藏块在遮挡上方
full.forEach((b, idx) => {
b.style.transition = 'none';
b.style.transform = `translateY(-${hiddenH}px)`;
if (idx < hiddenBlocks.length) b.style.opacity = '0';
});
inner.getBoundingClientRect(); // reflow
// 整体下滑,隐藏块同步显现(无级联,整体运动)
full.forEach((b, idx) => {
b.style.transition = `transform 260ms cubic-bezier(0.4,0,0.2,1), opacity 220ms ease`;
});
const targetH = refH + totalH;
const heightAnim = animateHeight(shellHeight, targetH).then(() => {
shellHeight = targetH;
});
requestAnimationFrame(() => {
full.forEach((b, idx) => {
b.style.transform = '';
if (idx < hiddenBlocks.length) b.style.opacity = '1';
});
});
// 等待动画结束
await Promise.all([heightAnim, wait(300)]);
// 清理内联样式,恢复自然流
full.forEach((b) => {
b.style.transition = '';
b.style.transform = '';
b.style.opacity = '';
});
};
// 折叠抽屉动画:把隐藏块一起“收回”到顶部
const collapseDrawer = async (hiddenBlocks, visibleBlocks) => {
if (!hiddenBlocks.length) {
// 没有需要折叠的,直接常规渲染
inner.innerHTML = '';
visibleBlocks.forEach((b) => {
normalizeBlockState(b);
inner.appendChild(b);
});
const ref = visibleBlocks[0] || blocks[0] || null;
const refH = ref ? measureBlockHeight(ref) : 0;
updateMore(0, refH, false, null);
await updateHeight();
return;
}
inner.innerHTML = '';
const full = [...hiddenBlocks, ...visibleBlocks];
full.forEach((b) => {
normalizeBlockState(b);
b.style.transition = 'none';
b.style.transform = '';
b.style.opacity = '1';
inner.appendChild(b);
});
const ref = full[0] || null;
const refH = ref ? measureBlockHeight(ref) : 0;
const firstVisible = visibleBlocks[0] || null;
const hiddenH = firstVisible ? firstVisible.offsetTop : measureListHeight(hiddenBlocks);
const visibleH = measureListHeight(visibleBlocks);
// 确保“更多”占位存在,显示折叠数量
updateMore(hiddenBlocks.length, refH, true, `${hiddenBlocks.length} 条折叠`);
inner.getBoundingClientRect(); // reflow
// 统一动画:整体向上移 hiddenH隐藏块淡出
full.forEach((b, idx) => {
b.style.transition = `transform 260ms cubic-bezier(0.4,0,0.2,1)`;
});
const targetHeight = refH + visibleH;
const heightAnim = animateHeight(shellHeight, targetHeight).then(() => {
shellHeight = targetHeight;
});
requestAnimationFrame(() => {
full.forEach((b) => {
b.style.transform = `translateY(-${hiddenH}px)`;
});
});
await Promise.all([heightAnim, wait(300)]);
// 移除隐藏块,恢复最近 VISIBLE
inner.innerHTML = '';
visibleBlocks.forEach((b) => {
b.style.transition = '';
b.style.transform = '';
inner.appendChild(b);
});
updateMore(hiddenBlocks.length, refH, false, null);
await updateHeight();
};
const render = async (options = {}) => {
const { mode } = options; // mode: 'expand' | 'collapse' | undefined
const prevPositions = capturePositions();
const hiddenRaw = Math.max(0, blocks.length - VISIBLE);
const visibleBlocks = showAll ? blocks : blocks.slice(-VISIBLE);
// 展开动画:折叠块逐个“吐”出
if (mode === 'expand') {
const hiddenBlocks = blocks.slice(0, -VISIBLE);
const visibleList = blocks.slice(-VISIBLE);
await expandDrawer(hiddenBlocks, visibleList);
return;
}
// 收起动画:整体上收
if (mode === 'collapse') {
const hiddenBlocks = blocks.slice(0, -VISIBLE);
const visibleList = blocks.slice(-VISIBLE);
await collapseDrawer(hiddenBlocks, visibleList);
return;
}
// 折叠或常规重渲染
inner.innerHTML = '';
visibleBlocks.forEach((b) => {
normalizeBlockState(b);
b.style.transition = '';
b.style.transform = '';
inner.appendChild(b);
});
const ref = visibleBlocks[0] || blocks[0] || null;
const refH = ref ? measureBlockHeight(ref) : 0;
const hiddenCount = showAll ? 0 : hiddenRaw;
const forceVisible = showAll; // 展开后依然保留“展开块”占位,避免顶部跳变
const label = showAll ? '已展开全部' : null;
updateMore(hiddenCount, refH, forceVisible, label);
const nextPositions = capturePositions();
await updateHeight(prevPositions, nextPositions);
};
const addThinking = async () => {
const { block, wrap, innerContent, desc } = createBlockEl('thinking');
wrap.dataset.type = 'thinking';
const progress = document.createElement('div');
progress.className = 'progress-indicator';
wrap.appendChild(progress);
if (innerContent) innerContent.textContent = '';
const willOverflow = !showAll && blocks.length >= VISIBLE;
const hiddenBefore = Math.max(0, blocks.length - VISIBLE);
blocks.push(block);
if (willOverflow) {
inner.appendChild(block);
const first = inner.firstElementChild;
const firstH = first ? measureBlockHeight(first) : measureBlockHeight(block);
const firstOverflow = hiddenBefore === 0;
setExpandedState(wrap, true);
streamThinkingContent(innerContent, wrap);
await conveyorShift(firstH, firstOverflow);
// 保留最近 VISIBLE
inner.innerHTML = '';
blocks.slice(-VISIBLE).forEach((b) => inner.appendChild(b));
const ref = inner.firstElementChild || block;
const refH = ref ? measureBlockHeight(ref) : firstH;
updateMore(blocks.length - VISIBLE, refH);
await updateHeight();
} else {
const prev = capturePositions();
inner.appendChild(block);
setExpandedState(wrap, true);
streamThinkingContent(innerContent, wrap);
const next = capturePositions();
await updateHeight(prev, next);
}
await wait(THINKING_DURATION);
desc.textContent = '思考完成,自动折叠';
wrap.removeChild(progress);
await wait(THINKING_COLLAPSE_DELAY);
setExpandedState(wrap, false);
await updateHeight();
if (showAll) {
const ref = blocks[0] || null;
const refH = ref ? measureBlockHeight(ref) : 0;
updateMore(Math.max(0, blocks.length - VISIBLE), refH, true, '已展开全部');
}
};
const addTool = async () => {
const { block, wrap, desc } = createBlockEl('tool');
wrap.dataset.type = 'tool';
setExpandedState(wrap, false);
const progress = document.createElement('div');
progress.className = 'progress-indicator';
wrap.appendChild(progress);
const willOverflow = !showAll && blocks.length >= VISIBLE;
const hiddenBefore = Math.max(0, blocks.length - VISIBLE);
blocks.push(block);
if (willOverflow) {
inner.appendChild(block);
const first = inner.firstElementChild;
const firstH = first ? measureBlockHeight(first) : measureBlockHeight(block);
const firstOverflow = hiddenBefore === 0;
await conveyorShift(firstH, firstOverflow);
inner.innerHTML = '';
blocks.slice(-VISIBLE).forEach((b) => inner.appendChild(b));
const ref = inner.firstElementChild || block;
const refH = ref ? measureBlockHeight(ref) : firstH;
updateMore(blocks.length - VISIBLE, refH);
await updateHeight();
} else {
const prev = capturePositions();
inner.appendChild(block);
const next = capturePositions();
await updateHeight(prev, next);
}
await wait(TOOL_DURATION);
desc.textContent = '工具完成 · 结果已写入';
wrap.removeChild(progress);
await updateHeight();
if (showAll) {
const ref = blocks[0] || null;
const refH = ref ? measureBlockHeight(ref) : 0;
updateMore(Math.max(0, blocks.length - VISIBLE), refH, true, '已展开全部');
}
};
const runSequence = async () => {
const sequence = ['thinking', 'tool', 'thinking', 'tool', 'thinking', 'tool', 'thinking', 'tool', 'thinking', 'tool'];
for (const type of sequence) {
if (!running) break;
if (type === 'thinking') await addThinking();
else await addTool();
await wait(300);
}
running = false;
btnPlay.disabled = false;
};
const reset = () => {
timers.forEach((t) => clearTimeout(t));
timers = [];
running = false;
blocks = [];
shellHeight = 0;
showAll = false;
toggling = false;
inner.innerHTML = '';
moreBlock.classList.remove('visible');
moreBlock.style.height = '0px';
shell.style.height = '0px';
btnPlay.disabled = false;
moreDesc.textContent = '无折叠';
};
moreBlock.addEventListener('click', async () => {
if (!blocks.length || toggling) return;
const hiddenCount = Math.max(0, blocks.length - VISIBLE);
if (hiddenCount === 0 && !showAll) return; // 无折叠内容
toggling = true;
const targetShowAll = !showAll;
showAll = targetShowAll;
await render({ mode: targetShowAll ? 'expand' : 'collapse' });
toggling = false;
});
btnPlay.addEventListener('click', () => {
if (running) return;
reset();
running = true;
btnPlay.disabled = true;
runSequence();
});
btnReset.addEventListener('click', reset);
shell.style.height = '0px';
</script>
</body>
</html>