197 lines
5.3 KiB
TypeScript
197 lines
5.3 KiB
TypeScript
// 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' } }
|
|
);
|
|
}
|
|
});
|