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:
@@ -115,7 +115,6 @@ const regenError = ref('')
|
||||
const guideContent = ref('')
|
||||
const guideLoading = ref(false)
|
||||
const guideCopied = ref(false)
|
||||
const guideMode = ref<'webhook' | 'polling'>('webhook')
|
||||
|
||||
const ARCHETYPES = [
|
||||
'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat',
|
||||
@@ -294,17 +293,16 @@ async function handleRegenerateSecret() {
|
||||
isRegenerating.value = false
|
||||
}
|
||||
|
||||
async function loadGuide(mode: 'webhook' | 'polling') {
|
||||
guideMode.value = mode
|
||||
async function loadGuide() {
|
||||
guideLoading.value = true
|
||||
guideContent.value = ''
|
||||
guideCopied.value = false
|
||||
const path = mode === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md'
|
||||
try {
|
||||
const res = await fetch(path)
|
||||
const res = await fetch('/docs/BOTFIGHTS.md')
|
||||
let content = await res.text()
|
||||
content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value)
|
||||
content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value)
|
||||
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
|
||||
guideContent.value = content
|
||||
} catch {
|
||||
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>
|
||||
</div>
|
||||
|
||||
<!-- Guide type selector -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-1.5 border font-display font-bold text-[10px] tracking-wider transition-all"
|
||||
:class="guideMode === 'webhook'
|
||||
? 'border-neon-cyan/50 text-neon-cyan bg-neon-cyan/10'
|
||||
: 'border-border text-text-muted hover:border-neon-cyan/30'"
|
||||
@click="loadGuide('webhook')"
|
||||
>
|
||||
WEBHOOK
|
||||
</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>
|
||||
<!-- Load the unified setup guide -->
|
||||
<button
|
||||
v-if="!guideContent"
|
||||
class="w-full py-1.5 border border-border text-text-secondary font-display font-bold text-[10px]
|
||||
tracking-wider hover:border-neon-purple/40 hover:text-neon-purple transition-all"
|
||||
:disabled="guideLoading"
|
||||
@click="loadGuide()"
|
||||
>
|
||||
{{ guideLoading ? 'LOADING...' : 'LOAD SETUP GUIDE' }}
|
||||
</button>
|
||||
|
||||
<!-- Guide content -->
|
||||
<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">
|
||||
<span class="font-display font-bold text-[9px] tracking-wider text-text-muted">
|
||||
{{ guideMode === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md' }}
|
||||
BOTFIGHTS.md
|
||||
</span>
|
||||
<button
|
||||
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
|
||||
overflow-x-auto max-h-60 overflow-y-auto whitespace-pre-wrap break-words select-all">{{ guideContent }}</pre>
|
||||
</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>
|
||||
|
||||
<!-- Not yet regenerated — show button -->
|
||||
|
||||
@@ -36,6 +36,32 @@ function copyText(text: string, id: string) {
|
||||
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
|
||||
|
||||
// ── Code examples ──
|
||||
@@ -542,6 +568,40 @@ async function runWebhookTest() {
|
||||
{{ error }}
|
||||
</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 -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 border-b border-border shrink-0">
|
||||
<button
|
||||
|
||||
@@ -520,11 +520,11 @@ const setupContentCopied = ref(false)
|
||||
watch(connectionMode, () => { setupContent.value = ''; showSetupContent.value = false })
|
||||
|
||||
function setupDocPath() {
|
||||
return connectionMode.value === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md'
|
||||
return '/docs/BOTFIGHTS.md'
|
||||
}
|
||||
|
||||
function setupDocName() {
|
||||
return connectionMode.value === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md'
|
||||
return 'BOTFIGHTS.md'
|
||||
}
|
||||
|
||||
async function toggleSetupContent() {
|
||||
@@ -536,6 +536,7 @@ async function toggleSetupContent() {
|
||||
let content = await res.text()
|
||||
content = content.replace(/YOUR_BOT_ID/g, botId.value)
|
||||
content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value)
|
||||
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
|
||||
setupContent.value = content
|
||||
} catch {
|
||||
setupContent.value = '# Failed to load setup guide'
|
||||
@@ -557,6 +558,7 @@ async function copyFullPrompt() {
|
||||
let content = await res.text()
|
||||
content = content.replace(/YOUR_BOT_ID/g, botId.value)
|
||||
content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value)
|
||||
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
|
||||
setupContent.value = content
|
||||
} catch { /* fall through with empty content */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user