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>
410 lines
23 KiB
HTML
410 lines
23 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>虚拟显示器 - 直接提取网页演示</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; }
|
||
.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:22px; }
|
||
.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(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; }
|
||
.traffic-dot { width:10px; height:10px; border-radius:50%; }
|
||
.traffic-dot.red { background:#ff5f56; } .traffic-dot.yellow { background:#febe2e; } .traffic-dot.green { background:#27c93f; }
|
||
|
||
.browser-window { width:460px; height:280px; }
|
||
.browser-body { padding:16px; display:flex; flex-direction:column; gap:14px; }
|
||
.search-bar { border:1px solid rgba(47,129,247,0.4); border-radius:14px; padding:9px 18px; background:#fff; min-height:42px; display:flex; align-items:center; font-size:14px; color:#0f172a; }
|
||
.browser-status { font-size:13px; color:var(--text-muted); }
|
||
|
||
.extraction-window { width:460px; height:300px; }
|
||
.extraction-window .extraction-body { padding:18px; display:flex; flex-direction:column; gap:12px; color:#0f255c; }
|
||
.extract-title { margin:0; font-size:16px; color:#0d1e44; }
|
||
.extract-url { font-size:13px; color:#4a608b; }
|
||
.extract-status { font-size:13px; color:#1f6feb; display:flex; gap:6px; align-items:center; }
|
||
.extract-status.complete { color:#1fb87a; }
|
||
.extract-status::before { content:""; width:8px; height:8px; border-radius:50%; background:currentColor; box-shadow:0 0 8px currentColor; }
|
||
.extract-summary { font-size:13px; line-height:1.6; color:#1c2952; min-height:120px; overflow:hidden; }
|
||
|
||
.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); }
|
||
.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; }
|
||
@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 · Web Extraction Only</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 browser-window" id="browserWindow">
|
||
<div class="window-header">
|
||
<span class="traffic-dot red"></span>
|
||
<span class="traffic-dot yellow"></span>
|
||
<span class="traffic-dot green"></span>
|
||
<span>多模态浏览器</span>
|
||
</div>
|
||
<div class="browser-body">
|
||
<div class="search-bar"><span id="searchText"></span></div>
|
||
<div class="browser-status" id="browserStatus">等待输入网址...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="window extraction-window" id="extractionWindow">
|
||
<div class="window-header">
|
||
<span class="traffic-dot red"></span>
|
||
<span class="traffic-dot yellow"></span>
|
||
<span class="traffic-dot green"></span>
|
||
<span>深度提取</span>
|
||
</div>
|
||
<div class="extraction-body">
|
||
<h4 class="extract-title" id="extractionTitle">-</h4>
|
||
<div class="extract-url" id="extractionUrl"></div>
|
||
<div class="extract-status" id="extractionState">等待提取</div>
|
||
<div class="extract-summary" id="extractionSummary"></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 browserWindow = document.getElementById('browserWindow');
|
||
const searchText = document.getElementById('searchText');
|
||
const browserStatus = document.getElementById('browserStatus');
|
||
const extractionWindow = document.getElementById('extractionWindow');
|
||
const extractionTitle = document.getElementById('extractionTitle');
|
||
const extractionUrl = document.getElementById('extractionUrl');
|
||
const extractionState = document.getElementById('extractionState');
|
||
const extractionSummary = document.getElementById('extractionSummary');
|
||
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([
|
||
[browserWindow, { x:0.08, y:0.08 }],
|
||
[extractionWindow, { x:0.1, y:0.52 }]
|
||
]);
|
||
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 { offsetWidth:w, offsetHeight:h } = el;
|
||
if(!w||!h) return;
|
||
const availW = Math.max(0, monitorScreen.clientWidth - WINDOW_PADDING*2 - w);
|
||
const availH = Math.max(0, monitorScreen.clientHeight - WINDOW_PADDING*2 - h);
|
||
const baseLeft = WINDOW_PADDING + availW * anchor.x;
|
||
const baseTop = WINDOW_PADDING + availH * anchor.y;
|
||
const left = Math.min(Math.max(WINDOW_PADDING, baseLeft), monitorScreen.clientWidth - w - WINDOW_PADDING);
|
||
const top = Math.min(Math.max(WINDOW_PADDING, baseTop), monitorScreen.clientHeight - h - WINDOW_PADDING);
|
||
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 updateStatus(label){ statusPill.textContent = label || '待机'; }
|
||
|
||
function createIcon(name, iconPath, extra=''){
|
||
const div = document.createElement('div');
|
||
div.className = `desktop-icon ${extra}`.trim();
|
||
div.innerHTML = `<img src="${iconPath}" alt="${name}" /><span>${name}</span>`;
|
||
return div;
|
||
}
|
||
|
||
function populateDesktop(){
|
||
appsGrid.innerHTML = '';
|
||
desktopGrid.innerHTML = '';
|
||
desktopApps.forEach(app=>{
|
||
const icon = createIcon(app.name, app.icon, 'app');
|
||
icon.id = app.id;
|
||
appsGrid.appendChild(icon);
|
||
});
|
||
rootFolders.forEach(folder=>{
|
||
const icon = createIcon(folder, '../icons/folder.svg');
|
||
icon.dataset.folder = folder;
|
||
desktopGrid.appendChild(icon);
|
||
});
|
||
}
|
||
|
||
populateDesktop();
|
||
layoutFloatingWindows();
|
||
|
||
const helpers = {
|
||
sleep: (ms)=>new Promise(res=>setTimeout(res, ms)),
|
||
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 }={}){
|
||
updateStatus('正在输出');
|
||
helpers.dismissBubble(true);
|
||
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;
|
||
const tip = helpers.getPointerTip();
|
||
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);
|
||
},
|
||
triggerClick(){
|
||
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.triggerClick();
|
||
await helpers.sleep(interval);
|
||
}
|
||
},
|
||
clamp(x,y,w,h,padding=12){
|
||
const maxX = monitorScreen.clientWidth - w - padding;
|
||
const maxY = monitorScreen.clientHeight - h - padding;
|
||
return { x: Math.min(Math.max(padding,x), Math.max(padding,maxX)), y: Math.min(Math.max(padding,y), Math.max(padding,maxY)) };
|
||
},
|
||
showBrowser(){
|
||
browserWindow.classList.add('visible');
|
||
positionFloatingWindow(browserWindow, windowAnchors.get(browserWindow));
|
||
},
|
||
async typeUrl(text){
|
||
searchText.textContent = '';
|
||
for(const char of text){
|
||
searchText.textContent += char;
|
||
await helpers.sleep(70);
|
||
}
|
||
},
|
||
showExtraction({ title, url, lines }){
|
||
extractionTitle.textContent = title;
|
||
extractionUrl.textContent = url;
|
||
extractionSummary.innerHTML = '';
|
||
extractionState.textContent = '正在提取...';
|
||
extractionState.classList.remove('complete');
|
||
extractionWindow.classList.add('visible');
|
||
positionFloatingWindow(extractionWindow, windowAnchors.get(extractionWindow));
|
||
lines.forEach((line,index)=>{
|
||
const p = document.createElement('p');
|
||
p.textContent = `• ${line}`;
|
||
extractionSummary.appendChild(p);
|
||
p.style.opacity = 0;
|
||
p.style.transform = 'translateY(6px)';
|
||
setTimeout(()=>{
|
||
p.style.transition = 'all 0.3s ease';
|
||
p.style.opacity = 1;
|
||
p.style.transform = 'translateY(0)';
|
||
}, 120*index);
|
||
});
|
||
setTimeout(()=>{
|
||
extractionState.textContent = '提取完成';
|
||
extractionState.classList.add('complete');
|
||
}, 800);
|
||
},
|
||
resetScene(){
|
||
browserWindow.classList.remove('visible');
|
||
extractionWindow.classList.remove('visible');
|
||
searchText.textContent='';
|
||
browserStatus.textContent='等待输入网址...';
|
||
extractionTitle.textContent='-';
|
||
extractionUrl.textContent='';
|
||
extractionState.textContent='等待提取';
|
||
extractionState.classList.remove('complete');
|
||
extractionSummary.innerHTML='';
|
||
pointerBase = { x:60, y:120 };
|
||
mousePointer.style.transform='translate3d(60px,120px,0)';
|
||
helpers.dismissBubble(true);
|
||
updateStatus('待机');
|
||
layoutFloatingWindows();
|
||
}
|
||
};
|
||
|
||
async function runStory(){
|
||
replayBtn.disabled = true;
|
||
helpers.resetScene();
|
||
updateStatus('正在准备');
|
||
await helpers.sleep(600);
|
||
await helpers.showBubble('无需搜索,我直接访问官方链接并提取内容。', { duration: 2200 });
|
||
|
||
const browserIcon = document.getElementById('browserApp');
|
||
await helpers.moveMouseTo(browserIcon);
|
||
await helpers.click({ count:2 });
|
||
helpers.showBrowser();
|
||
updateStatus('正在规划');
|
||
await helpers.sleep(600);
|
||
|
||
const searchBar = document.querySelector('.search-bar');
|
||
await helpers.moveMouseTo(searchBar, { offsetX: -40 });
|
||
await helpers.click({ count:1 });
|
||
await helpers.typeUrl('https://agentkit.dev/docs/latest');
|
||
browserStatus.textContent = '正在访问网页...';
|
||
updateStatus('正在提取');
|
||
await helpers.sleep(900);
|
||
|
||
await helpers.showBubble('正在提取网页结构化摘要...', { variant:'thinking', duration: 1600 });
|
||
helpers.showExtraction({
|
||
title: 'AgentKit 文档概览',
|
||
url: 'https://agentkit.dev/docs/latest',
|
||
lines: [
|
||
'模块分层:Core / Modules / Utils / Web UI。',
|
||
'工具脚本统一走 `/modules` 目录,入口保持干净。',
|
||
'提供 CLI + Web SocketIO 双前端。',
|
||
'记得在 PR 中附上日志片段与 UI 截图。'
|
||
]
|
||
});
|
||
await helpers.sleep(1800);
|
||
|
||
await helpers.showBubble('提取完成,内容已回传给模型。', { duration: 1900 });
|
||
updateStatus('已完成');
|
||
replayBtn.disabled = false;
|
||
}
|
||
|
||
runStory();
|
||
replayBtn.addEventListener('click', runStory);
|
||
</script>
|
||
</body>
|
||
</html>
|