diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 07d3707..b4423ad 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -17,6 +17,10 @@
Settings
Profile
Statistics
+ AI Chat
+ Enter message
+ OpenAI API Key
+ Send
Start Date
End Date
diff --git a/composeApp/src/commonMain/kotlin/feature/chat/presentation/ChatViewModel.kt b/composeApp/src/commonMain/kotlin/feature/chat/presentation/ChatViewModel.kt
new file mode 100644
index 0000000..f399536
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/feature/chat/presentation/ChatViewModel.kt
@@ -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(
+ 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!"
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatAction.kt b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatAction.kt
new file mode 100644
index 0000000..fb9113b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatAction.kt
@@ -0,0 +1,3 @@
+package feature.chat.presentation.models
+
+sealed class ChatAction
diff --git a/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatEvent.kt b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatEvent.kt
new file mode 100644
index 0000000..8a0fcbe
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatEvent.kt
@@ -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()
+}
diff --git a/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatMessage.kt b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatMessage.kt
new file mode 100644
index 0000000..10af364
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatMessage.kt
@@ -0,0 +1,6 @@
+package feature.chat.presentation.models
+
+data class ChatMessage(
+ val text: String,
+ val isUser: Boolean
+)
diff --git a/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatViewState.kt b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatViewState.kt
new file mode 100644
index 0000000..99e6afb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/feature/chat/presentation/models/ChatViewState.kt
@@ -0,0 +1,7 @@
+package feature.chat.presentation.models
+
+data class ChatViewState(
+ val messages: List = emptyList(),
+ val currentMessage: String = "",
+ val apiKey: String = ""
+)
diff --git a/composeApp/src/commonMain/kotlin/feature/chat/ui/ChatScreen.kt b/composeApp/src/commonMain/kotlin/feature/chat/ui/ChatScreen.kt
new file mode 100644
index 0000000..fc46cf7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/feature/chat/ui/ChatScreen.kt
@@ -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
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/navigation/AppScreens.kt b/composeApp/src/commonMain/kotlin/navigation/AppScreens.kt
index 4a9da47..81aea1f 100644
--- a/composeApp/src/commonMain/kotlin/navigation/AppScreens.kt
+++ b/composeApp/src/commonMain/kotlin/navigation/AppScreens.kt
@@ -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
@@ -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,
@@ -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)
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/navigation/MainScreen.kt b/composeApp/src/commonMain/kotlin/navigation/MainScreen.kt
index 39b69fe..329cc0e 100644
--- a/composeApp/src/commonMain/kotlin/navigation/MainScreen.kt
+++ b/composeApp/src/commonMain/kotlin/navigation/MainScreen.kt
@@ -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
@@ -33,6 +34,10 @@ enum class HealthScreens {
Start, Track, Create
}
+enum class ChatScreens {
+ Start
+}
+
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class,
ExperimentalComposeUiApi::class
)
@@ -43,6 +48,7 @@ fun MainScreen() {
AppScreens.Daily,
AppScreens.Health,
AppScreens.Statistics,
+ AppScreens.Chat,
AppScreens.Profile
)
@@ -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