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

1211 lines
46 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;
--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>