Improve voice dialog (#15084)

* Improve voice dialog

* Improve scrolling and dialog size

* Align messages to bottom for better keyboard support

* Add send button

* Simplify label
This commit is contained in:
Paul Bottein 2023-01-13 22:20:29 +01:00 committed by GitHub
parent 207380d0da
commit 1d20d6979e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 167 additions and 78 deletions

View File

@ -1,6 +1,6 @@
/* eslint-disable lit/prefer-static-styles */ /* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiClose, mdiMicrophone } from "@mdi/js"; import { mdiClose, mdiMicrophone, mdiSend } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -56,7 +56,11 @@ export class HaVoiceCommandDialog extends LitElement {
@state() private _agentInfo?: AgentInfo; @state() private _agentInfo?: AgentInfo;
@query("ha-dialog", true) private _dialog!: HaDialog; @state() private _showSendButton = false;
@query("#scroll-container") private _scrollContainer!: HaDialog;
@query("#message-input") private _messageInput!: HaTextField;
private recognition!: SpeechRecognition; private recognition!: SpeechRecognition;
@ -64,10 +68,8 @@ export class HaVoiceCommandDialog extends LitElement {
public async showDialog(): Promise<void> { public async showDialog(): Promise<void> {
this._opened = true; this._opened = true;
if (SpeechRecognition) {
this._startListening();
}
this._agentInfo = await getAgentInfo(this.hass); this._agentInfo = await getAgentInfo(this.hass);
this._scrollMessagesBottom();
} }
public async closeDialog(): Promise<void> { public async closeDialog(): Promise<void> {
@ -83,9 +85,17 @@ export class HaVoiceCommandDialog extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-dialog open @closed=${this.closeDialog}> <ha-dialog
<div slot="heading" class="heading"> open
@closed=${this.closeDialog}
.heading=${this.hass.localize("ui.dialogs.voice_command.title")}
flexContent
>
<div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title">
${this.hass.localize("ui.dialogs.voice_command.title")}
</span>
<ha-icon-button <ha-icon-button
slot="navigationIcon" slot="navigationIcon"
dialogAction="cancel" dialogAction="cancel"
@ -94,62 +104,77 @@ export class HaVoiceCommandDialog extends LitElement {
></ha-icon-button> ></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>
<div> <div class="messages">
${this._agentInfo && this._agentInfo.onboarding <div class="messages-container" id="scroll-container">
? html` ${this._agentInfo && this._agentInfo.onboarding
<div class="onboarding"> ? html`
${this._agentInfo.onboarding.text} <div class="onboarding">
<div class="side-by-side" @click=${this._completeOnboarding}> ${this._agentInfo.onboarding.text}
<a <div
class="button" class="side-by-side"
href=${this._agentInfo.onboarding.url} @click=${this._completeOnboarding}
target="_blank"
rel="noreferrer"
><mwc-button unelevated
>${this.hass.localize("ui.common.yes")}!</mwc-button
></a
>
<mwc-button outlined
>${this.hass.localize("ui.common.no")}</mwc-button
> >
<a
class="button"
href=${this._agentInfo.onboarding.url}
target="_blank"
rel="noreferrer"
><mwc-button unelevated
>${this.hass.localize("ui.common.yes")}!</mwc-button
></a
>
<mwc-button outlined
>${this.hass.localize("ui.common.no")}</mwc-button
>
</div>
</div> </div>
`
: ""}
${this._conversation.map(
(message) => html`
<div class=${this._computeMessageClasses(message)}>
${message.text}
</div> </div>
` `
: ""} )}
${this._conversation.map( ${this.results
(message) => html` ? html`
<div class=${this._computeMessageClasses(message)}> <div class="message user">
${message.text} <span
</div> class=${classMap({
` interimTranscript: !this.results.final,
)} })}
${this.results >${this.results.transcript}</span
? html` >${!this.results.final ? "…" : ""}
<div class="message user"> </div>
<span `
class=${classMap({ : ""}
interimTranscript: !this.results.final, </div>
})}
>${this.results.transcript}</span
>${!this.results.final ? "…" : ""}
</div>
`
: ""}
</div> </div>
<div class="input" slot="primaryAction"> <div class="input" slot="primaryAction">
<ha-textfield <ha-textfield
id="message-input"
@keyup=${this._handleKeyUp} @keyup=${this._handleKeyUp}
.label=${this.hass.localize( @input=${this._handleInput}
`ui.dialogs.voice_command.${ .label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
SpeechRecognition ? "label_voice" : "label"
}`
)}
dialogInitialFocus dialogInitialFocus
iconTrailing iconTrailing
> >
${SpeechRecognition <span slot="trailingIcon">
? html` ${this._showSendButton
<span slot="trailingIcon"> ? html`
<ha-icon-button
class="listening-icon"
.path=${mdiSend}
@click=${this._handleSendMessage}
.label=${this.hass.localize(
"ui.dialogs.voice_command.send_text"
)}
>
</ha-icon-button>
`
: SpeechRecognition
? html`
${this.results ${this.results
? html` ? html`
<div class="bouncer"> <div class="bouncer">
@ -159,13 +184,17 @@ export class HaVoiceCommandDialog extends LitElement {
` `
: ""} : ""}
<ha-icon-button <ha-icon-button
class="listening-icon"
.path=${mdiMicrophone} .path=${mdiMicrophone}
@click=${this._toggleListening} @click=${this._toggleListening}
.label=${this.hass.localize(
"ui.dialogs.voice_command.start_listening"
)}
> >
</ha-icon-button> </ha-icon-button>
</span> `
` : ""}
: ""} </span>
</ha-textfield> </ha-textfield>
${this._agentInfo && this._agentInfo.attribution ${this._agentInfo && this._agentInfo.attribution
? html` ? html`
@ -209,6 +238,24 @@ export class HaVoiceCommandDialog extends LitElement {
if (ev.keyCode === 13 && input.value) { if (ev.keyCode === 13 && input.value) {
this._processText(input.value); this._processText(input.value);
input.value = ""; input.value = "";
this._showSendButton = false;
}
}
private _handleInput(ev: InputEvent) {
const value = (ev.target as HaTextField).value;
if (value && !this._showSendButton) {
this._showSendButton = true;
} else if (!value && this._showSendButton) {
this._showSendButton = false;
}
}
private _handleSendMessage() {
if (this._messageInput.value) {
this._processText(this._messageInput.value);
this._messageInput.value = "";
this._showSendButton = false;
} }
} }
@ -221,6 +268,7 @@ export class HaVoiceCommandDialog extends LitElement {
this.recognition = new SpeechRecognition(); this.recognition = new SpeechRecognition();
this.recognition.interimResults = true; this.recognition.interimResults = true;
this.recognition.lang = this.hass.language; this.recognition.lang = this.hass.language;
this.recognition.continuous = false;
this.recognition.addEventListener("start", () => { this.recognition.addEventListener("start", () => {
this.results = { this.results = {
@ -319,7 +367,13 @@ export class HaVoiceCommandDialog extends LitElement {
if (!this.results) { if (!this.results) {
this._startListening(); this._startListening();
} else { } else {
this.recognition!.stop(); this._stopListening();
}
}
private _stopListening() {
if (this.recognition) {
this.recognition.stop();
} }
} }
@ -340,7 +394,7 @@ export class HaVoiceCommandDialog extends LitElement {
} }
private _scrollMessagesBottom() { private _scrollMessagesBottom() {
this._dialog.scrollToPos(0, 99999); this._scrollContainer.scrollTo(0, 99999);
} }
private _computeMessageClasses(message: Message) { private _computeMessageClasses(message: Message) {
@ -351,7 +405,7 @@ export class HaVoiceCommandDialog extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-icon-button { ha-icon-button.listening-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
margin-right: -24px; margin-right: -24px;
margin-inline-end: -24px; margin-inline-end: -24px;
@ -359,7 +413,7 @@ export class HaVoiceCommandDialog extends LitElement {
direction: var(--direction); direction: var(--direction);
} }
ha-icon-button[active] { ha-icon-button.listening-icon[active] {
color: var(--primary-color); color: var(--primary-color);
} }
@ -367,21 +421,19 @@ export class HaVoiceCommandDialog extends LitElement {
--primary-action-button-flex: 1; --primary-action-button-flex: 1;
--secondary-action-button-flex: 0; --secondary-action-button-flex: 0;
--mdc-dialog-max-width: 450px; --mdc-dialog-max-width: 450px;
--mdc-dialog-max-height: 500px;
--dialog-content-padding: 0;
} }
ha-header-bar { ha-header-bar {
display: none; --mdc-theme-on-primary: var(--primary-text-color);
} --mdc-theme-primary: var(--mdc-theme-surface);
@media all and (max-width: 450px), all and (max-height: 500px) { display: flex;
ha-header-bar { flex-shrink: 0;
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
} }
ha-textfield { ha-textfield {
display: block; display: block;
overflow: hidden;
} }
a.button { a.button {
text-decoration: none; text-decoration: none;
@ -403,6 +455,25 @@ export class HaVoiceCommandDialog extends LitElement {
.attribution { .attribution {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.messages {
display: block;
height: 300px;
box-sizing: border-box;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.messages {
height: 100%;
}
}
.messages-container {
position: absolute;
bottom: 0px;
right: 0px;
left: 0px;
padding: 24px;
overflow-y: auto;
max-height: 100%;
}
.message { .message {
font-size: 18px; font-size: 18px;
clear: both; clear: both;

View File

@ -3,13 +3,13 @@ import "@material/mwc-list/mwc-list-item";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { import {
mdiCodeBraces, mdiCodeBraces,
mdiCommentProcessingOutline,
mdiDotsVertical, mdiDotsVertical,
mdiFileMultiple, mdiFileMultiple,
mdiFormatListBulletedTriangle, mdiFormatListBulletedTriangle,
mdiHelp, mdiHelp,
mdiHelpCircle, mdiHelpCircle,
mdiMagnify, mdiMagnify,
mdiMicrophone,
mdiPencil, mdiPencil,
mdiPlus, mdiPlus,
mdiRefresh, mdiRefresh,
@ -302,9 +302,9 @@ class HUIRoot extends LitElement {
? html` ? html`
<ha-icon-button <ha-icon-button
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.menu.start_conversation" "ui.panel.lovelace.menu.assist"
)} )}
.path=${mdiMicrophone} .path=${mdiCommentProcessingOutline}
@click=${this._showVoiceCommandDialog} @click=${this._showVoiceCommandDialog}
></ha-icon-button> ></ha-icon-button>
` `
@ -324,7 +324,7 @@ class HUIRoot extends LitElement {
? html` ? html`
<mwc-list-item <mwc-list-item
graphic="icon" graphic="icon"
@request-selected=${this._showQuickBar} @request-selected=${this._handleShowQuickBar}
> >
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.lovelace.menu.search" "ui.panel.lovelace.menu.search"
@ -343,15 +343,15 @@ class HUIRoot extends LitElement {
<mwc-list-item <mwc-list-item
graphic="icon" graphic="icon"
@request-selected=${this @request-selected=${this
._showVoiceCommandDialog} ._handleShowVoiceCommandDialog}
> >
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.lovelace.menu.start_conversation" "ui.panel.lovelace.menu.assist"
)} )}
<ha-svg-icon <ha-svg-icon
slot="graphic" slot="graphic"
.path=${mdiMicrophone} .path=${mdiCommentProcessingOutline}
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
` `
@ -711,6 +711,13 @@ class HUIRoot extends LitElement {
}); });
} }
private _handleShowQuickBar(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showQuickBar();
}
private _showQuickBar(): void { private _showQuickBar(): void {
showQuickBar(this, { showQuickBar(this, {
commandMode: false, commandMode: false,
@ -762,6 +769,15 @@ class HUIRoot extends LitElement {
navigate(`${this.route?.prefix}/hass-unused-entities`); navigate(`${this.route?.prefix}/hass-unused-entities`);
} }
private _handleShowVoiceCommandDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showVoiceCommandDialog();
}
private _showVoiceCommandDialog(): void { private _showVoiceCommandDialog(): void {
showVoiceCommandDialog(this); showVoiceCommandDialog(this);
} }

View File

@ -803,13 +803,15 @@
"nothing_found": "Nothing found!" "nothing_found": "Nothing found!"
}, },
"voice_command": { "voice_command": {
"title": "Assistant",
"did_not_hear": "Home Assistant did not hear anything", "did_not_hear": "Home Assistant did not hear anything",
"did_not_understand": "Didn't quite get that", "did_not_understand": "Didn't quite get that",
"found": "I found the following for you:", "found": "I found the following for you:",
"error": "Oops, an error has occurred", "error": "Oops, an error has occurred",
"how_can_i_help": "How can I help?", "how_can_i_help": "How can I help?",
"label": "Type a question and press 'Enter'", "input_label": "Enter a request",
"label_voice": "Type and press 'Enter' or tap the microphone to speak" "send_text": "Send text",
"start_listening": "Start listening"
}, },
"generic": { "generic": {
"cancel": "Cancel", "cancel": "Cancel",
@ -3856,7 +3858,7 @@
"configure_ui": "Edit Dashboard", "configure_ui": "Edit Dashboard",
"help": "Help", "help": "Help",
"search": "Search", "search": "Search",
"start_conversation": "Start conversation", "assist": "Assist",
"reload_resources": "Reload resources", "reload_resources": "Reload resources",
"exit_edit_mode": "Done", "exit_edit_mode": "Done",
"close": "Close" "close": "Close"