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"];
|
||||
}
|
||||
};
|
||||
198
services/geminiService.ts
Normal file
198
services/geminiService.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { EventCategory, CalendarEvent, AppSettings } from "../types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const apiKey = process.env.API_KEY || '';
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
|
||||
// --- Event Parsing ---
|
||||
export const parseNaturalLanguageEvent = async (
|
||||
input: string,
|
||||
referenceDate: Date = new Date()
|
||||
): Promise<{ title: string; startTime: string; endTime: string; description?: string; category: EventCategory } | null> => {
|
||||
if (!apiKey) {
|
||||
console.warn("No API Key found for Gemini");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const model = 'gemini-3-flash-preview';
|
||||
const prompt = `
|
||||
Current Date context: ${referenceDate.toISOString()}
|
||||
User Input: "${input}"
|
||||
|
||||
Extract event details.
|
||||
Categorize into one of: 'general', 'chore', 'date', 'work', 'fun'.
|
||||
If it sounds romantic, it's a date. If it's cleaning/shopping, it's a chore.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
title: { type: Type.STRING },
|
||||
startTime: { type: Type.STRING, description: "ISO 8601 format date string" },
|
||||
endTime: { type: Type.STRING, description: "ISO 8601 format date string" },
|
||||
description: { type: Type.STRING },
|
||||
category: { type: Type.STRING, enum: ['general', 'chore', 'date', 'work', 'fun'] }
|
||||
},
|
||||
required: ["title", "startTime", "endTime", "category"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const text = response.text;
|
||||
if (!text) return null;
|
||||
return JSON.parse(text);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error parsing event with Gemini:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Chat & Assistant ---
|
||||
export const generateChatResponse = async (
|
||||
input: string,
|
||||
settings: AppSettings,
|
||||
userRole: string
|
||||
): Promise<{ text: string; event?: CalendarEvent }> => {
|
||||
if (!apiKey) return { text: "I need a Gemini API Key to function properly." };
|
||||
|
||||
try {
|
||||
const model = 'gemini-3-flash-preview';
|
||||
const systemPrompt = `
|
||||
You are a mystical calendar oracle for a couple.
|
||||
Current Date: ${new Date().toISOString()}
|
||||
User Role: ${userRole}
|
||||
Mom's Sign: ${settings.momZodiac}
|
||||
Dad's Sign: ${settings.dadZodiac}
|
||||
|
||||
If the user asks to create an event, output JSON with "isEvent": true.
|
||||
Otherwise, output JSON with "isEvent": false and a "message".
|
||||
Keep messages bubbly, mystical, and concise.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: `${systemPrompt}\n\nUser: ${input}`,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
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"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(response.text || "{}");
|
||||
|
||||
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 || `I've scheduled "${parsed.title}" for you!`, event: newEvent };
|
||||
} else {
|
||||
return { text: parsed.message || "The stars are silent." };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Gemini Chat Error", error);
|
||||
return { text: "The cosmic connection is weak right now." };
|
||||
}
|
||||
};
|
||||
|
||||
// --- Daily Horoscope ---
|
||||
export const generateDailyHoroscope = async (
|
||||
momSign: string,
|
||||
dadSign: string,
|
||||
settings: AppSettings
|
||||
): Promise<string> => {
|
||||
if (!apiKey) return "Magic requires an API Key.";
|
||||
|
||||
try {
|
||||
const model = 'gemini-3-flash-preview';
|
||||
const prompt = `
|
||||
Generate a very short, mystical horoscope prediction (max 10 words) for a couple with signs ${momSign} and ${dadSign} for today.
|
||||
Focus on their shared energy, love, or domestic life.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
prediction: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(response.text || "{}");
|
||||
return parsed.prediction || "Cosmic energy flows through your home.";
|
||||
} catch (e) {
|
||||
return "The stars are aligning.";
|
||||
}
|
||||
};
|
||||
|
||||
// --- Schedule Suggestions ---
|
||||
export const analyzeScheduleAndSuggest = async (
|
||||
events: CalendarEvent[],
|
||||
settings: AppSettings
|
||||
): Promise<string[]> => {
|
||||
if (!apiKey) return ["Plan a surprise dinner!", "Go for a moonlit walk.", "Cook a meal together."];
|
||||
|
||||
try {
|
||||
const model = 'gemini-3-flash-preview';
|
||||
const eventsSummary = events.slice(0, 20).map(e => // Limit to 20 for context window efficiency
|
||||
`- ${e.title} (${e.category}) on ${e.startTime}`
|
||||
).join('\n');
|
||||
|
||||
const prompt = `
|
||||
Analyze this couple's schedule:
|
||||
${eventsSummary}
|
||||
|
||||
Based on these events, suggest 3 specific, thoughtful, or fun additional events they should add to balance their life.
|
||||
Look for missing date nights, overwhelming work blocks, or chores that need doing.
|
||||
Return 3 short strings.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.parse(response.text || "[]");
|
||||
} catch (e) {
|
||||
return ["Plan a date night", "Take a rest day", "Organize the pantry"];
|
||||
}
|
||||
};
|
||||
64
services/mcpService.ts
Normal file
64
services/mcpService.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { CalendarEvent } from '../types';
|
||||
|
||||
type EventCallback = (event: CalendarEvent) => void;
|
||||
|
||||
class MCPService {
|
||||
private eventSource: EventSource | null = null;
|
||||
private listeners: EventCallback[] = [];
|
||||
|
||||
connect(url: string) {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Connecting to MCP Brain at ${url}...`);
|
||||
this.eventSource = new EventSource(url);
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
console.log('MCP Brain Connected!');
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
// Assuming the backend sends an event structure similar to ours
|
||||
// or wraps it in { type: 'new_event', payload: ... }
|
||||
if (data.id && data.title) {
|
||||
this.notify(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse MCP message', e);
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = (err) => {
|
||||
console.error('MCP Connection Error', err);
|
||||
// Optional: Implement retry logic here
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error('Invalid URL for MCP', e);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(callback: EventCallback) {
|
||||
this.listeners.push(callback);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
private notify(event: CalendarEvent) {
|
||||
this.listeners.forEach(cb => cb(event));
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpService = new MCPService();
|
||||
156
services/ollamaService.ts
Normal file
156
services/ollamaService.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { CalendarEvent, EventCategory, AppSettings } from '../types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface OllamaResponse {
|
||||
model: string;
|
||||
created_at: string;
|
||||
response: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export const generateOllamaResponse = async (
|
||||
prompt: string,
|
||||
settings: AppSettings,
|
||||
userRole: string
|
||||
): Promise<{ text: string; event?: CalendarEvent }> => {
|
||||
const url = `${settings.ollamaUrl}/api/generate`;
|
||||
const currentDate = new Date();
|
||||
|
||||
const systemPrompt = `
|
||||
You are a mystical calendar oracle for a couple.
|
||||
Current Date: ${currentDate.toISOString()}
|
||||
User Role: ${userRole}
|
||||
|
||||
Output JSON ONLY.
|
||||
|
||||
If user wants an event:
|
||||
{
|
||||
"isEvent": true,
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"startTime": "ISO 8601 string",
|
||||
"endTime": "ISO 8601 string",
|
||||
"category": "general" | "chore" | "date" | "work" | "fun" | "magic"
|
||||
}
|
||||
|
||||
If chatting:
|
||||
{
|
||||
"isEvent": false,
|
||||
"message": "your magical response here"
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: settings.ollamaModel,
|
||||
prompt: `${systemPrompt}\n\nUser: ${prompt}`,
|
||||
stream: false,
|
||||
format: "json"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Ollama Error`);
|
||||
const data: OllamaResponse = await response.json();
|
||||
const resultText = data.response;
|
||||
const parsed = JSON.parse(resultText);
|
||||
|
||||
if (parsed.isEvent) {
|
||||
const newEvent: CalendarEvent = {
|
||||
id: uuidv4(),
|
||||
title: parsed.title,
|
||||
description: parsed.description,
|
||||
startTime: parsed.startTime,
|
||||
endTime: parsed.endTime,
|
||||
category: parsed.category || 'general',
|
||||
createdBy: userRole as any
|
||||
};
|
||||
return { text: `The stars have aligned for "${parsed.title}".`, event: newEvent };
|
||||
} else {
|
||||
return { text: parsed.message || "The spirits are quiet." };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ollama Error", error);
|
||||
return { text: "I cannot reach the local spirit realm (Ollama)." };
|
||||
}
|
||||
};
|
||||
|
||||
export const analyzeScheduleAndSuggest = async (
|
||||
events: CalendarEvent[],
|
||||
settings: AppSettings
|
||||
): Promise<string[]> => {
|
||||
if (!settings.ollamaUrl) return ["The oracle is disconnected."];
|
||||
|
||||
const eventsSummary = events.map(e =>
|
||||
`- ${e.title} (${e.category}) on ${e.startTime.split('T')[0]}`
|
||||
).join('\n');
|
||||
|
||||
const prompt = `
|
||||
Analyze this couple's schedule:
|
||||
${eventsSummary}
|
||||
|
||||
Based on these events, suggest 3 specific, thoughtful, or fun additional events they should add to balance their life.
|
||||
Look for missing date nights, overwhelming work blocks, or chores that need doing.
|
||||
Return ONLY a JSON array of 3 strings.
|
||||
`;
|
||||
|
||||
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: "json"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return ["Plan a surprise dinner!", "Go for a moonlit walk.", "Cook a meal together."];
|
||||
|
||||
const data = await response.json();
|
||||
const parsed = JSON.parse(data.response);
|
||||
return Array.isArray(parsed) ? parsed : ["Plan a surprise dinner!", "Go for a moonlit walk.", "Cook a meal together."];
|
||||
} catch (e) {
|
||||
return ["Plan a surprise dinner!", "Go for a moonlit walk.", "Cook a meal together."];
|
||||
}
|
||||
};
|
||||
|
||||
export const generateDailyHoroscope = async (
|
||||
momSign: string,
|
||||
dadSign: string,
|
||||
settings: AppSettings
|
||||
): Promise<string> => {
|
||||
if (!settings.ollamaUrl) return "Stars are silent today.";
|
||||
|
||||
const currentDate = new Date().toDateString();
|
||||
const prompt = `
|
||||
Generate a very short, mystical horoscope prediction (max 10 words) for a couple with signs ${momSign} and ${dadSign} for today, ${currentDate}.
|
||||
Focus on their shared energy or love.
|
||||
Return JSON: { "prediction": "string" }
|
||||
`;
|
||||
|
||||
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: "json"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return "Cosmic energy flows.";
|
||||
|
||||
const data = await response.json();
|
||||
const parsed = JSON.parse(data.response);
|
||||
return parsed.prediction || "Alignment looks favorable.";
|
||||
} catch (e) {
|
||||
console.error("Horoscope gen failed", e);
|
||||
return "Magic is in the air.";
|
||||
}
|
||||
};
|
||||
90
services/storageService.ts
Normal file
90
services/storageService.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { CalendarEvent, UserRole, AppSettings } from '../types';
|
||||
|
||||
const EVENTS_KEY = 'ourtime_events_v1';
|
||||
const SETTINGS_KEY = 'ourtime_settings_v1';
|
||||
|
||||
export const getStoredEvents = (): CalendarEvent[] => {
|
||||
try {
|
||||
const stored = localStorage.getItem(EVENTS_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
console.error("Failed to load events", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveEvent = (event: CalendarEvent): CalendarEvent[] => {
|
||||
const events = getStoredEvents();
|
||||
// Check if update or new
|
||||
const index = events.findIndex(e => e.id === event.id);
|
||||
let newEvents;
|
||||
if (index >= 0) {
|
||||
newEvents = [...events];
|
||||
newEvents[index] = event;
|
||||
} else {
|
||||
newEvents = [...events, event];
|
||||
}
|
||||
|
||||
localStorage.setItem(EVENTS_KEY, JSON.stringify(newEvents));
|
||||
return newEvents;
|
||||
};
|
||||
|
||||
export const deleteEvent = (eventId: string): CalendarEvent[] => {
|
||||
const events = getStoredEvents();
|
||||
const newEvents = events.filter(e => e.id !== eventId);
|
||||
localStorage.setItem(EVENTS_KEY, JSON.stringify(newEvents));
|
||||
return newEvents;
|
||||
};
|
||||
|
||||
export const getSettings = (): AppSettings => {
|
||||
try {
|
||||
const stored = localStorage.getItem(SETTINGS_KEY);
|
||||
if (stored) return JSON.parse(stored);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return {
|
||||
momColor: '#F472B6',
|
||||
dadColor: '#60A5FA',
|
||||
mcpServerUrl: 'http://192.168.1.100:8000/stream',
|
||||
enableMcp: false,
|
||||
aiProvider: 'gemini', // Default to Gemini for reliability, user can switch to Ollama
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
ollamaModel: 'llama3',
|
||||
momZodiac: 'Unknown',
|
||||
dadZodiac: 'Unknown',
|
||||
familiar: 'None'
|
||||
};
|
||||
};
|
||||
|
||||
export const saveSettings = (settings: AppSettings) => {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
};
|
||||
|
||||
// Seed some initial data if empty for demo purposes
|
||||
export const seedInitialData = () => {
|
||||
if (!localStorage.getItem(EVENTS_KEY)) {
|
||||
const today = new Date();
|
||||
const initialEvents: CalendarEvent[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Date Night ❤️',
|
||||
startTime: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2, 19, 0).toISOString(),
|
||||
endTime: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2, 21, 0).toISOString(),
|
||||
createdBy: 'dad',
|
||||
category: 'date',
|
||||
description: 'Dinner at the Italian place',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Grocery Run',
|
||||
startTime: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 10, 0).toISOString(),
|
||||
endTime: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 11, 0).toISOString(),
|
||||
createdBy: 'mom',
|
||||
category: 'chore',
|
||||
description: 'Milk, Eggs, Bread',
|
||||
}
|
||||
];
|
||||
localStorage.setItem(EVENTS_KEY, JSON.stringify(initialEvents));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user