mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
In the app the portal painted the site's page glow, an opaque slab, over the pane's own fill, inset by the pane's padding. It takes `framed` now: no glow, no window height, the pane's fill and scroll. The dashboard counted points the server does not award (local actions, +25 for asking for Alpha) and promised rewards nothing pays (48 spots, a free week, 10% tokens back, +1 per $1, "Linked:" on every account). The score is the server's `score` once it answers one; until then verified points plus the referrals it counts. The copy states what scores: +1 per signup, +20 per referral that pays, +1 per 100K tokens, +10 per star, +1 per follow. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
import { api } from '@hanzo/app/lib/api'
|
|
import { scope } from '@hanzo/app/lib/session'
|
|
import type { Program } from '@/components/beta/program'
|
|
|
|
/**
|
|
* Beta records, waitlist standing, ecosystem beta requests, and growth actions
|
|
* on hanzoai/cloud apps/marketing/beta.go:
|
|
*
|
|
* GET /v1/marketing/beta the person's record, requested betas, points, and waitlist rank
|
|
* PUT /v1/marketing/beta save program, questionnaire answers, and requested betas
|
|
* POST /v1/marketing/beta/request request access to specific feature-flagged ecosystem betas (or alpha)
|
|
* POST /v1/marketing/beta/actions record a verified GitHub repo star (+10 pts) or founder follow (+1 pt)
|
|
*/
|
|
|
|
export interface BetaAnswer {
|
|
id: string
|
|
question: string
|
|
answer: string[]
|
|
}
|
|
|
|
export interface BetaAction {
|
|
action: 'star' | 'follow'
|
|
target: string
|
|
points: number
|
|
at: string
|
|
}
|
|
|
|
export interface BetaStanding {
|
|
applied: boolean
|
|
program: Program
|
|
answers: BetaAnswer[]
|
|
betas: string[]
|
|
requested: Record<string, string>
|
|
github: string
|
|
points: number
|
|
/** The waitlist score (points plus referrals and tokens); the server's, absent until it answers one. */
|
|
score?: number
|
|
place: number
|
|
of: number
|
|
actions: BetaAction[]
|
|
lead: string
|
|
since: string
|
|
}
|
|
|
|
export const POINTS = {
|
|
star: 10,
|
|
follow: 1,
|
|
referralSignup: 1,
|
|
referralDollar: 1,
|
|
} as const
|
|
|
|
export const HANZO_REPOS = [
|
|
{ slug: 'hanzoai/dev', label: 'hanzoai/dev', desc: 'Autonomous coding agent for terminal & IDE' },
|
|
{ slug: 'hanzoai/ai', label: 'hanzoai/ai', desc: 'Hanzo AI SDK, router, and agent framework' },
|
|
{ slug: 'hanzoai/engine', label: 'hanzoai/engine', desc: 'High-performance CUDA & Metal inference engine' },
|
|
{ slug: 'hanzoai/cloud', label: 'hanzoai/cloud', desc: 'Sovereign AI cloud & unified backend' },
|
|
{ slug: 'hanzoai/mcp', label: 'hanzoai/mcp', desc: 'Model Context Protocol tools & browser extension' },
|
|
{ slug: 'hanzoai/ui', label: 'hanzoai/ui', desc: 'Multi-framework React, GUI & design system' },
|
|
] as const
|
|
|
|
export const HANZO_SOCIALS = [
|
|
{ kind: 'discord' as const, label: 'Discord', handle: 'discord.gg/XthHQQj', href: 'https://discord.gg/XthHQQj' },
|
|
{ kind: 'x' as const, label: 'X @hanzoai', handle: '@hanzoai', href: 'https://x.com/hanzoai' },
|
|
{ kind: 'github' as const, label: 'GitHub @hanzoai', handle: '@hanzoai', href: 'https://github.com/hanzoai' },
|
|
{ kind: 'linkedin' as const, label: 'LinkedIn', handle: 'hanzo-ai', href: 'https://linkedin.com/company/hanzo-ai' },
|
|
{ kind: 'facebook' as const, label: 'Facebook', handle: '@hanzoinc', href: 'https://facebook.com/hanzoinc' },
|
|
{ kind: 'youtube' as const, label: 'YouTube @HanzoAI', handle: '@HanzoAI', href: 'https://youtube.com/@HanzoAI' },
|
|
{ kind: 'bluesky' as const, label: 'Bluesky', handle: '@hanzoai.bsky.social', href: 'https://bsky.app/profile/hanzoai.bsky.social' },
|
|
{ kind: 'reddit' as const, label: 'Reddit r/HanzoAI', handle: 'r/HanzoAI', href: 'https://www.reddit.com/r/HanzoAI/' },
|
|
{ kind: 'tiktok' as const, label: 'TikTok @hanzoinc', handle: '@hanzoinc', href: 'https://www.tiktok.com/@hanzoinc' },
|
|
{ kind: 'instagram' as const, label: 'Instagram @hanzoinc', handle: '@hanzoinc', href: 'https://www.instagram.com/hanzoinc' },
|
|
] as const
|
|
|
|
/** The founders, as hanzo.ai/about names them: Antje Worring, CEO; Zach Kelling, founding CTO. */
|
|
export const HANZO_FOUNDERS = [
|
|
{
|
|
id: 'antje' as const,
|
|
name: 'Antje Worring',
|
|
role: 'CEO',
|
|
channels: [
|
|
{ kind: 'github' as const, label: 'GitHub @zooqueen', href: 'https://github.com/zooqueen' },
|
|
{ kind: 'linkedin' as const, label: 'LinkedIn', href: 'https://www.linkedin.com/in/antje-worring/' },
|
|
],
|
|
},
|
|
{
|
|
id: 'zach' as const,
|
|
name: 'Zach Kelling',
|
|
role: 'Founding CTO',
|
|
channels: [
|
|
{ kind: 'github' as const, label: 'GitHub @zeekay', href: 'https://github.com/zeekay' },
|
|
{ kind: 'x' as const, label: 'X @zeekay', href: 'https://x.com/zeekay' },
|
|
{ kind: 'linkedin' as const, label: 'LinkedIn', href: 'https://www.linkedin.com/in/zeekay/' },
|
|
],
|
|
},
|
|
] as const
|
|
|
|
/**
|
|
* Stored with `hz_` prefix (like `hz_ref` in referrals.ts) so `own(subject())`
|
|
* does not clear them when an anonymous visitor signs up or logs in.
|
|
*/
|
|
const LOCAL_BETAS_KEY = 'hz_beta_requested'
|
|
const LOCAL_GITHUB_KEY = 'hz_beta_github'
|
|
const LOCAL_ACTIONS_KEY = 'hz_beta_actions'
|
|
|
|
export function getLocalRequestedBetas(): string[] {
|
|
if (typeof window === 'undefined') return []
|
|
try {
|
|
const raw = JSON.parse(window.localStorage.getItem(LOCAL_BETAS_KEY) ?? '[]')
|
|
return Array.isArray(raw) ? raw.filter((x): x is string => typeof x === 'string') : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function setLocalRequestedBetas(ids: string[]): void {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
const unique = Array.from(new Set(ids))
|
|
window.localStorage.setItem(LOCAL_BETAS_KEY, JSON.stringify(unique))
|
|
} catch {
|
|
/* ignore storage errors */
|
|
}
|
|
}
|
|
|
|
export function getLocalGithub(): string {
|
|
if (typeof window === 'undefined') return ''
|
|
try {
|
|
return window.localStorage.getItem(LOCAL_GITHUB_KEY) ?? ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export function setLocalGithub(gh: string): void {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
if (gh) window.localStorage.setItem(LOCAL_GITHUB_KEY, gh)
|
|
else window.localStorage.removeItem(LOCAL_GITHUB_KEY)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
export function getLocalActions(): BetaAction[] {
|
|
if (typeof window === 'undefined') return []
|
|
try {
|
|
const raw = JSON.parse(window.localStorage.getItem(LOCAL_ACTIONS_KEY) ?? '[]')
|
|
return Array.isArray(raw) ? raw : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function setLocalActions(actions: BetaAction[]): void {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
window.localStorage.setItem(LOCAL_ACTIONS_KEY, JSON.stringify(actions))
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function normalizeStanding(raw: Partial<BetaStanding>): BetaStanding {
|
|
const prog = raw.program === 'research' || raw.program === 'partner' ? raw.program : 'beta'
|
|
return {
|
|
applied: Boolean(raw.applied || raw.program),
|
|
program: prog,
|
|
answers: Array.isArray(raw.answers) ? raw.answers : [],
|
|
betas: Array.isArray(raw.betas) ? raw.betas : [],
|
|
requested: raw.requested && typeof raw.requested === 'object' ? raw.requested : {},
|
|
github: typeof raw.github === 'string' ? raw.github : '',
|
|
points: typeof raw.points === 'number' ? raw.points : 0,
|
|
score: typeof raw.score === 'number' ? raw.score : undefined,
|
|
place: typeof raw.place === 'number' ? raw.place : 0,
|
|
of: typeof raw.of === 'number' ? raw.of : 0,
|
|
actions: Array.isArray(raw.actions) ? raw.actions : [],
|
|
lead: typeof raw.lead === 'string' ? raw.lead : '',
|
|
since: typeof raw.since === 'string' ? raw.since : '',
|
|
}
|
|
}
|
|
|
|
/** Read the authenticated user's beta record and waitlist standing. */
|
|
export async function standing(): Promise<BetaStanding | null> {
|
|
const headers = scope()
|
|
if (!headers.Authorization) return null
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta`, { headers })
|
|
if (!res.ok) return null
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return normalizeStanding(body)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Save program, answers, and requested betas on the user's record. */
|
|
export async function keep(payload: {
|
|
program: Program
|
|
answers?: BetaAnswer[]
|
|
betas?: string[]
|
|
lead?: string
|
|
}): Promise<BetaStanding | null> {
|
|
const headers = scope()
|
|
if (!headers.Authorization) return null
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (!res.ok) return null
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return normalizeStanding(body)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Request access to one or more ecosystem betas (or 'alpha'). Also persists locally when signed out. */
|
|
export async function ask(betas: string[]): Promise<BetaStanding | null> {
|
|
const clean = betas.map((b) => b.trim().toLowerCase()).filter(Boolean)
|
|
if (!clean.length) return null
|
|
|
|
const existingLocal = getLocalRequestedBetas()
|
|
setLocalRequestedBetas([...existingLocal, ...clean])
|
|
|
|
const headers = scope()
|
|
if (!headers.Authorization) return null
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta/request`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify({ betas: clean }),
|
|
})
|
|
if (!res.ok) return null
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return normalizeStanding(body)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Record a verified GitHub repo star (+10 pts) or founder follow (+1 pt). */
|
|
export async function act(
|
|
action: 'star' | 'follow',
|
|
target: string,
|
|
github?: string,
|
|
): Promise<{ ok: boolean; standing?: BetaStanding; error?: string }> {
|
|
if (github) setLocalGithub(github)
|
|
|
|
const headers = scope()
|
|
if (!headers.Authorization) {
|
|
// Save locally until the user signs in
|
|
const current = getLocalActions()
|
|
if (!current.some((a) => a.action === action && a.target === target)) {
|
|
const pts = action === 'star' ? POINTS.star : POINTS.follow
|
|
setLocalActions([...current, { action, target, points: pts, at: new Date().toISOString() }])
|
|
}
|
|
return { ok: true }
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta/actions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify({ action, target, ...(github ? { github } : {}) }),
|
|
})
|
|
if (!res.ok) {
|
|
let msg = 'Could not verify action right now.'
|
|
try {
|
|
const errBody = (await res.json()) as { message?: string; error?: string }
|
|
msg = errBody.message || errBody.error || msg
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return { ok: false, error: msg }
|
|
}
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return { ok: true, standing: normalizeStanding(body) }
|
|
} catch {
|
|
return { ok: false, error: 'Network error while verifying action.' }
|
|
}
|
|
}
|
|
|
|
/** Check whether a star or follow action has already been recorded. */
|
|
export function did(actions: BetaAction[], action: 'star' | 'follow', target: string): boolean {
|
|
return actions.some((a) => a.action === action && a.target === target)
|
|
}
|