Minor Assist UI feedback/changes (#3607)

- Remove attribution
 - Scroll when messages are updated in case messages are longer than the "…" while running
 - Animation for active voice input
This commit is contained in:
Joris Pelgröm 2023-06-26 03:59:55 +02:00 committed by GitHub
parent 3c706bb7b1
commit 02793338e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 80 deletions

View file

@ -166,14 +166,11 @@ class AssistViewModel @Inject constructor(
selectedPipeline =
allPipelines[selectedServerId]?.firstOrNull { it.id == id } ?: serverManager.webSocketRepository(selectedServerId).getAssistPipeline(id)
selectedPipeline?.let {
val attribution = serverManager.webSocketRepository(selectedServerId).getConversationAgentInfo(it.conversationEngine)?.attribution
currentPipeline = AssistUiPipeline(
serverId = selectedServerId,
serverName = serverManager.getServer(selectedServerId)?.friendlyName ?: "",
id = it.id,
name = it.name,
attributionName = attribution?.name,
attributionUrl = attribution?.url
name = it.name
)
_conversation.clear()

View file

@ -1,10 +1,17 @@
package io.homeassistant.companion.android.assist.ui
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.Spacer
import androidx.compose.foundation.layout.WindowInsets
@ -22,9 +29,11 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
@ -35,6 +44,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
@ -51,19 +61,18 @@ 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.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mikepenz.iconics.compose.Image
@ -116,7 +125,7 @@ fun AssistSheetView(
) {
Column {
val lazyListState = rememberLazyListState()
LaunchedEffect(conversation.size) {
LaunchedEffect("${conversation.size}.${conversation.lastOrNull()?.message?.length}") {
lazyListState.animateScrollToItem(conversation.size)
}
@ -169,13 +178,9 @@ fun AssistSheetHeader(
)
if (currentPipeline != null) {
val color = colorResource(commonR.color.colorOnSurfaceVariant)
val weight = if (currentPipeline.attributionName != null) 0.5f else 1f
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Box(Modifier.weight(weight, fill = false)) {
Row(modifier = Modifier.fillMaxWidth()) {
Box {
var pipelineShowList by remember { mutableStateOf(false) }
val pipelineShowServer by rememberSaveable(pipelines.size) {
mutableStateOf(pipelines.distinctBy { it.serverId }.size > 1)
@ -216,22 +221,6 @@ fun AssistSheetHeader(
}
}
}
if (currentPipeline.attributionName != null) {
val uriHandler = LocalUriHandler.current
val baseModifier = Modifier.weight(weight, fill = false).padding(start = 8.dp)
val modifier = currentPipeline.attributionUrl?.let {
Modifier
.clickable { uriHandler.openUri(it) }
.then(baseModifier)
} ?: baseModifier
Text(
text = currentPipeline.attributionName,
textDecoration = if (currentPipeline.attributionUrl != null) TextDecoration.Underline else null,
color = color,
style = MaterialTheme.typography.caption,
modifier = modifier
)
}
}
}
}
@ -303,22 +292,49 @@ fun AssistSheetControls(
} else {
Spacer(Modifier.size(48.dp))
Spacer(Modifier.weight(0.5f))
OutlinedButton({ onMicrophoneInput() }) {
Box(
modifier = Modifier.size(64.dp),
contentAlignment = Alignment.Center
) {
val inputIsActive = inputMode == AssistViewModel.AssistInputMode.VOICE_ACTIVE
Image(
asset = CommunityMaterial.Icon3.cmd_microphone,
contentDescription = stringResource(
if (inputIsActive) commonR.string.assist_stop_listening else commonR.string.assist_start_listening
),
colorFilter = ColorFilter.tint(
if (inputIsActive) {
LocalContentColor.current
} else {
MaterialTheme.colors.onSurface
}
),
modifier = Modifier.size(32.dp)
)
if (inputIsActive) {
val transition = rememberInfiniteTransition()
val scale by transition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Surface(
color = colorResource(commonR.color.colorSpeechText),
modifier = Modifier.size(48.dp).scale(scale),
shape = CircleShape,
content = {}
)
}
OutlinedButton(
onClick = { onMicrophoneInput() },
modifier = Modifier.size(48.dp),
shape = CircleShape,
border = if (inputIsActive) null else ButtonDefaults.outlinedBorder,
colors = if (inputIsActive) {
ButtonDefaults.outlinedButtonColors(backgroundColor = Color.Transparent, contentColor = Color.Black)
} else {
ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.onSurface)
},
contentPadding = PaddingValues(all = 0.dp)
) {
Image(
asset = CommunityMaterial.Icon3.cmd_microphone,
contentDescription = stringResource(
if (inputIsActive) commonR.string.assist_stop_listening else commonR.string.assist_start_listening
),
colorFilter = ColorFilter.tint(LocalContentColor.current),
modifier = Modifier.size(28.dp)
)
}
}
Spacer(Modifier.weight(0.5f))
IconButton({ onChangeInput() }) {

View file

@ -4,7 +4,5 @@ data class AssistUiPipeline(
val serverId: Int,
val serverName: String,
val id: String,
val name: String,
val attributionName: String? = null,
val attributionUrl: String? = null
val name: String
)

View file

@ -9,7 +9,6 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.As
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineListResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CompressedStateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationAgentInfoResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CurrentUserResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.DeviceRegistryResponse
@ -87,12 +86,6 @@ interface WebSocketRepository {
*/
suspend fun getConversation(speech: String): ConversationResponse?
/**
* Get information about the conversation agent.
* @param agentId Should be an [AssistPipelineResponse.conversationEngine]
*/
suspend fun getConversationAgentInfo(agentId: String): ConversationAgentInfoResponse?
/**
* Get information about an Assist pipeline.
* @param pipelineId the ID of the pipeline to get details for, if not specified the preferred

View file

@ -34,7 +34,6 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.As
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineSttEnd
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineTtsEnd
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CompressedStateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationAgentInfoResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CurrentUserResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.DeviceRegistryResponse
@ -222,17 +221,6 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
return mapResponse(socketResponse)
}
override suspend fun getConversationAgentInfo(agentId: String): ConversationAgentInfoResponse? {
val socketResponse = sendMessage(
mapOf(
"type" to "conversation/agent/info",
"agent_id" to agentId
)
)
return mapResponse(socketResponse)
}
override suspend fun getAssistPipeline(pipelineId: String?): AssistPipelineResponse? {
val data = mapOf(
"type" to "assist_pipeline/pipeline/get"

View file

@ -1,14 +0,0 @@
package io.homeassistant.companion.android.common.data.websocket.impl.entities
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class ConversationAgentInfoResponse(
val attribution: ConversationAgentAttribution?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class ConversationAgentAttribution(
val name: String,
val url: String?
)