agent-Specialization/static/demo/reading-focus-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

1005 lines
37 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.apps {
flex: 0 0 auto;
}
.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; }
.folder-window {
width: 360px;
height: 260px;
}
.folder-body {
padding: 18px;
display: flex;
flex-wrap: wrap;
gap: 14px;
}
.folder-entry {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: #2b3859;
opacity: 0;
transform: translateY(8px);
transition: all 0.3s ease;
}
.folder-entry.visible {
opacity: 1;
transform: translateY(0);
}
.folder-entry img {
width: 36px;
height: 36px;
filter: drop-shadow(0 4px 8px rgba(15, 23, 42, 0.2));
}
.reading-window {
width: 480px;
height: 320px;
background: #f5f7ff;
border-color: rgba(47, 129, 247, 0.3);
}
.reading-window .window-header {
justify-content: space-between;
}
.reading-body {
padding: 18px 22px;
height: calc(100% - 54px);
overflow: hidden;
position: relative;
}
.reading-body::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), transparent 20%, transparent 80%, rgba(255, 255, 255, 0.9));
pointer-events: none;
}
.reading-lines {
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
font-size: 13px;
line-height: 1.6;
color: #0c1c40;
height: 100%;
overflow: hidden;
}
.reading-line {
opacity: 0;
transform: translateX(-16px);
transition: all 0.3s ease;
}
.reading-line.visible {
opacity: 1;
transform: translateX(0);
}
.reading-window.focused {
box-shadow: 0 0 0 3px rgba(47, 129, 247, 0.35), 0 18px 40px rgba(15, 23, 42, 0.3);
}
.ocr-window {
width: 460px;
height: 300px;
}
.ocr-body {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 18px;
padding: 18px;
height: calc(100% - 54px);
}
.ocr-image {
position: relative;
border-radius: 16px;
background: linear-gradient(135deg, #fefefe, #d9e9ff);
border: 1px solid rgba(15, 23, 42, 0.08);
overflow: hidden;
}
.ocr-image::after {
content: "流程图";
position: absolute;
bottom: 10px;
right: 12px;
font-size: 12px;
color: rgba(15, 23, 42, 0.6);
}
.scan-line {
position: absolute;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(180deg, rgba(47, 129, 247, 0.05), rgba(47, 129, 247, 0.25), rgba(47, 129, 247, 0.05));
animation: scanMove 3s linear infinite;
}
@keyframes scanMove {
0% { top: -40px; }
100% { top: calc(100% + 40px); }
}
.ocr-text {
border-radius: 14px;
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.08);
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
color: #1b2d66;
overflow: hidden;
}
.ocr-line {
opacity: 0;
transform: translateY(8px);
transition: all 0.3s ease;
}
.ocr-line.visible {
opacity: 1;
transform: translateY(0);
}
.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: 150px;
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: 35;
}
.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: 30;
--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); }
.speech-bubble.error { background: rgba(255, 77, 109, 0.92); }
.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;
}
.click-effect.right {
border-color: rgba(255, 168, 88, 0.9);
}
@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 · Reading Suite</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 folder-window" id="folderWindow">
<div class="window-header">
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span id="folderTitle">doc</span>
</div>
<div class="folder-body" id="folderBody"></div>
</div>
<div class="window reading-window" id="readingWindow">
<div class="window-header">
<div class="header-title">
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span id="readingTitle">summary.md</span>
</div>
</div>
<div class="reading-body">
<div class="reading-lines" id="readingLines"></div>
</div>
</div>
<div class="window ocr-window" id="ocrWindow">
<div class="window-header">
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span id="ocrTitle">diagram.png</span>
</div>
<div class="ocr-body">
<div class="ocr-image">
<div class="scan-line"></div>
</div>
<div class="ocr-text" id="ocrText"></div>
</div>
</div>
<div class="context-menu" id="focusMenu">
<button data-action="focus">聚焦文件</button>
<button data-action="unfocus">取消聚焦</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 folderWindow = document.getElementById('folderWindow');
const folderTitle = document.getElementById('folderTitle');
const folderBody = document.getElementById('folderBody');
const readingWindow = document.getElementById('readingWindow');
const readingTitle = document.getElementById('readingTitle');
const readingLines = document.getElementById('readingLines');
const focusMenu = document.getElementById('focusMenu');
const ocrWindow = document.getElementById('ocrWindow');
const ocrTitle = document.getElementById('ocrTitle');
const ocrText = document.getElementById('ocrText');
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;
let statusLabel = '待机';
const windowAnchors = new Map([
[folderWindow, { x: 0.68, y: 0.12 }],
[readingWindow, { x: 0.08, y: 0.08 }],
[ocrWindow, { x: 0.1, y: 0.55 }]
]);
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`;
element.style.right = 'auto';
}
function layoutFloatingWindows() {
windowAnchors.forEach((anchor, element) => positionFloatingWindow(element, anchor));
}
window.addEventListener('resize', () => {
screenRect = monitorScreen.getBoundingClientRect();
layoutFloatingWindows();
});
function updateStatusPill(label) {
statusLabel = label || '待机';
statusPill.textContent = statusLabel;
}
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);
},
getPointerTip() {
return {
x: pointerBase.x + POINTER_TIP_OFFSET.x,
y: pointerBase.y + POINTER_TIP_OFFSET.y
};
},
dismissBubble(immediate = false) {
speechBubble.classList.remove('thinking', 'error');
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);
const tip = helpers.getPointerTip();
speechBubble.style.visibility = 'hidden';
speechBubble.classList.remove('thinking', 'error');
if (variant === 'thinking') speechBubble.classList.add('thinking');
if (variant === 'error') speechBubble.classList.add('error');
speechBubble.textContent = text;
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 ${right ? 'right' : ''}`.trim();
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 };
},
showFocusMenu() {
if (!focusMenu) return;
focusMenu.classList.add('visible');
const tip = helpers.getPointerTip();
const { offsetWidth: w, offsetHeight: h } = focusMenu;
const { x, y } = helpers.clampToScreen(tip.x + 16, tip.y + 16, w, h, 12);
focusMenu.style.left = `${x}px`;
focusMenu.style.top = `${y}px`;
},
hideFocusMenu() {
if (!focusMenu) return;
focusMenu.classList.remove('visible');
focusMenu.querySelectorAll('button').forEach((btn) => btn.classList.remove('active'));
},
async highlightFocusAction(action) {
if (!focusMenu) return;
const btn = focusMenu.querySelector(`button[data-action="${action}"]`);
if (!btn) return;
btn.classList.add('active');
await helpers.sleep(300);
btn.classList.remove('active');
},
getFolderIcon(name) {
return Array.from(desktopGrid.children).find((el) => el.dataset.folder === name) || null;
},
async moveToIcon(iconEl, options) {
if (!iconEl) return;
const img = iconEl.querySelector('img');
if (img) {
return helpers.moveMouseTo(img, options);
}
return helpers.moveMouseTo(iconEl, options);
},
showFolderWindow({ title, entries }) {
folderTitle.textContent = title || '文件夹';
folderBody.innerHTML = '';
entries.forEach((entry, index) => {
const icon = createIconElement(entry.name, entry.icon || '../icons/file.svg', 'folder-entry');
icon.id = entry.id;
icon.dataset.entryType = entry.type || 'file';
folderBody.appendChild(icon);
requestAnimationFrame(() => {
setTimeout(() => icon.classList.add('visible'), index * 90);
});
});
folderWindow.classList.add('visible');
positionFloatingWindow(folderWindow, windowAnchors.get(folderWindow));
},
hideFolderWindow() {
folderWindow.classList.remove('visible');
folderBody.innerHTML = '';
},
showReadingWindow({ title, lines }) {
readingTitle.textContent = title || '文件';
readingLines.innerHTML = '';
readingWindow.classList.add('visible');
positionFloatingWindow(readingWindow, windowAnchors.get(readingWindow));
lines.forEach((line, index) => {
const div = document.createElement('div');
div.className = 'reading-line';
div.textContent = line;
readingLines.appendChild(div);
requestAnimationFrame(() => {
setTimeout(() => div.classList.add('visible'), index * 160);
});
});
},
hideReadingWindow() {
readingWindow.classList.remove('visible');
readingWindow.classList.remove('focused');
readingLines.innerHTML = '';
},
setFocusState(active) {
readingWindow.classList.toggle('focused', !!active);
},
showOcrWindow({ title, lines }) {
ocrTitle.textContent = title || '图像';
ocrText.innerHTML = '';
ocrWindow.classList.add('visible');
positionFloatingWindow(ocrWindow, windowAnchors.get(ocrWindow));
lines.forEach((line, index) => {
const div = document.createElement('div');
div.className = 'ocr-line';
div.textContent = line;
ocrText.appendChild(div);
requestAnimationFrame(() => {
setTimeout(() => div.classList.add('visible'), index * 220);
});
});
},
hideOcrWindow() {
ocrWindow.classList.remove('visible');
ocrText.innerHTML = '';
},
resetScene() {
helpers.hideFolderWindow();
helpers.hideReadingWindow();
helpers.hideOcrWindow();
helpers.hideFocusMenu();
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('我来阅读 doc 目录下的 summary.md并保持聚焦。', { duration: 2200 });
const docIcon = helpers.getFolderIcon('doc');
await helpers.moveToIcon(docIcon);
await helpers.click({ count: 2 });
helpers.showFolderWindow({
title: 'doc',
entries: [
{ id: 'summaryFile', name: 'summary.md', type: 'file' },
{ id: 'diagramFile', name: 'diagram.png', type: 'image' }
]
});
helpers.setStatus('正在浏览');
await helpers.sleep(800);
await helpers.showBubble('双击 summary.md开始阅读。', { duration: 1800 });
const summaryFile = document.getElementById('summaryFile');
await helpers.moveToIcon(summaryFile);
await helpers.click({ count: 2 });
helpers.setStatus('正在阅读');
helpers.showReadingWindow({
title: 'summary.md',
lines: [
'# 研究摘要:多阶段聚合',
'1. 采集:多源搜索获取候选。',
'2. 过滤:思考链确保可信度。',
'3. 聚焦:重要段落进入焦点模式。',
'4. 摘要:按需求输出工作笔记。',
'5. 校验:事实对齐并记录引用。'
]
});
await helpers.sleep(2200);
helpers.setStatus('正在规划');
await helpers.showBubble('右键文件选择“聚焦”,锁定当前阅读。', { duration: 2000 });
await helpers.moveToIcon(summaryFile);
await helpers.click({ right: true });
helpers.showFocusMenu();
await helpers.sleep(220);
const focusBtn = focusMenu.querySelector('button[data-action="focus"]');
await helpers.moveMouseTo(focusBtn, { duration: 420 });
await helpers.highlightFocusAction('focus');
await helpers.click({ count: 1 });
helpers.hideFocusMenu();
helpers.setFocusState(true);
helpers.setStatus('聚焦中');
await helpers.sleep(1400);
helpers.setStatus('正在规划');
await helpers.showBubble('解除聚焦,恢复普通阅读。', { duration: 1900 });
await helpers.moveToIcon(summaryFile);
await helpers.click({ right: true });
helpers.showFocusMenu();
await helpers.sleep(220);
const unfocusBtn = focusMenu.querySelector('button[data-action="unfocus"]');
await helpers.moveMouseTo(unfocusBtn, { duration: 420 });
await helpers.highlightFocusAction('unfocus');
await helpers.click({ count: 1 });
helpers.hideFocusMenu();
helpers.setFocusState(false);
helpers.setStatus('正在阅读');
await helpers.sleep(1000);
helpers.setStatus('正在规划');
await helpers.showBubble('接下来识别 diagram.png 中的图像信息。', { duration: 2100 });
const diagramFile = document.getElementById('diagramFile');
await helpers.moveToIcon(diagramFile);
await helpers.click({ count: 2 });
helpers.setStatus('正在识别');
helpers.showOcrWindow({
title: 'diagram.png',
lines: [
'• OCRAdaptive Agent Pipeline',
'• 节点 A输入解析 / 意图检测',
'• 节点 B检索+提取联合流程',
'• 节点 C写作代理含验证',
'• 状态Focus=OFF记忆可回溯'
]
});
await helpers.sleep(2600);
helpers.setStatus('正在规划');
await helpers.showBubble('阅读与识别都完成了,准备收尾。', { duration: 2000 });
helpers.setStatus('已完成');
replayBtn.disabled = false;
}
runStory();
replayBtn.addEventListener('click', runStory);
</script>
</body>
</html>