284
services/aiService.ts
Normal file
284
services/aiService.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
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"];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user