Fix mobile support for voice dialog (#4154)

* Fix mobile support for voice dialog

* Update ha-voice-command-dialog.ts

* typo

* Add extra data functions

* Start listening for choice

* Remove extra data logic
This commit is contained in:
Bram Kragten 2019-11-04 21:34:59 +01:00 committed by Paulus Schoutsen
parent da35c263d2
commit 5ca82fd39c
3 changed files with 221 additions and 196 deletions

View File

@ -1,5 +1,4 @@
import "@polymer/iron-icon/iron-icon"; import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-dialog-behavior/paper-dialog-shared-styles";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/dialog/ha-paper-dialog"; import "../../components/dialog/ha-paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
@ -13,16 +12,20 @@ import {
customElement, customElement,
query, query,
PropertyValues, PropertyValues,
TemplateResult,
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { processText } from "../../data/conversation"; import { processText } from "../../data/conversation";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { haStyleDialog } from "../../resources/styles";
// tslint:disable-next-line
import { PaperDialogScrollableElement } from "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
interface Message { interface Message {
who: string; who: string;
text: string; text?: string;
error?: boolean; error?: boolean;
} }
@ -57,7 +60,7 @@ export class HaVoiceCommandDialog extends LitElement {
}, },
]; ];
@property() private _opened = false; @property() private _opened = false;
@query("#messages") private messages!: HTMLDivElement; @query("#messages") private messages!: PaperDialogScrollableElement;
private recognition?: SpeechRecognition; private recognition?: SpeechRecognition;
public async showDialog(): Promise<void> { public async showDialog(): Promise<void> {
@ -67,74 +70,102 @@ export class HaVoiceCommandDialog extends LitElement {
} }
} }
protected render() { protected render(): TemplateResult {
// CSS custom property mixins only work in render https://github.com/Polymer/lit-element/issues/633
return html` return html`
<style>
paper-dialog-scrollable {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: auto;
max-height: 50vh !important;
}
}
paper-dialog-scrollable.can-scroll {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: touch;
max-height: 50vh !important;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog-scrollable {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: auto;
max-height: calc(100vh - 175px) !important;
}
}
paper-dialog-scrollable.can-scroll {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: touch;
max-height: calc(100vh - 175px) !important;
}
}
}
</style>
<ha-paper-dialog <ha-paper-dialog
with-backdrop with-backdrop
.opened=${this._opened} .opened=${this._opened}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
> >
<div class="content"> <paper-dialog-scrollable id="messages">
<div class="messages" id="messages"> ${this._conversation.map(
${this._conversation.map( (message) => html`
(message) => html` <div class="${this._computeMessageClasses(message)}">
<div class="${this._computeMessageClasses(message)}"> ${message.text}
${message.text} </div>
</div> `
` )}
)}
</div>
${this.results ${this.results
? html` ? html`
<div class="messages"> <div class="message user">
<div class="message user"> <span
<span class=${classMap({
class=${classMap({ interimTranscript: !this.results.final,
interimTranscript: !this.results.final, })}
})} >${this.results.transcript}</span
>${this.results.transcript}</span >${!this.results.final ? "…" : ""}
>${!this.results.final ? "…" : ""}
</div>
</div> </div>
` `
: ""} : ""}
<paper-input </paper-dialog-scrollable>
@keyup=${this._handleKeyUp} <paper-input
label="${this.hass!.localize( @keyup=${this._handleKeyUp}
`ui.dialogs.voice_command.${ label="${this.hass!.localize(
SpeechRecognition ? "label_voice" : "label" `ui.dialogs.voice_command.${
}` SpeechRecognition ? "label_voice" : "label"
)}" }`
autofocus )}"
> autofocus
${SpeechRecognition >
? html` ${SpeechRecognition
<span suffix="" slot="suffix"> ? html`
${this.results <span suffix="" slot="suffix">
? html` ${this.results
<div class="bouncer"> ? html`
<div class="double-bounce1"></div> <div class="bouncer">
<div class="double-bounce2"></div> <div class="double-bounce1"></div>
</div> <div class="double-bounce2"></div>
` </div>
: ""} `
<paper-icon-button : ""}
.active=${Boolean(this.results)} <paper-icon-button
icon="hass:microphone" .active=${Boolean(this.results)}
@click=${this._toggleListening} icon="hass:microphone"
> @click=${this._toggleListening}
</paper-icon-button> >
</span> </paper-icon-button>
` </span>
: ""} `
</paper-input> : ""}
</div> </paper-input>
</ha-paper-dialog> </ha-paper-dialog>
`; `;
} }
protected firstUpdated(changedPros: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedPros); super.updated(changedProps);
this._conversation = [ this._conversation = [
{ {
who: "hass", who: "hass",
@ -143,9 +174,9 @@ export class HaVoiceCommandDialog extends LitElement {
]; ];
} }
protected updated(changedPros: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedPros); super.updated(changedProps);
if (changedPros.has("_conversation")) { if (changedProps.has("_conversation") || changedProps.has("results")) {
this._scrollMessagesBottom(); this._scrollMessagesBottom();
} }
} }
@ -157,9 +188,6 @@ export class HaVoiceCommandDialog extends LitElement {
private _handleKeyUp(ev: KeyboardEvent) { private _handleKeyUp(ev: KeyboardEvent) {
const input = ev.target as PaperInputElement; const input = ev.target as PaperInputElement;
if (ev.keyCode === 13 && input.value) { if (ev.keyCode === 13 && input.value) {
if (this.recognition) {
this.recognition!.abort();
}
this._processText(input.value); this._processText(input.value);
input.value = ""; input.value = "";
} }
@ -219,13 +247,23 @@ export class HaVoiceCommandDialog extends LitElement {
} }
private async _processText(text: string) { private async _processText(text: string) {
if (this.recognition) {
this.recognition.abort();
}
this._addMessage({ who: "user", text }); this._addMessage({ who: "user", text });
const message: Message = {
who: "hass",
text: "…",
};
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(message);
try { try {
const response = await processText(this.hass, text); const response = await processText(this.hass, text);
this._addMessage({ const plain = response.speech.plain;
who: "hass", message.text = plain.speech;
text: response.speech.plain.speech,
}); this.requestUpdate("_conversation");
if (speechSynthesis) { if (speechSynthesis) {
const speech = new SpeechSynthesisUtterance( const speech = new SpeechSynthesisUtterance(
response.speech.plain.speech response.speech.plain.speech
@ -234,8 +272,9 @@ export class HaVoiceCommandDialog extends LitElement {
speechSynthesis.speak(speech); speechSynthesis.speak(speech);
} }
} catch { } catch {
this._conversation.slice(-1).pop()!.error = true; message.text = this.hass.localize("ui.dialogs.voice_command.error");
this.requestUpdate(); message.error = true;
this.requestUpdate("_conversation");
} }
} }
@ -264,9 +303,8 @@ export class HaVoiceCommandDialog extends LitElement {
} }
private _scrollMessagesBottom() { private _scrollMessagesBottom() {
this.messages.scrollTop = this.messages.scrollHeight; this.messages.scrollTarget.scrollTop = this.messages.scrollTarget.scrollHeight;
if (this.messages.scrollTarget.scrollTop === 0) {
if (this.messages.scrollTop === 0) {
fireEvent(this.messages, "iron-resize"); fireEvent(this.messages, "iron-resize");
} }
} }
@ -278,143 +316,128 @@ export class HaVoiceCommandDialog extends LitElement {
} }
} }
private _computeMessageClasses(message) { private _computeMessageClasses(message: Message) {
return "message " + message.who + (message.error ? " error" : ""); return `message ${message.who} ${message.error ? " error" : ""}`;
} }
static get styles(): CSSResult { static get styles(): CSSResult[] {
return css` return [
paper-icon-button { haStyleDialog,
color: var(--secondary-text-color); css`
}
paper-icon-button[active] {
color: var(--primary-color);
}
.content {
width: 450px;
min-height: 80px;
font-size: 18px;
padding: 16px;
}
.messages {
max-height: 50vh;
overflow: auto;
}
.messages::after {
content: "";
clear: both;
display: block;
}
.message {
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
}
.message.user {
margin-left: 24px;
float: right;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--primary-text-color);
}
.message.hass {
margin-right: 24px;
float: left;
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.message.error {
background-color: var(--google-red-500);
color: var(--text-primary-color);
}
.interimTranscript {
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
:host {
border-radius: 2px;
}
@media all and (max-width: 450px) {
:host { :host {
margin: 0; z-index: 103;
width: 100%; }
max-height: calc(100% - 64px);
position: fixed !important; paper-icon-button {
bottom: 0px; color: var(--secondary-text-color);
left: 0px; }
right: 0px;
overflow: auto; paper-icon-button[active] {
border-bottom-left-radius: 0px; color: var(--primary-color);
}
paper-input {
margin: 0 0 16px 0;
}
ha-paper-dialog {
width: 450px;
}
.message {
font-size: 18px;
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
}
.message.user {
margin-left: 24px;
float: right;
text-align: right;
border-bottom-right-radius: 0px; border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--primary-text-color);
} }
.messages { .message.hass {
max-height: 68vh; margin-right: 24px;
float: left;
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
} }
}
.bouncer { .message a {
width: 40px; color: var(--text-primary-color);
height: 40px;
position: absolute;
top: 0;
}
.double-bounce1,
.double-bounce2 {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-color);
opacity: 0.2;
position: absolute;
top: 0;
left: 0;
-webkit-animation: sk-bounce 2s infinite ease-in-out;
animation: sk-bounce 2s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0);
} }
50% {
-webkit-transform: scale(1); .message img {
width: 100%;
border-radius: 10px;
} }
}
@keyframes sk-bounce { .message.error {
0%, background-color: var(--google-red-500);
100% { color: var(--text-primary-color);
transform: scale(0);
-webkit-transform: scale(0);
} }
50% {
transform: scale(1); .interimTranscript {
-webkit-transform: scale(1); color: var(--secondary-text-color);
} }
}
`; .bouncer {
width: 40px;
height: 40px;
position: absolute;
top: 0;
}
.double-bounce1,
.double-bounce2 {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-color);
opacity: 0.2;
position: absolute;
top: 0;
left: 0;
-webkit-animation: sk-bounce 2s infinite ease-in-out;
animation: sk-bounce 2s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0);
}
50% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0);
-webkit-transform: scale(0);
}
50% {
transform: scale(1);
-webkit-transform: scale(1);
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.message {
font-size: 16px;
}
}
`,
];
} }
} }

View File

@ -163,7 +163,7 @@ export class HuiDialogEditCard extends LitElement {
} }
} }
@media all and (min-width: 660px) { @media all and (min-width: 850px) {
ha-paper-dialog { ha-paper-dialog {
width: 845px; width: 845px;
} }

View File

@ -556,6 +556,8 @@
"dialogs": { "dialogs": {
"voice_command": { "voice_command": {
"did_not_hear": "Home Assistant did not hear anything", "did_not_hear": "Home Assistant did not hear anything",
"found": "I found the following for you:",
"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>", "label": "Type a question and press <Enter>",
"label_voice": "Type and press <Enter> or tap the microphone icon to speak" "label_voice": "Type and press <Enter> or tap the microphone icon to speak"