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:
parent
b7bac6aa0b
commit
a4cf099b2c
5 changed files with 251 additions and 26 deletions
12
.replit
12
.replit
|
|
@ -47,18 +47,6 @@ externalPort = 3003
|
||||||
localPort = 8080
|
localPort = 8080
|
||||||
externalPort = 8080
|
externalPort = 8080
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 38557
|
|
||||||
externalPort = 3000
|
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 40437
|
|
||||||
externalPort = 3001
|
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 44157
|
|
||||||
externalPort = 4200
|
|
||||||
|
|
||||||
[deployment]
|
[deployment]
|
||||||
deploymentTarget = "autoscale"
|
deploymentTarget = "autoscale"
|
||||||
run = ["node", "dist/server/production.mjs"]
|
run = ["node", "dist/server/production.mjs"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Type } from '@google/genai';
|
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';
|
import type { FunctionDeclaration } from '@google/genai';
|
||||||
|
|
||||||
export const AETHEX_TOOLS: FunctionDeclaration[] = [
|
export const AETHEX_TOOLS: FunctionDeclaration[] = [
|
||||||
|
|
@ -158,7 +159,8 @@ Tone: Stern but encouraging. Focus on "shipping," not "dreaming."`,
|
||||||
"May reject creative but complex ideas",
|
"May reject creative but complex ideas",
|
||||||
"Tone is intentionally strict/stern"
|
"Tone is intentionally strict/stern"
|
||||||
],
|
],
|
||||||
requiredTier: 'Architect',
|
requiredTier: 'Pro',
|
||||||
|
unlockBadgeSlug: 'forge_apprentice',
|
||||||
realm: 'gameforge'
|
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)",
|
"Cannot verify official certifications (8(a), HUBZone)",
|
||||||
"Does not guarantee contract awards"
|
"Does not guarantee contract awards"
|
||||||
],
|
],
|
||||||
requiredTier: 'Architect',
|
requiredTier: 'Pro',
|
||||||
|
unlockBadgeSlug: 'sbs_scholar',
|
||||||
realm: 'corp'
|
realm: 'corp'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -238,7 +241,8 @@ Constraint: Keep language appropriate for a classroom setting.`,
|
||||||
"Lesson plans are theoretical structures",
|
"Lesson plans are theoretical structures",
|
||||||
"Cannot grade student work"
|
"Cannot grade student work"
|
||||||
],
|
],
|
||||||
requiredTier: 'Architect',
|
requiredTier: 'Pro',
|
||||||
|
unlockBadgeSlug: 'curriculum_creator',
|
||||||
realm: 'labs'
|
realm: 'labs'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -276,7 +280,8 @@ Tone: Concise, data-driven, executive. No fluff.`,
|
||||||
"Analysis depends on user-provided data accuracy",
|
"Analysis depends on user-provided data accuracy",
|
||||||
"No financial liability for advice"
|
"No financial liability for advice"
|
||||||
],
|
],
|
||||||
requiredTier: 'Architect',
|
requiredTier: 'Pro',
|
||||||
|
unlockBadgeSlug: 'data_pioneer',
|
||||||
realm: 'corp'
|
realm: 'corp'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -314,6 +319,7 @@ Your Job: Output a structured "Audio Brief" for a composer:
|
||||||
"Subjective artistic interpretation"
|
"Subjective artistic interpretation"
|
||||||
],
|
],
|
||||||
requiredTier: 'Council',
|
requiredTier: 'Council',
|
||||||
|
unlockBadgeSlug: 'sound_designer',
|
||||||
realm: 'gameforge'
|
realm: 'gameforge'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -352,6 +358,7 @@ Tone: Dark, mysterious, neon-soaked.`,
|
||||||
"Restricted to Cyberpunk/Sci-Fi themes"
|
"Restricted to Cyberpunk/Sci-Fi themes"
|
||||||
],
|
],
|
||||||
requiredTier: 'Council',
|
requiredTier: 'Council',
|
||||||
|
unlockBadgeSlug: 'lore_master',
|
||||||
realm: 'gameforge'
|
realm: 'gameforge'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -386,7 +393,8 @@ Your Job:
|
||||||
"Lyrics are text-only output",
|
"Lyrics are text-only output",
|
||||||
"Mood is locked to Retrowave aesthetics"
|
"Mood is locked to Retrowave aesthetics"
|
||||||
],
|
],
|
||||||
requiredTier: 'Architect',
|
requiredTier: 'Pro',
|
||||||
|
unlockBadgeSlug: 'synthwave_artist',
|
||||||
realm: 'labs'
|
realm: 'labs'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -423,7 +431,8 @@ Your Job:
|
||||||
"Advice is satirical/entertainment focused",
|
"Advice is satirical/entertainment focused",
|
||||||
"Does not actually invest money"
|
"Does not actually invest money"
|
||||||
],
|
],
|
||||||
requiredTier: 'Architect',
|
requiredTier: 'Pro',
|
||||||
|
unlockBadgeSlug: 'pitch_survivor',
|
||||||
realm: 'corp'
|
realm: 'corp'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -432,12 +441,16 @@ export const getPersonasByRealm = (realm: string): Persona[] => {
|
||||||
return PERSONAS.filter(p => p.realm === realm);
|
return PERSONAS.filter(p => p.realm === realm);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPersonasByTier = (tier: 'Free' | 'Architect' | 'Council'): Persona[] => {
|
export const getPersonasByTier = (tier: UserTier): Persona[] => {
|
||||||
const tierOrder = { 'Free': 0, 'Architect': 1, 'Council': 2 };
|
const tierOrder: Record<UserTier, number> = { 'Free': 0, 'Pro': 1, 'Council': 2 };
|
||||||
const userTierLevel = tierOrder[tier];
|
const userTierLevel = tierOrder[tier];
|
||||||
return PERSONAS.filter(p => tierOrder[p.requiredTier] <= userTierLevel);
|
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 => {
|
export const getDefaultPersona = (): Persona => {
|
||||||
return PERSONAS.find(p => p.id === 'network_agent') || PERSONAS[0];
|
return PERSONAS.find(p => p.id === 'network_agent') || PERSONAS[0];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { FunctionDeclaration } from '@google/genai';
|
import type { FunctionDeclaration } from '@google/genai';
|
||||||
|
import type { SubscriptionTier } from '../database.types';
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
role: 'user' | 'model';
|
role: 'user' | 'model';
|
||||||
|
|
@ -24,7 +25,8 @@ export interface Persona {
|
||||||
theme: PersonaTheme;
|
theme: PersonaTheme;
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
limitations: string[];
|
limitations: string[];
|
||||||
requiredTier: 'Free' | 'Architect' | 'Council';
|
requiredTier: UserTier;
|
||||||
|
unlockBadgeSlug?: string;
|
||||||
realm?: string;
|
realm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,14 +53,75 @@ export interface ChatSession {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserTier = 'Free' | 'Architect' | 'Council';
|
export type UserTier = 'Free' | 'Pro' | 'Council';
|
||||||
|
|
||||||
export const TIER_HIERARCHY: Record<UserTier, number> = {
|
export const TIER_HIERARCHY: Record<UserTier, number> = {
|
||||||
'Free': 0,
|
'Free': 0,
|
||||||
'Architect': 1,
|
'Pro': 1,
|
||||||
'Council': 2,
|
'Council': 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canAccessPersona = (userTier: UserTier, requiredTier: UserTier): boolean => {
|
export const dbTierToUserTier = (dbTier: SubscriptionTier | null | undefined): UserTier => {
|
||||||
return TIER_HIERARCHY[userTier] >= TIER_HIERARCHY[requiredTier];
|
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' };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -396,6 +396,9 @@ export type Database = {
|
||||||
longest_streak: number | null;
|
longest_streak: number | null;
|
||||||
last_streak_at: string | null;
|
last_streak_at: string | null;
|
||||||
total_xp: number | 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;
|
twitter_url: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
user_type: Database["public"]["Enums"]["user_type_enum"];
|
user_type: Database["public"]["Enums"]["user_type_enum"];
|
||||||
|
|
@ -420,6 +423,9 @@ export type Database = {
|
||||||
longest_streak?: number | null;
|
longest_streak?: number | null;
|
||||||
last_streak_at?: string | null;
|
last_streak_at?: string | null;
|
||||||
total_xp?: number | 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;
|
twitter_url?: string | null;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
user_type: Database["public"]["Enums"]["user_type_enum"];
|
user_type: Database["public"]["Enums"]["user_type_enum"];
|
||||||
|
|
@ -444,6 +450,9 @@ export type Database = {
|
||||||
longest_streak?: number | null;
|
longest_streak?: number | null;
|
||||||
last_streak_at?: string | null;
|
last_streak_at?: string | null;
|
||||||
total_xp?: number | 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;
|
twitter_url?: string | null;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
user_type?: Database["public"]["Enums"]["user_type_enum"];
|
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: {
|
Views: {
|
||||||
[_ in never]: never;
|
[_ in never]: never;
|
||||||
|
|
@ -474,6 +552,7 @@ export type Database = {
|
||||||
| "advanced"
|
| "advanced"
|
||||||
| "expert";
|
| "expert";
|
||||||
project_status_enum: "planning" | "in_progress" | "completed" | "on_hold";
|
project_status_enum: "planning" | "in_progress" | "completed" | "on_hold";
|
||||||
|
subscription_tier_enum: "free" | "pro" | "council";
|
||||||
user_type_enum:
|
user_type_enum:
|
||||||
| "game_developer"
|
| "game_developer"
|
||||||
| "client"
|
| "client"
|
||||||
|
|
@ -498,3 +577,6 @@ export type UserType = Database["public"]["Enums"]["user_type_enum"];
|
||||||
export type ExperienceLevel =
|
export type ExperienceLevel =
|
||||||
Database["public"]["Enums"]["experience_level_enum"];
|
Database["public"]["Enums"]["experience_level_enum"];
|
||||||
export type ProjectStatus = Database["public"]["Enums"]["project_status_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"];
|
||||||
|
|
|
||||||
79
supabase/migrations/20241212_add_tier_badges.sql
Normal file
79
supabase/migrations/20241212_add_tier_badges.sql
Normal 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;
|
||||||
Loading…
Reference in a new issue