feat: bot setup intro step with code template, safety info, payload examples

Adds a new "HOW IT WORKS" step to the join flow between naming and webhook:
- 3-step visual explainer (POST challenge, respond, best answer wins)
- "YOUR SERVER IS SAFE" callout with security details (one-way comms,
  private IP blocking, size limits, timeouts)
- Expandable starter bot code (Node.js) with copy button
- Collapsible example challenge payload with response format
- Improved webhook step with deployment tips (ngrok, Vercel, Railway)
- Container now scrollable for longer content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 10:18:46 +00:00
co-authored by Claude Opus 4.6
parent 33f08c35b5
commit 120250c01a
2 changed files with 941 additions and 29 deletions
+184 -22
View File
@@ -7,7 +7,7 @@ import SpritePreview from '../components/SpritePreview.vue'
const router = useRouter()
const { pubkey, bot, profilePicUrl, isLoggedIn, hasExtension, isLoading, login, registerBot, logout } = useNostr()
// Steps: 'login' | 'pick-character' | 'name-bot' | 'add-webhook' | 'ready'
// Steps: 'login' | 'pick-character' | 'name-bot' | 'bot-setup' | 'add-webhook' | 'ready'
const step = ref<string>('login')
const error = ref('')
const isJoining = ref(false)
@@ -18,6 +18,35 @@ let pollHandle: ReturnType<typeof setInterval> | null = null
const selectedArchetype = ref('standard')
const botName = ref('')
const webhookUrl = ref('')
const showCode = ref(false)
const codeCopied = ref(false)
const BOT_CODE = `const http = require('http')
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' })
return res.end(JSON.stringify({ status: 'ok' }))
}
let body = ''
req.on('data', c => { body += c })
req.on('end', () => {
const { type, challenge } = JSON.parse(body)
// Your bot logic goes here — answer the challenge!
const answer = type === 'webhook_test' ? 'pong' : challenge
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ answer, trash_talk: 'Too easy.' }))
})
})
server.listen(3000, () => console.log('Bot running on :3000'))
`
function copyCode() {
navigator.clipboard.writeText(BOT_CODE)
codeCopied.value = true
setTimeout(() => { codeCopied.value = false }, 2000)
}
const archetypeList = [
{ id: 'standard', label: 'FIGHTER', desc: 'Classic brawler' },
@@ -100,7 +129,7 @@ function confirmName() {
return
}
error.value = ''
step.value = 'add-webhook'
step.value = 'bot-setup'
}
async function confirmWebhook() {
@@ -151,7 +180,7 @@ function handleSignOut() {
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
<div class="min-h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 py-8 overflow-y-auto">
<div class="max-w-md w-full slide-up">
<!-- STEP: LOGIN -->
@@ -273,33 +302,109 @@ function handleSignOut() {
</div>
</template>
<!-- STEP: ADD WEBHOOK -->
<template v-else-if="step === 'add-webhook'">
<div class="text-center mb-6">
<!-- STEP: BOT SETUP how it works + code + safety -->
<template v-else-if="step === 'bot-setup'">
<div class="text-center mb-5">
<h2 class="font-display font-black text-2xl tracking-wider gradient-text mb-2">
ADD WEBHOOK
HOW IT WORKS
</h2>
<p class="font-mono text-text-muted text-xs">
Where we POST fight challenges to <span class="text-neon-cyan">{{ botName }}</span>.
Your bot is a tiny server that answers fight challenges.
</p>
</div>
<div class="mb-5">
<input
v-model="webhookUrl"
type="url"
required
placeholder="https://your-bot.example.com/fight"
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
text-text-primary placeholder-text-muted
focus:outline-none focus:border-neon-cyan/50 transition-colors"
@keyup.enter="confirmWebhook"
/>
<!-- How it works steps -->
<div class="space-y-2.5 mb-5">
<div class="flex items-start gap-3 p-2.5 border border-border bg-surface">
<span class="font-display font-black text-neon-cyan text-sm mt-0.5">1</span>
<div>
<p class="font-mono text-xs text-text-primary">We POST a challenge to your server</p>
<p class="font-mono text-[10px] text-text-muted mt-0.5">JSON with the question, type, and opponent info</p>
</div>
</div>
<div class="flex items-start gap-3 p-2.5 border border-border bg-surface">
<span class="font-display font-black text-neon-cyan text-sm mt-0.5">2</span>
<div>
<p class="font-mono text-xs text-text-primary">Your bot responds with an answer</p>
<p class="font-mono text-[10px] text-text-muted mt-0.5">JSON with your answer and optional trash talk</p>
</div>
</div>
<div class="flex items-start gap-3 p-2.5 border border-border bg-surface">
<span class="font-display font-black text-neon-cyan text-sm mt-0.5">3</span>
<div>
<p class="font-mono text-xs text-text-primary">Best answer wins the round</p>
<p class="font-mono text-[10px] text-text-muted mt-0.5">5-10 rounds per fight. Speed matters when tied.</p>
</div>
</div>
</div>
<!-- Safety callout -->
<div class="p-3 border border-neon-cyan/20 bg-neon-cyan/5 mb-5">
<p class="font-display font-bold text-[10px] tracking-wider text-neon-cyan mb-1.5">
YOUR SERVER IS SAFE
</p>
<ul class="font-mono text-[10px] text-text-muted space-y-1 leading-relaxed">
<li>We only send <span class="text-text-secondary">POST</span> requests with fight questions</li>
<li>We never read from your server only send challenges</li>
<li>Private IPs and internal URLs are <span class="text-text-secondary">blocked</span></li>
<li>Payloads are small JSON (<span class="text-text-secondary">&lt;2KB</span>), responses capped at <span class="text-text-secondary">10KB</span></li>
<li>5 second timeout we give up fast</li>
<li>All communication is <span class="text-text-secondary">one-way</span>: we ask, you answer</li>
</ul>
</div>
<!-- Starter code toggle -->
<button
class="w-full py-2 mb-3 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 text-center"
@click="showCode = !showCode"
>
{{ showCode ? 'HIDE' : 'SHOW' }} STARTER BOT CODE (NODE.JS)
</button>
<div v-if="showCode" class="mb-4">
<div class="relative">
<pre class="bg-bg border border-border p-3 text-[10px] font-mono text-text-muted overflow-x-auto max-h-[35vh] leading-relaxed"><code>{{ BOT_CODE }}</code></pre>
<button
class="absolute top-2 right-2 px-2 py-1 text-[9px] font-display font-bold tracking-wider
border border-border hover:border-neon-cyan/40 hover:text-neon-cyan transition-all"
:class="codeCopied ? 'text-neon-cyan border-neon-cyan/40' : 'text-text-muted'"
@click="copyCode"
>
{{ codeCopied ? 'COPIED' : 'COPY' }}
</button>
</div>
<p class="font-mono text-[10px] text-text-muted mt-1.5">
We POST challenge payloads here during fights.
Save as <span class="text-text-secondary">bot.js</span>, run <span class="text-text-secondary">node bot.js</span>, expose with <span class="text-text-secondary">ngrok http 3000</span>
</p>
</div>
<!-- Example payload -->
<details class="mb-5 group">
<summary class="font-display font-bold text-[10px] tracking-wider text-text-secondary cursor-pointer
hover:text-neon-purple transition-colors select-none">
EXAMPLE CHALLENGE PAYLOAD
</summary>
<pre class="mt-2 bg-bg border border-border p-3 text-[10px] font-mono text-text-muted overflow-x-auto leading-relaxed"><code>{
"fight_id": "f_abc123",
"round": 1,
"type": "speed_blitz",
"challenge": "What is the capital of France?",
"constraints": {
"timeout_ms": 8000,
"max_tokens": 500
},
"opponent": {
"name": "skull_crusher",
"wins": 12,
"losses": 3
}
}</code></pre>
<p class="font-mono text-[10px] text-text-muted mt-1.5">
Your response: <span class="text-text-secondary">{"answer": "Paris", "trash_talk": "Too easy."}</span>
</p>
</details>
<div class="flex gap-2">
<button
class="flex-1 py-3 border-2 border-border text-text-muted font-display font-bold text-sm tracking-wider
@@ -308,13 +413,70 @@ function handleSignOut() {
>
BACK
</button>
<button
class="flex-1 py-3 bg-neon-cyan/10 border-2 border-neon-cyan/50 text-neon-cyan
font-display font-bold text-sm tracking-wider
hover:bg-neon-cyan/20 transition-all"
@click="step = 'add-webhook'"
>
GOT IT, NEXT
</button>
</div>
</template>
<!-- STEP: ADD WEBHOOK -->
<template v-else-if="step === 'add-webhook'">
<div class="text-center mb-5">
<h2 class="font-display font-black text-2xl tracking-wider gradient-text mb-2">
CONNECT YOUR BOT
</h2>
<p class="font-mono text-text-muted text-xs">
Paste the public URL where <span class="text-neon-cyan">{{ botName }}</span> is running.
</p>
</div>
<div class="mb-3">
<input
v-model="webhookUrl"
type="url"
required
placeholder="https://your-bot.example.com/fight"
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
text-text-primary placeholder-text-muted
focus:outline-none focus:border-neon-cyan/50 transition-colors"
@keyup.enter="confirmWebhook"
/>
</div>
<!-- Webhook tips -->
<div class="p-2.5 border border-border bg-surface mb-5">
<p class="font-display font-bold text-[10px] tracking-wider text-text-secondary mb-1.5">
QUICK SETUP OPTIONS
</p>
<ul class="font-mono text-[10px] text-text-muted space-y-1">
<li><span class="text-text-secondary">Local dev?</span> Use ngrok, cloudflared, or localtunnel to expose your port</li>
<li><span class="text-text-secondary">Serverless?</span> Deploy to Vercel, Railway, or Fly.io</li>
<li><span class="text-text-secondary">VPS?</span> Any public HTTPS endpoint works</li>
</ul>
</div>
<div class="flex gap-2">
<button
class="flex-1 py-3 border-2 border-border text-text-muted font-display font-bold text-sm tracking-wider
hover:border-neon-purple/40 transition-all"
@click="step = 'bot-setup'"
>
BACK
</button>
<button
class="flex-1 py-3 bg-neon-pink/10 border-2 border-neon-pink/50 text-neon-pink
font-display font-bold text-sm tracking-wider
hover:bg-neon-pink/20 transition-all"
hover:bg-neon-pink/20 transition-all
disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="!webhookUrl.trim()"
@click="confirmWebhook"
>
CREATE FIGHTER
TEST & CREATE
</button>
</div>
</template>