| <!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>API 测试工具</title> |
| <link rel="stylesheet" href="/static/shared-styles.css"> |
| <script src="/static/shared.js"></script> |
| <style> |
| |
| .header-toolbar { |
| display: flex; |
| align-items: center; |
| gap: 20px; |
| margin-bottom: 20px; |
| flex-wrap: wrap; |
| } |
| |
| .auth-section { |
| flex: 1; |
| min-width: 300px; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .auth-section input { |
| flex: 1; |
| } |
| |
| .server-status { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 14px; |
| } |
| |
| .status-indicator { |
| width: 8px; |
| height: 8px; |
| border-radius: 50%; |
| background: var(--text-secondary); |
| } |
| |
| .status-indicator.healthy { |
| background: var(--success-color); |
| animation: pulse 2s infinite; |
| } |
| |
| .status-indicator.error { |
| background: var(--error-color); |
| } |
| |
| @keyframes pulse { |
| |
| 0%, |
| 100% { |
| opacity: 1; |
| } |
| |
| 50% { |
| opacity: 0.5; |
| } |
| } |
| |
| .quick-tools { |
| display: flex; |
| gap: 8px; |
| } |
| |
| .quick-tools button { |
| padding: 6px 12px; |
| font-size: 14px; |
| min-height: 32px; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| border-bottom: 2px solid var(--border-color); |
| margin-bottom: 20px; |
| } |
| |
| .tab { |
| padding: 12px 24px; |
| background: none; |
| border: none; |
| color: var(--text-secondary); |
| cursor: pointer; |
| font-size: 16px; |
| transition: all var(--transition-fast); |
| position: relative; |
| } |
| |
| .tab:hover { |
| color: var(--text-primary); |
| background: var(--primary-color-alpha); |
| } |
| |
| .tab.active { |
| color: var(--text-primary); |
| font-weight: 600; |
| background: var(--primary-color-alpha); |
| } |
| |
| .tab.active::after { |
| content: ''; |
| position: absolute; |
| bottom: -2px; |
| left: 0; |
| right: 0; |
| height: 3px; |
| background: var(--primary-color); |
| } |
| |
| .tab-content { |
| display: none; |
| } |
| |
| .tab-content.active { |
| display: block; |
| } |
| |
| |
| .chat-container { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px; |
| height: 600px; |
| } |
| |
| .chat-container.playground-mode { |
| grid-template-columns: 1fr; |
| } |
| |
| .chat-container.playground-mode .chat-right { |
| display: none; |
| } |
| |
| .chat-left, |
| .chat-right { |
| display: flex; |
| flex-direction: column; |
| background: var(--card-background); |
| border: 1px solid var(--border-color); |
| border-radius: var(--border-radius); |
| padding: 20px; |
| overflow: hidden; |
| } |
| |
| .mode-switcher { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 20px; |
| } |
| |
| .mode-switcher button { |
| padding: 6px 16px; |
| font-size: 14px; |
| min-height: 32px; |
| } |
| |
| |
| .raw-request { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .model-selector { |
| position: relative; |
| margin-bottom: 16px; |
| } |
| |
| .model-input { |
| width: 100%; |
| padding-right: 30px; |
| } |
| |
| .model-dropdown { |
| position: absolute; |
| top: 100%; |
| left: 0; |
| right: 0; |
| max-height: 200px; |
| overflow-y: auto; |
| background: var(--card-background); |
| border: 1px solid var(--border-color); |
| border-top: none; |
| border-radius: 0 0 4px 4px; |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
| z-index: 100; |
| display: none; |
| } |
| |
| .model-dropdown.show { |
| display: block; |
| } |
| |
| .model-option { |
| padding: 8px 12px; |
| cursor: pointer; |
| transition: background var(--transition-fast); |
| } |
| |
| .model-option:hover { |
| background: var(--primary-color-alpha); |
| } |
| |
| .model-option.selected { |
| background: var(--primary-color); |
| color: white; |
| } |
| |
| .json-editor { |
| flex: 1; |
| font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; |
| font-size: 14px; |
| line-height: 1.5; |
| tab-size: 2; |
| white-space: pre; |
| overflow: auto; |
| resize: none; |
| transition: border-color var(--transition-fast), background-color var(--transition-fast); |
| } |
| |
| .json-editor.valid { |
| border-color: var(--success-color); |
| background-color: rgba(40, 167, 69, 0.05); |
| } |
| |
| .json-editor.invalid { |
| border-color: var(--error-color); |
| background-color: rgba(220, 53, 69, 0.05); |
| } |
| |
| .request-options { |
| display: flex; |
| align-items: center; |
| gap: 20px; |
| margin-top: 16px; |
| } |
| |
| .stream-toggle { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .add-response-btn { |
| padding: 4px 12px; |
| font-size: 12px; |
| min-height: 24px; |
| margin-left: auto; |
| } |
| |
| |
| .playground-container { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| height: 100%; |
| overflow: hidden; |
| } |
| |
| .system-prompt { |
| margin-bottom: 16px; |
| flex-shrink: 0; |
| } |
| |
| .system-prompt textarea { |
| min-height: 80px; |
| font-size: 14px; |
| } |
| |
| .conversation-history { |
| flex: 1; |
| overflow-y: auto; |
| border: 1px solid var(--border-color); |
| border-radius: 4px; |
| padding: 16px; |
| margin-bottom: 16px; |
| background: var(--background-color); |
| min-height: 0; |
| } |
| |
| .message { |
| margin-bottom: 16px; |
| padding: 12px 16px; |
| border-radius: 8px; |
| animation: none; |
| width: fit-content; |
| max-width: 70%; |
| min-width: 80px; |
| word-wrap: break-word; |
| word-break: break-word; |
| overflow-wrap: break-word; |
| } |
| |
| .message.user { |
| background: var(--primary-color-alpha); |
| margin-left: auto; |
| margin-right: 0; |
| } |
| |
| .message.assistant { |
| background: var(--card-background); |
| border: 1px solid var(--border-color); |
| margin-left: 0; |
| margin-right: auto; |
| } |
| |
| |
| .chat-container.playground-mode .message { |
| max-width: 80%; |
| min-width: 60px; |
| } |
| |
| .chat-container.playground-mode .message.user { |
| margin-left: auto; |
| margin-right: 0; |
| } |
| |
| .chat-container.playground-mode .message.assistant { |
| margin-left: 0; |
| margin-right: auto; |
| } |
| |
| .message-role { |
| font-weight: 500; |
| margin-bottom: 4px; |
| font-size: 14px; |
| color: var(--text-secondary); |
| } |
| |
| .message-content { |
| line-height: 1.6; |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| word-break: break-word; |
| overflow-wrap: break-word; |
| } |
| |
| .user-input-area { |
| display: flex; |
| gap: 10px; |
| flex-shrink: 0; |
| } |
| |
| .user-input { |
| flex: 1; |
| min-height: 60px; |
| max-height: 150px; |
| resize: vertical; |
| } |
| |
| |
| .response-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 16px; |
| padding-bottom: 12px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .response-status { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| font-size: 14px; |
| } |
| |
| .status-code { |
| padding: 2px 8px; |
| border-radius: 4px; |
| font-weight: 500; |
| } |
| |
| .status-code.success { |
| background: var(--success-color); |
| color: white; |
| } |
| |
| .status-code.error { |
| background: var(--error-color); |
| color: white; |
| } |
| |
| .response-time { |
| color: var(--text-secondary); |
| } |
| |
| .response-body { |
| flex: 1; |
| overflow-y: auto; |
| font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; |
| font-size: 14px; |
| line-height: 1.5; |
| white-space: pre-wrap; |
| word-break: break-all; |
| padding: 16px; |
| background: var(--background-color); |
| border-radius: 4px; |
| } |
| |
| .stream-response { |
| padding: 16px; |
| background: var(--background-color); |
| border-radius: 4px; |
| min-height: 100px; |
| } |
| |
| |
| .models-container { |
| max-width: 800px; |
| margin: 0 auto; |
| } |
| |
| .models-params { |
| background: var(--card-background); |
| padding: 20px; |
| border-radius: var(--border-radius); |
| border: 1px solid var(--border-color); |
| margin-bottom: 20px; |
| } |
| |
| .update-data-section { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| padding: 12px; |
| background: var(--background-color); |
| border-radius: 4px; |
| margin-bottom: 16px; |
| } |
| |
| .update-data-section label { |
| font-weight: 500; |
| margin: 0; |
| } |
| |
| .params-section { |
| opacity: 0.5; |
| pointer-events: none; |
| transition: opacity var(--transition-fast); |
| } |
| |
| .params-section.enabled { |
| opacity: 1; |
| pointer-events: auto; |
| } |
| |
| .params-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| gap: 16px; |
| margin-bottom: 20px; |
| } |
| |
| .param-item { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .additional-models { |
| margin-top: 16px; |
| } |
| |
| .models-table { |
| width: 100%; |
| background: var(--card-background); |
| border-radius: var(--border-radius); |
| overflow: hidden; |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| } |
| |
| .models-table th { |
| background: var(--primary-color); |
| color: white; |
| padding: 12px; |
| text-align: left; |
| font-weight: 500; |
| position: sticky; |
| top: 0; |
| } |
| |
| .models-table td { |
| padding: 12px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .models-table tr:hover { |
| background: var(--primary-color-alpha); |
| } |
| |
| .model-features { |
| display: flex; |
| gap: 8px; |
| } |
| |
| .feature-badge { |
| padding: 2px 8px; |
| border-radius: 4px; |
| font-size: 12px; |
| background: var(--primary-color-alpha); |
| color: var(--primary-color); |
| } |
| |
| |
| .search-box { |
| margin-bottom: 16px; |
| } |
| |
| .search-box input { |
| width: 100%; |
| padding: 10px 16px; |
| font-size: 14px; |
| } |
| |
| |
| .status-bar { |
| position: fixed; |
| bottom: 0; |
| left: 0; |
| right: 0; |
| background: var(--card-background); |
| border-top: 1px solid var(--border-color); |
| padding: 8px 20px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| font-size: 12px; |
| color: var(--text-secondary); |
| } |
| |
| .status-info { |
| display: flex; |
| gap: 20px; |
| } |
| |
| |
| @media (max-width: 968px) { |
| .chat-container { |
| grid-template-columns: 1fr; |
| height: auto; |
| } |
| |
| .chat-left, |
| .chat-right { |
| min-height: 400px; |
| } |
| |
| .message { |
| min-width: 50px; |
| max-width: 90%; |
| } |
| |
| .message.user { |
| margin-left: auto; |
| margin-right: 10px; |
| } |
| |
| .message.assistant { |
| margin-left: 10px; |
| margin-right: auto; |
| } |
| |
| .header-toolbar { |
| flex-direction: column; |
| align-items: stretch; |
| } |
| |
| .auth-section { |
| min-width: 100%; |
| } |
| |
| .status-bar { |
| position: relative; |
| flex-direction: column; |
| gap: 8px; |
| text-align: center; |
| } |
| } |
| |
| |
| .loading { |
| display: inline-block; |
| width: 20px; |
| height: 20px; |
| border: 3px solid var(--border-color); |
| border-radius: 50%; |
| border-top-color: var(--primary-color); |
| animation: spin 1s ease-in-out infinite; |
| } |
| |
| @keyframes spin { |
| to { |
| transform: rotate(360deg); |
| } |
| } |
| |
| |
| .tooltip { |
| position: relative; |
| cursor: help; |
| } |
| |
| .tooltip::after { |
| content: attr(data-tooltip); |
| position: absolute; |
| bottom: 100%; |
| left: 50%; |
| transform: translateX(-50%); |
| padding: 4px 8px; |
| background: var(--text-primary); |
| color: var(--card-background); |
| font-size: 12px; |
| border-radius: 4px; |
| white-space: nowrap; |
| opacity: 0; |
| pointer-events: none; |
| transition: opacity var(--transition-fast); |
| } |
| |
| .tooltip:hover::after { |
| opacity: 1; |
| } |
| |
| |
| .copy-btn { |
| position: absolute; |
| top: 8px; |
| right: 8px; |
| padding: 4px 8px; |
| font-size: 12px; |
| min-height: 24px; |
| opacity: 0; |
| transition: opacity var(--transition-fast); |
| } |
| |
| .chat-right:hover .copy-btn, |
| .response-body:hover .copy-btn { |
| opacity: 1; |
| } |
| |
| |
| .token-stats { |
| padding: 8px 12px; |
| background: var(--background-color); |
| border-radius: 4px; |
| font-size: 14px; |
| display: flex; |
| gap: 16px; |
| } |
| |
| .token-stat { |
| display: flex; |
| align-items: center; |
| gap: 4px; |
| } |
| |
| .token-stat span { |
| font-weight: 500; |
| color: var(--primary-color); |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div class="container"> |
| <div class="header-toolbar"> |
| <div class="auth-section"> |
| <label for="authToken" style="margin: 0;">AUTH Token</label> |
| <input type="password" id="authToken" placeholder="请输入 AUTH Token"> |
| </div> |
|
|
| <div class="server-status"> |
| <span class="status-indicator" id="statusIndicator"></span> |
| <span id="statusText">检查中...</span> |
| </div> |
|
|
| <div class="quick-tools"> |
| <button onclick="generateHash()" class="secondary">生成 Hash</button> |
| <button onclick="generateChecksum()" class="secondary">生成 Checksum</button> |
| <button onclick="generateTimestampHeader()" class="secondary">生成 TimestampHeader</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="container"> |
| |
| <div class="tabs"> |
| <button class="tab active" onclick="switchTab('chat')">Chat Completions</button> |
| <button class="tab" onclick="switchTab('models')">Models</button> |
| </div> |
|
|
| |
| <div id="chatTab" class="tab-content active"> |
| <div class="mode-switcher"> |
| <button class="active" onclick="switchChatMode('raw')">Raw 模式</button> |
| <button onclick="switchChatMode('playground')">Playground 模式</button> |
| </div> |
|
|
| <div class="chat-container" id="chatContainer"> |
| |
| <div class="chat-left"> |
| |
| <div id="rawMode" class="raw-request"> |
| <div class="model-selector"> |
| <label for="modelInput">模型</label> |
| <input type="text" id="modelInput" class="model-input" placeholder="输入或选择模型..." autocomplete="off"> |
| <div id="modelDropdown" class="model-dropdown"></div> |
| </div> |
|
|
| <label for="requestBody">请求体 (JSON)</label> |
| <textarea id="requestBody" class="json-editor" placeholder='{ |
| "messages": [ |
| { |
| "role": "user", |
| "content": "Hello!" |
| } |
| ] |
| }'></textarea> |
|
|
| <div class="request-options"> |
| <div class="stream-toggle"> |
| <label for="streamMode">响应模式</label> |
| <select id="streamMode"> |
| <option value="normal">普通响应</option> |
| <option value="stream">流式响应</option> |
| <option value="stream-usage">流式响应 + Usage</option> |
| </select> |
| </div> |
| <button onclick="sendRequest()" id="sendBtn"> |
| 发送请求 |
| <span class="tooltip" data-tooltip="Ctrl + Enter">⌨</span> |
| </button> |
| <button class="add-response-btn secondary" onclick="addResponseToRequest()" id="addResponseBtn" |
| style="display: none;"> |
| 添加响应到请求 |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="playgroundMode" class="playground-container" style="display: none;"> |
| <div class="system-prompt"> |
| <label for="systemPrompt">系统提示词</label> |
| <textarea id="systemPrompt" placeholder="你是一个有用的助手..."></textarea> |
| </div> |
|
|
| <div class="conversation-history" id="conversationHistory"> |
| <div style="text-align: center; color: var(--text-secondary);"> |
| 对话历史将在这里显示 |
| </div> |
| </div> |
|
|
| <div class="user-input-area"> |
| <textarea id="userInput" class="user-input" placeholder="输入你的消息..." |
| onkeydown="handleUserInputKeydown(event)"></textarea> |
| <button onclick="sendPlaygroundMessage()" id="playgroundSendBtn">发送</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="chat-right"> |
| <div class="response-header"> |
| <div class="response-status"> |
| <span id="responseStatus">等待请求...</span> |
| <span class="response-time" id="responseTime"></span> |
| </div> |
| <button class="copy-btn secondary" onclick="copyResponse()">复制</button> |
| </div> |
|
|
| <div id="responseBody" class="response-body"> |
| <div style="text-align: center; color: var(--text-secondary); padding: 40px;"> |
| 响应内容将在这里显示 |
| </div> |
| </div> |
|
|
| <div id="tokenStats" class="token-stats" style="display: none;"> |
| <div class="token-stat"> |
| 提示词: <span id="promptTokens">0</span> |
| </div> |
| <div class="token-stat"> |
| 完成: <span id="completionTokens">0</span> |
| </div> |
| <div class="token-stat"> |
| 总计: <span id="totalTokens">0</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="modelsTab" class="tab-content"> |
| <div class="models-container"> |
| <div class="models-params"> |
| <div class="update-data-section"> |
| <input type="checkbox" id="updateData" onchange="toggleUpdateData()"> |
| <label for="updateData">更新数据(需要 AUTH Token)</label> |
| </div> |
|
|
| <div id="paramsSection" class="params-section"> |
| <h3>请求参数</h3> |
| <div class="params-grid"> |
| <div class="param-item"> |
| <input type="checkbox" id="isNightly"> |
| <label for="isNightly">包含 Nightly 版本</label> |
| </div> |
| <div class="param-item"> |
| <input type="checkbox" id="includeLongContext"> |
| <label for="includeLongContext">包含长上下文模型</label> |
| </div> |
| <div class="param-item"> |
| <input type="checkbox" id="excludeMaxNamed" checked> |
| <label for="excludeMaxNamed">排除 Max 命名模型</label> |
| </div> |
| </div> |
|
|
| <div class="additional-models"> |
| <label for="additionalModels">额外模型名称(逗号分隔)</label> |
| <input type="text" id="additionalModels" placeholder="model1, model2, model3"> |
| </div> |
| </div> |
|
|
| <div class="button-group" style="margin-top: 20px;"> |
| <button onclick="fetchModels()">获取模型列表</button> |
| </div> |
| </div> |
|
|
| <div class="search-box"> |
| <input type="text" id="modelSearch" placeholder="搜索模型..." oninput="filterModels()"> |
| </div> |
|
|
| <div id="modelsTableContainer"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="status-bar"> |
| <div class="status-info"> |
| <span id="versionInfo">版本: -</span> |
| <span id="uptimeInfo">运行时间: -</span> |
| <span id="lastRequestTime">最后请求: -</span> |
| </div> |
| <div> |
| <span id="requestCount">总请求数: 0</span> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let globalModels = []; |
| let currentMode = 'raw'; |
| let currentTab = 'chat'; |
| let conversationHistory = []; |
| let isStreaming = false; |
| let abortController = null; |
| let requestCount = 0; |
| let lastResponseData = null; |
| let responseAddedToRequest = false; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| initializeTokenHandling('authToken'); |
| |
| |
| checkServerHealth(); |
| setInterval(checkServerHealth, 300000); |
| |
| |
| initializeModelAutocomplete(); |
| |
| |
| bindShortcuts(); |
| |
| |
| restoreSessionData(); |
| |
| |
| initializeJsonValidation(); |
| }); |
| |
| |
| async function checkServerHealth() { |
| const indicator = document.getElementById('statusIndicator'); |
| const statusText = document.getElementById('statusText'); |
| |
| try { |
| const token = document.getElementById('authToken').value; |
| const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; |
| |
| |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 5000); |
| |
| const response = await fetch('/health', { |
| headers, |
| signal: controller.signal |
| }); |
| clearTimeout(timeoutId); |
| |
| const data = await response.json(); |
| |
| if (data.status === 'success') { |
| indicator.classList.add('healthy'); |
| indicator.classList.remove('error'); |
| statusText.textContent = 'Healthy'; |
| |
| |
| const version = data.service?.version ?? '未知'; |
| document.getElementById('versionInfo').textContent = `版本: ${version}`; |
| |
| const uptimeSeconds = data.runtime?.uptime_seconds; |
| if (typeof uptimeSeconds === 'number') { |
| document.getElementById('uptimeInfo').textContent = formatUptime(uptimeSeconds); |
| } else { |
| document.getElementById('uptimeInfo').textContent = '运行时间: 未知'; |
| } |
| |
| |
| globalModels = data.capabilities?.models || []; |
| updateModelDropdown(); |
| |
| |
| const totalRequests = data.runtime?.requests?.total; |
| if (typeof totalRequests === 'number') { |
| document.getElementById('requestCount').textContent = |
| `总请求数: ${totalRequests}`; |
| } else { |
| document.getElementById('requestCount').textContent = '总请求数: 未知'; |
| } |
| } else { |
| throw new Error('Server not healthy'); |
| } |
| } catch (error) { |
| indicator.classList.add('error'); |
| indicator.classList.remove('healthy'); |
| statusText.textContent = 'Error'; |
| console.error('Health check failed:', error); |
| } |
| } |
| |
| |
| function formatUptime(seconds) { |
| const days = Math.floor(seconds / 86400); |
| const hours = Math.floor((seconds % 86400) / 3600); |
| const minutes = Math.floor((seconds % 3600) / 60); |
| |
| if (days > 0) { |
| return `运行时间: ${days}天 ${hours}时 ${minutes}分`; |
| } else if (hours > 0) { |
| return `运行时间: ${hours}时 ${minutes}分`; |
| } else { |
| return `运行时间: ${minutes}分`; |
| } |
| } |
| |
| |
| function switchTab(tab) { |
| currentTab = tab; |
| |
| |
| document.querySelectorAll('.tab').forEach(t => { |
| t.classList.remove('active'); |
| }); |
| event.target.classList.add('active'); |
| |
| |
| document.querySelectorAll('.tab-content').forEach(content => { |
| content.classList.remove('active'); |
| }); |
| document.getElementById(`${tab}Tab`).classList.add('active'); |
| } |
| |
| |
| function switchChatMode(mode) { |
| currentMode = mode; |
| |
| |
| document.querySelectorAll('.mode-switcher button').forEach(btn => { |
| btn.classList.remove('active'); |
| }); |
| event.target.classList.add('active'); |
| |
| |
| const chatContainer = document.getElementById('chatContainer'); |
| if (mode === 'raw') { |
| document.getElementById('rawMode').style.display = 'flex'; |
| document.getElementById('playgroundMode').style.display = 'none'; |
| chatContainer.classList.remove('playground-mode'); |
| } else { |
| document.getElementById('rawMode').style.display = 'none'; |
| document.getElementById('playgroundMode').style.display = 'flex'; |
| chatContainer.classList.add('playground-mode'); |
| updateConversationDisplay(); |
| } |
| |
| |
| syncModeData(); |
| } |
| |
| |
| function initializeModelAutocomplete() { |
| const modelInput = document.getElementById('modelInput'); |
| const dropdown = document.getElementById('modelDropdown'); |
| |
| modelInput.addEventListener('input', (e) => { |
| const value = e.target.value.toLowerCase(); |
| updateModelDropdown(value); |
| }); |
| |
| modelInput.addEventListener('focus', () => { |
| updateModelDropdown(modelInput.value.toLowerCase()); |
| }); |
| |
| |
| document.addEventListener('click', (e) => { |
| if (!e.target.closest('.model-selector')) { |
| dropdown.classList.remove('show'); |
| } |
| }); |
| } |
| |
| |
| function updateModelDropdown(filter = '') { |
| const dropdown = document.getElementById('modelDropdown'); |
| dropdown.innerHTML = ''; |
| |
| const filteredModels = globalModels.filter(model => |
| model.toLowerCase().includes(filter) |
| ); |
| |
| if (filteredModels.length === 0) { |
| dropdown.classList.remove('show'); |
| return; |
| } |
| |
| filteredModels.forEach(model => { |
| const option = document.createElement('div'); |
| option.className = 'model-option'; |
| option.textContent = model; |
| option.onclick = () => selectModel(model); |
| dropdown.appendChild(option); |
| }); |
| |
| dropdown.classList.add('show'); |
| } |
| |
| |
| function selectModel(model) { |
| document.getElementById('modelInput').value = model; |
| document.getElementById('modelDropdown').classList.remove('show'); |
| |
| |
| if (currentMode === 'raw') { |
| try { |
| const requestBody = document.getElementById('requestBody'); |
| const json = JSON.parse(requestBody.value || '{}'); |
| json.model = model; |
| requestBody.value = JSON.stringify(json, null, 2); |
| } catch (e) { |
| |
| } |
| } |
| } |
| |
| |
| async function sendRequest() { |
| const token = document.getElementById('authToken').value; |
| if (!token) { |
| showGlobalMessage('请输入 AUTH Token', true); |
| return; |
| } |
| |
| const modelInput = document.getElementById('modelInput').value; |
| const requestBodyStr = document.getElementById('requestBody').value; |
| const streamMode = document.getElementById('streamMode').value; |
| |
| try { |
| |
| let requestBody = JSON.parse(requestBodyStr || '{}'); |
| if (modelInput) { |
| requestBody.model = modelInput; |
| } |
| |
| |
| if (streamMode === 'stream') { |
| requestBody.stream = true; |
| } else if (streamMode === 'stream-usage') { |
| requestBody.stream = true; |
| requestBody.stream_options = { |
| include_usage: true |
| }; |
| } else { |
| requestBody.stream = false; |
| } |
| |
| |
| const sendBtn = document.getElementById('sendBtn'); |
| sendBtn.disabled = true; |
| sendBtn.textContent = streamMode !== 'normal' ? '停止' : '发送中...'; |
| |
| document.getElementById('responseStatus').innerHTML = |
| '<span class="loading"></span> 请求中...'; |
| document.getElementById('responseTime').textContent = ''; |
| document.getElementById('responseBody').textContent = ''; |
| document.getElementById('tokenStats').style.display = 'none'; |
| |
| const startTime = Date.now(); |
| |
| |
| responseAddedToRequest = false; |
| lastResponseData = null; |
| document.getElementById('addResponseBtn').style.display = 'none'; |
| |
| if (streamMode !== 'normal') { |
| await handleStreamRequest(requestBody, token); |
| } else { |
| await handleNormalRequest(requestBody, token); |
| } |
| |
| const endTime = Date.now(); |
| document.getElementById('responseTime').textContent = |
| `${endTime - startTime}ms`; |
| |
| |
| updateLastRequestTime(); |
| requestCount++; |
| |
| } catch (error) { |
| console.error('Request error:', error); |
| document.getElementById('responseStatus').innerHTML = |
| '<span class="status-code error">错误</span>'; |
| document.getElementById('responseBody').textContent = |
| error.message || '请求失败'; |
| } finally { |
| const sendBtn = document.getElementById('sendBtn'); |
| sendBtn.disabled = false; |
| sendBtn.textContent = '发送请求'; |
| } |
| } |
| |
| |
| async function handleNormalRequest(requestBody, token) { |
| const response = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| |
| const responseText = await response.text(); |
| let data; |
| |
| try { |
| data = JSON.parse(responseText); |
| } catch (e) { |
| data = { error: 'Failed to parse response' }; |
| } |
| |
| if (response.ok) { |
| document.getElementById('responseStatus').innerHTML = |
| '<span class="status-code success">200 OK</span>'; |
| |
| |
| document.getElementById('responseBody').textContent = responseText; |
| |
| |
| lastResponseData = data; |
| document.getElementById('addResponseBtn').style.display = 'inline-block'; |
| |
| |
| if (data.usage) { |
| document.getElementById('tokenStats').style.display = 'flex'; |
| document.getElementById('promptTokens').textContent = data.usage.prompt_tokens || 0; |
| document.getElementById('completionTokens').textContent = data.usage.completion_tokens || 0; |
| document.getElementById('totalTokens').textContent = data.usage.total_tokens || 0; |
| } |
| |
| |
| if (requestBody.messages) { |
| conversationHistory = requestBody.messages; |
| if (data.choices && data.choices[0]) { |
| conversationHistory.push(data.choices[0].message); |
| } |
| } |
| } else { |
| document.getElementById('responseStatus').innerHTML = |
| `<span class="status-code error">${response.status} ${response.statusText}</span>`; |
| |
| |
| document.getElementById('responseBody').textContent = responseText; |
| } |
| } |
| |
| |
| async function handleStreamRequest(requestBody, token) { |
| isStreaming = true; |
| abortController = new AbortController(); |
| |
| const response = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(requestBody), |
| signal: abortController.signal |
| }); |
| |
| if (!response.ok) { |
| const error = await response.json(); |
| document.getElementById('responseStatus').innerHTML = |
| `<span class="status-code error">${response.status} ${response.statusText}</span>`; |
| document.getElementById('responseBody').textContent = |
| JSON.stringify(error, null, 2); |
| return; |
| } |
| |
| document.getElementById('responseStatus').innerHTML = |
| '<span class="status-code success">200 OK</span> (流式)'; |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder(); |
| let buffer = ''; |
| let fullContent = ''; |
| let rawData = ''; |
| let responseContainer = document.getElementById('responseBody'); |
| responseContainer.textContent = ''; |
| let streamedData = []; |
| let finalUsage = null; |
| |
| try { |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| |
| buffer += decoder.decode(value, { stream: true }); |
| const lines = buffer.split('\n'); |
| buffer = lines.pop() || ''; |
| |
| for (const line of lines) { |
| if (line.startsWith('data: ')) { |
| |
| rawData += line + '\n'; |
| responseContainer.textContent = rawData; |
| |
| const data = line.slice(6); |
| if (data === '[DONE]') { |
| isStreaming = false; |
| return; |
| } |
| |
| try { |
| const json = JSON.parse(data); |
| |
| streamedData.push(json); |
| |
| if (json.choices && json.choices[0] && json.choices[0].delta) { |
| const content = json.choices[0].delta.content || ''; |
| fullContent += content; |
| } |
| |
| |
| if (json.usage) { |
| finalUsage = json.usage; |
| document.getElementById('tokenStats').style.display = 'flex'; |
| document.getElementById('promptTokens').textContent = json.usage.prompt_tokens || 0; |
| document.getElementById('completionTokens').textContent = json.usage.completion_tokens || 0; |
| document.getElementById('totalTokens').textContent = json.usage.total_tokens || 0; |
| } |
| } catch (e) { |
| console.error('Parse error:', e); |
| } |
| |
| |
| responseContainer.scrollTop = responseContainer.scrollHeight; |
| } |
| } |
| } |
| } catch (error) { |
| if (error.name === 'AbortError') { |
| responseContainer.textContent += '\n\n[请求已取消]'; |
| } else { |
| throw error; |
| } |
| } finally { |
| isStreaming = false; |
| |
| |
| if (streamedData.length > 0) { |
| const firstChunk = streamedData[0]; |
| lastResponseData = { |
| id: firstChunk.id, |
| object: 'chat.completion', |
| created: firstChunk.created, |
| model: firstChunk.model, |
| choices: [{ |
| index: 0, |
| message: { |
| role: 'assistant', |
| content: fullContent |
| }, |
| finish_reason: 'stop' |
| }] |
| }; |
| |
| if (finalUsage) { |
| lastResponseData.usage = finalUsage; |
| } |
| |
| document.getElementById('addResponseBtn').style.display = 'inline-block'; |
| } |
| } |
| } |
| |
| |
| async function sendPlaygroundMessage() { |
| const token = document.getElementById('authToken').value; |
| if (!token) { |
| showGlobalMessage('请输入 AUTH Token', true); |
| return; |
| } |
| |
| const modelInput = document.getElementById('modelInput').value; |
| const systemPrompt = document.getElementById('systemPrompt').value; |
| const userInput = document.getElementById('userInput').value.trim(); |
| |
| if (!userInput) { |
| showGlobalMessage('请输入消息', true); |
| return; |
| } |
| |
| |
| const messages = []; |
| if (systemPrompt) { |
| messages.push({ role: 'system', content: systemPrompt }); |
| } |
| messages.push(...conversationHistory); |
| messages.push({ role: 'user', content: userInput }); |
| |
| |
| conversationHistory.push({ role: 'user', content: userInput }); |
| updateConversationDisplay(); |
| |
| |
| document.getElementById('userInput').value = ''; |
| |
| |
| const streamMode = document.getElementById('streamMode').value; |
| const requestBody = { |
| model: modelInput || 'default', |
| messages: messages |
| }; |
| |
| |
| if (streamMode === 'stream') { |
| requestBody.stream = true; |
| } else if (streamMode === 'stream-usage') { |
| requestBody.stream = true; |
| requestBody.stream_options = { |
| include_usage: true |
| }; |
| } else { |
| requestBody.stream = false; |
| } |
| |
| |
| const sendBtn = document.getElementById('playgroundSendBtn'); |
| sendBtn.disabled = true; |
| sendBtn.textContent = '发送中...'; |
| |
| try { |
| const startTime = Date.now(); |
| |
| if (requestBody.stream) { |
| await handlePlaygroundStream(requestBody, token); |
| } else { |
| await handlePlaygroundNormal(requestBody, token); |
| } |
| |
| const endTime = Date.now(); |
| document.getElementById('responseTime').textContent = |
| `${endTime - startTime}ms`; |
| |
| updateLastRequestTime(); |
| requestCount++; |
| |
| } catch (error) { |
| console.error('Playground error:', error); |
| showGlobalMessage('请求失败: ' + error.message, true); |
| } finally { |
| sendBtn.disabled = false; |
| sendBtn.textContent = '发送'; |
| } |
| } |
| |
| |
| async function handlePlaygroundNormal(requestBody, token) { |
| const response = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| const data = await response.json(); |
| |
| if (response.ok && data.choices && data.choices[0]) { |
| const assistantMessage = data.choices[0].message; |
| conversationHistory.push(assistantMessage); |
| updateConversationDisplay(); |
| |
| |
| if (currentMode === 'raw') { |
| document.getElementById('responseStatus').innerHTML = |
| '<span class="status-code success">200 OK</span>'; |
| document.getElementById('responseBody').textContent = |
| JSON.stringify(data, null, 2); |
| |
| |
| if (data.usage) { |
| document.getElementById('tokenStats').style.display = 'flex'; |
| document.getElementById('promptTokens').textContent = data.usage.prompt_tokens || 0; |
| document.getElementById('completionTokens').textContent = data.usage.completion_tokens || 0; |
| document.getElementById('totalTokens').textContent = data.usage.total_tokens || 0; |
| } |
| } |
| } else { |
| throw new Error(`${response.status} ${response.statusText}`); |
| } |
| } |
| |
| |
| async function handlePlaygroundStream(requestBody, token) { |
| |
| const assistantMessageIndex = conversationHistory.length; |
| conversationHistory.push({ role: 'assistant', content: '' }); |
| updateConversationDisplay(); |
| |
| isStreaming = true; |
| abortController = new AbortController(); |
| |
| const response = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(requestBody), |
| signal: abortController.signal |
| }); |
| |
| if (!response.ok) { |
| conversationHistory.pop(); |
| throw new Error(`${response.status} ${response.statusText}`); |
| } |
| |
| |
| if (currentMode === 'raw') { |
| document.getElementById('responseStatus').innerHTML = |
| '<span class="status-code success">200 OK</span> (流式)'; |
| } |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder(); |
| let buffer = ''; |
| let fullResponse = ''; |
| |
| try { |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| |
| buffer += decoder.decode(value, { stream: true }); |
| const lines = buffer.split('\n'); |
| buffer = lines.pop() || ''; |
| |
| for (const line of lines) { |
| if (line.startsWith('data: ')) { |
| const data = line.slice(6); |
| if (data === '[DONE]') { |
| isStreaming = false; |
| return; |
| } |
| |
| try { |
| const json = JSON.parse(data); |
| if (json.choices && json.choices[0] && json.choices[0].delta) { |
| const content = json.choices[0].delta.content || ''; |
| conversationHistory[assistantMessageIndex].content += content; |
| fullResponse += content; |
| updateConversationDisplay(); |
| |
| |
| if (currentMode === 'raw') { |
| document.getElementById('responseBody').textContent = fullResponse; |
| } |
| } |
| |
| |
| if (currentMode === 'raw' && json.usage) { |
| document.getElementById('tokenStats').style.display = 'flex'; |
| document.getElementById('promptTokens').textContent = json.usage.prompt_tokens || 0; |
| document.getElementById('completionTokens').textContent = json.usage.completion_tokens || 0; |
| document.getElementById('totalTokens').textContent = json.usage.total_tokens || 0; |
| } |
| } catch (e) { |
| console.error('Parse error:', e); |
| } |
| } |
| } |
| } |
| } catch (error) { |
| if (error.name === 'AbortError') { |
| conversationHistory[assistantMessageIndex].content += '\n[请求已取消]'; |
| updateConversationDisplay(); |
| } else { |
| throw error; |
| } |
| } finally { |
| isStreaming = false; |
| } |
| } |
| |
| |
| function updateConversationDisplay() { |
| const container = document.getElementById('conversationHistory'); |
| container.innerHTML = ''; |
| |
| if (conversationHistory.length === 0) { |
| container.innerHTML = ` |
| <div style="text-align: center; color: var(--text-secondary);"> |
| 对话历史将在这里显示 |
| </div> |
| `; |
| return; |
| } |
| |
| conversationHistory.forEach(msg => { |
| if (msg.role === 'system') return; |
| |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message ${msg.role}`; |
| |
| const roleDiv = document.createElement('div'); |
| roleDiv.className = 'message-role'; |
| roleDiv.textContent = msg.role === 'user' ? '用户' : '助手'; |
| |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'message-content'; |
| contentDiv.textContent = msg.content; |
| |
| messageDiv.appendChild(roleDiv); |
| messageDiv.appendChild(contentDiv); |
| container.appendChild(messageDiv); |
| }); |
| |
| |
| container.scrollTop = container.scrollHeight; |
| } |
| |
| |
| function syncModeData() { |
| if (currentMode === 'playground') { |
| |
| try { |
| const requestBodyStr = document.getElementById('requestBody').value; |
| const requestBody = JSON.parse(requestBodyStr || '{}'); |
| |
| if (requestBody.messages) { |
| conversationHistory = requestBody.messages.filter(m => m.role !== 'system'); |
| const systemMsg = requestBody.messages.find(m => m.role === 'system'); |
| if (systemMsg) { |
| document.getElementById('systemPrompt').value = systemMsg.content; |
| } |
| } |
| |
| updateConversationDisplay(); |
| } catch (e) { |
| |
| } |
| } else { |
| |
| const systemPrompt = document.getElementById('systemPrompt').value; |
| const messages = []; |
| |
| if (systemPrompt) { |
| messages.push({ role: 'system', content: systemPrompt }); |
| } |
| messages.push(...conversationHistory); |
| |
| const requestBody = { |
| model: document.getElementById('modelInput').value || 'gpt-4', |
| messages: messages |
| }; |
| |
| document.getElementById('requestBody').value = JSON.stringify(requestBody, null, 2); |
| } |
| } |
| |
| |
| function bindShortcuts() { |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.ctrlKey && e.key === 'Enter') { |
| e.preventDefault(); |
| if (currentTab === 'chat') { |
| if (currentMode === 'raw') { |
| sendRequest(); |
| } else { |
| sendPlaygroundMessage(); |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| function handleUserInputKeydown(event) { |
| if (event.ctrlKey && event.key === 'Enter') { |
| event.preventDefault(); |
| sendPlaygroundMessage(); |
| } |
| } |
| |
| |
| function copyResponse() { |
| const responseBody = document.getElementById('responseBody').textContent; |
| copyToClipboard(responseBody, { |
| successMessage: '响应内容已复制', |
| errorMessage: '复制失败' |
| }); |
| } |
| |
| |
| async function generateHash() { |
| try { |
| const response = await fetch('/gen-hash'); |
| const hash = await response.text(); |
| |
| copyToClipboard(hash, { |
| successMessage: 'Hash已复制到剪贴板' |
| }); |
| |
| showGlobalMessage(`生成的Hash: ${hash}`); |
| } catch (error) { |
| showGlobalMessage('生成Hash失败', true); |
| } |
| } |
| |
| async function generateChecksum() { |
| try { |
| const response = await fetch('/gen-checksum'); |
| const checksum = await response.text(); |
| |
| copyToClipboard(checksum, { |
| successMessage: 'Checksum已复制到剪贴板' |
| }); |
| |
| showGlobalMessage(`生成的Checksum: ${checksum}`); |
| } catch (error) { |
| showGlobalMessage('生成Checksum失败', true); |
| } |
| } |
| |
| async function generateTimestampHeader() { |
| try { |
| const response = await fetch('/get-timestamp-header'); |
| const timestampHeader = await response.text(); |
| |
| copyToClipboard(timestampHeader, { |
| successMessage: 'TimestampHeader已复制到剪贴板' |
| }); |
| |
| showGlobalMessage(`生成的TimestampHeader: ${timestampHeader}`); |
| } catch (error) { |
| showGlobalMessage('生成TimestampHeader失败', true); |
| } |
| } |
| |
| |
| function toggleUpdateData() { |
| const updateData = document.getElementById('updateData').checked; |
| const paramsSection = document.getElementById('paramsSection'); |
| |
| if (updateData) { |
| paramsSection.classList.add('enabled'); |
| } else { |
| paramsSection.classList.remove('enabled'); |
| } |
| } |
| |
| |
| async function fetchModels() { |
| const updateData = document.getElementById('updateData').checked; |
| const token = document.getElementById('authToken').value; |
| |
| |
| if (updateData && !token) { |
| showGlobalMessage('更新数据需要输入 AUTH Token', true); |
| return; |
| } |
| |
| try { |
| let url = '/v1/models'; |
| const headers = {}; |
| |
| |
| if (updateData) { |
| headers['Authorization'] = `Bearer ${token}`; |
| |
| |
| const queryParams = new URLSearchParams(); |
| |
| const isNightly = document.getElementById('isNightly').checked; |
| const includeLongContext = document.getElementById('includeLongContext').checked; |
| const excludeMaxNamed = document.getElementById('excludeMaxNamed').checked; |
| const additionalModels = document.getElementById('additionalModels').value |
| .split(',') |
| .map(s => s.trim()) |
| .filter(s => s); |
| |
| |
| if (isNightly) queryParams.append('is_nightly', 'true'); |
| if (includeLongContext) queryParams.append('include_long_context_models', 'true'); |
| if (excludeMaxNamed) queryParams.append('exclude_max_named_models', 'true'); |
| if (additionalModels.length > 0) { |
| additionalModels.forEach(model => { |
| queryParams.append('additional_model_names', model); |
| }); |
| } |
| |
| const s = queryParams.toString(); |
| if (s) { |
| url += '?' + s; |
| } |
| } |
| |
| const response = await fetch(url, { |
| method: 'GET', |
| headers: headers |
| }); |
| |
| const data = await response.json(); |
| |
| if (response.ok) { |
| displayModelsTable(data.data || []); |
| showGlobalMessage(`获取到 ${data.data.length} 个模型`); |
| } else { |
| showGlobalMessage('获取模型列表失败', true); |
| } |
| } catch (error) { |
| showGlobalMessage('请求失败: ' + error.message, true); |
| } |
| } |
| |
| |
| function displayModelsTable(models) { |
| const container = document.getElementById('modelsTableContainer'); |
| |
| if (models.length === 0) { |
| container.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">没有找到模型</p>'; |
| return; |
| } |
| |
| const table = document.createElement('table'); |
| table.className = 'models-table'; |
| |
| |
| const thead = document.createElement('thead'); |
| thead.innerHTML = ` |
| <tr> |
| <th>模型ID</th> |
| <th>显示名称</th> |
| <th>所有者</th> |
| <th>特性</th> |
| <th>创建时间</th> |
| </tr> |
| `; |
| table.appendChild(thead); |
| |
| |
| const tbody = document.createElement('tbody'); |
| models.forEach(model => { |
| const tr = document.createElement('tr'); |
| |
| |
| const features = []; |
| if (model.supports_thinking) features.push('思考'); |
| if (model.supports_images) features.push('图像'); |
| if (model.supports_max_mode) features.push('Max模式'); |
| if (model.supports_non_max_mode) features.push('非Max模式'); |
| |
| tr.innerHTML = ` |
| <td>${model.id}</td> |
| <td>${model.display_name || model.id}</td> |
| <td>${model.owned_by || '-'}</td> |
| <td> |
| <div class="model-features"> |
| ${features.map(f => `<span class="feature-badge">${f}</span>`).join('')} |
| </div> |
| </td> |
| <td>${model.created_at || new Date(model.created * 1000).toLocaleDateString()}</td> |
| `; |
| |
| tbody.appendChild(tr); |
| }); |
| |
| table.appendChild(tbody); |
| container.innerHTML = ''; |
| container.appendChild(table); |
| |
| |
| window.currentModelsData = models; |
| } |
| |
| |
| function filterModels() { |
| const searchTerm = document.getElementById('modelSearch').value.toLowerCase(); |
| |
| if (!window.currentModelsData) return; |
| |
| const filtered = window.currentModelsData.filter(model => |
| model.id.toLowerCase().includes(searchTerm) || |
| (model.display_name && model.display_name.toLowerCase().includes(searchTerm)) |
| ); |
| |
| displayModelsTable(filtered); |
| } |
| |
| |
| function updateLastRequestTime() { |
| const now = new Date(); |
| const timeStr = now.toLocaleTimeString('zh-CN'); |
| document.getElementById('lastRequestTime').textContent = `最后请求: ${timeStr}`; |
| } |
| |
| |
| function saveSessionData() { |
| const sessionData = { |
| model: document.getElementById('modelInput').value, |
| systemPrompt: document.getElementById('systemPrompt').value, |
| conversationHistory: conversationHistory, |
| requestBody: document.getElementById('requestBody').value, |
| streamMode: document.getElementById('streamMode').value |
| }; |
| |
| localStorage.setItem('apiTestSession', JSON.stringify(sessionData)); |
| } |
| |
| function restoreSessionData() { |
| try { |
| const sessionStr = localStorage.getItem('apiTestSession'); |
| if (!sessionStr) return; |
| |
| const session = JSON.parse(sessionStr); |
| |
| if (session.model) { |
| document.getElementById('modelInput').value = session.model; |
| } |
| if (session.systemPrompt) { |
| document.getElementById('systemPrompt').value = session.systemPrompt; |
| } |
| if (session.conversationHistory) { |
| conversationHistory = session.conversationHistory; |
| } |
| if (session.requestBody) { |
| document.getElementById('requestBody').value = session.requestBody; |
| } |
| if (session.streamMode) { |
| document.getElementById('streamMode').value = session.streamMode; |
| } |
| } catch (e) { |
| console.error('Failed to restore session:', e); |
| } |
| } |
| |
| |
| setInterval(saveSessionData, 30000); |
| |
| |
| window.addEventListener('beforeunload', saveSessionData); |
| |
| |
| function initializeJsonValidation() { |
| const jsonEditor = document.getElementById('requestBody'); |
| |
| jsonEditor.addEventListener('input', () => { |
| try { |
| JSON.parse(jsonEditor.value || '{}'); |
| jsonEditor.classList.remove('invalid'); |
| jsonEditor.classList.add('valid'); |
| } catch (e) { |
| jsonEditor.classList.remove('valid'); |
| jsonEditor.classList.add('invalid'); |
| } |
| }); |
| |
| |
| jsonEditor.dispatchEvent(new Event('input')); |
| } |
| |
| |
| function addResponseToRequest() { |
| if (!lastResponseData || responseAddedToRequest) { |
| return; |
| } |
| |
| try { |
| const requestBody = document.getElementById('requestBody'); |
| const currentRequest = JSON.parse(requestBody.value || '{}'); |
| |
| |
| if (!currentRequest.messages) { |
| currentRequest.messages = []; |
| } |
| |
| if (lastResponseData.choices && lastResponseData.choices[0] && lastResponseData.choices[0].message) { |
| currentRequest.messages.push(lastResponseData.choices[0].message); |
| } |
| |
| requestBody.value = JSON.stringify(currentRequest, null, 2); |
| |
| |
| requestBody.dispatchEvent(new Event('input')); |
| |
| |
| responseAddedToRequest = true; |
| document.getElementById('addResponseBtn').disabled = true; |
| document.getElementById('addResponseBtn').textContent = '已添加'; |
| |
| showGlobalMessage('响应已添加到请求'); |
| } catch (e) { |
| showGlobalMessage('添加响应失败: ' + e.message, true); |
| } |
| } |
| </script> |
| </body> |
| </html> |