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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 7 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ jobs:
timeout-minutes: 60
permissions:
contents: write # Needed for git-auto-commit-action
defaults:
run:
working-directory: ai-sample-catalog
strategy:
fail-fast: false


steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -48,7 +52,7 @@ jobs:
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 🤖 Apply Spotless formatting
file_pattern: '**/*.kt **/*.kts **/*.java **/*.xml'
file_pattern: '**/*.kt **/*.kts **/*.xml'

- name: Build debug APK
run: ./gradlew assembleDebug --no-configuration-cache
Expand Down Expand Up @@ -119,6 +123,7 @@ jobs:

- name: Grant execute permission for gradlew
run: chmod +x gradlew
working-directory: ai-sample-catalog

- name: Build projects and run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
Expand All @@ -128,7 +133,7 @@ jobs:
disable-animations: true
disk-size: 6000M
heap-size: 600M
script: ./gradlew connectedDebugAndroidTest
script: cd ai-sample-catalog && ./gradlew connectedDebugAndroidTest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      script: ./gradlew connectedDebugAndroidTest
      working-directory: ai-sample-catalog



- name: Upload test reports
Expand Down
13 changes: 6 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/build
/.gradle
/.idea
/.kotlin
app/google-services.json
build
/local.properties
build/
.gradle/
.idea/
.kotlin/
**/local.properties
**/google-services.json
.DS_Store
90 changes: 18 additions & 72 deletions README.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions ai-sample-catalog/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
build/
.gradle/
.idea/
.kotlin/
app/google-services.json
local.properties
.DS_Store
File renamed without changes.
84 changes: 84 additions & 0 deletions ai-sample-catalog/README.md

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.android.ai.catalog.R
import com.android.ai.samples.geminichatbot.GeminiChatbotScreen
import com.android.ai.samples.geminihybrid.GeminiHybridScreen
import com.android.ai.samples.geminiimagechat.GeminiImageChatScreen
import com.android.ai.samples.geminilivetodo.ui.TodoScreen
import com.android.ai.samples.geminimultimodal.ui.GeminiMultimodalScreen
Expand All @@ -31,9 +32,8 @@ import com.android.ai.samples.geminivideosummary.ui.VideoSummarizationScreen
import com.android.ai.samples.genai_image_description.GenAIImageDescriptionScreen
import com.android.ai.samples.genai_summarization.GenAISummarizationScreen
import com.android.ai.samples.genai_writing_assistance.GenAIWritingAssistanceScreen
import com.android.ai.samples.geminihybrid.GeminiHybridScreen
import com.android.ai.samples.nanobanana.ui.NanobananaScreen
import com.android.ai.samples.magicselfie.ui.MagicSelfieScreen
import com.android.ai.samples.nanobanana.ui.NanobananaScreen
import com.android.ai.theme.extendedColorScheme
import com.google.firebase.ai.type.PublicPreviewAPI

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,12 @@ import com.google.firebase.ai.InferenceMode
import com.google.firebase.ai.OnDeviceModelOption
import com.google.firebase.ai.type.PublicPreviewAPI


