Update AI personas to include new subscription tiers and unlockable badges

Introduce a new 'Pro' tier for AI personas, update database schemas with tier and Stripe fields, and create a 'badges' table to manage persona access through earned badges.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 66a80a5c-6d47-429a-93e1-cf315013edf0
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/LFvmEVc
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-12 23:12:45 +00:00
parent b7bac6aa0b
commit a4cf099b2c
5 changed files with 251 additions and 26 deletions

12
.replit
View file

@ -47,18 +47,6 @@ externalPort = 3003
localPort = 8080
externalPort = 8080
[[ports]]
localPort = 38557
externalPort = 3000
[[ports]]
localPort = 40437
externalPort = 3001
[[ports]]
localPort = 44157
externalPort = 4200
[deployment]
deploymentTarget = "autoscale"
run = ["node", "dist/server/production.mjs"]

View file

@ -1,5 +1,6 @@
import { Type } from '@google/genai';
import type { Persona } from './types';
import type { Persona, UserTier, UserBadgeInfo } from './types';
import { canAccessPersona } from './types';
import type { FunctionDeclaration } from '@google/genai';
export const AETHEX_TOOLS: FunctionDeclaration[] = [
@ -158,7 +159,8 @@ Tone: Stern but encouraging. Focus on "shipping," not "dreaming."`,
"May reject creative but complex ideas",
"Tone is intentionally strict/stern"
],
requiredTier: 'Architect',
requiredTier: 'Pro',
unlockBadgeSlug: 'forge_apprentice',
realm: 'gameforge'
},
{
@ -198,7 +200,8 @@ Constraint: Do not hallucinate certifications (like 8(a) or HUBZone) if the user
"Cannot verify official certifications (8(a), HUBZone)",
"Does not guarantee contract awards"
],
requiredTier: 'Architect',
requiredTier: 'Pro',
unlockBadgeSlug: 'sbs_scholar',
realm: 'corp'
},
{
@ -238,7 +241,8 @@ Constraint: Keep language appropriate for a classroom setting.`,
"Lesson plans are theoretical structures",
"Cannot grade student work"
],
requiredTier: 'Architect',
requiredTier: 'Pro',
unlockBadgeSlug: 'curriculum_creator',
realm: 'labs'
},
{
@ -276,7 +280,8 @@ Tone: Concise, data-driven, executive. No fluff.`,
"Analysis depends on user-provided data accuracy",
"No financial liability for advice"
],
requiredTier: 'Architect',
requiredTier: 'Pro',
unlockBadgeSlug: 'data_pioneer',
realm: 'corp'
},
{
@ -314,6 +319,7 @@ Your Job: Output a structured "Audio Brief" for a composer:
"Subjective artistic interpretation"
],
requiredTier: 'Council',
unlockBadgeSlug: 'sound_designer',
realm: 'gameforge'
},
{
@ -352,6 +358,7 @@ Tone: Dark, mysterious, neon-soaked.`,
"Restricted to Cyberpunk/Sci-Fi themes"
],
requiredTier: 'Council',
unlockBadgeSlug: 'lore_master',
realm: 'gameforge'
},
{
@ -386,7 +393,8 @@ Your Job:
"Lyrics are text-only output",
"Mood is locked to Retrowave aesthetics"
],
requiredTier: 'Architect',
requiredTier: 'Pro',
unlockBadgeSlug: 'synthwave_artist',
realm: 'labs'
},
{
@ -423,7 +431,8 @@ Your Job:
"Advice is satirical/entertainment focused",
"Does not actually invest money"
],
requiredTier: 'Architect',
requiredTier: 'Pro',
unlockBadgeSlug: 'pitch_survivor',
realm: 'corp'
}
];
@ -432,12 +441,16 @@ export const getPersonasByRealm = (realm: string): Persona[] => {
return PERSONAS.filter(p => p.realm === realm);
};
export const getPersonasByTier = (tier: 'Free' | 'Architect' | 'Council'): Persona[] => {
const tierOrder = { 'Free': 0, 'Architect': 1, 'Council': 2 };
export const getPersonasByTier = (tier: UserTier): Persona[] => {
const tierOrder: Record<UserTier, number> = { 'Free': 0, 'Pro': 1, 'Council': 2 };
const userTierLevel = tierOrder[tier];
return PERSONAS.filter(p => tierOrder[p.requiredTier] <= userTierLevel);
};
export const getAccessiblePersonas = (tier: UserTier, badges: UserBadgeInfo[]): Persona[] => {
return PERSONAS.filter(p => canAccessPersona(tier, p.requiredTier, badges, p.unlockBadgeSlug));
};
export const getDefaultPersona = (): Persona => {
return PERSONAS.find(p => p.id === 'network_agent') || PERSONAS[0];
};

View file

@ -1,4 +1,5 @@
import type { FunctionDeclaration } from '@google/genai';
import type { SubscriptionTier } from '../database.types';
export interface ChatMessage {
role: 'user' | 'model';
@ -24,7 +25,8 @@ export interface Persona {
theme: PersonaTheme;
capabilities: string[];
limitations: string[];
requiredTier: 'Free' | 'Architect' | 'Council';
requiredTier: UserTier;
unlockBadgeSlug?: string;
realm?: string;
}
@ -51,14 +53,75 @@ export interface ChatSession {
timestamp: number;
}
export type UserTier = 'Free' | 'Architect' | 'Council';
export type UserTier = 'Free' | 'Pro' | 'Council';
export const TIER_HIERARCHY: Record<UserTier, number> = {
'Free': 0,
'Architect': 1,
'Pro': 1,
'Council': 2,
};
export const canAccessPersona = (userTier: UserTier, requiredTier: UserTier): boolean => {
return TIER_HIERARCHY[userTier] >= TIER_HIERARCHY[requiredTier];
export const dbTierToUserTier = (dbTier: SubscriptionTier | null | undefined): UserTier => {
if (!dbTier) return 'Free';
switch (dbTier) {
case 'council': return 'Council';
case 'pro': return 'Pro';
default: return 'Free';
}
};
export const userTierToDbTier = (userTier: UserTier): SubscriptionTier => {
switch (userTier) {
case 'Council': return 'council';
case 'Pro': return 'pro';
default: return 'free';
}
};
export interface UserBadgeInfo {
slug: string;
name: string;
earnedAt: string;
}
export interface PersonaAccessContext {
tier: UserTier;
badges: UserBadgeInfo[];
}
export const canAccessPersona = (
userTier: UserTier,
requiredTier: UserTier,
userBadges?: UserBadgeInfo[],
unlockBadgeSlug?: string
): boolean => {
if (TIER_HIERARCHY[userTier] >= TIER_HIERARCHY[requiredTier]) {
return true;
}
if (unlockBadgeSlug && userBadges) {
return userBadges.some(badge => badge.slug === unlockBadgeSlug);
}
return false;
};
export const getPersonaAccessReason = (
userTier: UserTier,
requiredTier: UserTier,
userBadges?: UserBadgeInfo[],
unlockBadgeSlug?: string
): { hasAccess: boolean; reason: 'tier' | 'badge' | 'none'; badgeName?: string } => {
if (TIER_HIERARCHY[userTier] >= TIER_HIERARCHY[requiredTier]) {
return { hasAccess: true, reason: 'tier' };
}
if (unlockBadgeSlug && userBadges) {
const badge = userBadges.find(b => b.slug === unlockBadgeSlug);
if (badge) {
return { hasAccess: true, reason: 'badge', badgeName: badge.name };
}
}
return { hasAccess: false, reason: 'none' };
};

View file

@ -396,6 +396,9 @@ export type Database = {
longest_streak: number | null;
last_streak_at: string | null;
total_xp: number | null;
tier: Database["public"]["Enums"]["subscription_tier_enum"] | null;
stripe_customer_id: string | null;
stripe_subscription_id: string | null;
twitter_url: string | null;
updated_at: string;
user_type: Database["public"]["Enums"]["user_type_enum"];
@ -420,6 +423,9 @@ export type Database = {
longest_streak?: number | null;
last_streak_at?: string | null;
total_xp?: number | null;
tier?: Database["public"]["Enums"]["subscription_tier_enum"] | null;
stripe_customer_id?: string | null;
stripe_subscription_id?: string | null;
twitter_url?: string | null;
updated_at?: string;
user_type: Database["public"]["Enums"]["user_type_enum"];
@ -444,6 +450,9 @@ export type Database = {
longest_streak?: number | null;
last_streak_at?: string | null;
total_xp?: number | null;
tier?: Database["public"]["Enums"]["subscription_tier_enum"] | null;
stripe_customer_id?: string | null;
stripe_subscription_id?: string | null;
twitter_url?: string | null;
updated_at?: string;
user_type?: Database["public"]["Enums"]["user_type_enum"];
@ -460,6 +469,75 @@ export type Database = {
},
];
};
badges: {
Row: {
id: string;
name: string;
slug: string;
description: string | null;
icon: string | null;
unlock_criteria: string | null;
unlocks_persona: string | null;
created_at: string;
};
Insert: {
id?: string;
name: string;
slug: string;
description?: string | null;
icon?: string | null;
unlock_criteria?: string | null;
unlocks_persona?: string | null;
created_at?: string;
};
Update: {
id?: string;
name?: string;
slug?: string;
description?: string | null;
icon?: string | null;
unlock_criteria?: string | null;
unlocks_persona?: string | null;
created_at?: string;
};
Relationships: [];
};
user_badges: {
Row: {
id: string;
user_id: string;
badge_id: string;
earned_at: string;
};
Insert: {
id?: string;
user_id: string;
badge_id: string;
earned_at?: string;
};
Update: {
id?: string;
user_id?: string;
badge_id?: string;
earned_at?: string;
};
Relationships: [
{
foreignKeyName: "user_badges_user_id_fkey";
columns: ["user_id"];
isOneToOne: false;
referencedRelation: "user_profiles";
referencedColumns: ["id"];
},
{
foreignKeyName: "user_badges_badge_id_fkey";
columns: ["badge_id"];
isOneToOne: false;
referencedRelation: "badges";
referencedColumns: ["id"];
},
];
};
};
Views: {
[_ in never]: never;
@ -474,6 +552,7 @@ export type Database = {
| "advanced"
| "expert";
project_status_enum: "planning" | "in_progress" | "completed" | "on_hold";
subscription_tier_enum: "free" | "pro" | "council";
user_type_enum:
| "game_developer"
| "client"
@ -498,3 +577,6 @@ export type UserType = Database["public"]["Enums"]["user_type_enum"];
export type ExperienceLevel =
Database["public"]["Enums"]["experience_level_enum"];
export type ProjectStatus = Database["public"]["Enums"]["project_status_enum"];
export type SubscriptionTier = Database["public"]["Enums"]["subscription_tier_enum"];
export type Badge = Database["public"]["Tables"]["badges"]["Row"];
export type UserBadge = Database["public"]["Tables"]["user_badges"]["Row"];

View file

@ -0,0 +1,79 @@
-- Migration: Add tier and badges system for AI persona access
-- Run this migration in your Supabase SQL Editor
-- 1. Create subscription tier enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'subscription_tier_enum') THEN
CREATE TYPE subscription_tier_enum AS ENUM ('free', 'pro', 'council');
END IF;
END $$;
-- 2. Add tier and Stripe columns to user_profiles
ALTER TABLE user_profiles
ADD COLUMN IF NOT EXISTS tier subscription_tier_enum DEFAULT 'free',
ADD COLUMN IF NOT EXISTS stripe_customer_id TEXT,
ADD COLUMN IF NOT EXISTS stripe_subscription_id TEXT;
-- 3. Create badges table
CREATE TABLE IF NOT EXISTS badges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT,
icon TEXT,
unlock_criteria TEXT,
unlocks_persona TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 4. Create user_badges junction table
CREATE TABLE IF NOT EXISTS user_badges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
badge_id UUID NOT NULL REFERENCES badges(id) ON DELETE CASCADE,
earned_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, badge_id)
);
-- 5. Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_user_badges_user_id ON user_badges(user_id);
CREATE INDEX IF NOT EXISTS idx_user_badges_badge_id ON user_badges(badge_id);
CREATE INDEX IF NOT EXISTS idx_badges_slug ON badges(slug);
CREATE INDEX IF NOT EXISTS idx_user_profiles_tier ON user_profiles(tier);
CREATE INDEX IF NOT EXISTS idx_user_profiles_stripe_customer ON user_profiles(stripe_customer_id);
-- 6. Enable RLS on new tables
ALTER TABLE badges ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_badges ENABLE ROW LEVEL SECURITY;
-- 7. RLS Policies for badges (read-only for authenticated users)
CREATE POLICY IF NOT EXISTS "Badges are viewable by everyone"
ON badges FOR SELECT
USING (true);
-- 8. RLS Policies for user_badges
CREATE POLICY IF NOT EXISTS "Users can view their own badges"
ON user_badges FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY IF NOT EXISTS "Users can view others badges"
ON user_badges FOR SELECT
USING (true);
-- 9. Seed initial badges that unlock AI personas
INSERT INTO badges (name, slug, description, icon, unlock_criteria, unlocks_persona) VALUES
('Forge Apprentice', 'forge_apprentice', 'Complete 3 game design reviews with Forge Master', 'hammer', 'Complete 3 game design reviews', 'forge_master'),
('SBS Scholar', 'sbs_scholar', 'Create 5 business profiles with SBS Architect', 'building', 'Create 5 business profiles', 'sbs_architect'),
('Curriculum Creator', 'curriculum_creator', 'Generate 10 lesson plans with Curriculum Weaver', 'book', 'Generate 10 lesson plans', 'curriculum_weaver'),
('Data Pioneer', 'data_pioneer', 'Analyze 20 datasets with QuantumLeap', 'chart', 'Analyze 20 datasets', 'quantum_leap'),
('Synthwave Artist', 'synthwave_artist', 'Write 15 song lyrics with Vapor', 'wave', 'Write 15 song lyrics', 'vapor'),
('Pitch Survivor', 'pitch_survivor', 'Receive 10 critiques from Apex VC', 'money', 'Receive 10 critiques', 'apex'),
('Sound Designer', 'sound_designer', 'Generate 25 audio briefs with Ethos Producer', 'music', 'Generate 25 audio briefs', 'ethos_producer'),
('Lore Master', 'lore_master', 'Create 50 lore entries with AeThex Archivist', 'scroll', 'Create 50 lore entries', 'aethex_archivist')
ON CONFLICT (slug) DO NOTHING;
-- 10. Grant permissions
GRANT SELECT ON badges TO authenticated;
GRANT SELECT ON user_badges TO authenticated;
GRANT INSERT, DELETE ON user_badges TO authenticated;