Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6adbf79e7 | ||
| 7399a27600 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -243,7 +243,9 @@ describe('App Component', () => {
|
||||
mockedApiClient.fetchShoppingLists.mockImplementation(() =>
|
||||
Promise.resolve(new Response(JSON.stringify([]))),
|
||||
);
|
||||
mockedAiApiClient.rescanImageArea.mockResolvedValue({ text: 'mocked text' }); // Mock for FlyerCorrectionTool
|
||||
mockedAiApiClient.rescanImageArea.mockResolvedValue(
|
||||
new Response(JSON.stringify({ text: 'mocked text' })),
|
||||
); // Mock for FlyerCorrectionTool
|
||||
console.log('[TEST DEBUG] beforeEach: Setup complete');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
// src/services/aiAnalysisService.ts
|
||||
import { Flyer, FlyerItem, MasterGroceryItem, GroundedResponse } from '../types';
|
||||
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.
|
||||
@@ -15,8 +22,7 @@ export class AiAnalysisService {
|
||||
*/
|
||||
async getQuickInsights(items: FlyerItem[]): Promise<string> {
|
||||
logger.info('[AiAnalysisService] getQuickInsights called.');
|
||||
const result = await aiApiClient.getQuickInsights(items);
|
||||
return result.text;
|
||||
return aiApiClient.getQuickInsights(items).then((res) => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,8 +32,7 @@ export class AiAnalysisService {
|
||||
*/
|
||||
async getDeepDiveAnalysis(items: FlyerItem[]): Promise<string> {
|
||||
logger.info('[AiAnalysisService] getDeepDiveAnalysis called.');
|
||||
const result = await aiApiClient.getDeepDiveAnalysis(items);
|
||||
return result.text;
|
||||
return aiApiClient.getDeepDiveAnalysis(items).then((res) => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +44,18 @@ export class AiAnalysisService {
|
||||
logger.info('[AiAnalysisService] searchWeb called.');
|
||||
// Construct a query string from the item names.
|
||||
const query = items.map((item) => item.item).join(', ');
|
||||
return aiApiClient.searchWeb(query);
|
||||
// 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: 'Untitled' }
|
||||
: { uri: '', title: 'Untitled' }) as Source,
|
||||
);
|
||||
return { ...response, sources: mappedSources };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +68,7 @@ export class AiAnalysisService {
|
||||
logger.info('[AiAnalysisService] planTripWithMaps called.');
|
||||
// Encapsulate geolocation logic within the service.
|
||||
const userLocation = await this.getCurrentLocation();
|
||||
return aiApiClient.planTripWithMaps(items, store, userLocation);
|
||||
return aiApiClient.planTripWithMaps(items, store, userLocation).then((res) => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +78,17 @@ export class AiAnalysisService {
|
||||
*/
|
||||
async compareWatchedItemPrices(watchedItems: MasterGroceryItem[]): Promise<GroundedResponse> {
|
||||
logger.info('[AiAnalysisService] compareWatchedItemPrices called.');
|
||||
return aiApiClient.compareWatchedItemPrices(watchedItems);
|
||||
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: 'Untitled' }
|
||||
: { uri: '', title: 'Untitled' }) as Source,
|
||||
);
|
||||
return { ...response, sources: mappedSources };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,8 +98,7 @@ export class AiAnalysisService {
|
||||
*/
|
||||
async generateImageFromText(prompt: string): Promise<string> {
|
||||
logger.info('[AiAnalysisService] generateImageFromText called.');
|
||||
const result = await aiApiClient.generateImageFromText(prompt);
|
||||
return result.imageUrl;
|
||||
return aiApiClient.generateImageFromText(prompt).then((res) => res.json());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -104,16 +104,16 @@ export const getJobStatus = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const isImageAFlyer = async (
|
||||
export const isImageAFlyer = (
|
||||
imageFile: File,
|
||||
tokenOverride?: string,
|
||||
): Promise<{ is_flyer: boolean }> => {
|
||||
): Promise<Response> => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
|
||||
// Use apiFetchWithAuth for FormData to let the browser set the correct Content-Type.
|
||||
// The URL must be relative, as the helper constructs the full path.
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/check-flyer',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -121,28 +121,16 @@ export const isImageAFlyer = async (
|
||||
},
|
||||
{ tokenOverride },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const extractAddressFromImage = async (
|
||||
export const extractAddressFromImage = (
|
||||
imageFile: File,
|
||||
tokenOverride?: string,
|
||||
): Promise<{ address: string }> => {
|
||||
): Promise<Response> => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/extract-address',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -150,30 +138,18 @@ export const extractAddressFromImage = async (
|
||||
},
|
||||
{ tokenOverride },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const extractLogoFromImage = async (
|
||||
export const extractLogoFromImage = (
|
||||
imageFiles: File[],
|
||||
tokenOverride?: string,
|
||||
): Promise<{ store_logo_base_64: string | null }> => {
|
||||
): Promise<Response> => {
|
||||
const formData = new FormData();
|
||||
imageFiles.forEach((file) => {
|
||||
formData.append('images', file);
|
||||
});
|
||||
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/extract-logo',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -181,26 +157,14 @@ export const extractLogoFromImage = async (
|
||||
},
|
||||
{ tokenOverride },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getQuickInsights = async (
|
||||
export const getQuickInsights = (
|
||||
items: Partial<FlyerItem>[],
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<{ text: string }> => {
|
||||
const response = await apiFetch(
|
||||
): Promise<Response> => {
|
||||
return apiFetch(
|
||||
'/ai/quick-insights',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -210,26 +174,14 @@ export const getQuickInsights = async (
|
||||
},
|
||||
{ tokenOverride, signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getDeepDiveAnalysis = async (
|
||||
export const getDeepDiveAnalysis = (
|
||||
items: Partial<FlyerItem>[],
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<{ text: string }> => {
|
||||
const response = await apiFetch(
|
||||
): Promise<Response> => {
|
||||
return apiFetch(
|
||||
'/ai/deep-dive',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -239,26 +191,14 @@ export const getDeepDiveAnalysis = async (
|
||||
},
|
||||
{ tokenOverride, signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const searchWeb = async (
|
||||
export const searchWeb = (
|
||||
query: string,
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<GroundedResponse> => {
|
||||
const response = await apiFetch(
|
||||
): Promise<Response> => {
|
||||
return apiFetch(
|
||||
'/ai/search-web',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -268,18 +208,6 @@ export const searchWeb = async (
|
||||
},
|
||||
{ tokenOverride, signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -292,9 +220,9 @@ export const planTripWithMaps = async (
|
||||
userLocation: GeolocationCoordinates,
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<GroundedResponse> => {
|
||||
): Promise<Response> => {
|
||||
logger.debug('Stub: planTripWithMaps called with location:', { userLocation });
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/plan-trip',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -303,18 +231,6 @@ export const planTripWithMaps = async (
|
||||
},
|
||||
{ signal, tokenOverride },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -322,13 +238,13 @@ export const planTripWithMaps = async (
|
||||
* @param prompt A description of the image to generate (e.g., a meal plan).
|
||||
* @returns A base64-encoded string of the generated PNG image.
|
||||
*/
|
||||
export const generateImageFromText = async (
|
||||
export const generateImageFromText = (
|
||||
prompt: string,
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<{ imageUrl: string }> => {
|
||||
): Promise<Response> => {
|
||||
logger.debug('Stub: generateImageFromText called with prompt:', { prompt });
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/generate-image',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -338,18 +254,6 @@ export const generateImageFromText = async (
|
||||
},
|
||||
{ tokenOverride, signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -357,13 +261,13 @@ export const generateImageFromText = async (
|
||||
* @param text The text to be spoken.
|
||||
* @returns A base64-encoded string of the raw audio data.
|
||||
*/
|
||||
export const generateSpeechFromText = async (
|
||||
export const generateSpeechFromText = (
|
||||
text: string,
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<{ audioUrl: string }> => {
|
||||
): Promise<Response> => {
|
||||
logger.debug('Stub: generateSpeechFromText called with text:', { text });
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/generate-speech',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -373,18 +277,6 @@ export const generateSpeechFromText = async (
|
||||
},
|
||||
{ tokenOverride, signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -426,34 +318,22 @@ export const startVoiceSession = (callbacks: {
|
||||
* @param tokenOverride Optional token for testing.
|
||||
* @returns A promise that resolves to the API response containing the extracted text.
|
||||
*/
|
||||
export const rescanImageArea = async (
|
||||
export const rescanImageArea = (
|
||||
imageFile: File,
|
||||
cropArea: { x: number; y: number; width: number; height: number },
|
||||
extractionType: 'store_name' | 'dates' | 'item_details',
|
||||
tokenOverride?: string,
|
||||
): Promise<{ text: string | undefined }> => {
|
||||
): Promise<Response> => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
formData.append('cropArea', JSON.stringify(cropArea));
|
||||
formData.append('extractionType', extractionType);
|
||||
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/rescan-area',
|
||||
{ method: 'POST', body: formData },
|
||||
{ tokenOverride },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -461,13 +341,13 @@ export const rescanImageArea = async (
|
||||
* @param watchedItems An array of the user's watched master grocery items.
|
||||
* @returns A promise that resolves to the raw `Response` object from the API.
|
||||
*/
|
||||
export const compareWatchedItemPrices = async (
|
||||
export const compareWatchedItemPrices = (
|
||||
watchedItems: MasterGroceryItem[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<GroundedResponse> => {
|
||||
): Promise<Response> => {
|
||||
// Use the apiFetch wrapper for consistency with other API calls in this file.
|
||||
// This centralizes token handling and base URL logic.
|
||||
const response = await apiFetch(
|
||||
return apiFetch(
|
||||
'/ai/compare-prices',
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -475,17 +355,4 @@ export const compareWatchedItemPrices = async (
|
||||
body: JSON.stringify({ items: watchedItems }),
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = { message: await response.text() };
|
||||
}
|
||||
throw { status: response.status, body: errorBody };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
)};
|
||||
|
||||
Reference in New Issue
Block a user