-
-
Notifications
You must be signed in to change notification settings - Fork 136
Add Android Digital Assistant integration #385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package org.stypox.dicio.io.assistant | ||
|
|
||
| import android.content.Intent | ||
| import android.os.Bundle | ||
| import androidx.activity.ComponentActivity | ||
|
|
||
| /** | ||
| * A transparent activity that immediately launches the assistant overlay service | ||
| * and finishes itself. This is used to handle ASSIST and VOICE_COMMAND intents. | ||
| */ | ||
| class AssistantLauncherActivity : ComponentActivity() { | ||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| AssistantOverlayService.start(this) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
| finish() | ||
| } | ||
| } | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of creating new components from scratch, can't you reuse the other components, and just pass modifiers (or other parameters) to make them smaller? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| package org.stypox.dicio.io.assistant | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.isSystemInDarkTheme | ||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.size | ||
| import androidx.compose.foundation.layout.widthIn | ||
| import androidx.compose.foundation.lazy.LazyColumn | ||
| import androidx.compose.foundation.lazy.items | ||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.material.icons.Icons | ||
| import androidx.compose.material.icons.filled.Close | ||
| import androidx.compose.material.icons.filled.Person | ||
| import androidx.compose.material3.Card | ||
| import androidx.compose.material3.CardDefaults | ||
| import androidx.compose.material3.Icon | ||
| import androidx.compose.material3.IconButton | ||
| import androidx.compose.material3.MaterialTheme | ||
| import androidx.compose.material3.Surface | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.material3.darkColorScheme | ||
| import androidx.compose.material3.lightColorScheme | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableIntStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.isActive | ||
| import androidx.compose.ui.draw.clip | ||
| import androidx.compose.ui.platform.LocalConfiguration | ||
| import androidx.compose.ui.text.font.FontStyle | ||
| import androidx.compose.ui.text.font.FontWeight | ||
| import androidx.compose.ui.unit.dp | ||
| import org.dicio.skill.context.SkillContext | ||
| import org.stypox.dicio.io.input.SttState | ||
| import org.stypox.dicio.ui.home.InteractionLog | ||
| import org.stypox.dicio.ui.home.SttFab | ||
|
|
||
| @Composable | ||
| fun AssistantOverlay( | ||
| skillContext: SkillContext, | ||
| interactionLog: InteractionLog, | ||
| sttState: SttState?, | ||
| onSttClick: () -> Unit, | ||
| onDismiss: () -> Unit, | ||
| ) { | ||
| // Use a simple MaterialTheme without the Activity-dependent SideEffect | ||
| val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to use a wrong theme (?). I get purple buttons. Also, please add a boolean parameter to |
||
|
|
||
| MaterialTheme(colorScheme = colorScheme) { | ||
| Surface( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = 16.dp, vertical = 8.dp), | ||
| shape = RoundedCornerShape(24.dp), | ||
| shadowElevation = 8.dp, | ||
| tonalElevation = 2.dp, | ||
| ) { | ||
| Column( | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of making this a |
||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .background(MaterialTheme.colorScheme.surface) | ||
| .padding(16.dp) | ||
| ) { | ||
| // Header with close button | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| horizontalArrangement = Arrangement.SpaceBetween, | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| Text( | ||
| text = androidx.compose.ui.platform.LocalContext.current.getString(org.stypox.dicio.R.string.app_name), | ||
| style = MaterialTheme.typography.titleMedium, | ||
| color = MaterialTheme.colorScheme.onSurface | ||
| ) | ||
| IconButton(onClick = onDismiss) { | ||
| Icon( | ||
| imageVector = Icons.Default.Close, | ||
| contentDescription = "Dismiss", | ||
| tint = MaterialTheme.colorScheme.onSurface | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| Spacer(modifier = Modifier.height(8.dp)) | ||
|
|
||
| // Compact interaction list | ||
| CompactInteractionList( | ||
| skillContext = skillContext, | ||
| interactionLog = interactionLog, | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .height(200.dp) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should adapt properly to different screen sizes: e.g. it could be |
||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(16.dp)) | ||
|
|
||
| // Microphone button | ||
| Box( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| if (sttState != null) { | ||
| SttFab( | ||
| state = sttState, | ||
| onClick = onSttClick, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun CompactInteractionList( | ||
| skillContext: SkillContext, | ||
| interactionLog: InteractionLog, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| val listState = rememberLazyListState() | ||
| val interactions = interactionLog.interactions | ||
| val pendingQuestion = interactionLog.pendingQuestion | ||
|
|
||
| // Continuously scroll to bottom while there's a pending question | ||
| LaunchedEffect(pendingQuestion, interactions) { | ||
| if (pendingQuestion != null) { | ||
| // Keep scrolling while the question is pending | ||
| while (isActive && pendingQuestion != null) { | ||
| val itemCount = listState.layoutInfo.totalItemsCount | ||
| if (itemCount > 0) { | ||
| listState.scrollToItem(itemCount - 1) | ||
| } | ||
| delay(150) | ||
| } | ||
| } else { | ||
| // Scroll once when new answer is added | ||
| val itemCount = listState.layoutInfo.totalItemsCount | ||
| if (itemCount > 0) { | ||
| delay(100) // Small delay to let content render | ||
| listState.animateScrollToItem(itemCount - 1) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| LazyColumn( | ||
| modifier = modifier | ||
| .clip(RoundedCornerShape(12.dp)) | ||
| .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) | ||
| .padding(8.dp), | ||
| state = listState, | ||
| verticalArrangement = Arrangement.spacedBy(8.dp) | ||
| ) { | ||
| interactions.forEach { interaction -> | ||
| items(interaction.questionsAnswers) { qa -> | ||
| Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { | ||
| if (qa.question != null) { | ||
| CompactQuestionBubble(text = qa.question) | ||
| } | ||
|
|
||
| CompactAnswerBubble { | ||
| qa.answer.GraphicalOutput(ctx = skillContext) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (pendingQuestion != null) { | ||
| item { | ||
| CompactQuestionBubble( | ||
| text = pendingQuestion.userInput, | ||
| isPending = true | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| item { | ||
| Spacer(modifier = Modifier.height(4.dp)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun CompactQuestionBubble( | ||
| text: String, | ||
| isPending: Boolean = false, | ||
| ) { | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| horizontalArrangement = Arrangement.End | ||
| ) { | ||
| Card( | ||
| colors = CardDefaults.cardColors( | ||
| containerColor = MaterialTheme.colorScheme.tertiaryContainer | ||
| ), | ||
| modifier = Modifier.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f) | ||
| ) { | ||
| Row( | ||
| horizontalArrangement = Arrangement.spacedBy(6.dp), | ||
| modifier = Modifier.padding(8.dp), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Default.Person, | ||
| contentDescription = null, | ||
| modifier = Modifier.size(16.dp), | ||
| tint = MaterialTheme.colorScheme.onTertiaryContainer | ||
| ) | ||
| Text( | ||
| text = text, | ||
| style = MaterialTheme.typography.bodyMedium.copy( | ||
| fontWeight = if (isPending) FontWeight.Normal else FontWeight.Medium, | ||
| fontStyle = if (isPending) FontStyle.Italic else FontStyle.Normal | ||
| ), | ||
| color = MaterialTheme.colorScheme.onTertiaryContainer | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun CompactAnswerBubble( | ||
| content: @Composable () -> Unit, | ||
| ) { | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| horizontalArrangement = Arrangement.Start | ||
| ) { | ||
| Card( | ||
| colors = CardDefaults.cardColors( | ||
| containerColor = MaterialTheme.colorScheme.secondaryContainer | ||
| ), | ||
| modifier = Modifier.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f) | ||
| ) { | ||
| Box( | ||
| modifier = Modifier.padding(8.dp), | ||
| contentAlignment = Alignment.CenterStart | ||
| ) { | ||
| content() | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this supposed to be
specialUseormicrophone? Please add a comment or a documentation source.