444 lines
11 KiB
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>
|