agent-Specialization/website-design/exp/site-assets/anim-modes.js
JOJO 39a7a5fc6f chore(website): 归档官网文档内容与设计素材
- website-content/:官网 10 篇文档内容 + README
- website-design/:官网设计稿、动画实验、预览图与资源
- 根目录官网截图两张
- .gitignore 忽略 website-design/exp/static/dist 构建产物(7.1MB)
2026-09-02 14:38:51 +08:00

322 lines
11 KiB
JavaScript
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.

/* 运行模式动画 —— 可复用模块(主页 feature 行 / anim-demo 页共用)
* AnimModes.init(container):在容器内建 DOM模式 tab + 消息区 + 输入栏 + 假鼠标),
* 点击 tab 切换并从头播放,再次点击重播。
*/
window.AnimModes = (function () {
'use strict';
function init(container) {
container.innerHTML =
'<div class="anim-window" id="animWindow">' +
'<div class="anim-topbar">' +
'<button class="anim-mode-tab" data-mode="plan">计划</button>' +
'<button class="anim-mode-tab" data-mode="ask">询问</button>' +
'<button class="anim-mode-tab" data-mode="execute">执行</button>' +
'</div>' +
'<div class="anim-chat"></div>' +
'<div class="anim-composer"><span class="anim-composer-text"></span><span class="anim-composer-caret"></span></div>' +
'<div class="anim-cursor" aria-hidden="true">' +
'<svg viewBox="0 0 24 24" width="18" height="18"><path d="M5 3 L19 12 L12.5 12.8 L15.5 20 L13 21 L10 13.5 L5 17 Z" fill="#ffffff" stroke="#1a1f2e" stroke-width="1.4" stroke-linejoin="round"/></svg>' +
'</div>' +
'</div>';
var chat = container.querySelector('.anim-chat');
var composer = container.querySelector('.anim-composer');
var composerText = composer.querySelector('.anim-composer-text');
var cursor = container.querySelector('.anim-cursor');
var tabs = Array.prototype.slice.call(container.querySelectorAll('.anim-mode-tab'));
var animWindow = container.querySelector('.anim-window');
/* ── 代次令牌:切换模式时作废旧序列的所有后续步骤 ── */
var gen = 0;
/* rAF 累计「可见时间」的等待:后台标签页 rAF 暂停,流程随之冻结,恢复时从冻结点继续——
setTimeout 在后台只是降频不会停,会攒动画导致恢复瞬间全部放完 */
function rafWait(ms) {
return new Promise(function (resolve) {
var acc = 0, last = performance.now();
function tick(now) {
acc += Math.min(now - last, 100); /* 恢复后首帧 dt 巨大,截断防跳变 */
last = now;
if (acc >= ms) { resolve(); return; }
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
}
/** 可中断 sleep醒来后发现已切换模式则抛错终止序列 */
async function step(ms, myGen) {
await rafWait(ms);
if (myGen !== gen) throw new Error('cancelled');
}
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text != null) e.textContent = text;
return e;
}
/* ── 基础构件 ── */
/** 入场元素统一入口:挂载并平滑滚到底部(旧内容自然顶出) */
function push(node) {
chat.appendChild(node);
requestAnimationFrame(function () {
chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' });
});
return node;
}
function addUserMsg(text) {
return push(el('div', 'msg-user anim-item', text));
}
function addThinkRow() {
var row = el('div', 'think-row anim-item');
var dots = el('span', 'think-dots');
dots.appendChild(el('i'));
dots.appendChild(el('i'));
dots.appendChild(el('i'));
row.appendChild(dots);
return push(row);
}
function removeEl(e) {
if (e && e.parentNode) e.parentNode.removeChild(e);
}
/** 往目标元素里逐字打出(带光标),返回 Promise */
async function typeInto(span, text, myGen, cps) {
var caret = el('span', 'type-caret');
span.parentNode.insertBefore(caret, span.nextSibling);
for (var i = 0; i < text.length; i++) {
span.textContent += text[i];
chat.scrollTop = chat.scrollHeight; // 打字时保持底部可见
await step(cps || 34, myGen);
}
removeEl(caret);
}
/** AI 裸文字消息:逐字流式 */
async function typeAiMsg(text, myGen) {
var b = el('div', 'msg-ai anim-item');
var span = el('span');
b.appendChild(span);
push(b);
await typeInto(span, text, myGen, 34);
return b;
}
/** 工具摘要行:状态图标 + intent + 关键参数(不显示工具名) */
async function addToolLine(intent, param, runMs, myGen) {
var b = el('div', 'tool-line is-running anim-item');
b.appendChild(el('span', 'tool-status'));
b.appendChild(el('span', 'tool-intent', intent));
if (param) b.appendChild(el('span', 'tool-param', param));
push(b);
await step(runMs, myGen);
b.classList.remove('is-running');
b.classList.add('is-done');
return b;
}
/* ── 假鼠标:出现 → 移到目标中心 → 点击 → 消失 ── */
function cursorMoveTo(x, y, immediate) {
if (immediate) cursor.style.transition = 'none';
cursor.style.transform = 'translate(' + x + 'px,' + y + 'px)';
if (immediate) {
cursor.getBoundingClientRect(); // 强制 reflow恢复过渡
cursor.style.transition = '';
}
}
async function cursorClick(targetEl, myGen) {
var win = animWindow;
var wr = win.getBoundingClientRect();
var tr = targetEl.getBoundingClientRect();
cursorMoveTo(wr.width - 60, wr.height - 40, true);
cursor.classList.add('is-visible');
await step(260, myGen);
cursorMoveTo(tr.left - wr.left + tr.width / 2 - 4, tr.top - wr.top + tr.height / 2 - 2, false);
await step(760, myGen);
cursor.classList.add('is-clicking');
targetEl.classList.add('is-pressed');
await step(140, myGen);
cursor.classList.remove('is-clicking');
targetEl.classList.remove('is-pressed');
await step(120, myGen);
cursor.classList.remove('is-visible');
}
/* ── 模式一:计划 ── */
async function playPlan(myGen) {
addUserMsg('帮我给这个项目加一个设置页面');
await step(700, myGen);
var think = addThinkRow();
await step(1300, myGen);
removeEl(think);
await addToolLine('规划设计页面', '.astrion/plan/settings-page.md', 900, myGen);
await step(350, myGen);
// 计划:标题与条目全部逐字流式
var body = el('div', 'plan-body anim-item');
push(body);
var title = el('div', 'plan-title');
var titleSpan = el('span');
title.appendChild(titleSpan);
body.appendChild(title);
await typeInto(titleSpan, '设置页面开发计划', myGen, 55);
var items = ['梳理现有个性化设置字段与保存接口', '编写页面布局与表单组件', '接入保存并构建验证'];
for (var i = 0; i < items.length; i++) {
var row = el('div', 'plan-item');
row.appendChild(el('b', null, (i + 1) + '.'));
var itemSpan = el('span');
row.appendChild(itemSpan);
body.appendChild(row);
await typeInto(itemSpan, items[i], myGen, 26);
await step(120, myGen);
}
var actions = el('div', 'plan-actions anim-item');
var approve = el('button', 'plan-btn is-primary', '批准计划');
actions.appendChild(approve);
body.appendChild(actions);
chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' });
await step(700, myGen);
// 鼠标点击批准
await cursorClick(approve, myGen);
approve.textContent = '已批准 ✓';
approve.classList.remove('is-primary');
approve.classList.add('is-approved');
await step(700, myGen);
// 按钮淡出收起,释放垂直空间
actions.classList.add('is-gone');
await step(380, myGen);
removeEl(actions);
// 批准后开始实施
await typeAiMsg('好的,开始实施。', myGen);
await step(250, myGen);
await addToolLine('编写设置页面组件', 'SettingsPanel.tsx', 1000, myGen);
await step(280, myGen);
await addToolLine('构建验证', 'npm run build', 1000, myGen);
await step(320, myGen);
await typeAiMsg('设置页面已完成,构建通过。', myGen);
}
/* ── 模式二:询问 ── */
async function playAsk(myGen) {
addUserMsg('帮我优化一下这个页面的性能');
await step(700, myGen);
var think = addThinkRow();
await step(1100, myGen);
removeEl(think);
await typeAiMsg('好的,我先和您聊聊。这个页面是首次加载慢,还是滚动、交互的时候慢?', myGen);
await step(500, myGen);
// 输入栏出现,用户逐字输入回答
composerText.textContent = '';
composer.classList.add('is-visible');
await step(450, myGen);
var reply = '主要是首次加载慢';
for (var i = 0; i < reply.length; i++) {
composerText.textContent += reply[i];
await step(70, myGen);
}
await step(600, myGen);
// 发送:输入栏淡出,内容变成右侧用户气泡
composer.classList.add('is-sent');
await step(240, myGen);
composer.classList.remove('is-visible', 'is-sent');
composerText.textContent = '';
addUserMsg(reply);
await step(700, myGen);
// 模型开始调查
var think2 = addThinkRow();
await step(1000, myGen);
removeEl(think2);
await typeAiMsg('好的,我来调查一下。', myGen);
await step(250, myGen);
await addToolLine('分析构建产物体积', 'npm run build', 1200, myGen);
await step(300, myGen);
await typeAiMsg('找到了:图表库被全量引入,首屏多出 2.1MB。', myGen);
}
/* ── 模式三:执行 ── */
async function playExecute(myGen) {
addUserMsg('把页面上的按钮都改成蓝色');
await step(700, myGen);
var think = addThinkRow();
await step(900, myGen);
removeEl(think);
await typeAiMsg('好的,我来改。', myGen);
await step(250, myGen);
await addToolLine('修改按钮颜色', 'css/style.css', 1000, myGen);
await step(300, myGen);
await addToolLine('构建验证', 'npm run build', 1100, myGen);
await step(350, myGen);
await typeAiMsg('已完成,所有按钮已切换为蓝色。', myGen);
}
/* ── 调度 ── */
var PLAYERS = { plan: playPlan, ask: playAsk, execute: playExecute };
function setActiveTab(mode) {
tabs.forEach(function (t) {
t.classList.toggle('is-active', t.dataset.mode === mode);
});
}
function play(mode) {
gen += 1;
var myGen = gen;
chat.innerHTML = '';
chat.scrollTop = 0;
composerText.textContent = '';
composer.classList.remove('is-visible', 'is-sent');
cursor.classList.remove('is-visible', 'is-clicking');
setActiveTab(mode);
PLAYERS[mode](myGen).catch(function (e) {
if (e && e.message !== 'cancelled') throw e;
});
}
tabs.forEach(function (t) {
t.addEventListener('click', function () { play(t.dataset.mode); });
});
// 首次进入默认播放「计划」——进入视口才开始:页面打开时用户可能还没滚到功能区,避免错过开场
var started = false;
var io = new IntersectionObserver(function (entries) {
if (!started && entries[0].isIntersecting) {
started = true;
io.disconnect();
play('plan');
}
}, { threshold: 0.3 });
io.observe(container);
}
return { init: init };
})();