feat: interactive bot developer documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e10c1dc8aa
commit
c654ff9f73
@@ -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 = `{
|
||||
<pre class="bg-bg p-3 text-[11px] font-mono text-text-secondary overflow-x-auto max-h-[400px] overflow-y-auto whitespace-pre-wrap">{{ systemPrompt }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Node.js bot -->
|
||||
<div class="border-2 border-border bg-surface p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider">
|
||||
EXAMPLE BOT (NODE.JS)
|
||||
</h3>
|
||||
<button
|
||||
class="px-3 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-pink/50 bg-bg transition-colors"
|
||||
:class="copiedId === 'node' ? 'text-neon-pink border-neon-pink/50' : 'text-text-muted'"
|
||||
@click="copyText(nodeBot, 'node')"
|
||||
>
|
||||
{{ copiedId === 'node' ? 'COPIED' : 'COPY CODE' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="font-mono text-text-muted text-[10px] mb-3">
|
||||
Zero dependencies. Save as bot.mjs, run with: node bot.mjs
|
||||
</p>
|
||||
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-pink/80 overflow-x-auto max-h-[500px] overflow-y-auto">{{ nodeBot }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Curl example -->
|
||||
<div class="border-2 border-border bg-surface p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider">
|
||||
TESTING WITH CURL
|
||||
</h3>
|
||||
<button
|
||||
class="px-3 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-cyan/50 bg-bg transition-colors"
|
||||
:class="copiedId === 'curl' ? 'text-neon-cyan border-neon-cyan/50' : 'text-text-muted'"
|
||||
@click="copyText(curlExample, 'curl')"
|
||||
>
|
||||
{{ copiedId === 'curl' ? 'COPIED' : 'COPY' }}
|
||||
</button>
|
||||
</div>
|
||||
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto max-h-[300px] overflow-y-auto">{{ curlExample }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Character customization -->
|
||||
<div class="border-2 border-border bg-surface p-5">
|
||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||
@@ -677,10 +827,93 @@ const registrationTest = `{
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════ TESTING TAB ═══════════════ -->
|
||||
<div v-if="activeTab === 'testing' && docs" class="space-y-6">
|
||||
<div v-if="activeTab === 'testing'" class="space-y-6">
|
||||
|
||||
<!-- Interactive webhook tester -->
|
||||
<div class="border-2 border-neon-pink/30 bg-surface p-5">
|
||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-4">
|
||||
WEBHOOK TESTER
|
||||
</h3>
|
||||
<p class="font-mono text-text-muted text-[10px] mb-4">
|
||||
Paste your webhook URL, pick a challenge type, and we'll send a real test payload.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-[9px] font-display font-bold text-text-muted uppercase tracking-wider block mb-1">Webhook URL</label>
|
||||
<input
|
||||
v-model="testUrl"
|
||||
type="url"
|
||||
placeholder="https://your-bot.example.com/webhook"
|
||||
class="w-full bg-bg border-2 border-border px-3 py-2 font-mono text-xs text-text-primary placeholder-text-muted/40 focus:border-neon-pink/50 focus:outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-[9px] font-display font-bold text-text-muted uppercase tracking-wider block mb-1">Challenge Type</label>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="t in testTypes"
|
||||
:key="t"
|
||||
class="px-2.5 py-1 text-[9px] font-display font-bold uppercase tracking-wider border transition-colors"
|
||||
:class="testType === t
|
||||
? 'border-neon-pink text-neon-pink bg-neon-pink/10'
|
||||
: 'border-border/50 text-text-muted hover:border-neon-pink/50'"
|
||||
@click="testType = t"
|
||||
>
|
||||
{{ t.replace('_', ' ') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-full py-2.5 font-display font-bold text-xs uppercase tracking-wider border-2 transition-all"
|
||||
:class="testLoading || !testUrl
|
||||
? 'border-border/30 text-text-muted cursor-not-allowed'
|
||||
: 'border-neon-pink text-neon-pink hover:bg-neon-pink/10'"
|
||||
:disabled="testLoading || !testUrl"
|
||||
@click="runWebhookTest"
|
||||
>
|
||||
{{ testLoading ? 'TESTING...' : 'SEND TEST' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Test result -->
|
||||
<div v-if="testResult" class="mt-4 space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="px-2 py-0.5 text-[9px] font-display font-bold uppercase tracking-wider border"
|
||||
:class="testResult.success
|
||||
? 'border-green-400/50 text-green-400 bg-green-400/10'
|
||||
: 'border-red-400/50 text-red-400 bg-red-400/10'"
|
||||
>
|
||||
{{ testResult.success ? 'PASS' : 'FAIL' }}
|
||||
</span>
|
||||
<span v-if="testResult.elapsed" class="font-mono text-text-muted text-[10px]">
|
||||
{{ testResult.elapsed }}ms
|
||||
</span>
|
||||
<span v-if="testResult.correct === true" class="font-mono text-green-400 text-[10px]">CORRECT</span>
|
||||
<span v-if="testResult.correct === false" class="font-mono text-red-400 text-[10px]">WRONG ANSWER</span>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.error" class="bg-bg p-3 border border-red-400/30">
|
||||
<p class="font-mono text-red-400 text-xs">{{ testResult.error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.payload" class="bg-bg p-3">
|
||||
<p class="text-[9px] font-display font-bold text-text-muted uppercase tracking-wider mb-1">SENT</p>
|
||||
<pre class="text-[10px] font-mono text-neon-cyan/70 overflow-x-auto">{{ JSON.stringify(testResult.payload, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.response" class="bg-bg p-3">
|
||||
<p class="text-[9px] font-display font-bold text-text-muted uppercase tracking-wider mb-1">RECEIVED</p>
|
||||
<pre class="text-[10px] font-mono text-neon-pink/70 overflow-x-auto">{{ JSON.stringify(testResult.response, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test endpoints -->
|
||||
<div
|
||||
<div v-if="docs"
|
||||
v-for="(test, key) in docs.testing"
|
||||
:key="key"
|
||||
class="border-2 border-border bg-surface p-5"
|
||||
@@ -699,7 +932,7 @@ const registrationTest = `{
|
||||
</div>
|
||||
|
||||
<!-- Tips -->
|
||||
<div class="border-2 border-neon-cyan/20 bg-neon-cyan/5 p-5">
|
||||
<div v-if="docs" class="border-2 border-neon-cyan/20 bg-neon-cyan/5 p-5">
|
||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||
TIPS
|
||||
</h3>
|
||||
|
||||
@@ -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<string, { challenge: string; answers?: string[] }> = {
|
||||
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,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user