Skip to content
Open
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 @@ -21,29 +21,29 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.firebase.Firebase
import com.google.firebase.appcheck.appCheck
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory
import com.google.firebase.quickstart.ai.feature.live.BidiViewModel
import com.google.firebase.quickstart.ai.feature.hybrid.FunctionCallingViewModel
import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel
import com.google.firebase.quickstart.ai.feature.hybrid.ThinkingModeViewModel
import com.google.firebase.quickstart.ai.feature.hybrid.SystemInstructionViewModel
import com.google.firebase.quickstart.ai.feature.hybrid.ThinkingModeViewModel
import com.google.firebase.quickstart.ai.feature.live.BidiViewModel
import com.google.firebase.quickstart.ai.feature.text.ChatViewModel
import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
import com.google.firebase.quickstart.ai.ui.ChatScreen
import com.google.firebase.quickstart.ai.ui.FunctionCallingScreen
import com.google.firebase.quickstart.ai.ui.HybridInferenceScreen
import com.google.firebase.quickstart.ai.ui.ServerPromptScreen
import com.google.firebase.quickstart.ai.ui.StreamRealtimeScreen
import com.google.firebase.quickstart.ai.ui.StreamRealtimeVideoScreen
import com.google.firebase.quickstart.ai.ui.HybridInferenceScreen
import com.google.firebase.quickstart.ai.ui.SvgScreen
import com.google.firebase.quickstart.ai.ui.ThinkingModeScreen
import com.google.firebase.quickstart.ai.ui.SystemInstructionScreen
import com.google.firebase.quickstart.ai.ui.ThinkingModeScreen
import com.google.firebase.quickstart.ai.ui.navigation.FIREBASE_AI_SAMPLES
import com.google.firebase.quickstart.ai.ui.navigation.MainMenuScreen
import com.google.firebase.quickstart.ai.ui.navigation.ScreenType
Expand Down Expand Up @@ -143,6 +143,10 @@ class MainActivity : ComponentActivity() {
ScreenType.SYSTEM_INSTRUCTION -> {
(vm as? SystemInstructionViewModel)?.let { SystemInstructionScreen(it) }
}

ScreenType.FUNCTION_CALLING -> {
(vm as? FunctionCallingViewModel)?.let { FunctionCallingScreen(it) }
}
}
}
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
package com.google.firebase.quickstart.ai.ui

import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.quickstart.ai.feature.hybrid.FunctionCallingViewModel
import com.google.firebase.quickstart.ai.feature.hybrid.FunctionToolOption
import com.google.firebase.quickstart.ai.feature.hybrid.GenerationMethod
import com.google.firebase.quickstart.ai.feature.hybrid.InferenceModeOption
import com.google.firebase.quickstart.ai.feature.hybrid.OnDeviceModelOptionSelection

@OptIn(PublicPreviewAPI::class)
@Composable
fun FunctionCallingScreen(
viewModel: FunctionCallingViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
var prompt by rememberSaveable {
mutableStateOf("What was the weather in Boston, MA on October 17, 2024?")
}
var selectedToolOption by rememberSaveable { mutableStateOf(FunctionToolOption.MANUAL) }
var selectedModelOption by rememberSaveable { mutableStateOf(OnDeviceModelOptionSelection.DEFAULT) }
var selectedMode by rememberSaveable { mutableStateOf(InferenceModeOption.PREFER_ON_DEVICE) }
var selectedMethod by rememberSaveable { mutableStateOf(GenerationMethod.UNARY) }

Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = "On-Device Model Status",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text(
text = uiState.modelStatus,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}

Text(
text = "1. Function Tool Configuration:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FunctionToolOption.entries.forEach { option ->
FilterChip(
selected = selectedToolOption == option,
onClick = { selectedToolOption = option },
label = { Text(option.label) }
)
}
}

Text(
text = "2. On-Device Model Option:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OnDeviceModelOptionSelection.entries.forEach { modelOpt ->
FilterChip(
selected = selectedModelOption == modelOpt,
onClick = {
selectedModelOption = modelOpt
viewModel.checkAndDownloadModel(modelOpt.option)
},
Comment on lines +114 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To avoid redundant network requests and job cancellations when the user taps an already selected model option, add a check to ensure checkAndDownloadModel is only called when the selection actually changes.

                    onClick = {
                        if (selectedModelOption != modelOpt) {
                            selectedModelOption = modelOpt
                            viewModel.checkAndDownloadModel(modelOpt.option)
                        }
                    },

label = { Text(modelOpt.label) }
)
}
}

OutlinedTextField(
value = prompt,
onValueChange = { prompt = it },
label = { Text("User Prompt") },
modifier = Modifier.fillMaxWidth(),
maxLines = 4
)

Text(
text = "3. Inference Mode:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
InferenceModeOption.entries.forEach { modeOption ->
FilterChip(
selected = selectedMode == modeOption,
onClick = { selectedMode = modeOption },
label = { Text(modeOption.label) }
)
}
}

Text(
text = "4. Generation Method:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
GenerationMethod.entries.forEach { method ->
FilterChip(
selected = selectedMethod == method,
onClick = { selectedMethod = method },
label = { Text(method.label) }
)
}
}

Button(
onClick = {
viewModel.runTest(
promptText = prompt,
toolOption = selectedToolOption,
modelOptionSelection = selectedModelOption,
mode = selectedMode.mode,
method = selectedMethod
)
},
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.size(8.dp))
}
Text("Run Function Calling Test")
}

val errorMessage = uiState.errorMessage
if (errorMessage != null) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = "Error / Exception",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
text = errorMessage,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}

if (uiState.functionCalls.isNotEmpty()) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Function Call(s) Detected (response.functionCalls)",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
uiState.functionCalls.forEach { call ->
Column(modifier = Modifier.padding(vertical = 4.dp)) {
Text(
text = "Function: ${call.name}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "Arguments: ${call.args}",
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
if (call.id != null) {
Text(
text = "ID: ${call.id}",
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}

val execResult = uiState.functionExecutionResult
if (execResult != null) {
Spacer(modifier = Modifier.size(4.dp))
Text(
text = "Execution Result:",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = execResult,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}
}

val responseText = uiState.responseText
if (responseText != null) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Model Response (response.text)",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Surface(
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.small
) {
Text(
text = uiState.inferenceSource ?: "UNKNOWN",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold
)
}
}
Text(
text = responseText,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}

Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "Diagnostics & Logcat Summary",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold
)
Text(
text = uiState.logOutput,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace
)
}
}
}
}
Loading
Loading