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>
1211 lines
46 KiB
HTML
1211 lines
46 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;
|
||
--danger: #ff4d6d;
|
||
--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: 34px;
|
||
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;
|
||
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; }
|
||
|
||
.browser-window {
|
||
width: 440px;
|
||
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);
|
||
}
|
||
|
||
.results-list {
|
||
background: rgba(255, 255, 255, 0.82);
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
max-height: 140px;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
.results-list ul {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.results-list li {
|
||
padding: 8px 10px;
|
||
border-radius: 10px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
font-size: 13px;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.results-list li strong {
|
||
display: block;
|
||
color: #0f255c;
|
||
}
|
||
|
||
.results-list li.highlight {
|
||
border: 1px solid rgba(47, 129, 247, 0.65);
|
||
box-shadow: 0 0 0 2px rgba(47, 129, 247, 0.15);
|
||
}
|
||
|
||
.results-scroll ul.scrolling {
|
||
animation: scrollList 8s linear infinite;
|
||
}
|
||
|
||
@keyframes scrollList {
|
||
0% { transform: translateY(0); }
|
||
100% { transform: translateY(-50%); }
|
||
}
|
||
|
||
.extraction-window {
|
||
width: 440px;
|
||
height: 280px;
|
||
}
|
||
|
||
.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: 100px;
|
||
}
|
||
|
||
.folder-window {
|
||
width: 360px;
|
||
height: 240px;
|
||
}
|
||
|
||
.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));
|
||
}
|
||
|
||
.editor-window {
|
||
width: 420px;
|
||
height: 280px;
|
||
background: #0a1428;
|
||
border-color: rgba(64, 158, 255, 0.35);
|
||
color: #d1e3ff;
|
||
}
|
||
|
||
.editor-window .window-header {
|
||
background: rgba(6, 12, 28, 0.9);
|
||
color: #c7dfff;
|
||
}
|
||
|
||
.editor-body {
|
||
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
|
||
padding: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.code-line {
|
||
opacity: 0;
|
||
transform: translateX(-16px);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.code-line.visible {
|
||
opacity: 1;
|
||
transform: translateX(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: 20;
|
||
}
|
||
|
||
.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); }
|
||
|
||
.speech-bubble .bubble-icon {
|
||
display: inline-flex;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.mobile-hide { display: none; }
|
||
|
||
.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) {
|
||
.monitor {
|
||
padding: 18px;
|
||
}
|
||
|
||
.monitor-shell { padding: 18px; }
|
||
|
||
.desktop-layer { padding: 22px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="monitor-stage">
|
||
<div class="monitor">
|
||
<div class="monitor-shell">
|
||
<div class="monitor-top">
|
||
<div>Agent Display Surface</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 class="results-list results-scroll">
|
||
<ul id="resultsList"></ul>
|
||
</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="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>research_notes</span>
|
||
</div>
|
||
<div class="folder-body" id="folderBody"></div>
|
||
</div>
|
||
|
||
<div class="window editor-window" id="editorWindow">
|
||
<div class="window-header">
|
||
<span class="traffic-dot red"></span>
|
||
<span class="traffic-dot yellow"></span>
|
||
<span class="traffic-dot green"></span>
|
||
<span>search_summary.md</span>
|
||
</div>
|
||
<div class="editor-body" id="editorBody"></div>
|
||
</div>
|
||
|
||
<div class="context-menu" id="desktopMenu">
|
||
<button data-action="file">新建文件</button>
|
||
<button data-action="folder">新建文件夹</button>
|
||
</div>
|
||
<div class="context-menu" id="folderMenu">
|
||
<button data-action="file">新建文件</button>
|
||
</div>
|
||
<div class="context-menu" id="browserMenu">
|
||
<button data-action="save">保存网页</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 extraApps = [
|
||
{ id: "browserApp", name: "浏览器", icon: "../icons/globe.svg" },
|
||
{ id: "terminalApp", name: "终端", icon: "../icons/terminal.svg" },
|
||
{ id: "noteApp", name: "记忆", icon: "../icons/sticky-note.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 browserStatus = document.getElementById("browserStatus");
|
||
const searchText = document.getElementById("searchText");
|
||
const resultsContainer = document.querySelector(".results-list");
|
||
const resultsList = document.getElementById("resultsList");
|
||
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 folderWindow = document.getElementById("folderWindow");
|
||
const folderBody = document.getElementById("folderBody");
|
||
const editorWindow = document.getElementById("editorWindow");
|
||
const editorBody = document.getElementById("editorBody");
|
||
const desktopMenu = document.getElementById("desktopMenu");
|
||
const folderMenu = document.getElementById("folderMenu");
|
||
const browserMenu = document.getElementById("browserMenu");
|
||
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([
|
||
[browserWindow, { x: 0.05, y: 0.05 }],
|
||
[extractionWindow, { x: 0.05, y: 0.55 }],
|
||
[folderWindow, { x: 0.74, y: 0.12 }],
|
||
[editorWindow, { x: 0.72, y: 0.5 }]
|
||
]);
|
||
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 = "";
|
||
extraApps.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, icon = "" } = {}) {
|
||
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.innerHTML = icon ? `<span class="bubble-icon">${icon}</span>${text}` : 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);
|
||
}
|
||
},
|
||
showBrowserWindow() {
|
||
browserWindow.classList.add("visible");
|
||
positionFloatingWindow(browserWindow, windowAnchors.get(browserWindow));
|
||
},
|
||
hideBrowserWindow() {
|
||
browserWindow.classList.remove("visible");
|
||
},
|
||
async typeSearch(text) {
|
||
searchText.textContent = "";
|
||
for (const char of text) {
|
||
searchText.textContent += char;
|
||
await helpers.sleep(80);
|
||
}
|
||
},
|
||
populateResults(results) {
|
||
resultsList.innerHTML = "";
|
||
results.forEach((item) => {
|
||
const li = document.createElement("li");
|
||
const title = document.createElement("strong");
|
||
title.textContent = item.title;
|
||
const meta = document.createElement("span");
|
||
meta.textContent = item.meta;
|
||
li.appendChild(title);
|
||
li.appendChild(meta);
|
||
if (item.highlight) li.classList.add("highlight");
|
||
resultsList.appendChild(li);
|
||
});
|
||
resultsList.classList.add("scrolling");
|
||
if (resultsContainer) resultsContainer.scrollTop = 0;
|
||
},
|
||
focusResult(el) {
|
||
if (!el || !resultsContainer) return;
|
||
helpers.stopResultsScroll();
|
||
const offset = el.offsetTop;
|
||
resultsContainer.scrollTo({ top: offset, behavior: "smooth" });
|
||
},
|
||
async showExtractionWindow({ title, url, summaryLines }) {
|
||
extractionTitle.textContent = title;
|
||
extractionUrl.textContent = url;
|
||
extractionState.textContent = "正在提取...";
|
||
extractionState.classList.remove("complete");
|
||
extractionSummary.innerHTML = "";
|
||
extractionWindow.classList.add("visible");
|
||
positionFloatingWindow(extractionWindow, windowAnchors.get(extractionWindow));
|
||
const lines = Array.isArray(summaryLines)
|
||
? summaryLines
|
||
: summaryLines
|
||
? [summaryLines]
|
||
: [];
|
||
for (const line of lines) {
|
||
const row = document.createElement("p");
|
||
row.textContent = `• ${line}`;
|
||
extractionSummary.appendChild(row);
|
||
await helpers.sleep(200);
|
||
}
|
||
extractionState.textContent = "提取完成";
|
||
extractionState.classList.add("complete");
|
||
},
|
||
hideExtractionWindow() {
|
||
extractionWindow.classList.remove("visible");
|
||
extractionState.textContent = "等待提取";
|
||
extractionState.classList.remove("complete");
|
||
extractionTitle.textContent = "-";
|
||
extractionUrl.textContent = "";
|
||
extractionSummary.innerHTML = "";
|
||
},
|
||
stopResultsScroll() {
|
||
resultsList.classList.remove("scrolling");
|
||
},
|
||
async moveToIcon(iconEl, options) {
|
||
if (!iconEl) return;
|
||
const img = iconEl.querySelector("img");
|
||
if (img) {
|
||
return helpers.moveMouseTo(img, options);
|
||
}
|
||
return helpers.moveMouseTo(iconEl, options);
|
||
},
|
||
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 };
|
||
},
|
||
showContextMenu(menu) {
|
||
const element = menu === "desktop"
|
||
? desktopMenu
|
||
: menu === "folder"
|
||
? folderMenu
|
||
: browserMenu;
|
||
element.classList.add("visible");
|
||
const tip = helpers.getPointerTip();
|
||
const desiredX = tip.x + 16;
|
||
const desiredY = tip.y + 16;
|
||
const { offsetWidth: w, offsetHeight: h } = element;
|
||
const { x, y } = helpers.clampToScreen(desiredX, desiredY, w, h);
|
||
element.style.left = `${x}px`;
|
||
element.style.top = `${y}px`;
|
||
},
|
||
hideContextMenus() {
|
||
desktopMenu.classList.remove("visible");
|
||
folderMenu.classList.remove("visible");
|
||
browserMenu.classList.remove("visible");
|
||
desktopMenu.querySelectorAll("button").forEach((btn) => btn.classList.remove("active"));
|
||
folderMenu.querySelectorAll("button").forEach((btn) => btn.classList.remove("active"));
|
||
browserMenu.querySelectorAll("button").forEach((btn) => btn.classList.remove("active"));
|
||
},
|
||
async highlightMenu(menu, action) {
|
||
const element = menu === "desktop"
|
||
? desktopMenu
|
||
: menu === "folder"
|
||
? folderMenu
|
||
: browserMenu;
|
||
const btn = element.querySelector(`button[data-action="${action}"]`);
|
||
if (!btn) return;
|
||
btn.classList.add("active");
|
||
await helpers.sleep(300);
|
||
btn.classList.remove("active");
|
||
},
|
||
createFolderIcon(name) {
|
||
const icon = createIconElement(name, "../icons/folder.svg");
|
||
icon.id = "researchFolder";
|
||
desktopGrid.appendChild(icon);
|
||
return icon;
|
||
},
|
||
setFolderOpenState(icon, isOpen) {
|
||
if (!icon) return;
|
||
const img = icon.querySelector("img");
|
||
if (!img) return;
|
||
img.src = isOpen ? "../icons/folder-open.svg" : "../icons/folder.svg";
|
||
},
|
||
showFolderWindow() {
|
||
folderWindow.classList.add("visible");
|
||
positionFloatingWindow(folderWindow, windowAnchors.get(folderWindow));
|
||
},
|
||
hideFolderWindow() {
|
||
folderWindow.classList.remove("visible");
|
||
},
|
||
addFileToFolder(name) {
|
||
const file = createIconElement(name, "../icons/file.svg", "folder-entry");
|
||
folderBody.appendChild(file);
|
||
requestAnimationFrame(() => file.classList.add("visible"));
|
||
file.id = "summaryFile";
|
||
return file;
|
||
},
|
||
spawnDesktopFile(name) {
|
||
const icon = createIconElement(name, "../icons/file.svg", "app");
|
||
icon.classList.add("temporary-shortcut");
|
||
icon.style.opacity = 0;
|
||
desktopGrid.appendChild(icon);
|
||
requestAnimationFrame(() => {
|
||
icon.style.transition = "opacity .4s ease, transform .4s ease";
|
||
icon.style.opacity = 1;
|
||
icon.style.transform = "translateY(-6px)";
|
||
});
|
||
return icon;
|
||
},
|
||
showEditorWindow() {
|
||
editorWindow.classList.add("visible");
|
||
positionFloatingWindow(editorWindow, windowAnchors.get(editorWindow));
|
||
editorBody.innerHTML = "";
|
||
},
|
||
async typeEditorLines(lines) {
|
||
editorBody.innerHTML = "";
|
||
for (const line of lines) {
|
||
const div = document.createElement("div");
|
||
div.className = "code-line";
|
||
div.textContent = line;
|
||
editorBody.appendChild(div);
|
||
requestAnimationFrame(() => div.classList.add("visible"));
|
||
await helpers.sleep(180);
|
||
}
|
||
},
|
||
resetScene() {
|
||
helpers.hideContextMenus();
|
||
helpers.hideExtractionWindow();
|
||
browserWindow.classList.remove("visible");
|
||
folderWindow.classList.remove("visible");
|
||
editorWindow.classList.remove("visible");
|
||
folderBody.innerHTML = "";
|
||
searchText.textContent = "";
|
||
browserStatus.textContent = "准备搜索...";
|
||
resultsList.innerHTML = "";
|
||
resultsList.classList.remove("scrolling");
|
||
const researchIcon = document.getElementById("researchFolder");
|
||
if (researchIcon) researchIcon.remove();
|
||
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.sleep(400);
|
||
await helpers.showBubble("我来为您搜索“自适应代理架构”,并写入摘要。", { duration: 2200 });
|
||
|
||
const browserIcon = document.getElementById("browserApp");
|
||
await helpers.moveToIcon(browserIcon);
|
||
await helpers.click({ count: 2 });
|
||
helpers.showBrowserWindow();
|
||
helpers.setStatus("正在搜索");
|
||
await helpers.sleep(400);
|
||
|
||
const searchBar = document.querySelector(".search-bar");
|
||
await helpers.moveMouseTo(searchBar, { offsetX: -40 });
|
||
await helpers.click({ count: 1 });
|
||
await helpers.typeSearch("自适应代理架构 最新进展");
|
||
browserStatus.textContent = "正在搜索...";
|
||
await helpers.sleep(900);
|
||
|
||
const mockResults = [
|
||
{ title: "自适应代理编排:2025 多模型协作的新基线", meta: "blog.multiagent.studio · 10分钟前" },
|
||
{ title: "行业案例 | 金融风控中的多工具增强代理", meta: "fin-ai.report · 35分钟前" },
|
||
{ title: "多代理协作在知识检索中的应用", meta: "lab.deepsearch.io · 1小时前", highlight: true },
|
||
{ title: "开放基座:可插拔工具链的最佳实践", meta: "oss.agentkit.dev · 2小时前" }
|
||
];
|
||
helpers.populateResults(mockResults);
|
||
browserStatus.textContent = "搜索完成,播放最新结果";
|
||
await helpers.sleep(800);
|
||
helpers.setStatus("正在规划");
|
||
await helpers.showBubble("(思考)这些结果里第三条更适合深入提取...", { variant: "thinking", duration: 1600 });
|
||
await helpers.sleep(200);
|
||
|
||
helpers.setStatus("正在规划");
|
||
helpers.stopResultsScroll();
|
||
await helpers.showBubble("我来为您提取第三条结果的信息。", { duration: 1800 });
|
||
const highlighted = Array.from(resultsList.children).find((li) => li.classList.contains("highlight"));
|
||
helpers.focusResult(highlighted);
|
||
await helpers.sleep(400);
|
||
await helpers.moveMouseTo(highlighted, { duration: 700 });
|
||
await helpers.click({ count: 2 });
|
||
helpers.setStatus("正在提取");
|
||
await helpers.showExtractionWindow({
|
||
title: "多代理协作在知识检索中的应用",
|
||
url: "https://lab.deepsearch.io/articles/multi-agent-search",
|
||
summaryLines: [
|
||
"Agent 团队拆分检索与去噪任务。",
|
||
"结果在写作模块前先做事实对齐。",
|
||
"若工具失败则回退到长期记忆。"
|
||
]
|
||
});
|
||
await helpers.sleep(1600);
|
||
helpers.setStatus("保存网页");
|
||
await helpers.moveMouseTo(browserWindow, { offsetX: 20, offsetY: -80, duration: 700 });
|
||
await helpers.click({ right: true });
|
||
helpers.showContextMenu("browser");
|
||
await helpers.sleep(250);
|
||
const browserSaveBtn = browserMenu.querySelector('button[data-action="save"]');
|
||
await helpers.moveMouseTo(browserSaveBtn, { duration: 400 });
|
||
await helpers.highlightMenu("browser", "save");
|
||
await helpers.click({ count: 1 });
|
||
helpers.hideContextMenus();
|
||
helpers.spawnDesktopFile("search-summary.html");
|
||
await helpers.sleep(800);
|
||
helpers.hideExtractionWindow();
|
||
helpers.hideBrowserWindow();
|
||
|
||
helpers.setStatus("正在规划");
|
||
await helpers.showBubble("我来为您创建文件来记录摘要。", { duration: 1800 });
|
||
helpers.setStatus("正在创建");
|
||
await helpers.sleep(200);
|
||
await helpers.moveMouseTo(monitorScreen, { offsetX: 240, offsetY: 210, duration: 800 });
|
||
await helpers.click({ right: true });
|
||
helpers.showContextMenu("desktop");
|
||
await helpers.sleep(250);
|
||
const desktopFileBtn = desktopMenu.querySelector('button[data-action="file"]');
|
||
await helpers.moveMouseTo(desktopFileBtn, { duration: 450 });
|
||
await helpers.highlightMenu("desktop", "file");
|
||
await helpers.click({ count: 1 });
|
||
helpers.hideContextMenus();
|
||
await helpers.showBubble("不能在根目录创建文件", { variant: "error", duration: 1800, icon: "✕" });
|
||
await helpers.sleep(800);
|
||
|
||
helpers.setStatus("正在规划");
|
||
await helpers.showBubble("那我先在资料区新建一个文件夹。", { duration: 1800 });
|
||
helpers.setStatus("正在创建");
|
||
await helpers.moveMouseTo(monitorScreen, { offsetX: 200, offsetY: 160, duration: 700 });
|
||
await helpers.click({ right: true });
|
||
helpers.showContextMenu("desktop");
|
||
await helpers.sleep(250);
|
||
const desktopFolderBtn = desktopMenu.querySelector('button[data-action="folder"]');
|
||
await helpers.moveMouseTo(desktopFolderBtn, { duration: 450 });
|
||
await helpers.highlightMenu("desktop", "folder");
|
||
await helpers.click({ count: 1 });
|
||
helpers.hideContextMenus();
|
||
const folderIcon = helpers.createFolderIcon("research_notes");
|
||
helpers.setStatus("正在创建");
|
||
await helpers.sleep(1100);
|
||
|
||
helpers.setStatus("正在规划");
|
||
await helpers.showBubble("现在在文件夹里建一个文件。", { duration: 1700 });
|
||
await helpers.moveToIcon(folderIcon, { duration: 800 });
|
||
await helpers.click({ count: 2 });
|
||
helpers.showFolderWindow();
|
||
helpers.setFolderOpenState(folderIcon, true);
|
||
helpers.setStatus("正在创建");
|
||
await helpers.sleep(500);
|
||
await helpers.moveMouseTo(folderWindow, { offsetX: -100, offsetY: 50, duration: 700 });
|
||
await helpers.click({ right: true });
|
||
helpers.showContextMenu("folder");
|
||
await helpers.sleep(250);
|
||
const folderFileBtn = folderMenu.querySelector('button[data-action="file"]');
|
||
await helpers.moveMouseTo(folderFileBtn, { duration: 400 });
|
||
await helpers.highlightMenu("folder", "file");
|
||
await helpers.click({ count: 1 });
|
||
helpers.hideContextMenus();
|
||
const fileIcon = helpers.addFileToFolder("search_summary.md");
|
||
await helpers.sleep(900);
|
||
|
||
helpers.setStatus("正在规划");
|
||
await helpers.showBubble("现在写入内容。", { duration: 1700 });
|
||
await helpers.moveToIcon(fileIcon, { duration: 700 });
|
||
await helpers.click({ count: 2 });
|
||
helpers.showEditorWindow();
|
||
helpers.hideFolderWindow();
|
||
helpers.setFolderOpenState(folderIcon, false);
|
||
helpers.setStatus("正在写入");
|
||
const lines = [
|
||
"# 搜索摘要:多代理架构",
|
||
"1. 目标:构建可插拔工具编排。",
|
||
"2. 搜索代理筛选最新技术稿。",
|
||
"3. 抽取代理聚合跨源事实。",
|
||
"4. 验证代理回放 API 日志。",
|
||
"5. 记忆体更新长期策略。",
|
||
"6. 失败回退至记忆缓存。",
|
||
"7. 产出 summary + 下一步建议。"
|
||
];
|
||
await helpers.typeEditorLines(lines);
|
||
await helpers.sleep(1200);
|
||
|
||
await helpers.showBubble("好了!我已经完成了所需操作。", { duration: 2000 });
|
||
helpers.setStatus("已完成");
|
||
replayBtn.disabled = false;
|
||
}
|
||
|
||
runStory();
|
||
replayBtn.addEventListener("click", runStory);
|
||
</script>
|
||
</body>
|
||
</html>
|