/* 运行模式动画 —— 可复用模块(主页 feature 行 / anim-demo 页共用)
* AnimModes.init(container):在容器内建 DOM(模式 tab + 消息区 + 输入栏 + 假鼠标),
* 点击 tab 切换并从头播放,再次点击重播。
*/
window.AnimModes = (function () {
'use strict';
function init(container) {
container.innerHTML =
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
';
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;
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
/** 可中断 sleep:醒来后发现已切换模式则抛错终止序列 */
async function step(ms, myGen) {
await sleep(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); });
});
// 首次进入默认播放「计划」
play('plan');
}
return { init: init };
})();