mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
781 lines
28 KiB
HTML
781 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Routstr Chat Completions Tester</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
color: #333;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
color: white;
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
font-size: 2.5rem;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.subtitle {
|
|
color: rgba(255,255,255,0.9);
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255,255,255,0.95);
|
|
padding: 25px;
|
|
border-radius: 10px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.card h2 {
|
|
margin-bottom: 20px;
|
|
color: #667eea;
|
|
font-size: 1.5rem;
|
|
border-bottom: 2px solid #667eea;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
color: #555;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
font-size: 0.95rem;
|
|
transition: border-color 0.3s;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group textarea:focus,
|
|
.form-group select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.form-group textarea {
|
|
resize: vertical;
|
|
min-height: 100px;
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
}
|
|
|
|
.form-group small {
|
|
display: block;
|
|
margin-top: 5px;
|
|
color: #6b7280;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
.btn {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 14px 28px;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
width: 100%;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
}
|
|
|
|
.response-container {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.response-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.response-status {
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.response-status.success {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.response-status.error {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.response-body {
|
|
background: #1e1e1e;
|
|
color: #d4d4d4;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
overflow-x: auto;
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.9rem;
|
|
line-height: 1.6;
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.response-body pre {
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.copy-btn {
|
|
background: #374151;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.copy-btn:hover {
|
|
background: #4b5563;
|
|
}
|
|
|
|
.loading {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.loading.active {
|
|
display: block;
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid #f3f4f6;
|
|
border-top: 3px solid #667eea;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.message-list {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.message-item {
|
|
background: #f9fafb;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.message-item.system {
|
|
border-left-color: #10b981;
|
|
}
|
|
|
|
.message-item.user {
|
|
border-left-color: #667eea;
|
|
}
|
|
|
|
.message-item.assistant {
|
|
border-left-color: #f59e0b;
|
|
}
|
|
|
|
.message-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.message-role {
|
|
font-weight: 600;
|
|
text-transform: capitalize;
|
|
color: #374151;
|
|
}
|
|
|
|
.message-content {
|
|
color: #1f2937;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.remove-message-btn {
|
|
background: #ef4444;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 12px;
|
|
border-radius: 4px;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.add-message-btn {
|
|
background: #10b981;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.add-message-btn:hover {
|
|
background: #059669;
|
|
}
|
|
|
|
.curl-preview {
|
|
background: #1e1e1e;
|
|
color: #d4d4d4;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.85rem;
|
|
overflow-x: auto;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.curl-preview pre {
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid #e5e7eb;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
color: #6b7280;
|
|
border-bottom: 2px solid transparent;
|
|
margin-bottom: -2px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: #667eea;
|
|
}
|
|
|
|
.tab.active {
|
|
color: #667eea;
|
|
border-bottom-color: #667eea;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.preset-container {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.preset-btn {
|
|
display: inline-block;
|
|
margin: 5px;
|
|
padding: 8px 16px;
|
|
background: #f3f4f6;
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.preset-btn:hover {
|
|
background: #e5e7eb;
|
|
border-color: #667eea;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🚀 Chat Completions Tester</h1>
|
|
<p class="subtitle">Test your /v1/chat/completions endpoint with Cashu authentication</p>
|
|
<div class="card">
|
|
<h2>Configuration</h2>
|
|
<div class="preset-container">
|
|
<strong>Quick Presets:</strong>
|
|
<button class="preset-btn" onclick="loadPreset('local')">Local Dev</button>
|
|
<button class="preset-btn" onclick="loadPreset('production')">Production</button>
|
|
<button class="preset-btn" onclick="loadPreset('example')">Example Token</button>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="endpoint">API Endpoint</label>
|
|
<input
|
|
type="text"
|
|
id="endpoint"
|
|
placeholder="http://localhost:8000/v1/chat/completions"
|
|
value="http://localhost:8000/v1/chat/completions"
|
|
>
|
|
<small>The full URL to the chat completions endpoint</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="authToken">Authorization Token</label>
|
|
<textarea id="authToken" rows="4" placeholder="cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpGFhBGFzeEBj..."></textarea>
|
|
<small>Cashu token (without "Bearer " prefix - will be added automatically)</small>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Request Parameters</h2>
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="switchTab('basic')">Basic</button>
|
|
<button class="tab" onclick="switchTab('advanced')">Advanced</button>
|
|
<button class="tab" onclick="switchTab('curl')">cURL Preview</button>
|
|
</div>
|
|
<div id="basic-tab" class="tab-content active">
|
|
<div class="form-group">
|
|
<label for="model">Model</label>
|
|
<input
|
|
type="text"
|
|
id="model"
|
|
placeholder="gpt-4o-mini"
|
|
value="gpt-4o-mini"
|
|
>
|
|
<small>Model identifier (e.g., gpt-4o-mini, claude-3-haiku-20240307)</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Messages</label>
|
|
<div class="message-list" id="messageList"></div>
|
|
<button class="add-message-btn" onclick="addMessage()">+ Add Message</button>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="maxTokens">Max Tokens</label>
|
|
<input
|
|
type="number"
|
|
id="maxTokens"
|
|
placeholder="16"
|
|
value="16"
|
|
min="1"
|
|
>
|
|
<small>Maximum tokens to generate</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="temperature">Temperature</label>
|
|
<input
|
|
type="number"
|
|
id="temperature"
|
|
placeholder="1.0"
|
|
value="1.0"
|
|
min="0"
|
|
max="2"
|
|
step="0.1"
|
|
>
|
|
<small>Sampling temperature (0-2)</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="advanced-tab" class="tab-content">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="topP">Top P</label>
|
|
<input
|
|
type="number"
|
|
id="topP"
|
|
placeholder="1.0"
|
|
value="1.0"
|
|
min="0"
|
|
max="1"
|
|
step="0.1"
|
|
>
|
|
<small>Nucleus sampling parameter</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="topK">Top K</label>
|
|
<input
|
|
type="number"
|
|
id="topK"
|
|
placeholder=""
|
|
value=""
|
|
min="0"
|
|
>
|
|
<small>Optional: Top-k sampling parameter</small>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="frequencyPenalty">Frequency Penalty</label>
|
|
<input
|
|
type="number"
|
|
id="frequencyPenalty"
|
|
placeholder="0"
|
|
value="0"
|
|
min="-2"
|
|
max="2"
|
|
step="0.1"
|
|
>
|
|
<small>Penalize repeated tokens (-2 to 2)</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="presencePenalty">Presence Penalty</label>
|
|
<input
|
|
type="number"
|
|
id="presencePenalty"
|
|
placeholder="0"
|
|
value="0"
|
|
min="-2"
|
|
max="2"
|
|
step="0.1"
|
|
>
|
|
<small>Penalize new topics (-2 to 2)</small>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="stream">Stream Response</label>
|
|
<select id="stream">
|
|
<option value="false">No (default)</option>
|
|
<option value="true">Yes (SSE streaming)</option>
|
|
</select>
|
|
<small>Enable Server-Sent Events streaming</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="stop">Stop Sequences</label>
|
|
<input type="text" id="stop" placeholder='["\\n", "user:"]'>
|
|
<small>JSON array of stop sequences</small>
|
|
</div>
|
|
</div>
|
|
<div id="curl-tab" class="tab-content">
|
|
<div class="curl-preview">
|
|
<pre id="curlPreview">Click "Send Request" to generate cURL command</pre>
|
|
</div>
|
|
<button class="copy-btn" onclick="copyCurl()" style="margin-top: 10px;">Copy cURL Command</button>
|
|
</div>
|
|
<button class="btn" onclick="sendRequest()" id="sendBtn">Send Request</button>
|
|
</div>
|
|
<div class="card" id="responseCard" style="display: none;">
|
|
<div class="response-header">
|
|
<h2>Response</h2>
|
|
<div>
|
|
<span class="response-status" id="responseStatus"></span>
|
|
<button class="copy-btn" onclick="copyResponse()" style="margin-left: 10px;">Copy Response</button>
|
|
</div>
|
|
</div>
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<p style="margin-top: 10px; color: #6b7280;">Sending request...</p>
|
|
</div>
|
|
<div class="response-body" id="responseBody"></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
let messages = [];
|
|
|
|
function switchTab(tabName) {
|
|
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
|
|
document.querySelector(`[onclick="switchTab('${tabName}')"]`).classList.add('active');
|
|
document.getElementById(`${tabName}-tab`).classList.add('active');
|
|
|
|
if (tabName === 'curl') {
|
|
updateCurlPreview();
|
|
}
|
|
}
|
|
|
|
function loadPreset(preset) {
|
|
switch(preset) {
|
|
case 'local':
|
|
document.getElementById('endpoint').value = 'http://localhost:8000/v1/chat/completions';
|
|
break;
|
|
case 'production':
|
|
document.getElementById('endpoint').value = 'https://your-production-url.com/v1/chat/completions';
|
|
break;
|
|
case 'example':
|
|
document.getElementById('authToken').value = 'cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpGFhBGFzeEBjMDg1NDgzZDA3Njk0MDkwZDJmMGRkOTg5NmYxMGZmZDk2Y2Q5ODNhZWNlOGYyMmQ0ZmVlMmNhZGZhMGQzMDAyYWNYIQNcxP6wwsZ7_-dY45f-tXt-01xrgjNEVzZczbOmXb77iWFko2FlWCC45YfoOF48khLnTWNG3M-siukLc9I4zAV5awIZnNjbzWFzWCBwYixTGifYTMKSMq5fQEa7OiPqHOihIqYilqQZm60JsmFyWCCXLpDK8gxEPaP4X-QPBzAx3gVdXp-0FiSm3APNIxCA_A';
|
|
break;
|
|
}
|
|
}
|
|
|
|
function addMessage(role = 'user', content = '') {
|
|
const message = { role, content };
|
|
messages.push(message);
|
|
renderMessages();
|
|
}
|
|
|
|
function removeMessage(index) {
|
|
messages.splice(index, 1);
|
|
renderMessages();
|
|
}
|
|
|
|
function renderMessages() {
|
|
const messageList = document.getElementById('messageList');
|
|
|
|
if (messages.length === 0) {
|
|
messageList.innerHTML = '<p style="color: #6b7280; padding: 10px;">No messages yet. Click "Add Message" to start.</p>';
|
|
return;
|
|
}
|
|
|
|
messageList.innerHTML = messages.map((msg, index) => `
|
|
<div class="message-item ${msg.role}">
|
|
<div class="message-header">
|
|
<select onchange="updateMessageRole(${index}, this.value)" style="border: 1px solid #e5e7eb; padding: 4px 8px; border-radius: 4px;">
|
|
<option value="system" ${msg.role === 'system' ? 'selected' : ''}>System</option>
|
|
<option value="user" ${msg.role === 'user' ? 'selected' : ''}>User</option>
|
|
<option value="assistant" ${msg.role === 'assistant' ? 'selected' : ''}>Assistant</option>
|
|
</select>
|
|
<button class="remove-message-btn" onclick="removeMessage(${index})">Remove</button>
|
|
</div>
|
|
<textarea class="message-content" onchange="updateMessageContent(${index}, this.value)" style="width: 100%; min-height: 60px; border: 1px solid #e5e7eb; border-radius: 4px; padding: 8px; font-family: inherit;">${msg.content}</textarea>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function updateMessageRole(index, role) {
|
|
messages[index].role = role;
|
|
renderMessages();
|
|
}
|
|
|
|
function updateMessageContent(index, content) {
|
|
messages[index].content = content;
|
|
}
|
|
|
|
function buildRequestBody() {
|
|
const body = {
|
|
model: document.getElementById('model').value,
|
|
messages: messages.filter(msg => msg.content.trim() !== '')
|
|
};
|
|
|
|
const maxTokens = parseInt(document.getElementById('maxTokens').value, 10);
|
|
if (!isNaN(maxTokens) && maxTokens > 0) {
|
|
body.max_tokens = maxTokens;
|
|
}
|
|
|
|
const temperature = parseFloat(document.getElementById('temperature').value);
|
|
if (!isNaN(temperature)) body.temperature = temperature;
|
|
|
|
const topP = parseFloat(document.getElementById('topP').value);
|
|
if (!isNaN(topP)) body.top_p = topP;
|
|
|
|
const topK = parseInt(document.getElementById('topK').value, 10);
|
|
if (!isNaN(topK) && topK > 0) body.top_k = topK;
|
|
|
|
const frequencyPenalty = parseFloat(document.getElementById('frequencyPenalty').value);
|
|
if (!isNaN(frequencyPenalty) && frequencyPenalty !== 0) body.frequency_penalty = frequencyPenalty;
|
|
|
|
const presencePenalty = parseFloat(document.getElementById('presencePenalty').value);
|
|
if (!isNaN(presencePenalty) && presencePenalty !== 0) body.presence_penalty = presencePenalty;
|
|
|
|
const stream = document.getElementById('stream').value === 'true';
|
|
if (stream) body.stream = true;
|
|
|
|
const stop = document.getElementById('stop').value.trim();
|
|
if (stop) {
|
|
try {
|
|
body.stop = JSON.parse(stop);
|
|
} catch (e) {
|
|
console.warn('Invalid stop sequences JSON');
|
|
}
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
function updateCurlPreview() {
|
|
const endpoint = document.getElementById('endpoint').value;
|
|
const token = document.getElementById('authToken').value.trim();
|
|
const body = buildRequestBody();
|
|
|
|
const curlCommand = `curl -i -X POST ${endpoint} \\
|
|
-H "Content-Type: application/json" \\
|
|
-H "Authorization: Bearer ${token}" \\
|
|
-d '${JSON.stringify(body, null, 2)}'`;
|
|
|
|
document.getElementById('curlPreview').textContent = curlCommand;
|
|
}
|
|
|
|
async function sendRequest() {
|
|
const endpoint = document.getElementById('endpoint').value.trim();
|
|
const token = document.getElementById('authToken').value.trim();
|
|
|
|
if (!endpoint) {
|
|
alert('Please enter an API endpoint');
|
|
return;
|
|
}
|
|
|
|
if (!token) {
|
|
alert('Please enter an authorization token');
|
|
return;
|
|
}
|
|
|
|
if (messages.length === 0 || messages.every(m => m.content.trim() === '')) {
|
|
alert('Please add at least one message');
|
|
return;
|
|
}
|
|
|
|
const body = buildRequestBody();
|
|
updateCurlPreview();
|
|
|
|
const responseCard = document.getElementById('responseCard');
|
|
const loading = document.getElementById('loading');
|
|
const responseBody = document.getElementById('responseBody');
|
|
const responseStatus = document.getElementById('responseStatus');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
|
|
responseCard.style.display = 'block';
|
|
loading.classList.add('active');
|
|
responseBody.innerHTML = '';
|
|
sendBtn.disabled = true;
|
|
|
|
const stream = document.getElementById('stream').value === 'true';
|
|
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
loading.classList.remove('active');
|
|
|
|
const statusCode = response.status;
|
|
const statusText = response.statusText;
|
|
|
|
if (response.ok) {
|
|
responseStatus.textContent = `${statusCode} ${statusText}`;
|
|
responseStatus.className = 'response-status success';
|
|
} else {
|
|
responseStatus.textContent = `${statusCode} ${statusText}`;
|
|
responseStatus.className = 'response-status error';
|
|
}
|
|
|
|
if (stream && response.ok) {
|
|
responseBody.innerHTML = '<pre>Streaming response:\n\n</pre>';
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) break;
|
|
|
|
const chunk = decoder.decode(value);
|
|
responseBody.querySelector('pre').textContent += chunk;
|
|
}
|
|
} else {
|
|
const responseData = await response.text();
|
|
|
|
try {
|
|
const jsonData = JSON.parse(responseData);
|
|
responseBody.innerHTML = `<pre>${JSON.stringify(jsonData, null, 2)}</pre>`;
|
|
} catch (e) {
|
|
responseBody.innerHTML = `<pre>${responseData}</pre>`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
loading.classList.remove('active');
|
|
responseStatus.textContent = 'Error';
|
|
responseStatus.className = 'response-status error';
|
|
responseBody.innerHTML = `<pre>Error: ${error.message}</pre>`;
|
|
} finally {
|
|
sendBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function copyResponse() {
|
|
const responseText = document.getElementById('responseBody').innerText;
|
|
navigator.clipboard.writeText(responseText).then(() => {
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Copied!';
|
|
setTimeout(() => btn.textContent = originalText, 2000);
|
|
});
|
|
}
|
|
|
|
function copyCurl() {
|
|
updateCurlPreview();
|
|
const curlText = document.getElementById('curlPreview').textContent;
|
|
navigator.clipboard.writeText(curlText).then(() => {
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Copied!';
|
|
setTimeout(() => btn.textContent = originalText, 2000);
|
|
});
|
|
}
|
|
|
|
addMessage('user', 'what is cashubtc');
|
|
</script>
|
|
</body>
|
|
</html>
|