Re-enable image pickers when config is changed, and other UI bug fixes.

This commit is contained in:
Jing Jin 2025-05-19 11:08:03 -07:00
parent 0f5142e67e
commit 9544b8ddcc
7 changed files with 374 additions and 380 deletions

View file

@ -30,7 +30,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.9.1" versionName = "0.9.2"
// Needed for HuggingFace auth workflows. // Needed for HuggingFace auth workflows.
manifestPlaceholders["appAuthRedirectScheme"] = "com.google.aiedge.gallery.oauth" manifestPlaceholders["appAuthRedirectScheme"] = "com.google.aiedge.gallery.oauth"

View file

@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
@ -44,6 +46,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.aiedge.gallery.data.AppBarAction import com.google.aiedge.gallery.data.AppBarAction
@ -70,6 +73,7 @@ fun GalleryTopAppBar(
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
subtitle: String = "", subtitle: String = "",
) { ) {
val titleColor = MaterialTheme.colorScheme.primary
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
@ -85,9 +89,16 @@ fun GalleryTopAppBar(
tint = Color.Unspecified, tint = Color.Unspecified,
) )
} }
Text( BasicText(
title, text = title,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold) maxLines = 1,
color = { titleColor },
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
autoSize = TextAutoSize.StepBased(
minFontSize = 14.sp,
maxFontSize = 22.sp,
stepSize = 1.sp
)
) )
} }
if (subtitle.isNotEmpty()) { if (subtitle.isNotEmpty()) {

View file

@ -16,9 +16,10 @@
package com.google.aiedge.gallery.ui.common.chat package com.google.aiedge.gallery.ui.common.chat
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -143,6 +144,18 @@ fun ChatPanel(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
var selectedImageMessage by remember { mutableStateOf<ChatMessageImage?>(null) } var selectedImageMessage by remember { mutableStateOf<ChatMessageImage?>(null) }
val hasImageMessageToLastConfigChange = remember(messages) {
var foundImageMessage = false
for (message in messages.reversed()) {
if (message is ChatMessageConfigValuesChange) {
break
}
if (message is ChatMessageImage) {
foundImageMessage = true
}
}
foundImageMessage
}
var curMessage by remember { mutableStateOf("") } // Correct state var curMessage by remember { mutableStateOf("") } // Correct state
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -222,8 +235,6 @@ fun ChatPanel(
showErrorDialog = modelInitializationStatus?.status == ModelInitializationStatusType.ERROR showErrorDialog = modelInitializationStatus?.status == ModelInitializationStatusType.ERROR
} }
SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
AnimatedContent(targetState = selectedImageMessage) { targetSelectedImageMessage ->
Column( Column(
modifier = modifier.imePadding() modifier = modifier.imePadding()
) { ) {
@ -293,7 +304,8 @@ fun ChatPanel(
is ChatMessageConfigValuesChange -> MessageBodyConfigUpdate(message = message) is ChatMessageConfigValuesChange -> MessageBodyConfigUpdate(message = message)
// Prompt templates. // Prompt templates.
is ChatMessagePromptTemplates -> MessageBodyPromptTemplates(message = message, is ChatMessagePromptTemplates -> MessageBodyPromptTemplates(
message = message,
task = task, task = task,
onPromptClicked = { template -> onPromptClicked = { template ->
onSendMessage( onSendMessage(
@ -308,8 +320,7 @@ fun ChatPanel(
var messageBubbleModifier = Modifier var messageBubbleModifier = Modifier
.clip( .clip(
MessageBubbleShape( MessageBubbleShape(
radius = bubbleBorderRadius, radius = bubbleBorderRadius, hardCornerAtLeftOrRight = hardCornerAtLeftOrRight
hardCornerAtLeftOrRight = hardCornerAtLeftOrRight
) )
) )
.background(backgroundColor) .background(backgroundColor)
@ -333,24 +344,9 @@ fun ChatPanel(
// Image // Image
is ChatMessageImage -> { is ChatMessageImage -> {
if (targetSelectedImageMessage != message) { MessageBodyImage(message = message, modifier = Modifier.clickable {
MessageBodyImage(
message = message,
modifier = Modifier
.clickable {
selectedImageMessage = message selectedImageMessage = message
} })
.sharedElement(
sharedContentState = rememberSharedContentState(key = "selected_image"),
animatedVisibilityScope = this@AnimatedContent,
clipInOverlayDuringTransition = OverlayClip(
MessageBubbleShape(
radius = bubbleBorderRadius
)
)
),
)
}
} }
// Image with history (for image gen) // Image with history (for image gen)
@ -376,6 +372,7 @@ fun ChatPanel(
else -> {} else -> {}
} }
} }
if (message.side == ChatSide.AGENT) { if (message.side == ChatSide.AGENT) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -413,10 +410,8 @@ fun ChatPanel(
} }
// Remove the stats message. // Remove the stats message.
else { else {
val curMessageIndex = val curMessageIndex = viewModel.getMessageIndex(
viewModel.getMessageIndex( model = selectedModel, message = message
model = selectedModel,
message = message
) )
viewModel.removeMessageAt( viewModel.removeMessageAt(
model = selectedModel, index = curMessageIndex + 1 model = selectedModel, index = curMessageIndex + 1
@ -488,14 +483,13 @@ fun ChatPanel(
ChatInputType.TEXT -> { ChatInputType.TEXT -> {
// val isLlmTask = task.type == TaskType.LLM_CHAT // val isLlmTask = task.type == TaskType.LLM_CHAT
// val notLlmStartScreen = !(messages.size == 1 && messages[0] is ChatMessagePromptTemplates) // val notLlmStartScreen = !(messages.size == 1 && messages[0] is ChatMessagePromptTemplates)
val hasImageMessage = messages.any { it is ChatMessageImage }
MessageInputText( MessageInputText(
modelManagerViewModel = modelManagerViewModel, modelManagerViewModel = modelManagerViewModel,
curMessage = curMessage, curMessage = curMessage,
inProgress = uiState.inProgress, inProgress = uiState.inProgress,
isResettingSession = uiState.isResettingSession, isResettingSession = uiState.isResettingSession,
modelPreparing = uiState.preparing, modelPreparing = uiState.preparing,
hasImageMessage = hasImageMessage, hasImageMessage = hasImageMessageToLastConfigChange,
modelInitializing = modelInitializationStatus?.status == ModelInitializationStatusType.INITIALIZING, modelInitializing = modelInitializationStatus?.status == ModelInitializationStatusType.INITIALIZING,
textFieldPlaceHolderRes = task.textInputPlaceHolderRes, textFieldPlaceHolderRes = task.textInputPlaceHolderRes,
onValueChanged = { curMessage = it }, onValueChanged = { curMessage = it },
@ -545,32 +539,27 @@ fun ChatPanel(
} }
// A full-screen image viewer. // A full-screen image viewer.
if (targetSelectedImageMessage != null) { val curSelectedImageMessage = selectedImageMessage
AnimatedVisibility(
visible = curSelectedImageMessage != null,
enter = fadeIn(),
exit = fadeOut(),
) {
if (curSelectedImageMessage == null) return@AnimatedVisibility
ZoomableBox( ZoomableBox(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.Black.copy(alpha = 0.9f)) .background(Color.Black.copy(alpha = 0.9f))
.sharedElement(
rememberSharedContentState(key = "bounds"),
animatedVisibilityScope = this,
)
.skipToLookaheadSize(),
) { ) {
// Image. // Image.
Image( Image(
bitmap = targetSelectedImageMessage.imageBitMap, bitmap = curSelectedImageMessage.imageBitMap,
contentDescription = "", contentDescription = "",
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer( .graphicsLayer(
scaleX = scale, scaleX = scale, scaleY = scale, translationX = offsetX, translationY = offsetY
scaleY = scale,
translationX = offsetX,
translationY = offsetY
)
.sharedElement(
sharedContentState = rememberSharedContentState(key = "selected_image"),
animatedVisibilityScope = this@AnimatedContent,
), ),
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
@ -579,22 +568,16 @@ fun ChatPanel(
IconButton( IconButton(
onClick = { onClick = {
selectedImageMessage = null selectedImageMessage = null
}, }, colors = IconButtonDefaults.iconButtonColors(
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceVariant,
), ), modifier = Modifier.offset(x = (-8).dp, y = 8.dp)
modifier = Modifier.offset(x = (-8).dp, y = 8.dp)
) { ) {
Icon( Icon(
Icons.Rounded.Close, Icons.Rounded.Close, contentDescription = "", tint = MaterialTheme.colorScheme.primary
contentDescription = "",
tint = MaterialTheme.colorScheme.primary
) )
} }
} }
} }
}
}
// Error dialog. // Error dialog.
if (showErrorDialog) { if (showErrorDialog) {
@ -700,8 +683,7 @@ fun ZoomableBox(
var offsetX by remember { mutableFloatStateOf(0f) } var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) }
var size by remember { mutableStateOf(IntSize.Zero) } var size by remember { mutableStateOf(IntSize.Zero) }
Box( Box(modifier = modifier
modifier = modifier
.clip(RectangleShape) .clip(RectangleShape)
.onSizeChanged { size = it } .onSizeChanged { size = it }
.pointerInput(Unit) { .pointerInput(Unit) {
@ -714,8 +696,7 @@ fun ZoomableBox(
val minY = -maxY val minY = -maxY
offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y)) offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y))
} }
}, }, contentAlignment = Alignment.TopEnd
contentAlignment = Alignment.TopEnd
) { ) {
val scope = ZoomableBoxScopeImpl(scale, offsetX, offsetY) val scope = ZoomableBoxScopeImpl(scale, offsetX, offsetY)
scope.content() scope.content()
@ -729,9 +710,7 @@ interface ZoomableBoxScope {
} }
private data class ZoomableBoxScopeImpl( private data class ZoomableBoxScopeImpl(
override val scale: Float, override val scale: Float, override val offsetX: Float, override val offsetY: Float
override val offsetX: Float,
override val offsetY: Float
) : ZoomableBoxScope ) : ZoomableBoxScope
@Preview(showBackground = true) @Preview(showBackground = true)

View file

@ -16,7 +16,6 @@
package com.google.aiedge.gallery.ui.home package com.google.aiedge.gallery.ui.home
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -354,7 +353,7 @@ private fun TaskList(
val linkColor = MaterialTheme.customColors.linkColor val linkColor = MaterialTheme.customColors.linkColor
val introText = buildAnnotatedString { val introText = buildAnnotatedString {
append("Welcome to AI Edge Gallery! Explore a world of \namazing on-device models from ") append("Welcome to Google AI Edge Gallery! Explore a world of \namazing on-device models from ")
withLink( withLink(
link = LinkAnnotation.Url( link = LinkAnnotation.Url(
url = "https://huggingface.co/litert-community", // Replace with the actual URL url = "https://huggingface.co/litert-community", // Replace with the actual URL

View file

@ -42,6 +42,7 @@ object LlmChatModelHelper {
fun initialize( fun initialize(
context: Context, model: Model, onDone: (String) -> Unit context: Context, model: Model, onDone: (String) -> Unit
) { ) {
// Prepare options.
val maxTokens = val maxTokens =
model.getIntConfigValue(key = ConfigKey.MAX_TOKENS, defaultValue = DEFAULT_MAX_TOKEN) model.getIntConfigValue(key = ConfigKey.MAX_TOKENS, defaultValue = DEFAULT_MAX_TOKEN)
val topK = model.getIntConfigValue(key = ConfigKey.TOPK, defaultValue = DEFAULT_TOPK) val topK = model.getIntConfigValue(key = ConfigKey.TOPK, defaultValue = DEFAULT_TOPK)
@ -62,7 +63,7 @@ object LlmChatModelHelper {
.setMaxNumImages(if (model.llmSupportImage) 1 else 0) .setMaxNumImages(if (model.llmSupportImage) 1 else 0)
.build() .build()
// Create an instance of the LLM Inference task // Create an instance of the LLM Inference task and session.
try { try {
val llmInference = LlmInference.createFromOptions(context, options) val llmInference = LlmInference.createFromOptions(context, options)
@ -145,6 +146,9 @@ object LlmChatModelHelper {
} }
// Start async inference. // Start async inference.
//
// For a model that supports image modality, we need to add the text query chunk before adding
// image.
val session = instance.session val session = instance.session
session.addQueryChunk(input) session.addQueryChunk(input)
if (image != null) { if (image != null) {

View file

@ -205,7 +205,8 @@ fun GalleryTheme(
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val themeOverride = ThemeSettings.themeOverride val themeOverride = ThemeSettings.themeOverride
val darkTheme: Boolean = isSystemInDarkTheme() || themeOverride.value == THEME_DARK val darkTheme: Boolean =
(isSystemInDarkTheme() || themeOverride.value == THEME_DARK) && themeOverride.value != THEME_LIGHT
StatusBarColorController(useDarkTheme = darkTheme) StatusBarColorController(useDarkTheme = darkTheme)

View file

@ -15,7 +15,7 @@
--> -->
<resources> <resources>
<string name="app_name">AI Edge Gallery</string> <string name="app_name">Google AI Edge Gallery</string>
<string name="model_manager">Model Manager</string> <string name="model_manager">Model Manager</string>
<string name="downloaded_size">%1$s downloaded</string> <string name="downloaded_size">%1$s downloaded</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>