diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index c1b0a6c..7de2575 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -11,9 +11,7 @@ on: jobs: build-and-test: - runs-on: macos-latest - env: - ANDROID_SDK_ROOT: /Users/runner/Library/Android/sdk + runs-on: ubuntu-latest steps: - name: Checkout repository @@ -35,35 +33,44 @@ jobs: distribution: temurin java-version: 17 + - name: Enable KVM + run: | + sudo apt-get update + sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils + sudo adduser $USER libvirt + sudo adduser $USER kvm + sudo chmod 666 /dev/kvm + ls -l /dev/kvm + - name: Install Android SDK - uses: android-actions/setup-android@v2 - with: - api-level: 33 - build-tools: "33.0.2" - components: | - platform-tools - emulator - tools - platforms;android-33 - system-images;android-33;google_apis;x86_64 + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + sdkmanager --install \ + "platform-tools" \ + "platforms;android-33" \ + "build-tools;33.0.2" \ + "emulator" \ + "system-images;android-33;google_apis;x86_64" - name: Accept Android SDK licenses run: yes | sdkmanager --licenses - - name: Create and start emulator + - name: Create and start emulator, run instrumentation tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 33 target: google_apis arch: x86_64 force-avd-creation: true - emulator-options: -no-window -no-boot-anim -gpu swiftshader_indirect + emulator-options: -no-window -no-boot-anim -noaudio -gpu swiftshader_indirect -no-snapshot -no-snapshot-save -wipe-data -accel on disable-animations: true emulator-boot-timeout: 180 + script: ./gradlew connectedDebugAndroidTest --stacktrace - - name: Build debug APK, run unit & instrumentation tests + - name: Build debug APK, run unit tests run: | ./gradlew clean assembleDebug \ testDebugUnitTest \ - connectedDebugAndroidTest \ --stacktrace diff --git a/.gitignore b/.gitignore index 639cdd5..df40795 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ local.properties +.idea/ +.gradle/ +.kotlin/ +internalDocs/ +gradle/local-properites.kts +out.txt \ No newline at end of file diff --git a/README.md b/README.md index f42c403..54af958 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ Back-end часть проекта доступна по ссылке: - [SmartCalendarServer](https://github.com/hse-project-Java-2025/server) +## Video + - AI-assist demonstration + https://github.com/user-attachments/assets/963f8ed1-c95e-4326-ae8b-bdfbd8c01e74 + - Main functionality demonstration + https://drive.google.com/file/d/1Zq_mHprqxPhZRkU3m5l3qvVPfQEbaJFy/view?usp=sharing + # Key Features ## 👤 User Authentication & Profile diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt index 717219a..87d971e 100644 --- a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -67,6 +67,7 @@ class AchievementsScreenTest { fun achievementsShowsStreak() { val statisticsViewModel = StatisticsViewModel() val listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) + listViewModel.loadDailyTasks() //нужно потестить каждый элемент:Planning everything - без заданий 0, // с заданием 5ч 5/10, c 24ч 24 часа composeTestRule.setContent { diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/GreetingScreenKtTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/GreetingScreenKtTest.kt index 187907d..4ee9a9d 100644 --- a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/GreetingScreenKtTest.kt +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/GreetingScreenKtTest.kt @@ -1,9 +1,19 @@ package org.hse.smartcalendar.ui.screens -import androidx.compose.ui.test.* +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.hse.smartcalendar.activity.MainActivity +import org.hse.smartcalendar.data.WorkManagerHolder +import org.hse.smartcalendar.ui.navigation.App +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.view.model.ListViewModel +import org.hse.smartcalendar.view.model.StatisticsManager +import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.view.model.TaskEditViewModel import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -12,11 +22,25 @@ import org.junit.runner.RunWith class GreetingFlowTest { @get:Rule - val composeTestRule = createAndroidComposeRule() + val composeTestRule = createAndroidComposeRule() @Test fun testLogin() { - composeTestRule.waitForIdle() + WorkManagerHolder.init(composeTestRule.activity) + val statisticsViewModel = StatisticsViewModel() + val listViewModel = ListViewModel(StatisticsManager(statisticsViewModel)) + val taskEditViewModel = TaskEditViewModel(listViewModel) + + composeTestRule.setContent { + SmartCalendarTheme { + App( + statisticsVM = statisticsViewModel, + listVM = listViewModel, + taskEditVM = taskEditViewModel + ) + } + } + composeTestRule.onNodeWithTag("authorizationButtonTest") .assertIsDisplayed() .performClick() @@ -27,13 +51,9 @@ class GreetingFlowTest { composeTestRule.onNodeWithTag("passwordField") .assertIsDisplayed() .performTextInput("superpassword") - composeTestRule.onNodeWithTag("passwordField") - .performImeAction() composeTestRule.onNodeWithTag("loginSubmitButton") .assertIsDisplayed() .performClick() composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag("loginField") - .assertDoesNotExist() } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt index 53f29b3..b0bdc5b 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt @@ -12,6 +12,15 @@ class DailySchedule (val date : LocalDate = Clock.System.now() { private var dailyTasksList: LinkedList = LinkedList() + fun tryAddTask(newTask : DailyTask) : Boolean { + val iterator = dailyTasksList.iterator() + iterator.forEach { task -> + if (task.isNestedTasks(newTask)) { + return false + } + } + return true + } fun addDailyTask(newTask : DailyTask) : Boolean { val iterator = dailyTasksList.iterator() iterator.forEach { task -> diff --git a/app/src/main/java/org/hse/smartcalendar/data/SuggestedTask.kt b/app/src/main/java/org/hse/smartcalendar/data/SuggestedTask.kt new file mode 100644 index 0000000..d9c7bae --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/data/SuggestedTask.kt @@ -0,0 +1,7 @@ +package org.hse.smartcalendar.data + +data class SuggestedTask ( + val task: DailyTask, + var status: TaskStatus = TaskStatus.PENDING){ + enum class TaskStatus { PENDING, ACCEPTED, DECLINED } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt index 51c04fe..f1f0c69 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt @@ -20,6 +20,7 @@ import java.time.OffsetDateTime object ApiClient { private const val SERVER_BASE_URL = "http://10.0.2.2:8080/" + //в случае работы с реальным телефоном адрес сервера это адрес сети wi-fi, где запущен docker var authToken: String? = null private val client = OkHttpClient.Builder() .addInterceptor(AuthInterceptor { authToken }) @@ -53,6 +54,9 @@ object ApiClient { val inviteApiService: InviteApiInterface by lazy { retrofit.create(InviteApiInterface::class.java) } + val chatGptApiService: ChatGptApiInterface by lazy { + retrofit.create(ChatGptApiInterface::class.java) + } } class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt index 5f86fff..0417f66 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt @@ -110,4 +110,18 @@ interface InviteApiInterface { @Path("eventId") eventId: UUID, @Body request: InviteRequest ): Response +} + +interface ChatGptApiInterface { + @POST("api/chatgpt/{userId}/generate/suggestions") + suspend fun generateSuggestions( + @Path("userId") userId: Long, + @Body request: ChatGptRequest + ): Response + @Multipart + @POST("api/chatgpt/{userId}/generate/suggestions/audio") + suspend fun generateSuggestionsFromAudio( + @Path("userId") userId: Long, + @Part file: MultipartBody.Part + ): Response } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt index c165de2..630d5b2 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt @@ -118,3 +118,4 @@ data class ChatTaskResponse( applyToState(newEnd, endTime) } } +data class ChatGptResponse(val events: List) \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/NetworkResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/NetworkResponse.kt index a22cfed..cbe97c8 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/NetworkResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/NetworkResponse.kt @@ -23,4 +23,12 @@ sealed class NetworkResponse { return Error("token is null") } } + fun mapSuccess(transform: (T) -> R): NetworkResponse { + return when (this) { + is NetworkResponse.Success -> NetworkResponse.Success(transform(this.data)) + is NetworkResponse.Error -> NetworkResponse.Error(this.message) + is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(this.exceptionMessage) + is NetworkResponse.Loading -> NetworkResponse.Loading + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt index 3ebced0..921aff2 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.utility.TimeUtils +data class ChatGptRequest(val query: String) data class InviteRequest(val loginOrEmail: String) data class LoginRequest( val username: String, diff --git a/app/src/main/java/org/hse/smartcalendar/repository/ChatGptRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/ChatGptRepository.kt new file mode 100644 index 0000000..009d9ff --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/repository/ChatGptRepository.kt @@ -0,0 +1,53 @@ +package org.hse.smartcalendar.repository + +import org.hse.smartcalendar.data.DailySchedule +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.network.ChatGptApiInterface +import org.hse.smartcalendar.network.ChatGptRequest +import org.hse.smartcalendar.network.NetworkResponse +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import org.hse.smartcalendar.network.AudioApiInterface +import org.hse.smartcalendar.network.ChatTaskResponse +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.RequestBody.Companion.asRequestBody +import org.hse.smartcalendar.network.ChatGptResponse +import org.hse.smartcalendar.store.StatisticsStore + + +class ChatGptRepository( + private val api: ChatGptApiInterface +) : BaseRepository() { + + val mainSchedule = User.getSchedule() + private val id = User.id + + suspend fun generateSuggestions(query: String): NetworkResponse> { + return withIdRequest { userId -> + api.generateSuggestions(userId, ChatGptRequest(query)) + }.mapSuccess { response -> + response.events.map { it.toDailyTask() } + } + } + suspend fun generateSuggestionsAudio(audioFile: File): NetworkResponse> { + return withIdRequest { userId -> + val requestFile = audioFile.asRequestBody("audio/*".toMediaType()) + val part = MultipartBody.Part.createFormData("file", audioFile.name, requestFile) + api.generateSuggestionsFromAudio(userId, part) + }.mapSuccess { response -> + response.events.map { it.toDailyTask() } + } + } + + + fun tryAdd(task: DailyTask): Boolean { + return mainSchedule.getOrCreateDailySchedule(task.getTaskDate()).tryAddTask(task) + } + + fun declineTask(task: DailyTask): Boolean { + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/AudioRecorderButton.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/AudioRecorderButton.kt index f172d80..758de6c 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/elements/AudioRecorderButton.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/AudioRecorderButton.kt @@ -3,6 +3,8 @@ package org.hse.smartcalendar.ui.elements import android.Manifest import androidx.compose.foundation.layout.size import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -29,13 +31,16 @@ import java.io.File fun AudioRecorderButton( modifier: Modifier = Modifier, audioFile: MutableState, - onStop: () -> Unit + onStop: () -> Unit, + buttonColors: ButtonColors = ButtonDefaults.buttonColors(), + iconColor: Color = Color.White ) { val recordingState = rememberSaveable { mutableStateOf(RecordState.IDLE) } val context = LocalContext.current val audioPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO) Button( + colors = buttonColors, modifier = modifier, onClick = { when { @@ -65,7 +70,7 @@ fun AudioRecorderButton( Icon( painter = painterResource(R.drawable.mic_asset), contentDescription = "Recording", - tint = Color.White, + tint = iconColor, modifier = Modifier.size(24.dp) ) @@ -73,7 +78,7 @@ fun AudioRecorderButton( Icon( painter = painterResource(R.drawable.stop_asset), contentDescription = "Recording", - tint = Color.White, + tint = iconColor, modifier = Modifier.size(24.dp) ) diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt index 6b3d666..4cb1968 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt @@ -9,6 +9,52 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +val BotMessageSquare: ImageVector + get() { + if (_BotMessageSquare != null) return _BotMessageSquare!! + + _BotMessageSquare = ImageVector.Builder( + name = "BotMessageSquare", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(12f, 6f) + verticalLineTo(2f) + horizontalLineTo(8f) + moveToRelative(0f, 16f) + lineToRelative(-4f, 4f) + verticalLineTo(8f) + arcToRelative(2f, 2f, 0f, false, true, 2f, -2f) + horizontalLineToRelative(12f) + arcToRelative(2f, 2f, 0f, false, true, 2f, 2f) + verticalLineToRelative(8f) + arcToRelative(2f, 2f, 0f, false, true, -2f, 2f) + close() + moveToRelative(-6f, -6f) + horizontalLineToRelative(2f) + moveToRelative(5f, -1f) + verticalLineToRelative(2f) + moveToRelative(6f, -2f) + verticalLineToRelative(2f) + moveToRelative(5f, -1f) + horizontalLineToRelative(2f) + } + }.build() + + return _BotMessageSquare!! + } + +private var _BotMessageSquare: ImageVector? = null + + val People: ImageVector get() { if (_People != null) return _People!! diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt index 6be3ecb..1a97061 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt @@ -1,6 +1,7 @@ +package org.hse.smartcalendar.ui.elements + import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DismissDirection import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi @@ -21,23 +22,24 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.data.SuggestedTask import org.hse.smartcalendar.ui.task.DailyTaskCard import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import java.util.UUID @OptIn(ExperimentalMaterialApi::class) @Composable -fun InviteItem( - invite: Invite, +fun TaskItemBase( + task: DailyTask, + expandedState: MutableState = remember { mutableStateOf(false) }, onAccept: () -> Boolean, - onDecline: () -> Unit + onDecline: () -> Unit, + expandable: Boolean = true, + statusContent: @Composable (() -> Unit)? = null ) { - var expanded by remember { mutableStateOf(false) } val dismissState = rememberDismissState { dismissValue -> when (dismissValue) { - DismissValue.DismissedToEnd -> { - onAccept() - } + DismissValue.DismissedToEnd -> onAccept() DismissValue.DismissedToStart -> { onDecline() true @@ -80,63 +82,59 @@ fun InviteItem( .fillMaxWidth() .padding(vertical = 4.dp) ) { - Card( - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - elevation = CardDefaults.cardElevation(2.dp) - ) { - Row( - Modifier + DailyTaskCard( + task = task, + onCompletionChange = {}, + onLongPressAction = {}, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + ) + statusContent?.let { + Box( + modifier = Modifier .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = 16.dp, vertical = 4.dp), + contentAlignment = Alignment.CenterEnd ) { - Column { - Text(invite.inviterName, style = MaterialTheme.typography.labelMedium) - Spacer(Modifier.height(4.dp)) - Text(invite.task.getDailyTaskTitle(), style = MaterialTheme.typography.bodyLarge) - } - Column(horizontalAlignment = Alignment.End) { - Text( - invite.task.getDailyTaskArrangementString(), - style = MaterialTheme.typography.bodySmall - ) - Spacer(Modifier.height(4.dp)) - Text( - "Date: ${invite.task.getTaskDate()}", - style = MaterialTheme.typography.bodySmall - ) - } + it() } } + if (expandable){ Row( - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.End ) { - TextButton(onClick = { expanded = !expanded }) { - Text(if (expanded) "Hide Details" else "Show Details") - } + TextButton(onClick = { expandedState.value = !expandedState.value }) { + Text(if (expandedState.value) "Hide Details" else "Show Details") + } } - if (expanded) { + } + if (expandedState.value) { DailyTaskCard( - task = invite.task, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + task = task, onCompletionChange = {}, - onLongPressAction = {} + onLongPressAction = {}, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) ) } } } ) } + +@Composable +fun InviteItem( + invite: Invite, + onAccept: () -> Boolean, + onDecline: () -> Unit +) { + TaskItemBase( + task = invite.task, + onAccept = onAccept, + onDecline = onDecline, + ) +} + + @Preview(showBackground = true) @Composable fun InviteItemPreview() { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/SuggestedTaskItem.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/SuggestedTaskItem.kt new file mode 100644 index 0000000..b06203a --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/SuggestedTaskItem.kt @@ -0,0 +1,79 @@ +package org.hse.smartcalendar.ui.elements + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.data.SuggestedTask +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import java.util.UUID + +@Composable +fun SuggestedTaskItem( + suggestedTask: SuggestedTask, + onAccept: () -> Boolean, + onDecline: () -> Unit, +) { + TaskItemBase( + task = suggestedTask.task, + onAccept = onAccept, + onDecline = onDecline, + expandable = false, + statusContent = { + when (suggestedTask.status) { + SuggestedTask.TaskStatus.PENDING -> Icon(Icons.Default.Close, contentDescription = "Pending", tint = Color.Gray) + SuggestedTask.TaskStatus.ACCEPTED -> Icon(Icons.Default.Check, contentDescription = "Accepted", tint = Color.Green) + SuggestedTask.TaskStatus.DECLINED -> Icon(Icons.Default.Close, contentDescription = "Declined", tint = Color.Red) + } + } + ) +} + +@Preview(showBackground = true) +@Composable +fun SuggestedTaskItemPreview() { + SmartCalendarTheme { + val task = DailyTask.fromTimeAndType( + type = DailyTaskType.WORK, + start = LocalTime(4, 0), + end = LocalTime(5, 0), + date = DailyTask.defaultDate + ) + val suggestedTaskPending = SuggestedTask( + task = task, + status = SuggestedTask.TaskStatus.PENDING + ) + + val suggestedTaskAccepted = SuggestedTask( + task = task, + status = SuggestedTask.TaskStatus.ACCEPTED + ) + + Surface(modifier = Modifier.padding(16.dp)) { + Column() { + SuggestedTaskItem( + suggestedTask = suggestedTaskPending, + onAccept = { true }, + onDecline = {} + ) + SuggestedTaskItem( + suggestedTask = suggestedTaskAccepted, + onAccept = { true }, + onDecline = {} + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/UserMessageBox.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/UserMessageBox.kt new file mode 100644 index 0000000..6e58c2a --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/UserMessageBox.kt @@ -0,0 +1,37 @@ +package org.hse.smartcalendar.ui.elements + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + + +@Composable +fun UserMessageBox( + text: String, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(8.dp) + .background( + color = Color(0xFFEDF2FA), + shape = RoundedCornerShape(12.dp) + ) + .padding(12.dp) + ) { + Text( + text = text, + color = Color.Black, + style = MaterialTheme.typography.bodyLarge + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 43687e4..4203315 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -25,6 +25,7 @@ import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.screens.AchievementsScreen import org.hse.smartcalendar.ui.screens.ChangeLogin import org.hse.smartcalendar.ui.screens.ChangePassword +import org.hse.smartcalendar.ui.screens.ChatGptScreen import org.hse.smartcalendar.ui.screens.GreetingScreen import org.hse.smartcalendar.ui.screens.InvitesScreen import org.hse.smartcalendar.ui.screens.LoadingScreen @@ -35,6 +36,7 @@ import org.hse.smartcalendar.ui.task.TaskEditWindow import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation +import org.hse.smartcalendar.view.model.ChatGptViewModel import org.hse.smartcalendar.view.model.InvitesViewModel import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel @@ -49,6 +51,7 @@ fun App( listVM: ListViewModel, taskEditVM: TaskEditViewModel ) { + val chatGptViewModel: ChatGptViewModel = viewModel () val invitesModel: InvitesViewModel = viewModel() val authModel: AuthViewModel = viewModel() val navigation = rememberNavigation() @@ -81,16 +84,16 @@ fun App( }, drawerState = navigationDrawerState, gesturesEnabled = !isExpandedScreen - ){NestedNavigator(navigation, authModel,openDrawer,statisticsVM, listVM, taskEditVM, invitesModel ) + ){NestedNavigator(navigation, authModel,openDrawer,statisticsVM, listVM, taskEditVM, invitesModel, chatGptViewModel) } } else { - NestedNavigator(navigation, authModel,openDrawer, statisticsVM, listVM, taskEditVM, invitesModel ) + NestedNavigator(navigation, authModel,openDrawer, statisticsVM, listVM, taskEditVM, invitesModel, chatGptViewModel) } } @Composable fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: ()-> Unit, statisticsModel: StatisticsViewModel, listModel: ListViewModel, editModel: TaskEditViewModel, - invitesModel: InvitesViewModel){ + invitesModel: InvitesViewModel, chatGptViewModel: ChatGptViewModel){ val reminderModel: ReminderViewModel = viewModel(factory = ReminderViewModelFactory( LocalContext.current.applicationContext as Application )) @@ -164,6 +167,9 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: navController = navigation.navController ) } + composable(route = Screens.AI_ASSISTANT.route) { + ChatGptScreen(navigation, openDrawer, chatGptViewModel, listViewModel = listModel) + } } } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt index 91f11eb..b1c0c60 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview +import org.hse.smartcalendar.ui.elements.BotMessageSquare import org.hse.smartcalendar.ui.elements.Calendar_add_on import org.hse.smartcalendar.ui.elements.Calendar_today import org.hse.smartcalendar.ui.elements.Event_upcoming @@ -46,6 +47,8 @@ fun AppDrawer( currentRoute = currentRoute, navigation = navigation, closeDrawer = closeDrawer) AppNavigationDrawerItem(label = "Shared events", icon = Event_upcoming, destination = Screens.SHARED_EVENTS, currentRoute = currentRoute, navigation = navigation, closeDrawer = closeDrawer) + AppNavigationDrawerItem(label = "Ai Assistant", icon = BotMessageSquare, destination = Screens.AI_ASSISTANT, + currentRoute = currentRoute, navigation = navigation, closeDrawer = closeDrawer) } } @Composable diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/ChatGptScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/ChatGptScreen.kt new file mode 100644 index 0000000..6bfa088 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/ChatGptScreen.kt @@ -0,0 +1,212 @@ +package org.hse.smartcalendar.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.hse.smartcalendar.data.SuggestedTask +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.ui.elements.AudioRecorderButton +import org.hse.smartcalendar.ui.elements.SuggestedTaskItem +import org.hse.smartcalendar.ui.elements.UserMessageBox +import org.hse.smartcalendar.ui.navigation.TopButton +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.utility.Navigation +import org.hse.smartcalendar.utility.rememberNavigation +import org.hse.smartcalendar.view.model.ChatGptViewModel +import org.hse.smartcalendar.view.model.ListViewModel +import org.hse.smartcalendar.view.model.StatisticsManager +import org.hse.smartcalendar.view.model.StatisticsViewModel +import java.io.File + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatGptScreen( + navigation: Navigation, + openMenu: () -> Unit, + viewModel: ChatGptViewModel, + listViewModel: ListViewModel, +) { + val audioFile: MutableState = rememberSaveable { mutableStateOf(null) } + val onRecordStop: () -> Unit = {audioFile.value?.let { file -> + viewModel.fetchSuggestionsAudio(file) + }} + val chatHistory = viewModel.chatHistory + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + var query by remember { mutableStateOf("") } + val chatResult by viewModel.chatResult.observeAsState() + var showLoading by remember { mutableStateOf(false) } + + LaunchedEffect(chatResult) { + if (chatResult is NetworkResponse.Loading) { + delay(500) + if (chatResult is NetworkResponse.Loading) { + showLoading = true + } + } else { + showLoading = false + } + } + Scaffold( + topBar = { TopButton(openMenu, navigation, "Ai assistant") }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + Button(onClick = { viewModel.clearHistory() }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = Color.DarkGray + )) { + Text("Clear dialogue") + } + } + LazyColumn(modifier = Modifier.weight(1f)) { + itemsIndexed(chatHistory) {queryIndex, (question, tasks) -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + UserMessageBox("Q: $question") + tasks.forEachIndexed { taskIndex, suggestedTask -> + if (suggestedTask.status!= SuggestedTask.TaskStatus.DECLINED) { + SuggestedTaskItem( + suggestedTask = suggestedTask, + onAccept = { + if (suggestedTask.status == SuggestedTask.TaskStatus.PENDING) { + val notNested = viewModel.acceptTask(suggestedTask) + scope.launch { + if (notNested) { + listViewModel.addDailyTask(suggestedTask.task) + snackbarHostState.showSnackbar("Task accepted") + } else { + snackbarHostState.showSnackbar("Time conflict with existing task") + } + } + notNested + } else false + }, + onDecline = { + if (suggestedTask.status == SuggestedTask.TaskStatus.PENDING) { + viewModel.declineTask(suggestedTask) + scope.launch { + snackbarHostState.showSnackbar("Task declined") + } + } + } + ) + } + } + Spacer(modifier = Modifier.padding(8.dp)) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.Center + ) { + AudioRecorderButton( + modifier = Modifier, + audioFile = audioFile, + onStop = onRecordStop, + buttonColors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = Color.DarkGray + ), + iconColor = Color.DarkGray + ) + } + Row { + TextField( + value = query, + onValueChange = { query = it }, + modifier = Modifier.weight(1f), + placeholder = { Text("Write question") }, + colors = OutlinedTextFieldDefaults.colors( unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer ) + ) + Button(shape = RectangleShape, + onClick = { + if (query.isNotBlank()) { + viewModel.fetchSuggestions(query) + query = "" + }}, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = Color.DarkGray + )) + { + Text("Send") + } + } + + if (showLoading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) + } + } + } +} +@Preview(showBackground = true) +@Composable +fun ChatGptScreenPreview() { + SmartCalendarTheme { + val statisticsViewModel: StatisticsViewModel = viewModel () + val listViewModel: ListViewModel = ListViewModel(StatisticsManager(statisticsViewModel)) + val chatGptViewModel: ChatGptViewModel = viewModel() + ChatGptScreen( + navigation = rememberNavigation(), + viewModel = chatGptViewModel, + openMenu = {}, + listViewModel = listViewModel + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt index b1939dc..23aee5a 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt @@ -1,6 +1,5 @@ package org.hse.smartcalendar.ui.screens -import InviteItem import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -25,6 +24,7 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.Invite import org.hse.smartcalendar.data.store.InvitesStore +import org.hse.smartcalendar.ui.elements.InviteItem import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt index 3960c3b..bf9da8c 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt @@ -62,7 +62,7 @@ fun BottomSheet( isBottomSheetVisible: MutableState, sheetState: SheetState, onDismiss: () -> Unit, - onRecordStop: () -> ChatTaskResponse? = { null }, + onRecordStop: () -> Unit = { null }, audioFile: MutableState, taskTitle: MutableState, taskType: MutableState, @@ -83,12 +83,25 @@ fun BottomSheet( val fstFiledHasFormatError = rememberSaveable { mutableStateOf(false) } val sndFiledHasFormatError = rememberSaveable { mutableStateOf(false) } val actionResult by viewModel.actionResult.observeAsState() + val audioResponse by viewModel.audioResponse.observeAsState() val isErrorInRecorder = rememberSaveable { mutableStateOf(false) } LaunchedEffect(isBottomSheetVisible.value) { if (isBottomSheetVisible.value){ isErrorInRecorder.value = false } } + LaunchedEffect(audioResponse) { + audioResponse?.let { task -> + task.applyToUiStates( + taskTitle = taskTitle, + taskDescription = taskDescription, + taskType = taskType, + isErrorInRecorder = isErrorInRecorder, + startTime = startTime, + endTime = endTime + ) + } + } if (isBottomSheetVisible.value) { ModalBottomSheet( onDismissRequest = { @@ -199,20 +212,7 @@ fun BottomSheet( AudioRecorderButton( modifier = Modifier.weight(1.0f), audioFile = audioFile, - onStop = { - val task = onRecordStop() - if (task != null) { - task.applyToUiStates( - taskTitle = taskTitle, - taskDescription = taskDescription, - taskType = taskType, - isErrorInRecorder = isErrorInRecorder, - startTime = startTime, - endTime = endTime) - } else { - isErrorInRecorder.value = true - } - } + onStop = onRecordStop ) Spacer(modifier = Modifier.padding(12.dp)) Button( diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt index 4d7d77a..6b6089f 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt @@ -106,6 +106,9 @@ fun DailyTasksList( showLoading = false } } + LaunchedEffect(Unit) { + viewModel.loadDailyTasks() + } Scaffold( topBar = { TopButton( @@ -144,10 +147,10 @@ fun DailyTasksList( .invokeOnCompletion { isBottomSheetVisible.value = false } }, onRecordStop = { - viewModel.sendAudio( - audioFile = audioFile, - description = ListViewModel.AudioDescription.CONVERT_AUDIO, - ) + audioFile.value?.let { file -> + viewModel.sendAudio(file) + } + }, audioFile = audioFile, taskTitle = taskTitle, diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ChatGptViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ChatGptViewModel.kt new file mode 100644 index 0000000..7ada7b8 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ChatGptViewModel.kt @@ -0,0 +1,68 @@ +package org.hse.smartcalendar.view.model + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.SuggestedTask +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.repository.ChatGptRepository +import java.io.File + +class ChatGptViewModel() : ViewModel() { + private val repository: ChatGptRepository = ChatGptRepository(ApiClient.chatGptApiService) + + private val _chatHistory = mutableStateListOf>>() + val chatHistory: List>> get() = _chatHistory + private val _suggestions = MutableLiveData>>() + val chatResult: LiveData>> = _suggestions + + fun fetchSuggestions(query: String) { + viewModelScope.launch { + _suggestions.value = NetworkResponse.Loading + val tasks = repository.generateSuggestions(query) + _suggestions.value = tasks + if (tasks is NetworkResponse.Success) { + val suggested = tasks.data.map { SuggestedTask(it) } + _chatHistory.add(query to suggested) + } + } + } + fun fetchSuggestionsAudio(audioFile: File) { + viewModelScope.launch { + val result = repository.generateSuggestionsAudio(audioFile) + if (result is NetworkResponse.Success) { + val suggested = result.data.map { SuggestedTask(it) } + _chatHistory.add("Audio query" to suggested) + } + } + } + + fun acceptTask(task: SuggestedTask) : Boolean { + val success = repository.tryAdd(task.task) + if (success) { + task.status = SuggestedTask.TaskStatus.ACCEPTED + return true + } + return false + } + + fun declineTask(task: SuggestedTask) { + task.status = SuggestedTask.TaskStatus.DECLINED +// if (queryIndex in _chatHistory.indices) { +// val (q, tasks) = _chatHistory[queryIndex] +// val task = tasks[taskIndex] +// if (repository.declineTask(task)) { +// val newTasks = tasks.toMutableList().apply { removeAt(taskIndex) } +// _chatHistory[queryIndex] = q to newTasks +// } +// } + } + fun clearHistory() { + _chatHistory.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt index 20d6f95..b4eef64 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt @@ -5,23 +5,22 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.Invite import org.hse.smartcalendar.data.InviteAction -import org.hse.smartcalendar.data.MainSchedule import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.data.store.InvitesStore import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.repository.InviteRepository import org.hse.smartcalendar.work.InviteApiWorker -import kotlinx.serialization.json.Json -import org.hse.smartcalendar.data.WorkManagerHolder -import androidx.work.ExistingWorkPolicy class InvitesViewModel : ViewModel() { var _initResult = MutableLiveData>>() @@ -41,7 +40,7 @@ class InvitesViewModel : ViewModel() { updateUI() } _initResult.value = response - kotlinx.coroutines.delay(10000) + kotlinx.coroutines.delay(100000) } } } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index 80d446f..a729bde 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -144,6 +144,9 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie } } class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewModel(statisticsManager) { + private val _audioResponse = MutableLiveData() + val audioResponse: LiveData = _audioResponse + private val workManager = WorkManagerHolder.getInstance() private val audioRepo = AudioRepository(ApiClient.audioApiService) fun getInviteesRequestList(task: DailyTask): List{ @@ -183,23 +186,17 @@ class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewMode ) } } - fun sendAudio( - audioFile: MutableState, - description: AudioDescription, - ): ChatTaskResponse? { - val file = audioFile.value - var response: ChatTaskResponse? = null - if (file!=null) { - viewModelScope.launch { - _actionResult.value = NetworkResponse.Loading - val result = audioRepo.sendAudioGetResponse(file) - if (result is NetworkResponse.Success){ - response = result.data - } - _actionResult.value = result + fun sendAudio(audioFile: File) { + viewModelScope.launch { + _actionResult.value = NetworkResponse.Loading + val result = audioRepo.sendAudioGetResponse(audioFile) + if (result is NetworkResponse.Success) { + _audioResponse.value = result.data + } else { + _audioResponse.value = null } + _actionResult.value = result } - return response } class NestedTask(val nestedTask: DailyTask) : diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt index d4bde24..8ff6c7a 100644 --- a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt @@ -80,6 +80,7 @@ class StatisticsTest { fun init(){ statisticsViewModel = StatisticsViewModel() listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) + listViewModel.loadDailyTasks() setTasks() } @AfterEach//Clear, call after EACH test