diff --git a/Android/src/app/build.gradle.kts b/Android/src/app/build.gradle.kts index 36da147..a751214 100644 --- a/Android/src/app/build.gradle.kts +++ b/Android/src/app/build.gradle.kts @@ -30,7 +30,7 @@ android { minSdk = 26 targetSdk = 35 versionCode = 1 - versionName = "0.9.1" + versionName = "0.9.2" // Needed for HuggingFace auth workflows. manifestPlaceholders["appAuthRedirectScheme"] = "com.google.aiedge.gallery.oauth" diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt b/Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt index 9fb9caa..02c0138 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt +++ b/Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding 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.automirrored.rounded.ArrowBack 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.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.google.aiedge.gallery.data.AppBarAction @@ -70,6 +73,7 @@ fun GalleryTopAppBar( scrollBehavior: TopAppBarScrollBehavior? = null, subtitle: String = "", ) { + val titleColor = MaterialTheme.colorScheme.primary CenterAlignedTopAppBar( title = { Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -85,9 +89,16 @@ fun GalleryTopAppBar( tint = Color.Unspecified, ) } - Text( - title, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold) + BasicText( + text = title, + 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()) { diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/chat/ChatPanel.kt b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/chat/ChatPanel.kt index 3ae0043..626ef09 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/chat/ChatPanel.kt +++ b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/chat/ChatPanel.kt @@ -16,9 +16,10 @@ 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.SharedTransitionLayout +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -143,6 +144,18 @@ fun ChatPanel( val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current var selectedImageMessage by remember { mutableStateOf(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 val focusManager = LocalFocusManager.current @@ -222,378 +235,348 @@ fun ChatPanel( showErrorDialog = modelInitializationStatus?.status == ModelInitializationStatusType.ERROR } - SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { - AnimatedContent(targetState = selectedImageMessage) { targetSelectedImageMessage -> - Column( - modifier = modifier.imePadding() + Column( + modifier = modifier.imePadding() + ) { + Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier.weight(1f)) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(nestedScrollConnection), + state = listState, verticalArrangement = Arrangement.Top, ) { - Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier.weight(1f)) { - LazyColumn( + items(messages) { message -> + val imageHistoryCurIndex = remember { mutableIntStateOf(0) } + var hAlign: Alignment.Horizontal = Alignment.End + var backgroundColor: Color = MaterialTheme.customColors.userBubbleBgColor + var hardCornerAtLeftOrRight = false + var extraPaddingStart = 48.dp + var extraPaddingEnd = 0.dp + if (message.side == ChatSide.AGENT) { + hAlign = Alignment.Start + backgroundColor = MaterialTheme.customColors.agentBubbleBgColor + hardCornerAtLeftOrRight = true + extraPaddingStart = 0.dp + extraPaddingEnd = 48.dp + } else if (message.side == ChatSide.SYSTEM) { + extraPaddingStart = 24.dp + extraPaddingEnd = 24.dp + if (message.type == ChatMessageType.PROMPT_TEMPLATES) { + extraPaddingStart = 12.dp + extraPaddingEnd = 12.dp + } + } + if (message.type == ChatMessageType.IMAGE) { + backgroundColor = Color.Transparent + } + val bubbleBorderRadius = dimensionResource(R.dimen.chat_bubble_corner_radius) + + Column( modifier = Modifier - .fillMaxSize() - .nestedScroll(nestedScrollConnection), - state = listState, verticalArrangement = Arrangement.Top, + .fillMaxWidth() + .padding( + start = 12.dp + extraPaddingStart, + end = 12.dp + extraPaddingEnd, + top = 6.dp, + bottom = 6.dp, + ), + horizontalAlignment = hAlign, ) { - items(messages) { message -> - val imageHistoryCurIndex = remember { mutableIntStateOf(0) } - var hAlign: Alignment.Horizontal = Alignment.End - var backgroundColor: Color = MaterialTheme.customColors.userBubbleBgColor - var hardCornerAtLeftOrRight = false - var extraPaddingStart = 48.dp - var extraPaddingEnd = 0.dp - if (message.side == ChatSide.AGENT) { - hAlign = Alignment.Start - backgroundColor = MaterialTheme.customColors.agentBubbleBgColor - hardCornerAtLeftOrRight = true - extraPaddingStart = 0.dp - extraPaddingEnd = 48.dp - } else if (message.side == ChatSide.SYSTEM) { - extraPaddingStart = 24.dp - extraPaddingEnd = 24.dp - if (message.type == ChatMessageType.PROMPT_TEMPLATES) { - extraPaddingStart = 12.dp - extraPaddingEnd = 12.dp + // Sender row. + MessageSender( + message = message, + agentNameRes = task.agentNameRes, + imageHistoryCurIndex = imageHistoryCurIndex.intValue + ) + + // Message body. + when (message) { + // Loading. + is ChatMessageLoading -> MessageBodyLoading() + + // Info. + is ChatMessageInfo -> MessageBodyInfo(message = message) + + // Warning + is ChatMessageWarning -> MessageBodyWarning(message = message) + + // Config values change. + is ChatMessageConfigValuesChange -> MessageBodyConfigUpdate(message = message) + + // Prompt templates. + is ChatMessagePromptTemplates -> MessageBodyPromptTemplates( + message = message, + task = task, + onPromptClicked = { template -> + onSendMessage( + selectedModel, + listOf(ChatMessageText(content = template.prompt, side = ChatSide.USER)) + ) + }) + + // Non-system messages. + else -> { + // The bubble shape around the message body. + var messageBubbleModifier = Modifier + .clip( + MessageBubbleShape( + radius = bubbleBorderRadius, hardCornerAtLeftOrRight = hardCornerAtLeftOrRight + ) + ) + .background(backgroundColor) + if (message is ChatMessageText) { + messageBubbleModifier = messageBubbleModifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + longPressedMessage.value = message + showMessageLongPressedSheet = true + }, + ) + } } - } - if (message.type == ChatMessageType.IMAGE) { - backgroundColor = Color.Transparent - } - val bubbleBorderRadius = dimensionResource(R.dimen.chat_bubble_corner_radius) + Box( + modifier = messageBubbleModifier, + ) { + when (message) { + // Text + is ChatMessageText -> MessageBodyText(message = message) - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 12.dp + extraPaddingStart, - end = 12.dp + extraPaddingEnd, - top = 6.dp, - bottom = 6.dp, - ), - horizontalAlignment = hAlign, - ) { - // Sender row. - MessageSender( - message = message, - agentNameRes = task.agentNameRes, - imageHistoryCurIndex = imageHistoryCurIndex.intValue - ) - - // Message body. - when (message) { - // Loading. - is ChatMessageLoading -> MessageBodyLoading() - - // Info. - is ChatMessageInfo -> MessageBodyInfo(message = message) - - // Warning - is ChatMessageWarning -> MessageBodyWarning(message = message) - - // Config values change. - is ChatMessageConfigValuesChange -> MessageBodyConfigUpdate(message = message) - - // Prompt templates. - is ChatMessagePromptTemplates -> MessageBodyPromptTemplates(message = message, - task = task, - onPromptClicked = { template -> - onSendMessage( - selectedModel, - listOf(ChatMessageText(content = template.prompt, side = ChatSide.USER)) - ) - }) - - // Non-system messages. - else -> { - // The bubble shape around the message body. - var messageBubbleModifier = Modifier - .clip( - MessageBubbleShape( - radius = bubbleBorderRadius, - hardCornerAtLeftOrRight = hardCornerAtLeftOrRight - ) - ) - .background(backgroundColor) - if (message is ChatMessageText) { - messageBubbleModifier = messageBubbleModifier.pointerInput(Unit) { - detectTapGestures( - onLongPress = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - longPressedMessage.value = message - showMessageLongPressedSheet = true - }, - ) - } + // Image + is ChatMessageImage -> { + MessageBodyImage(message = message, modifier = Modifier.clickable { + selectedImageMessage = message + }) } - Box( - modifier = messageBubbleModifier, - ) { - when (message) { - // Text - is ChatMessageText -> MessageBodyText(message = message) - // Image - is ChatMessageImage -> { - if (targetSelectedImageMessage != message) { - MessageBodyImage( - message = message, - modifier = Modifier - .clickable { - selectedImageMessage = message - } - .sharedElement( - sharedContentState = rememberSharedContentState(key = "selected_image"), - animatedVisibilityScope = this@AnimatedContent, - clipInOverlayDuringTransition = OverlayClip( - MessageBubbleShape( - radius = bubbleBorderRadius - ) - ) - ), + // Image with history (for image gen) + is ChatMessageImageWithHistory -> MessageBodyImageWithHistory( + message = message, imageHistoryCurIndex = imageHistoryCurIndex + ) + + // Classification result + is ChatMessageClassification -> MessageBodyClassification( + message = message, modifier = Modifier.width( + message.maxBarWidth ?: CLASSIFICATION_BAR_MAX_WIDTH + ) + ) + + // Benchmark result. + is ChatMessageBenchmarkResult -> MessageBodyBenchmark(message = message) + + // Benchmark LLM result. + is ChatMessageBenchmarkLlmResult -> MessageBodyBenchmarkLlm( + message = message, modifier = Modifier.wrapContentWidth() + ) + + else -> {} + } + } + + if (message.side == ChatSide.AGENT) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + LatencyText(message = message) + // A button to show stats for the LLM message. + if (task.type.id.startsWith("llm_") && message is ChatMessageText + // This means we only want to show the action button when the message is done + // generating, at which point the latency will be set. + && message.latencyMs >= 0 + ) { + val showingStats = + viewModel.isShowingStats(model = selectedModel, message = message) + MessageActionButton( + label = if (showingStats) "Hide stats" else "Show stats", + icon = Icons.Outlined.Timer, + onClick = { + // Toggle showing stats. + viewModel.toggleShowingStats(selectedModel, message) + + // Add the stats message after the LLM message. + if (viewModel.isShowingStats( + model = selectedModel, message = message + ) + ) { + val llmBenchmarkResult = message.llmBenchmarkResult + if (llmBenchmarkResult != null) { + viewModel.insertMessageAfter( + model = selectedModel, + anchorMessage = message, + messageToAdd = llmBenchmarkResult, + ) + } + } + // Remove the stats message. + else { + val curMessageIndex = viewModel.getMessageIndex( + model = selectedModel, message = message + ) + viewModel.removeMessageAt( + model = selectedModel, index = curMessageIndex + 1 ) } - } - - // Image with history (for image gen) - is ChatMessageImageWithHistory -> MessageBodyImageWithHistory( - message = message, imageHistoryCurIndex = imageHistoryCurIndex - ) - - // Classification result - is ChatMessageClassification -> MessageBodyClassification( - message = message, modifier = Modifier.width( - message.maxBarWidth ?: CLASSIFICATION_BAR_MAX_WIDTH - ) - ) - - // Benchmark result. - is ChatMessageBenchmarkResult -> MessageBodyBenchmark(message = message) - - // Benchmark LLM result. - is ChatMessageBenchmarkLlmResult -> MessageBodyBenchmarkLlm( - message = message, modifier = Modifier.wrapContentWidth() - ) - - else -> {} - } + }, + enabled = !uiState.inProgress + ) + } + } + } else if (message.side == ChatSide.USER) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Run again button. + if (selectedModel.showRunAgainButton) { + MessageActionButton( + label = stringResource(R.string.run_again), + icon = Icons.Rounded.Refresh, + onClick = { + onRunAgainClicked(selectedModel, message) + }, + enabled = !uiState.inProgress + ) } - if (message.side == ChatSide.AGENT) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - LatencyText(message = message) - // A button to show stats for the LLM message. - if (task.type.id.startsWith("llm_") && message is ChatMessageText - // This means we only want to show the action button when the message is done - // generating, at which point the latency will be set. - && message.latencyMs >= 0 - ) { - val showingStats = - viewModel.isShowingStats(model = selectedModel, message = message) - MessageActionButton( - label = if (showingStats) "Hide stats" else "Show stats", - icon = Icons.Outlined.Timer, - onClick = { - // Toggle showing stats. - viewModel.toggleShowingStats(selectedModel, message) - // Add the stats message after the LLM message. - if (viewModel.isShowingStats( - model = selectedModel, message = message - ) - ) { - val llmBenchmarkResult = message.llmBenchmarkResult - if (llmBenchmarkResult != null) { - viewModel.insertMessageAfter( - model = selectedModel, - anchorMessage = message, - messageToAdd = llmBenchmarkResult, - ) - } - } - // Remove the stats message. - else { - val curMessageIndex = - viewModel.getMessageIndex( - model = selectedModel, - message = message - ) - viewModel.removeMessageAt( - model = selectedModel, index = curMessageIndex + 1 - ) - } - }, - enabled = !uiState.inProgress - ) - } - } - } else if (message.side == ChatSide.USER) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - // Run again button. - if (selectedModel.showRunAgainButton) { - MessageActionButton( - label = stringResource(R.string.run_again), - icon = Icons.Rounded.Refresh, - onClick = { - onRunAgainClicked(selectedModel, message) - }, - enabled = !uiState.inProgress - ) - } - - // Benchmark button - if (selectedModel.showBenchmarkButton) { - MessageActionButton( - label = stringResource(R.string.benchmark), - icon = Icons.Outlined.Timer, - onClick = { - showBenchmarkConfigsDialog = true - benchmarkMessage.value = message - }, - enabled = !uiState.inProgress - ) - } - } + // Benchmark button + if (selectedModel.showBenchmarkButton) { + MessageActionButton( + label = stringResource(R.string.benchmark), + icon = Icons.Outlined.Timer, + onClick = { + showBenchmarkConfigsDialog = true + benchmarkMessage.value = message + }, + enabled = !uiState.inProgress + ) } } } } } } - - SnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(vertical = 4.dp)) - - // Show an info message for ask image task to get users started. - if (task.type == TaskType.LLM_ASK_IMAGE && messages.isEmpty()) { - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - MessageBodyInfo( - ChatMessageInfo(content = "To get started, click + below to add an image and type a prompt to ask a question about it."), - smallFontSize = false - ) - } - } - } - - // Chat input - when (chatInputType) { - ChatInputType.TEXT -> { -// val isLlmTask = task.type == TaskType.LLM_CHAT -// val notLlmStartScreen = !(messages.size == 1 && messages[0] is ChatMessagePromptTemplates) - val hasImageMessage = messages.any { it is ChatMessageImage } - MessageInputText( - modelManagerViewModel = modelManagerViewModel, - curMessage = curMessage, - inProgress = uiState.inProgress, - isResettingSession = uiState.isResettingSession, - modelPreparing = uiState.preparing, - hasImageMessage = hasImageMessage, - modelInitializing = modelInitializationStatus?.status == ModelInitializationStatusType.INITIALIZING, - textFieldPlaceHolderRes = task.textInputPlaceHolderRes, - onValueChanged = { curMessage = it }, - onSendMessage = { - onSendMessage(selectedModel, it) - curMessage = "" - }, - onOpenPromptTemplatesClicked = { - onSendMessage( - selectedModel, listOf( - ChatMessagePromptTemplates( - templates = selectedModel.llmPromptTemplates, showMakeYourOwn = false - ) - ) - ) - }, - onStopButtonClicked = onStopButtonClicked, -// showPromptTemplatesInMenu = isLlmTask && notLlmStartScreen, - showPromptTemplatesInMenu = false, - showImagePickerInMenu = selectedModel.llmSupportImage, - showStopButtonWhenInProgress = showStopButtonInInputWhenInProgress, - ) - } - - ChatInputType.IMAGE -> MessageInputImage( - disableButtons = uiState.inProgress, - streamingMessage = streamingMessage, - onImageSelected = { bitmap -> - onSendMessage( - selectedModel, listOf( - ChatMessageImage( - bitmap = bitmap, imageBitMap = bitmap.asImageBitmap(), side = ChatSide.USER - ) - ) - ) - }, - onStreamImage = { bitmap -> - onStreamImageMessage( - selectedModel, ChatMessageImage( - bitmap = bitmap, imageBitMap = bitmap.asImageBitmap(), side = ChatSide.USER - ) - ) - }, - onStreamEnd = onStreamEnd, - ) } } - // A full-screen image viewer. - if (targetSelectedImageMessage != null) { - ZoomableBox( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.9f)) - .sharedElement( - rememberSharedContentState(key = "bounds"), - animatedVisibilityScope = this, - ) - .skipToLookaheadSize(), - ) { - // Image. - Image( - bitmap = targetSelectedImageMessage.imageBitMap, - contentDescription = "", - modifier = modifier - .fillMaxSize() - .graphicsLayer( - scaleX = scale, - scaleY = scale, - translationX = offsetX, - translationY = offsetY - ) - .sharedElement( - sharedContentState = rememberSharedContentState(key = "selected_image"), - animatedVisibilityScope = this@AnimatedContent, - ), - contentScale = ContentScale.Fit, - ) + SnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(vertical = 4.dp)) - // Close button. - IconButton( - onClick = { - selectedImageMessage = null - }, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - ), - modifier = Modifier.offset(x = (-8).dp, y = 8.dp) - ) { - Icon( - Icons.Rounded.Close, - contentDescription = "", - tint = MaterialTheme.colorScheme.primary - ) - } + // Show an info message for ask image task to get users started. + if (task.type == TaskType.LLM_ASK_IMAGE && messages.isEmpty()) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + MessageBodyInfo( + ChatMessageInfo(content = "To get started, click + below to add an image and type a prompt to ask a question about it."), + smallFontSize = false + ) } } } + + // Chat input + when (chatInputType) { + ChatInputType.TEXT -> { +// val isLlmTask = task.type == TaskType.LLM_CHAT +// val notLlmStartScreen = !(messages.size == 1 && messages[0] is ChatMessagePromptTemplates) + MessageInputText( + modelManagerViewModel = modelManagerViewModel, + curMessage = curMessage, + inProgress = uiState.inProgress, + isResettingSession = uiState.isResettingSession, + modelPreparing = uiState.preparing, + hasImageMessage = hasImageMessageToLastConfigChange, + modelInitializing = modelInitializationStatus?.status == ModelInitializationStatusType.INITIALIZING, + textFieldPlaceHolderRes = task.textInputPlaceHolderRes, + onValueChanged = { curMessage = it }, + onSendMessage = { + onSendMessage(selectedModel, it) + curMessage = "" + }, + onOpenPromptTemplatesClicked = { + onSendMessage( + selectedModel, listOf( + ChatMessagePromptTemplates( + templates = selectedModel.llmPromptTemplates, showMakeYourOwn = false + ) + ) + ) + }, + onStopButtonClicked = onStopButtonClicked, +// showPromptTemplatesInMenu = isLlmTask && notLlmStartScreen, + showPromptTemplatesInMenu = false, + showImagePickerInMenu = selectedModel.llmSupportImage, + showStopButtonWhenInProgress = showStopButtonInInputWhenInProgress, + ) + } + + ChatInputType.IMAGE -> MessageInputImage( + disableButtons = uiState.inProgress, + streamingMessage = streamingMessage, + onImageSelected = { bitmap -> + onSendMessage( + selectedModel, listOf( + ChatMessageImage( + bitmap = bitmap, imageBitMap = bitmap.asImageBitmap(), side = ChatSide.USER + ) + ) + ) + }, + onStreamImage = { bitmap -> + onStreamImageMessage( + selectedModel, ChatMessageImage( + bitmap = bitmap, imageBitMap = bitmap.asImageBitmap(), side = ChatSide.USER + ) + ) + }, + onStreamEnd = onStreamEnd, + ) + } + } + + // A full-screen image viewer. + val curSelectedImageMessage = selectedImageMessage + AnimatedVisibility( + visible = curSelectedImageMessage != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + if (curSelectedImageMessage == null) return@AnimatedVisibility + + ZoomableBox( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.9f)) + ) { + // Image. + Image( + bitmap = curSelectedImageMessage.imageBitMap, + contentDescription = "", + modifier = modifier + .fillMaxSize() + .graphicsLayer( + scaleX = scale, scaleY = scale, translationX = offsetX, translationY = offsetY + ), + contentScale = ContentScale.Fit, + ) + + // Close button. + IconButton( + onClick = { + selectedImageMessage = null + }, colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), modifier = Modifier.offset(x = (-8).dp, y = 8.dp) + ) { + Icon( + Icons.Rounded.Close, contentDescription = "", tint = MaterialTheme.colorScheme.primary + ) + } + } } // Error dialog. @@ -635,7 +618,7 @@ fun ChatPanel( } } - // Benchmark config dialog. +// Benchmark config dialog. if (showBenchmarkConfigsDialog) { BenchmarkConfigDialog(onDismissed = { showBenchmarkConfigsDialog = false }, messageToBenchmark = benchmarkMessage.value, @@ -644,7 +627,7 @@ fun ChatPanel( }) } - // Sheet to show when a message is long-pressed. +// Sheet to show when a message is long-pressed. if (showMessageLongPressedSheet) { val message = longPressedMessage.value if (message != null && message is ChatMessageText) { @@ -700,22 +683,20 @@ fun ZoomableBox( var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } var size by remember { mutableStateOf(IntSize.Zero) } - Box( - modifier = modifier - .clip(RectangleShape) - .onSizeChanged { size = it } - .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> - scale = maxOf(minScale, minOf(scale * zoom, maxScale)) - val maxX = (size.width * (scale - 1)) / 2 - val minX = -maxX - offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x)) - val maxY = (size.height * (scale - 1)) / 2 - val minY = -maxY - offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y)) - } - }, - contentAlignment = Alignment.TopEnd + Box(modifier = modifier + .clip(RectangleShape) + .onSizeChanged { size = it } + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + scale = maxOf(minScale, minOf(scale * zoom, maxScale)) + val maxX = (size.width * (scale - 1)) / 2 + val minX = -maxX + offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x)) + val maxY = (size.height * (scale - 1)) / 2 + val minY = -maxY + offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y)) + } + }, contentAlignment = Alignment.TopEnd ) { val scope = ZoomableBoxScopeImpl(scale, offsetX, offsetY) scope.content() @@ -729,9 +710,7 @@ interface ZoomableBoxScope { } private data class ZoomableBoxScopeImpl( - override val scale: Float, - override val offsetX: Float, - override val offsetY: Float + override val scale: Float, override val offsetX: Float, override val offsetY: Float ) : ZoomableBoxScope @Preview(showBackground = true) diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/home/HomeScreen.kt b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/home/HomeScreen.kt index f3c369d..7ec6cfb 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/home/HomeScreen.kt +++ b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/home/HomeScreen.kt @@ -16,7 +16,6 @@ package com.google.aiedge.gallery.ui.home -import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri @@ -354,7 +353,7 @@ private fun TaskList( val linkColor = MaterialTheme.customColors.linkColor 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( link = LinkAnnotation.Url( url = "https://huggingface.co/litert-community", // Replace with the actual URL diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/llmchat/LlmChatModelHelper.kt b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/llmchat/LlmChatModelHelper.kt index f0b8dec..11a94b0 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/llmchat/LlmChatModelHelper.kt +++ b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/llmchat/LlmChatModelHelper.kt @@ -42,6 +42,7 @@ object LlmChatModelHelper { fun initialize( context: Context, model: Model, onDone: (String) -> Unit ) { + // Prepare options. val maxTokens = model.getIntConfigValue(key = ConfigKey.MAX_TOKENS, defaultValue = DEFAULT_MAX_TOKEN) val topK = model.getIntConfigValue(key = ConfigKey.TOPK, defaultValue = DEFAULT_TOPK) @@ -62,7 +63,7 @@ object LlmChatModelHelper { .setMaxNumImages(if (model.llmSupportImage) 1 else 0) .build() - // Create an instance of the LLM Inference task + // Create an instance of the LLM Inference task and session. try { val llmInference = LlmInference.createFromOptions(context, options) @@ -145,6 +146,9 @@ object LlmChatModelHelper { } // 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 session.addQueryChunk(input) if (image != null) { diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/theme/Theme.kt b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/theme/Theme.kt index 4f95376..6e9e02b 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/theme/Theme.kt +++ b/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/theme/Theme.kt @@ -205,7 +205,8 @@ fun GalleryTheme( content: @Composable () -> Unit ) { 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) diff --git a/Android/src/app/src/main/res/values/strings.xml b/Android/src/app/src/main/res/values/strings.xml index 7b0242d..fafbfb2 100644 --- a/Android/src/app/src/main/res/values/strings.xml +++ b/Android/src/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ --> - AI Edge Gallery + Google AI Edge Gallery Model Manager %1$s downloaded Cancel