Skip to content

Commit

Permalink
Merge pull request #119 from Taewan-P/feat/edit-user-question
Browse files Browse the repository at this point in the history
Edit user question support
  • Loading branch information
Taewan-P authored Nov 18, 2024
2 parents 1127efb + 807e665 commit 1deb5d8
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 155 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@

## To be supported

- Manual Languages Setting for Android 12 and below
- More platforms
- Image, file support for multimodal models

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ android {
applicationId = "dev.chungjungsoo.gptmobile"
minSdk = 31
targetSdk = 35
versionCode = 11
versionName = "0.5.3"
versionCode = 12
versionName = "0.6.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class ChatRepositoryImpl @Inject constructor(

return model.generateContentStream(request)
.map<com.google.ai.edge.aicore.GenerateContentResponse, ApiState> { response -> ApiState.Success(response.text ?: "") }
.catch { throwable -> emit(ApiState.Error(throwable.message ?: "Unknown error")) }
.catch { throwable -> emit(ApiState.Error("Cannot process this request at the moment.")) }
.onStart { emit(ApiState.Loading) }
.onCompletion { emit(ApiState.Done) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.chungjungsoo.gptmobile.presentation.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
Expand All @@ -12,7 +11,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
Expand Down Expand Up @@ -401,15 +399,15 @@ fun GPTMobileTheme(
themeMode: ThemeMode = ThemeMode.LIGHT,
content: @Composable () -> Unit
) {
val useDynamicColor = dynamicTheme == DynamicTheme.ON && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val useDynamicColor = dynamicTheme == DynamicTheme.ON
val useDarkTheme = when (themeMode) {
ThemeMode.SYSTEM -> isSystemInDarkTheme()
ThemeMode.DARK -> true
ThemeMode.LIGHT -> false
}

val colorScheme = when {
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
useDynamicColor -> {
val context = LocalContext.current
if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
Expand All @@ -422,8 +420,6 @@ fun GPTMobileTheme(
SideEffect {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !useDarkTheme
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
Expand All @@ -37,6 +39,8 @@ import dev.jeziellago.compose.markdowntext.MarkdownText
fun UserChatBubble(
modifier: Modifier = Modifier,
text: String,
isLoading: Boolean,
onEditClick: () -> Unit,
onCopyClick: () -> Unit
) {
val cardColor = CardColors(
Expand All @@ -59,7 +63,13 @@ fun UserChatBubble(
linkifyMask = Linkify.WEB_URLS
)
}
CopyTextChip(onCopyClick)
Row {
if (!isLoading) {
EditTextChip(onEditClick)
Spacer(modifier = Modifier.width(8.dp))
}
CopyTextChip(onCopyClick)
}
}
}

Expand Down Expand Up @@ -113,6 +123,21 @@ fun OpponentChatBubble(
}
}

@Composable
private fun EditTextChip(onEditClick: () -> Unit) {
AssistChip(
onClick = onEditClick,
label = { Text(stringResource(R.string.edit)) },
leadingIcon = {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.edit),
modifier = Modifier.size(AssistChipDefaults.IconSize)
)
}
)
}

@Composable
private fun CopyTextChip(onCopyClick: () -> Unit) {
AssistChip(
Expand Down Expand Up @@ -167,7 +192,7 @@ fun UserChatBubblePreview() {
in Python?
""".trimIndent()
GPTMobileTheme {
UserChatBubble(text = sampleText, onCopyClick = {})
UserChatBubble(text = sampleText, isLoading = false, onCopyClick = {}, onEditClick = {})
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package dev.chungjungsoo.gptmobile.presentation.ui.chat

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import dev.chungjungsoo.gptmobile.R
import dev.chungjungsoo.gptmobile.data.database.entity.Message

@Composable
fun ChatTitleDialog(
initialTitle: String,
aiCoreModeEnabled: Boolean,
aiGeneratedResult: String,
isAICoreLoading: Boolean,
onDefaultTitleMode: () -> String?,
onAICoreTitleMode: () -> Unit,
onRetryRequest: () -> Unit,
onConfirmRequest: (title: String) -> Unit,
onDismissRequest: () -> Unit
) {
val configuration = LocalConfiguration.current
var title by rememberSaveable { mutableStateOf(initialTitle) }
var useAICore by rememberSaveable { mutableStateOf(false) }
val untitledChat = stringResource(R.string.untitled_chat)

AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier
.widthIn(max = configuration.screenWidthDp.dp - 40.dp)
.heightIn(max = configuration.screenHeightDp.dp - 80.dp),
title = { Text(text = stringResource(R.string.chat_title)) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(text = stringResource(R.string.chat_title_dialog_description))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp),
value = title,
singleLine = true,
isError = title.length > 50,
supportingText = {
if (title.length > 50) {
Text(stringResource(R.string.title_length_limit, title.length))
}
},
onValueChange = { title = it },
label = { Text(stringResource(R.string.chat_title)) }
)
Row(
modifier = Modifier.fillMaxWidth()
) {
FilledTonalButton(
modifier = Modifier
.padding(horizontal = 8.dp)
.height(48.dp)
.weight(1F),
enabled = !isAICoreLoading,
onClick = { title = onDefaultTitleMode.invoke() ?: untitledChat }
) { Text(text = stringResource(R.string.default_mode)) }

FilledTonalButton(
enabled = aiCoreModeEnabled && !isAICoreLoading,
modifier = Modifier
.padding(horizontal = 8.dp)
.height(48.dp)
.weight(1F),
onClick = {
onAICoreTitleMode.invoke()
useAICore = true
}
) { Text(text = stringResource(R.string.ai_generated)) }
}

if (useAICore) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 64.dp)
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 8.dp)
) {
Text(
text = aiGeneratedResult.trimIndent() + if (isAICoreLoading) "" else "",
fontWeight = FontWeight.Bold
)
Row(modifier = Modifier.fillMaxWidth()) {
Spacer(Modifier.weight(1f))
if (!isAICoreLoading) {
IconButton(
onClick = {
title = aiGeneratedResult.trimIndent().replace('\n', ' ')
useAICore = false
}
) { Icon(Icons.Default.Done, contentDescription = stringResource(R.string.apply_generated_title)) }
IconButton(
onClick = onRetryRequest
) { Icon(Icons.Rounded.Refresh, contentDescription = stringResource(R.string.retry_ai_title)) }
}
}
}
}
}
}
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
enabled = title.isNotBlank() && title != initialTitle,
onClick = {
onConfirmRequest(title)
onDismissRequest()
}
) {
Text(stringResource(R.string.update))
}
},
dismissButton = {
TextButton(
onClick = onDismissRequest
) {
Text(stringResource(R.string.cancel))
}
}
)
}

@Composable
fun ChatQuestionEditDialog(
initialQuestion: Message,
onDismissRequest: () -> Unit,
onConfirmRequest: (q: Message) -> Unit
) {
val configuration = LocalConfiguration.current
var question by remember { mutableStateOf(initialQuestion.content) }

AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier
.widthIn(max = configuration.screenWidthDp.dp - 40.dp)
.heightIn(max = configuration.screenHeightDp.dp - 80.dp),
title = { Text(text = stringResource(R.string.edit_question)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 80.dp)
.padding(horizontal = 20.dp, vertical = 16.dp),
value = question,
onValueChange = { question = it },
label = { Text(stringResource(R.string.user_message)) }
)
}
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
enabled = question.isNotBlank() && question != initialQuestion.content,
onClick = { onConfirmRequest(initialQuestion.copy(content = question)) }
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(
onClick = onDismissRequest
) {
Text(stringResource(R.string.cancel))
}
}
)
}
Loading

0 comments on commit 1deb5d8

Please sign in to comment.