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:
co-authored by
Claude Opus 4.6
parent
3b8208be08
commit
530f46f9cf
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ const links = [
|
||||
{ to: '/join', label: 'FIGHT!' },
|
||||
{ to: '/arena', label: 'WATCH' },
|
||||
{ to: '/leaderboard', label: 'RANKINGS' },
|
||||
{ to: '/docs', label: 'DOCS' },
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user