diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt index d185cc9..11d99b8 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt @@ -20,16 +20,16 @@ import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.content import com.google.firebase.example.friendlymeals.data.schema.MealSchema import com.google.firebase.example.friendlymeals.data.schema.RecipeSchema -import com.google.firebase.example.friendlymeals.data.schema.StoreLocalizerResult import com.google.firebase.perf.performance import com.google.firebase.perf.trace import com.google.firebase.remoteconfig.FirebaseRemoteConfig import kotlinx.serialization.json.Json import javax.inject.Inject import com.google.firebase.ai.type.LatLng -import com.google.firebase.ai.type.Tool -import com.google.firebase.ai.type.ToolConfig +import com.google.firebase.ai.type.TemplateTool +import com.google.firebase.ai.type.TemplateToolConfig import com.google.firebase.ai.type.retrievalConfig +import com.google.firebase.example.friendlymeals.data.schema.StoreFinderResult import com.google.firebase.example.friendlymeals.data.schema.StoreSchema @OptIn(PublicPreviewAPI::class) @@ -46,17 +46,16 @@ class AIRemoteDataSource @Inject constructor( private val templateGenerativeModel = aiModel.templateGenerativeModel() - suspend fun localizeIngredients( + suspend fun findStores( ingredients: List, latitude: Double, longitude: Double, currentTime: String, dayOfWeek: String ): List { - val groundingModel = aiModel.generativeModel( - modelName = remoteConfig.getString(GROUNDING_MODEL_KEY), - tools = listOf(Tool.googleMaps()), - toolConfig = ToolConfig( + val groundingModel = aiModel.templateGenerativeModel( + tools = listOf(TemplateTool.googleMaps()), + toolConfig = TemplateToolConfig( retrievalConfig = retrievalConfig { latLng = LatLng(latitude = latitude, longitude = longitude) languageCode = LANGUAGE @@ -64,13 +63,16 @@ class AIRemoteDataSource @Inject constructor( ) ) - val groundingPrompt = remoteConfig.getString(GROUNDING_PROMPT_KEY) - .replace("{{ingredients}}", ingredients.joinToString(", ")) - .replace("{{dayOfWeek}}", dayOfWeek) - .replace("{{currentTime}}", currentTime) - return try { - val response = groundingModel.generateContent(groundingPrompt) + val response = groundingModel.generateContent( + templateId = remoteConfig.getString(FIND_STORES_KEY), + inputs = mapOf( + INGREDIENTS_FIELD to ingredients.joinToString(), + DAY_FIELD to dayOfWeek, + TIME_FIELD to currentTime + ) + ) + val rawText = response.text ?: return emptyList() val cleanJson = rawText @@ -78,9 +80,9 @@ class AIRemoteDataSource @Inject constructor( .replace("```", "") .trim() - json.decodeFromString(cleanJson).stores + json.decodeFromString(cleanJson).stores } catch (e: Exception) { - Log.e(TAG, "Error localizing ingredients", e) + Log.e(TAG, "Error finding stores with these ingredients", e) emptyList() } } @@ -186,10 +188,9 @@ class AIRemoteDataSource @Inject constructor( private const val GENERATE_RECIPE_KEY = "generate_recipe" private const val GENERATE_RECIPE_PHOTO_GEMINI_KEY = "generate_recipe_photo_gemini" private const val SCAN_MEAL_KEY = "scan_meal" + private const val FIND_STORES_KEY = "find_stores" private const val HYBRID_CLOUD_MODEL_KEY = "hybrid_cloud_model" private const val HYBRID_INGREDIENTS_PROMPT_KEY = "hybrid_ingredients_prompt" - private const val GROUNDING_MODEL_KEY = "grounding_model" - private const val GROUNDING_PROMPT_KEY = "grounding_prompt" //Template input fields private const val IMAGE_DATA_FIELD = "imageData" @@ -197,6 +198,8 @@ class AIRemoteDataSource @Inject constructor( private const val INGREDIENTS_FIELD = "ingredients" private const val NOTES_FIELD = "notes" private const val RECIPE_TITLE_FIELD = "recipeTitle" + private const val DAY_FIELD = "dayOfWeek" + private const val TIME_FIELD = "currentTime" //Template input values private const val MIME_TYPE_VALUE = "image/jpeg" diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt index a19f6dc..2d945b4 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt @@ -14,14 +14,14 @@ class AIRepository @Inject constructor( return aiRemoteDataSource.generateIngredients(image) } - suspend fun localizeIngredients( + suspend fun findStores( ingredients: List, latitude: Double, longitude: Double, currentTime: String, dayOfWeek: String ): List { - return aiRemoteDataSource.localizeIngredients( + return aiRemoteDataSource.findStores( ingredients, latitude, longitude, diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt index c057384..543edbf 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt @@ -15,6 +15,6 @@ data class StoreSchema( ) @Serializable -data class StoreLocalizerResult( +data class StoreFinderResult( val stores: List = emptyList() ) diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt index 27cf646..ae7fd70 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt @@ -98,21 +98,21 @@ fun GroceryListScreen( onAddItem = viewModel::addItem, onToggleItem = viewModel::toggleItem, onDeleteItem = viewModel::deleteItem, - onLocalize = viewModel::localizeGroceryList, - onResetLocalizer = viewModel::resetLocalizer + onFindStore = viewModel::findStores, + onResetStores = viewModel::resetStoreFinder ) } @Composable fun GroceryListScreenContent( groceries: List, - uiState: StoreLocalizerUiState = StoreLocalizerUiState.Idle, + uiState: StoreFinderUiState = StoreFinderUiState.Idle, showError: () -> Unit = {}, onAddItem: (String) -> Unit = {}, onToggleItem: (GroceryItem) -> Unit = {}, onDeleteItem: (GroceryItem) -> Unit = {}, - onLocalize: (Double, Double) -> Unit = { _, _ -> }, - onResetLocalizer: () -> Unit = {} + onFindStore: (Double, Double) -> Unit = { _, _ -> }, + onResetStores: () -> Unit = {} ) { var inputText by remember { mutableStateOf("") } val context = LocalContext.current @@ -130,7 +130,7 @@ fun GroceryListScreenContent( coroutineScope.launch { getCurrentLocation(fusedLocationClient, onSuccess = { lat, lng -> - onLocalize(lat, lng) + onFindStore(lat, lng) }, onFailure = { showError() @@ -140,12 +140,12 @@ fun GroceryListScreenContent( } } - suspend fun startLocalizer() { + suspend fun startStoreFinder() { if (hasLocationPermission(context)) { showBottomSheet = true getCurrentLocation(fusedLocationClient, onSuccess = { lat, lng -> - onLocalize(lat, lng) + onFindStore(lat, lng) }, onFailure = { showError() @@ -184,14 +184,14 @@ fun GroceryListScreenContent( IconButton( onClick = { coroutineScope.launch { - startLocalizer() + startStoreFinder() } }, modifier = Modifier.size(48.dp) ) { Icon( painter = painterResource(R.drawable.ic_map_pin), - contentDescription = stringResource(R.string.store_localizer_button_content_description), + contentDescription = stringResource(R.string.store_finder_button_content_description), tint = Teal, modifier = Modifier.size(28.dp) ) @@ -275,17 +275,17 @@ fun GroceryListScreenContent( } } if (showBottomSheet) { - StoreLocalizerBottomSheet( + StoreFinderBottomSheet( uiState = uiState, onDismiss = { showBottomSheet = false - onResetLocalizer() + onResetStores() }, onRetry = { coroutineScope.launch { getCurrentLocation(fusedLocationClient, onSuccess = { lat, lng -> - onLocalize(lat, lng) + onFindStore(lat, lng) }, onFailure = { showError() @@ -376,8 +376,8 @@ fun GroceryCard( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun StoreLocalizerBottomSheet( - uiState: StoreLocalizerUiState, +fun StoreFinderBottomSheet( + uiState: StoreFinderUiState, onDismiss: () -> Unit, onRetry: () -> Unit ) { @@ -395,19 +395,19 @@ fun StoreLocalizerBottomSheet( .padding(bottom = 32.dp) ) { Text( - text = stringResource(R.string.store_localizer_title), + text = stringResource(R.string.store_finder_title), fontSize = 22.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 8.dp) ) Text( - text = stringResource(R.string.store_localizer_subtitle), + text = stringResource(R.string.store_finder_subtitle), fontSize = 14.sp, modifier = Modifier.padding(bottom = 20.dp) ) when (uiState) { - is StoreLocalizerUiState.Idle -> { + is StoreFinderUiState.Idle -> { Column( modifier = Modifier .fillMaxWidth() @@ -422,13 +422,13 @@ fun StoreLocalizerBottomSheet( Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(R.string.store_localizer_determining_location), + text = stringResource(R.string.store_finder_determining_location), fontSize = 16.sp, fontWeight = FontWeight.Medium ) } } - is StoreLocalizerUiState.Loading -> { + is StoreFinderUiState.Loading -> { Column( modifier = Modifier .fillMaxWidth() @@ -443,13 +443,13 @@ fun StoreLocalizerBottomSheet( Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(R.string.store_localizer_locating_stores), + text = stringResource(R.string.store_finder_locating_stores), fontSize = 16.sp, fontWeight = FontWeight.Medium ) } } - is StoreLocalizerUiState.Error -> { + is StoreFinderUiState.Error -> { Column( modifier = Modifier .fillMaxWidth() @@ -469,11 +469,11 @@ fun StoreLocalizerBottomSheet( onClick = onRetry, colors = ButtonDefaults.buttonColors(containerColor = Teal) ) { - Text(stringResource(R.string.store_localizer_retry)) + Text(stringResource(R.string.store_finder_retry)) } } } - is StoreLocalizerUiState.Success -> { + is StoreFinderUiState.Success -> { if (uiState.stores.isEmpty()) { Box( modifier = Modifier @@ -481,7 +481,7 @@ fun StoreLocalizerBottomSheet( .padding(vertical = 40.dp), contentAlignment = Alignment.Center ) { - Text(stringResource(R.string.store_localizer_no_stores)) + Text(stringResource(R.string.store_finder_no_stores)) } } else { LazyColumn( @@ -589,7 +589,7 @@ fun StoreCard(store: StoreSchema) { Spacer(modifier = Modifier.width(6.dp)) Text( - text = stringResource(R.string.store_localizer_open_now), + text = stringResource(R.string.store_finder_open_now), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF2E7D32) @@ -611,7 +611,7 @@ fun StoreCard(store: StoreSchema) { Spacer(modifier = Modifier.width(6.dp)) Text( - text = stringResource(R.string.store_localizer_closed), + text = stringResource(R.string.store_finder_closed), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFFC62828) @@ -633,7 +633,7 @@ fun StoreCard(store: StoreSchema) { ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = stringResource(R.string.store_localizer_closing_soon), + text = stringResource(R.string.store_finder_closing_soon), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFFEF6C00) @@ -643,7 +643,7 @@ fun StoreCard(store: StoreSchema) { val parkingColor = if (store.hasParking) Color(0xFFE3F2FD) else Color(0xFFECEFF1) val parkingTextColor = if (store.hasParking) Color(0xFF1565C0) else Color(0xFF455A64) - val text = if (store.hasParking) R.string.store_localizer_parking else R.string.store_localizer_no_parking + val text = if (store.hasParking) R.string.store_finder_parking else R.string.store_finder_no_parking Text( text = stringResource(text), diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt index c60eb5d..65079b7 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt @@ -24,8 +24,8 @@ class GroceryListViewModel @Inject constructor( private val _groceries = MutableStateFlow>(emptyList()) val groceries: StateFlow> = _groceries.asStateFlow() - private val _uiState = MutableStateFlow(StoreLocalizerUiState.Idle) - val uiState: StateFlow = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(StoreFinderUiState.Idle) + val uiState: StateFlow = _uiState.asStateFlow() val userId: String get() = authRepository.currentUser?.uid.orEmpty() @@ -70,28 +70,28 @@ class GroceryListViewModel @Inject constructor( } } - fun resetLocalizer() { - _uiState.value = StoreLocalizerUiState.Idle + fun resetStoreFinder() { + _uiState.value = StoreFinderUiState.Idle } - fun localizeGroceryList(latitude: Double, longitude: Double) { + fun findStores(latitude: Double, longitude: Double) { val uncheckedIngredients = _groceries.value .filter { !it.checked } .map { it.name } if (uncheckedIngredients.isEmpty()) { - _uiState.value = StoreLocalizerUiState.Error(EMPTY_ITEMS_ERROR) + _uiState.value = StoreFinderUiState.Error(EMPTY_ITEMS_ERROR) return } - _uiState.value = StoreLocalizerUiState.Loading + _uiState.value = StoreFinderUiState.Loading launchCatching { val now = LocalDateTime.now() val dayOfWeek = now.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.US) val currentTime = String.format(Locale.US, "%02d:%02d", now.hour, now.minute) - val stores = aiRepository.localizeIngredients( + val stores = aiRepository.findStores( ingredients = uncheckedIngredients, latitude = latitude, longitude = longitude, @@ -100,9 +100,9 @@ class GroceryListViewModel @Inject constructor( ) if (stores.isEmpty()) { - _uiState.value = StoreLocalizerUiState.Error(EMPTY_STORE_ERROR) + _uiState.value = StoreFinderUiState.Error(EMPTY_STORE_ERROR) } else { - _uiState.value = StoreLocalizerUiState.Success(stores) + _uiState.value = StoreFinderUiState.Success(stores) } } } diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreFinderUiState.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreFinderUiState.kt new file mode 100644 index 0000000..103790a --- /dev/null +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreFinderUiState.kt @@ -0,0 +1,10 @@ +package com.google.firebase.example.friendlymeals.ui.groceryList + +import com.google.firebase.example.friendlymeals.data.schema.StoreSchema + +sealed interface StoreFinderUiState { + object Idle : StoreFinderUiState + object Loading : StoreFinderUiState + data class Success(val stores: List) : StoreFinderUiState + data class Error(val message: String) : StoreFinderUiState +} \ No newline at end of file diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt deleted file mode 100644 index e6cd463..0000000 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.google.firebase.example.friendlymeals.ui.groceryList - -import com.google.firebase.example.friendlymeals.data.schema.StoreSchema - -sealed interface StoreLocalizerUiState { - object Idle : StoreLocalizerUiState - object Loading : StoreLocalizerUiState - data class Success(val stores: List) : StoreLocalizerUiState - data class Error(val message: String) : StoreLocalizerUiState -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f9ab68..de67148 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,16 +64,16 @@ Add ingredients to Grocery List Added to Grocery List! Your grocery list is empty - Ingredient-to-Store Localizer - Find stores nearby with the ingredients you need. - Determining your location… - Locating nearest stores with Google Maps… - No stores found matching your ingredients nearby. - Open Now - Closed - Closing Soon - Retry - 🅿️ Parking Available - No Parking - Store Localizer + Ingredient-to-Store Finder + Find stores nearby with the ingredients you need. + Determining your location… + Locating nearest stores with Google Maps… + No stores found matching your ingredients nearby. + Open Now + Closed + Closing Soon + Retry + 🅿️ Parking Available + No Parking + Store Finder \ No newline at end of file diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml index e4d0920..4673261 100644 --- a/app/src/main/res/xml/remote_config_defaults.xml +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -1,11 +1,7 @@ - grounding_model - gemini-3.1-flash-lite - - - grounding_prompt - What are the nearest grocery stores or markets near me that stock these ingredients: {{ingredients}}? For each place, tell me their business hours, if it's open right now on {{dayOfWeek}} at {{currentTime}}, and if it's closing in less than 30 minutes. Tell me what the parking situation is like at each place: is there a dedicated lot or should I look for street parking? Tell the Map URL so I can open it in Google Maps. Format your response strictly as a JSON object with a "stores" array containing store objects. Each store object must have these EXACT keys: "name": string, "address": string, "distance": string, "openNow": boolean, "closingSoon": boolean, "hasParking": boolean, "parkingDetails": string, "mapUrl": string. Do NOT include markdown code block formatting (like ```json). Output only the raw JSON string. + find_stores + find-stores-template-v1-0-0 hybrid_cloud_model