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
5 changes: 5 additions & 0 deletions firebase-ai/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ dependencies {
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.ai)

// Image loading
implementation(libs.coil3.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.svg)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,5 +408,28 @@ val FIREBASE_AI_SAMPLES = listOf(
thinkingBudget = -1 // Dynamic Thinking
}
}
)
),
Sample(
title = "SVG Generator",
description = "Use Gemini 3 Flash preview to create SVG illustrations",
navRoute = "svg",
categories = listOf(Category.IMAGE, Category.TEXT),
initialPrompt = content {
text(
"a kitten"
)
},
generationConfig = generationConfig {
thinkingConfig {
thinkingBudget = -1
}
},
systemInstructions = content { text("""
You are an expert at turning image prompts into SVG code. When given a prompt,
use your creativity to code a 800x600 SVG rendering of it.
Always add viewBox="0 0 800 600" to the root svg tag. Do
not import external assets, they won't work. Return ONLY the SVG code, nothing else,
no commentary.
""".trimIndent()) }
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute
import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoScreen
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenScreen
import com.google.firebase.quickstart.ai.feature.svg.SvgRoute
import com.google.firebase.quickstart.ai.feature.svg.SvgScreen
import com.google.firebase.quickstart.ai.feature.text.ChatRoute
import com.google.firebase.quickstart.ai.feature.text.ChatScreen
import com.google.firebase.quickstart.ai.feature.text.TextGenRoute
Expand All @@ -47,7 +49,6 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

enableEdgeToEdge()
catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat)
setContent {
Expand Down Expand Up @@ -98,6 +99,9 @@ class MainActivity : ComponentActivity() {
"text" -> {
navController.navigate(TextGenRoute(it.id))
}
"svg" -> {
navController.navigate(SvgRoute(it.id))
}
}
}
)
Expand Down Expand Up @@ -125,6 +129,9 @@ class MainActivity : ComponentActivity() {
composable<TextGenRoute> {
TextGenScreen()
}
composable<SvgRoute> {
SvgScreen()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.google.firebase.quickstart.ai.feature.svg

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.SubcomposeAsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.Serializable
import java.nio.ByteBuffer

@Serializable
class SvgRoute(val sampleId: String)

@Composable
fun SvgScreen(
svgViewModel: SvgViewModel = viewModel<SvgViewModel>()
) {
var prompt by rememberSaveable { mutableStateOf(svgViewModel.initialPrompt) }
val errorMessage by svgViewModel.errorMessage.collectAsStateWithLifecycle()
val isLoading by svgViewModel.isLoading.collectAsStateWithLifecycle()
val generatedSvgs by svgViewModel.generatedSvgs.collectAsStateWithLifecycle()

Column {
ElevatedCard(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large
) {
OutlinedTextField(
value = prompt,
label = { Text("Generate a SVG of") },
placeholder = { Text("Enter text to generate image") },
onValueChange = { prompt = it },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
)
TextButton(
onClick = {
svgViewModel.generateSVG(prompt)
},
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 16.dp)
.align(Alignment.End)
) {
Text("Generate")
}
}
if (isLoading) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(all = 8.dp)
.align(Alignment.CenterHorizontally)
) {
CircularProgressIndicator()
}
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(generatedSvgs) { svg ->
Card(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(ByteBuffer.wrap(svg.toByteArray()))
.decoderFactory(SvgDecoder.Factory())
.decoderCoroutineContext(Dispatchers.Main)
.crossfade(true)
.build(),
contentDescription = "Generated SVG",
modifier = Modifier
.fillMaxWidth()
)
}
}
}
errorMessage?.let {
Card(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(all = 16.dp)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.google.firebase.quickstart.ai.feature.svg

import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.google.firebase.Firebase
import com.google.firebase.ai.GenerativeModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.TextPart
import com.google.firebase.ai.type.asTextOrNull
import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
import com.google.firebase.quickstart.ai.feature.text.ChatRoute
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class SvgViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val sampleId = savedStateHandle.toRoute<ChatRoute>().sampleId
private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId }
val initialPrompt: String =
sample.initialPrompt?.parts
?.filterIsInstance<TextPart>()
?.first()
?.asTextOrNull().orEmpty()

private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading

private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage

private val _generatedSvgList = mutableStateListOf<String>()
val generatedSvgs: StateFlow<List<String>> =
MutableStateFlow<List<String>>(_generatedSvgList)

private val generativeModel: GenerativeModel

init {
generativeModel = Firebase.ai(
backend = sample.backend
).generativeModel(
modelName = sample.modelName ?: "gemini-3-flash-preview",
systemInstruction = sample.systemInstructions,
generationConfig = sample.generationConfig,
tools = sample.tools
)
}

fun generateSVG(prompt: String) {
_isLoading.value = true
viewModelScope.launch(Dispatchers.IO) {
try {
val response = generativeModel.generateContent(prompt)
response.text?.let {
_generatedSvgList.add(0, it)
}
_errorMessage.value = null
} catch (e: Exception) {
_errorMessage.value = e.localizedMessage
} finally {
_isLoading.value = false
}
}

}
}
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ activityCompose = "1.12.1"
agp = "8.13.2"
camerax = "1.5.2"
coilCompose = "2.7.0"
coil3Compose = "3.3.0"
composeBom = "2025.12.00"
composeNavigation = "2.9.6"
coreKtx = "1.17.0"
Expand Down Expand Up @@ -44,6 +45,9 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3Compose" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3Compose" }
coil3-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3Compose" }
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"}
firebase-ai = { module = "com.google.firebase:firebase-ai" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
Expand Down
Loading