init
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// Supabase Edge Function: generate-habits
|
||||
// Deploy: supabase functions deploy generate-habits --no-verify-jwt
|
||||
// Set secret: supabase secrets set GEMINI_API_KEY=your-key-here
|
||||
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
|
||||
const GEMINI_URL =
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
function buildPrompt(story: string): string {
|
||||
return `You are a thoughtful and emotionally intelligent personal growth coach.
|
||||
|
||||
A user has written a personal story about their current life and who they want to become.
|
||||
|
||||
Your job is NOT just to generate habits.
|
||||
Your job is to deeply understand the person and reflect their intention back in a meaningful way.
|
||||
|
||||
---
|
||||
|
||||
INSTRUCTIONS:
|
||||
|
||||
1. Read the user's story carefully.
|
||||
2. Understand:
|
||||
- their struggles
|
||||
- their desires
|
||||
- their emotional tone
|
||||
|
||||
3. Then generate:
|
||||
|
||||
A. Identity Title
|
||||
- A short, powerful sentence
|
||||
- Feels personal and motivating
|
||||
- Example: "I am someone who shows up even when it's hard"
|
||||
|
||||
B. Identity Summary
|
||||
- 2-3 sentences
|
||||
- Reflect their story in a human tone
|
||||
- Make it feel like someone truly understands them
|
||||
- Avoid generic phrases
|
||||
|
||||
C. Suggested Habits (5-8 items)
|
||||
|
||||
Rules:
|
||||
- Very simple and actionable
|
||||
- Can be done daily
|
||||
- No complexity
|
||||
- Each habit max 1 sentence
|
||||
- Feels aligned with their story (not random)
|
||||
|
||||
IMPORTANT:
|
||||
- Habits should feel like "small proof of identity"
|
||||
- Not tasks, but expressions of who they want to become
|
||||
|
||||
D. Tone Style:
|
||||
- Warm
|
||||
- Supportive
|
||||
- Slightly motivational
|
||||
- Never robotic
|
||||
- Never overly formal
|
||||
- Avoid buzzwords like "optimize", "maximize"
|
||||
|
||||
---
|
||||
|
||||
OUTPUT FORMAT (JSON ONLY):
|
||||
Return ONLY valid JSON. No explanation, no markdown, no extra text.
|
||||
|
||||
{
|
||||
"identity_title": "",
|
||||
"identity_summary": "",
|
||||
"suggested_habits": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
EXAMPLE:
|
||||
|
||||
User story: "I feel like I procrastinate a lot and I want to be more focused and disciplined."
|
||||
|
||||
Bad output: "Improve productivity with structured routines"
|
||||
Good output: "I am someone who takes action even when I don't feel ready"
|
||||
|
||||
Make the user feel understood, not analyzed.
|
||||
|
||||
---
|
||||
|
||||
User story:
|
||||
"""
|
||||
${story}
|
||||
"""`;
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { story } = await req.json();
|
||||
|
||||
if (!story || typeof story !== 'string' || story.trim().length < 10) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Please write at least a few sentences about yourself.' }),
|
||||
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const apiKey = Deno.env.get('GEMINI_API_KEY');
|
||||
if (!apiKey) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'AI service not configured. Please set GEMINI_API_KEY.' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Call Gemini API
|
||||
const geminiResponse = await fetch(`${GEMINI_URL}?key=${apiKey}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: buildPrompt(story.trim()) }] }],
|
||||
generationConfig: {
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxOutputTokens: 1024,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!geminiResponse.ok) {
|
||||
const errText = await geminiResponse.text();
|
||||
console.error('Gemini API error:', errText);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'AI generation failed. Please try again.' }),
|
||||
{ status: 502, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const geminiData = await geminiResponse.json();
|
||||
|
||||
// Extract text from Gemini response
|
||||
const rawText =
|
||||
geminiData?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
|
||||
// Parse JSON from response (handle potential markdown wrapping)
|
||||
let cleaned = rawText.trim();
|
||||
// Remove markdown code blocks if present
|
||||
cleaned = cleaned.replace(/```json\s*/gi, '').replace(/```\s*/gi, '');
|
||||
cleaned = cleaned.trim();
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(cleaned);
|
||||
} catch {
|
||||
console.error('Failed to parse Gemini output:', cleaned);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'AI returned invalid data. Please try again.' }),
|
||||
{ status: 502, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate structure
|
||||
if (
|
||||
!result.identity_title ||
|
||||
!result.identity_summary ||
|
||||
!Array.isArray(result.suggested_habits) ||
|
||||
result.suggested_habits.length === 0
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'AI returned incomplete data. Please try again.' }),
|
||||
{ status: 502, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Function error:', err);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Something went wrong. Please try again.' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
-- Nova40 Database Schema
|
||||
-- Run this in your Supabase SQL editor
|
||||
|
||||
-- Enable UUID extension
|
||||
create extension if not exists "uuid-ossp";
|
||||
|
||||
-- 1. Identities table
|
||||
create table identities (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
user_id uuid references auth.users(id) on delete cascade not null,
|
||||
title text not null,
|
||||
description text,
|
||||
start_date date not null,
|
||||
end_date date not null,
|
||||
created_at timestamptz default now()
|
||||
);
|
||||
|
||||
alter table identities enable row level security;
|
||||
|
||||
create policy "Users can view their own identities"
|
||||
on identities for select using (auth.uid() = user_id);
|
||||
|
||||
create policy "Users can create their own identities"
|
||||
on identities for insert with check (auth.uid() = user_id);
|
||||
|
||||
-- 2. Habits table
|
||||
create table habits (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
identity_id uuid references identities(id) on delete cascade not null,
|
||||
title text not null,
|
||||
description text
|
||||
);
|
||||
|
||||
alter table habits enable row level security;
|
||||
|
||||
create policy "Users can view habits for their identities"
|
||||
on habits for select using (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
create policy "Users can create habits for their identities"
|
||||
on habits for insert with check (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- 3. Daily logs table
|
||||
create table daily_logs (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
identity_id uuid references identities(id) on delete cascade not null,
|
||||
date date not null,
|
||||
day_number int not null,
|
||||
identity_check text check (identity_check in ('yes', 'almost', 'no')),
|
||||
note text,
|
||||
unique (identity_id, date)
|
||||
);
|
||||
|
||||
alter table daily_logs enable row level security;
|
||||
|
||||
create policy "Users can view their daily logs"
|
||||
on daily_logs for select using (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
create policy "Users can insert daily logs"
|
||||
on daily_logs for insert with check (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
create policy "Users can update their daily logs"
|
||||
on daily_logs for update using (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- 4. Habit logs table
|
||||
create table habit_logs (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
habit_id uuid references habits(id) on delete cascade not null,
|
||||
date date not null,
|
||||
completed boolean default false,
|
||||
unique (habit_id, date)
|
||||
);
|
||||
|
||||
alter table habit_logs enable row level security;
|
||||
|
||||
create policy "Users can view their habit logs"
|
||||
on habit_logs for select using (
|
||||
habit_id in (
|
||||
select h.id from habits h
|
||||
join identities i on h.identity_id = i.id
|
||||
where i.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "Users can insert habit logs"
|
||||
on habit_logs for insert with check (
|
||||
habit_id in (
|
||||
select h.id from habits h
|
||||
join identities i on h.identity_id = i.id
|
||||
where i.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "Users can update their habit logs"
|
||||
on habit_logs for update using (
|
||||
habit_id in (
|
||||
select h.id from habits h
|
||||
join identities i on h.identity_id = i.id
|
||||
where i.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- 5. Game sessions table
|
||||
create table game_sessions (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
identity_id uuid references identities(id) on delete cascade not null,
|
||||
game_type text not null,
|
||||
score int default 0,
|
||||
created_at timestamptz default now()
|
||||
);
|
||||
|
||||
alter table game_sessions enable row level security;
|
||||
|
||||
create policy "Users can view their game sessions"
|
||||
on game_sessions for select using (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
create policy "Users can insert game sessions"
|
||||
on game_sessions for insert with check (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- 6. Stats table
|
||||
create table stats (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
identity_id uuid references identities(id) on delete cascade not null unique,
|
||||
discipline_score int default 0,
|
||||
focus_score int default 0,
|
||||
consistency_score int default 0
|
||||
);
|
||||
|
||||
alter table stats enable row level security;
|
||||
|
||||
create policy "Users can view their stats"
|
||||
on stats for select using (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
create policy "Users can insert their stats"
|
||||
on stats for insert with check (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
|
||||
create policy "Users can update their stats"
|
||||
on stats for update using (
|
||||
identity_id in (select id from identities where user_id = auth.uid())
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Nova40 Demo User Seeder
|
||||
-- Run this in Supabase SQL Editor
|
||||
--
|
||||
-- Creates: demo@nova40.app / 123456 (pre-confirmed, ready to login)
|
||||
|
||||
-- Step 1: Create the demo user in auth.users
|
||||
do $$
|
||||
declare
|
||||
demo_uid uuid := gen_random_uuid();
|
||||
begin
|
||||
-- Skip if demo user already exists
|
||||
if exists (select 1 from auth.users where email = 'demo@nova40.app') then
|
||||
raise notice 'Demo user already exists, skipping.';
|
||||
return;
|
||||
end if;
|
||||
|
||||
-- Insert into auth.users with minimal required columns
|
||||
insert into auth.users (
|
||||
id,
|
||||
instance_id,
|
||||
aud,
|
||||
role,
|
||||
email,
|
||||
encrypted_password,
|
||||
email_confirmed_at,
|
||||
raw_app_meta_data,
|
||||
raw_user_meta_data,
|
||||
created_at,
|
||||
updated_at,
|
||||
confirmation_token
|
||||
) values (
|
||||
demo_uid,
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'authenticated',
|
||||
'authenticated',
|
||||
'demo@nova40.app',
|
||||
crypt('123456', gen_salt('bf')),
|
||||
now(),
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'{"email": "demo@nova40.app"}'::jsonb,
|
||||
now(),
|
||||
now(),
|
||||
''
|
||||
);
|
||||
|
||||
-- Insert into auth.identities (required for sign-in to work)
|
||||
insert into auth.identities (
|
||||
id,
|
||||
user_id,
|
||||
provider_id,
|
||||
identity_data,
|
||||
provider,
|
||||
last_sign_in_at,
|
||||
created_at,
|
||||
updated_at
|
||||
) values (
|
||||
demo_uid,
|
||||
demo_uid,
|
||||
'demo@nova40.app',
|
||||
jsonb_build_object(
|
||||
'sub', demo_uid::text,
|
||||
'email', 'demo@nova40.app',
|
||||
'email_verified', true
|
||||
),
|
||||
'email',
|
||||
now(),
|
||||
now(),
|
||||
now()
|
||||
);
|
||||
|
||||
raise notice 'Demo user created successfully (id: %)', demo_uid;
|
||||
end $$;
|
||||
|
||||
-- Verify: you should see one row
|
||||
select id, email, email_confirmed_at from auth.users where email = 'demo@nova40.app';
|
||||
@@ -0,0 +1,107 @@
|
||||
-- Nova40: Fix auth schema and create demo user
|
||||
-- Run this in Supabase SQL Editor
|
||||
--
|
||||
-- This script:
|
||||
-- 1. Cleans up any broken demo user entries from previous seeder attempts
|
||||
-- 2. Verifies auth schema health
|
||||
-- 3. Creates a proper demo user
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: Clean up any broken demo user data
|
||||
-- ============================================
|
||||
delete from auth.identities
|
||||
where user_id in (select id from auth.users where email = 'demo@nova40.app');
|
||||
|
||||
delete from auth.users
|
||||
where email = 'demo@nova40.app';
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: Verify auth schema is healthy
|
||||
-- ============================================
|
||||
-- This should return a number (0 is fine)
|
||||
select count(*) as total_users from auth.users;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: Create demo user properly
|
||||
-- ============================================
|
||||
-- We need the pgcrypto extension for crypt()
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
do $$
|
||||
declare
|
||||
new_uid uuid := gen_random_uuid();
|
||||
begin
|
||||
-- Insert user
|
||||
insert into auth.users (
|
||||
id,
|
||||
instance_id,
|
||||
aud,
|
||||
role,
|
||||
email,
|
||||
encrypted_password,
|
||||
email_confirmed_at,
|
||||
raw_app_meta_data,
|
||||
raw_user_meta_data,
|
||||
created_at,
|
||||
updated_at,
|
||||
confirmation_token,
|
||||
recovery_token,
|
||||
email_change_token_new,
|
||||
email_change
|
||||
) values (
|
||||
new_uid,
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'authenticated',
|
||||
'authenticated',
|
||||
'demo@nova40.app',
|
||||
crypt('123456', gen_salt('bf')),
|
||||
now(),
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'{"email": "demo@nova40.app"}'::jsonb,
|
||||
now(),
|
||||
now(),
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
);
|
||||
|
||||
-- Insert identity (REQUIRED for Supabase auth login to work)
|
||||
insert into auth.identities (
|
||||
id,
|
||||
user_id,
|
||||
provider_id,
|
||||
identity_data,
|
||||
provider,
|
||||
last_sign_in_at,
|
||||
created_at,
|
||||
updated_at
|
||||
) values (
|
||||
new_uid,
|
||||
new_uid,
|
||||
'demo@nova40.app',
|
||||
jsonb_build_object(
|
||||
'sub', new_uid::text,
|
||||
'email', 'demo@nova40.app',
|
||||
'email_verified', true
|
||||
),
|
||||
'email',
|
||||
now(),
|
||||
now(),
|
||||
now()
|
||||
);
|
||||
|
||||
raise notice '✅ Demo user created! ID: %', new_uid;
|
||||
end $$;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 4: Verify the user was created
|
||||
-- ============================================
|
||||
select
|
||||
u.id,
|
||||
u.email,
|
||||
u.email_confirmed_at,
|
||||
i.provider
|
||||
from auth.users u
|
||||
left join auth.identities i on i.user_id = u.id
|
||||
where u.email = 'demo@nova40.app';
|
||||
Reference in New Issue
Block a user