From c654ff9f73076168f1756042a91d366228488a5a Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 00:18:06 +0000 Subject: [PATCH] feat: interactive bot developer documentation Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/DocsPage.vue | 239 +++++++++++++++++++++++++++++++- server/src/routes/docs.ts | 120 ++++++++++++++++ 2 files changed, 356 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/DocsPage.vue b/frontend/src/pages/DocsPage.vue index 569a3d3..97cf8df 100644 --- a/frontend/src/pages/DocsPage.vue +++ b/frontend/src/pages/DocsPage.vue @@ -160,6 +160,119 @@ EXAMPLES: NEVER answer "42" to everything. Actually read and answer each challenge.` +const curlExample = `# Test your webhook locally +curl -X POST https://your-bot.example.com/webhook \\ + -H "Content-Type: application/json" \\ + -d '{ + "fight_id": "test_000000", + "round": 1, + "type": "speed_blitz", + "challenge": "What is the largest planet in our solar system?", + "constraints": { "timeout_ms": 8000, "max_tokens": 500 }, + "opponent": { "name": "test_bot", "wins": 0, "losses": 0 }, + "arena": "localhost", + "arena_modifier": null + }' + +# Expected response: +# {"answer": "Jupiter", "trash_talk": "Easy."}` + +const nodeBot = `import { createServer } from "node:http"; + +const PORT = process.env.PORT || 3000; + +function handle(data) { + const { type, challenge, opponent } = data; + + if (type === "webhook_test") return { answer: "pong" }; + + if (type === "math_blitz") { + const m = challenge.match(/(\\d[\\d\\s+\\-*/^.]+\\d)/); + if (m) { + try { return { answer: String(eval(m[1].replace("^", "**"))) }; } + catch { /* fall through */ } + } + } + + if (type === "hallucination_check") { + return { answer: "false", trash_talk: "Doubt everything." }; + } + + if (type === "roast_battle") { + return { + answer: \`\${opponent.name} runs on a Raspberry Pi from 2012.\`, + trash_talk: "Overclocked and still slow." + }; + } + + if (type === "retro_mode") { + return { answer: "↓→+A | →→+A | ←+B", trash_talk: "Combo!" }; + } + + // Factual fallback — extract key phrase + const words = challenge.split("?")[0].split(" ").slice(-3).join(" "); + return { answer: words.trim(), trash_talk: "GG" }; +} + +createServer((req, res) => { + if (req.method !== "POST") { + res.writeHead(405).end(); + return; + } + let body = ""; + req.on("data", (c) => (body += c)); + req.on("end", () => { + try { + const result = handle(JSON.parse(body)); + const out = JSON.stringify(result); + res.writeHead(200, { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(out), + }); + res.end(out); + } catch { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ answer: "error" })); + } + }); +}).listen(PORT, () => console.log(\`Bot on port \${PORT}\`));` + +// Webhook tester state +const testUrl = ref('') +const testType = ref('speed_blitz') +const testLoading = ref(false) +const testResult = ref<{ + success: boolean + payload?: any + response?: any + correct?: boolean | null + error?: string + elapsed?: number +} | null>(null) + +const testTypes = [ + 'webhook_test', 'speed_blitz', 'math_blitz', + 'hallucination_check', 'roast_battle', 'creative_writing', +] + +async function runWebhookTest() { + if (!testUrl.value) return + testLoading.value = true + testResult.value = null + try { + const res = await fetch('/api/docs/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: testUrl.value, type: testType.value }), + }) + testResult.value = await res.json() + } catch { + testResult.value = { success: false, error: 'Network error' } + } finally { + testLoading.value = false + } +} + const registrationTest = `{ "fight_id": "test_000000", "round": 0, @@ -649,6 +762,43 @@ const registrationTest = `{
{{ systemPrompt }}
+ +
+
+

+ EXAMPLE BOT (NODE.JS) +

+ +
+

+ Zero dependencies. Save as bot.mjs, run with: node bot.mjs +

+
{{ nodeBot }}
+
+ + +
+
+

+ TESTING WITH CURL +

+ +
+
{{ curlExample }}
+
+

