diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index 526b761..9b610df 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -2,6 +2,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue' import { useRoute, useRouter, RouterLink } from 'vue-router' import { useNostr, type NostrProfile } from '../composables/useNostr' +import { authFetch } from '../lib/nostr-auth' import SpritePreview from '../components/SpritePreview.vue' import HumanPreview from '../components/HumanPreview.vue' import WalletConnect from '../components/WalletConnect.vue' @@ -97,6 +98,17 @@ const showCustomize = ref(false) const isSaving = ref(false) const custError = ref('') +// Claim winnings (Cashu payouts sitting unclaimed — see payments.ts payWinner: +// mints a bearer token server-side when the winner has no NWC/Lightning-address +// wallet linked, which is the common case for a Cashu-primary bot). Fetched +// proactively (not behind a toggle) since this is the owner's own money. +interface UnclaimedWinning { paymentId: string; amountSats: number } +const unclaimedWinnings = ref([]) +const claimingId = ref(null) +const claimError = ref('') +const claimedTokens = ref<{ paymentId: string; amountSats: number; token: string }[]>([]) +const tokenCopiedId = ref(null) + // Webhook management const showWebhook = ref(false) const webhookInput = ref('') @@ -106,6 +118,68 @@ const webhookTestResult = ref<{ reachable: boolean; validResponse: boolean; late const webhookError = ref('') const webhookSuccess = ref('') +// AI-answer settings (existing bot — see /api/bots/:name/ai-config). Same +// feature as JoinBoutPage's creation-time setup, but reachable afterward: +// that flow only ever had the bot's own secret in hand at the moment of +// creation, with nowhere to come back to later. +const showAiConfig = ref(false) +const aiConfigLoaded = ref(false) +const aiConfigured = ref(false) +const aiConfigProvider = ref<'anthropic' | 'openai' | null>(null) +const aiProviderInput = ref<'anthropic' | 'openai'>('anthropic') +const aiApiKeyInput = ref('') +const aiConfigSaving = ref(false) +const aiConfigError = ref('') + +async function loadAiConfig() { + if (!stats.value || aiConfigLoaded.value) return + try { + const res = await authFetch(`/api/bots/${encodeURIComponent(botName)}/ai-config`) + if (res.ok) { + const data = await res.json() as { configured: boolean; provider: 'anthropic' | 'openai' | null } + aiConfigured.value = data.configured + aiConfigProvider.value = data.provider + } + } catch (err) { + console.warn('[BotProfile] ai-config load failed:', err) + } + aiConfigLoaded.value = true +} + +async function saveAiConfig() { + if (!aiApiKeyInput.value.trim() || aiConfigSaving.value) return + aiConfigSaving.value = true + aiConfigError.value = '' + try { + const res = await authFetch(`/api/bots/${encodeURIComponent(botName)}/ai-config`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ provider: aiProviderInput.value, apiKey: aiApiKeyInput.value.trim() }), + }) + const data = await res.json() + if (!res.ok) { + aiConfigError.value = data.error || 'Failed to save API key.' + return + } + aiConfigured.value = true + aiConfigProvider.value = aiProviderInput.value + aiApiKeyInput.value = '' // never keep the raw key in page state longer than needed + } catch { + aiConfigError.value = 'Connection failed. Try again.' + } finally { + aiConfigSaving.value = false + } +} + +async function removeAiConfig() { + try { + await authFetch(`/api/bots/${encodeURIComponent(botName)}/ai-config`, { method: 'DELETE' }) + } finally { + aiConfigured.value = false + aiConfigProvider.value = null + } +} + // Setup guide const showSetupGuide = ref(false) const isRegenerating = ref(false) @@ -240,6 +314,45 @@ async function saveCustomization() { isSaving.value = false } +async function fetchUnclaimedWinnings() { + if (!stats.value || !isOwner.value) return + try { + const res = await authFetch(`/api/payments/winnings/${stats.value.id}`) + if (res.ok) { + const data = await res.json() as { unclaimed: UnclaimedWinning[] } + unclaimedWinnings.value = data.unclaimed || [] + } + } catch (err) { + console.warn('[BotProfile] winnings fetch failed:', err) + } +} + +async function claimWinning(paymentId: string, amountSats: number) { + if (claimingId.value) return + claimingId.value = paymentId + claimError.value = '' + try { + const res = await authFetch(`/api/payments/claim/${paymentId}`, { method: 'POST' }) + const data = await res.json() + if (!res.ok) { + claimError.value = data.error || 'Claim failed.' + return + } + claimedTokens.value.unshift({ paymentId, amountSats, token: data.cashuToken }) + unclaimedWinnings.value = unclaimedWinnings.value.filter(w => w.paymentId !== paymentId) + } catch { + claimError.value = 'Network error claiming winnings.' + } finally { + claimingId.value = null + } +} + +function copyClaimedToken(paymentId: string, token: string) { + navigator.clipboard.writeText(token) + tokenCopiedId.value = paymentId + setTimeout(() => { if (tokenCopiedId.value === paymentId) tokenCopiedId.value = null }, 2500) +} + async function testWebhook() { if (!stats.value || isTestingWebhook.value) return isTestingWebhook.value = true @@ -343,6 +456,10 @@ onMounted(async () => { }).catch(err => console.warn('[BotProfile] webhook test failed:', err)) } + // Unclaimed Cashu winnings (non-blocking, owner only — fetchUnclaimedWinnings + // itself checks isOwner, but stats must be loaded first) + fetchUnclaimedWinnings() + // Poll queue for "choose your fight" pollQueue() pollHandle = setInterval(pollQueue, 4000) @@ -602,6 +719,56 @@ const tierClass = (t: number) => `tier-${t}` + +
+

+ 🏆 YOU WON {{ unclaimedWinnings.reduce((s, w) => s + w.amountSats, 0) }} SATS — CLAIM YOUR CASHU +

+
+ {{ w.amountSats }} sats + +
+

{{ claimError }}

+
+ + +
+
+

+ ✓ CLAIMED {{ c.amountSats }} SATS — paste into your Cashu wallet +

+
+

+ {{ c.token }} +

+ +
+
+
+
@@ -834,6 +1001,78 @@ const tierClass = (t: number) => `tier-${t}`
+ +
+ + +
+

+ Let BotFights answer poll-mode fights for you using your own + Anthropic or OpenAI API key — no script or webhook required. +

+ +
Loading...
+ +
+ + ✓ Configured ({{ aiConfigProvider }}) + + +
+ +
+
+ +
+ + +
+ +

{{ aiConfigError }}

+
+
+