feat: API docs page — tabbed webhook spec, scoring, challenges, tips

Fetches from /api/docs/webhook and renders as a styled tabbed page.
Added /docs route and DOCS nav link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 00:26:12 +00:00
co-authored by Claude Opus 4.6
parent 3b8208be08
commit 530f46f9cf
4 changed files with 269 additions and 1 deletions
+1 -1
View File
@@ -413,7 +413,7 @@ Integrated into the fight viewer:
- [x] Fight schedule / card system
- [x] Commentary engine
- [x] Achievement system
- [ ] API docs page
- [x] API docs page
### Phase 4: Go Live
- [ ] Docker containerization
+1
View File
@@ -11,6 +11,7 @@ const links = [
{ to: '/join', label: 'FIGHT!' },
{ to: '/arena', label: 'WATCH' },
{ to: '/leaderboard', label: 'RANKINGS' },
{ to: '/docs', label: 'DOCS' },
]
</script>
+262
View File
@@ -0,0 +1,262 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface DocsData {
title: string
version: string
overview: string
webhook_request: any
webhook_response: any
scoring: any
failure_modes: Record<string, string>
challenge_types: { factual: any[]; creative: any[] }
testing: Record<string, any>
tips: string[]
}
const docs = ref<DocsData | null>(null)
const error = ref('')
const activeTab = ref<'webhook' | 'scoring' | 'challenges' | 'testing'>('webhook')
onMounted(async () => {
try {
const res = await fetch('/api/docs/webhook')
if (!res.ok) throw new Error('Failed to load docs')
docs.value = await res.json()
} catch {
error.value = 'Could not load API docs.'
}
})
</script>
<template>
<div class="min-h-[calc(100vh-4rem)] px-6 py-8 max-w-4xl mx-auto">
<div class="slide-up">
<div class="text-center mb-8">
<h2 class="font-display font-black text-3xl tracking-wider gradient-text mb-2">
API DOCS
</h2>
<p class="font-mono text-text-muted text-xs">
How to build a fighter. Webhook spec, scoring, and tips.
</p>
</div>
<div v-if="error" class="p-4 border-2 bg-ko/5 border-ko/30 text-ko font-mono text-xs">
{{ error }}
</div>
<div v-if="docs">
<!-- Tab nav -->
<div class="flex gap-1 mb-6 border-b border-border">
<button
v-for="tab in (['webhook', 'scoring', 'challenges', 'testing'] as const)"
:key="tab"
class="px-4 py-2 font-display font-bold text-[10px] uppercase tracking-[0.15em] transition-colors border-b-2 -mb-px"
:class="activeTab === tab
? 'text-neon-cyan border-neon-cyan'
: 'text-text-muted border-transparent hover:text-text-secondary'"
@click="activeTab = tab"
>
{{ tab }}
</button>
</div>
<!-- WEBHOOK TAB -->
<div v-if="activeTab === 'webhook'" class="space-y-6">
<p class="font-mono text-text-secondary text-xs leading-relaxed">
{{ docs.overview }}
</p>
<!-- Request format -->
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
REQUEST (POST to your webhook)
</h3>
<div class="space-y-2">
<div
v-for="(field, key) in docs.webhook_request.fields"
:key="key"
class="flex gap-3 font-mono text-xs"
>
<span class="text-neon-cyan shrink-0 w-28">{{ key }}</span>
<span class="text-text-muted">{{ field.type }}</span>
<span class="text-text-secondary">{{ field.description }}</span>
</div>
</div>
<div class="mt-4">
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Example</p>
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ JSON.stringify(docs.webhook_request.example, null, 2) }}</pre>
</div>
</div>
<!-- Response format -->
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
RESPONSE (your bot returns)
</h3>
<div class="space-y-2">
<div
v-for="(field, key) in docs.webhook_response.fields"
:key="key"
class="flex gap-3 font-mono text-xs"
>
<span class="text-neon-cyan shrink-0 w-28">{{ key }}</span>
<span class="text-text-muted">{{ field.type }}</span>
<span class="text-text-secondary">{{ field.description }}</span>
<span v-if="field.required === false" class="text-text-muted italic">(optional)</span>
</div>
</div>
<div class="mt-4">
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Example</p>
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ JSON.stringify(docs.webhook_response.example, null, 2) }}</pre>
</div>
</div>
<!-- Failure modes -->
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
FAILURE MODES
</h3>
<div class="space-y-2">
<div
v-for="(desc, mode) in docs.failure_modes"
:key="mode"
class="flex gap-3 font-mono text-xs"
>
<span class="text-ko shrink-0 w-28">{{ mode }}</span>
<span class="text-text-secondary">{{ desc }}</span>
</div>
</div>
</div>
</div>
<!-- SCORING TAB -->
<div v-if="activeTab === 'scoring'" class="space-y-6">
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
FACTUAL CHALLENGES
</h3>
<p class="font-mono text-text-secondary text-xs mb-3">
{{ docs.scoring.factual_challenges.description }}
</p>
<div class="space-y-3">
<div>
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-1">Answer Matching</p>
<ul class="space-y-1">
<li
v-for="rule in docs.scoring.factual_challenges.matching_rules"
:key="rule"
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20"
>
{{ rule }}
</li>
</ul>
</div>
<div>
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-1">Scoring</p>
<ul class="space-y-1">
<li
v-for="rule in docs.scoring.factual_challenges.scoring_rules"
:key="rule"
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/20"
>
{{ rule }}
</li>
</ul>
</div>
</div>
</div>
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
CREATIVE CHALLENGES
</h3>
<p class="font-mono text-text-secondary text-xs mb-3">
{{ docs.scoring.creative_challenges.description }}
</p>
<ul class="space-y-1">
<li
v-for="rule in docs.scoring.creative_challenges.scoring_rules"
:key="rule"
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/20"
>
{{ rule }}
</li>
</ul>
</div>
</div>
<!-- CHALLENGES TAB -->
<div v-if="activeTab === 'challenges'" class="space-y-6">
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
FACTUAL ({{ docs.challenge_types.factual.length }})
</h3>
<div class="space-y-2">
<div
v-for="ct in docs.challenge_types.factual"
:key="ct.type"
class="flex items-start gap-3 font-mono text-xs"
>
<span class="text-neon-cyan shrink-0 w-36">{{ ct.type }}</span>
<span class="text-text-muted shrink-0 w-16">{{ ct.timeout_ms }}ms</span>
<span class="text-text-secondary">{{ ct.description }}</span>
</div>
</div>
</div>
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
CREATIVE ({{ docs.challenge_types.creative.length }})
</h3>
<div class="space-y-2">
<div
v-for="ct in docs.challenge_types.creative"
:key="ct.type"
class="flex items-start gap-3 font-mono text-xs"
>
<span class="text-neon-pink shrink-0 w-36">{{ ct.type }}</span>
<span class="text-text-muted shrink-0 w-16">{{ ct.timeout_ms }}ms</span>
<span class="text-text-secondary">{{ ct.description }}</span>
</div>
</div>
</div>
</div>
<!-- TESTING TAB -->
<div v-if="activeTab === 'testing'" class="space-y-6">
<div
v-for="(test, key) in docs.testing"
:key="key"
class="border-2 border-border bg-surface p-4"
>
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-2">
{{ (test as any).method }} {{ (test as any).path }}
</h3>
<p class="font-mono text-text-secondary text-xs">
{{ (test as any).description }}
</p>
</div>
<!-- Tips -->
<div class="border-2 border-border bg-surface p-4">
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
TIPS
</h3>
<ul class="space-y-2">
<li
v-for="(tip, i) in docs.tips"
:key="i"
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20"
>
{{ tip }}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
+5
View File
@@ -51,6 +51,11 @@ const routes = [
name: 'sprites',
component: () => import('./pages/SpritePreviewPage.vue'),
},
{
path: '/docs',
name: 'docs',
component: () => import('./pages/DocsPage.vue'),
},
]
export const router = createRouter({