Skip to content

Commit 9b6440b

Browse files
authored
Merge pull request #48 from yml-org/feature/CM-1218/image-generations
feat: Add ImageGenerations API
2 parents d34eb8f + 1f69fb9 commit 9b6440b

File tree

33 files changed

+613
-12
lines changed

33 files changed

+613
-12
lines changed

buildSrc/src/main/kotlin/Dependencies.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ object Versions {
1111
const val COMPOSE_ACTIVITY = "1.6.1"
1212
const val COMPOSE_NAVIGATION = "2.5.3"
1313
const val COMPOSE_LIVEDATA = "1.3.3"
14+
const val COIL = "2.2.2"
1415
const val KTOR = "2.2.2"
1516
const val KOIN = "3.2.0"
1617
const val MATERIAL_DESIGN = "1.6.1"
@@ -52,6 +53,7 @@ object Dependencies {
5253
const val COMPOSE_ACTIVITY = "androidx.activity:activity-compose:${Versions.COMPOSE_ACTIVITY}"
5354
const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:${Versions.COMPOSE_NAVIGATION}"
5455
const val COMPOSE_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.COMPOSE_LIVEDATA}"
56+
const val COIL = "io.coil-kt:coil-compose:${Versions.COIL}"
5557
}
5658

5759
object Test {

sample/android/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ dependencies {
5353
implementation(Dependencies.UI.COMPOSE_ACTIVITY)
5454
implementation(Dependencies.UI.COMPOSE_NAVIGATION)
5555
implementation(Dependencies.UI.COMPOSE_LIVEDATA)
56+
implementation(Dependencies.UI.COIL)
5657
implementation(Dependencies.DI.KOIN_CORE)
5758
implementation(Dependencies.DI.KOIN_ANDROID)
5859
implementation(Dependencies.DI.KOIN_COMPOSE)

sample/android/src/main/java/co/yml/ychat/android/MainViewModel.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
66
import androidx.lifecycle.ViewModel
77
import androidx.lifecycle.viewModelScope
88
import co.yml.ychat.YChat
9+
import co.yml.ychat.YChat.Callback
910
import kotlinx.coroutines.delay
1011
import kotlinx.coroutines.launch
1112

@@ -20,6 +21,10 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() {
2021
)
2122
}
2223

24+
private val imageGenerations by lazy {
25+
chatGpt.imageGenerations()
26+
}
27+
2328
private val _items = mutableStateListOf<MessageItem>()
2429
val items = _items
2530

@@ -30,7 +35,7 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() {
3035
private var typingItem = mutableStateOf(MessageItem(message = typingTxt.value, isOut = false))
3136

3237
private fun setLoading(isLoading: Boolean) {
33-
_isLoading.value = isLoading
38+
_isLoading.postValue(isLoading)
3439
}
3540

3641
fun onSendMessage(message: String, typingStr: String) {
@@ -41,6 +46,22 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() {
4146
}
4247
}
4348

49+
fun onImageRequest(prompt: String, typingStr: String) {
50+
updateTypingMessage(typingStr)
51+
viewModelScope.launch {
52+
showTypingAnimation(prompt)
53+
imageGenerations.execute(prompt, object : Callback<List<String>> {
54+
override fun onSuccess(result: List<String>) {
55+
showImages(result)
56+
}
57+
58+
override fun onError(throwable: Throwable) {
59+
writeResponse(ERROR)
60+
}
61+
})
62+
}
63+
}
64+
4465
private suspend fun showTypingAnimation(message: String) {
4566
items.add(MessageItem(message = message, isOut = true))
4667
delay((1000..2000).random().toLong())
@@ -54,6 +75,14 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() {
5475
setLoading(false)
5576
}
5677

78+
private fun showImages(result: List<String>) {
79+
items.remove(items[items.lastIndex])
80+
result.forEach {
81+
items.add(MessageItem(message = IMAGE, isOut = false, url = it))
82+
}
83+
setLoading(false)
84+
}
85+
5786
private suspend fun requestCompletion(message: String): String {
5887
return try {
5988
chatCompletions.execute(message).last().content
@@ -77,5 +106,6 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() {
77106
companion object {
78107
private const val ERROR = "Error"
79108
private const val MAX_TOKENS = 1024
109+
private const val IMAGE = "image"
80110
}
81111
}

sample/android/src/main/java/co/yml/ychat/android/MessageItem.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ package co.yml.ychat.android
33
data class MessageItem(
44
val message: String,
55
val isOut: Boolean,
6+
val url: String? = null
67
)

sample/android/src/main/java/co/yml/ychat/android/ui/ChatLayout.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,15 @@ fun ChatLayout(
7777
.padding(spaceMedium),
7878
) {
7979
items(messages) { message ->
80-
MessageItemLayout(
81-
messageText = message.message, isOut = message.isOut
82-
)
80+
message.url?.let {
81+
ImageItemLayout(
82+
messageText = message.url, isOut = message.isOut
83+
)
84+
} ?: run {
85+
MessageItemLayout(
86+
messageText = message.message, isOut = message.isOut
87+
)
88+
}
8389
}
8490
coroutineScope.launch {
8591
listState.animateScrollToItem(messages.size)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package co.yml.ychat.android.ui
2+
3+
import android.content.res.Configuration.UI_MODE_NIGHT_YES
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.width
14+
import androidx.compose.foundation.shape.CircleShape
15+
import androidx.compose.foundation.shape.RoundedCornerShape
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.draw.clip
20+
import androidx.compose.ui.res.colorResource
21+
import androidx.compose.ui.res.painterResource
22+
import androidx.compose.ui.tooling.preview.Preview
23+
import androidx.compose.ui.unit.dp
24+
import co.yml.ychat.android.R
25+
import co.yml.ychat.android.ui.Dimensions.default
26+
import co.yml.ychat.android.ui.Dimensions.robotMessageIconSize
27+
import co.yml.ychat.android.ui.Dimensions.robotMessagePaddingSize
28+
import co.yml.ychat.android.ui.Dimensions.spaceExtraSmall
29+
import co.yml.ychat.android.ui.Dimensions.spaceMedium
30+
import co.yml.ychat.android.ui.Dimensions.spaceSmall
31+
import coil.compose.AsyncImage
32+
33+
@Composable
34+
fun ImageItemLayout(
35+
messageText: String,
36+
isOut: Boolean
37+
) {
38+
Column(
39+
modifier = Modifier.fillMaxWidth(),
40+
horizontalAlignment = if (isOut) Alignment.End else Alignment.Start
41+
) {
42+
Row(
43+
modifier = Modifier.padding(top = spaceMedium),
44+
verticalAlignment = Alignment.Bottom
45+
) {
46+
if (isOut.not()) {
47+
Image(
48+
painterResource(R.drawable.ic_robot),
49+
contentDescription = "",
50+
modifier = Modifier
51+
.width(robotMessageIconSize)
52+
.height(robotMessageIconSize)
53+
.clip(shape = CircleShape)
54+
.background(colorResource(id = R.color.softGreen))
55+
.padding(robotMessagePaddingSize),
56+
)
57+
Spacer(modifier = Modifier.padding(spaceExtraSmall))
58+
}
59+
Box(
60+
modifier = Modifier
61+
.clip(
62+
shape = RoundedCornerShape(
63+
topStart = spaceMedium,
64+
topEnd = spaceMedium,
65+
bottomEnd = if (isOut) default else spaceMedium,
66+
bottomStart = if (isOut) spaceMedium else default
67+
)
68+
)
69+
.background(if (isOut) colorResource(id = R.color.softBlue) else colorResource(id = R.color.opaqueWhite))
70+
.padding(spaceSmall)
71+
) {
72+
AsyncImage(
73+
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
74+
model = messageText,
75+
contentDescription = messageText,
76+
placeholder = painterResource(R.drawable.ic_robot),
77+
)
78+
}
79+
}
80+
}
81+
}
82+
83+
@Preview(uiMode = UI_MODE_NIGHT_YES)
84+
@Composable
85+
fun PreviewImageItemLayout() {
86+
MessageItemLayout(messageText = "Message", isOut = false)
87+
}

sample/android/src/main/java/co/yml/ychat/android/ui/SendMessageLayout.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ fun SendMessageLayout() {
5252
val scope = rememberCoroutineScope()
5353
val viewModel = koinViewModel<MainViewModel>()
5454
val isLoading: Boolean by viewModel.isLoading.observeAsState(initial = false)
55+
5556
Row(
5657
modifier = Modifier
5758
.background(color = MaterialTheme.colors.background)
@@ -91,7 +92,11 @@ fun SendMessageLayout() {
9192
.background(if (textFieldState.isNotEmpty() && isLoading.not()) colorResource(id = R.color.softBlue) else colorResource(id = R.color.opaqueWhite)),
9293
onClick = {
9394
scope.launch {
94-
viewModel.onSendMessage(textFieldState, typingString)
95+
if (textFieldState.startsWith("/image ")) {
96+
viewModel.onImageRequest(textFieldState, typingString)
97+
} else {
98+
viewModel.onSendMessage(textFieldState, typingString)
99+
}
95100
textFieldState = ""
96101
}
97102
},

sample/ios/YChatApp/Features/Completion/CompletionView.swift

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,14 @@ private extension CompletionView {
7272
}
7373
case .bot:
7474
HStack {
75-
botChatBubble(message: chatMessage.message)
76-
Spacer().frame(width: 60)
77-
Spacer()
75+
if let imageUrl = chatMessage.url {
76+
botImageBubble(imageUrl)
77+
Spacer()
78+
} else {
79+
botChatBubble(message: chatMessage.message)
80+
Spacer().frame(width: 60)
81+
Spacer()
82+
}
7883
}
7984
case .loading:
8085
HStack {
@@ -120,6 +125,28 @@ private extension CompletionView {
120125
.cornerRadius(16, corners: [.bottomLeft, .bottomLeft, .topRight])
121126
}
122127
}
128+
129+
@ViewBuilder
130+
private func botImageBubble(_ url: String) -> some View {
131+
HStack(alignment: .top, spacing: 4) {
132+
Circle()
133+
.fill(.green)
134+
.frame(width: 40, height: 40)
135+
.overlay {
136+
Image(uiImage: Icon.bot.uiImage)
137+
.renderingMode(.template)
138+
.foregroundColor(.white)
139+
}
140+
ZStack {
141+
AsyncImage(url: URL(string: url))
142+
.foregroundColor(.grayDark)
143+
}
144+
.padding(.horizontal, 16)
145+
.padding(.vertical, 8)
146+
.background(Color.grayLight)
147+
.cornerRadius(16, corners: [.bottomLeft, .bottomLeft, .topRight])
148+
}
149+
}
123150

124151
@ViewBuilder
125152
private func sendMessageSection() -> some View {

sample/ios/YChatApp/Features/Completion/Model/ChatMessage.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct ChatMessage: Identifiable, Equatable {
1212
let id: String
1313
var message: String = ""
1414
var type: MessageType = .human(error: false)
15+
var url: String?
1516

1617
enum MessageType: Equatable {
1718
case human(error: Bool), bot, loading

sample/ios/YChatApp/Features/Completion/ViewModel/CompletionViewModel.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ internal final class CompletionViewModel: ObservableObject {
1919
content: "You are a helpful assistant."
2020
)
2121

22+
private var imageGenerations: ImageGenerations =
23+
YChatCompanion.shared.create(apiKey: Config.apiKey)
24+
.imageGenerations()
25+
2226
@Published
2327
var message: String = ""
2428

@@ -37,9 +41,15 @@ internal final class CompletionViewModel: ObservableObject {
3741
cleanLastMessage()
3842
addLoading()
3943
do {
40-
let result = try await chatCompletions.execute(content: input)[0].content
41-
removeLoading()
42-
addAIMessage(message: result)
44+
if input.contains("/image ") {
45+
let result = try await imageGenerations.execute(prompt: input)[0].url
46+
removeLoading()
47+
addAIImage(url: result)
48+
} else {
49+
let result = try await chatCompletions.execute(content: input)[0].content
50+
removeLoading()
51+
addAIMessage(message: result)
52+
}
4353
} catch {
4454
removeLoading()
4555
setError()
@@ -64,6 +74,15 @@ internal final class CompletionViewModel: ObservableObject {
6474
)
6575
chatMessageList.append(chatMessage)
6676
}
77+
78+
private func addAIImage(url: String) {
79+
let chatMessage = ChatMessage(
80+
id: UUID().uuidString,
81+
type: .bot,
82+
url: url
83+
)
84+
chatMessageList.append(chatMessage)
85+
}
6786

6887
private func addLoading() {
6988
let chatMessage = ChatMessage(

0 commit comments

Comments
 (0)