Skip to content

Commit

Permalink
Add Circuit logic to AI Bot (#955)
Browse files Browse the repository at this point in the history
Adding Circuit logic, which has a Chat Screen state, UI (ChatWindowUi
object) and Presenter which handles the sending logic. I followed the
[tutorial](https://slackhq.github.io/circuit/tutorial/#__tabbed_2_1) and
Project Gen for reference, but I also haven't used Circuit before, so I
would appreciate any feedback.

f27ddc1

<!--
  ⬆ Put your description above this! ⬆

  Please be descriptive and detailed.
  
Please read our [Contributing
Guidelines](https://github.com/tinyspeck/slack-gradle-plugin/blob/main/.github/CONTRIBUTING.md)
and [Code of Conduct](https://slackhq.github.io/code-of-conduct).

Don't worry about deleting this, it's not visible in the PR!
-->
  • Loading branch information
kateliu20 authored Sep 16, 2024
1 parent cc2346c commit 7b2b6a4
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/
package slack.tooling.aibot

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.awt.ComposePanel
import com.slack.circuit.foundation.Circuit
import com.slack.circuit.foundation.CircuitContent
import java.awt.Dimension
import javax.swing.JComponent
import slack.tooling.projectgen.SlackDesktopTheme
Expand All @@ -24,7 +28,19 @@ object ChatPanel {
fun createPanel(): JComponent {
return ComposePanel().apply {
preferredSize = Dimension(400, 600)
setContent { SlackDesktopTheme { ChatWindow() } }
setContent { SlackDesktopTheme { ChatApp() } }
}
}

@Composable
private fun ChatApp() {
val circuit = remember {
Circuit.Builder()
.addPresenter<ChatScreen, ChatScreen.State>(ChatPresenter())
.addUi<ChatScreen, ChatScreen.State> { state, modifier -> ChatWindowUi(state, modifier) }
.build()
}

CircuitContent(ChatScreen, circuit = circuit)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 slack.tooling.aibot

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.slack.circuit.runtime.presenter.Presenter

class ChatPresenter : Presenter<ChatScreen.State> {
@Composable
override fun present(): ChatScreen.State {
var messages by remember { mutableStateOf(emptyList<Message>()) }

return ChatScreen.State(messages = messages) { event ->
when (event) {
is ChatScreen.Event.SendMessage -> {
val newMessage = Message(event.message, isMe = true)
messages = messages + newMessage
val response = Message(callApi(event.message), isMe = false)
messages = messages + response
}
}
}
}

private fun callApi(message: String): String {
// function set up to call the DevXP API in the future.
// right now, just sends back the user input message
return ("I am a bot. You said \"${message}\"")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 slack.tooling.aibot

import com.slack.circuit.runtime.CircuitUiEvent
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

sealed class Event : CircuitUiEvent {
data class SendMessage(val message: String) : Event()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -60,11 +59,10 @@ import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.TextArea

@Composable
fun ChatWindow(modifier: Modifier = Modifier) {
var messages by remember { mutableStateOf(listOf<Message>()) }
Column(modifier = Modifier.fillMaxSize().background(JewelTheme.globalColors.paneBackground)) {
fun ChatWindowUi(state: ChatScreen.State, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxSize().background(JewelTheme.globalColors.paneBackground)) {
LazyColumn(modifier = Modifier.weight(1f), reverseLayout = true) {
items(messages.reversed()) { message ->
items(state.messages.reversed()) { message ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isMe) Arrangement.End else Arrangement.Start,
Expand All @@ -74,18 +72,14 @@ fun ChatWindow(modifier: Modifier = Modifier) {
}
}
ConversationField(
modifier = modifier,
onSendMessage = { userMessage ->
messages = messages + Message(userMessage, true)
val response = callApi(userMessage)
messages = messages + Message(response, false)
},
modifier = Modifier,
onSendMessage = { userMessage -> state.eventSink(ChatScreen.Event.SendMessage(userMessage)) },
)
}
}

@Composable
fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (String) -> Unit) {
private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (String) -> Unit) {
var textValue by remember { mutableStateOf(TextFieldValue()) }
val isTextNotEmpty = textValue.text.isNotBlank()

Expand Down Expand Up @@ -130,8 +124,6 @@ fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (String) ->
}
},
placeholder = { Text("Start your conversation") },
keyboardActions = KeyboardActions.Default,
maxLines = Int.MAX_VALUE,
)
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
// button will be disabled if there is no text
Expand All @@ -155,7 +147,7 @@ fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (String) ->
}

@Composable
fun ChatBubble(message: Message, modifier: Modifier = Modifier) {
private fun ChatBubble(message: Message, modifier: Modifier = Modifier) {
Box(
Modifier.wrapContentWidth()
.padding(8.dp)
Expand All @@ -174,14 +166,6 @@ fun ChatBubble(message: Message, modifier: Modifier = Modifier) {
}
}

fun Modifier.enabled(enabled: Boolean): Modifier {
private fun Modifier.enabled(enabled: Boolean): Modifier {
return this.then(if (enabled) Modifier.alpha(1.0f) else Modifier.alpha(0.38f))
}

fun callApi(message: String): String {
// function set up to call the DevXP API in the future.
// right now, just sends back the user input message
return (message)
}

data class Message(val text: String, val isMe: Boolean)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 slack.tooling.aibot

import androidx.compose.runtime.Immutable

@Immutable data class Message(val text: String, val isMe: Boolean)

0 comments on commit 7b2b6a4

Please sign in to comment.