init
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import safeParseAI from '../utils/safeParseAI';
|
||||
import { getSuggestionsForIdentity } from '../utils/helpers';
|
||||
import { GEMINI_API_KEY } from '../config/keys';
|
||||
|
||||
const GEMINI_URL =
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
|
||||
|
||||
function buildPrompt(story) {
|
||||
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}
|
||||
"""`;
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 2;
|
||||
const RETRY_DELAY_MS = 4000;
|
||||
|
||||
async function callGemini(story, attempt = 1) {
|
||||
const response = await fetch(`${GEMINI_URL}?key=${GEMINI_API_KEY}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: buildPrompt(story) }] }],
|
||||
generationConfig: {
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxOutputTokens: 1024,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Rate limited — retry after delay
|
||||
if (response.status === 429 && attempt <= MAX_RETRIES) {
|
||||
console.warn(`Gemini rate limited, retrying in ${RETRY_DELAY_MS}ms (attempt ${attempt})...`);
|
||||
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
||||
return callGemini(story, attempt + 1);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
console.warn('Gemini API error:', response.status, errText);
|
||||
throw new Error(`AI request failed (${response.status})`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate identity + habits from user's personal story using Gemini AI.
|
||||
* Retries on rate limit. Falls back to local generation if AI fails.
|
||||
*/
|
||||
export async function generateFromStory(story) {
|
||||
try {
|
||||
const json = await callGemini(story);
|
||||
const rawText = json?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
|
||||
const parsed = safeParseAI(rawText);
|
||||
|
||||
if (
|
||||
parsed.identity_title &&
|
||||
parsed.identity_summary &&
|
||||
parsed.suggested_habits.length > 0
|
||||
) {
|
||||
return {
|
||||
...parsed,
|
||||
suggested_habits: parsed.suggested_habits.slice(0, 8),
|
||||
source: 'ai',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid AI response');
|
||||
} catch (e) {
|
||||
console.warn('AI generation failed, using fallback:', e.message);
|
||||
return generateFallback(story);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local fallback when AI is unavailable.
|
||||
*/
|
||||
function generateFallback(story) {
|
||||
const words = story.toLowerCase();
|
||||
|
||||
// Extract identity title from story patterns
|
||||
let title = 'A better version of myself';
|
||||
const patterns = [
|
||||
{ match: /want to be(come)?\s+(.+?)[\.\,\!\n]/i, group: 2 },
|
||||
{ match: /i want to\s+(.+?)[\.\,\!\n]/i, group: 1 },
|
||||
{ match: /dream of\s+(.+?)[\.\,\!\n]/i, group: 1 },
|
||||
{ match: /goal is to\s+(.+?)[\.\,\!\n]/i, group: 1 },
|
||||
{ match: /i wish i (was|were|could)\s+(.+?)[\.\,\!\n]/i, group: 2 },
|
||||
{ match: /i need to\s+(.+?)[\.\,\!\n]/i, group: 1 },
|
||||
{ match: /i('m| am) tired of\s+(.+?)[\.\,\!\n]/i, group: 2 },
|
||||
{ match: /i struggle with\s+(.+?)[\.\,\!\n]/i, group: 1 },
|
||||
];
|
||||
|
||||
for (const p of patterns) {
|
||||
const m = story.match(p.match);
|
||||
if (m && m[p.group]) {
|
||||
let extracted = m[p.group].trim();
|
||||
// Convert negative patterns to positive identity
|
||||
if (p.match.source.includes('tired of') || p.match.source.includes('struggle')) {
|
||||
extracted = 'overcoming ' + extracted;
|
||||
}
|
||||
title = extracted.charAt(0).toUpperCase() + extracted.slice(1);
|
||||
if (title.length > 50) title = title.slice(0, 50);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate varied habits from the story text
|
||||
let habits = getSuggestionsForIdentity(words);
|
||||
if (habits.length < 3) {
|
||||
// Also try with the extracted title
|
||||
const titleHabits = getSuggestionsForIdentity(title);
|
||||
titleHabits.forEach((h) => { if (!habits.includes(h)) habits.push(h); });
|
||||
}
|
||||
|
||||
return {
|
||||
identity_title: title,
|
||||
identity_summary: `Based on your story, this journey is about becoming ${title.toLowerCase()}. Every day is a step closer to the person you described.`,
|
||||
suggested_habits: habits.slice(0, 8),
|
||||
source: 'fallback',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user