284 lines
8.8 KiB
TypeScript
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"];
|
|
}
|
|
}; |