@@ -677,10 +827,93 @@ const registrationTest = `{

-
+
+ + +
+

+ WEBHOOK TESTER +

+

+ Paste your webhook URL, pick a challenge type, and we'll send a real test payload. +

+ +
+
+ + +
+ +
+ +
+ +
+
+ + +
+ + +
+
+ + {{ testResult.success ? 'PASS' : 'FAIL' }} + + + {{ testResult.elapsed }}ms + + CORRECT + WRONG ANSWER +
+ +
+

{{ testResult.error }}

+
+ +
+

SENT

+
{{ JSON.stringify(testResult.payload, null, 2) }}
+
+ +
+

RECEIVED

+
{{ JSON.stringify(testResult.response, null, 2) }}
+
+
+
-
-
+

TIPS

diff --git a/server/src/routes/docs.ts b/server/src/routes/docs.ts index d43f494..1d5e147 100644 --- a/server/src/routes/docs.ts +++ b/server/src/routes/docs.ts @@ -162,3 +162,123 @@ docsRouter.get('/webhook', (c) => { ], }) }) + +// POST /test — interactive webhook tester (no auth required) +docsRouter.post('/test', async (c) => { + const body = await c.req.json() + const { url, type } = body as { url?: string; type?: string } + + if (!url || typeof url !== 'string') { + return c.json({ error: 'Missing "url" field' }, 400) + } + + // Basic URL validation — must be https, no private IPs + let parsed: URL + try { + parsed = new URL(url) + } catch { + return c.json({ error: 'Invalid URL' }, 400) + } + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + return c.json({ error: 'URL must use https://' }, 400) + } + const host = parsed.hostname + if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.') || host.startsWith('10.') || host.endsWith('.local')) { + return c.json({ error: 'Cannot test private/local URLs' }, 400) + } + + const challengeTypes: Record = { + speed_blitz: { challenge: 'What is the largest planet in our solar system?', answers: ['jupiter'] }, + math_blitz: { challenge: 'What is 17 * 23?', answers: ['391'] }, + hallucination_check: { challenge: 'True or false: The Great Wall of China is visible from space with the naked eye.', answers: ['false'] }, + roast_battle: { challenge: 'Roast your opponent who calls themselves "test_bot".' }, + creative_writing: { challenge: 'Write a haiku about a robot learning to fight.' }, + webhook_test: { challenge: 'Respond with {"answer": "pong"}', answers: ['pong'] }, + } + + const selectedType = (type && challengeTypes[type]) ? type : 'speed_blitz' + const ct = challengeTypes[selectedType] + + const payload = { + fight_id: 'test_000000', + round: 1, + type: selectedType, + challenge: ct.challenge, + constraints: { timeout_ms: 10000, max_tokens: 500 }, + opponent: { name: 'test_bot', wins: 42, losses: 10 }, + arena: 'test_arena', + arena_modifier: null, + } + + const startMs = Date.now() + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }) + clearTimeout(timeout) + + const elapsed = Date.now() - startMs + + if (!res.ok) { + return c.json({ + success: false, + payload, + error: `HTTP ${res.status} ${res.statusText}`, + elapsed, + }) + } + + let responseBody: any + try { + responseBody = await res.json() + } catch { + return c.json({ + success: false, + payload, + error: 'Response is not valid JSON', + elapsed, + }) + } + + const answer = responseBody?.answer + if (typeof answer !== 'string') { + return c.json({ + success: false, + payload, + response: responseBody, + error: 'Missing "answer" field in response', + elapsed, + }) + } + + // Check correctness for factual types + let correct: boolean | null = null + if (ct.answers) { + const normalized = answer.toLowerCase().trim() + correct = ct.answers.some(a => normalized.includes(a)) + } + + return c.json({ + success: true, + payload, + response: responseBody, + correct, + elapsed, + }) + } catch (err: unknown) { + const elapsed = Date.now() - startMs + const message = err instanceof Error ? err.message : 'Unknown error' + return c.json({ + success: false, + payload, + error: message.includes('abort') ? 'Timeout (10s)' : message, + elapsed, + }) + } +})