Launch
This commit is contained in:
@@ -1,132 +1,132 @@
|
||||
import { IdentificationResult, Language } from '../types';
|
||||
import { resolveImageUri, tryResolveImageUri } from '../utils/imageUri';
|
||||
import { getConfiguredApiBaseUrl } from '../utils/backendUrl';
|
||||
import { backendApiClient } from './backend/backendApiClient';
|
||||
import { BackendDatabaseEntry, isBackendApiError } from './backend/contracts';
|
||||
import { createIdempotencyKey } from '../utils/idempotency';
|
||||
import { getMockCatalog, searchMockCatalog } from './backend/mockCatalog';
|
||||
|
||||
export interface DatabaseEntry extends IdentificationResult {
|
||||
imageUri: string;
|
||||
imageStatus?: 'ok' | 'missing' | 'invalid';
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
category?: string | null;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export type SemanticSearchStatus = 'success' | 'timeout' | 'provider_error' | 'no_results' | 'insufficient_credits';
|
||||
|
||||
export interface SemanticSearchResult {
|
||||
status: SemanticSearchStatus;
|
||||
results: DatabaseEntry[];
|
||||
}
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT = 500;
|
||||
|
||||
const hasConfiguredPlantBackend = (): boolean => Boolean(
|
||||
String(
|
||||
process.env.EXPO_PUBLIC_API_URL
|
||||
|| process.env.EXPO_PUBLIC_BACKEND_URL
|
||||
|| process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL
|
||||
|| '',
|
||||
).trim(),
|
||||
);
|
||||
|
||||
const normalizeImageStatus = (status?: string, imageUri?: string): 'ok' | 'missing' | 'invalid' => {
|
||||
if (status === 'ok' || status === 'missing' || status === 'invalid') return status;
|
||||
const resolved = tryResolveImageUri(imageUri || '');
|
||||
if (resolved) return 'ok';
|
||||
return imageUri && imageUri.trim() ? 'invalid' : 'missing';
|
||||
};
|
||||
|
||||
const mapBackendEntry = (entry: Partial<BackendDatabaseEntry> & { imageUri?: string | null }): DatabaseEntry => {
|
||||
const imageStatus = normalizeImageStatus(entry.imageStatus, entry.imageUri || undefined);
|
||||
const strictImageUri = tryResolveImageUri(entry.imageUri || undefined);
|
||||
const imageUri = imageStatus === 'ok'
|
||||
? (strictImageUri || resolveImageUri(entry.imageUri))
|
||||
: (typeof entry.imageUri === 'string' ? entry.imageUri.trim() : '');
|
||||
|
||||
return {
|
||||
name: entry.name || '',
|
||||
botanicalName: entry.botanicalName || '',
|
||||
confidence: typeof entry.confidence === 'number' ? entry.confidence : 0,
|
||||
description: entry.description || '',
|
||||
careInfo: entry.careInfo || { waterIntervalDays: 7, light: 'Unknown', temp: 'Unknown' },
|
||||
imageUri,
|
||||
imageStatus,
|
||||
categories: Array.isArray(entry.categories) ? entry.categories : [],
|
||||
};
|
||||
};
|
||||
|
||||
export const PlantDatabaseService = {
|
||||
async getAllPlants(lang: Language): Promise<DatabaseEntry[]> {
|
||||
if (!hasConfiguredPlantBackend()) {
|
||||
return getMockCatalog(lang).map(mapBackendEntry);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getConfiguredApiBaseUrl()}/plants?lang=${lang}`);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const data = await response.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map(mapBackendEntry);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch plants', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async searchPlants(query: string, lang: Language, options: SearchOptions = {}): Promise<DatabaseEntry[]> {
|
||||
const { category, limit = DEFAULT_SEARCH_LIMIT } = options;
|
||||
|
||||
if (!hasConfiguredPlantBackend()) {
|
||||
let results = searchMockCatalog(query || '', lang, limit);
|
||||
if (category) {
|
||||
results = results.filter(r => r.categories.includes(category));
|
||||
}
|
||||
return results.map(mapBackendEntry);
|
||||
}
|
||||
|
||||
const url = new URL(`${getConfiguredApiBaseUrl()}/plants`);
|
||||
url.searchParams.append('lang', lang);
|
||||
if (query) url.searchParams.append('q', query);
|
||||
if (category) url.searchParams.append('category', category);
|
||||
if (limit) url.searchParams.append('limit', limit.toString());
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const data = await response.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map(mapBackendEntry);
|
||||
} catch (e) {
|
||||
console.error('Failed to search plants', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async semanticSearchDetailed(query: string, lang: Language): Promise<SemanticSearchResult> {
|
||||
const idempotencyKey = createIdempotencyKey(`semantic-${query}-${lang}`);
|
||||
try {
|
||||
const response = await backendApiClient.semanticSearch({
|
||||
query,
|
||||
language: lang,
|
||||
idempotencyKey,
|
||||
});
|
||||
|
||||
const results: DatabaseEntry[] = (response.results as BackendDatabaseEntry[]).map(mapBackendEntry);
|
||||
return { status: results.length > 0 ? 'success' : 'no_results', results };
|
||||
} catch (error) {
|
||||
if (isBackendApiError(error)) {
|
||||
if (error.code === 'INSUFFICIENT_CREDITS') {
|
||||
return { status: 'insufficient_credits', results: [] };
|
||||
}
|
||||
return { status: 'provider_error', results: [] };
|
||||
}
|
||||
return { status: 'timeout', results: [] };
|
||||
}
|
||||
},
|
||||
};
|
||||
import { IdentificationResult, Language } from '../types';
|
||||
import { resolveImageUri, tryResolveImageUri } from '../utils/imageUri';
|
||||
import { getConfiguredApiBaseUrl } from '../utils/backendUrl';
|
||||
import { backendApiClient } from './backend/backendApiClient';
|
||||
import { BackendDatabaseEntry, isBackendApiError } from './backend/contracts';
|
||||
import { createIdempotencyKey } from '../utils/idempotency';
|
||||
import { getMockCatalog, searchMockCatalog } from './backend/mockCatalog';
|
||||
|
||||
export interface DatabaseEntry extends IdentificationResult {
|
||||
imageUri: string;
|
||||
imageStatus?: 'ok' | 'missing' | 'invalid';
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
category?: string | null;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export type SemanticSearchStatus = 'success' | 'timeout' | 'provider_error' | 'no_results' | 'insufficient_credits';
|
||||
|
||||
export interface SemanticSearchResult {
|
||||
status: SemanticSearchStatus;
|
||||
results: DatabaseEntry[];
|
||||
}
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT = 500;
|
||||
|
||||
const hasConfiguredPlantBackend = (): boolean => Boolean(
|
||||
String(
|
||||
process.env.EXPO_PUBLIC_API_URL
|
||||
|| process.env.EXPO_PUBLIC_BACKEND_URL
|
||||
|| process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL
|
||||
|| '',
|
||||
).trim(),
|
||||
);
|
||||
|
||||
const normalizeImageStatus = (status?: string, imageUri?: string): 'ok' | 'missing' | 'invalid' => {
|
||||
if (status === 'ok' || status === 'missing' || status === 'invalid') return status;
|
||||
const resolved = tryResolveImageUri(imageUri || '');
|
||||
if (resolved) return 'ok';
|
||||
return imageUri && imageUri.trim() ? 'invalid' : 'missing';
|
||||
};
|
||||
|
||||
const mapBackendEntry = (entry: Partial<BackendDatabaseEntry> & { imageUri?: string | null }): DatabaseEntry => {
|
||||
const imageStatus = normalizeImageStatus(entry.imageStatus, entry.imageUri || undefined);
|
||||
const strictImageUri = tryResolveImageUri(entry.imageUri || undefined);
|
||||
const imageUri = imageStatus === 'ok'
|
||||
? (strictImageUri || resolveImageUri(entry.imageUri))
|
||||
: (typeof entry.imageUri === 'string' ? entry.imageUri.trim() : '');
|
||||
|
||||
return {
|
||||
name: entry.name || '',
|
||||
botanicalName: entry.botanicalName || '',
|
||||
confidence: typeof entry.confidence === 'number' ? entry.confidence : 0,
|
||||
description: entry.description || '',
|
||||
careInfo: entry.careInfo || { waterIntervalDays: 7, light: 'Unknown', temp: 'Unknown' },
|
||||
imageUri,
|
||||
imageStatus,
|
||||
categories: Array.isArray(entry.categories) ? entry.categories : [],
|
||||
};
|
||||
};
|
||||
|
||||
export const PlantDatabaseService = {
|
||||
async getAllPlants(lang: Language): Promise<DatabaseEntry[]> {
|
||||
if (!hasConfiguredPlantBackend()) {
|
||||
return getMockCatalog(lang).map(mapBackendEntry);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getConfiguredApiBaseUrl()}/plants?lang=${lang}`);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const data = await response.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map(mapBackendEntry);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch plants', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async searchPlants(query: string, lang: Language, options: SearchOptions = {}): Promise<DatabaseEntry[]> {
|
||||
const { category, limit = DEFAULT_SEARCH_LIMIT } = options;
|
||||
|
||||
if (!hasConfiguredPlantBackend()) {
|
||||
let results = searchMockCatalog(query || '', lang, limit);
|
||||
if (category) {
|
||||
results = results.filter(r => r.categories.includes(category));
|
||||
}
|
||||
return results.map(mapBackendEntry);
|
||||
}
|
||||
|
||||
const url = new URL(`${getConfiguredApiBaseUrl()}/plants`);
|
||||
url.searchParams.append('lang', lang);
|
||||
if (query) url.searchParams.append('q', query);
|
||||
if (category) url.searchParams.append('category', category);
|
||||
if (limit) url.searchParams.append('limit', limit.toString());
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const data = await response.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map(mapBackendEntry);
|
||||
} catch (e) {
|
||||
console.error('Failed to search plants', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async semanticSearchDetailed(query: string, lang: Language): Promise<SemanticSearchResult> {
|
||||
const idempotencyKey = createIdempotencyKey(`semantic-${query}-${lang}`);
|
||||
try {
|
||||
const response = await backendApiClient.semanticSearch({
|
||||
query,
|
||||
language: lang,
|
||||
idempotencyKey,
|
||||
});
|
||||
|
||||
const results: DatabaseEntry[] = (response.results as BackendDatabaseEntry[]).map(mapBackendEntry);
|
||||
return { status: results.length > 0 ? 'success' : 'no_results', results };
|
||||
} catch (error) {
|
||||
if (isBackendApiError(error)) {
|
||||
if (error.code === 'INSUFFICIENT_CREDITS') {
|
||||
return { status: 'insufficient_credits', results: [] };
|
||||
}
|
||||
return { status: 'provider_error', results: [] };
|
||||
}
|
||||
return { status: 'timeout', results: [] };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user