This commit is contained in:
dios.one
2026-04-21 18:00:30 +07:00
parent 2173a765c9
commit 23dc306f12
67 changed files with 10302 additions and 67 deletions
+196
View File
@@ -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' } }
);
}
});
+157
View File
@@ -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';