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
+206
View File
@@ -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',
};
}