Current status includes: - Virtual monitor surface and components - Monitor store for state management - Tool call animations and transitions - Liquid glass shader integration Known issue to fix: Tool status display timing - "正在xx" appears after tool execution completes instead of when tool call starts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
661 lines
25 KiB
HTML
661 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>虚拟显示器 - 单次终端指令(run_command)演示</title>
|
||
<style>
|
||
:root {
|
||
--panel: #0b0d14;
|
||
--monitor-frame: #151726;
|
||
--monitor-screen: #dff0ff;
|
||
--desktop-bg: linear-gradient(160deg, #d4e9ff 0%, #c8dbff 45%, #ebf6ff 100%);
|
||
--text-primary: #0f172a;
|
||
--text-muted: #465066;
|
||
--accent: #2f81f7;
|
||
--shadow-deep: 0 30px 80px rgba(0, 0, 0, 0.55);
|
||
font-family: "IBM Plex Sans", "PingFang SC", "Microsoft YaHei", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
background: radial-gradient(circle at top, rgba(38, 56, 120, 0.7), rgba(9, 12, 20, 0.95));
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 48px 24px;
|
||
}
|
||
|
||
.monitor-stage {
|
||
width: min(1180px, 100%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.monitor {
|
||
width: 100%;
|
||
aspect-ratio: 16 / 9;
|
||
max-height: 720px;
|
||
background: rgba(10, 11, 19, 0.8);
|
||
border-radius: 32px;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
padding: 28px;
|
||
box-shadow: var(--shadow-deep);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
}
|
||
|
||
.monitor-shell {
|
||
flex: 1;
|
||
border-radius: 24px;
|
||
background: linear-gradient(150deg, #101223, #1a1d33);
|
||
padding: 26px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
}
|
||
|
||
.monitor-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
color: #c7d5ff;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.status-pill {
|
||
padding: 8px 20px;
|
||
border-radius: 18px;
|
||
border: 1px solid rgba(79, 136, 255, 0.5);
|
||
background: rgba(64, 158, 255, 0.18);
|
||
color: #dff3ff;
|
||
font-size: 13px;
|
||
letter-spacing: 0.06em;
|
||
min-width: 260px;
|
||
text-align: right;
|
||
box-shadow: inset 0 0 12px rgba(64, 158, 255, 0.15);
|
||
}
|
||
|
||
.monitor-screen {
|
||
flex: 1;
|
||
border-radius: 20px;
|
||
background: var(--monitor-screen);
|
||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.45), inset 0 60px 120px rgba(255, 255, 255, 0.35);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.desktop-layer {
|
||
position: absolute;
|
||
inset: 0;
|
||
padding: 30px;
|
||
background: var(--desktop-bg);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 26px;
|
||
}
|
||
|
||
.desktop-section {
|
||
background: rgba(255, 255, 255, 0.38);
|
||
border-radius: 18px;
|
||
padding: 16px;
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.desktop-section.files { flex: 1 1 auto; overflow: auto; }
|
||
|
||
.desktop-grid,
|
||
.apps-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||
gap: 18px;
|
||
align-content: start;
|
||
}
|
||
|
||
.desktop-icon {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
text-align: center;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.desktop-icon img {
|
||
width: 42px;
|
||
height: 42px;
|
||
filter: drop-shadow(0 4px 8px rgba(15, 23, 42, 0.25));
|
||
}
|
||
|
||
.desktop-icon span { font-size: 13px; color: #102a57; word-break: break-all; }
|
||
.desktop-icon.app span { font-weight: 600; color: #0c1c3f; }
|
||
|
||
.window {
|
||
position: absolute;
|
||
border-radius: 16px;
|
||
background: rgba(244, 246, 251, 0.98);
|
||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.25);
|
||
overflow: hidden;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.4s ease;
|
||
max-width: calc(100% - 36px);
|
||
max-height: calc(100% - 36px);
|
||
}
|
||
|
||
.window.visible { opacity: 1; }
|
||
|
||
.window-header {
|
||
background: #e0e7ff;
|
||
padding: 10px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
letter-spacing: 0.04em;
|
||
color: #1d2351;
|
||
}
|
||
|
||
.window-header .traffic-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.traffic-dot.red { background: #ff5f56; }
|
||
.traffic-dot.yellow { background: #febe2e; }
|
||
.traffic-dot.green { background: #27c93f; }
|
||
|
||
.command-window {
|
||
width: 460px;
|
||
height: 300px;
|
||
background: #050910;
|
||
border: 1px solid rgba(47, 129, 247, 0.45);
|
||
color: #d9e4ff;
|
||
}
|
||
|
||
.command-window .window-header {
|
||
background: rgba(6, 12, 26, 0.96);
|
||
color: #dfe7ff;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.command-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100% - 54px);
|
||
padding: 18px 20px;
|
||
gap: 16px;
|
||
}
|
||
|
||
.command-input {
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(47, 129, 247, 0.6);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
padding: 12px 16px;
|
||
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
|
||
font-size: 13px;
|
||
color: #f4f8ff;
|
||
min-height: 54px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.command-output {
|
||
flex: 1;
|
||
border-radius: 14px;
|
||
background: rgba(8, 15, 28, 0.9);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
padding: 14px 18px;
|
||
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
|
||
font-size: 13px;
|
||
color: #d0e1ff;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.output-line {
|
||
opacity: 0;
|
||
transform: translateY(6px);
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.output-line.visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.command-footer {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.execute-btn {
|
||
border: none;
|
||
border-radius: 999px;
|
||
padding: 8px 18px;
|
||
font-size: 13px;
|
||
letter-spacing: 0.08em;
|
||
background: linear-gradient(120deg, #2f81f7, #67d5ff);
|
||
color: #fff;
|
||
cursor: pointer;
|
||
box-shadow: 0 10px 20px rgba(47, 129, 247, 0.35);
|
||
}
|
||
|
||
.speech-bubble {
|
||
position: absolute;
|
||
min-width: 160px;
|
||
max-width: 320px;
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
background: rgba(13, 24, 45, 0.92);
|
||
color: #f8fbff;
|
||
font-size: 13px;
|
||
line-height: 1.45;
|
||
box-shadow: 0 14px 28px rgba(13, 24, 45, 0.45);
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
transform-origin: bottom left;
|
||
pointer-events: none;
|
||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||
z-index: 40;
|
||
--arrow-offset: 50%;
|
||
--arrow-size: 18px;
|
||
}
|
||
|
||
.speech-bubble::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: var(--arrow-size);
|
||
height: var(--arrow-size);
|
||
bottom: calc(var(--arrow-size) / -2);
|
||
left: var(--arrow-offset);
|
||
transform: translateX(-50%) rotate(45deg);
|
||
background: inherit;
|
||
}
|
||
|
||
.speech-bubble.visible { opacity: 1; transform: scale(1); }
|
||
|
||
.mouse-pointer {
|
||
position: absolute;
|
||
width: 36px;
|
||
height: 36px;
|
||
pointer-events: none;
|
||
transform: translate3d(60px, 120px, 0);
|
||
transition: transform var(--mouse-duration, 0.8s) cubic-bezier(0.4, 0, 0.2, 1);
|
||
z-index: 40;
|
||
}
|
||
|
||
.mouse-pointer img {
|
||
width: 100%;
|
||
height: 100%;
|
||
filter: drop-shadow(0 6px 12px rgba(15, 23, 42, 0.4));
|
||
}
|
||
|
||
.click-effect {
|
||
position: absolute;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
border: 2px solid rgba(47, 129, 247, 0.85);
|
||
pointer-events: none;
|
||
animation: clickPulse 0.45s ease;
|
||
z-index: 35;
|
||
}
|
||
|
||
@keyframes clickPulse {
|
||
0% { opacity: 1; transform: scale(0.6); }
|
||
100% { opacity: 0; transform: scale(1.8); }
|
||
}
|
||
|
||
.monitor-stand {
|
||
width: 180px;
|
||
height: 12px;
|
||
border-radius: 999px;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
margin: 14px auto 0;
|
||
}
|
||
|
||
.control-bar button {
|
||
border: none;
|
||
border-radius: 999px;
|
||
padding: 10px 26px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
color: #f6f8ff;
|
||
font-size: 15px;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.control-bar button:hover { background: rgba(255, 255, 255, 0.22); }
|
||
.control-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
|
||
@media (max-width: 960px) {
|
||
body { padding: 12px; }
|
||
.monitor { padding: 18px; }
|
||
.monitor-shell { padding: 18px; }
|
||
.desktop-layer { padding: 20px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="monitor-stage">
|
||
<div class="monitor">
|
||
<div class="monitor-shell">
|
||
<div class="monitor-top">
|
||
<div>Agent Display Surface · run_command</div>
|
||
<div class="status-pill" id="statusPill">待机</div>
|
||
</div>
|
||
<div class="monitor-screen" id="monitorScreen">
|
||
<div class="desktop-layer">
|
||
<div class="desktop-section apps">
|
||
<div class="apps-grid" id="appsGrid"></div>
|
||
</div>
|
||
<div class="desktop-section files">
|
||
<div class="desktop-grid" id="desktopGrid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="window command-window" id="commandWindow">
|
||
<div class="window-header">
|
||
<div class="header-group">
|
||
<span class="traffic-dot red"></span>
|
||
<span class="traffic-dot yellow"></span>
|
||
<span class="traffic-dot green"></span>
|
||
<span>单次命令执行</span>
|
||
</div>
|
||
<div id="commandStatus">未执行</div>
|
||
</div>
|
||
<div class="command-body">
|
||
<div class="command-input" id="commandInput">$</div>
|
||
<div class="command-output" id="commandOutput"></div>
|
||
<div class="command-footer">
|
||
<button class="execute-btn" id="executeBtn" type="button">执行命令</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="speech-bubble" id="speechBubble"></div>
|
||
<div class="mouse-pointer" id="mousePointer">
|
||
<img src="../icons/mouse-pointer-2.svg" alt="pointer" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="monitor-stand"></div>
|
||
</div>
|
||
<div class="control-bar">
|
||
<button id="replayBtn">重新播放流程</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const rootFolders = [
|
||
'__pycache__','config','core','data','doc','docker','logs','modules','node_modules','project','prompts','scratch_test','scripts','static','sub_agent','test','users','utils'
|
||
];
|
||
|
||
const desktopApps = [
|
||
{ id: 'browserApp', name: '浏览器', icon: '../icons/globe.svg' },
|
||
{ id: 'terminalApp', name: '终端', icon: '../icons/terminal.svg' },
|
||
{ id: 'pythonApp', name: 'Python', icon: '../icons/python.svg' },
|
||
{ id: 'memoryApp', name: '记忆', icon: '../icons/sticky-note.svg' },
|
||
{ id: 'todoApp', name: '待办', icon: '../icons/clipboard.svg' }
|
||
];
|
||
|
||
const appsGrid = document.getElementById('appsGrid');
|
||
const desktopGrid = document.getElementById('desktopGrid');
|
||
const speechBubble = document.getElementById('speechBubble');
|
||
const mousePointer = document.getElementById('mousePointer');
|
||
const monitorScreen = document.getElementById('monitorScreen');
|
||
const statusPill = document.getElementById('statusPill');
|
||
const commandWindow = document.getElementById('commandWindow');
|
||
const commandStatus = document.getElementById('commandStatus');
|
||
const commandInput = document.getElementById('commandInput');
|
||
const commandOutput = document.getElementById('commandOutput');
|
||
const executeBtn = document.getElementById('executeBtn');
|
||
const replayBtn = document.getElementById('replayBtn');
|
||
|
||
let screenRect = monitorScreen.getBoundingClientRect();
|
||
let pointerBase = { x: 60, y: 120 };
|
||
const POINTER_TIP_OFFSET = { x: 8, y: 6 };
|
||
const BUBBLE_SCREEN_PADDING = 12;
|
||
const BUBBLE_VERTICAL_GAP = 26;
|
||
const BUBBLE_ARROW_GUTTER = 24;
|
||
const windowAnchors = new Map([[commandWindow, { x: 0.25, y: 0.18 }]]);
|
||
const WINDOW_PADDING = 18;
|
||
|
||
function clamp(value, min, max) {
|
||
return Math.min(Math.max(value, min), max);
|
||
}
|
||
|
||
function placeSpeechBubble(tip, bubbleWidth, bubbleHeight) {
|
||
const containerWidth = monitorScreen.clientWidth;
|
||
const containerHeight = monitorScreen.clientHeight;
|
||
const horizontalMax = Math.max(BUBBLE_SCREEN_PADDING, containerWidth - bubbleWidth - BUBBLE_SCREEN_PADDING);
|
||
const verticalMax = Math.max(BUBBLE_SCREEN_PADDING, containerHeight - bubbleHeight - BUBBLE_SCREEN_PADDING);
|
||
let left = tip.x - bubbleWidth / 2;
|
||
let top = tip.y - bubbleHeight - BUBBLE_VERTICAL_GAP;
|
||
left = clamp(left, BUBBLE_SCREEN_PADDING, horizontalMax);
|
||
top = clamp(top, BUBBLE_SCREEN_PADDING, verticalMax);
|
||
speechBubble.style.left = `${left}px`;
|
||
speechBubble.style.top = `${top}px`;
|
||
const pointerOffset = clamp(tip.x - left, BUBBLE_ARROW_GUTTER, bubbleWidth - BUBBLE_ARROW_GUTTER);
|
||
speechBubble.style.setProperty('--arrow-offset', `${pointerOffset}px`);
|
||
}
|
||
|
||
function positionFloatingWindow(el, anchor) {
|
||
if (!el || !anchor) return;
|
||
const width = el.offsetWidth;
|
||
const height = el.offsetHeight;
|
||
if (!width || !height) return;
|
||
const availableWidth = Math.max(0, monitorScreen.clientWidth - WINDOW_PADDING * 2 - width);
|
||
const availableHeight = Math.max(0, monitorScreen.clientHeight - WINDOW_PADDING * 2 - height);
|
||
const baseLeft = WINDOW_PADDING + availableWidth * anchor.x;
|
||
const baseTop = WINDOW_PADDING + availableHeight * anchor.y;
|
||
const maxLeft = monitorScreen.clientWidth - width - WINDOW_PADDING;
|
||
const maxTop = monitorScreen.clientHeight - height - WINDOW_PADDING;
|
||
const left = Math.min(Math.max(WINDOW_PADDING, baseLeft), Math.max(WINDOW_PADDING, maxLeft));
|
||
const top = Math.min(Math.max(WINDOW_PADDING, baseTop), Math.max(WINDOW_PADDING, maxTop));
|
||
el.style.left = `${left}px`;
|
||
el.style.top = `${top}px`;
|
||
}
|
||
|
||
function layoutFloatingWindows() {
|
||
windowAnchors.forEach((anchor, el) => positionFloatingWindow(el, anchor));
|
||
}
|
||
|
||
window.addEventListener('resize', () => {
|
||
screenRect = monitorScreen.getBoundingClientRect();
|
||
layoutFloatingWindows();
|
||
});
|
||
|
||
function updateStatusPill(label) { statusPill.textContent = label || '待机'; }
|
||
|
||
function createIconElement(name, iconPath, extraClass = '') {
|
||
const div = document.createElement('div');
|
||
div.className = `desktop-icon ${extraClass}`.trim();
|
||
const img = document.createElement('img');
|
||
img.src = iconPath; img.alt = name;
|
||
div.appendChild(img);
|
||
const span = document.createElement('span'); span.textContent = name; div.appendChild(span);
|
||
return div;
|
||
}
|
||
|
||
function populateDesktop() {
|
||
appsGrid.innerHTML = '';
|
||
desktopGrid.innerHTML = '';
|
||
desktopApps.forEach((app) => {
|
||
const icon = createIconElement(app.name, app.icon, 'app');
|
||
icon.id = app.id;
|
||
appsGrid.appendChild(icon);
|
||
});
|
||
rootFolders.forEach((folder) => {
|
||
const icon = createIconElement(folder, '../icons/folder.svg');
|
||
icon.dataset.folder = folder;
|
||
desktopGrid.appendChild(icon);
|
||
});
|
||
}
|
||
|
||
populateDesktop();
|
||
layoutFloatingWindows();
|
||
|
||
const helpers = {
|
||
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
|
||
setStatus(label) { updateStatusPill(label); },
|
||
getPointerTip() { return { x: pointerBase.x + POINTER_TIP_OFFSET.x, y: pointerBase.y + POINTER_TIP_OFFSET.y }; },
|
||
dismissBubble(immediate = false) {
|
||
speechBubble.classList.remove('thinking');
|
||
if (!speechBubble.classList.contains('visible')) {
|
||
if (immediate) speechBubble.style.visibility = '';
|
||
return;
|
||
}
|
||
speechBubble.classList.remove('visible');
|
||
speechBubble.style.visibility = '';
|
||
},
|
||
async showBubble(text, { duration = 1800, variant } = {}) {
|
||
helpers.setStatus('正在输出');
|
||
helpers.dismissBubble(true);
|
||
speechBubble.style.visibility = 'hidden';
|
||
speechBubble.classList.remove('thinking');
|
||
if (variant === 'thinking') speechBubble.classList.add('thinking');
|
||
speechBubble.textContent = text;
|
||
const tip = helpers.getPointerTip();
|
||
speechBubble.classList.add('visible');
|
||
await helpers.sleep(16);
|
||
const bubbleWidth = speechBubble.offsetWidth;
|
||
const bubbleHeight = speechBubble.offsetHeight;
|
||
placeSpeechBubble(tip, bubbleWidth, bubbleHeight);
|
||
speechBubble.style.visibility = 'visible';
|
||
await helpers.sleep(duration);
|
||
helpers.dismissBubble();
|
||
},
|
||
async moveMouseTo(target, { offsetX = 0, offsetY = 0, duration = 900 } = {}) {
|
||
helpers.dismissBubble(true);
|
||
let el = target;
|
||
if (typeof target === 'string') el = document.querySelector(target);
|
||
if (!el) return;
|
||
const rect = el.getBoundingClientRect();
|
||
const desiredX = rect.left - screenRect.left + rect.width / 2 + offsetX;
|
||
const desiredY = rect.top - screenRect.top + rect.height / 2 + offsetY;
|
||
const pointerX = desiredX - POINTER_TIP_OFFSET.x;
|
||
const pointerY = desiredY - POINTER_TIP_OFFSET.y;
|
||
mousePointer.style.setProperty('--mouse-duration', `${duration}ms`);
|
||
mousePointer.style.transform = `translate3d(${pointerX}px, ${pointerY}px, 0)`;
|
||
pointerBase = { x: pointerX, y: pointerY };
|
||
return helpers.sleep(duration + 80);
|
||
},
|
||
triggerClickEffect() {
|
||
const tip = helpers.getPointerTip();
|
||
const circle = document.createElement('span');
|
||
circle.className = 'click-effect';
|
||
circle.style.left = `${tip.x - 9}px`;
|
||
circle.style.top = `${tip.y - 9}px`;
|
||
monitorScreen.appendChild(circle);
|
||
setTimeout(() => circle.remove(), 450);
|
||
},
|
||
async click({ count = 1, interval = 130 } = {}) {
|
||
helpers.dismissBubble(true);
|
||
for (let i = 0; i < count; i += 1) {
|
||
helpers.triggerClickEffect();
|
||
await helpers.sleep(interval);
|
||
}
|
||
},
|
||
clampToScreen(x, y, width, height, padding = 12) {
|
||
const maxX = monitorScreen.clientWidth - width - padding;
|
||
const maxY = monitorScreen.clientHeight - height - padding;
|
||
return {
|
||
x: Math.min(Math.max(padding, x), Math.max(padding, maxX)),
|
||
y: Math.min(Math.max(padding, y), Math.max(padding, maxY))
|
||
};
|
||
},
|
||
getAppIcon(id) { return document.getElementById(id); },
|
||
showCommandWindow() {
|
||
commandWindow.classList.add('visible');
|
||
positionFloatingWindow(commandWindow, windowAnchors.get(commandWindow));
|
||
},
|
||
async typeCommand(text) {
|
||
commandInput.textContent = '$ ';
|
||
for (const char of text) {
|
||
commandInput.textContent += char;
|
||
await helpers.sleep(60);
|
||
}
|
||
},
|
||
showOutput(lines) {
|
||
commandOutput.innerHTML = '';
|
||
lines.forEach((line, index) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'output-line';
|
||
div.textContent = line;
|
||
commandOutput.appendChild(div);
|
||
requestAnimationFrame(() => {
|
||
setTimeout(() => div.classList.add('visible'), index * 200);
|
||
});
|
||
});
|
||
},
|
||
resetScene() {
|
||
commandWindow.classList.remove('visible');
|
||
commandStatus.textContent = '未执行';
|
||
commandInput.textContent = '$';
|
||
commandOutput.innerHTML = '';
|
||
pointerBase = { x: 60, y: 120 };
|
||
mousePointer.style.transform = 'translate3d(60px, 120px, 0)';
|
||
helpers.dismissBubble(true);
|
||
updateStatusPill('待机');
|
||
layoutFloatingWindows();
|
||
}
|
||
};
|
||
|
||
async function runStory() {
|
||
replayBtn.disabled = true;
|
||
helpers.resetScene();
|
||
helpers.setStatus('正在准备');
|
||
await helpers.sleep(600);
|
||
helpers.setStatus('正在规划');
|
||
await helpers.showBubble('使用 run_command 执行一次性 shell 指令。', { duration: 2200 });
|
||
|
||
const terminalIcon = helpers.getAppIcon('terminalApp');
|
||
await helpers.moveMouseTo(terminalIcon);
|
||
await helpers.click({ count: 2 });
|
||
helpers.showCommandWindow();
|
||
helpers.setStatus('命令准备');
|
||
await helpers.sleep(800);
|
||
|
||
await helpers.showBubble('输入命令:pnpm exec lint --filter agent-ui。', { duration: 2100 });
|
||
await helpers.moveMouseTo(commandInput, { duration: 600, offsetX: 50 });
|
||
await helpers.click({ count: 1 });
|
||
helpers.setStatus('正在输入');
|
||
await helpers.typeCommand('pnpm exec lint --filter agent-ui');
|
||
await helpers.sleep(600);
|
||
|
||
helpers.setStatus('正在执行');
|
||
await helpers.moveMouseTo(executeBtn, { duration: 600 });
|
||
await helpers.click({ count: 1 });
|
||
commandStatus.textContent = '执行中…';
|
||
helpers.showOutput([
|
||
'> pnpm exec lint --filter agent-ui',
|
||
'ELIFECYCLE 命令执行成功',
|
||
'files checked: 142 · warnings: 0',
|
||
'✨ All clean in 8.2s'
|
||
]);
|
||
await helpers.sleep(2200);
|
||
|
||
helpers.setStatus('正在输出');
|
||
await helpers.showBubble('run_command 完成,已返回标准输出。', { duration: 1900 });
|
||
commandStatus.textContent = '执行完成';
|
||
helpers.setStatus('已完成');
|
||
replayBtn.disabled = false;
|
||
}
|
||
|
||
runStory();
|
||
replayBtn.addEventListener('click', runStory);
|
||
</script>
|
||
</body>
|
||
</html>
|