Files
proxy-bun/api/test-api-content.ts
T
MythEclipse d5f8dfc1d7 fix: auto-detect base URL from window.location.origin
- public/test-api.html: value di baseUrl input kosong, apiBase() fallback ke window.location.origin,
  plus DOMContentLoaded listener utk auto-fill
- api/test-api-content.ts: sama (legacy Next.js route)
- Router.ts handleDocs() sudah pakai pendekatan ini sebelumnya
2026-06-20 21:49:37 +07:00

3 lines
15 KiB
TypeScript

export default "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Edge Proxy — API Test</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #e1e4e8; padding: 1.5rem; }\n main { max-width: 960px; margin: 0 auto; }\n h1 { font-size: 1.5rem; color: #58a6ff; margin-bottom: 0.25rem; }\n .sub { color: #8b949e; margin-bottom: 1.5rem; font-size: 0.9rem; }\n\n .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; margin-bottom: 1rem; overflow: hidden; }\n .card-header { padding: 0.75rem 1rem; background: #1c2128; border-bottom: 1px solid #30363d; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; user-select: none; }\n .card-header:hover { background: #21262d; }\n .card-header .method { font-size: 0.7rem; padding: 1px 6px; border-radius: 3px; font-weight: 600; }\n .get { background: #1f6feb33; color: #58a6ff; }\n .post { background: #23863633; color: #3fb950; }\n .card-body { padding: 1rem; display: none; }\n .card.open .card-body { display: block; }\n .card-header .arrow { margin-left: auto; transition: transform .15s; }\n .card.open .arrow { transform: rotate(90deg); }\n\n label { display: block; font-size: 0.8rem; color: #8b949e; margin-bottom: 0.25rem; }\n input, textarea, select { width: 100%; background: #0d1117; border: 1px solid #30363d; border-radius: 4px; color: #e1e4e8; padding: 0.5rem 0.75rem; font-family: inherit; font-size: 0.9rem; margin-bottom: 0.75rem; }\n input:focus, textarea:focus { border-color: #58a6ff; outline: none; }\n textarea { min-height: 60px; resize: vertical; font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 0.8rem; }\n textarea.code { min-height: 120px; }\n\n .row { display: flex; gap: 0.75rem; }\n .row > * { flex: 1; }\n\n .btn { background: #238636; color: #fff; border: none; border-radius: 6px; padding: 0.5rem 1.25rem; font-size: 0.85rem; cursor: pointer; font-weight: 500; }\n .btn:hover { background: #2ea043; }\n .btn:disabled { opacity: 0.5; cursor: not-allowed; }\n .btn-outline { background: transparent; border: 1px solid #30363d; color: #c9d1d9; }\n .btn-outline:hover { background: #21262d; border-color: #8b949e; }\n .btn-danger { background: #da3633; }\n .btn-danger:hover { background: #f85149; }\n\n .output { background: #0d1117; border: 1px solid #30363d; border-radius: 4px; padding: 0.75rem; margin-top: 0.5rem; }\n .output pre { font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 0.78rem; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow: auto; color: #c9d1d9; }\n .output .meta { font-size: 0.75rem; color: #8b949e; margin-bottom: 0.5rem; }\n .output .meta .status.ok { color: #3fb950; }\n .output .meta .status.err { color: #f85149; }\n\n .toast { position: fixed; bottom: 1.5rem; right: 1.5rem; background: #1c2128; border: 1px solid #30363d; border-radius: 6px; padding: 0.75rem 1rem; font-size: 0.85rem; display: none; z-index: 100; }\n </style>\n</head>\n<body>\n<main>\n <h1>Edge Proxy Relay — API Test</h1>\n <p class=\"sub\">Test all proxy endpoints from the browser</p>\n\n <!-- Global Auth -->\n <div class=\"card open\">\n <div class=\"card-header\">\n <span class=\"method get\">AUTH</span> API Key\n <span class=\"arrow\">▶</span>\n </div>\n <div class=\"card-body\">\n <label>API Key (default: <code>sk-dummy-key</code>)</label>\n <div class=\"row\">\n <input type=\"text\" id=\"apiKey\" value=\"sk-dummy-key\" />\n <input type=\"text\" id=\"baseUrl\" value=\"http://localhost:3000\" placeholder=\"http://localhost:3000\" />\n </div>\n </div>\n </div>\n\n <!-- Health -->\n <div class=\"card open\">\n <div class=\"card-header\" onclick=\"toggleCard(this)\">\n <span class=\"method get\">GET</span> /health\n <span class=\"arrow\">▶</span>\n </div>\n <div class=\"card-body\">\n <button class=\"btn\" onclick=\"callHealth()\">Ping Health</button>\n <div id=\"output-health\" class=\"output\" style=\"display:none\"><pre></pre></div>\n </div>\n </div>\n\n <!-- Models -->\n <div class=\"card open\">\n <div class=\"card-header\" onclick=\"toggleCard(this)\">\n <span class=\"method get\">GET</span> /v1/models\n <span class=\"arrow\">▶</span>\n </div>\n <div class=\"card-body\">\n <button class=\"btn\" onclick=\"callModels()\">List Models</button>\n <div id=\"output-models\" class=\"output\" style=\"display:none\"><pre></pre></div>\n </div>\n </div>\n\n <!-- Chat Completions -->\n <div class=\"card\">\n <div class=\"card-header\" onclick=\"toggleCard(this)\">\n <span class=\"method post\">POST</span> /v1/chat/completions\n <span class=\"arrow\">▶</span>\n </div>\n <div class=\"card-body\">\n <label>Model</label>\n <input type=\"text\" id=\"chatModel\" value=\"gpt-4o-mini\" />\n <label>Messages (JSON array)</label>\n <textarea id=\"chatMessages\" class=\"code\">[{\"role\":\"user\",\"content\":\"Say hello in one word\"}]</textarea>\n <label><input type=\"checkbox\" id=\"chatStream\" /> Stream (SSE)</label>\n <div style=\"margin-top:0.5rem\"><button class=\"btn\" onclick=\"callChat()\">Send</button></div>\n <div id=\"output-chat\" class=\"output\" style=\"display:none\"><pre></pre></div>\n </div>\n </div>\n\n <!-- Anthropic Messages -->\n <div class=\"card\">\n <div class=\"card-header\" onclick=\"toggleCard(this)\">\n <span class=\"method post\">POST</span> /v1/messages (Anthropic)\n <span class=\"arrow\">▶</span>\n </div>\n <div class=\"card-body\">\n <label>Model</label>\n <input type=\"text\" id=\"anthModel\" value=\"claude-sonnet-4-6\" />\n <label>Max Tokens</label>\n <input type=\"number\" id=\"anthMaxTokens\" value=\"256\" />\n <label>Messages (JSON)</label>\n <textarea id=\"anthMessages\" class=\"code\">[{\"role\":\"user\",\"content\":\"Say hello in one word\"}]</textarea>\n <label><input type=\"checkbox\" id=\"anthStream\" /> Stream (SSE)</label>\n <div style=\"margin-top:0.5rem\"><button class=\"btn\" onclick=\"callAnthropic()\">Send</button></div>\n <div id=\"output-anth\" class=\"output\" style=\"display:none\"><pre></pre></div>\n </div>\n </div>\n\n <!-- Generic Relay -->\n <div class=\"card\">\n <div class=\"card-header\" onclick=\"toggleCard(this)\">\n <span class=\"method post\">RELAY</span> Generic HTTP Relay\n <span class=\"arrow\">▶</span>\n </div>\n <div class=\"card-body\">\n <label>Relay Target URL</label>\n <input type=\"text\" id=\"relayTarget\" value=\"https://httpbin.org\" />\n <label>Relay Path</label>\n <input type=\"text\" id=\"relayPath\" value=\"/get\" />\n <label>Method</label>\n <select id=\"relayMethod\"><option>GET</option><option>POST</option></select>\n <label>Body (JSON, optional)</label>\n <textarea id=\"relayBody\" class=\"code\" placeholder='{\"test\":true}'></textarea>\n <div style=\"margin-top:0.5rem\"><button class=\"btn\" onclick=\"callRelay()\">Send</button></div>\n <div id=\"output-relay\" class=\"output\" style=\"display:none\"><pre></pre></div>\n </div>\n </div>\n\n <div id=\"toast\" class=\"toast\"></div>\n</main>\n\n<script>\n// --- Config ---\nfunction apiBase() { return document.getElementById('baseUrl').value || ''; }\nfunction authHeaders() {\n const k = document.getElementById('apiKey').value || 'sk-dummy-key';\n return { 'Authorization': 'Bearer ' + k, 'Content-Type': 'application/json' };\n}\n\n// --- Toggle ---\nfunction toggleCard(header) {\n header.parentElement.classList.toggle('open');\n}\n\n// --- Toast ---\nlet toastTimer;\nfunction showToast(msg, ok = true) {\n const el = document.getElementById('toast');\n el.textContent = (ok ? '✓ ' : '✗ ') + msg;\n el.style.display = 'block';\n el.style.borderColor = ok ? '#3fb950' : '#f85149';\n clearTimeout(toastTimer);\n toastTimer = setTimeout(() => el.style.display = 'none', 3000);\n}\n\n// --- Shared fetch ---\nasync function apiFetch(method, path, body, { rawHeaders, stream } = {}) {\n const headers = rawHeaders || authHeaders();\n let resp;\n if (stream) {\n resp = await fetch(apiBase() + path, { method, headers, body: body ? JSON.stringify(body) : undefined });\n } else {\n resp = await fetch(apiBase() + path, { method, headers, body: body ? JSON.stringify(body) : undefined });\n }\n if (stream && resp.ok && resp.headers.get('content-type')?.includes('text/event-stream')) {\n return { resp, streamReader: resp.body.getReader() };\n }\n let data = null;\n const text = await resp.text();\n try { data = JSON.parse(text); } catch { data = text; }\n return { resp, data };\n}\n\nfunction showOutput(id, resp, data, raw) {\n const el = document.getElementById('output-' + id);\n el.style.display = 'block';\n const ok = resp.status >= 200 && resp.status < 300;\n el.innerHTML = `<div class=\"meta\">\n <span class=\"status ${ok ? 'ok' : 'err'}\">${resp.status} ${resp.statusText}</span>\n ${elapsed()}ms\n </div>\n <pre>${escapeHtml(typeof data === 'string' ? data : JSON.stringify(data, null, 2)).slice(0, 10000)}</pre>`;\n showToast(`${resp.status} ${resp.statusText}`, ok);\n}\n\nfunction streamOutput(id, chunk) {\n const el = document.getElementById('output-' + id);\n el.style.display = 'block';\n const pre = el.querySelector('pre');\n if (!pre) {\n el.innerHTML = `<div class=\"meta\">streaming…</div><pre></pre>`;\n }\n const p = el.querySelector('pre');\n p.textContent += chunk;\n p.scrollTop = p.scrollHeight;\n}\n\nlet _elapsedBase = 0;\nfunction elapsed() { return Date.now() - _elapsedBase; }\n\n// --- Health ---\nasync function callHealth() {\n _elapsedBase = Date.now();\n try {\n const { resp, data } = await apiFetch('GET', '/health');\n showOutput('health', resp, data);\n } catch (e) { showOutput('health', {status:0,statusText:'Network Error'}, e.message); showToast(e.message, false); }\n}\n\n// --- Models ---\nasync function callModels() {\n _elapsedBase = Date.now();\n try {\n const { resp, data } = await apiFetch('GET', '/v1/models');\n showOutput('models', resp, data);\n } catch (e) { showOutput('models', {status:0,statusText:'Network Error'}, e.message); showToast(e.message, false); }\n}\n\n// --- Chat Completions ---\nasync function callChat() {\n _elapsedBase = Date.now();\n const stream = document.getElementById('chatStream').checked;\n let messages;\n try { messages = JSON.parse(document.getElementById('chatMessages').value); }\n catch { showToast('Invalid messages JSON', false); return; }\n\n const body = {\n model: document.getElementById('chatModel').value || 'gpt-4o-mini',\n messages,\n stream,\n max_tokens: 128,\n };\n\n try {\n if (stream) {\n const el = document.getElementById('output-chat');\n el.style.display = 'block';\n el.innerHTML = `<div class=\"meta\">streaming text/event-stream …</div><pre></pre>`;\n const resp = await fetch(apiBase() + '/v1/chat/completions', {\n method: 'POST',\n headers: authHeaders(),\n body: JSON.stringify(body),\n });\n if (!resp.ok) {\n showOutput('chat', resp, await resp.text());\n return;\n }\n const reader = resp.body.getReader();\n const decoder = new TextDecoder();\n let done;\n while (!done) {\n const { value, done: d } = await reader.read();\n done = d;\n if (value) streamOutput('chat', decoder.decode(value));\n }\n showToast('stream complete');\n } else {\n const { resp, data } = await apiFetch('POST', '/v1/chat/completions', body);\n showOutput('chat', resp, data);\n }\n } catch (e) { showOutput('chat', {status:0,statusText:'Network Error'}, e.message); showToast(e.message, false); }\n}\n\n// --- Anthropic ---\nasync function callAnthropic() {\n _elapsedBase = Date.now();\n const stream = document.getElementById('anthStream').checked;\n let messages;\n try { messages = JSON.parse(document.getElementById('anthMessages').value); }\n catch { showToast('Invalid messages JSON', false); return; }\n\n const body = {\n model: document.getElementById('anthModel').value || 'claude-sonnet-4-6',\n max_tokens: parseInt(document.getElementById('anthMaxTokens').value) || 256,\n messages,\n stream,\n };\n\n try {\n if (stream) {\n const el = document.getElementById('output-anth');\n el.style.display = 'block';\n el.innerHTML = `<div class=\"meta\">streaming …</div><pre></pre>`;\n const resp = await fetch(apiBase() + '/v1/messages', {\n method: 'POST',\n headers: authHeaders(),\n body: JSON.stringify(body),\n });\n if (!resp.ok) {\n showOutput('anth', resp, await resp.text());\n return;\n }\n const reader = resp.body.getReader();\n const decoder = new TextDecoder();\n let done;\n while (!done) {\n const { value, done: d } = await reader.read();\n done = d;\n if (value) streamOutput('anth', decoder.decode(value));\n }\n showToast('stream complete');\n } else {\n const { resp, data } = await apiFetch('POST', '/v1/messages', body);\n showOutput('anth', resp, data);\n }\n } catch (e) { showOutput('anth', {status:0,statusText:'Network Error'}, e.message); showToast(e.message, false); }\n}\n\n// --- Relay ---\nasync function callRelay() {\n _elapsedBase = Date.now();\n const target = document.getElementById('relayTarget').value;\n const path = document.getElementById('relayPath').value || '/';\n const method = document.getElementById('relayMethod').value;\n const rawBody = document.getElementById('relayBody').value;\n\n try {\n const resp = await fetch(apiBase() + '/', {\n method,\n headers: {\n 'x-relay-target': target,\n 'x-relay-path': path,\n ...(rawBody ? { 'Content-Type': 'application/json' } : {}),\n },\n body: rawBody || undefined,\n });\n const text = await resp.text();\n let data;\n try { data = JSON.parse(text); } catch { data = text; }\n showOutput('relay', resp, data);\n } catch (e) { showOutput('relay', {status:0,statusText:'Network Error'}, e.message); showToast(e.message, false); }\n}\n\nfunction escapeHtml(s) {\n const d = document.createElement('div');\n d.textContent = s;\n return d.innerHTML;\n}\ndocument.addEventListener('DOMContentLoaded',()=>{const e=document.getElementById('baseUrl');if(e&&!e.value)e.value=window.location.origin;});
</script>\n</body>\n</html>\n";