Files
my-loves-calendar/services/aiService.ts
drjones c373bbdab4 first commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 21:05:27 -08:00

284 lines
8.8 KiB
TypeScript

import { GoogleGenAI, Type } from "@google/genai";
import { EventCategory, CalendarEvent, AppSettings, PulseAnalysis } from "../types";
import { v4 as uuidv4 } from 'uuid';
const apiKey = process.env.API_KEY || '';
const geminiAi = new GoogleGenAI({ apiKey });
// --- Helper for Ollama ---
const callOllama = async (settings: AppSettings, prompt: string, jsonMode: boolean = true) => {
try {
const response = await fetch(`${settings.ollamaUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: settings.ollamaModel,
prompt: prompt,
stream: false,
format: jsonMode ? "json" : undefined
})
});
if (!response.ok) throw new Error("Ollama connection failed. Check your IP/Port and CORS settings.");
const data = await response.json();
return jsonMode ? JSON.parse(data.response) : data.response;
} catch (error) {
console.error("Ollama Error:", error);
throw error;
}
};
// --- Helper for Gemini ---
const callGemini = async (prompt: string, schema?: any) => {
try {
const model = 'gemini-3-flash-preview';
const config: any = {};
if (schema) {
config.responseMimeType = "application/json";
config.responseSchema = schema;
}
const response = await geminiAi.models.generateContent({
model,
contents: prompt,
config
});
return schema ? JSON.parse(response.text || "{}") : response.text;
} catch (error) {
console.error("Gemini Error:", error);
throw error;
}
};
// --- Features ---
export const parseNaturalLanguageEvent = async (
input: string,
settings: AppSettings
): Promise<{ title: string; startTime: string; endTime: string; description?: string; category: EventCategory } | null> => {
const referenceDate = new Date();
const prompt = `
Current Date context: ${referenceDate.toISOString()}
User Input: "${input}"
Extract event details. Return JSON.
Categorize into one of: 'general', 'chore', 'date', 'work', 'fun'.
Required fields: title, startTime (ISO), endTime (ISO), category.
If exact time not given, guess reasonable time.
`;
try {
if (settings.aiProvider === 'ollama') {
return await callOllama(settings, prompt, true);
} else {
return await callGemini(prompt, {
type: Type.OBJECT,
properties: {
title: { type: Type.STRING },
startTime: { type: Type.STRING },
endTime: { type: Type.STRING },
description: { type: Type.STRING },
category: { type: Type.STRING, enum: ['general', 'chore', 'date', 'work', 'fun'] }
},
required: ["title", "startTime", "endTime", "category"]
});
}
} catch (e) {
console.warn("AI Event Parse Failed");
return null;
}
};
export const analyzeRelationshipPulse = async (
events: CalendarEvent[],
settings: AppSettings
): Promise<PulseAnalysis> => {
const now = new Date();
const futureEvents = events.filter(e => new Date(e.startTime) > now).slice(0, 30);
const summary = futureEvents.map(e =>
`[${e.createdBy.toUpperCase()}] ${e.title} (${e.category}) on ${e.startTime}`
).join('\n');
const prompt = `
Analyze this couple's schedule for the "Cosmic Pulse" dashboard.
Events:
${summary}
Tasks:
1. Calculate "harmonyScore" (0-100). Higher if balanced mix of work/chores vs dates/fun. Lower if too much work or undefined.
2. "vibe": One word (e.g., Chaotic, Romantic, Busy).
3. "burnoutWarning": If one person has too many 'work'/'chore' items, warn them. Null if fine.
4. "upcomingConflicts": Identify any potential timing clashes.
5. "smartNudges": Suggest 2 nice things they can say/do based on schedule (e.g. "Wish Mom luck on presentation").
Return JSON matching PulseAnalysis interface.
`;
const fallback: PulseAnalysis = {
harmonyScore: 85,
vibe: "Stable",
burnoutWarning: null,
upcomingConflicts: [],
smartNudges: ["Hug each other", "Plan a date"]
};
try {
let result;
if (settings.aiProvider === 'ollama') {
result = await callOllama(settings, prompt, true);
} else {
result = await callGemini(prompt, {
type: Type.OBJECT,
properties: {
harmonyScore: { type: Type.NUMBER },
vibe: { type: Type.STRING },
burnoutWarning: { type: Type.STRING, nullable: true },
upcomingConflicts: { type: Type.ARRAY, items: { type: Type.STRING } },
smartNudges: { type: Type.ARRAY, items: { type: Type.STRING } }
},
required: ["harmonyScore", "vibe", "smartNudges"]
});
}
return { ...fallback, ...result };
} catch (e) {
console.error("Pulse check failed", e);
return fallback;
}
};
export const interpretDream = async (
dreamText: string,
settings: AppSettings
): Promise<string> => {
const prompt = `
Interpret this dream in a mystical, positive, and bubbly way.
Keep it short (max 2 sentences).
Dream: "${dreamText}"
Return JSON: { "interpretation": "string" }
`;
try {
let res;
if (settings.aiProvider === 'ollama') {
res = await callOllama(settings, prompt, true);
} else {
res = await callGemini(prompt, {
type: Type.OBJECT,
properties: { interpretation: { type: Type.STRING } }
});
}
return res.interpretation || "A mysterious vision of the future.";
} catch (e) {
return "The mists of time obscure this meaning.";
}
};
export const generateChatResponse = async (
input: string,
settings: AppSettings,
userRole: string
): Promise<{ text: string; event?: CalendarEvent }> => {
const prompt = `
You are a mystical calendar assistant.
Current Date: ${new Date().toISOString()}
User Role: ${userRole}
If request is to create/add/schedule event:
Output JSON with "isEvent": true and all event fields.
Else:
Output JSON with "isEvent": false and "message".
User: ${input}
`;
try {
let parsed;
if (settings.aiProvider === 'ollama') {
parsed = await callOllama(settings, prompt, true);
} else {
parsed = await callGemini(prompt, {
type: Type.OBJECT,
properties: {
isEvent: { type: Type.BOOLEAN },
message: { type: Type.STRING },
title: { type: Type.STRING },
description: { type: Type.STRING },
startTime: { type: Type.STRING },
endTime: { type: Type.STRING },
category: { type: Type.STRING, enum: ['general', 'chore', 'date', 'work', 'fun', 'magic'] }
},
required: ["isEvent"]
});
}
if (parsed.isEvent) {
const newEvent: CalendarEvent = {
id: uuidv4(),
title: parsed.title || "New Event",
description: parsed.description || "",
startTime: parsed.startTime || new Date().toISOString(),
endTime: parsed.endTime || new Date().toISOString(),
category: (parsed.category as EventCategory) || 'general',
createdBy: userRole as any
};
return { text: parsed.message || `Scheduled "${parsed.title}"!`, event: newEvent };
} else {
return { text: parsed.message || "I heard the stars whispering." };
}
} catch (e) {
return { text: "The connection to the oracle is hazy." };
}
};
export const generateDailyHoroscope = async (
momSign: string,
dadSign: string,
settings: AppSettings
): Promise<string> => {
const prompt = `
Generate a mystical, short love horoscope (max 10 words) for signs ${momSign} and ${dadSign}.
Return JSON: { "prediction": "string" }
`;
try {
let parsed;
if (settings.aiProvider === 'ollama') {
parsed = await callOllama(settings, prompt, true);
} else {
parsed = await callGemini(prompt, {
type: Type.OBJECT,
properties: { prediction: { type: Type.STRING } }
});
}
return parsed.prediction || "Love is in the air.";
} catch (e) {
return "Stars align for you two.";
}
};
export const analyzeScheduleAndSuggest = async (
events: CalendarEvent[],
settings: AppSettings
): Promise<string[]> => {
const eventsSummary = events.slice(0, 20).map(e => `- ${e.title} (${e.category})`).join('\n');
const prompt = `
Analyze this schedule:
${eventsSummary}
Suggest 3 fun, specific events for a couple to add.
Return JSON: { "suggestions": ["string", "string", "string"] }
`;
try {
let parsed;
if (settings.aiProvider === 'ollama') {
parsed = await callOllama(settings, prompt, true);
} else {
parsed = await callGemini(prompt, {
type: Type.OBJECT,
properties: { suggestions: { type: Type.ARRAY, items: { type: Type.STRING } } }
});
}
return parsed.suggestions || ["Date Night", "Walk in Park", "Movie Marathon"];
} catch (e) {
return ["Plan a date", "Relax together", "Cook dinner"];
}
};