agent-Specialization/demo/stacked-blocks/script.js

343 lines
10 KiB
JavaScript
Raw Permalink 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.

(() => {
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();
})();