Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -46,41 +46,43 @@ class AIRemoteDataSource @Inject constructor(

private val templateGenerativeModel = aiModel.templateGenerativeModel()

suspend fun localizeIngredients(
suspend fun findStores(
ingredients: List<String>,
latitude: Double,
longitude: Double,
currentTime: String,
dayOfWeek: String
): List<StoreSchema> {
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
}
)
)

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
.replace("```json", "")
.replace("```", "")
.trim()

json.decodeFromString<StoreLocalizerResult>(cleanJson).stores
json.decodeFromString<StoreFinderResult>(cleanJson).stores
} catch (e: Exception) {
Log.e(TAG, "Error localizing ingredients", e)
Log.e(TAG, "Error finding stores with these ingredients", e)
emptyList()
}
}
Expand Down Expand Up @@ -186,17 +188,18 @@ 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"
private const val MIME_TYPE_FIELD = "mimeType"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ class AIRepository @Inject constructor(
return aiRemoteDataSource.generateIngredients(image)
}

suspend fun localizeIngredients(
suspend fun findStores(
ingredients: List<String>,
latitude: Double,
longitude: Double,
currentTime: String,
dayOfWeek: String
): List<StoreSchema> {
return aiRemoteDataSource.localizeIngredients(
return aiRemoteDataSource.findStores(
ingredients,
latitude,
longitude,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ data class StoreSchema(
)

@Serializable
data class StoreLocalizerResult(
data class StoreFinderResult(
val stores: List<StoreSchema> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<GroceryItem>,
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
Expand All @@ -130,7 +130,7 @@ fun GroceryListScreenContent(
coroutineScope.launch {
getCurrentLocation(fusedLocationClient,
onSuccess = { lat, lng ->
onLocalize(lat, lng)
onFindStore(lat, lng)
},
onFailure = {
showError()
Expand All @@ -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()
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -376,8 +376,8 @@ fun GroceryCard(

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StoreLocalizerBottomSheet(
uiState: StoreLocalizerUiState,
fun StoreFinderBottomSheet(
uiState: StoreFinderUiState,
onDismiss: () -> Unit,
onRetry: () -> Unit
) {
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -469,19 +469,19 @@ 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
.fillMaxWidth()
.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(
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class GroceryListViewModel @Inject constructor(
private val _groceries = MutableStateFlow<List<GroceryItem>>(emptyList())
val groceries: StateFlow<List<GroceryItem>> = _groceries.asStateFlow()

private val _uiState = MutableStateFlow<StoreLocalizerUiState>(StoreLocalizerUiState.Idle)
val uiState: StateFlow<StoreLocalizerUiState> = _uiState.asStateFlow()
private val _uiState = MutableStateFlow<StoreFinderUiState>(StoreFinderUiState.Idle)
val uiState: StateFlow<StoreFinderUiState> = _uiState.asStateFlow()

val userId: String get() = authRepository.currentUser?.uid.orEmpty()

Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
}
}
Expand Down
Loading