Agent/utils/tool-renderer.ts
2026-08-06 22:17:39 +08:00

114 lines
4.5 KiB
TypeScript

// @ts-nocheck
import { getToolNameLabel } from '../ui/sidebar.js';
let _toolSeq = 0;
export function createToolCard(parentEl, opts) {
const { name, args, protocol = 'mcp' } = opts;
const seq = ++_toolSeq;
const el = document.createElement('div');
const isMeta = name === 'metro_search_tools' || name === 'metro_get_tool_detail';
el.className = `tool-card running${isMeta ? ' meta' : ''}`;
el.dataset.seq = seq;
el.innerHTML = `
<div class="tool-header">
<span class="tool-icon"></span>
<span class="tool-name" data-role="name" title="${escapeHtml(name)}">正在调用 ${escapeHtml(getToolNameLabel(name))}</span>
<span class="tool-protocol ${protocol}">${protocol.toUpperCase()}</span>
<span class="tool-status running" data-role="status">运行中</span>
<span class="tool-chevron">▶</span>
</div>
<div class="tool-body">
<div class="tool-section" data-role="progress-section" style="display:none;">
<div class="tool-section-label">进度</div>
<div class="tool-progress" data-role="progress"></div>
</div>
<div class="tool-section">
<div class="tool-section-label">参数</div>
<div class="tool-section-content" data-role="args">${escapeHtml(JSON.stringify(args, null, 2))}</div>
</div>
<div class="tool-section" data-role="result-section" style="display:none;">
<div class="tool-section-label">结果</div>
<div class="tool-section-content" data-role="result"></div>
</div>
</div>
`;
parentEl.appendChild(el);
const nameEl = el.querySelector('[data-role="name"]');
const statusEl = el.querySelector('[data-role="status"]');
const progressSection = el.querySelector('[data-role="progress-section"]');
const progressEl = el.querySelector('[data-role="progress"]');
const resultSection = el.querySelector('[data-role="result-section"]');
const resultEl = el.querySelector('[data-role="result"]');
const label = getToolNameLabel(name);
el.querySelector('.tool-header').addEventListener('click', () => {
el.classList.toggle('expanded');
});
return {
el,
setStatus(status, summary) {
el.classList.remove('running');
if (status === 'success' || status === 'error') {
progressSection.style.display = 'none';
}
if (status === 'success') {
nameEl.textContent = label;
el.classList.add('success');
statusEl.className = 'tool-status success';
statusEl.textContent = summary || '完成';
} else if (status === 'error') {
nameEl.textContent = label;
el.classList.add('error');
statusEl.className = 'tool-status error';
statusEl.textContent = summary || '失败';
}
},
setResult(result, isError) {
resultSection.style.display = '';
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
resultEl.textContent = text;
if (text.length > 400) {
resultEl.classList.add('collapsed');
resultEl.addEventListener('click', () => {
resultEl.classList.remove('collapsed');
}, { once: true });
}
},
updateProgress(progress) {
if (!progress) return;
progressSection.style.display = '';
el.classList.add('expanded');
let html = '';
if (progress.stage) html += `<div class="progress-stage">${escapeHtml(progress.stage)}</div>`;
if (progress.total && progress.current) {
const pct = Math.round((progress.current / progress.total) * 100);
html += `<div class="progress-bar"><div class="progress-bar-fill" style="width:${pct}%"></div></div>`;
html += `<div class="progress-count">${progress.current}/${progress.total}(${pct}%)</div>`;
}
if (progress.detail) html += `<div class="progress-detail">${escapeHtml(progress.detail)}</div>`;
progressEl.innerHTML = html;
},
};
}
function escapeHtml(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}