// static/app-enhanced.js - 修复版,正确实现Token实时更新 function updateViewportHeightVar() { const docEl = document.documentElement; const visualViewport = window.visualViewport; if (visualViewport) { const vh = visualViewport.height; const bottomInset = Math.max( 0, (window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop ); docEl.style.setProperty('--app-viewport', `${vh}px`); docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`); } else { const height = window.innerHeight || docEl.clientHeight; if (height) { docEl.style.setProperty('--app-viewport', `${height}px`); } docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)'); } } updateViewportHeightVar(); window.addEventListener('resize', updateViewportHeightVar); window.addEventListener('orientationchange', updateViewportHeightVar); window.addEventListener('pageshow', updateViewportHeightVar); if (window.visualViewport) { window.visualViewport.addEventListener('resize', updateViewportHeightVar); window.visualViewport.addEventListener('scroll', updateViewportHeightVar); } const SOCKET_IO_CDN_SOURCES = [ 'https://cdn.socket.io/4.7.5/socket.io.min.js', 'https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js' ]; const ICONS = Object.freeze({ bot: '/static/icons/bot.svg', book: '/static/icons/book.svg', brain: '/static/icons/brain.svg', camera: '/static/icons/camera.svg', check: '/static/icons/check.svg', checkbox: '/static/icons/checkbox.svg', circleAlert: '/static/icons/circle-alert.svg', clipboard: '/static/icons/clipboard.svg', clock: '/static/icons/clock.svg', eye: '/static/icons/eye.svg', file: '/static/icons/file.svg', flag: '/static/icons/flag.svg', folder: '/static/icons/folder.svg', folderOpen: '/static/icons/folder-open.svg', globe: '/static/icons/globe.svg', hammer: '/static/icons/hammer.svg', info: '/static/icons/info.svg', laptop: '/static/icons/laptop.svg', menu: '/static/icons/menu.svg', monitor: '/static/icons/monitor.svg', octagon: '/static/icons/octagon.svg', pencil: '/static/icons/pencil.svg', python: '/static/icons/python.svg', recycle: '/static/icons/recycle.svg', save: '/static/icons/save.svg', search: '/static/icons/search.svg', settings: '/static/icons/settings.svg', sparkles: '/static/icons/sparkles.svg', stickyNote: '/static/icons/sticky-note.svg', terminal: '/static/icons/terminal.svg', trash: '/static/icons/trash.svg', triangleAlert: '/static/icons/triangle-alert.svg', user: '/static/icons/user.svg', wrench: '/static/icons/wrench.svg', x: '/static/icons/x.svg', zap: '/static/icons/zap.svg' }); const TOOL_ICON_MAP = Object.freeze({ append_to_file: 'pencil', close_sub_agent: 'octagon', create_file: 'file', create_folder: 'folder', create_sub_agent: 'bot', delete_file: 'trash', extract_webpage: 'globe', focus_file: 'eye', modify_file: 'pencil', ocr_image: 'camera', read_file: 'book', rename_file: 'pencil', run_command: 'terminal', run_python: 'python', save_webpage: 'save', sleep: 'clock', todo_create: 'stickyNote', todo_finish: 'flag', todo_finish_confirm: 'circleAlert', todo_update_task: 'check', terminal_input: 'terminal', terminal_reset: 'recycle', terminal_session: 'monitor', terminal_snapshot: 'clipboard', unfocus_file: 'eye', update_memory: 'brain', wait_sub_agent: 'clock', web_search: 'search', trigger_easter_egg: 'sparkles' }); const TOOL_CATEGORY_ICON_MAP = Object.freeze({ network: 'globe', file_edit: 'pencil', read_focus: 'eye', terminal_realtime: 'monitor', terminal_command: 'terminal', memory: 'brain', todo: 'stickyNote', sub_agent: 'bot', easter_egg: 'sparkles' }); function randRange(min, max) { return Math.random() * (max - min) + min; } function randInt(min, max) { return Math.floor(randRange(min, max + 1)); } function pickOne(arr) { if (!Array.isArray(arr) || arr.length === 0) { return null; } return arr[Math.floor(Math.random() * arr.length)]; } function injectScriptSequentially(urls, onSuccess, onFailure) { let index = 0; const tryLoad = () => { if (index >= urls.length) { onFailure(); return; } const url = urls[index]; const script = document.createElement('script'); script.src = url; script.async = false; script.onload = () => { if (typeof io !== 'undefined') { console.log(`Socket.IO 已从 ${url} 加载`); onSuccess(); } else { index += 1; tryLoad(); } }; script.onerror = () => { console.warn(`无法从 ${url} 加载 Socket.IO,尝试下一个源`); index += 1; tryLoad(); }; document.head.appendChild(script); }; tryLoad(); } async function ensureSocketIOLoaded() { if (typeof io !== 'undefined') { return true; } return await new Promise((resolve) => { injectScriptSequentially( SOCKET_IO_CDN_SOURCES, () => resolve(true), () => resolve(false) ); }); } async function bootstrapApp() { // 检查必要的库是否加载 if (typeof Vue === 'undefined') { console.error('错误:Vue.js 未加载'); document.body.innerHTML = '
...
return html.replace(/]*)>([\s\S]*?)<\/code><\/pre>/g, (match, attributes, content) => {
// 提取语言
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
const language = langMatch ? langMatch[1] : 'text';
// 生成唯一ID
const blockId = `code-${Date.now()}-${counter++}`;
// 转义引号用于data属性
const escapedContent = content
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
// 构建新的HTML,保持code元素原样
return `
${language}
${content}
`;
});
},
getLanguageClass(path) {
const ext = path.split('.').pop().toLowerCase();
const langMap = {
'py': 'language-python',
'js': 'language-javascript',
'html': 'language-html',
'css': 'language-css',
'json': 'language-json',
'md': 'language-markdown',
'txt': 'language-plain'
};
return langMap[ext] || 'language-plain';
},
scrollToBottom() {
setTimeout(() => {
const messagesArea = this.$refs.messagesArea;
if (messagesArea) {
// 标记为程序触发的滚动
if (this._setScrollingFlag) {
this._setScrollingFlag(true);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
// 滚动完成后重置标记
setTimeout(() => {
if (this._setScrollingFlag) {
this._setScrollingFlag(false);
}
}, 100);
}
}, 50);
},
conditionalScrollToBottom() {
// 严格检查:只在明确允许时才滚动
if (this.autoScrollEnabled === true && this.userScrolling === false) {
this.scrollToBottom();
}
},
toggleScrollLock() {
const currentlyLocked = this.autoScrollEnabled && !this.userScrolling;
if (currentlyLocked) {
this.autoScrollEnabled = false;
this.userScrolling = true;
} else {
this.autoScrollEnabled = true;
this.userScrolling = false;
this.scrollToBottom();
}
},
scrollThinkingToBottom(blockId) {
if (!this.thinkingScrollLocks.get(blockId)) return;
const refName = `thinkingContent-${blockId}`;
const elRef = this.$refs[refName];
const el = Array.isArray(elRef) ? elRef[0] : elRef;
if (el) {
el.scrollTop = el.scrollHeight;
}
},
handleThinkingScroll(blockId, event) {
const el = event.target;
const threshold = 12;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
this.thinkingScrollLocks.set(blockId, atBottom);
},
// 面板调整方法
startResize(panel, event) {
this.isResizing = true;
this.resizingPanel = panel;
if (panel === 'right' && this.rightCollapsed) {
this.rightCollapsed = false;
if (this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
}
document.addEventListener('mousemove', this.handleResize);
document.addEventListener('mouseup', this.stopResize);
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
event.preventDefault();
},
handleResize(event) {
if (!this.isResizing) return;
const containerWidth = document.querySelector('.main-container').offsetWidth;
if (this.resizingPanel === 'left') {
let newWidth = event.clientX - (this.sidebarCollapsed ? 60 : 300);
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.leftWidth = newWidth;
} else if (this.resizingPanel === 'right') {
let newWidth = containerWidth - event.clientX;
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.rightWidth = newWidth;
} else if (this.resizingPanel === 'conversation') {
// 对话侧边栏宽度调整
let newWidth = event.clientX;
newWidth = Math.max(200, Math.min(newWidth, 400));
// 这里可以动态调整对话侧边栏宽度,暂时不实现
}
},
stopResize() {
this.isResizing = false;
this.resizingPanel = null;
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.stopResize);
document.body.style.userSelect = '';
document.body.style.cursor = '';
},
handleEasterEggPayload(payload) {
if (!payload) {
return;
}
let parsed = payload;
if (typeof payload === 'string') {
try {
parsed = JSON.parse(payload);
} catch (error) {
console.warn('无法解析彩蛋结果:', payload);
return;
}
}
if (!parsed || typeof parsed !== 'object') {
return;
}
if (!parsed.success) {
if (parsed.error) {
console.warn('彩蛋触发失败:', parsed.error);
}
this.destroyEasterEggEffect();
return;
}
const effectName = (parsed.effect || '').toLowerCase();
if (effectName === 'flood') {
this.startFloodEffect(parsed);
}
},
startFloodEffect(payload = {}) {
this.clearFloodAnimations();
this.teardownEasterEggStyle();
this.easterEgg.active = true;
this.easterEgg.effect = 'flood';
this.easterEgg.retreating = false;
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
this.easterEgg.cleanupTimer = null;
}
this.$nextTick(() => {
this.runFloodAnimation(payload);
});
const durationSeconds = Math.max(8, Number(payload.duration_seconds) || 45);
this.easterEgg.cleanupTimer = setTimeout(() => {
this.destroyEasterEggEffect();
}, durationSeconds * 1000);
},
runFloodAnimation(payload = {}) {
const container = this.$refs.easterEggWaterContainer;
if (!container) {
return;
}
const waves = container.querySelectorAll('.wave');
if (!waves.length) {
return;
}
const styleEl = document.createElement('style');
styleEl.setAttribute('data-easter-egg', 'flood');
document.head.appendChild(styleEl);
this.easterEgg.styleNode = styleEl;
const sheet = styleEl.sheet;
if (!sheet) {
return;
}
container.style.animation = 'none';
container.style.height = '0%';
void container.offsetHeight;
const range = Array.isArray(payload.intensity_range) && payload.intensity_range.length === 2
? payload.intensity_range
: [0.85, 0.92];
const minHeight = Math.min(range[0], range[1]);
const maxHeight = Math.max(range[0], range[1]);
const targetHeight = randRange(minHeight * 100, maxHeight * 100);
const riseDuration = randRange(30, 40);
const easing = pickOne([
'ease-in-out',
'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'cubic-bezier(0.42, 0, 0.58, 1)'
]) || 'ease-in-out';
const riseName = `easter_egg_rise_${Date.now()}`;
sheet.insertRule(`@keyframes ${riseName} { 0% { height: 0%; } 100% { height: ${targetHeight}%; } }`, sheet.cssRules.length);
container.style.animation = `${riseName} ${riseDuration}s ${easing} forwards`;
const directionSets = [
[1, 1, -1],
[1, -1, -1],
[-1, 1, 1],
[-1, -1, 1]
];
const directions = pickOne(directionSets) || [1, -1, 1];
const colors = [
'rgba(135, 206, 250, 0.35)',
'rgba(100, 181, 246, 0.45)',
'rgba(33, 150, 243, 0.4)'
];
waves.forEach((wave, index) => {
const svgData = this.buildFloodWaveShape(index);
const color = colors[index % colors.length];
const svg = ``;
const encoded = encodeURIComponent(svg);
wave.style.backgroundImage = `url("data:image/svg+xml,${encoded}")`;
wave.style.backgroundSize = `${svgData.waveWidth}px 100%`;
const animationName = `easter_egg_wave_${index}_${Date.now()}`;
const startPosition = randInt(-200, 200);
const distance = svgData.waveWidth * (directions[index] || 1);
const duration = index === 0 ? randRange(16, 22) : index === 1 ? randRange(11, 16) : randRange(7, 12);
const delay = randRange(0, 1.5);
sheet.insertRule(`@keyframes ${animationName} { 0% { background-position-x: ${startPosition}px; } 100% { background-position-x: ${startPosition + distance}px; } }`, sheet.cssRules.length);
wave.style.animation = `${animationName} ${duration}s linear infinite`;
wave.style.animationDelay = `${delay}s`;
wave.style.backgroundPositionX = `${startPosition}px`;
});
},
buildFloodWaveShape(layerIndex) {
const baseHeight = 180 + layerIndex * 12;
const cycles = 4;
let path = `M0,${baseHeight}`;
let currentX = 0;
let previousAmplitude = randRange(40, 80);
for (let i = 0; i < cycles; i++) {
const waveLength = randRange(700, 900);
const minAmp = Math.max(20, previousAmplitude - 20);
const maxAmp = Math.min(90, previousAmplitude + 20);
const amplitude = randRange(minAmp, maxAmp);
previousAmplitude = amplitude;
const halfWave = waveLength / 2;
const peakX = currentX + halfWave / 2;
path += ` Q${peakX},${baseHeight - amplitude} ${currentX + halfWave},${baseHeight}`;
const troughX = currentX + halfWave + halfWave / 2;
path += ` Q${troughX},${baseHeight + amplitude} ${currentX + waveLength},${baseHeight}`;
currentX += waveLength;
}
path += ` L${currentX},1000 L0,1000 Z`;
return {
path,
width: currentX,
waveWidth: currentX / cycles
};
},
clearFloodAnimations() {
const container = this.$refs.easterEggWaterContainer;
if (!container) {
return;
}
container.style.animation = 'none';
container.style.transition = 'none';
container.style.height = '0%';
const waves = container.querySelectorAll('.wave');
waves.forEach((wave) => {
wave.style.animation = 'none';
wave.style.backgroundImage = '';
wave.style.backgroundSize = '';
wave.style.backgroundPositionX = '0px';
});
},
teardownEasterEggStyle() {
if (this.easterEgg.styleNode && this.easterEgg.styleNode.parentNode) {
this.easterEgg.styleNode.parentNode.removeChild(this.easterEgg.styleNode);
}
this.easterEgg.styleNode = null;
},
destroyEasterEggEffect() {
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
this.easterEgg.cleanupTimer = null;
}
if (this.easterEgg.effect === 'flood' && this.easterEgg.active && !this.easterEgg.retreating) {
this.startFloodRetreat();
return;
}
if (this.easterEgg.effect === 'flood' && this.easterEgg.retreating) {
return;
}
this.finishEasterEggCleanup();
},
startFloodRetreat() {
const container = this.$refs.easterEggWaterContainer;
if (!container) {
this.finishEasterEggCleanup();
return;
}
this.easterEgg.retreating = true;
const measuredHeight = container.offsetHeight || container.clientHeight;
const computedHeight = window.getComputedStyle(container).height;
const currentHeight = measuredHeight
? `${measuredHeight}px`
: (computedHeight && computedHeight !== 'auto' ? computedHeight : '0px');
container.style.animation = 'none';
container.style.transition = 'none';
container.style.height = currentHeight;
void container.offsetHeight;
const retreatDuration = 8;
container.style.transition = `height ${retreatDuration}s ease-in-out`;
requestAnimationFrame(() => {
container.style.height = '0px';
});
this.easterEgg.cleanupTimer = setTimeout(() => {
container.style.transition = 'none';
this.clearFloodAnimations();
this.teardownEasterEggStyle();
this.easterEgg.active = false;
this.easterEgg.effect = null;
this.easterEgg.retreating = false;
}, retreatDuration * 1000);
},
finishEasterEggCleanup() {
this.clearFloodAnimations();
this.teardownEasterEggStyle();
this.easterEgg.active = false;
this.easterEgg.effect = null;
this.easterEgg.retreating = false;
},
// 格式化token显示(修复NaN问题)
formatTokenCount(tokens) {
// 确保tokens是数字,防止NaN
const num = Number(tokens) || 0;
if (num < 1000) {
return num.toString();
} else if (num < 1000000) {
return (num / 1000).toFixed(1) + 'K';
} else {
return (num / 1000000).toFixed(1) + 'M';
}
}
}
});
app.component('file-node', {
name: 'FileNode',
props: {
node: {
type: Object,
required: true
},
level: {
type: Number,
default: 0
},
expandedFolders: {
type: Object,
required: true
}
},
emits: ['toggle-folder', 'context-menu'],
computed: {
isExpanded() {
if (this.node.type !== 'folder') {
return false;
}
const value = this.expandedFolders[this.node.path];
return value === undefined ? true : value;
},
folderPadding() {
return {
paddingLeft: `${12 + this.level * 16}px`
};
},
filePadding() {
return {
paddingLeft: `${40 + this.level * 16}px`
};
}
},
methods: {
iconStyle(iconKey) {
const iconPath = ICONS[iconKey];
return iconPath ? { '--icon-src': `url(${iconPath})` } : {};
},
toggle() {
if (this.node.type === 'folder') {
this.$emit('toggle-folder', this.node.path);
}
}
},
template: `
{{ node.name }}
{{ node.annotation }}
`
});
app.mount('#app');
console.log('Vue应用初始化完成');
}
window.addEventListener('load', () => {
bootstrapApp();
});