Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<string name="title_settings">Settings</string>
<string name="title_profile">Profile</string>
<string name="title_statistics">Statistics</string>
<string name="chat_title">AI Chat</string>
<string name="chat_message_hint">Enter message</string>
<string name="chat_api_key_hint">OpenAI API Key</string>
<string name="chat_send">Send</string>

<string name="title_start_date">Start Date</string>
<string name="title_end_date">End Date</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package feature.chat.presentation

import base.BaseViewModel
import feature.chat.presentation.models.ChatAction
import feature.chat.presentation.models.ChatEvent
import feature.chat.presentation.models.ChatMessage
import feature.chat.presentation.models.ChatViewState

class ChatViewModel : BaseViewModel<ChatViewState, ChatAction, ChatEvent>(
initialState = ChatViewState()
) {
override fun obtainEvent(viewEvent: ChatEvent) {
when (viewEvent) {
is ChatEvent.MessageChanged -> viewState = viewState.copy(currentMessage = viewEvent.text)
is ChatEvent.ApiKeyChanged -> viewState = viewState.copy(apiKey = viewEvent.key)
ChatEvent.SendClicked -> sendMessage()
}
}

private fun sendMessage() {
val text = viewState.currentMessage.trim()
if (text.isEmpty()) return
val updatedMessages = viewState.messages + ChatMessage(text, true)
val reply = generateReply(text)
viewState = viewState.copy(
messages = updatedMessages + ChatMessage(reply, false),
currentMessage = ""
)
}

private fun generateReply(message: String): String {
val calories = message.filter { it.isDigit() }.toIntOrNull()
return if (calories != null) {
"You logged $calories kcal. Consider adjusting your meal plan if this exceeds your goal."
} else {
"Thanks for the message!"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package feature.chat.presentation.models

sealed class ChatAction
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package feature.chat.presentation.models

sealed class ChatEvent {
data class MessageChanged(val text: String) : ChatEvent()
data class ApiKeyChanged(val key: String) : ChatEvent()
data object SendClicked : ChatEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package feature.chat.presentation.models

data class ChatMessage(
val text: String,
val isUser: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package feature.chat.presentation.models

data class ChatViewState(
val messages: List<ChatMessage> = emptyList(),
val currentMessage: String = "",
val apiKey: String = ""
)
114 changes: 114 additions & 0 deletions composeApp/src/commonMain/kotlin/feature/chat/ui/ChatScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package feature.chat.ui

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import feature.chat.presentation.ChatViewModel
import feature.chat.presentation.models.ChatEvent
import feature.chat.presentation.models.ChatMessage
import org.jetbrains.compose.resources.stringResource
import tech.mobiledeveloper.jethabit.resources.Res
import tech.mobiledeveloper.jethabit.resources.chat_api_key_hint
import tech.mobiledeveloper.jethabit.resources.chat_message_hint
import tech.mobiledeveloper.jethabit.resources.chat_send
import tech.mobiledeveloper.jethabit.resources.chat_title
import ui.themes.JetHabitTheme
import ui.themes.components.JetHabitButton

@Composable
fun ChatScreen(
navController: NavController,
viewModel: ChatViewModel = viewModel { ChatViewModel() }
) {
val viewState by viewModel.viewStates().collectAsState()

ChatView(viewState = viewState) { viewModel.obtainEvent(it) }
}

@Composable
private fun ChatView(viewState: feature.chat.presentation.models.ChatViewState, eventHandler: (ChatEvent) -> Unit) {
Surface(
modifier = Modifier.fillMaxSize(),
color = JetHabitTheme.colors.primaryBackground
) {
Column(modifier = Modifier.fillMaxSize().padding(JetHabitTheme.shapes.padding)) {
OutlinedTextField(
value = viewState.apiKey,
onValueChange = { eventHandler(ChatEvent.ApiKeyChanged(it)) },
modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(Res.string.chat_api_key_hint)) },
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = JetHabitTheme.colors.primaryText,
focusedBorderColor = JetHabitTheme.colors.tintColor,
unfocusedBorderColor = JetHabitTheme.colors.secondaryText,
focusedLabelColor = JetHabitTheme.colors.tintColor,
unfocusedLabelColor = JetHabitTheme.colors.secondaryText
)
)

Spacer(modifier = Modifier.height(16.dp))

LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth()
) {
items(viewState.messages) { message ->
ChatRow(message)
}
}

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = viewState.currentMessage,
onValueChange = { eventHandler(ChatEvent.MessageChanged(it)) },
modifier = Modifier.weight(1f),
label = { Text(stringResource(Res.string.chat_message_hint)) },
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = JetHabitTheme.colors.primaryText,
focusedBorderColor = JetHabitTheme.colors.tintColor,
unfocusedBorderColor = JetHabitTheme.colors.secondaryText,
focusedLabelColor = JetHabitTheme.colors.tintColor,
unfocusedLabelColor = JetHabitTheme.colors.secondaryText
)
)

Spacer(modifier = Modifier.width(8.dp))

JetHabitButton(
onClick = { eventHandler(ChatEvent.SendClicked) },
text = stringResource(Res.string.chat_send)
)
}
}
}
}

