message / plugins /json /frontend /index.html
hunian
refactor(plugins): 插件短名并统一 MCP tool 为 {plugin}-{tool}
cc826a1
Raw
History Blame Contribute Delete
10.9 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSON 对话查看器</title>
<link rel="stylesheet" href="/static/plugin-ui.css">
<style>
.conversation-list { display: flex; flex-direction: column; gap: 12px; }
.message-bubble { padding: 12px; border-radius: 8px; max-width: 85%; }
.message-role { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
.message-content { white-space: pre-wrap; }
.message-user { background: var(--accent-bg, #e3f2fd); align-self: flex-end; }
.message-assistant { background: var(--bg-muted, #f5f5f5); align-self: flex-start; }
.message-system { background: var(--warning-bg, #fff3e0); align-self: center; font-size: 13px; }
.tool-call { background: var(--code-bg, #f8f8f8); border: 1px solid var(--border); padding: 8px; border-radius: 4px; margin-top: 8px; font-size: 13px; }
.tool-call-header { font-weight: 600; cursor: pointer; }
.tool-call-body { display: none; margin-top: 8px; }
.tool-call-body.expanded { display: block; }
.tabs { display: flex; gap: 4px; margin-bottom: 8px; }
.tab { padding: 6px 12px; border-radius: 4px 4px 0 0; cursor: pointer; background: var(--bg-muted); color: var(--text-muted); border: 1px solid var(--border); border-bottom: none; }
.tab.active { background: var(--bg); color: var(--text); font-weight: 600; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.warnings { background: var(--warning-bg, #fff3e0); padding: 8px; border-radius: 4px; margin-bottom: 12px; font-size: 13px; }
</style>
</head>
<body>
<main class="plugin-shell">
<header class="plugin-header">
<div>
<h1 class="plugin-title">JSON 对话查看器</h1>
<p class="plugin-subtitle">上传或粘贴 OpenAI 格式的对话 JSON,可视化查看对话记录。</p>
</div>
<span id="statusPill" class="plugin-status">检查中</span>
</header>
<div id="message" class="plugin-message"></div>
<section class="plugin-grid">
<div class="plugin-panel">
<h2 class="plugin-panel-title">输入</h2>
<div class="plugin-stack">
<!-- 文件上传 -->
<div class="plugin-field">
<label class="plugin-label">上传 JSON/JSONL 文件</label>
<input id="file" type="file" accept=".json,.jsonl">
</div>
<!-- 粘贴 JSON -->
<div class="plugin-field">
<label class="plugin-label" for="jsonInput">或粘贴 JSON</label>
<textarea id="jsonInput" rows="6" placeholder='{"messages":[{"role":"user","content":"你好"},{"role":"assistant","content":"你好!"}]}'></textarea>
</div>
<button id="parseBtn" class="plugin-button" onclick="parseConversation()">解析对话</button>
</div>
</div>
<!-- 对话视图 -->
<div id="conversationPanel" class="plugin-panel" style="display:none;">
<h2 class="plugin-panel-title">对话视图</h2>
<!-- Tabs -->
<div class="tabs">
<div class="tab active" data-tab="conversation" onclick="switchTab('conversation')">对话</div>
<div class="tab" data-tab="raw" onclick="switchTab('raw')">原始 JSON</div>
</div>
<!-- 警告 -->
<div id="warnings" class="warnings" style="display:none;"></div>
<!-- Tab 内容 -->
<div id="tab-conversation" class="tab-content active">
<div id="conversationList" class="conversation-list"></div>
</div>
<div id="tab-raw" class="tab-content">
<pre id="rawOutput" class="plugin-output"></pre>
</div>
</div>
</section>
</main>
<script src="/static/plugin-ui.js"></script>
<script>
const API = "/plugins/json/api";
let lastResult = null;
async function loadStatus() {
try {
const data = await PluginUI.request(`${API}/status`);
const pill = document.getElementById("statusPill");
pill.textContent = data.enabled === false ? "插件未启用" : "可用";
pill.className = `plugin-status ${data.enabled === false ? "warn" : "ok"}`;
} catch {
document.getElementById("statusPill").textContent = "状态未知";
}
}
// 切换 tab
function switchTab(tabName) {
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active"));
document.querySelector(`[data-tab="${tabName}"]`).classList.add("active");
document.getElementById(`tab-${tabName}`).classList.add("active");
}
// 解析对话
async function parseConversation() {
const btn = document.getElementById("parseBtn");
btn.disabled = true;
PluginUI.clearMessage("message");
try {
const fileInput = document.getElementById("file");
const jsonInput = document.getElementById("jsonInput").value.trim();
let result;
if (fileInput.files.length > 0) {
// 上传文件
const form = new FormData();
form.append("file", fileInput.files[0]);
result = await PluginUI.request(`${API}/upload`, { method: "POST", body: form });
} else if (jsonInput) {
// 粘贴 JSON
const jsonData = JSON.parse(jsonInput);
result = await PluginUI.request(`${API}/conversation/parse`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(jsonData),
});
} else {
throw new Error("请上传文件或粘贴 JSON");
}
lastResult = result;
renderConversation(result);
document.getElementById("conversationPanel").style.display = "block";
PluginUI.showMessage("message", `解析成功: ${result.messages?.length || 0} 条消息`, "success");
} catch (error) {
PluginUI.showMessage("message", error.message, "error");
} finally {
btn.disabled = false;
}
}
// 渲染对话
function renderConversation(result) {
// 显示警告
const warningsEl = document.getElementById("warnings");
if (result.raw_warnings && result.raw_warnings.length > 0) {
warningsEl.textContent = "警告: " + result.raw_warnings.join("; ");
warningsEl.style.display = "block";
} else {
warningsEl.style.display = "none";
}
// 渲染对话列表
const list = document.getElementById("conversationList");
list.innerHTML = "";
if (!result.messages || result.messages.length === 0) {
list.innerHTML = '<div class="plugin-muted">无消息</div>';
return;
}
for (const msg of result.messages) {
const bubble = document.createElement("div");
bubble.className = `message-bubble message-${msg.role}`;
// 角色标签
const roleEl = document.createElement("div");
roleEl.className = "message-role";
roleEl.textContent = {
"system": "系统",
"user": "用户",
"assistant": "助手",
}[msg.role] || msg.role;
bubble.appendChild(roleEl);
// 内容
if (msg.content_parts && msg.content_parts.length > 0) {
for (const part of msg.content_parts) {
if (part.type === "text") {
const contentEl = document.createElement("div");
contentEl.className = "message-content";
contentEl.textContent = part.text;
bubble.appendChild(contentEl);
} else if (part.type === "thinking") {
const thinkingEl = document.createElement("details");
thinkingEl.innerHTML = `<summary>思考过程</summary><div class="message-content">${part.text}</div>`;
bubble.appendChild(thinkingEl);
} else if (part.type === "tool_use") {
const toolEl = renderToolCall(part);
bubble.appendChild(toolEl);
} else if (part.type === "tool_result") {
const resultEl = renderToolResult(part);
bubble.appendChild(resultEl);
}
}
}
// tool_calls
if (msg.tool_calls && msg.tool_calls.length > 0) {
for (const tc of msg.tool_calls) {
const toolEl = document.createElement("div");
toolEl.className = "tool-call";
toolEl.innerHTML = `
<div class="tool-call-header" onclick="this.nextElementSibling.classList.toggle('expanded')">
🔧 调用工具: ${tc.function_name}
</div>
<div class="tool-call-body">
<pre>${JSON.stringify(tc.function_arguments, null, 2)}</pre>
</div>
`;
bubble.appendChild(toolEl);
}
}
// tool_results
if (msg.tool_results && msg.tool_results.length > 0) {
for (const tr of msg.tool_results) {
const resultEl = document.createElement("div");
resultEl.className = "tool-call";
resultEl.innerHTML = `
<div class="tool-call-header" onclick="this.nextElementSibling.classList.toggle('expanded')">
📋 工具结果 ${tr.is_error ? '(错误)' : ''}
</div>
<div class="tool-call-body">
<pre>${typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content, null, 2)}</pre>
</div>
`;
bubble.appendChild(resultEl);
}
}
list.appendChild(bubble);
}
// 渲染原始 JSON
document.getElementById("rawOutput").textContent = JSON.stringify(result, null, 2);
}
// 渲染工具调用
function renderToolCall(part) {
const el = document.createElement("div");
el.className = "tool-call";
el.innerHTML = `
<div class="tool-call-header" onclick="this.nextElementSibling.classList.toggle('expanded')">
🔧 调用工具: ${part.tool_name}
</div>
<div class="tool-call-body">
<pre>${JSON.stringify(part.tool_input, null, 2)}</pre>
</div>
`;
return el;
}
// 渲染工具结果
function renderToolResult(part) {
const el = document.createElement("div");
el.className = "tool-call";
el.innerHTML = `
<div class="tool-call-header" onclick="this.nextElementSibling.classList.toggle('expanded')">
📋 工具结果 ${part.is_error ? '(错误)' : ''}
</div>
<div class="tool-call-body">
<pre>${typeof part.content === 'string' ? part.content : JSON.stringify(part.content, null, 2)}</pre>
</div>
`;
return el;
}
loadStatus();
</script>
</body>
</html>