117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
// src/services/aiAnalysisService.ts
|
|
import { Flyer, FlyerItem, MasterGroceryItem, GroundedResponse, Source } from '../types';
|
|
import * as aiApiClient from './aiApiClient';
|
|
import { logger } from './logger.client';
|
|
|
|
interface RawSource {
|
|
web?: {
|
|
uri?: string;
|
|
title?: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A service class to encapsulate all AI analysis API calls and related business logic.
|
|
* This decouples the React components and hooks from the data fetching implementation.
|
|
*/
|
|
export class AiAnalysisService {
|
|
/**
|
|
* Fetches quick insights for a given set of flyer items.
|
|
* @param items - The flyer items to analyze.
|
|
* @returns The string result from the API.
|
|
*/
|
|
async getQuickInsights(items: FlyerItem[]): Promise<string> {
|
|
logger.info('[AiAnalysisService] getQuickInsights called.');
|
|
return aiApiClient.getQuickInsights(items).then((res) => res.json());
|
|
}
|
|
|
|
/**
|
|
* Fetches a deep dive analysis for a given set of flyer items.
|
|
* @param items - The flyer items to analyze.
|
|
* @returns The string result from the API.
|
|
*/
|
|
async getDeepDiveAnalysis(items: FlyerItem[]): Promise<string> {
|
|
logger.info('[AiAnalysisService] getDeepDiveAnalysis called.');
|
|
return aiApiClient.getDeepDiveAnalysis(items).then((res) => res.json());
|
|
}
|
|
|
|
/**
|
|
* Performs a web search based on the flyer items.
|
|
* @param items - The flyer items to use as context for the search.
|
|
* @returns A grounded response with text and sources.
|
|
*/
|
|
async searchWeb(items: FlyerItem[]): Promise<GroundedResponse> {
|
|
logger.info('[AiAnalysisService] searchWeb called.');
|
|
// Construct a query string from the item names.
|
|
const query = items.map((item) => item.item).join(', ');
|
|
// The API client returns a specific shape that we need to await the JSON from
|
|
const response: { text: string; sources: RawSource[] } = await aiApiClient
|
|
.searchWeb(query)
|
|
.then((res) => res.json());
|
|
// Normalize sources to a consistent format.
|
|
const mappedSources = (response.sources || []).map(
|
|
(s: RawSource) =>
|
|
(s.web ? { uri: s.web.uri || '', title: s.web.title || 'Untitled' } : { uri: '', title: 'Untitled' }) as Source,
|
|
);
|
|
return { ...response, sources: mappedSources };
|
|
}
|
|
|
|
/**
|
|
* Plans a shopping trip using maps and the user's location.
|
|
* @param items - The flyer items for the trip.
|
|
* @param store - The store associated with the flyer.
|
|
* @returns A grounded response with the trip plan and sources.
|
|
*/
|
|
async planTripWithMaps(items: FlyerItem[], store: Flyer['store']): Promise<GroundedResponse> {
|
|
logger.info('[AiAnalysisService] planTripWithMaps called.');
|
|
// Encapsulate geolocation logic within the service.
|
|
const userLocation = await this.getCurrentLocation();
|
|
return aiApiClient.planTripWithMaps(items, store, userLocation).then((res) => res.json());
|
|
}
|
|
|
|
/**
|
|
* Compares prices for a user's watched items.
|
|
* @param watchedItems - The list of master grocery items to compare.
|
|
* @returns A grounded response with the price comparison.
|
|
*/
|
|
async compareWatchedItemPrices(watchedItems: MasterGroceryItem[]): Promise<GroundedResponse> {
|
|
logger.info('[AiAnalysisService] compareWatchedItemPrices called.');
|
|
const response: { text: string; sources: RawSource[] } = await aiApiClient
|
|
.compareWatchedItemPrices(watchedItems)
|
|
.then((res) => res.json());
|
|
// Normalize sources to a consistent format.
|
|
const mappedSources = (response.sources || []).map(
|
|
(s: RawSource) =>
|
|
(s.web ? { uri: s.web.uri || '', title: s.web.title || 'Untitled' } : { uri: '', title: 'Untitled' }) as Source,
|
|
);
|
|
return { ...response, sources: mappedSources };
|
|
}
|
|
|
|
/**
|
|
* Generates an image based on a provided text prompt (e.g., a meal plan).
|
|
* @param prompt - The text to use for image generation.
|
|
* @returns A base64 encoded string of the generated image.
|
|
*/
|
|
async generateImageFromText(prompt: string): Promise<string> {
|
|
logger.info('[AiAnalysisService] generateImageFromText called.');
|
|
return aiApiClient.generateImageFromText(prompt).then((res) => res.json());
|
|
}
|
|
|
|
/**
|
|
* A helper to promisify the Geolocation API.
|
|
* @returns A promise that resolves with the user's coordinates.
|
|
* @private
|
|
*/
|
|
private getCurrentLocation(): Promise<GeolocationCoordinates> {
|
|
return new Promise((resolve, reject) => {
|
|
if (!navigator.geolocation) {
|
|
return reject(new Error('Geolocation is not supported by your browser.'));
|
|
}
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => resolve(pos.coords),
|
|
(err) => reject(err),
|
|
);
|
|
});
|
|
}
|
|
}
|