Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isLoading State to Chat #1018

Merged
merged 25 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f1d2089
Add isLoading state to set up loading animation
kateliu20 Oct 9, 2024
6bccd43
Run spotless
kateliu20 Oct 9, 2024
d58e16f
Reorder params
kateliu20 Oct 9, 2024
bef184b
Merge branch 'main' into kl/add_loading_state
kateliu20 Oct 9, 2024
750f1ac
Add loading animation
kateliu20 Oct 9, 2024
63c0bfb
run spotless
kateliu20 Oct 9, 2024
d93869e
Merge branch 'kl/add_loading_state' of github.com:slackhq/slack-gradl…
kateliu20 Oct 9, 2024
713c584
Uncomment setting isLoading to false
kateliu20 Oct 9, 2024
57dc232
Use 3 loading animation dots instead of 4
kateliu20 Oct 10, 2024
3ca3039
Adjust remember on animatedDots
kateliu20 Oct 11, 2024
5f8baf9
uncomment isLoading
kateliu20 Oct 11, 2024
4cf1980
Merge branch 'main' into kl/add_loading_state
kateliu20 Oct 14, 2024
57c180c
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Oct 14, 2024
49824e7
Merge branch 'kl/add_loading_state' of github.com:slackhq/slack-gradl…
kateliu20 Oct 14, 2024
a2bb67e
Merge branch 'main' into kl/add_loading_state
kateliu20 Oct 14, 2024
6bd30e5
Merge branch 'main' into kl/add_loading_state
kateliu20 Oct 14, 2024
843a85b
Convert to doc
kateliu20 Oct 14, 2024
54bb716
Merge branch 'kl/add_loading_state' of github.com:slackhq/slack-gradl…
kateliu20 Oct 14, 2024
23bf68d
Add description
kateliu20 Oct 14, 2024
166d850
apply spotless
kateliu20 Oct 14, 2024
d6d7098
Adjust kdoc
kateliu20 Oct 14, 2024
431d4ed
Run spotless
kateliu20 Oct 14, 2024
b457fec
Adjust comment
kateliu20 Oct 14, 2024
e582e72
Change animatedDots for each
kateliu20 Oct 15, 2024
432e408
Merge branch 'main' into kl/add_loading_state
kateliu20 Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ class ChatPresenter : Presenter<ChatScreen.State> {
@Composable
override fun present(): ChatScreen.State {
var messages by remember { mutableStateOf(emptyList<Message>()) }
var isLoading by remember { mutableStateOf(false) }

return ChatScreen.State(messages = messages) { event ->
return ChatScreen.State(messages = messages, isLoading = isLoading) { event ->
when (event) {
is ChatScreen.Event.SendMessage -> {
val newMessage = Message(event.message, isMe = true)
messages = messages + newMessage
isLoading = true
val response = Message(callApi(event.message), isMe = false)
messages = messages + response
isLoading = false
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.screen.Screen

object ChatScreen : Screen {
data class State(val messages: List<Message>, val eventSink: (Event) -> Unit = {}) :
CircuitUiState
data class State(
val messages: List<Message>,
val isLoading: Boolean,
val eventSink: (Event) -> Unit = {},
) : CircuitUiState

sealed class Event : CircuitUiEvent {
data class SendMessage(val message: String) : Event()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,23 @@ fun ChatWindowUi(state: ChatScreen.State, modifier: Modifier = Modifier) {
}
}
}
if (state.isLoading) {
LoadingAnimation(modifier = Modifier.padding(15.dp))
}
ConversationField(
isLoading = state.isLoading,
modifier = Modifier,
onSendMessage = { userMessage -> state.eventSink(ChatScreen.Event.SendMessage(userMessage)) },
)
}
}

@Composable
private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (String) -> Unit) {
private fun ConversationField(
isLoading: Boolean,
modifier: Modifier = Modifier,
onSendMessage: (String) -> Unit,
) {
val textState by remember { mutableStateOf(TextFieldState()) }
val isTextNotEmpty = textState.text.isNotBlank()
val (hasStartedConversation, setHasStartedConversation) = remember { mutableStateOf(false) }
Expand Down Expand Up @@ -131,13 +139,13 @@ private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (Str
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
// button will be disabled if there is no text
IconButton(
modifier = Modifier.padding(4.dp).enabled(isTextNotEmpty),
modifier = Modifier.padding(4.dp),
onClick = {
if (isTextNotEmpty) {
sendMessage()
}
},
enabled = isTextNotEmpty,
enabled = isTextNotEmpty && !isLoading,
) {
Icon(
painter = painterResource("drawable/send.svg"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2024 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foundry.intellij.compose.aibot

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import org.jetbrains.jewel.foundation.theme.JewelTheme

/**
* This shows a loading animation of three bouncing dots that occurs when the user is waiting on a
* response.
*
* Adopted from the Three-Dot Loading Animation Tutorial with Jetpack Compose by Stevdza-San from
*
* @see "https://www.youtube.com/watch?v=xakNOVaYLAg"
*/
@Composable
fun LoadingAnimation(
modifier: Modifier = Modifier,
dotSize: Dp = 7.dp,
dotColor: Color = JewelTheme.contentColor,
spacing: Dp = 7.dp,
movementDistance: Dp = 10.dp,
) {
val animatedDots = remember {
mutableStateListOf<Animatable<Float, AnimationVector1D>>().apply {
repeat(4) { add(Animatable(0f)) }
}
}
animatedDots.forEachIndexed { index, dot ->
LaunchedEffect(dot) {
delay(index * 100L)
dot.animateTo(
targetValue = 1f,
animationSpec =
infiniteRepeatable(
animation =
keyframes {
durationMillis = 1200
0.0f at 0 using LinearOutSlowInEasing
1.0f at 300 using LinearOutSlowInEasing
0.0f at 600 using LinearOutSlowInEasing
0.0f at 1200 using LinearOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
),
)
}
}

val pixelDistance = with(LocalDensity.current) { movementDistance.toPx() }

Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(spacing)) {
animatedDots.forEach { dot ->
Box(
modifier =
Modifier.size(dotSize)
.graphicsLayer { translationY = -dot.value * pixelDistance }
.background(color = dotColor, shape = CircleShape)
)
}
}
}