Files
cursor-api/static/logs.html
2025-03-05 04:21:37 +08:00

1344 lines
38 KiB
HTML
Raw 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">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>请求日志查看</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
/* 创建正确的堆叠上下文 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: var(--spacing);
}
.stat-card {
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all var(--transition-fast);
border: 1px solid var(--border-color);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.stat-card h4 {
margin: 0 0 8px 0;
color: var(--primary-color);
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: var(--primary-color);
margin-top: 4px;
}
.refresh-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.auto-refresh {
display: flex;
align-items: center;
gap: 8px;
background: var(--card-background);
padding: 8px 16px;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
overflow-y: hidden;
}
.modal-content {
background-color: var(--card-background);
margin: 5% auto;
padding: 20px;
border-radius: var(--border-radius);
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.modal-header {
position: sticky;
top: 0;
background: var(--card-background);
z-index: 10;
padding-bottom: 15px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.modal-header h3 {
margin: 0;
flex-grow: 1;
}
#conversation-content-container {
flex: 1;
overflow-y: auto;
margin: 0 -20px;
padding: 20px;
}
.close {
float: right;
cursor: pointer;
font-size: 28px;
}
.info-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
transition: all var(--transition-fast);
background: var(--primary-color-alpha);
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.info-button:hover {
background: var(--primary-color);
color: white;
}
.message-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
margin: 0;
border: 1px solid var(--border-color);
}
.message-table th,
.message-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
transition: background-color var(--transition-fast);
}
.message-table td {
word-break: break-word;
}
.message-table td:nth-child(2) {
max-width: 600px;
}
.message-table td:first-child {
width: 80px;
white-space: nowrap;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 5px 10px;
}
.close:hover {
color: var(--primary-color);
}
.usage-progress-container {
margin: 16px 0;
height: 8px;
background-color: var(--border-color);
border-radius: 4px;
overflow: hidden;
}
.usage-progress-bar {
height: 100%;
width: 0%;
transition: width 0.3s ease;
background-color: var(--primary-color);
border-radius: 4px;
}
/* 根据使用比例改变颜色 */
.usage-progress-bar.low {
background-color: #4caf50;
/* 绿色 */
}
.usage-progress-bar.medium {
background-color: #ff9800;
/* 橙色 */
}
.usage-progress-bar.high {
background-color: #f44336;
/* 红色 */
}
/* Token 信息和对话预览的通用样式 */
.token-info-tooltip {
position: relative;
display: inline-block;
}
.token-info-tooltip .tooltip-content {
visibility: hidden;
position: absolute;
z-index: 1002;
background-color: var(--card-background);
padding: 12px 15px;
border-radius: var(--border-radius);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 280px;
left: 50%;
transform: translateX(-50%);
bottom: calc(100% + 8px);
opacity: 0;
transition: opacity 0.3s, visibility 0.3s;
text-align: left;
line-height: 1.6;
border: 1px solid var(--border-color);
pointer-events: none;
}
.token-info-tooltip:hover .tooltip-content {
visibility: visible;
opacity: 1;
}
/* 添加小三角形指示器 */
.token-info-tooltip .tooltip-content::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -8px;
border-width: 8px;
border-style: solid;
border-color: var(--card-background) transparent transparent transparent;
}
/* 添加不可见的连接区域 */
.token-info-tooltip::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100%;
width: 100%;
height: 10px;
background: transparent;
}
/* Token 信息特定样式 */
.token-info-tooltip .tooltip-info-row {
display: flex;
justify-content: space-between;
margin: 2px 0;
}
.token-info-tooltip .tooltip-info-row .label {
color: var(--text-secondary);
margin-right: 10px;
}
.token-info-tooltip .tooltip-info-row .value {
font-weight: 500;
word-break: break-word;
}
/* 对话预览特定样式 */
.prompt-preview .tooltip-content {
width: 320px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
word-break: break-word;
}
.prompt-preview .tooltip-content .message-meta {
font-size: 0.8em;
color: var(--text-secondary);
padding: 0;
margin: 0 0 4px 0;
}
.prompt-preview .tooltip-content .last-message {
font-size: 0.9em;
line-height: 1.5;
color: var(--text-primary);
margin: 0;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
/* 优化滚动条样式 */
.prompt-preview .tooltip-content::-webkit-scrollbar {
width: 6px;
}
.prompt-preview .tooltip-content::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 3px;
}
.prompt-preview .tooltip-content::-webkit-scrollbar-track {
background-color: var(--card-background);
}
/* 优化表格样式 */
.table-container {
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--border-color);
}
#logsTable {
position: relative;
z-index: 1;
}
#logsTable th {
position: sticky;
top: 0;
z-index: 2;
background: var(--primary-color);
white-space: nowrap;
transition: background-color 0.2s ease;
}
#logsTable td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
transition: background-color var(--transition-fast);
}
/* 优化表格悬停效果 */
#logsTable tr:hover td {
background-color: var(--hover-color, rgba(0, 0, 0, 0.02));
}
.modal-actions {
display: flex;
align-items: center;
gap: 12px;
}
.danger-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
background: var(--error-color-alpha);
color: var(--error-color);
border: 1px solid var(--error-color);
cursor: pointer;
transition: all var(--transition-fast);
}
.danger-button:hover {
background: var(--error-color);
color: white;
}
/* 图表样式 */
.chart-container {
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: var(--spacing);
border: 1px solid var(--border-color);
height: 300px;
}
/* 添加菜单切换样式 */
.tab-menu {
display: flex;
margin-bottom: 15px;
}
.tab-button {
padding: 8px 16px;
background: var(--card-background);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all var(--transition-fast);
font-size: 14px;
}
.tab-button:first-child {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
.tab-button:last-child {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.tab-button.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.delay-chart-container {
height: 200px;
margin-bottom: 20px;
}
.delay-table-container {
max-height: none;
overflow-y: visible;
}
/* 响应式样式 - 所有@media查询集中在此 */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
padding: 16px;
}
.stat-value {
font-size: 24px;
}
.modal-content {
margin: 2% auto;
width: 95%;
padding: 16px;
}
.chart-container {
height: 200px;
padding: 16px;
}
.modal-header {
flex-direction: column;
align-items: flex-start;
}
.tab-menu {
width: 100%;
}
.tab-button {
flex: 1;
}
}
</style>
</head>
<body>
<h1>请求日志查看</h1>
<div class="container">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
<div class="refresh-container">
<div class="button-group">
<button onclick="fetchLogs()">刷新日志</button>
</div>
<div class="auto-refresh">
<input type="checkbox" id="autoRefresh" checked>
<label for="autoRefresh">自动刷新 (60秒)</label>
</div>
</div>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<h4>总请求数</h4>
<div id="totalRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>活跃请求数</h4>
<div id="activeRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>错误请求数</h4>
<div id="errorRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>最后更新</h4>
<div id="lastUpdate" class="stat-value">-</div>
</div>
</div>
<!-- 添加图表容器 -->
<div class="chart-container">
<canvas id="requestsChart"></canvas>
</div>
<div class="table-container">
<table id="logsTable">
<thead>
<tr>
<th></th>
<th>时间</th>
<th>模型</th>
<th>Token信息</th>
<th>对话</th>
<th>用时</th>
<th>流式响应</th>
<th>状态</th>
<th>错误信息</th>
</tr>
</thead>
<tbody id="logsBody"></tbody>
</table>
</div>
</div>
<div id="message"></div>
<!-- 添加弹窗组件 -->
<div id="tokenModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Token 详细信息</h3>
<div class="modal-actions">
<button class="danger-button" id="deleteTokenBtn">删除此Token</button>
<span class="close">&times;</span>
</div>
</div>
<table class="message-table">
<tr>
<td>Token:</td>
<td id="modalToken"></td>
</tr>
<tr>
<td>校验和:</td>
<td id="modalChecksum"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
用户信息
</td>
</tr>
<tr>
<td>邮箱:</td>
<td id="modalEmail"></td>
</tr>
<tr>
<td>用户名:</td>
<td id="modalName"></td>
</tr>
<tr>
<td>用户ID:</td>
<td id="modalId"></td>
</tr>
<tr>
<td>更新时间:</td>
<td id="modalUpdatedAt"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
会员信息
</td>
</tr>
<tr>
<td>会员类型:</td>
<td id="modalMemberType"></td>
</tr>
<tr>
<td>支付ID:</td>
<td id="modalPaymentId"></td>
</tr>
<tr>
<td>试用剩余:</td>
<td id="modalTrialDays"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
使用量统计 (最近30天)
</td>
</tr>
<tr>
<td>Premium models:</td>
<td id="modalPremiumUsage"></td>
</tr>
<tr>
<td>Standard models:</td>
<td id="modalStandardUsage"></td>
</tr>
<tr>
<td>Unknown models:</td>
<td id="modalUnknownUsage"></td>
</tr>
</table>
<div id="usageProgressContainer"></div>
</div>
</div>
<div id="conversationModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>对话详情</h3>
<div class="tab-menu">
<button id="tab-prompt" class="tab-button active">对话内容</button>
<button id="tab-delays" class="tab-button">延迟分析</button>
</div>
<span class="close">&times;</span>
</div>
<div id="conversation-content-container">
<div id="dialogContent" class="tab-content active"></div>
<div id="delaysContent" class="tab-content">
<div class="chart-container delay-chart-container">
<canvas id="delayChart"></canvas>
</div>
<h4>事件表</h4>
<div class="delay-table-container">
<table class="message-table" id="delaysTable">
<thead>
<tr>
<th style="width: 8%">序号</th>
<th style="width: 52%">文本</th>
<th style="width: 15%">间隔 (秒)</th>
<th style="width: 25%">速率 (字符/秒)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 添加 Chart.js 库 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.js"
integrity="sha512-0im+NZpDrlsC+p6iSc13cqlMNPqdT6e0hUF8NAaxdaGOmPuV9DdVpWYOCHHrMQNVDb2TByQoDbHx34MT6g16ZA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
let refreshInterval;
let requestsChart;
function updateStats(data) {
document.getElementById('totalRequests').textContent = data.total || 0;
document.getElementById('activeRequests').textContent = data.active || 0;
if (data.error) {
document.getElementById('errorRequests').textContent = data.error;
document.getElementById('errorRequests').parentElement.style.display = '';
} else {
document.getElementById('errorRequests').parentElement.style.display = 'none';
}
document.getElementById('lastUpdate').textContent =
new Date(data.timestamp).toLocaleTimeString();
}
function getProgressBarClass(percentage) {
if (percentage <= 60) return 'low';
if (percentage <= 85) return 'medium';
return 'high';
}
function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal');
const deleteBtn = document.getElementById('deleteTokenBtn');
// 存储当前token用于删除操作
const currentToken = tokenInfo.token;
// 更新删除按钮点击事件
deleteBtn.onclick = async () => {
if (!currentToken) {
showGlobalMessage('无效的Token', true);
return;
}
if (!confirm('确定要删除此Token吗此操作不可撤销。')) {
return;
}
const data = await makeAuthenticatedRequest('/tokens/delete', {
method: 'POST',
body: JSON.stringify({
tokens: [currentToken],
expectation: 'failed_tokens'
})
});
if (data) {
modal.style.display = 'none';
let message = 'Token删除成功';
if (data.failed_tokens?.length) {
message = 'Token删除失败未找到该Token';
}
showGlobalMessage(message);
// 刷新日志列表
fetchLogs();
}
};
document.getElementById('modalToken').textContent = tokenInfo.token || '-';
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
if (tokenInfo.profile) {
const { user, stripe, usage } = tokenInfo.profile;
// 设置用户信息
document.getElementById('modalEmail').textContent = user.email || '-';
document.getElementById('modalName').textContent = user.name || '-';
document.getElementById('modalId').textContent = user.id || '-';
document.getElementById('modalUpdatedAt').textContent = user.updated_at ? new Date(user.updated_at).toLocaleString() : '-';
// 设置会员信息
document.getElementById('modalMemberType').textContent =
formatMembershipType(stripe.membership_type);
document.getElementById('modalPaymentId').textContent = stripe.payment_id || '-';
document.getElementById('modalTrialDays').textContent =
stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}` : '-';
// 处理使用量信息
const container = document.getElementById('usageProgressContainer');
container.innerHTML = '';
const models = {
'modalPremiumUsage': usage.premium,
'modalStandardUsage': usage.standard,
'modalUnknownUsage': usage.unknown
};
Object.entries(models).forEach(([elementId, modelData]) => {
const element = document.getElementById(elementId);
if (modelData) {
const { requests, tokens, max_requests } = modelData;
if (max_requests) {
const percentage = (requests / max_requests * 100).toFixed(1);
element.textContent = `${requests}/${max_requests} requests (${percentage}%), ${tokens} tokens`;
const progressDiv = document.createElement('div');
progressDiv.className = 'usage-progress-container';
const colorClass = getProgressBarClass(parseFloat(percentage));
progressDiv.innerHTML = `<div class="usage-progress-bar ${colorClass}" style="width: ${percentage}%"></div>`;
container.appendChild(progressDiv);
} else {
element.textContent = `${requests} requests, ${tokens} tokens`;
}
} else {
element.textContent = '-';
}
});
} else {
// 如果没有 profile 信息,清空所有字段
[
'modalEmail',
'modalName',
'modalId',
'modalUpdatedAt',
'modalMemberType',
'modalPaymentId',
'modalTrialDays',
'modalPremiumUsage',
'modalStandardUsage',
'modalUnknownUsage'
].forEach(id => document.getElementById(id).textContent = '-');
document.getElementById('usageProgressContainer').innerHTML = '';
}
modal.style.display = 'block';
}
function formatSimpleTokenInfo(tokenInfo) {
if (!tokenInfo.profile) return '无用户信息';
const { user, stripe, usage } = tokenInfo.profile;
const premiumUsage = usage.premium ?
`${usage.premium.requests}/${usage.premium.max_requests}` : '-';
const rows = [
['邮箱', user.email || '-'],
...(user.name ? [['用户名', user.name]] : []),
['会员', formatMembershipType(stripe.membership_type)],
['Premium', premiumUsage]
];
return rows.map(([label, value]) => `<div class="tooltip-info-row"><span class="label">${label}:</span><span class="value">${value}</span></div>`).join('');
}
function initChart() {
const ctx = document.getElementById('requestsChart').getContext('2d');
requestsChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '每小时请求数',
data: [],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 10
}
}
},
plugins: {
title: {
display: true,
text: '24小时请求统计'
}
}
}
});
}
// 更新图表数据
function updateChart(data) {
if (!requestsChart) {
initChart();
}
// 按小时统计请求数量
const hourlyStats = new Map();
const now = new Date();
const past24Hours = new Date(now - 24 * 60 * 60 * 1000);
// 初始化24小时的时间段
for (let i = 0; i < 24; i++) {
const hour = new Date(now - i * 60 * 60 * 1000);
const hourKey = hour.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: 'numeric'
});
hourlyStats.set(hourKey, 0);
}
// 统计每小时的请求数
data.logs.forEach(log => {
const logTime = new Date(log.timestamp);
if (logTime >= past24Hours) {
const hourKey = logTime.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: 'numeric'
});
if (hourlyStats.has(hourKey)) {
hourlyStats.set(hourKey, hourlyStats.get(hourKey) + 1);
}
}
});
// 转换为图表数据
const sortedHours = Array.from(hourlyStats.keys()).reverse();
const counts = sortedHours.map(hour => hourlyStats.get(hour));
requestsChart.data.labels = sortedHours;
requestsChart.data.datasets[0].data = counts;
requestsChart.update();
}
function updateTable(data) {
const tbody = document.getElementById('logsBody');
updateStats(data);
updateChart(data);
tbody.innerHTML = data.logs.map(log => {
// 预处理延迟数据以避免HTML解析问题
let delaysData = '';
if (log.chain && log.chain.delays) {
// 为每个delay项创建安全的文本和数值对
const safeDelays = log.chain.delays.map(item => {
if (!Array.isArray(item) || item.length < 2) return ["", 0];
// 将延迟数据中的文本部分编码为Base64避免HTML解析问题
const textPart = typeof item[0] === 'string' ? btoa(encodeURIComponent(item[0])) : "";
const delayPart = typeof item[1] === 'number' ? item[1] : 0;
return [textPart, delayPart];
});
delaysData = JSON.stringify(safeDelays);
}
return `<tr>
<td>${log.id}</td>
<td>${new Date(log.timestamp).toLocaleString()}</td>
<td>${log.model}</td>
<td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td>
<td>${log.chain ? `<div class="token-info-tooltip prompt-preview"><button class="info-button view-conversation" data-prompt="${encodeURIComponent(log.chain.prompt)}" data-delays='${delaysData}'>查看对话<div class="tooltip-content">${formatDialogPreview(log.chain.prompt)}</div></button></div>` : '-'}</td>
<td>${formatTiming(log.timing.total)}</td>
<td>${log.stream ? '是' : '否'}</td>
<td>${log.status}</td>
<td>${log.error || '-'}</td>
</tr>`;
}).join('');
// 添加事件监听器
tbody.querySelectorAll('.view-conversation').forEach(button => {
button.addEventListener('click', function () {
const prompt = decodeURIComponent(this.dataset.prompt);
const delays = this.dataset.delays ? JSON.parse(this.dataset.delays) : [];
showConversationModal(prompt, delays);
});
});
}
function formatTiming(total) {
return `${total.toFixed(2)}s`;
}
function formatDialogPreview(promptStr) {
try {
const messages = parsePrompt(promptStr);
if (!messages || messages.length === 0) {
return '无对话内容';
}
// 获取最后一条消息
const lastMessage = messages[messages.length - 1];
const roleLabels = {
'system': '系统',
'user': '用户',
'assistant': '助手'
};
return `<div class="message-meta">最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):</div><div class="last-message">${lastMessage.content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')}</div>`;
} catch (e) {
console.error('预览对话内容失败:', e);
return '无法解析对话内容';
}
}
async function fetchLogs() {
const data = await makeAuthenticatedRequest('/logs');
if (data) {
updateTable(data);
showGlobalMessage('日志获取成功');
}
}
// 自动刷新控制
document.getElementById('autoRefresh').addEventListener('change', function (e) {
if (e.target.checked) {
refreshInterval = setInterval(fetchLogs, 60000);
} else {
clearInterval(refreshInterval);
}
});
// 页面加载完成后自动获取日志
document.addEventListener('DOMContentLoaded', () => {
const authToken = getAuthToken();
if (authToken) {
document.getElementById('authToken').value = authToken;
fetchLogs();
}
// 启动自动刷新
refreshInterval = setInterval(fetchLogs, 60000);
});
// 初始化 token 处理
initializeTokenHandling('authToken');
// 添加清理逻辑
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
// 添加模态框关闭逻辑
document.querySelectorAll('.modal .close').forEach(closeBtn => {
closeBtn.onclick = function () {
this.closest('.modal').style.display = 'none';
}
});
window.onclick = function (event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
/**
* 解析对话内容
* @param {string} promptStr - 原始prompt字符串
* @returns {Array<{role: string, content: string}>} 解析后的对话数组
*/
function parsePrompt(promptStr) {
if (!promptStr) return [];
const messages = [];
const lines = promptStr.split('\n');
let currentRole = '';
let currentContent = '';
const roleMap = {
'BEGIN_SYSTEM': 'system',
'BEGIN_USER': 'user',
'BEGIN_ASSISTANT': 'assistant'
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检查是否是角色标记行
let foundRole = false;
for (const [marker, role] of Object.entries(roleMap)) {
if (line.includes(marker)) {
// 保存之前的消息(如果有)
if (currentRole && currentContent.trim()) {
messages.push({
role: currentRole,
content: currentContent.trim()
});
}
// 设置新角色
currentRole = role;
currentContent = '';
foundRole = true;
break;
}
}
// 如果不是角色标记行且不是END标记行则添加到当前内容
if (!foundRole && !line.includes('END_')) {
currentContent += line + '\n';
}
}
// 添加最后一条消息
if (currentRole && currentContent.trim()) {
messages.push({
role: currentRole,
content: currentContent.trim()
});
}
return messages;
}
/**
* 格式化对话内容为HTML表格
* @param {Array<{role: string, content: string}>} messages - 对话消息数组
* @returns {string} HTML表格字符串
*/
function formatPromptToTable(messages) {
if (!messages || messages.length === 0) {
return '<p>无对话内容</p>';
}
const roleLabels = {
'system': '系统',
'user': '用户',
'assistant': '助手'
};
function escapeHtml(content) {
// 先转义HTML特殊字符
const escaped = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// 将HTML标签文本用引号包裹使其更易读
// return escaped.replace(/&lt;(\/?[^>]+)&gt;/g, '"<$1>"');
return escaped;
}
return `<table class="message-table"><thead><tr><th>角色</th><th>内容</th></tr></thead><tbody>${messages.map(msg => `<tr><td>${roleLabels[msg.role] || msg.role}</td><td>${escapeHtml(msg.content).replace(/\n/g, '<br>')}</td></tr>`).join('')}</tbody></table>`;
}
/**
* 显示对话详情弹窗
* @param {string} promptStr - 对话提示字符串
* @param {Array} delays - 延迟数据数组
*/
function showConversationModal(promptStr, delays) {
try {
const modal = document.getElementById('conversationModal');
const dialogContent = document.getElementById('dialogContent');
const delaysContent = document.getElementById('delaysContent');
const tabPrompt = document.getElementById('tab-prompt');
const tabDelays = document.getElementById('tab-delays');
if (!modal || !dialogContent || !delaysContent || !tabPrompt || !tabDelays) {
console.error('Modal elements not found');
return;
}
// 显示对话内容
const messages = parsePrompt(promptStr);
dialogContent.innerHTML = formatPromptToTable(messages);
// 处理延迟数据
if (delays && delays.length > 0) {
const delaysTableBody = document.querySelector('#delaysTable tbody');
delaysTableBody.innerHTML = '';
let totalChars = 0;
let totalTime = 0;
// 解码并显示延迟数据
delays.forEach(([encodedText, delay], index) => {
try {
const text = encodedText ? decodeURIComponent(atob(encodedText)) : '';
totalChars += text.length;
totalTime += delay;
const rate = text.length / delay;
const avgRate = totalChars / totalTime;
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td>${text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')}</td>
<td>${delay.toFixed(3)}</td>
<td>${rate.toFixed(1)} (平均: ${avgRate.toFixed(1)})</td>
`;
delaysTableBody.appendChild(row);
} catch (e) {
console.error('处理延迟数据项失败:', e);
}
});
// 初始化延迟图表
initDelayChart(delays);
} else {
document.querySelector('#delaysTable tbody').innerHTML = '<tr><td colspan="2">无延迟数据</td></tr>';
document.querySelector('.delay-chart-container').innerHTML = '<div style="text-align: center; padding: 20px;">无延迟数据可供分析</div>';
}
// 设置标签切换事件
tabPrompt.onclick = () => setActiveTab('prompt');
tabDelays.onclick = () => setActiveTab('delays');
// 设置默认激活的标签页
setActiveTab('prompt');
modal.style.display = 'block';
} catch (e) {
console.error('显示对话详情失败:', e);
console.error('原始prompt:', promptStr);
}
}
/**
* 设置当前活动标签页
* @param {string} tabName - 标签页名称 ('prompt' 或 'delays')
*/
function setActiveTab(tabName) {
const dialogContent = document.getElementById('dialogContent');
const delaysContent = document.getElementById('delaysContent');
const tabPrompt = document.getElementById('tab-prompt');
const tabDelays = document.getElementById('tab-delays');
if (!dialogContent || !delaysContent || !tabPrompt || !tabDelays) {
console.error('Tab elements not found');
return;
}
if (tabName === 'prompt') {
dialogContent.classList.add('active');
delaysContent.classList.remove('active');
tabPrompt.classList.add('active');
tabDelays.classList.remove('active');
} else {
dialogContent.classList.remove('active');
delaysContent.classList.add('active');
tabPrompt.classList.remove('active');
tabDelays.classList.add('active');
}
}
/**
* 初始化延迟图表
* @param {Array} delays - 延迟数组
*/
function initDelayChart(delays) {
if (!delays || delays.length <= 1) {
return;
}
const ctx = document.getElementById('delayChart');
if (!ctx) {
console.error('找不到图表canvas元素');
return;
}
// 销毁之前的图表(如果存在)
const existingChart = Chart.getChart(ctx);
if (existingChart) {
existingChart.destroy();
}
// 计算字符数和累计时间
const rawDataPoints = [];
let totalChars = 0;
let accumulatedTime = 0;
// 解密所有数据点并计算累计时间
for (let i = 0; i < delays.length; i++) {
const [encodedText, delay] = delays[i];
if (encodedText && typeof delay === 'number') {
try {
const text = decodeURIComponent(atob(encodedText));
totalChars += text.length;
accumulatedTime += delay; // 累加延迟时间
rawDataPoints.push({
time: accumulatedTime, // 使用累计时间
chars: totalChars,
text: text
});
} catch (e) {
console.error('解码延迟数据失败:', e);
}
}
}
// 优化数据点密度
const maxPoints = 10;
let dataPoints = [];
if (rawDataPoints.length > maxPoints) {
// 计算采样间隔
const interval = Math.floor(rawDataPoints.length / maxPoints);
// 确保包含第一个点
dataPoints.push(rawDataPoints[0]);
// 采样中间点,使用更大的间隔
for (let i = interval; i < rawDataPoints.length - interval; i += interval) {
// 在每个采样点周围计算平均值,使曲线更平滑
const start = Math.max(0, i - Math.floor(interval / 2));
const end = Math.min(rawDataPoints.length, i + Math.floor(interval / 2));
const avgPoint = rawDataPoints[i];
dataPoints.push(avgPoint);
}
// 确保包含最后一个点
if (dataPoints[dataPoints.length - 1] !== rawDataPoints[rawDataPoints.length - 1]) {
dataPoints.push(rawDataPoints[rawDataPoints.length - 1]);
}
} else {
dataPoints = rawDataPoints;
}
// 创建新图表
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: '累计输出字符数',
data: dataPoints.map(point => ({
x: point.time,
y: point.chars
})),
borderColor: 'rgb(75,192,192)',
backgroundColor: 'rgba(75,192,192,0.2)',
tension: 0.1,
fill: true,
pointRadius: 3,
pointHoverRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '字符输出进度'
},
tooltip: {
callbacks: {
title: function (context) {
return `用时: ${context[0].raw.x.toFixed(1)}`;
},
label: function (context) {
const point = context.raw;
const rate = point.x > 0 ? (point.y / point.x).toFixed(1) : 0;
return [
`字符数: ${point.y}`,
`平均速率: ${rate} 字符/秒`,
`文本: ${dataPoints[context.dataIndex].text}`
];
}
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '字符数'
}
},
x: {
type: 'linear',
title: {
display: true,
text: '用时(秒)'
},
ticks: {
callback: function (value) {
return value.toFixed(1) + 's';
}
}
}
}
}
});
}
</script>
</body>
</html>