| <!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> |
| |
| <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> |
| |
| <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> |
| |
| <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 = "状态未知"; |
| } |
| } |
| |
| |
| 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) { |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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> |
|
|