agent-Specialization/static/demo/minimal-tool-roller-demo.html

444 lines
11 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>极简模式 · 并行工具转轮动画 Demo</title>
<style>
:root {
color-scheme: light;
--surface-base: #faf9f5;
--surface-card: #f0ece3;
--surface-muted: #e7e1d5;
--surface-raised: #fffdf8;
--text-primary: #25231f;
--text-secondary: #6c665d;
--text-muted: #91887a;
--border-subtle: #ddd5c8;
--accent: #b9654f;
--state-running: #8b7567;
--state-done: #47735e;
--shadow-panel: 0 18px 52px rgba(53, 45, 35, .14);
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background:
linear-gradient(180deg, var(--surface-base), var(--surface-muted));
color: var(--text-primary);
font-family: var(--sans);
}
.stage {
width: min(760px, calc(100vw - 40px));
padding: 34px;
border: 1px solid var(--border-subtle);
border-radius: 24px;
background: var(--surface-raised);
box-shadow: var(--shadow-panel);
}
.eyebrow {
margin: 0 0 10px;
color: var(--text-muted);
font-size: 12px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
}
h1 {
margin: 0 0 12px;
font-size: 24px;
line-height: 1.25;
letter-spacing: -.03em;
}
.desc {
max-width: 620px;
margin: 0 0 28px;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.75;
}
.minimal-line {
display: grid;
grid-template-columns: 10px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
height: 46px;
padding: 0 14px;
border: 1px solid var(--border-subtle);
border-radius: 16px;
background: var(--surface-card);
overflow: hidden;
}
.dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--state-running);
transform-origin: center;
animation: breathe 1.25s ease-in-out infinite;
}
.dot.done {
background: var(--state-done);
animation: none;
}
@keyframes breathe {
0%, 100% { transform: scale(.72); opacity: .55; }
50% { transform: scale(1); opacity: 1; }
}
.summary {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
font-size: 14px;
white-space: nowrap;
}
.intent {
flex: 0 0 auto;
color: var(--text-secondary);
}
.tool-window {
position: relative;
flex: 1 1 auto;
min-width: 0;
height: 26px;
overflow: hidden;
font-family: var(--mono);
color: var(--text-primary);
}
.tool-window::before,
.tool-window::after {
content: "";
position: absolute;
left: 0;
right: 0;
z-index: 2;
height: 7px;
pointer-events: none;
}
.tool-window::before {
top: 0;
background: linear-gradient(180deg, var(--surface-card), transparent);
}
.tool-window::after {
bottom: 0;
background: linear-gradient(0deg, var(--surface-card), transparent);
}
.tool-item,
.typing-text,
.done-text {
height: 26px;
display: flex;
align-items: center;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.typing-text,
.done-text {
position: absolute;
inset: 0;
}
.typing-text::after {
content: "";
width: 1px;
height: 15px;
margin-left: 2px;
background: var(--accent);
animation: caret .75s steps(1) infinite;
}
@keyframes caret {
0%, 45% { opacity: 1; }
46%, 100% { opacity: 0; }
}
.tool-reel {
position: absolute;
left: 0;
right: 0;
top: 0;
will-change: transform;
transform: translateY(0);
}
.tool-reel.rolling {
transition: transform 520ms cubic-bezier(.22, .9, .25, 1);
}
.tool-reel.settle {
transition: transform 170ms cubic-bezier(.2, .72, .26, 1);
}
.badge {
flex: 0 0 auto;
height: 24px;
display: inline-flex;
align-items: center;
padding: 0 9px;
border-radius: 999px;
background: var(--surface-muted);
color: var(--text-secondary);
font-size: 12px;
font-weight: 650;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
button {
height: 36px;
padding: 0 14px;
border: 1px solid var(--border-subtle);
border-radius: 12px;
background: var(--surface-card);
color: var(--text-primary);
font: 650 13px/1 var(--sans);
cursor: pointer;
}
button.primary {
border-color: var(--accent);
background: var(--accent);
color: var(--surface-raised);
}
button:disabled {
cursor: not-allowed;
opacity: .55;
}
.notes {
margin: 18px 0 0;
padding-left: 18px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.8;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}
</style>
</head>
<body>
<main class="stage">
<p class="eyebrow">Minimal Mode Motion Test</p>
<h1>并行工具调用摘要行 · 转轮动画</h1>
<p class="desc">
演示逻辑:并行批次开始后,第一个工具先按打字机显示;随后摘要行在本批次工具之间做纵向转轮循环,直到批次结束后停在最终状态。
</p>
<section class="minimal-line" aria-label="极简模式摘要行">
<span id="dot" class="dot" aria-hidden="true"></span>
<div class="summary">
<span id="intent" class="intent">正在并行调用</span>
<span id="toolWindow" class="tool-window" aria-live="polite"></span>
</div>
<span id="badge" class="badge">准备中</span>
</section>
<div class="controls">
<button id="startBtn" class="primary" type="button">播放并行调用</button>
<button id="finishBtn" type="button" disabled>结束批次</button>
<button id="resetBtn" type="button">重置</button>
</div>
<ul class="notes">
<li>工具列表是一整列,窗口只露出当前命中的一项;所有工具同时向上滚动。</li>
<li>每项停留约 1.45 秒;落位时只轻微多冲一点,再回弹,模拟克制的“刹不住车”顿挫。</li>
<li>第一个工具保留打字机式出现,避免开头突兀。</li>
</ul>
</main>
<script>
const tools = [
"search_query · 检索相关资料",
"read_file · 读取项目片段",
"run_command · 执行最小验证",
"browser_preview · 检查本地界面"
];
const toolWindow = document.getElementById("toolWindow");
const badge = document.getElementById("badge");
const dot = document.getElementById("dot");
const startBtn = document.getElementById("startBtn");
const finishBtn = document.getElementById("finishBtn");
const resetBtn = document.getElementById("resetBtn");
let typingTimer = null;
let wheelTimer = null;
let settleTimer = null;
let index = 0;
let running = false;
const itemHeight = 26;
function clearTimers() {
window.clearInterval(typingTimer);
window.clearInterval(wheelTimer);
window.clearTimeout(settleTimer);
typingTimer = null;
wheelTimer = null;
settleTimer = null;
}
function clearToolWindow() {
toolWindow.replaceChildren();
}
function setTextNode(text, className) {
clearToolWindow();
const node = document.createElement("span");
node.className = className;
node.textContent = text;
toolWindow.appendChild(node);
return node;
}
function buildReel() {
clearToolWindow();
const reel = document.createElement("div");
reel.id = "toolReel";
reel.className = "tool-reel";
[...tools, tools[0]].forEach((tool) => {
const item = document.createElement("div");
item.className = "tool-item";
item.textContent = tool;
reel.appendChild(item);
});
toolWindow.appendChild(reel);
index = 0;
reel.style.transform = "translateY(0)";
return reel;
}
function typeFirstTool(done) {
const text = tools[0];
let cursor = 0;
const typingNode = setTextNode("", "typing-text");
badge.textContent = "1 / " + tools.length;
typingTimer = window.setInterval(() => {
cursor += 1;
typingNode.textContent = text.slice(0, cursor);
if (cursor >= text.length) {
window.clearInterval(typingTimer);
typingTimer = null;
window.setTimeout(done, 520);
}
}, 34);
}
function spinToNext() {
const reel = document.getElementById("toolReel");
if (!reel) return;
index = (index + 1) % tools.length;
badge.textContent = `${index + 1} / ${tools.length}`;
const visualIndex = index === 0 ? tools.length : index;
const target = -visualIndex * itemHeight;
const overshoot = target - 2;
reel.className = "tool-reel rolling";
reel.style.transform = `translateY(${overshoot}px)`;
settleTimer = window.setTimeout(() => {
reel.className = "tool-reel settle";
reel.style.transform = `translateY(${target}px)`;
if (index === 0) {
settleTimer = window.setTimeout(() => {
reel.className = "tool-reel";
reel.style.transform = "translateY(0)";
}, 175);
}
}, 520);
}
function startWheel() {
buildReel();
wheelTimer = window.setInterval(spinToNext, 1450);
}
function startDemo() {
if (running) return;
clearTimers();
running = true;
index = 0;
dot.classList.remove("done");
startBtn.disabled = true;
finishBtn.disabled = false;
badge.textContent = "启动中";
typeFirstTool(startWheel);
}
function finishDemo() {
if (!running) return;
clearTimers();
running = false;
setTextNode("并行工具调用完成", "done-text");
badge.textContent = "完成";
dot.classList.add("done");
startBtn.disabled = false;
finishBtn.disabled = true;
}
function resetDemo() {
clearTimers();
running = false;
index = 0;
setTextNode("等待并行工具批次", "done-text");
badge.textContent = "准备中";
dot.classList.remove("done");
startBtn.disabled = false;
finishBtn.disabled = true;
}
startBtn.addEventListener("click", startDemo);
finishBtn.addEventListener("click", finishDemo);
resetBtn.addEventListener("click", resetDemo);
resetDemo();
</script>
</body>
</html>