agent-Specialization/static/demo/memory-tools-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

369 lines
22 KiB
HTML
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.

<!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 {
--monitor-screen: #dff0ff;
--desktop-bg: linear-gradient(160deg, #d4e9ff 0%, #c8dbff 45%, #ebf6ff 100%);
--text-primary: #0f172a;
--accent: #d9814f;
--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; }
.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; }
.apps-grid, .desktop-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(255,255,255,0.96); 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:#ffe1c7; padding:10px 16px; display:flex; align-items:center; gap:8px; font-size:13px; letter-spacing:0.04em; color:#53291b; justify-content:space-between; }
.traffic-dot { width:10px; height:10px; border-radius:50%; }
.traffic-dot.red { background:#ff5f56; } .traffic-dot.yellow { background:#febe2e; } .traffic-dot.green { background:#27c93f; }
.memory-window { width: 500px; height: 320px; border: 1px solid rgba(189, 93, 58, 0.3); background: #fff9f2; }
.memory-body { padding: 18px 22px 10px; height: calc(100% - 96px); overflow: hidden; }
.memory-list { display: flex; flex-direction: column; gap: 12px; height: 100%; overflow-y: auto; }
.memory-item { border-radius: 14px; padding: 12px 14px; background: #fff; border: 1px solid rgba(189, 93, 58, 0.2); box-shadow: 0 8px 20px rgba(83,41,27,0.12); display: flex; gap: 12px; align-items: flex-start; opacity: 0; transform: translateY(8px); transition: all 0.3s ease; }
.memory-item.visible { opacity: 1; transform: translateY(0); }
.memory-item.updated { border-color: rgba(47,129,247,0.6); box-shadow: 0 10px 24px rgba(47,129,247,0.2); }
.memory-item.new { border-color: rgba(32, 201, 151, 0.7); box-shadow: 0 10px 24px rgba(32,201,151,0.25); }
.memory-tag { font-size: 12px; letter-spacing: 0.1em; color: #a35a2d; min-width: 70px; text-transform: uppercase; }
.memory-text { font-size: 13px; color: #3a2a1f; line-height: 1.5; }
.memory-footer { padding: 0 22px 18px; display: flex; justify-content: space-between; font-size: 12px; color: #7a4a37; }
.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) * -0.5); 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 · Memory Notebook</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 memory-window" id="memoryWindow">
<div class="window-header">
<div>
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span>记忆记录</span>
</div>
<span id="memoryStatus">同步待命</span>
</div>
<div class="memory-body">
<div class="memory-list" id="memoryList"></div>
</div>
<div class="memory-footer">
<span>总计 <strong id="memoryCount">0</strong></span>
<span>最后更新 <strong id="memoryTime">--:--</strong></span>
</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 memoryWindow = document.getElementById('memoryWindow');
const memoryList = document.getElementById('memoryList');
const memoryStatus = document.getElementById('memoryStatus');
const memoryCount = document.getElementById('memoryCount');
const memoryTime = document.getElementById('memoryTime');
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([[memoryWindow, { x: 0.2, y: 0.2 }]]);
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 offsetWithinBubble = clamp(tip.x - left, BUBBLE_ARROW_GUTTER, bubbleWidth - BUBBLE_ARROW_GUTTER);
speechBubble.style.setProperty('--arrow-offset', `${offsetWithinBubble}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;
el.style.left = `${Math.min(Math.max(WINDOW_PADDING, baseLeft), Math.max(WINDOW_PADDING, maxLeft))}px`;
el.style.top = `${Math.min(Math.max(WINDOW_PADDING, baseTop), Math.max(WINDOW_PADDING, maxTop))}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', 'error');
if (!speechBubble.classList.contains('visible')) {
if (immediate) speechBubble.style.visibility = '';
return;
}
speechBubble.classList.remove('visible');
speechBubble.style.visibility = '';
},
async showBubble(text, { duration = 1800 } = {}) {
helpers.setStatus('正在输出');
helpers.dismissBubble(true);
const tip = helpers.getPointerTip();
speechBubble.style.visibility = 'hidden';
speechBubble.textContent = text;
speechBubble.classList.add('visible');
await helpers.sleep(16);
const w = speechBubble.offsetWidth;
const h = speechBubble.offsetHeight;
placeSpeechBubble(tip, w, h);
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))
};
},
showMemoryWindow() {
memoryWindow.classList.add('visible');
positionFloatingWindow(memoryWindow, windowAnchors.get(memoryWindow));
},
addMemoryItem({ id, tag, text, state = 'normal' }, delay = 0) {
const item = document.createElement('div');
item.className = `memory-item ${state !== 'normal' ? state : ''}`.trim();
item.dataset.memoryId = id;
item.innerHTML = `<div class="memory-tag">${tag}</div><div class="memory-text">${text}</div>`;
memoryList.appendChild(item);
requestAnimationFrame(() => {
setTimeout(() => item.classList.add('visible'), delay);
});
helpers.updateMemoryStats();
return item;
},
updateMemoryItem(id, newText) {
const item = memoryList.querySelector(`[data-memory-id="${id}"]`);
if (!item) return;
const textEl = item.querySelector('.memory-text');
if (textEl) textEl.textContent = newText;
item.classList.add('updated');
setTimeout(() => item.classList.remove('updated'), 1200);
},
updateMemoryStats() {
memoryCount.textContent = memoryList.querySelectorAll('.memory-item').length;
},
setMemoryStatus(text, timeLabel) {
memoryStatus.textContent = text;
if (timeLabel) {
memoryTime.textContent = timeLabel;
}
},
resetScene() {
memoryWindow.classList.remove('visible');
memoryList.innerHTML = '';
helpers.setMemoryStatus('同步待命', '--:--');
helpers.updateMemoryStats();
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('打开记忆工具,查看已存的上下文锚点。', { duration: 2100 });
const memoryIcon = document.getElementById('memoryApp');
await helpers.moveMouseTo(memoryIcon);
await helpers.click({ count: 2 });
helpers.showMemoryWindow();
helpers.setStatus('记忆载入');
helpers.addMemoryItem({ id: 'pref', tag: '偏好', text: '回复语言保持中文语气' }, 0);
helpers.addMemoryItem({ id: 'path', tag: '路径', text: '工作目录 /Users/jojo/Desktop/agents' }, 120);
helpers.updateMemoryStats();
helpers.setMemoryStatus('同步完成', '17:40');
await helpers.sleep(1400);
helpers.setStatus('正在规划');
await helpers.showBubble('update_memory刷新偏好描述突出“虚拟显示器”需求。', { duration: 2200 });
await helpers.moveMouseTo(memoryList.firstElementChild, { duration: 600 });
await helpers.click({ count: 1 });
helpers.updateMemoryItem('pref', '用户要求:全程以中文交流,视觉风格参照虚拟显示器。');
helpers.setMemoryStatus('记忆已更新', '17:41');
await helpers.sleep(1600);
helpers.setStatus('正在规划');
await helpers.showBubble('update_memory新增任务型记忆。', { duration: 2000 });
await helpers.moveMouseTo(memoryWindow, { duration: 600, offsetX: -120, offsetY: 60 });
await helpers.click({ count: 1 });
helpers.addMemoryItem({ id: 'task', tag: '任务', text: '实现“虚拟电脑显示器”展示逻辑', state: 'new' }, 0);
helpers.setMemoryStatus('新增记忆 · 已同步', '17:42');
await helpers.sleep(1400);
helpers.setStatus('正在输出');
await helpers.showBubble('记忆更新完成,可以继续后续步骤。', { duration: 1800 });
helpers.setStatus('已完成');
replayBtn.disabled = false;
}
runStory();
replayBtn.addEventListener('click', runStory);
</script>
</body>
</html>