agent-Specialization/static/demo/realtime-terminal-demo.html
JOJO 2f75c1c8bb feat: stable version before virtual monitor timing fix
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>
2025-12-13 17:12:12 +08:00

1035 lines
38 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>虚拟显示器 - 实时终端工具演示</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;
--danger: #ff4d6d;
--success: #1fb87a;
--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;
justify-content: flex-start;
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; }
.terminal-window {
width: 600px;
height: 360px;
background: #050910;
border: 1px solid rgba(47, 129, 247, 0.4);
color: #c9dbff;
}
.terminal-window .window-header {
background: rgba(6, 12, 26, 0.95);
color: #dfe8ff;
justify-content: space-between;
}
.terminal-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
letter-spacing: 0.08em;
}
.terminal-status::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--danger);
box-shadow: 0 0 10px var(--danger);
}
.terminal-window.connected .terminal-status::before {
background: var(--success);
box-shadow: 0 0 10px var(--success);
}
.terminal-body {
display: grid;
grid-template-rows: 1fr auto;
height: calc(100% - 54px);
}
.terminal-screen {
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
font-size: 13px;
background: radial-gradient(circle at top, rgba(21, 33, 62, 0.9), rgba(5, 9, 16, 0.95));
color: #d9e4ff;
padding: 18px 24px;
overflow-y: auto;
overflow-x: hidden;
position: relative;
scroll-behavior: smooth;
}
.terminal-lines {
display: flex;
flex-direction: column;
gap: 4px;
}
.terminal-line {
opacity: 0;
transform: translateY(4px);
transition: all 0.25s ease;
}
.terminal-line.visible {
opacity: 1;
transform: translateY(0);
}
.terminal-footer {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(6, 10, 18, 0.9);
}
.terminal-input {
flex: 1;
border-radius: 999px;
border: 1px solid rgba(47, 129, 247, 0.5);
background: rgba(255, 255, 255, 0.08);
color: #fff;
font-family: inherit;
font-size: 13px;
padding: 8px 16px;
min-height: 34px;
}
.terminal-controls {
display: flex;
gap: 8px;
}
.terminal-btn {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.08);
color: #cbe3ff;
font-size: 12px;
cursor: pointer;
}
.terminal-btn:hover {
background: rgba(47, 129, 247, 0.3);
}
.snapshot-panel {
position: absolute;
right: 32px;
bottom: 34px;
width: 180px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(15, 23, 42, 0.1);
padding: 12px;
box-shadow: 0 14px 24px rgba(15, 23, 42, 0.25);
opacity: 0;
transform: translateY(12px);
transition: all 0.3s ease;
}
.snapshot-panel.visible {
opacity: 1;
transform: translateY(0);
}
.snapshot-preview {
height: 80px;
border-radius: 10px;
background: repeating-linear-gradient(
135deg,
rgba(47, 129, 247, 0.25),
rgba(47, 129, 247, 0.25) 6px,
rgba(47, 129, 247, 0.15) 6px,
rgba(47, 129, 247, 0.15) 12px
);
margin-bottom: 8px;
position: relative;
overflow: hidden;
}
.snapshot-preview::after {
content: 'SNAPSHOT';
position: absolute;
bottom: 6px;
right: 8px;
font-size: 10px;
letter-spacing: 0.2em;
color: rgba(255, 255, 255, 0.9);
}
.terminal-window.flash::after {
content: '';
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.35);
animation: flash 0.6s ease;
pointer-events: none;
}
@keyframes flash {
from { opacity: 1; }
to { opacity: 0; }
}
.terminal-window.sleep::before {
content: '';
position: absolute;
inset: 0;
background: rgba(5, 9, 16, 0.8);
backdrop-filter: blur(2px);
}
.sleep-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #dfe8ff;
font-size: 14px;
letter-spacing: 0.1em;
z-index: 2;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.terminal-window.sleep .sleep-overlay {
opacity: 1;
}
.context-menu {
position: absolute;
background: rgba(9, 16, 32, 0.96);
color: #e6edff;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 6px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 160px;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.55);
opacity: 0;
pointer-events: none;
transform: translateY(-6px);
transition: all 0.2s ease;
z-index: 45;
}
.context-menu.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.context-menu button {
border: none;
background: transparent;
padding: 8px 10px;
border-radius: 8px;
color: inherit;
text-align: left;
cursor: pointer;
font-size: 13px;
}
.context-menu button:hover,
.context-menu button.active {
background: rgba(47, 129, 247, 0.3);
}
.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);
}
.speech-bubble.thinking { background: rgba(91, 96, 115, 0.95); }
.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: 50;
}
.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: 45;
}
@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 · Realtime Terminal</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 terminal-window" id="terminalWindow">
<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>实时终端 Session</span>
</div>
<div class="terminal-status" id="terminalStatus">等待连接</div>
</div>
<div class="terminal-body">
<div class="terminal-screen" id="terminalScreen">
<div class="terminal-lines" id="terminalLines"></div>
</div>
<div class="terminal-footer">
<div class="terminal-input" id="terminalInput">$</div>
<div class="terminal-controls">
<button class="terminal-btn" id="snapshotBtn" type="button">生成快照</button>
<button class="terminal-btn" id="resetBtn" type="button">重置终端</button>
</div>
</div>
</div>
<div class="sleep-overlay" id="sleepOverlay">
<div>SESSION PAUSED</div>
<div id="sleepCountdown">3s</div>
</div>
</div>
<div class="snapshot-panel" id="snapshotPanel">
<div class="snapshot-preview"></div>
<div class="snapshot-meta">实时终端快照 · 22:34:18</div>
</div>
<div class="context-menu" id="terminalMenu">
<button data-action="sleep">暂停会话 (sleep)</button>
</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 terminalWindow = document.getElementById('terminalWindow');
const terminalStatus = document.getElementById('terminalStatus');
const terminalScreen = document.getElementById('terminalScreen');
const terminalLines = document.getElementById('terminalLines');
const terminalInput = document.getElementById('terminalInput');
const snapshotPanel = document.getElementById('snapshotPanel');
const snapshotBtn = document.getElementById('snapshotBtn');
const resetBtn = document.getElementById('resetBtn');
const terminalMenu = document.getElementById('terminalMenu');
const sleepOverlay = document.getElementById('sleepOverlay');
const sleepCountdown = document.getElementById('sleepCountdown');
const replayBtn = document.getElementById('replayBtn');
let screenRect = monitorScreen.getBoundingClientRect();
let pointerBase = { x: 60, y: 120 };
let snapshotTimer = null;
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([[terminalWindow, { x: 0.18, y: 0.08 }]]);
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(element, anchor) {
if (!element || !anchor) return;
const width = element.offsetWidth;
const height = element.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));
element.style.left = `${left}px`;
element.style.top = `${top}px`;
}
function layoutFloatingWindows() {
windowAnchors.forEach((anchor, element) => positionFloatingWindow(element, 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 iconEl = createIconElement(app.name, app.icon, 'app');
iconEl.id = app.id;
appsGrid.appendChild(iconEl);
});
rootFolders.forEach((folder) => {
const iconEl = createIconElement(folder, '../icons/folder.svg');
iconEl.dataset.folder = folder;
desktopGrid.appendChild(iconEl);
});
}
populateDesktop();
layoutFloatingWindows();
const helpers = {
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
setStatus(label) {
updateStatusPill(label);
},
prepareScene() {
snapshotPanel.classList.remove('visible');
if (snapshotTimer) {
clearTimeout(snapshotTimer);
snapshotTimer = null;
}
},
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, { variant = 'info', duration = 1800 } = {}) {
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({ right = false } = {}) {
const tip = helpers.getPointerTip();
const circle = document.createElement('span');
circle.className = 'click-effect';
circle.style.borderColor = right ? 'rgba(255,168,88,0.9)' : 'rgba(47,129,247,0.85)';
circle.style.left = `${tip.x - 9}px`;
circle.style.top = `${tip.y - 9}px`;
monitorScreen.appendChild(circle);
setTimeout(() => circle.remove(), 450);
},
async click({ right = false, count = 1, interval = 130 } = {}) {
helpers.dismissBubble(true);
for (let i = 0; i < count; i += 1) {
helpers.triggerClickEffect({ right });
await helpers.sleep(interval);
}
},
clampToScreen(x, y, width, height, padding = 12) {
const maxX = monitorScreen.clientWidth - width - padding;
const maxY = monitorScreen.clientHeight - height - padding;
const clampedX = Math.min(Math.max(padding, x), Math.max(padding, maxX));
const clampedY = Math.min(Math.max(padding, y), Math.max(padding, maxY));
return { x: clampedX, y: clampedY };
},
getAppIcon(id) {
return document.getElementById(id);
},
async typeTerminal(text) {
terminalInput.textContent = '$ ';
for (const char of text) {
terminalInput.textContent += char;
await helpers.sleep(70);
}
},
async clearTerminalInput() {
let text = terminalInput.textContent || '';
if (!text || text.trim() === '$') {
terminalInput.textContent = '$ ';
return;
}
while (text.length > 2) {
text = text.slice(0, -1);
terminalInput.textContent = text;
await helpers.sleep(30);
}
terminalInput.textContent = '$ ';
},
appendTerminalLines(lines, { reset = false } = {}) {
if (reset) {
terminalLines.innerHTML = '';
}
lines.forEach((line, index) => {
const div = document.createElement('div');
div.className = 'terminal-line';
div.textContent = line;
terminalLines.appendChild(div);
requestAnimationFrame(() => {
setTimeout(() => {
div.classList.add('visible');
helpers.scrollTerminal();
}, index * 200);
});
});
},
scrollTerminal() {
if (!terminalScreen) return;
terminalScreen.scrollTo({ top: terminalScreen.scrollHeight, behavior: 'smooth' });
},
showTerminalWindow() {
terminalWindow.classList.add('visible');
positionFloatingWindow(terminalWindow, windowAnchors.get(terminalWindow));
},
setTerminalStatus(label, connected = false) {
terminalStatus.textContent = label;
if (connected) {
terminalWindow.classList.add('connected');
} else {
terminalWindow.classList.remove('connected');
}
},
showSnapshotPanel() {
snapshotPanel.classList.add('visible');
if (snapshotTimer) {
clearTimeout(snapshotTimer);
snapshotTimer = null;
}
snapshotTimer = setTimeout(() => {
snapshotPanel.classList.remove('visible');
snapshotTimer = null;
}, 2200);
},
flashTerminal() {
terminalWindow.classList.add('flash');
setTimeout(() => terminalWindow.classList.remove('flash'), 500);
},
showContextMenu(menu, position) {
const element = menu === 'terminal' ? terminalMenu : null;
if (!element) return;
element.classList.add('visible');
const { width, height } = element.getBoundingClientRect();
const { x, y } = helpers.clampToScreen(position.x, position.y, width, height, 12);
element.style.left = `${x}px`;
element.style.top = `${y}px`;
},
hideContextMenus() {
terminalMenu.classList.remove('visible');
terminalMenu.querySelectorAll('button').forEach((btn) => btn.classList.remove('active'));
},
async highlightMenuAction(action) {
const btn = terminalMenu.querySelector(`button[data-action="${action}"]`);
if (!btn) return;
btn.classList.add('active');
await helpers.sleep(300);
btn.classList.remove('active');
},
setSleepState(active) {
terminalWindow.classList.toggle('sleep', !!active);
},
async animateSleepCountdown(seconds) {
for (let s = seconds; s >= 0; s -= 1) {
sleepCountdown.textContent = `${s}s`;
await helpers.sleep(400);
}
},
resetScene() {
helpers.hideContextMenus();
terminalWindow.classList.remove('visible', 'connected', 'sleep', 'flash');
terminalLines.innerHTML = '';
terminalInput.textContent = '$';
terminalStatus.textContent = '等待连接';
pointerBase = { x: 60, y: 120 };
mousePointer.style.transform = 'translate3d(60px, 120px, 0)';
helpers.dismissBubble(true);
updateStatusPill('待机');
layoutFloatingWindows();
}
};
async function runStory() {
replayBtn.disabled = true;
helpers.prepareScene();
helpers.resetScene();
helpers.setStatus('正在准备');
await helpers.sleep(600);
helpers.setStatus('正在规划');
await helpers.showBubble('接入实时终端terminal_session 即将建立。', { duration: 2200 });
const terminalIcon = helpers.getAppIcon('terminalApp');
await helpers.moveMouseTo(terminalIcon);
await helpers.click({ count: 2 });
helpers.showTerminalWindow();
helpers.setTerminalStatus('建立连接...', false);
helpers.setStatus('终端接入');
await helpers.sleep(1200);
helpers.setTerminalStatus('session 已连接', true);
helpers.setStatus('正在规划');
await helpers.showBubble('terminal_input逐字敲入命令。', { duration: 2000 });
await helpers.moveMouseTo(terminalInput, { duration: 600, offsetX: 40, offsetY: 0 });
await helpers.click({ count: 1 });
helpers.setStatus('输入命令');
await helpers.typeTerminal('npm run status --watch');
helpers.appendTerminalLines([
'connecting…',
'watching containers · ready',
'> npm run status --watch',
'└─ compiling metrics (42%)',
'└─ compiling metrics (86%)',
'net stats :: ↑2.4MB/s / ↓1.1MB/s'
], { reset: true });
await helpers.sleep(2500);
helpers.setStatus('正在规划');
await helpers.showBubble('terminal_snapshot捕获当前画面。', { duration: 2000 });
await helpers.moveMouseTo(snapshotBtn, { duration: 600 });
await helpers.click({ count: 1 });
helpers.showSnapshotPanel();
helpers.setStatus('已捕获快照');
await helpers.sleep(1200);
helpers.setStatus('正在规划');
await helpers.showBubble('terminal_reset快速清场并恢复初始提示符。', { duration: 2200 });
await helpers.moveMouseTo(resetBtn, { duration: 650 });
await helpers.click({ count: 1 });
helpers.appendTerminalLines(['session reset complete', '$'], { reset: true });
helpers.flashTerminal();
helpers.setStatus('终端已重置');
await helpers.sleep(1500);
helpers.setStatus('正在规划');
await helpers.showBubble('再次输入 tail -f logs/agent.log继续追踪。', { duration: 2300 });
await helpers.moveMouseTo(terminalInput, { duration: 600, offsetX: 40 });
await helpers.clearTerminalInput();
await helpers.typeTerminal('tail -f logs/agent.log');
helpers.appendTerminalLines([
'> tail -f logs/agent.log',
'[2025-12-12 22:34:41] container ready',
'[2025-12-12 22:34:43] streaming tokens…',
'[2025-12-12 22:34:45] heartbeat ok'
]);
await helpers.sleep(2000);
helpers.setStatus('正在规划');
await helpers.showBubble('在终端里调用 sleep短暂休眠 session。', { duration: 2300 });
await helpers.moveMouseTo(terminalWindow, { duration: 700, offsetX: 180, offsetY: -60 });
await helpers.click({ right: true });
const tip = helpers.getPointerTip();
helpers.showContextMenu('terminal', { x: tip.x + 20, y: tip.y + 16 });
await helpers.sleep(240);
await helpers.highlightMenuAction('sleep');
const sleepBtn = terminalMenu.querySelector('button[data-action="sleep"]');
await helpers.moveMouseTo(sleepBtn, { duration: 400 });
await helpers.click({ count: 1 });
helpers.hideContextMenus();
helpers.setSleepState(true);
helpers.setStatus('sleep 中');
await helpers.animateSleepCountdown(3);
helpers.setSleepState(false);
helpers.setStatus('session 唤醒');
await helpers.sleep(800);
helpers.setStatus('正在规划');
await helpers.showBubble('实时终端工具链演示完成。', { duration: 2000 });
helpers.setStatus('已完成');
replayBtn.disabled = false;
}
runStory();
replayBtn.addEventListener('click', runStory);
</script>
</body>
</html>