From 65c8513950b96aa373a1cbdf1e3973183390e5b5 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Mon, 23 Mar 2026 11:03:53 +0100 Subject: [PATCH 1/7] Implement changes --- .../canvasapi2/apis/JourneyAssistAPI.kt | 2 + .../aiassistant/chat/AiAssistChatScreen.kt | 3 +- .../aiassistant/common/AiAssistRepository.kt | 2 +- .../common/composable/AiAssistFeedback.kt | 4 +- .../composable/AiAssistResponseTextBlock.kt | 22 +---- .../common/composable/AiAssistScaffold.kt | 72 +++++++--------- .../composable/AiAssistSuggestionTextBlock.kt | 4 +- .../common/composable/AiAssistTextArea.kt | 2 +- .../common/composable/AiAssistToolbar.kt | 83 +++++++++---------- .../flashcard/composable/AiAssistFlashcard.kt | 2 +- .../aiassistant/main/AiAssistMainScreen.kt | 2 +- .../features/home/HomeBottomNavigationBar.kt | 64 ++++---------- .../ModuleItemSequenceScreen.kt | 7 +- libs/horizon/src/main/res/values/strings.xml | 18 ++-- 14 files changed, 113 insertions(+), 174 deletions(-) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/JourneyAssistAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/JourneyAssistAPI.kt index 0e7db49635..92299b3e5a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/JourneyAssistAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/JourneyAssistAPI.kt @@ -4,10 +4,12 @@ import com.instructure.canvasapi2.models.journey.JourneyAssistRequestBody import com.instructure.canvasapi2.models.journey.JourneyAssistResponse import retrofit2.http.Body import retrofit2.http.POST +import retrofit2.http.Query interface JourneyAssistAPI { @POST("assist") suspend fun answerPrompt( @Body body: JourneyAssistRequestBody, + @Query("studyToolsOnly") studyToolsOnly: Boolean ): JourneyAssistResponse } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt index bdedd9460e..8e4211e65a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt @@ -43,7 +43,6 @@ import com.instructure.canvasapi2.models.journey.JourneyAssistRole import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.aiassistant.common.composable.AiAssistMessage -import com.instructure.horizon.features.aiassistant.common.composable.AiAssistMessage import com.instructure.horizon.features.aiassistant.common.composable.AiAssistScaffold import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessage import com.instructure.horizon.features.aiassistant.navigation.AiAssistRoute @@ -129,7 +128,7 @@ fun AiAssistChatScreen( modifier = Modifier .fillMaxWidth() .semantics { - contentDescription = context.getString(R.string.a11y_igniteAiLoadingContentDescription) + contentDescription = context.getString(R.string.a11y_studyToolsLoadingContentDescription) } .focusRequester(loadingFocusRequester) .focusable() diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepository.kt index f69136e619..05eb2aac0d 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepository.kt @@ -22,7 +22,7 @@ class AiAssistRepository @Inject constructor( state: JourneyAssistState ): AiAssistResponse { val requestBody = JourneyAssistRequestBody(prompt, history, state) - val response = journeyAssistAPI.answerPrompt(requestBody) + val response = journeyAssistAPI.answerPrompt(requestBody, true) val message = AiAssistMessage( text = response.response.orEmpty(), role = JourneyAssistRole.Assistant, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistFeedback.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistFeedback.kt index f510b336f5..f145e39d96 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistFeedback.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistFeedback.kt @@ -71,7 +71,7 @@ private fun AiAssistPositiveFeedbackIcon( } else { painterResource(R.drawable.thumb_up) }, - contentDescription = stringResource(R.string.a11y_igniteAIPositiveFeedback), + contentDescription = stringResource(R.string.a11y_studyToolsPositiveFeedback), tint = HorizonColors.Text.surfaceColored(), modifier = Modifier.clickable { onClick() }, ) @@ -88,7 +88,7 @@ private fun AiAssistNegativeFeedbackIcon( } else { painterResource(R.drawable.thumb_down) }, - contentDescription = stringResource(R.string.a11y_igniteAINegativeFeedback), + contentDescription = stringResource(R.string.a11y_studyToolsNegativeFeedback), tint = HorizonColors.Text.surfaceColored(), modifier = Modifier.clickable { onClick() }, ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistResponseTextBlock.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistResponseTextBlock.kt index d3e714cc14..5905477668 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistResponseTextBlock.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistResponseTextBlock.kt @@ -103,29 +103,13 @@ fun AiAssistResponseTextBlock( HorizonSpace(SpaceSize.SPACE_8) FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { chips.forEach { chip -> AiAssistSuggestionTextBlock( text = chip.label, - onClick = chip.onClick - ) - } - } - } - - if (chips.isNotEmpty()) { - HorizonSpace(SpaceSize.SPACE_8) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - chips.forEach { chip -> - AiAssistSuggestionTextBlock( - text = chip.label, - onClick = chip.onClick + onClick = chip.onClick, ) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt index 9104fa1819..db3c80cf29 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt @@ -16,6 +16,7 @@ */ package com.instructure.horizon.features.aiassistant.common.composable +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -23,14 +24,12 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.instructure.horizon.horizonui.foundation.HorizonColors -import com.instructure.horizon.horizonui.foundation.HorizonSpace -import com.instructure.horizon.horizonui.foundation.SpaceSize -import com.instructure.horizon.horizonui.molecules.HorizonDivider +import com.instructure.horizon.horizonui.organisms.scaffolds.EdgeToEdgeScaffold @Composable fun AiAssistScaffold( @@ -43,46 +42,39 @@ fun AiAssistScaffold( modifier: Modifier = Modifier, content: @Composable (Modifier) -> Unit, ) { - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - AiAssistToolbar( - onDismissPressed = { - onClearChatHistory() - onDismiss() - }, - onBackPressed = if (navController.previousBackStackEntry != null) { - { + EdgeToEdgeScaffold( + statusBarColor = HorizonColors.Surface.aiGradientStart(), + statusBarAlpha = 0f, + navigationBarColor = HorizonColors.Surface.aiGradientEnd(), + navigationBarAlpha = 0f, + containerColor = Color.Transparent, + modifier = modifier.background(HorizonColors.Surface.aiGradient()) + ) { contentPadding -> + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(contentPadding) + ) { + AiAssistToolbar( + onDismissPressed = { onClearChatHistory() - navController.popBackStack() - } - } else { - null - }, - modifier = Modifier - .padding(horizontal = 16.dp) - ) - - HorizonDivider(color = HorizonColors.Surface.pagePrimary()) - HorizonSpace(SpaceSize.SPACE_16) - - content(Modifier.weight(1f).padding(horizontal = 24.dp)) - - if (inputTextValue != null && onInputTextChanged != null && onInputTextSubmitted != null) { - val focusManager = LocalFocusManager.current - AiAssistInput( - value = inputTextValue, - onValueChange = { onInputTextChanged(it) }, - onSubmitPressed = { - focusManager.clearFocus() - onInputTextSubmitted() + onDismiss() + }, + onBackPressed = if (navController.previousBackStackEntry != null) { + { + onClearChatHistory() + navController.popBackStack() + } + } else { + null }, modifier = Modifier - .padding(horizontal = 24.dp) - .padding(top = 8.dp, bottom = 24.dp) ) + + content(Modifier + .weight(1f) + .padding(horizontal = 24.dp)) } } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistSuggestionTextBlock.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistSuggestionTextBlock.kt index c1f7f18ce7..7fd907ca03 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistSuggestionTextBlock.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistSuggestionTextBlock.kt @@ -19,6 +19,7 @@ package com.instructure.horizon.features.aiassistant.common.composable import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -37,10 +38,11 @@ fun AiAssistSuggestionTextBlock( ) { Box( modifier = modifier + .fillMaxWidth() .border( 1.dp, HorizonColors.LineAndBorder.lineStroke(), - HorizonCornerRadius.level4 + HorizonCornerRadius.level1_5 ) .padding(16.dp) .clickable { onClick() } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistTextArea.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistTextArea.kt index 1cbe110747..a3f57d23f8 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistTextArea.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistTextArea.kt @@ -69,7 +69,7 @@ private fun TextAreaBox( innerTextField() if (value.text.isEmpty()) { Text( - text = stringResource(R.string.igniteAIEnterAPromptLabel), + text = stringResource(R.string.studyToolsEnterAPromptLabel), style = HorizonTypography.p1, color = HorizonColors.Text.timestamp(), ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt index dc45c50ad6..8c5e7e81a0 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt @@ -17,16 +17,14 @@ package com.instructure.horizon.features.aiassistant.common.composable import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -49,53 +47,48 @@ fun AiAssistToolbar( modifier: Modifier = Modifier, onBackPressed: (() -> Unit)? = null, ) { - CenterAlignedTopAppBar( - colors = TopAppBarDefaults.centerAlignedTopAppBarColors().copy( - containerColor = Color.Transparent - ), - modifier = modifier, - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(R.drawable.ai_filled), - contentDescription = null, - tint = HorizonColors.Icon.surfaceColored(), - modifier = Modifier - .size(24.dp) - ) - - HorizonSpace(SpaceSize.SPACE_4) - - Text( - text = stringResource(R.string.igniteAIToolbarTitle), - style = HorizonTypography.h3, - color = HorizonColors.Text.surfaceColored() - ) - } - }, - navigationIcon = { - if (onBackPressed != null) { - IconButton( - iconRes = R.drawable.arrow_back, - contentDescription = stringResource(R.string.a11yNavigateBack), - size = IconButtonSize.SMALL, - color = IconButtonColor.WhiteOutline, - onClick = onBackPressed, - ) - } - }, - actions = { + Row( + modifier = modifier.padding(24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (onBackPressed != null) { IconButton( - iconRes = R.drawable.close, - contentDescription = stringResource(R.string.igniteAIDismissContentDescription), + iconRes = R.drawable.arrow_back, + contentDescription = stringResource(R.string.a11yNavigateBack), size = IconButtonSize.SMALL, color = IconButtonColor.WhiteOutline, - onClick = onDismissPressed, + onClick = onBackPressed, ) + HorizonSpace(SpaceSize.SPACE_4) } - ) + + Icon( + painter = painterResource(R.drawable.ai_filled), + contentDescription = null, + tint = HorizonColors.Icon.surfaceColored(), + modifier = Modifier + .size(24.dp) + ) + + HorizonSpace(SpaceSize.SPACE_4) + + Text( + text = stringResource(R.string.studyToolsToolbarTitle), + style = HorizonTypography.h3, + color = HorizonColors.Text.surfaceColored(), + modifier = modifier.weight(1f) + ) + + HorizonSpace(SpaceSize.SPACE_4) + + IconButton( + iconRes = R.drawable.close, + contentDescription = stringResource(R.string.studyToolsDismissContentDescription), + size = IconButtonSize.SMALL, + color = IconButtonColor.WhiteOutline, + onClick = onDismissPressed, + ) + } } @Composable diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/composable/AiAssistFlashcard.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/composable/AiAssistFlashcard.kt index a33fb4f288..668cb24e05 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/composable/AiAssistFlashcard.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/composable/AiAssistFlashcard.kt @@ -89,7 +89,7 @@ fun AiAssistFlashcard( } .clearAndSetSemantics { onClick( - label = context.getString(R.string.a11y_aiAssistFlashcardClickActionLabel), + label = context.getString(R.string.a11y_studyToolsFlashcardClickActionLabel), action = { onClick(); true } ) contentDescription = cardContentDescription diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt index ba5386346c..440e040b7a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt @@ -130,7 +130,7 @@ fun AiAssistMainScreen( modifier = Modifier .fillMaxWidth() .semantics { - contentDescription = context.getString(R.string.a11y_igniteAiLoadingContentDescription) + contentDescription = context.getString(R.string.a11y_studyToolsLoadingContentDescription) } .focusRequester(loadingFocusRequester) .focusable() diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeBottomNavigationBar.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeBottomNavigationBar.kt index db98b13b6f..978b2e4375 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeBottomNavigationBar.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeBottomNavigationBar.kt @@ -22,33 +22,23 @@ import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.requiredSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.NavigationBar import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import com.instructure.horizon.R -import com.instructure.horizon.features.aiassistant.AiAssistantScreen import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonElevation -import com.instructure.horizon.horizonui.molecules.IconButton -import com.instructure.horizon.horizonui.molecules.IconButtonColor import com.instructure.horizon.horizonui.organisms.navelements.SelectableNavigationItem data class BottomNavItem( - val route: String?, + val route: String, @StringRes val label: Int, @DrawableRes val icon: Int, @DrawableRes val selectedIcon: Int, @@ -58,7 +48,6 @@ data class BottomNavItem( private val bottomNavItems = listOf( BottomNavItem(HomeNavigationRoute.Dashboard.route, R.string.bottomNav_home, R.drawable.home, R.drawable.home_filled), BottomNavItem(HomeNavigationRoute.Learn.route, R.string.bottomNav_learn, R.drawable.book_2, R.drawable.book_2_filled, true), - BottomNavItem(null, R.string.bottomNav_igniteAI, R.drawable.ai, R.drawable.ai_filled), BottomNavItem(HomeNavigationRoute.Skillspace.route, R.string.bottomNav_skillspace, R.drawable.hub, R.drawable.hub_filled), BottomNavItem( HomeNavigationRoute.Account.route, @@ -84,10 +73,6 @@ fun HomeBottomNavigationBar( buttonsEnabled: Boolean = true, modifier: Modifier = Modifier ) { - var isAiAssistVisible by rememberSaveable { mutableStateOf(false) } - if (isAiAssistVisible) { - AiAssistantScreen(navController) { isAiAssistVisible = false } - } val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination val visible = isBottomBarVisible(navController) @@ -103,43 +88,22 @@ fun HomeBottomNavigationBar( modifier = modifier ) { bottomNavItems.forEach { item -> - val selected = - currentDestination?.hierarchy?.any { it.route == item.route } == true - if (item.route == null) { - AiAssistantItem(item, buttonsEnabled, onClick = { - isAiAssistVisible = true - }) - } else { - SelectableNavigationItem(item, selected, buttonsEnabled, onClick = { - navController.navigate(item.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - - // Do not restore screen state when navigating to Dashboard screen - // Restore when navigating between other screens - restoreState = item.route != HomeNavigationRoute.Dashboard.route || - (item.route == HomeNavigationRoute.Dashboard.route && currentDestination?.route == HomeNavigationRoute.Dashboard.route) + val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true + SelectableNavigationItem(item, selected, buttonsEnabled, onClick = { + navController.navigate(item.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true } - }) - } + launchSingleTop = true + + // Do not restore screen state when navigating to Dashboard screen + // Restore when navigating between other screens + restoreState = item.route != HomeNavigationRoute.Dashboard.route || + (item.route == HomeNavigationRoute.Dashboard.route && currentDestination?.route == HomeNavigationRoute.Dashboard.route) + } + }) } } } } -} - -@Composable -fun RowScope.AiAssistantItem(item: BottomNavItem, enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { - IconButton( - modifier = modifier - .requiredSize(44.dp) - .weight(1f), - onClick = onClick, - contentDescription = stringResource(item.label), - iconRes = R.drawable.ai, - color = IconButtonColor.Ai, - enabled = enabled - ) } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt index 0da6b49cc5..1b4713b339 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt @@ -144,6 +144,8 @@ fun ModuleItemSequenceScreen(navController: NavHostController, uiState: ModuleIt showNextButton = uiState.currentPosition < uiState.items.size - 1, showPreviousButton = uiState.currentPosition > 0, showNotebookButton = uiState.currentItem?.moduleItemContent is ModuleItemContent.Page, + showAiAssistButton = (uiState.currentItem?.moduleItemContent is ModuleItemContent.File) + || (uiState.currentItem?.moduleItemContent is ModuleItemContent.Page), showAssignmentToolsButton = uiState.currentItem?.moduleItemContent is ModuleItemContent.Assignment, onNextClick = uiState.onNextClick, onPreviousClick = uiState.onPreviousClick, @@ -557,6 +559,7 @@ private fun ModuleItemSequenceBottomBar( showNextButton: Boolean, showPreviousButton: Boolean, showNotebookButton: Boolean, + showAiAssistButton: Boolean, showAssignmentToolsButton: Boolean, onNextClick: () -> Unit, onPreviousClick: () -> Unit, @@ -592,9 +595,9 @@ private fun ModuleItemSequenceBottomBar( .align(Alignment.Center), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally) ) { - IconButton( + if (showAiAssistButton) IconButton( iconRes = R.drawable.ai, - contentDescription = stringResource(R.string.a11y_openIgniteAI), + contentDescription = stringResource(R.string.a11y_openStudyTools), enabled = aiAssistEnabled, color = IconButtonColor.Ai, elevation = HorizonElevation.level4, diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index a5eca76fbf..d06d812585 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Learn Skillspace Account - IgniteAI + Study Tools %1$s\%% Resume learning Assignment @@ -199,10 +199,10 @@ Download file Open with File couldn’t be downloaded. - IgniteAI - Dismiss IgniteAI - Rate IgniteAI with positive feedback - Rate IgniteAI with negative feedback + Study tools + Dismiss Study tools + Rate Study tools with positive feedback + Rate Study tools with negative feedback Enter a prompt Submit prompt Check answer @@ -506,7 +506,7 @@ Unread Previous module item Previous module - Open IgniteAI + Open Study tools Open notebook Open more options Next module item @@ -515,9 +515,9 @@ Courses Programs File a ticket for a personal response from our support team. - Enter a prompt - Loading response - Flip the card + Enter a prompt + Loading response + Flip the card Failed to load courses All courses Not started From 7f33161c2dbd9b7b0b434c534a572cea9eee4630 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Mon, 23 Mar 2026 16:49:07 +0100 Subject: [PATCH 2/7] Implement ai information --- .../aiinformation/AiInformationScreen.kt | 289 ++++++++++++++++++ .../aiinformation/model/AiInformationData.kt | 63 ++++ .../AiInformationNutritionFactsScreen.kt | 228 ++++++++++++++ .../AiInformationPermissionLevelsScreen.kt | 237 ++++++++++++++ libs/horizon/src/main/res/values/strings.xml | 2 + 5 files changed, 819 insertions(+) create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/model/AiInformationData.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/nutritionfacts/AiInformationNutritionFactsScreen.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt new file mode 100644 index 0000000000..a5e0e838c2 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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 + * + * http://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 com.instructure.horizon.features.aiassistant.aiinformation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.features.aiassistant.aiinformation.model.AiInformationData +import com.instructure.horizon.features.aiassistant.aiinformation.model.AiInformationNutritionFactsData +import com.instructure.horizon.features.aiassistant.aiinformation.model.AiInformationPermissionLevelsData +import com.instructure.horizon.features.aiassistant.aiinformation.model.DataPermissionLevel +import com.instructure.horizon.features.aiassistant.aiinformation.model.NutritionFactBlock +import com.instructure.horizon.features.aiassistant.aiinformation.model.NutritionFactSegment +import com.instructure.horizon.features.aiassistant.aiinformation.nutritionfacts.AiInformationNutritionFactsScreen +import com.instructure.horizon.features.aiassistant.aiinformation.permissionlevels.AiInformationPermissionLevelsScreen +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.IconButton +import com.instructure.horizon.horizonui.molecules.IconButtonColor +import com.instructure.horizon.horizonui.molecules.IconButtonSize +import com.instructure.horizon.horizonui.molecules.TextLink +import com.instructure.horizon.horizonui.molecules.TextLinkColor +import com.instructure.horizon.horizonui.molecules.TextLinkSize + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AiInformationScreen( + data: AiInformationData, + onDismiss: () -> Unit, +) { + var showPermissionLevels by rememberSaveable { mutableStateOf(false) } + var showNutritionFacts by rememberSaveable { mutableStateOf(false) } + + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + containerColor = HorizonColors.Surface.pageSecondary(), + onDismissRequest = onDismiss, + sheetState = bottomSheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp) + .navigationBarsPadding() + ) { + AiInformationHeader(onDismiss = onDismiss) + HorizonSpace(SpaceSize.SPACE_16) + Text( + text = data.title, + style = HorizonTypography.h1, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_24) + Text( + text = data.permissionLevelText, + style = HorizonTypography.labelLargeBold, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = data.permissionLevel, + style = HorizonTypography.tag, + color = HorizonColors.PrimitivesViolet.violet40(), + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = data.description, + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + ) + HorizonSpace(SpaceSize.SPACE_16) + TextLink( + text = data.permissionLevelsModalTriggerText, + textLinkColor = TextLinkColor.Black, + textLinkSize = TextLinkSize.NORMAL, + onClick = { showPermissionLevels = true }, + ) + HorizonSpace(SpaceSize.SPACE_24) + Text( + text = data.modelNameText, + style = HorizonTypography.labelLargeBold, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = data.modelName, + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + ) + HorizonSpace(SpaceSize.SPACE_16) + TextLink( + text = data.nutritionFactsModalTriggerText, + textLinkColor = TextLinkColor.Black, + textLinkSize = TextLinkSize.NORMAL, + onClick = { showNutritionFacts = true }, + ) + HorizonSpace(SpaceSize.SPACE_32) + } + } + + if (showPermissionLevels) { + AiInformationPermissionLevelsScreen( + data = data.permissionLevelsData, + onDismiss = { showPermissionLevels = false }, + ) + } + + if (showNutritionFacts) { + AiInformationNutritionFactsScreen( + data = data.nutritionFactsData, + onDismiss = { showNutritionFacts = false }, + ) + } +} + +@Composable +internal fun AiInformationHeader( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.ai_filled), + contentDescription = null, + tint = HorizonColors.Surface.aiGradientStart(), + modifier = Modifier.size(20.dp), + ) + HorizonSpace(SpaceSize.SPACE_4) + Text( + text = stringResource(R.string.igniteAIToolbarTitle), + style = HorizonTypography.labelLargeBold, + color = HorizonColors.Surface.aiGradientStart(), + modifier = Modifier.weight(1f), + ) + IconButton( + iconRes = R.drawable.close, + contentDescription = stringResource(R.string.igniteAIDismissContentDescription), + size = IconButtonSize.SMALL, + color = IconButtonColor.Ghost, + onClick = onDismiss, + ) + } +} + +@Composable +@Preview(showBackground = true) +private fun AiInformationScreenPreview() { + ContextKeeper.appContext = LocalContext.current + AiInformationScreen( + data = previewAiInformationData(), + onDismiss = {}, + ) +} + +internal fun previewAiInformationData() = AiInformationData( + title = "Study Tools", + permissionLevelText = "Permission Level", + permissionLevel = "LEVEL 2", + description = "We utilise off-the-shelf AI models and customer data as input to provide AI-powered features. No data is used for training this model.", + permissionLevelsModalTriggerText = "View Permission Levels", + modelNameText = "Base Model", + modelName = "Claude 3.5 Haiku by Anthropic and Cohere multi-language v3", + nutritionFactsModalTriggerText = "View AI Nutrition Facts", + permissionLevelsData = AiInformationPermissionLevelsData( + title = "Data Permission Levels", + currentFeatureText = "Current Feature:", + currentFeature = "Study Tools", + closeButtonText = "Close", + levels = listOf( + DataPermissionLevel( + level = "LEVEL 1", + title = "Descriptive Analytics and Research", + description = "We leverage anonymised aggregate data for detailed analytics to inform model development and product improvements. No AI models are used at this level.", + ), + DataPermissionLevel( + level = "LEVEL 2", + title = "AI-Powered Features Without Data Training", + description = "We utilise off-the-shelf AI models and customer data as input to provide AI-powered features. No data is used for training this model.", + isHighlighted = true, + ), + DataPermissionLevel( + level = "LEVEL 3", + title = "AI Customization for Individual Institutions", + description = "We customise AI solutions tailored to the unique needs and resources of educational institutions.", + ), + DataPermissionLevel( + level = "LEVEL 4", + title = "Collaborative AI Consortium", + description = "We established a consortium with educational institutions that shares anonymised data, best practices, and research findings.", + ), + ), + ), + nutritionFactsData = AiInformationNutritionFactsData( + title = "Nutrition Facts", + featureName = "Study Tools", + closeButtonText = "Close", + blocks = listOf( + NutritionFactBlock( + blockTitle = "Model & Data", + segments = listOf( + NutritionFactSegment( + segmentTitle = "Base Model", + description = "The foundational AI on which further training and customizations are built.", + value = "Claude 3.5 Haiku by Anthropic and Cohere multi-language v3", + ), + NutritionFactSegment( + segmentTitle = "Trained with User Data", + description = "Indicates the AI model has been given customer data in order to improve its results.", + value = "No", + ), + NutritionFactSegment( + segmentTitle = "Data Shared with Model", + description = "Indicates which training or operational content was given to the model.", + value = "Course content", + ), + ), + ), + NutritionFactBlock( + blockTitle = "Privacy & Compliance", + segments = listOf( + NutritionFactSegment( + segmentTitle = "Data Retention", + description = "How long the model stores customer data.", + value = "No", + ), + NutritionFactSegment( + segmentTitle = "Data Logging", + description = "Recording the AI's performance for auditing, analysis, and improvement.", + value = "No", + ), + NutritionFactSegment( + segmentTitle = "Regions Supported", + description = "The locations where the AI model is officially available and supported.", + value = "Global", + ), + NutritionFactSegment( + segmentTitle = "PII", + description = "Sensitive data that can be used to identify an individual.", + value = "No", + ), + ), + ), + ), + ), +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/model/AiInformationData.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/model/AiInformationData.kt new file mode 100644 index 0000000000..fc16696233 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/model/AiInformationData.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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 + * + * http://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 com.instructure.horizon.features.aiassistant.aiinformation.model + +data class AiInformationData( + val title: String, + val permissionLevelText: String, + val permissionLevel: String, + val description: String, + val permissionLevelsModalTriggerText: String, + val modelNameText: String, + val modelName: String, + val nutritionFactsModalTriggerText: String, + val permissionLevelsData: AiInformationPermissionLevelsData, + val nutritionFactsData: AiInformationNutritionFactsData, +) + +data class AiInformationPermissionLevelsData( + val title: String, + val currentFeatureText: String, + val currentFeature: String, + val closeButtonText: String, + val levels: List, +) + +data class DataPermissionLevel( + val level: String, + val title: String, + val description: String, + val isHighlighted: Boolean = false, +) + +data class AiInformationNutritionFactsData( + val title: String, + val featureName: String, + val closeButtonText: String, + val blocks: List, +) + +data class NutritionFactBlock( + val blockTitle: String, + val segments: List, +) + +data class NutritionFactSegment( + val segmentTitle: String, + val description: String, + val value: String, + val valueDescription: String? = null, +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/nutritionfacts/AiInformationNutritionFactsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/nutritionfacts/AiInformationNutritionFactsScreen.kt new file mode 100644 index 0000000000..42dcfa5509 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/nutritionfacts/AiInformationNutritionFactsScreen.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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 + * + * http://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 com.instructure.horizon.features.aiassistant.aiinformation.nutritionfacts + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.features.aiassistant.aiinformation.AiInformationHeader +import com.instructure.horizon.features.aiassistant.aiinformation.model.AiInformationNutritionFactsData +import com.instructure.horizon.features.aiassistant.aiinformation.model.NutritionFactBlock +import com.instructure.horizon.features.aiassistant.aiinformation.model.NutritionFactSegment +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.Button +import com.instructure.horizon.horizonui.molecules.ButtonColor + +@Composable +fun AiInformationNutritionFactsScreen( + data: AiInformationNutritionFactsData, + onDismiss: () -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(HorizonColors.Surface.pageSecondary()) + .statusBarsPadding(), + ) { + Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) { + AiInformationHeader(onDismiss = onDismiss) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = data.title, + style = HorizonTypography.h2, + color = HorizonColors.Text.title(), + ) + } + HorizontalDivider(thickness = 1.dp, color = HorizonColors.LineAndBorder.lineStroke()) + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(horizontal = 24.dp), + contentPadding = PaddingValues(vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + item { + Text( + text = data.featureName, + style = HorizonTypography.h2, + color = HorizonColors.Text.title(), + ) + } + items(data.blocks) { block -> + NutritionFactBlockSection(block = block) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + .navigationBarsPadding(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Button( + label = data.closeButtonText, + color = ButtonColor.Black, + onClick = onDismiss, + ) + } + } + } +} + +@Composable +private fun NutritionFactBlockSection( + block: NutritionFactBlock, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = block.blockTitle, + style = HorizonTypography.h3, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_16) + block.segments.forEach { segment -> + NutritionFactSegmentCard(segment = segment) + HorizonSpace(SpaceSize.SPACE_16) + } + } +} + +@Composable +private fun NutritionFactSegmentCard( + segment: NutritionFactSegment, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(HorizonCornerRadius.level2) + .border( + width = 1.dp, + color = HorizonColors.LineAndBorder.lineStroke(), + shape = HorizonCornerRadius.level2, + ) + .padding(16.dp), + ) { + Text( + text = segment.segmentTitle, + style = HorizonTypography.labelLargeBold, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_4) + Text( + text = segment.description, + style = HorizonTypography.p2, + color = HorizonColors.Surface.attention(), + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = segment.value, + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + ) + if (segment.valueDescription != null) { + HorizonSpace(SpaceSize.SPACE_4) + Text( + text = segment.valueDescription, + style = HorizonTypography.p2, + color = HorizonColors.Text.body(), + ) + } + } +} + +@Composable +@Preview +private fun AiInformationNutritionFactsScreenPreview() { + ContextKeeper.appContext = LocalContext.current + AiInformationNutritionFactsScreen( + data = AiInformationNutritionFactsData( + title = "Nutrition Facts", + featureName = "Study Tools", + closeButtonText = "Close", + blocks = listOf( + NutritionFactBlock( + blockTitle = "Model & Data", + segments = listOf( + NutritionFactSegment( + segmentTitle = "Base Model", + description = "The foundational AI on which further training and customizations are built.", + value = "Claude 3.5 Haiku by Anthropic and Cohere multi-language v3", + ), + NutritionFactSegment( + segmentTitle = "Trained with User Data", + description = "Indicates the AI model has been given customer data in order to improve its results.", + value = "No", + ), + NutritionFactSegment( + segmentTitle = "Data Shared with Model", + description = "Indicates which training or operational content was given to the model.", + value = "Course content", + ), + ), + ), + NutritionFactBlock( + blockTitle = "Privacy & Compliance", + segments = listOf( + NutritionFactSegment( + segmentTitle = "Data Retention", + description = "How long the model stores customer data.", + value = "No", + ), + NutritionFactSegment( + segmentTitle = "Data Logging", + description = "Recording the AI's performance for auditing, analysis, and improvement.", + value = "No", + ), + ), + ), + ), + ), + onDismiss = {}, + ) +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt new file mode 100644 index 0000000000..f76b245541 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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 + * + * http://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 com.instructure.horizon.features.aiassistant.aiinformation.permissionlevels + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.features.aiassistant.aiinformation.AiInformationHeader +import com.instructure.horizon.features.aiassistant.aiinformation.model.AiInformationPermissionLevelsData +import com.instructure.horizon.features.aiassistant.aiinformation.model.DataPermissionLevel +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.Button +import com.instructure.horizon.horizonui.molecules.ButtonColor + +@Composable +fun AiInformationPermissionLevelsScreen( + data: AiInformationPermissionLevelsData, + onDismiss: () -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(HorizonColors.Surface.pageSecondary()) + .statusBarsPadding(), + ) { + Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) { + AiInformationHeader(onDismiss = onDismiss) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = data.title, + style = HorizonTypography.h2, + color = HorizonColors.Text.title(), + ) + } + HorizontalDivider(thickness = 1.dp, color = HorizonColors.LineAndBorder.lineStroke()) + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(horizontal = 24.dp), + contentPadding = PaddingValues(vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + items(data.levels) { level -> + if (level.isHighlighted) { + HighlightedPermissionLevelItem( + level = level, + currentFeatureText = data.currentFeatureText, + currentFeature = data.currentFeature, + ) + } else { + PermissionLevelItem(level = level) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + .navigationBarsPadding(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Button( + label = data.closeButtonText, + color = ButtonColor.Black, + onClick = onDismiss, + ) + } + } + } +} + +@Composable +private fun PermissionLevelItem( + level: DataPermissionLevel, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = level.level, + style = HorizonTypography.tag, + color = HorizonColors.PrimitivesViolet.violet40(), + ) + HorizonSpace(SpaceSize.SPACE_4) + Text( + text = level.title, + style = HorizonTypography.h4, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = level.description, + style = HorizonTypography.p1, + color = HorizonColors.Surface.attention(), + ) + } +} + +@Composable +private fun HighlightedPermissionLevelItem( + level: DataPermissionLevel, + currentFeatureText: String, + currentFeature: String, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(HorizonCornerRadius.level2) + .border( + width = 1.dp, + color = HorizonColors.LineAndBorder.lineStroke(), + shape = HorizonCornerRadius.level2, + ), + ) { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .background(brush = HorizonColors.Surface.aiGradient()) + .padding(16.dp), + ) { + Text( + text = "$currentFeatureText $currentFeature", + style = HorizonTypography.labelLargeBold, + color = HorizonColors.Text.surfaceColored(), + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .background(HorizonColors.Surface.cardPrimary()) + .padding(16.dp), + ) { + Text( + text = level.level, + style = HorizonTypography.tag, + color = HorizonColors.PrimitivesViolet.violet40(), + ) + HorizonSpace(SpaceSize.SPACE_4) + Text( + text = level.title, + style = HorizonTypography.h4, + color = HorizonColors.Text.title(), + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = level.description, + style = HorizonTypography.p1, + color = HorizonColors.Surface.attention(), + ) + } + } + } +} + +@Composable +@Preview +private fun AiInformationPermissionLevelsScreenPreview() { + ContextKeeper.appContext = LocalContext.current + AiInformationPermissionLevelsScreen( + data = AiInformationPermissionLevelsData( + title = "Data Permission Levels", + currentFeatureText = "Current Feature:", + currentFeature = "Study Tools", + closeButtonText = "Close", + levels = listOf( + DataPermissionLevel( + level = "LEVEL 1", + title = "Descriptive Analytics and Research", + description = "We leverage anonymised aggregate data for detailed analytics to inform model development and product improvements. No AI models are used at this level.", + ), + DataPermissionLevel( + level = "LEVEL 2", + title = "AI-Powered Features Without Data Training", + description = "We utilise off-the-shelf AI models and customer data as input to provide AI-powered features. No data is used for training this model.", + isHighlighted = true, + ), + DataPermissionLevel( + level = "LEVEL 3", + title = "AI Customization for Individual Institutions", + description = "We customise AI solutions tailored to the unique needs and resources of educational institutions.", + ), + DataPermissionLevel( + level = "LEVEL 4", + title = "Collaborative AI Consortium", + description = "We established a consortium with educational institutions that shares anonymised data, best practices, and research findings.", + ), + ), + ), + onDismiss = {}, + ) +} diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index d06d812585..baf8e0d990 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -201,6 +201,8 @@ File couldn’t be downloaded. Study tools Dismiss Study tools + IgniteAI + Dismiss IgniteAI Rate Study tools with positive feedback Rate Study tools with negative feedback Enter a prompt From 483cadd6bd0af8781f5b500980c385888736fe47 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Mon, 23 Mar 2026 17:58:33 +0100 Subject: [PATCH 3/7] Implement ui --- .../aiinformation/AiInformationScreen.kt | 126 +++++++++++++++++- .../common/composable/AiAssistScaffold.kt | 16 ++- .../common/composable/AiAssistToolbar.kt | 12 ++ libs/horizon/src/main/res/values/strings.xml | 56 ++++++++ 4 files changed, 208 insertions(+), 2 deletions(-) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt index a5e0e838c2..23ddca5cde 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt @@ -64,8 +64,8 @@ import com.instructure.horizon.horizonui.molecules.TextLinkSize @OptIn(ExperimentalMaterial3Api::class) @Composable fun AiInformationScreen( - data: AiInformationData, onDismiss: () -> Unit, + data: AiInformationData = defaultAiInformationData(), ) { var showPermissionLevels by rememberSaveable { mutableStateOf(false) } var showNutritionFacts by rememberSaveable { mutableStateOf(false) } @@ -154,6 +154,130 @@ fun AiInformationScreen( } } +@Composable +private fun defaultAiInformationData() = AiInformationData( + title = stringResource(R.string.aiInformation_title), + permissionLevelText = stringResource(R.string.aiInformation_permissionLevelText), + permissionLevel = stringResource(R.string.aiInformation_permissionLevel), + description = stringResource(R.string.aiInformation_description), + permissionLevelsModalTriggerText = stringResource(R.string.aiInformation_permissionLevelsModalTriggerText), + modelNameText = stringResource(R.string.aiInformation_modelNameText), + modelName = stringResource(R.string.aiInformation_modelName), + nutritionFactsModalTriggerText = stringResource(R.string.aiInformation_nutritionFactsModalTriggerText), + permissionLevelsData = AiInformationPermissionLevelsData( + title = stringResource(R.string.aiInformation_permissionLevels_title), + currentFeatureText = stringResource(R.string.aiInformation_permissionLevels_currentFeatureText), + currentFeature = stringResource(R.string.aiInformation_permissionLevels_currentFeature), + closeButtonText = stringResource(R.string.aiInformation_close), + levels = listOf( + DataPermissionLevel( + level = stringResource(R.string.aiInformation_level1), + title = stringResource(R.string.aiInformation_level1_title), + description = stringResource(R.string.aiInformation_level1_description), + isHighlighted = true, + ), + DataPermissionLevel( + level = stringResource(R.string.aiInformation_level2), + title = stringResource(R.string.aiInformation_level2_title), + description = stringResource(R.string.aiInformation_level2_description), + ), + DataPermissionLevel( + level = stringResource(R.string.aiInformation_level3), + title = stringResource(R.string.aiInformation_level3_title), + description = stringResource(R.string.aiInformation_level3_description), + ), + DataPermissionLevel( + level = stringResource(R.string.aiInformation_level4), + title = stringResource(R.string.aiInformation_level4_title), + description = stringResource(R.string.aiInformation_level4_description), + ), + ), + ), + nutritionFactsData = AiInformationNutritionFactsData( + title = stringResource(R.string.aiInformation_nutritionFacts_title), + featureName = stringResource(R.string.aiInformation_nutritionFacts_featureName), + closeButtonText = stringResource(R.string.aiInformation_close), + blocks = listOf( + NutritionFactBlock( + blockTitle = stringResource(R.string.aiInformation_block_modelData), + segments = listOf( + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_baseModel_title), + description = stringResource(R.string.aiInformation_segment_baseModel_description), + value = stringResource(R.string.aiInformation_segment_value), + valueDescription = stringResource(R.string.aiInformation_segment_valueDescription), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_trainedWithUserData_title), + description = stringResource(R.string.aiInformation_segment_trainedWithUserData_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_dataSharedWithModel_title), + description = stringResource(R.string.aiInformation_segment_dataSharedWithModel_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + ), + ), + NutritionFactBlock( + blockTitle = stringResource(R.string.aiInformation_block_privacyCompliance), + segments = listOf( + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_dataRetention_title), + description = stringResource(R.string.aiInformation_segment_dataRetention_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_dataLogging_title), + description = stringResource(R.string.aiInformation_segment_dataLogging_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_regionsSupported_title), + description = stringResource(R.string.aiInformation_segment_regionsSupported_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_pii_title), + description = stringResource(R.string.aiInformation_segment_pii_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + ), + ), + NutritionFactBlock( + blockTitle = stringResource(R.string.aiInformation_block_outputs), + segments = listOf( + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_aiSettingsControl_title), + description = stringResource(R.string.aiInformation_segment_aiSettingsControl_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_humanInTheLoop_title), + description = stringResource(R.string.aiInformation_segment_humanInTheLoop_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_guardrails_title), + description = stringResource(R.string.aiInformation_segment_guardrails_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_expectedRisks_title), + description = stringResource(R.string.aiInformation_segment_expectedRisks_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + NutritionFactSegment( + segmentTitle = stringResource(R.string.aiInformation_segment_intendedOutcomes_title), + description = stringResource(R.string.aiInformation_segment_intendedOutcomes_description), + value = stringResource(R.string.aiInformation_segment_value), + ), + ), + ), + ), + ), +) + @Composable internal fun AiInformationHeader( onDismiss: () -> Unit, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt index db3c80cf29..1db1d85efd 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt @@ -23,11 +23,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import com.instructure.horizon.features.aiassistant.aiinformation.AiInformationScreen import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.organisms.scaffolds.EdgeToEdgeScaffold @@ -42,6 +47,8 @@ fun AiAssistScaffold( modifier: Modifier = Modifier, content: @Composable (Modifier) -> Unit, ) { + var showAiInformation by rememberSaveable { mutableStateOf(false) } + EdgeToEdgeScaffold( statusBarColor = HorizonColors.Surface.aiGradientStart(), statusBarAlpha = 0f, @@ -69,6 +76,7 @@ fun AiAssistScaffold( } else { null }, + onInfoPressed = { showAiInformation = true }, modifier = Modifier ) @@ -77,4 +85,10 @@ fun AiAssistScaffold( .padding(horizontal = 24.dp)) } } -} \ No newline at end of file + + if (showAiInformation) { + AiInformationScreen( + onDismiss = { showAiInformation = false }, + ) + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt index 8c5e7e81a0..fb2dd0e89a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt @@ -46,6 +46,7 @@ fun AiAssistToolbar( onDismissPressed: () -> Unit, modifier: Modifier = Modifier, onBackPressed: (() -> Unit)? = null, + onInfoPressed: (() -> Unit)? = null, ) { Row( modifier = modifier.padding(24.dp), @@ -81,6 +82,17 @@ fun AiAssistToolbar( HorizonSpace(SpaceSize.SPACE_4) + if (onInfoPressed != null) { + IconButton( + iconRes = R.drawable.info, + contentDescription = stringResource(R.string.a11y_aiInformation), + size = IconButtonSize.SMALL, + color = IconButtonColor.WhiteOutline, + onClick = onInfoPressed, + ) + HorizonSpace(SpaceSize.SPACE_4) + } + IconButton( iconRes = R.drawable.close, contentDescription = stringResource(R.string.studyToolsDismissContentDescription), diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index baf8e0d990..a39192448d 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -203,6 +203,7 @@ Dismiss Study tools IgniteAI Dismiss IgniteAI + AI Information Rate Study tools with positive feedback Rate Study tools with negative feedback Enter a prompt @@ -546,4 +547,59 @@ Error: %1$s Required + Features + Permission Level + LEVEL 1 + We leverage anonymized aggregate data for detailed analytics to inform model development and product improvements. No AI models are used at this level. + Permission Levels + Base Model + Claude 3 Haiku + AI Nutrition Facts + Data Permission Levels + Current Feature: + Feature name + Close + LEVEL 1 + Descriptive Analytics and Research + We leverage anonymized aggregate data for detailed analytics to inform model development and product improvements. No AI models are used at this level. + LEVEL 2 + AI-Powered Features Without Data Training + We utilize off-the-shelf AI models and customer data as input to provide AI-powered features. No data is used for training this model. + LEVEL 3 + AI Customization for Individual Institutions + We customize AI solutions tailored to the unique needs and resources of educational institutions. We use customer data to fine-tune data and train AI models that only serve your institution. Your institution\'s data only serves them through trained models. + LEVEL 4 + Collaborative AI Consortium + We established a consortium with educational institutions that shares anonymized data, best practices, and research findings. This fosters collaboration and accelerates the responsible development of AI in education. Specialized AI models are created for better outcomes in education, cost savings, and more. + Nutrition Facts + Feature Name + Model & Data + Privacy & Compliance + Outputs + Value + Description + Base Model + The foundational AI on which further training and customizations are built. + Trained with User Data + Indicates the AI model has been given customer data in order to improve its results. + Data Shared with Model + Indicates which training or operational content was given to the model. + Data Retention + How long the model stores customer data. + Data Logging + Recording the AI\'s performance for auditing, analysis, and improvement. + Regions Supported + The locations where the AI model is officially available and supported. + PII + Sensitive data that can be used to identify an individual. + AI Settings Control + The ability to turn the AI on or off within the product. + Human in the Loop + Indicates if a human is involved in the AI\'s process or output. + Guardrails + Preventative safety mechanisms or limitations built into the AI model. + Expected Risks + Any risks the model may pose to the user. + Intended Outcomes + The specific results the AI model is meant to achieve. \ No newline at end of file From 264b6c215b1422c468bb1d244be8dae677bfee71 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Tue, 24 Mar 2026 09:46:35 +0100 Subject: [PATCH 4/7] Implement remaining changes --- .../aiinformation/AiInformationScreen.kt | 25 +++++++++---------- .../AiInformationPermissionLevelsScreen.kt | 9 ++++--- .../aiassistant/chat/AiAssistChatScreen.kt | 5 ---- .../aiassistant/chat/AiAssistChatUiState.kt | 4 --- .../aiassistant/chat/AiAssistChatViewModel.kt | 24 ------------------ .../common/composable/AiAssistScaffold.kt | 4 --- .../common/composable/AiAssistToolbar.kt | 4 +-- .../aiassistant/main/AiAssistMainScreen.kt | 6 ----- libs/horizon/src/main/res/values/strings.xml | 23 ++++++++++++----- 9 files changed, 37 insertions(+), 67 deletions(-) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt index 23ddca5cde..eecff5ac89 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/AiInformationScreen.kt @@ -174,12 +174,12 @@ private fun defaultAiInformationData() = AiInformationData( level = stringResource(R.string.aiInformation_level1), title = stringResource(R.string.aiInformation_level1_title), description = stringResource(R.string.aiInformation_level1_description), - isHighlighted = true, ), DataPermissionLevel( level = stringResource(R.string.aiInformation_level2), title = stringResource(R.string.aiInformation_level2_title), description = stringResource(R.string.aiInformation_level2_description), + isHighlighted = true, ), DataPermissionLevel( level = stringResource(R.string.aiInformation_level3), @@ -204,18 +204,17 @@ private fun defaultAiInformationData() = AiInformationData( NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_baseModel_title), description = stringResource(R.string.aiInformation_segment_baseModel_description), - value = stringResource(R.string.aiInformation_segment_value), - valueDescription = stringResource(R.string.aiInformation_segment_valueDescription), + value = stringResource(R.string.aiInformation_modelName), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_trainedWithUserData_title), description = stringResource(R.string.aiInformation_segment_trainedWithUserData_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_trainedWithUserData_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_dataSharedWithModel_title), description = stringResource(R.string.aiInformation_segment_dataSharedWithModel_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_dataSharedWithModel_value), ), ), ), @@ -225,22 +224,22 @@ private fun defaultAiInformationData() = AiInformationData( NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_dataRetention_title), description = stringResource(R.string.aiInformation_segment_dataRetention_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_dataRetention_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_dataLogging_title), description = stringResource(R.string.aiInformation_segment_dataLogging_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_dataLogging_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_regionsSupported_title), description = stringResource(R.string.aiInformation_segment_regionsSupported_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_regionsSupported_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_pii_title), description = stringResource(R.string.aiInformation_segment_pii_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_pii_value), ), ), ), @@ -255,22 +254,22 @@ private fun defaultAiInformationData() = AiInformationData( NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_humanInTheLoop_title), description = stringResource(R.string.aiInformation_segment_humanInTheLoop_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_humanInTheLoop_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_guardrails_title), description = stringResource(R.string.aiInformation_segment_guardrails_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_guardrails_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_expectedRisks_title), description = stringResource(R.string.aiInformation_segment_expectedRisks_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_expectedRisks_value), ), NutritionFactSegment( segmentTitle = stringResource(R.string.aiInformation_segment_intendedOutcomes_title), description = stringResource(R.string.aiInformation_segment_intendedOutcomes_description), - value = stringResource(R.string.aiInformation_segment_value), + value = stringResource(R.string.aiInformation_segment_intendedOutcomes_value), ), ), ), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt index f76b245541..21010b2c50 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/aiinformation/permissionlevels/AiInformationPermissionLevelsScreen.kt @@ -79,8 +79,7 @@ fun AiInformationPermissionLevelsScreen( HorizontalDivider(thickness = 1.dp, color = HorizonColors.LineAndBorder.lineStroke()) LazyColumn( modifier = Modifier - .weight(1f) - .padding(horizontal = 24.dp), + .weight(1f), contentPadding = PaddingValues(vertical = 24.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { @@ -90,9 +89,13 @@ fun AiInformationPermissionLevelsScreen( level = level, currentFeatureText = data.currentFeatureText, currentFeature = data.currentFeature, + modifier = Modifier.padding(horizontal = 8.dp) ) } else { - PermissionLevelItem(level = level) + PermissionLevelItem( + level = level, + modifier = Modifier.padding(horizontal = 24.dp) + ) } } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt index 8e4211e65a..5f6f2f499a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri @@ -86,9 +85,6 @@ fun AiAssistChatScreen( navController = navController, onClearChatHistory = state.onClearChatHistory, onDismiss = { onDismiss() }, - inputTextValue = state.inputTextValue, - onInputTextChanged = { state.onInputTextChanged(it) }, - onInputTextSubmitted = { state.onInputTextSubmitted() }, ) { modifier -> val scrollState = rememberLazyListState() LaunchedEffect(state.messages.size) { @@ -158,7 +154,6 @@ private fun AssistChatScreenPreview() { role = JourneyAssistRole.Assistant ) ), - inputTextValue = TextFieldValue("Hi,"), isLoading = true ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatUiState.kt index 0d36a7d23c..aa3f7a5328 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatUiState.kt @@ -1,6 +1,5 @@ package com.instructure.horizon.features.aiassistant.chat -import androidx.compose.ui.text.input.TextFieldValue import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessage data class AiAssistChatUiState( @@ -8,9 +7,6 @@ data class AiAssistChatUiState( val isLoading: Boolean = false, val error: String? = null, val isFeedbackEnabled: Boolean = false, - val inputTextValue: TextFieldValue = TextFieldValue(""), - val onInputTextChanged: (TextFieldValue) -> Unit = {}, - val onInputTextSubmitted: () -> Unit = {}, val onClearChatHistory: () -> Unit = {}, val onChipClicked: (String) -> Unit = {}, val onNavigateToCards: () -> Unit = {}, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModel.kt index e90465a24d..e27dfbaac4 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModel.kt @@ -16,7 +16,6 @@ */ package com.instructure.horizon.features.aiassistant.chat -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.instructure.canvasapi2.models.journey.JourneyAssistRole @@ -39,8 +38,6 @@ class AiAssistChatViewModel @Inject constructor( private val aiAssistContextProvider: AiAssistContextProvider, ): ViewModel() { private val _uiState = MutableStateFlow(AiAssistChatUiState( - onInputTextChanged = ::onTextInputChanged, - onInputTextSubmitted = ::onTextInputSubmitted, onClearChatHistory = ::onClearChatHistory, onChipClicked = ::onChipClicked, onNavigateToCards = ::onNavigateToCards, @@ -51,27 +48,6 @@ class AiAssistChatViewModel @Inject constructor( private var aiAssistContextState = aiAssistContextProvider.aiAssistContext.state private var aiAssistMessages = aiAssistContextProvider.aiAssistContext.chatHistory.toMutableList() - private fun onTextInputChanged(newValue: TextFieldValue) { - _uiState.update { - it.copy( - inputTextValue = newValue, - ) - } - } - - private fun onTextInputSubmitted() { - val prompt = _uiState.value.inputTextValue.text - val message = addMessage(prompt) - _uiState.update { - it.copy( - inputTextValue = TextFieldValue(""), - messages = it.messages + message, - ) - } - - evaluatePrompt(message) - } - private fun evaluatePrompt(message: AiAssistMessage) { viewModelScope.tryLaunch { _uiState.update { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt index 1db1d85efd..65fe13394c 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistScaffold.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.instructure.horizon.features.aiassistant.aiinformation.AiInformationScreen @@ -41,9 +40,6 @@ fun AiAssistScaffold( navController: NavHostController, onClearChatHistory: () -> Unit, onDismiss: () -> Unit, - inputTextValue: TextFieldValue? = null, - onInputTextChanged: ((TextFieldValue) -> Unit)? = null, - onInputTextSubmitted: (() -> Unit)? = null, modifier: Modifier = Modifier, content: @Composable (Modifier) -> Unit, ) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt index fb2dd0e89a..208d9d3503 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistToolbar.kt @@ -87,7 +87,7 @@ fun AiAssistToolbar( iconRes = R.drawable.info, contentDescription = stringResource(R.string.a11y_aiInformation), size = IconButtonSize.SMALL, - color = IconButtonColor.WhiteOutline, + color = IconButtonColor.BlackGhost, onClick = onInfoPressed, ) HorizonSpace(SpaceSize.SPACE_4) @@ -97,7 +97,7 @@ fun AiAssistToolbar( iconRes = R.drawable.close, contentDescription = stringResource(R.string.studyToolsDismissContentDescription), size = IconButtonSize.SMALL, - color = IconButtonColor.WhiteOutline, + color = IconButtonColor.BlackGhost, onClick = onDismissPressed, ) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt index 440e040b7a..edb60a8abf 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/main/AiAssistMainScreen.kt @@ -97,12 +97,6 @@ fun AiAssistMainScreen( navController = navController, onClearChatHistory = { }, onDismiss = { onDismiss() }, - inputTextValue = promptInput, - onInputTextChanged = { promptInput = it }, - onInputTextSubmitted = { - state.sendMessage(promptInput.text) - promptInput = TextFieldValue("") - } ) { modifier -> LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index a39192448d..b73f177443 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -547,17 +547,17 @@ Error: %1$s Required - Features + Study tools Permission Level LEVEL 1 - We leverage anonymized aggregate data for detailed analytics to inform model development and product improvements. No AI models are used at this level. + We utilize off-the-shelf AI models and customer data as input to provide AI-powered features. No data is used for training this model. Permission Levels Base Model - Claude 3 Haiku - AI Nutrition Facts + Claude 3.5 Haiku by Anthropic and Cohere multi-language v3 + View AI Nutrition Facts Data Permission Levels Current Feature: - Feature name + Study tools Close LEVEL 1 Descriptive Analytics and Research @@ -572,7 +572,7 @@ Collaborative AI Consortium We established a consortium with educational institutions that shares anonymized data, best practices, and research findings. This fosters collaboration and accelerates the responsible development of AI in education. Specialized AI models are created for better outcomes in education, cost savings, and more. Nutrition Facts - Feature Name + Study tools Model & Data Privacy & Compliance Outputs @@ -582,24 +582,35 @@ The foundational AI on which further training and customizations are built. Trained with User Data Indicates the AI model has been given customer data in order to improve its results. + No Data Shared with Model Indicates which training or operational content was given to the model. + Course content Data Retention How long the model stores customer data. + No Data Logging Recording the AI\'s performance for auditing, analysis, and improvement. + Chat logs are retained for 30 days for troubleshooting and debugging Regions Supported The locations where the AI model is officially available and supported. + Global PII Sensitive data that can be used to identify an individual. + Not Exposed AI Settings Control The ability to turn the AI on or off within the product. + No Human in the Loop Indicates if a human is involved in the AI\'s process or output. + Yes Guardrails Preventative safety mechanisms or limitations built into the AI model. + Yes Expected Risks Any risks the model may pose to the user. + Low risk Intended Outcomes The specific results the AI model is meant to achieve. + AI Generated content may contain mistakes or inaccurate information and should always be verified \ No newline at end of file From ffe60d6cecb67dd40dc90872ae4a4f59974c7085 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Tue, 24 Mar 2026 13:22:38 +0100 Subject: [PATCH 5/7] Fix tests --- .../horizon/pages/HorizonHomePage.kt | 8 - .../chat/AiAssistChatViewModelTest.kt | 231 ------------------ .../common/AiAssistRepositoryTest.kt | 18 +- 3 files changed, 10 insertions(+), 247 deletions(-) diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt index 07977dd90c..912d7330e6 100644 --- a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt @@ -18,7 +18,6 @@ package com.instructure.horizon.pages import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -28,8 +27,6 @@ class HorizonHomePage(private val composeTestRule: ComposeTestRule) { .assertIsDisplayed() composeTestRule.onNodeWithText("Learn") .assertIsDisplayed() - composeTestRule.onNodeWithContentDescription("IgniteAI") - .assertIsDisplayed() composeTestRule.onNodeWithText("Skillspace") .assertIsDisplayed() composeTestRule.onNodeWithText("Account") @@ -46,11 +43,6 @@ class HorizonHomePage(private val composeTestRule: ComposeTestRule) { .performClick() } - fun clickAiAssistantTab() { - composeTestRule.onNodeWithContentDescription("IgniteAI") - .performClick() - } - fun clickSkillspaceTab() { composeTestRule.onNodeWithText("Skillspace") .performClick() diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt index f45de8e061..3f26ce106a 100644 --- a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt @@ -16,8 +16,6 @@ */ package com.instructure.horizon.features.aiassistant.chat -import androidx.compose.ui.text.input.TextFieldValue -import com.instructure.canvasapi2.models.journey.JourneyAssistChipOption import com.instructure.canvasapi2.models.journey.JourneyAssistRole import com.instructure.canvasapi2.models.journey.JourneyAssistState import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider @@ -26,11 +24,9 @@ import com.instructure.horizon.features.aiassistant.common.AiAssistResponse import com.instructure.horizon.features.aiassistant.common.model.AiAssistContext import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessage import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.unmockkAll -import io.mockk.verify import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue @@ -106,233 +102,6 @@ class AiAssistChatViewModelTest { assertFalse(viewModel.uiState.value.isLoading) } - @Test - fun `Text input change updates state`() = runTest { - val viewModel = getViewModel() - - val newText = TextFieldValue("Test input") - viewModel.uiState.value.onInputTextChanged(newText) - - assertEquals("Test input", viewModel.uiState.value.inputTextValue.text) - } - - @Test - fun `Text submission sends message and clears input`() = runTest { - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - assertTrue(viewModel.uiState.value.messages.any { - it.role == JourneyAssistRole.User && it.text == "Test message" - }) - assertEquals("", viewModel.uiState.value.inputTextValue.text) - } - - @Test - fun `Text submission receives response from repository`() = runTest { - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - coVerify { repository.answerPrompt("Test message", any(), any()) } - assertTrue(viewModel.uiState.value.messages.any { - it.role == JourneyAssistRole.Assistant && it.text == "Test response" - }) - assertFalse(viewModel.uiState.value.isLoading) - } - - @Test - fun `Loading state is set during message submission`() = runTest { - coEvery { - repository.answerPrompt(any(), any(), any()) - } coAnswers { - kotlinx.coroutines.delay(100) - val message = AiAssistMessage( - text = "Response", - role = JourneyAssistRole.Assistant - ) - AiAssistResponse(message, testState) - } - - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) - viewModel.uiState.value.onInputTextSubmitted() - - assertTrue(viewModel.uiState.value.isLoading) - - testDispatcher.scheduler.advanceUntilIdle() - - assertFalse(viewModel.uiState.value.isLoading) - } - - @Test - fun `Messages are appended in correct order`() = runTest { - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("First message")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Second message")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - val messages = viewModel.uiState.value.messages - assertEquals(4, messages.size) - assertEquals(JourneyAssistRole.User, messages[0].role) - assertEquals("First message", messages[0].text) - assertEquals(JourneyAssistRole.Assistant, messages[1].role) - assertEquals(JourneyAssistRole.User, messages[2].role) - assertEquals("Second message", messages[2].text) - assertEquals(JourneyAssistRole.Assistant, messages[3].role) - } - - @Test - fun `Chip click sends prompt and receives response`() = runTest { - val viewModel = getViewModel() - - viewModel.uiState.value.onChipClicked("Suggested prompt") - testDispatcher.scheduler.advanceUntilIdle() - - assertTrue(viewModel.uiState.value.messages.any { - it.role == JourneyAssistRole.User && it.text == "Suggested prompt" - }) - coVerify { repository.answerPrompt("Suggested prompt", any(), any()) } - assertTrue(viewModel.uiState.value.messages.any { - it.role == JourneyAssistRole.Assistant - }) - } - - @Test - fun `Clear chat history updates context provider`() = runTest { - val existingMessage = AiAssistMessage( - text = "Existing message", - role = JourneyAssistRole.User - ) - val contextWithHistory = testContext.copy(chatHistory = listOf(existingMessage)) - every { aiAssistContextProvider.aiAssistContext } returns contextWithHistory - - val viewModel = getViewModel() - viewModel.uiState.value.onClearChatHistory() - - verify { - aiAssistContextProvider.aiAssistContext = match { - it.chatHistory.isEmpty() - } - } - } - - @Test - fun `Navigate to cards updates context and removes last message`() = runTest { - val responseMessage = AiAssistMessage( - text = "Response with cards", - role = JourneyAssistRole.Assistant, - chipOptions = listOf(JourneyAssistChipOption("Option 1", "prompt1")) - ) - coEvery { - repository.answerPrompt(any(), any(), any()) - } returns AiAssistResponse(responseMessage, testState) - - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Generate cards")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - val messageCountBefore = viewModel.uiState.value.messages.size - viewModel.uiState.value.onNavigateToCards() - - assertEquals(messageCountBefore - 1, viewModel.uiState.value.messages.size) - verify { aiAssistContextProvider.aiAssistContext = any() } - } - - @Test - fun `Repository is called with conversation history`() = runTest { - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("First")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Second")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - coVerify(exactly = 2) { - repository.answerPrompt(any(), any(), any()) - } - - coVerify { - repository.answerPrompt( - "Second", - match { history -> - history.any { it.role == JourneyAssistRole.User && it.text == "First" } && - history.any { it.role == JourneyAssistRole.Assistant } && - history.any { it.role == JourneyAssistRole.User && it.text == "Second" } - }, - any() - ) - } - } - - @Test - fun `State is updated from repository response`() = runTest { - val updatedState = JourneyAssistState( - courseID = "456", - fileID = "789", - pageID = "101" - ) - val message = AiAssistMessage( - text = "Response", - role = JourneyAssistRole.Assistant - ) - coEvery { - repository.answerPrompt(any(), any(), any()) - } returns AiAssistResponse(message, updatedState) - - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Second")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - coVerify { - repository.answerPrompt( - "Second", - any(), - match { state -> - state.courseID == "456" && - state.fileID == "789" && - state.pageID == "101" - } - ) - } - } - - @Test - fun `Error handling sets loading to false`() = runTest { - coEvery { - repository.answerPrompt(any(), any(), any()) - } throws Exception("Network error") - - val viewModel = getViewModel() - - viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test")) - viewModel.uiState.value.onInputTextSubmitted() - testDispatcher.scheduler.advanceUntilIdle() - - assertFalse(viewModel.uiState.value.isLoading) - } - private fun getViewModel(): AiAssistChatViewModel { return AiAssistChatViewModel(repository, aiAssistContextProvider) } diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepositoryTest.kt index bd61791e23..afacc31ffc 100644 --- a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepositoryTest.kt +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/common/AiAssistRepositoryTest.kt @@ -91,7 +91,7 @@ class AiAssistRepositoryTest { ) coEvery { - journeyAssistAPI.answerPrompt(any()) + journeyAssistAPI.answerPrompt(any(), true) } returns apiResponse val result = repository.answerPrompt( @@ -117,7 +117,7 @@ class AiAssistRepositoryTest { ) coEvery { - journeyAssistAPI.answerPrompt(any()) + journeyAssistAPI.answerPrompt(any(), true) } returns apiResponse val result = repository.answerPrompt( @@ -144,7 +144,7 @@ class AiAssistRepositoryTest { ) coEvery { - journeyAssistAPI.answerPrompt(any()) + journeyAssistAPI.answerPrompt(any(), true) } returns JourneyAssistResponse(response = "Response") repository.answerPrompt(prompt, history, testState) @@ -155,7 +155,8 @@ class AiAssistRepositoryTest { requestBody.prompt == prompt && requestBody.history == history && requestBody.state == testState - } + }, + true ) } } @@ -169,7 +170,7 @@ class AiAssistRepositoryTest { ) coEvery { - journeyAssistAPI.answerPrompt(any()) + journeyAssistAPI.answerPrompt(any(), true) } returns JourneyAssistResponse( response = "Response", state = updatedState @@ -187,7 +188,7 @@ class AiAssistRepositoryTest { @Test fun `answerPrompt handles null state in response`() = runTest { coEvery { - journeyAssistAPI.answerPrompt(any()) + journeyAssistAPI.answerPrompt(any(), true) } returns JourneyAssistResponse( response = "Response", state = null @@ -205,7 +206,7 @@ class AiAssistRepositoryTest { @Test fun `answerPrompt with empty history`() = runTest { coEvery { - journeyAssistAPI.answerPrompt(any()) + journeyAssistAPI.answerPrompt(any(), true) } returns JourneyAssistResponse(response = "Response") repository.answerPrompt( @@ -216,7 +217,8 @@ class AiAssistRepositoryTest { coVerify { journeyAssistAPI.answerPrompt( - match { it.history.isEmpty() } + match { it.history.isEmpty() }, + true ) } } From 927dc505cb0ad60651b2e08292833728f9cf4d68 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Tue, 24 Mar 2026 14:57:19 +0100 Subject: [PATCH 6/7] Fix tests --- .../features/aiassistant/chat/AiAssistChatViewModelTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt index 3f26ce106a..591e12b71b 100644 --- a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt @@ -16,8 +16,8 @@ */ package com.instructure.horizon.features.aiassistant.chat -import com.instructure.canvasapi2.models.journey.JourneyAssistRole -import com.instructure.canvasapi2.models.journey.JourneyAssistState +import com.instructure.canvasapi2.models.journey.assist.JourneyAssistRole +import com.instructure.canvasapi2.models.journey.assist.JourneyAssistState import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider import com.instructure.horizon.features.aiassistant.common.AiAssistRepository import com.instructure.horizon.features.aiassistant.common.AiAssistResponse From d60b49d421fd16aac0ff225124a36a673fce71a8 Mon Sep 17 00:00:00 2001 From: domonkosadam Date: Tue, 24 Mar 2026 17:31:51 +0100 Subject: [PATCH 7/7] Fix corner radius --- .../aiassistant/common/composable/AiAssistDetailedFeedback.kt | 2 +- .../features/aiassistant/common/composable/AiAssistInput.kt | 2 +- .../aiassistant/common/composable/AiAssistUserTextBlock.kt | 2 +- .../aiassistant/quiz/composable/AiAssistQuizAnswer.kt | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistDetailedFeedback.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistDetailedFeedback.kt index 4d822859d3..6db1295d3f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistDetailedFeedback.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistDetailedFeedback.kt @@ -74,7 +74,7 @@ fun AiAssistDetailedFeedback( ) { Column( modifier = modifier - .clip(HorizonCornerRadius.level2) + .clip(HorizonCornerRadius.level1_5) .background(Color.White.copy(alpha = 0.1f)) .padding(16.dp) ) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistInput.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistInput.kt index 6784067c21..e5f84ed0b3 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistInput.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistInput.kt @@ -51,7 +51,7 @@ fun AiAssistInput( .fillMaxWidth() .background( HorizonColors.Surface.cardPrimary(), - HorizonCornerRadius.level2 + HorizonCornerRadius.level1_5 ) ) { AiAssistTextArea( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistUserTextBlock.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistUserTextBlock.kt index 855c4668d6..1737c6219a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistUserTextBlock.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/common/composable/AiAssistUserTextBlock.kt @@ -36,7 +36,7 @@ fun AiAssistUserTextBlock( ) { Box( modifier = modifier - .clip(HorizonCornerRadius.level2) + .clip(HorizonCornerRadius.level1_5) .background(HorizonColors.Surface.cardPrimary()) .padding(16.dp) ) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/composable/AiAssistQuizAnswer.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/composable/AiAssistQuizAnswer.kt index 83443f06a8..0619064632 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/composable/AiAssistQuizAnswer.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/composable/AiAssistQuizAnswer.kt @@ -65,7 +65,7 @@ fun AiAssistQuizAnswer( Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .clip(HorizonCornerRadius.level2) + .clip(HorizonCornerRadius.level1_5) .clickable { onClick() } .conditional(status != AiAssistQuizAnswerStatus.UNSELECTED) { this @@ -75,7 +75,7 @@ fun AiAssistQuizAnswer( this .border( HorizonBorder.level1(HorizonColors.Surface.pageSecondary()), - shape = HorizonCornerRadius.level2, + shape = HorizonCornerRadius.level1_5, ) } .padding(16.dp)