feat(09-03): point every in-app setup surface at the one prompt, add copy affordances
CI / check (push) Failing after 6m7s
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:
@@ -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}}')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 */ }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user