@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun GeminiHybridScreen(viewModel: GeminiHybridViewModel = hiltViewModel()) {

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

val context = LocalContext.current
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher

Expand Down Expand Up @@ -167,7 +166,7 @@ fun GeminiHybridScreen(viewModel: GeminiHybridViewModel = hiltViewModel()) {
onTranslate = {
viewModel.translate(
uiState.reviewText,
uiState.selectedLanguage
uiState.selectedLanguage,
)
},
onReset = viewModel::reset,
Expand Down Expand Up @@ -412,10 +411,7 @@ fun LanguageDropdown(

@PublicPreviewAPI
@Composable
fun InferenceModeDropdown(
selectedMode: InferenceMode,
onModeSelected: (InferenceMode) -> Unit,
) {
fun InferenceModeDropdown(selectedMode: InferenceMode, onModeSelected: (InferenceMode) -> Unit, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
val modes = listOf(
InferenceMode.ONLY_ON_DEVICE to stringResource(R.string.gemini_hybrid_mode_only_on_device),
Expand All @@ -425,7 +421,7 @@ fun InferenceModeDropdown(
)
val selectedText = modes.find { it.first == selectedMode }?.second ?: ""

Box(modifier = Modifier.padding(start = 8.dp, top = 12.dp)) {
Box(modifier = modifier.padding(start = 8.dp, top = 12.dp)) {
SplitButtonLayout(
leadingButton = {
SplitButtonDefaults.LeadingButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import com.google.firebase.Firebase
import com.google.firebase.ai.InferenceMode
import com.google.firebase.ai.InferenceSource
import com.google.firebase.ai.OnDeviceConfig
import com.google.firebase.ai.ai
import com.google.firebase.ai.OnDeviceModelOption
import com.google.firebase.ai.OnDeviceModelStatus
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.PublicPreviewAPI
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -40,13 +40,13 @@ sealed interface GeminiStatus {
data class Generating(
val isCloud: Boolean,
val partialOutput: String = "",
val isTranslation: Boolean = false
val isTranslation: Boolean = false,
) : GeminiStatus

data class Success(
val output: String,
val isCloud: Boolean,
val isTranslation: Boolean = false
val isTranslation: Boolean = false,
) : GeminiStatus

data class Error(val message: String) : GeminiStatus
Expand All @@ -61,7 +61,7 @@ data class GeminiHybridUiState(
val reviewText: String = "",
val reviewInferenceStatus: Int? = null,
val selectedLanguage: String = "Korean",
val status: GeminiStatus = GeminiStatus.Initial
val status: GeminiStatus = GeminiStatus.Initial,
)

@PublicPreviewAPI
Expand All @@ -82,8 +82,8 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
"gemini-2.5-flash-lite",
onDeviceConfig = OnDeviceConfig(
mode = InferenceMode.ONLY_ON_DEVICE,
modelOption = OnDeviceModelOption.PREVIEW
)
modelOption = OnDeviceModelOption.PREVIEW,
),
)
val status = model.onDeviceExtension?.checkStatus()
val isAvailable = status == OnDeviceModelStatus.AVAILABLE
Expand All @@ -109,7 +109,7 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
"Korean" to R.string.gemini_hybrid_lang_korean,
"Spanish" to R.string.gemini_hybrid_lang_spanish,
"French" to R.string.gemini_hybrid_lang_french,
"German" to R.string.gemini_hybrid_lang_german
"German" to R.string.gemini_hybrid_lang_german,
)

fun setInferenceMode(mode: InferenceMode) {
Expand Down Expand Up @@ -150,8 +150,8 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
it.copy(
status = GeminiStatus.Generating(
isCloud = it.selectedMode == InferenceMode.ONLY_IN_CLOUD,
isTranslation = false
)
isTranslation = false,
),
)
}
try {
Expand All @@ -165,8 +165,8 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
"gemini-2.5-flash-lite",
onDeviceConfig = OnDeviceConfig(
mode = _uiState.value.selectedMode,
modelOption = _uiState.value.selectedModelOption
)
modelOption = _uiState.value.selectedModelOption,
),
)
model.generateContentStream(prompt).collect { chunk ->
val isCloud = chunk.inferenceSource == InferenceSource.IN_CLOUD
Expand All @@ -175,13 +175,13 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
val newStatus = if (currentStatus is GeminiStatus.Generating) {
currentStatus.copy(
isCloud = isCloud,
partialOutput = currentStatus.partialOutput + (chunk.text ?: "")
partialOutput = currentStatus.partialOutput + (chunk.text ?: ""),
)
} else {
GeminiStatus.Generating(
isCloud = isCloud,
partialOutput = chunk.text ?: "",
isTranslation = false
isTranslation = false,
)
}
state.copy(status = newStatus)
Expand All @@ -201,7 +201,7 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
it.copy(
reviewText = output,
reviewInferenceStatus = inferenceStatusResId,
status = GeminiStatus.Success(output, finalStatus.isCloud, isTranslation = false)
status = GeminiStatus.Success(output, finalStatus.isCloud, isTranslation = false),
)
}
}
Expand All @@ -225,8 +225,8 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
it.copy(
status = GeminiStatus.Generating(
isCloud = it.selectedMode == InferenceMode.ONLY_IN_CLOUD,
isTranslation = true
)
isTranslation = true,
),
)
}
try {
Expand All @@ -238,8 +238,8 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
"gemini-2.5-flash-lite",
onDeviceConfig = OnDeviceConfig(
mode = _uiState.value.selectedMode,
modelOption = _uiState.value.selectedModelOption
)
modelOption = _uiState.value.selectedModelOption,
),
)

model.generateContentStream(prompt).collect { chunk ->
Expand All @@ -249,13 +249,13 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
val newStatus = if (currentStatus is GeminiStatus.Generating) {
currentStatus.copy(
isCloud = isCloud,
partialOutput = currentStatus.partialOutput + (chunk.text ?: "")
partialOutput = currentStatus.partialOutput + (chunk.text ?: ""),
)
} else {
GeminiStatus.Generating(
isCloud = isCloud,
partialOutput = chunk.text ?: "",
isTranslation = true
isTranslation = true,
)
}
state.copy(status = newStatus)
Expand All @@ -270,8 +270,8 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {
status = GeminiStatus.Success(
finalStatus.partialOutput,
finalStatus.isCloud,
isTranslation = true
)
isTranslation = true,
),
)
}
}
Expand All @@ -286,7 +286,7 @@ class GeminiHybridViewModel @Inject constructor() : ViewModel() {

fun reset() {
_uiState.value = GeminiHybridUiState(
isPreviewModelAvailable = _uiState.value.isPreviewModelAvailable
isPreviewModelAvailable = _uiState.value.isPreviewModelAvailable,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package com.android.ai.samples.geminilivetodo.data

import kotlin.random.Random


data class Todo(
val id: Int = Random.nextInt(),
val task: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class TodoRepository @Inject constructor() {

fun getTodoList(): List<Todo> = _todos.value

fun addTodo(taskDescription: String) : Int? {
fun addTodo(taskDescription: String): Int? {
if (taskDescription.isNotBlank()) {
val newTodo = Todo(task = taskDescription)
_todos.update { currentList ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long

@OptIn(PublicPreviewAPI::class)
@HiltViewModel
Expand Down Expand Up @@ -198,7 +197,7 @@ class TodoScreenViewModel @Inject constructor(private val todoRepository: TodoRe
val taskDescription = functionCall.args["taskDescription"]!!.jsonPrimitive.content
val id = todoRepository.addTodo(taskDescription)

if (id!=null) {
if (id != null) {
val response = JsonObject(
mapOf(
"success" to JsonPrimitive(true),
Expand All @@ -215,7 +214,6 @@ class TodoScreenViewModel @Inject constructor(private val todoRepository: TodoRe
)
FunctionResponsePart(functionCall.name, response, functionCall.id)
}

}
"removeTodo" -> {
try {
Expand All @@ -237,7 +235,6 @@ class TodoScreenViewModel @Inject constructor(private val todoRepository: TodoRe
)
FunctionResponsePart(functionCall.name, response, functionCall.id)
}

}
"toggleTodoStatus" -> {
val taskId = functionCall.args["todoId"]!!.jsonPrimitive.int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class MagicSelfieRepository @Inject constructor() {
modelName = "gemini-3.1-flash-image-preview",
generationConfig = generationConfig {
responseModalities = listOf(ResponseModality.TEXT, ResponseModality.IMAGE)
}
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class NanobananaDataSource @Inject constructor() {
modelName = "gemini-3.1-flash-image-preview",
generationConfig = generationConfig {
responseModalities = listOf(ResponseModality.IMAGE)
}
},
)
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading