feat(09-03): point every in-app setup surface at the one prompt, add copy affordances
CI / check (push) Failing after 6m7s

- JoinBoutPage.vue: setupDocPath()/setupDocName() collapse to /docs/BOTFIGHTS.md
  regardless of connection mode; keep YOUR_BOT_ID/YOUR_BOT_SECRET substitution
  and add a third substitution for {{ARENA_URL}} -> window.location.origin
- BotProfilePage.vue: same collapse; the webhook/polling guide-type selector
  is gone (there's only one guide now) -- replaced with a single 'LOAD SETUP
  GUIDE' button, same credential + {{ARENA_URL}} substitution
- DocsPage.vue: new 'GIVE THIS TO YOUR AI' panel at the top of the page --
  copy-to-clipboard for the full /api/docs/prompt response, plus the literal
  URL shown so a user can hand an agent the link instead of the text.
  Existing API reference / webhook tester tabs unchanged below it
- e2e/signup-bot.spec.ts: two new tests -- docs page shows the copy
  affordance, and GET /api/docs/prompt (via page.request) returns 200 with
  the registration endpoint and no leftover {{ARENA_URL}} token
This commit is contained in:
Dorian
2026-07-30 22:24:48 -04:00
parent a0809565f2
commit 2dd9947516
4 changed files with 95 additions and 35 deletions
+17
View File
@@ -41,3 +41,20 @@ test.describe('bot registration flow', () => {
await expect(page.getByText(/choose your fighter/i).first()).toBeVisible({ timeout: 5_000 }) await expect(page.getByText(/choose your fighter/i).first()).toBeVisible({ timeout: 5_000 })
}) })
}) })
test.describe('unified AI bot-setup prompt (BOT-02)', () => {
test('docs page shows the "give this to your AI" copy affordance', async ({ page }) => {
await page.goto('/docs')
await expect(page.getByText(/give this to your ai/i).first()).toBeVisible({ timeout: 10_000 })
await expect(page.getByText(/copy full prompt/i).first()).toBeVisible({ timeout: 5_000 })
})
test('GET /api/docs/prompt returns the self-contained prompt an agent could consume', async ({ page }) => {
await page.goto('/docs')
const res = await page.request.get('/api/docs/prompt')
expect(res.status()).toBe(200)
const body = await res.text()
expect(body).toContain('/api/bots')
expect(body).not.toContain('{{ARENA_URL}}')
})
})
+14 -33
View File
@@ -115,7 +115,6 @@ const regenError = ref('')
const guideContent = ref('') const guideContent = ref('')
const guideLoading = ref(false) const guideLoading = ref(false)
const guideCopied = ref(false) const guideCopied = ref(false)
const guideMode = ref<'webhook' | 'polling'>('webhook')
const ARCHETYPES = [ const ARCHETYPES = [
'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat', 'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat',
@@ -294,17 +293,16 @@ async function handleRegenerateSecret() {
isRegenerating.value = false isRegenerating.value = false
} }
async function loadGuide(mode: 'webhook' | 'polling') { async function loadGuide() {
guideMode.value = mode
guideLoading.value = true guideLoading.value = true
guideContent.value = '' guideContent.value = ''
guideCopied.value = false guideCopied.value = false
const path = mode === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md'
try { try {
const res = await fetch(path) const res = await fetch('/docs/BOTFIGHTS.md')
let content = await res.text() let content = await res.text()
content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value) content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value)
content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value) content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value)
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
guideContent.value = content guideContent.value = content
} catch { } catch {
guideContent.value = '# Failed to load setup guide' guideContent.value = '# Failed to load setup guide'
@@ -856,33 +854,22 @@ const tierClass = (t: number) => `tier-${t}`
<p class="font-mono text-[9px] text-ko mt-2">Save this now. It will not be shown again after you leave this page.</p> <p class="font-mono text-[9px] text-ko mt-2">Save this now. It will not be shown again after you leave this page.</p>
</div> </div>
<!-- Guide type selector --> <!-- Load the unified setup guide -->
<div class="flex gap-2"> <button
<button v-if="!guideContent"
class="flex-1 py-1.5 border font-display font-bold text-[10px] tracking-wider transition-all" class="w-full py-1.5 border border-border text-text-secondary font-display font-bold text-[10px]
:class="guideMode === 'webhook' tracking-wider hover:border-neon-purple/40 hover:text-neon-purple transition-all"
? 'border-neon-cyan/50 text-neon-cyan bg-neon-cyan/10' :disabled="guideLoading"
: 'border-border text-text-muted hover:border-neon-cyan/30'" @click="loadGuide()"
@click="loadGuide('webhook')" >
> {{ guideLoading ? 'LOADING...' : 'LOAD SETUP GUIDE' }}
WEBHOOK </button>
</button>
<button
class="flex-1 py-1.5 border font-display font-bold text-[10px] tracking-wider transition-all"
:class="guideMode === 'polling'
? 'border-neon-purple/50 text-neon-purple bg-neon-purple/10'
: 'border-border text-text-muted hover:border-neon-purple/30'"
@click="loadGuide('polling')"
>
POLLING
</button>
</div>
<!-- Guide content --> <!-- Guide content -->
<div v-if="guideContent" class="border border-border bg-black/40 overflow-hidden"> <div v-if="guideContent" class="border border-border bg-black/40 overflow-hidden">
<div class="flex items-center justify-between px-3 py-2 border-b border-border/50 bg-surface-raised/30"> <div class="flex items-center justify-between px-3 py-2 border-b border-border/50 bg-surface-raised/30">
<span class="font-display font-bold text-[9px] tracking-wider text-text-muted"> <span class="font-display font-bold text-[9px] tracking-wider text-text-muted">
{{ guideMode === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md' }} BOTFIGHTS.md
</span> </span>
<button <button
class="font-display font-bold text-[9px] tracking-wider px-2 py-0.5 border transition-all" class="font-display font-bold text-[9px] tracking-wider px-2 py-0.5 border transition-all"
@@ -897,12 +884,6 @@ const tierClass = (t: number) => `tier-${t}`
<pre class="p-3 font-mono text-[10px] text-text-secondary leading-relaxed <pre class="p-3 font-mono text-[10px] text-text-secondary leading-relaxed
overflow-x-auto max-h-60 overflow-y-auto whitespace-pre-wrap break-words select-all">{{ guideContent }}</pre> overflow-x-auto max-h-60 overflow-y-auto whitespace-pre-wrap break-words select-all">{{ guideContent }}</pre>
</div> </div>
<div v-else-if="guideLoading" class="p-4 text-center">
<p class="font-mono text-[10px] text-text-muted animate-pulse">Loading...</p>
</div>
<p v-else class="font-mono text-[10px] text-text-muted text-center">
Choose webhook or polling above to view the setup guide.
</p>
</template> </template>
<!-- Not yet regenerated show button --> <!-- Not yet regenerated show button -->
+60
View File
@@ -36,6 +36,32 @@ function copyText(text: string, id: string) {
setTimeout(() => { if (copiedId.value === id) copiedId.value = '' }, 2000) setTimeout(() => { if (copiedId.value === id) copiedId.value = '' }, 2000)
} }
// ── "Give this to your AI" — the single self-contained setup prompt (BOT-02) ──
const promptUrl = computed(() => `${window.location.origin}/api/docs/prompt`)
const promptLoading = ref(false)
const promptCopied = ref<'' | 'url' | 'text'>('')
function copyPromptUrl() {
navigator.clipboard.writeText(promptUrl.value)
promptCopied.value = 'url'
setTimeout(() => { if (promptCopied.value === 'url') promptCopied.value = '' }, 2000)
}
async function copyFullPromptText() {
promptLoading.value = true
try {
const res = await fetch('/api/docs/prompt')
const text = await res.text()
navigator.clipboard.writeText(text)
promptCopied.value = 'text'
setTimeout(() => { if (promptCopied.value === 'text') promptCopied.value = '' }, 2000)
} catch {
// no-op — user can retry
} finally {
promptLoading.value = false
}
}
const tabs = ['quickstart', 'api', 'challenges', 'scoring', 'security', 'testing'] as const const tabs = ['quickstart', 'api', 'challenges', 'scoring', 'security', 'testing'] as const
// ── Code examples ── // ── Code examples ──
@@ -542,6 +568,40 @@ async function runWebhookTest() {
{{ error }} {{ error }}
</div> </div>
<!-- "Give this to your AI" the one self-contained setup prompt -->
<div class="border-2 border-neon-cyan/40 bg-neon-cyan/5 p-5 mb-4 shrink-0">
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-2">
GIVE THIS TO YOUR AI
</h3>
<p class="font-mono text-text-muted text-[10px] mb-3">
One self-contained prompt covers everything: registration, credentials, both
protocols, every endpoint. Paste it into your AI, or hand it the URL below no other
docs required.
</p>
<div class="flex items-center gap-2 mb-3">
<code class="flex-1 bg-bg border border-border px-3 py-2 font-mono text-[10px] text-neon-cyan overflow-x-auto whitespace-nowrap">{{ promptUrl }}</code>
<button
class="px-3 py-2 text-[9px] font-display font-bold uppercase tracking-wider border transition-colors shrink-0"
:class="promptCopied === 'url'
? 'text-neon-green border-neon-green/50'
: 'text-text-muted border-border hover:border-neon-cyan/50'"
@click="copyPromptUrl"
>
{{ promptCopied === 'url' ? 'COPIED' : 'COPY URL' }}
</button>
</div>
<button
class="w-full py-2.5 font-display font-bold text-xs uppercase tracking-wider border-2 transition-all"
:class="promptLoading
? 'border-border/30 text-text-muted cursor-not-allowed'
: (promptCopied === 'text' ? 'border-neon-green text-neon-green' : 'border-neon-cyan text-neon-cyan hover:bg-neon-cyan/10')"
:disabled="promptLoading"
@click="copyFullPromptText"
>
{{ promptLoading ? 'LOADING...' : (promptCopied === 'text' ? 'COPIED!' : 'COPY FULL PROMPT') }}
</button>
</div>
<!-- Tab nav --> <!-- Tab nav -->
<div class="flex flex-wrap gap-1 mb-4 border-b border-border shrink-0"> <div class="flex flex-wrap gap-1 mb-4 border-b border-border shrink-0">
<button <button
+4 -2
View File
@@ -520,11 +520,11 @@ const setupContentCopied = ref(false)
watch(connectionMode, () => { setupContent.value = ''; showSetupContent.value = false }) watch(connectionMode, () => { setupContent.value = ''; showSetupContent.value = false })
function setupDocPath() { function setupDocPath() {
return connectionMode.value === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md' return '/docs/BOTFIGHTS.md'
} }
function setupDocName() { function setupDocName() {
return connectionMode.value === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md' return 'BOTFIGHTS.md'
} }
async function toggleSetupContent() { async function toggleSetupContent() {
@@ -536,6 +536,7 @@ async function toggleSetupContent() {
let content = await res.text() let content = await res.text()
content = content.replace(/YOUR_BOT_ID/g, botId.value) content = content.replace(/YOUR_BOT_ID/g, botId.value)
content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value)
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
setupContent.value = content setupContent.value = content
} catch { } catch {
setupContent.value = '# Failed to load setup guide' setupContent.value = '# Failed to load setup guide'
@@ -557,6 +558,7 @@ async function copyFullPrompt() {
let content = await res.text() let content = await res.text()
content = content.replace(/YOUR_BOT_ID/g, botId.value) content = content.replace(/YOUR_BOT_ID/g, botId.value)
content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value)
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
setupContent.value = content setupContent.value = content
} catch { /* fall through with empty content */ } } catch { /* fall through with empty content */ }
} }