Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee9bde8
Creating library
adalpari Oct 13, 2025
df7aa44
Creating list screen
adalpari Oct 13, 2025
488c373
Adding conversation screen
adalpari Oct 13, 2025
1fee31d
Adding the + and the welcome items
adalpari Oct 13, 2025
036fec7
Navigating to the new ai support
adalpari Oct 13, 2025
817feb0
Renaming
adalpari Oct 13, 2025
b74f1ab
Some refactor
adalpari Oct 13, 2025
77423c4
Extracting common methods
adalpari Oct 13, 2025
5ac7784
Accessing the API
adalpari Oct 13, 2025
6324b04
Modifying fields
adalpari Oct 13, 2025
d677ce0
Fixing message visibuil
adalpari Oct 13, 2025
e14fe06
Some styling
adalpari Oct 13, 2025
d804184
Extracting strings
adalpari Oct 13, 2025
82f1da0
Improving previews in code
adalpari Oct 13, 2025
54e8621
Previewing dark mode
adalpari Oct 13, 2025
2b6c494
Removing unused func
adalpari Oct 13, 2025
959072d
Some styling
adalpari Oct 13, 2025
0195829
Extracting model classes
adalpari Oct 14, 2025
368ea9a
Some refactoring
adalpari Oct 14, 2025
53463e8
detekt
adalpari Oct 14, 2025
f17a90f
Username
adalpari Oct 14, 2025
e80af2d
manifest fix
adalpari Oct 14, 2025
8cb5d72
Merge branch 'trunk' into feature/CMM-837-Oddie-bot-support-UI
adalpari Oct 14, 2025
617429d
Moving module inside wordpress
adalpari Oct 14, 2025
a29d75e
Fixing theme
adalpari Oct 14, 2025
d1e25ee
Message fix
adalpari Oct 14, 2025
fe88e81
Min fixes
adalpari Oct 14, 2025
cea5b38
Remove empty lines
adalpari Oct 14, 2025
df2a5a4
Merge remote-tracking branch 'origin/trunk' into feature/CMM-837-Oddi…
adalpari Oct 15, 2025
167992c
Removing debug code
adalpari Oct 15, 2025
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
1 change: 1 addition & 0 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ dependencies {
implementation project(":libs:login")
implementation("$gradle.ext.aboutAutomatticBinaryPath:${libs.versions.automattic.about.get()}")


implementation("$gradle.ext.gutenbergKitBinaryPath:${libs.versions.gutenberg.kit.get()}")

implementation(libs.automattic.rest) {
Expand Down
4 changes: 4 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@
android:theme="@style/WordPress.NoActionBar"
android:label="@string/subscribers"/>

<activity android:name="org.wordpress.android.support.aibot.ui.AIBotSupportActivity"
android:theme="@style/WordPress.NoActionBar"
android:label="@string/ai_bot_conversations_title"/>

<!-- Deep Linking Activity -->
<activity
android:name="org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.wordpress.android.support.aibot.model

import java.util.Date

data class BotConversation(
val id: Long,
val createdAt: Date,
val mostRecentMessageDate: Date,
val lastMessage: String,
val messages: List<BotMessage>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.wordpress.android.support.aibot.model

import java.util.Date

data class BotMessage(
val id: Long,
val text: String,
val date: Date,
val userWantsToTalkToHuman: Boolean,
val isWrittenByUser: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.wordpress.android.support.aibot.ui

import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.ui.compose.theme.AppThemeM3

@AndroidEntryPoint
class AIBotSupportActivity : AppCompatActivity() {
private val viewModel by viewModels<AIBotSupportViewModel>()

private lateinit var composeView: ComposeView
private lateinit var navController: NavHostController

private lateinit var userName: String

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userName = intent.getStringExtra(USERNAME).orEmpty()
composeView = ComposeView(this)
setContentView(
composeView.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.isForceDarkAllowed = false
}
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
NavigableContent()
}
}
)
viewModel.init(intent.getStringExtra(ACCESS_TOKEN_ID)!!)
}

private enum class ConversationScreen {
List,
Detail
}

@Composable
private fun NavigableContent() {
navController = rememberNavController()

AppThemeM3 {
NavHost(
navController = navController,
startDestination = ConversationScreen.List.name
) {
composable(route = ConversationScreen.List.name) {
ConversationsListScreen(
conversations = viewModel.conversations,
onConversationClick = { conversation ->
viewModel.selectConversation(conversation)
navController.navigate(ConversationScreen.Detail.name)
},
onBackClick = { finish() },
onCreateNewConversationClick = {
viewModel.createNewConversation()
viewModel.selectedConversation.value?.let { newConversation ->
navController.navigate(ConversationScreen.Detail.name)
}
}
)
}

composable(route = ConversationScreen.Detail.name) {
val selectedConversation by viewModel.selectedConversation.collectAsState()
selectedConversation?.let { conversation ->
ConversationDetailScreen(
userName = userName,
conversation = conversation,
onBackClick = { navController.navigateUp() },
onSendMessage = { text ->
viewModel.sendMessage(text)
}
)
}
}
}
}
}

companion object {
private const val ACCESS_TOKEN_ID = "arg_access_token_id"
private const val USERNAME = "arg_username"
@JvmStatic
fun createIntent(
context: Context,
accessToken: String,
userName: String,
): Intent = Intent(context, AIBotSupportActivity::class.java).apply {
putExtra(ACCESS_TOKEN_ID, accessToken)
putExtra(USERNAME, userName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package org.wordpress.android.support.aibot.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.wordpress.android.support.aibot.util.generateSampleBotConversations
import org.wordpress.android.support.aibot.model.BotConversation
import org.wordpress.android.support.aibot.model.BotMessage
import rs.wordpress.api.kotlin.WpComApiClient
import rs.wordpress.api.kotlin.WpRequestResult
import uniffi.wp_api.BotConversationSummary
import uniffi.wp_api.WpAuthentication
import uniffi.wp_api.WpAuthenticationProvider
import java.util.Date
import javax.inject.Inject

private const val BOT_ID = "jetpack-chat-mobile"

@HiltViewModel
class AIBotSupportViewModel @Inject constructor() : ViewModel() {
private val _conversations = MutableStateFlow<List<BotConversation>>(emptyList())
val conversations: StateFlow<List<BotConversation>> = _conversations.asStateFlow()

private val _selectedConversation = MutableStateFlow<BotConversation?>(null)
val selectedConversation: StateFlow<BotConversation?> = _selectedConversation.asStateFlow()

private lateinit var accessToken: String

private val wpComApiClient: WpComApiClient by lazy {
WpComApiClient(
WpAuthenticationProvider.staticWithAuth(WpAuthentication.Bearer(token = accessToken)
)
)
}

fun init(accessToken: String) {
loadDummyData()

this.accessToken = accessToken
// loadConversations()
}

fun loadConversations() {
viewModelScope.launch {
val response = wpComApiClient.request { requestBuilder ->
requestBuilder.supportBots().getBotConverationList(BOT_ID)
}
when (response) {
is WpRequestResult.Success -> {
val conversations = response.response.data
_conversations.value = conversations.toBotConversations()
}

else -> {
// stub for now
}
}
}
}

fun selectConversation(conversation: BotConversation) {
_selectedConversation.value = conversation
}

fun createNewConversation() {
val now = Date()

// Create initial bot greeting message
val greetingMessage = BotMessage(
id = 0,
text = "Hi! I'm here to help you with any questions about WordPress. How can I assist you today?",
date = now,
userWantsToTalkToHuman = false,
isWrittenByUser = false
)

val newConversation = BotConversation(
id = 0,
mostRecentMessageDate = now,
messages = listOf(greetingMessage),
createdAt = now,
lastMessage = greetingMessage.text
)

// Add to the top of the conversations list
_conversations.value = listOf(newConversation) + _conversations.value

// Select the new conversation
_selectedConversation.value = newConversation
}

fun sendMessage(text: String) {
val currentConversation = _selectedConversation.value ?: return
val now = Date()
val userMessageId = System.currentTimeMillis()

// Create new user message
val userMessage = BotMessage(
id = userMessageId,
text = text,
date = now,
userWantsToTalkToHuman = false,
isWrittenByUser = true
)

// Create bot response (dummy response for now)
val botMessage = BotMessage(
id = userMessageId + 1, // Ensure unique ID by incrementing
text = "Thanks for your message! This is a dummy response. In a real implementation, " +
"this would connect to the support bot API.",
date = Date(now.time + 1), // Slightly later timestamp
userWantsToTalkToHuman = false,
isWrittenByUser = false
)

// Update conversation with new messages
val updatedMessages = currentConversation.messages + listOf(userMessage, botMessage)
val updatedConversation = currentConversation.copy(
messages = updatedMessages,
mostRecentMessageDate = botMessage.date
)

// Update the conversation in the list
_conversations.value = _conversations.value.map { conversation ->
if (conversation.id == updatedConversation.id) {
updatedConversation
} else {
conversation
}
}

// Update selected conversation
_selectedConversation.value = updatedConversation
}

private fun loadDummyData() {
_conversations.value = generateSampleBotConversations()
}

private fun List<BotConversationSummary>.toBotConversations(): List<BotConversation> =
map { it.toBotConversation() }


private fun BotConversationSummary.toBotConversation(): BotConversation =
BotConversation (
id = chatId.toLong(),
createdAt = createdAt,
mostRecentMessageDate = lastMessage.createdAt,
lastMessage = lastMessage.content,
messages = listOf()
)
}
Loading