Skip to content

Commit 1deb5d8

Browse files
authored
Merge pull request #119 from Taewan-P/feat/edit-user-question
Edit user question support
2 parents 1127efb + 807e665 commit 1deb5d8

File tree

11 files changed

+293
-155
lines changed

11 files changed

+293
-155
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757

5858
## To be supported
5959

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

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ android {
1717
applicationId = "dev.chungjungsoo.gptmobile"
1818
minSdk = 31
1919
targetSdk = 35
20-
versionCode = 11
21-
versionName = "0.5.3"
20+
versionCode = 12
21+
versionName = "0.6.0"
2222

2323
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2424
vectorDrawables {

app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ class ChatRepositoryImpl @Inject constructor(
202202

203203
return model.generateContentStream(request)
204204
.map<com.google.ai.edge.aicore.GenerateContentResponse, ApiState> { response -> ApiState.Success(response.text ?: "") }
205-
.catch { throwable -> emit(ApiState.Error(throwable.message ?: "Unknown error")) }
205+
.catch { throwable -> emit(ApiState.Error("Cannot process this request at the moment.")) }
206206
.onStart { emit(ApiState.Loading) }
207207
.onCompletion { emit(ApiState.Done) }
208208
}

app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/theme/Theme.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.chungjungsoo.gptmobile.presentation.theme
22

33
import android.app.Activity
4-
import android.os.Build
54
import androidx.compose.foundation.isSystemInDarkTheme
65
import androidx.compose.material3.MaterialTheme
76
import androidx.compose.material3.darkColorScheme
@@ -12,7 +11,6 @@ import androidx.compose.runtime.Composable
1211
import androidx.compose.runtime.Immutable
1312
import androidx.compose.runtime.SideEffect
1413
import androidx.compose.ui.graphics.Color
15-
import androidx.compose.ui.graphics.toArgb
1614
import androidx.compose.ui.platform.LocalContext
1715
import androidx.compose.ui.platform.LocalView
1816
import androidx.core.view.WindowCompat
@@ -401,15 +399,15 @@ fun GPTMobileTheme(
401399
themeMode: ThemeMode = ThemeMode.LIGHT,
402400
content: @Composable () -> Unit
403401
) {
404-
val useDynamicColor = dynamicTheme == DynamicTheme.ON && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
402+
val useDynamicColor = dynamicTheme == DynamicTheme.ON
405403
val useDarkTheme = when (themeMode) {
406404
ThemeMode.SYSTEM -> isSystemInDarkTheme()
407405
ThemeMode.DARK -> true
408406
ThemeMode.LIGHT -> false
409407
}
410408

411409
val colorScheme = when {
412-
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
410+
useDynamicColor -> {
413411
val context = LocalContext.current
414412
if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
415413
}
@@ -422,8 +420,6 @@ fun GPTMobileTheme(
422420
SideEffect {
423421
val window = (view.context as Activity).window
424422
WindowCompat.setDecorFitsSystemWindows(window, false)
425-
window.statusBarColor = Color.Transparent.toArgb()
426-
window.navigationBarColor = Color.Transparent.toArgb()
427423
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !useDarkTheme
428424
}
429425
}

app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatBubble.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.size
1111
import androidx.compose.foundation.layout.width
1212
import androidx.compose.foundation.shape.RoundedCornerShape
1313
import androidx.compose.material.icons.Icons
14+
import androidx.compose.material.icons.outlined.Edit
15+
import androidx.compose.material.icons.rounded.Edit
1416
import androidx.compose.material.icons.rounded.Refresh
1517
import androidx.compose.material3.AssistChip
1618
import androidx.compose.material3.AssistChipDefaults
@@ -37,6 +39,8 @@ import dev.jeziellago.compose.markdowntext.MarkdownText
3739
fun UserChatBubble(
3840
modifier: Modifier = Modifier,
3941
text: String,
42+
isLoading: Boolean,
43+
onEditClick: () -> Unit,
4044
onCopyClick: () -> Unit
4145
) {
4246
val cardColor = CardColors(
@@ -59,7 +63,13 @@ fun UserChatBubble(
5963
linkifyMask = Linkify.WEB_URLS
6064
)
6165
}
62-
CopyTextChip(onCopyClick)
66+
Row {
67+
if (!isLoading) {
68+
EditTextChip(onEditClick)
69+
Spacer(modifier = Modifier.width(8.dp))
70+
}
71+
CopyTextChip(onCopyClick)
72+
}
6373
}
6474
}
6575

@@ -113,6 +123,21 @@ fun OpponentChatBubble(
113123
}
114124
}
115125

126+
@Composable
127+
private fun EditTextChip(onEditClick: () -> Unit) {
128+
AssistChip(
129+
onClick = onEditClick,
130+
label = { Text(stringResource(R.string.edit)) },
131+
leadingIcon = {
132+
Icon(
133+
Icons.Outlined.Edit,
134+
contentDescription = stringResource(R.string.edit),
135+
modifier = Modifier.size(AssistChipDefaults.IconSize)
136+
)
137+
}
138+
)
139+
}
140+
116141
@Composable
117142
private fun CopyTextChip(onCopyClick: () -> Unit) {
118143
AssistChip(
@@ -167,7 +192,7 @@ fun UserChatBubblePreview() {
167192
in Python?
168193
""".trimIndent()
169194
GPTMobileTheme {
170-
UserChatBubble(text = sampleText, onCopyClick = {})
195+
UserChatBubble(text = sampleText, isLoading = false, onCopyClick = {}, onEditClick = {})
171196
}
172197
}
173198

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package dev.chungjungsoo.gptmobile.presentation.ui.chat
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.height
8+
import androidx.compose.foundation.layout.heightIn
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.layout.widthIn
11+
import androidx.compose.foundation.rememberScrollState
12+
import androidx.compose.foundation.verticalScroll
13+
import androidx.compose.material.icons.Icons
14+
import androidx.compose.material.icons.filled.Done
15+
import androidx.compose.material.icons.rounded.Refresh
16+
import androidx.compose.material3.AlertDialog
17+
import androidx.compose.material3.Card
18+
import androidx.compose.material3.CardDefaults
19+
import androidx.compose.material3.FilledTonalButton
20+
import androidx.compose.material3.Icon
21+
import androidx.compose.material3.IconButton
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.OutlinedTextField
24+
import androidx.compose.material3.Text
25+
import androidx.compose.material3.TextButton
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.getValue
28+
import androidx.compose.runtime.mutableStateOf
29+
import androidx.compose.runtime.remember
30+
import androidx.compose.runtime.saveable.rememberSaveable
31+
import androidx.compose.runtime.setValue
32+
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.platform.LocalConfiguration
34+
import androidx.compose.ui.res.stringResource
35+
import androidx.compose.ui.text.font.FontWeight
36+
import androidx.compose.ui.unit.dp
37+
import androidx.compose.ui.window.DialogProperties
38+
import dev.chungjungsoo.gptmobile.R
39+
import dev.chungjungsoo.gptmobile.data.database.entity.Message
40+
41+
@Composable
42+
fun ChatTitleDialog(
43+
initialTitle: String,
44+
aiCoreModeEnabled: Boolean,
45+
aiGeneratedResult: String,
46+
isAICoreLoading: Boolean,
47+
onDefaultTitleMode: () -> String?,
48+
onAICoreTitleMode: () -> Unit,
49+
onRetryRequest: () -> Unit,
50+
onConfirmRequest: (title: String) -> Unit,
51+
onDismissRequest: () -> Unit
52+
) {
53+
val configuration = LocalConfiguration.current
54+
var title by rememberSaveable { mutableStateOf(initialTitle) }
55+
var useAICore by rememberSaveable { mutableStateOf(false) }
56+
val untitledChat = stringResource(R.string.untitled_chat)
57+
58+
AlertDialog(
59+
properties = DialogProperties(usePlatformDefaultWidth = false),
60+
modifier = Modifier
61+
.widthIn(max = configuration.screenWidthDp.dp - 40.dp)
62+
.heightIn(max = configuration.screenHeightDp.dp - 80.dp),
63+
title = { Text(text = stringResource(R.string.chat_title)) },
64+
text = {
65+
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
66+
Text(text = stringResource(R.string.chat_title_dialog_description))
67+
OutlinedTextField(
68+
modifier = Modifier
69+
.fillMaxWidth()
70+
.padding(horizontal = 8.dp, vertical = 16.dp),
71+
value = title,
72+
singleLine = true,
73+
isError = title.length > 50,
74+
supportingText = {
75+
if (title.length > 50) {
76+
Text(stringResource(R.string.title_length_limit, title.length))
77+
}
78+
},
79+
onValueChange = { title = it },
80+
label = { Text(stringResource(R.string.chat_title)) }
81+
)
82+
Row(
83+
modifier = Modifier.fillMaxWidth()
84+
) {
85+
FilledTonalButton(
86+
modifier = Modifier
87+
.padding(horizontal = 8.dp)
88+
.height(48.dp)
89+
.weight(1F),
90+
enabled = !isAICoreLoading,
91+
onClick = { title = onDefaultTitleMode.invoke() ?: untitledChat }
92+
) { Text(text = stringResource(R.string.default_mode)) }
93+
94+
FilledTonalButton(
95+
enabled = aiCoreModeEnabled && !isAICoreLoading,
96+
modifier = Modifier
97+
.padding(horizontal = 8.dp)
98+
.height(48.dp)
99+
.weight(1F),
100+
onClick = {
101+
onAICoreTitleMode.invoke()
102+
useAICore = true
103+
}
104+
) { Text(text = stringResource(R.string.ai_generated)) }
105+
}
106+
107+
if (useAICore) {
108+
Card(
109+
colors = CardDefaults.cardColors(
110+
containerColor = MaterialTheme.colorScheme.surfaceVariant
111+
),
112+
modifier = Modifier
113+
.fillMaxWidth()
114+
.padding(horizontal = 8.dp, vertical = 16.dp)
115+
) {
116+
Column(
117+
modifier = Modifier
118+
.fillMaxWidth()
119+
.heightIn(min = 64.dp)
120+
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 8.dp)
121+
) {
122+
Text(
123+
text = aiGeneratedResult.trimIndent() + if (isAICoreLoading) "" else "",
124+
fontWeight = FontWeight.Bold
125+
)
126+
Row(modifier = Modifier.fillMaxWidth()) {
127+
Spacer(Modifier.weight(1f))
128+
if (!isAICoreLoading) {
129+
IconButton(
130+
onClick = {
131+
title = aiGeneratedResult.trimIndent().replace('\n', ' ')
132+
useAICore = false
133+
}
134+
) { Icon(Icons.Default.Done, contentDescription = stringResource(R.string.apply_generated_title)) }
135+
IconButton(
136+
onClick = onRetryRequest
137+
) { Icon(Icons.Rounded.Refresh, contentDescription = stringResource(R.string.retry_ai_title)) }
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}
144+
},
145+
onDismissRequest = onDismissRequest,
146+
confirmButton = {
147+
TextButton(
148+
enabled = title.isNotBlank() && title != initialTitle,
149+
onClick = {
150+
onConfirmRequest(title)
151+
onDismissRequest()
152+
}
153+
) {
154+
Text(stringResource(R.string.update))
155+
}
156+
},
157+
dismissButton = {
158+
TextButton(
159+
onClick = onDismissRequest
160+
) {
161+
Text(stringResource(R.string.cancel))
162+
}
163+
}
164+
)
165+
}
166+
167+
@Composable
168+
fun ChatQuestionEditDialog(
169+
initialQuestion: Message,
170+
onDismissRequest: () -> Unit,
171+
onConfirmRequest: (q: Message) -> Unit
172+
) {
173+
val configuration = LocalConfiguration.current
174+
var question by remember { mutableStateOf(initialQuestion.content) }
175+
176+
AlertDialog(
177+
properties = DialogProperties(usePlatformDefaultWidth = false),
178+
modifier = Modifier
179+
.widthIn(max = configuration.screenWidthDp.dp - 40.dp)
180+
.heightIn(max = configuration.screenHeightDp.dp - 80.dp),
181+
title = { Text(text = stringResource(R.string.edit_question)) },
182+
text = {
183+
Column(
184+
modifier = Modifier.verticalScroll(rememberScrollState())
185+
) {
186+
OutlinedTextField(
187+
modifier = Modifier
188+
.fillMaxWidth()
189+
.heightIn(min = 80.dp)
190+
.padding(horizontal = 20.dp, vertical = 16.dp),
191+
value = question,
192+
onValueChange = { question = it },
193+
label = { Text(stringResource(R.string.user_message)) }
194+
)
195+
}
196+
},
197+
onDismissRequest = onDismissRequest,
198+
confirmButton = {
199+
TextButton(
200+
enabled = question.isNotBlank() && question != initialQuestion.content,
201+
onClick = { onConfirmRequest(initialQuestion.copy(content = question)) }
202+
) {
203+
Text(stringResource(R.string.confirm))
204+
}
205+
},
206+
dismissButton = {
207+
TextButton(
208+
onClick = onDismissRequest
209+
) {
210+
Text(stringResource(R.string.cancel))
211+
}
212+
}
213+
)
214+
}

0 commit comments

Comments
 (0)