@Composable
private fun ChatRow(message: ChatMessage) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = if (message.isUser) JetHabitTheme.colors.tintColor else JetHabitTheme.colors.secondaryBackground
) {
Text(
text = message.text,
modifier = Modifier.padding(8.dp),
color = if (message.isUser) MaterialTheme.colors.onPrimary else JetHabitTheme.colors.primaryText
)
}
}
}
3 changes: 3 additions & 0 deletions composeApp/src/commonMain/kotlin/navigation/AppScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.outlined.Check
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.StringResource
Expand All @@ -12,6 +13,7 @@ import tech.mobiledeveloper.jethabit.resources.health_tab
import tech.mobiledeveloper.jethabit.resources.title_daily
import tech.mobiledeveloper.jethabit.resources.title_profile
import tech.mobiledeveloper.jethabit.resources.title_statistics
import tech.mobiledeveloper.jethabit.resources.chat_title

enum class AppScreens(
val title: String,
Expand All @@ -21,5 +23,6 @@ enum class AppScreens(
Daily("daily", Icons.Default.Home, Res.string.title_daily),
Health("health", Icons.Default.Favorite, Res.string.health_tab),
Statistics("statistics", Icons.Outlined.Check, Res.string.title_statistics),
Chat("chat", Icons.Default.Chat, Res.string.chat_title),
Profile("profile", Icons.Default.Person, Res.string.title_profile)
}
11 changes: 10 additions & 1 deletion composeApp/src/commonMain/kotlin/navigation/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import feature.daily.ui.DailyScreen
import feature.detail.ui.DetailScreen
import feature.health.list.ui.HealthScreen
import feature.health.track.ui.TrackHabitScreen
import feature.chat.ui.ChatScreen
import screens.settings.SettingsScreen
import feature.statistics.ui.StatisticsScreen
import feature.create.ui.ComposeScreen
Expand All @@ -33,6 +34,10 @@ enum class HealthScreens {
Start, Track, Create
}

enum class ChatScreens {
Start
}

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class,
ExperimentalComposeUiApi::class
)
Expand All @@ -43,6 +48,7 @@ fun MainScreen() {
AppScreens.Daily,
AppScreens.Health,
AppScreens.Statistics,
AppScreens.Chat,
AppScreens.Profile
)

Expand Down Expand Up @@ -73,9 +79,12 @@ fun MainScreen() {
ComposeScreen(type = type)
}
}
composable(AppScreens.Statistics.title) {
composable(AppScreens.Statistics.title) {
StatisticsScreen()
}
composable(AppScreens.Chat.title) {
ChatScreen(navController)
}
navigation(
startDestination = ProfileScreens.Start.name,
route = AppScreens.Profile.title
Expand Down
Loading