first commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
drjones
2026-02-16 21:05:27 -08:00
commit c373bbdab4
25 changed files with 2964 additions and 0 deletions

284
services/aiService.ts Normal file
View 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
View 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
View 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
View 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.";
}
};

View 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));
}
};