diff --git a/package.json b/package.json index 92e12babb0..1ef24fb8f0 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "@types/leaflet": "^1.4.3", "@types/memoize-one": "4.1.0", "@types/mocha": "^5.2.6", + "@types/webspeechapi": "^0.0.29", "babel-loader": "^8.0.5", "chai": "^4.2.0", "copy-webpack-plugin": "^5.0.2", diff --git a/src/common/config/is_component_loaded.ts b/src/common/config/is_component_loaded.ts index c8535cda6b..6275d705dc 100644 --- a/src/common/config/is_component_loaded.ts +++ b/src/common/config/is_component_loaded.ts @@ -1,9 +1,7 @@ import { HomeAssistant } from "../../types"; /** Return if a component is loaded. */ -export default function isComponentLoaded( +export const isComponentLoaded = ( hass: HomeAssistant, component: string -): boolean { - return hass && hass.config.components.indexOf(component) !== -1; -} +): boolean => hass && hass.config.components.indexOf(component) !== -1; diff --git a/src/components/ha-start-voice-button.js b/src/components/ha-start-voice-button.js deleted file mode 100644 index 7f04bd49c8..0000000000 --- a/src/components/ha-start-voice-button.js +++ /dev/null @@ -1,56 +0,0 @@ -import "@polymer/paper-icon-button/paper-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import { EventsMixin } from "../mixins/events-mixin"; - -import isComponentLoaded from "../common/config/is_component_loaded"; -import { fireEvent } from "../common/dom/fire_event"; - -/* - * @appliesMixin EventsMixin - */ -class HaStartVoiceButton extends EventsMixin(PolymerElement) { - static get template() { - return html` - - `; - } - - static get properties() { - return { - hass: { - type: Object, - value: null, - }, - - canListen: { - type: Boolean, - computed: "computeCanListen(hass)", - notify: true, - }, - }; - } - - computeCanListen(hass) { - return ( - "webkitSpeechRecognition" in window && - isComponentLoaded(hass, "conversation") - ); - } - - handleListenClick() { - fireEvent(this, "show-dialog", { - dialogImport: () => - import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog"), - dialogTag: "ha-voice-command-dialog", - }); - } -} - -customElements.define("ha-start-voice-button", HaStartVoiceButton); diff --git a/src/data/conversation.ts b/src/data/conversation.ts new file mode 100644 index 0000000000..8a3bf388e8 --- /dev/null +++ b/src/data/conversation.ts @@ -0,0 +1,14 @@ +import { HomeAssistant } from "../types"; + +interface ProcessResults { + card: { [key: string]: { [key: string]: string } }; + speech: { + [SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string } + }; +} + +export const processText = ( + hass: HomeAssistant, + text: string +): Promise => + hass.callApi("POST", "conversation/process", { text }); diff --git a/src/dialogs/ha-more-info-dialog.js b/src/dialogs/ha-more-info-dialog.js index 131132c535..1d95816055 100644 --- a/src/dialogs/ha-more-info-dialog.js +++ b/src/dialogs/ha-more-info-dialog.js @@ -9,7 +9,7 @@ import "./more-info/more-info-controls"; import "./more-info/more-info-settings"; import { computeStateDomain } from "../common/entity/compute_state_domain"; -import isComponentLoaded from "../common/config/is_component_loaded"; +import { isComponentLoaded } from "../common/config/is_component_loaded"; import DialogMixin from "../mixins/dialog-mixin"; diff --git a/src/dialogs/ha-voice-command-dialog.js b/src/dialogs/ha-voice-command-dialog.js deleted file mode 100644 index 911934784e..0000000000 --- a/src/dialogs/ha-voice-command-dialog.js +++ /dev/null @@ -1,266 +0,0 @@ -import "@polymer/iron-icon/iron-icon"; -import "@polymer/paper-dialog-behavior/paper-dialog-shared-styles"; -import "@polymer/paper-icon-button/paper-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import DialogMixin from "../mixins/dialog-mixin"; - -/* - * @appliesMixin DialogMixin - */ -class HaVoiceCommandDialog extends DialogMixin(PolymerElement) { - static get template() { - return html` - - -
-
- -
- -
- -
-
- `; - } - - static get properties() { - return { - hass: Object, - results: { - type: Object, - value: null, - observer: "_scrollMessagesBottom", - }, - - _conversation: { - type: Array, - value: function() { - return [{ who: "hass", text: "How can I help?" }]; - }, - observer: "_scrollMessagesBottom", - }, - }; - } - - static get observers() { - return ["dialogOpenChanged(opened)"]; - } - - showDialog() { - this.opened = true; - } - - initRecognition() { - /* eslint-disable new-cap */ - this.recognition = new webkitSpeechRecognition(); - /* eslint-enable new-cap */ - - this.recognition.onstart = function() { - this.results = { - final: "", - interim: "", - }; - }.bind(this); - this.recognition.onerror = function() { - this.recognition.abort(); - var text = this.results.final || this.results.interim; - this.results = null; - if (text === "") { - text = ""; - } - this.push("_conversation", { who: "user", text: text, error: true }); - }.bind(this); - this.recognition.onend = function() { - // Already handled by onerror - if (this.results == null) { - return; - } - var text = this.results.final || this.results.interim; - this.results = null; - this.push("_conversation", { who: "user", text: text }); - - this.hass.callApi("post", "conversation/process", { text: text }).then( - function(response) { - this.push("_conversation", { - who: "hass", - text: response.speech.plain.speech, - }); - }.bind(this), - function() { - this.set( - ["_conversation", this._conversation.length - 1, "error"], - true - ); - }.bind(this) - ); - }.bind(this); - - this.recognition.onresult = function(event) { - var oldResults = this.results; - var finalTranscript = ""; - var interimTranscript = ""; - - for (var ind = event.resultIndex; ind < event.results.length; ind++) { - if (event.results[ind].isFinal) { - finalTranscript += event.results[ind][0].transcript; - } else { - interimTranscript += event.results[ind][0].transcript; - } - } - - this.results = { - interim: interimTranscript, - final: oldResults.final + finalTranscript, - }; - }.bind(this); - } - - startListening() { - if (!this.recognition) { - this.initRecognition(); - } - - this.results = { - interim: "", - final: "", - }; - this.recognition.start(); - } - - _scrollMessagesBottom() { - setTimeout(() => { - this.$.messages.scrollTop = this.$.messages.scrollHeight; - - if (this.$.messages.scrollTop !== 0) { - this.$.dialog.fire("iron-resize"); - } - }, 10); - } - - dialogOpenChanged(newVal) { - if (newVal) { - this.startListening(); - } else if (!newVal && this.results) { - this.recognition.abort(); - } - } - - _computeMessageClasses(message) { - return "message " + message.who + (message.error ? " error" : ""); - } -} - -customElements.define("ha-voice-command-dialog", HaVoiceCommandDialog); diff --git a/src/dialogs/more-info/controls/more-info-media_player.js b/src/dialogs/more-info/controls/more-info-media_player.js index b56cda1b06..c914e84ea7 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.js +++ b/src/dialogs/more-info/controls/more-info-media_player.js @@ -11,7 +11,7 @@ import "../../../components/ha-paper-dropdown-menu"; import HassMediaPlayerEntity from "../../../util/hass-media-player-model"; import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import isComponentLoaded from "../../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; diff --git a/src/dialogs/more-info/more-info-controls.js b/src/dialogs/more-info/more-info-controls.js index 2e280e5191..2f1ca302b8 100644 --- a/src/dialogs/more-info/more-info-controls.js +++ b/src/dialogs/more-info/more-info-controls.js @@ -13,7 +13,7 @@ import "./controls/more-info-content"; import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; -import isComponentLoaded from "../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; import { EventsMixin } from "../../mixins/events-mixin"; import { computeRTL } from "../../common/util/compute_rtl"; diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts new file mode 100644 index 0000000000..188e55501f --- /dev/null +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -0,0 +1,425 @@ +import "@polymer/iron-icon/iron-icon"; +import "@polymer/paper-dialog-behavior/paper-dialog-shared-styles"; +import "@polymer/paper-icon-button/paper-icon-button"; +import "../../components/dialog/ha-paper-dialog"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; + +import { + LitElement, + html, + property, + CSSResult, + css, + customElement, + query, + PropertyValues, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import { processText } from "../../data/conversation"; +import { classMap } from "lit-html/directives/class-map"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; + +interface Message { + who: string; + text: string; + error?: boolean; +} + +interface Results { + transcript: string; + final: boolean; +} + +/* tslint:disable */ +// @ts-ignore +window.SpeechRecognition = + // @ts-ignore + window.SpeechRecognition || window.webkitSpeechRecognition; +// @ts-ignore +window.SpeechGrammarList = + // @ts-ignore + window.SpeechGrammarList || window.webkitSpeechGrammarList; +// @ts-ignore +window.SpeechRecognitionEvent = + // @ts-ignore + window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent; +/* tslint:enable */ + +@customElement("ha-voice-command-dialog") +export class HaVoiceCommandDialog extends LitElement { + @property() public hass!: HomeAssistant; + @property() public results: Results | null = null; + @property() private _conversation: Message[] = [ + { + who: "hass", + text: "", + }, + ]; + @property() private _opened = false; + @query("#messages") private messages!: HTMLDivElement; + private recognition?: SpeechRecognition; + + public async showDialog(): Promise { + this._opened = true; + if (SpeechRecognition) { + this._startListening(); + } + } + + protected render() { + return html` + +
+
+ ${this._conversation.map( + (message) => html` +
+ ${message.text} +
+ ` + )} +
+ ${this.results + ? html` +
+
+ ${this.results.transcript}${!this.results.final ? "…" : ""} +
+
+ ` + : ""} + + ${SpeechRecognition + ? html` + + ${this.results + ? html` +
+
+
+
+ ` + : ""} + + +
+ ` + : ""} +
+
+
+ `; + } + + protected firstUpdated(changedPros: PropertyValues) { + super.updated(changedPros); + this._conversation = [ + { + who: "hass", + text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"), + }, + ]; + } + + protected updated(changedPros: PropertyValues) { + super.updated(changedPros); + if (changedPros.has("_conversation")) { + this._scrollMessagesBottom(); + } + } + + private _addMessage(message: Message) { + this._conversation = [...this._conversation, message]; + } + + private _handleKeyUp(ev: KeyboardEvent) { + const input = ev.target as PaperInputElement; + if (ev.keyCode === 13 && input.value) { + if (this.recognition) { + this.recognition!.abort(); + } + this._processText(input.value); + input.value = ""; + } + } + + private _initRecognition() { + this.recognition = new SpeechRecognition(); + this.recognition.interimResults = true; + this.recognition.lang = "en-US"; + + this.recognition!.onstart = () => { + this.results = { + final: false, + transcript: "", + }; + }; + this.recognition!.onerror = (event) => { + this.recognition!.abort(); + if (event.error !== "aborted") { + const text = + this.results && this.results.transcript + ? this.results.transcript + : `<${this.hass.localize( + "ui.dialogs.voice_command.did_not_hear" + )}>`; + this._addMessage({ who: "user", text, error: true }); + } + this.results = null; + }; + this.recognition!.onend = () => { + // Already handled by onerror + if (this.results == null) { + return; + } + const text = this.results.transcript; + this.results = null; + if (text) { + this._processText(text); + } else { + this._addMessage({ + who: "user", + text: `<${this.hass.localize( + "ui.dialogs.voice_command.did_not_hear" + )}>`, + error: true, + }); + } + }; + + this.recognition!.onresult = (event) => { + const result = event.results[0]; + this.results = { + transcript: result[0].transcript, + final: result.isFinal, + }; + }; + } + + private async _processText(text: string) { + this._addMessage({ who: "user", text }); + try { + const response = await processText(this.hass, text); + this._addMessage({ + who: "hass", + text: response.speech.plain.speech, + }); + if (speechSynthesis) { + const speech = new SpeechSynthesisUtterance( + response.speech.plain.speech + ); + speech.lang = "en-US"; + speechSynthesis.speak(speech); + } + } catch { + this._conversation.slice(-1).pop()!.error = true; + this.requestUpdate(); + } + } + + private _toggleListening() { + if (!this.results) { + this._startListening(); + } else { + this.recognition!.stop(); + } + } + + private _startListening() { + if (!this.recognition) { + this._initRecognition(); + } + + if (this.results) { + return; + } + + this.results = { + transcript: "", + final: false, + }; + this.recognition!.start(); + } + + private _scrollMessagesBottom() { + this.messages.scrollTop = this.messages.scrollHeight; + + if (this.messages.scrollTop === 0) { + fireEvent(this.messages, "iron-resize"); + } + } + + private _openedChanged(ev: CustomEvent) { + this._opened = ev.detail.value; + if (!this._opened && this.recognition) { + this.recognition.abort(); + } + } + + private _computeMessageClasses(message) { + return "message " + message.who + (message.error ? " error" : ""); + } + + static get styles(): CSSResult { + return css` + paper-icon-button { + color: var(--secondary-text-color); + } + + 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 { + margin: 0; + width: 100%; + max-height: calc(100% - 64px); + + position: fixed !important; + bottom: 0px; + left: 0px; + right: 0px; + overflow: auto; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + } + + .messages { + max-height: 68vh; + } + } + + .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); + } + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-command-dialog": HaVoiceCommandDialog; + } +} diff --git a/src/dialogs/voice-command-dialog/show-ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/show-ha-voice-command-dialog.ts new file mode 100644 index 0000000000..cfa2200a03 --- /dev/null +++ b/src/dialogs/voice-command-dialog/show-ha-voice-command-dialog.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +const loadVoiceCommandDialog = () => + import(/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog"); + +export const showVoiceCommandDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-voice-command-dialog", + dialogImport: loadVoiceCommandDialog, + dialogParams: {}, + }); +}; diff --git a/src/panels/config/core/ha-config-section-core.js b/src/panels/config/core/ha-config-section-core.js index 8112846cdb..04be1e7684 100644 --- a/src/panels/config/core/ha-config-section-core.js +++ b/src/panels/config/core/ha-config-section-core.js @@ -9,7 +9,7 @@ import "../../../resources/ha-style"; import "../ha-config-section"; -import isComponentLoaded from "../../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import LocalizeMixin from "../../../mixins/localize-mixin"; import "./ha-config-name-form"; diff --git a/src/panels/config/dashboard/ha-config-dashboard.js b/src/panels/config/dashboard/ha-config-dashboard.js index e47377fa84..50c28acaf8 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.js +++ b/src/panels/config/dashboard/ha-config-dashboard.js @@ -14,7 +14,7 @@ import "../../../components/ha-icon-next"; import "../ha-config-section"; import "./ha-config-navigation"; -import isComponentLoaded from "../../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import LocalizeMixin from "../../../mixins/localize-mixin"; import NavigateMixin from "../../../mixins/navigate-mixin"; diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index 09fd1770dc..ed64269965 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -2,7 +2,7 @@ import "@polymer/iron-icon/iron-icon"; import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item"; -import isComponentLoaded from "../../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 6998abdd73..ea57036028 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -1,6 +1,6 @@ import { property, PropertyValues, customElement } from "lit-element"; import "../../layouts/hass-loading-screen"; -import isComponentLoaded from "../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { HomeAssistant } from "../../types"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { listenMediaQuery } from "../../common/dom/media_query"; diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index defc96144b..bbb0c73bd1 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -222,7 +222,7 @@ class DialogPersonDetail extends LitElement { } private _openedChanged(ev: PolymerChangedEvent): void { - if (!(ev.detail as any).value) { + if (ev.detail.value) { this._params = undefined; } } diff --git a/src/panels/config/server_control/ha-config-section-server-control.js b/src/panels/config/server_control/ha-config-section-server-control.js index 6382de6c5e..34df71a399 100644 --- a/src/panels/config/server_control/ha-config-section-server-control.js +++ b/src/panels/config/server_control/ha-config-section-server-control.js @@ -9,7 +9,7 @@ import "../../../resources/ha-style"; import "../ha-config-section"; -import isComponentLoaded from "../../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import LocalizeMixin from "../../../mixins/localize-mixin"; /* diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts index 9e22c2b97f..f8cc681dde 100644 --- a/src/panels/developer-tools/ha-panel-developer-tools.ts +++ b/src/panels/developer-tools/ha-panel-developer-tools.ts @@ -22,7 +22,7 @@ import scrollToTarget from "../../common/dom/scroll-to-target"; import { haStyle } from "../../resources/styles"; import { HomeAssistant, Route } from "../../types"; import { navigate } from "../../common/navigate"; -import isComponentLoaded from "../../common/config/is_component_loaded"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; @customElement("ha-panel-developer-tools") class PanelDeveloperTools extends LitElement { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index f1dce64914..629de76eb8 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -24,7 +24,6 @@ import "@polymer/paper-tabs/paper-tabs"; import scrollToTarget from "../../common/dom/scroll-to-target"; import "../../layouts/ha-app-layout"; -import "../../components/ha-start-voice-button"; import "../../components/ha-paper-icon-button-arrow-next"; import "../../components/ha-paper-icon-button-arrow-prev"; import "../../components/ha-icon"; @@ -49,9 +48,12 @@ import { afterNextRender } from "../../common/util/render-status"; import { haStyle } from "../../resources/styles"; import { computeRTLDirection } from "../../common/util/compute_rtl"; import { loadLovelaceResources } from "./common/load-resources"; +import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import memoizeOne from "memoize-one"; class HUIRoot extends LitElement { - @property() public hass?: HomeAssistant; + @property() public hass!: HomeAssistant; @property() public lovelace?: Lovelace; @property() public columns?: number; @property() public narrow?: boolean; @@ -62,6 +64,10 @@ class HUIRoot extends LitElement { private _debouncedConfigChanged: () => void; + private _conversation = memoizeOne((_components) => + isComponentLoaded(this.hass, "conversation") + ); + constructor() { super(); // The view can trigger a re-render when it knows that certain @@ -161,9 +167,15 @@ class HUIRoot extends LitElement { .narrow=${this.narrow} >
${this.config.title || "Home Assistant"}
- + ${this._conversation(this.hass.config.components) + ? html` + + ` + : ""}
[[localize('panel.shopping_list')]]
- + + + -
+
[[localize('ui.panel.shopping-list.microphone_tip')]]
@@ -143,7 +148,10 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) { return { hass: Object, narrow: Boolean, - canListen: Boolean, + conversation: { + type: Boolean, + computed: "_computeConversation(hass)", + }, items: { type: Array, value: [], @@ -207,6 +215,14 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) { } } + _computeConversation(hass) { + return isComponentLoaded(hass, "conversation"); + } + + _showVoiceCommandDialog() { + showVoiceCommandDialog(this); + } + _saveEdit(ev) { const { index, item } = ev.model; const name = ev.target.value; diff --git a/src/panels/states/ha-panel-states.js b/src/panels/states/ha-panel-states.js index 39a1a64d5d..34dda5d87a 100644 --- a/src/panels/states/ha-panel-states.js +++ b/src/panels/states/ha-panel-states.js @@ -12,7 +12,6 @@ import "@polymer/paper-tabs/paper-tabs"; import "../../components/ha-cards"; import "../../components/ha-icon"; import "../../components/ha-menu-button"; -import "../../components/ha-start-voice-button"; import "../../layouts/ha-app-layout"; @@ -23,6 +22,8 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain"; import computeLocationName from "../../common/config/location_name"; import NavigateMixin from "../../mixins/navigate-mixin"; import { EventsMixin } from "../../mixins/events-mixin"; +import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; const ALWAYS_SHOW_DOMAIN = ["persistent_notification", "configurator"]; @@ -72,7 +73,12 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
[[computeTitle(views, defaultView, locationName)]]
- +
@@ -174,6 +180,11 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) { value: 1, }, + conversation: { + type: Boolean, + computed: "_computeConversation(hass)", + }, + locationName: { type: String, value: "", @@ -241,6 +252,14 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) { ); } + _computeConversation(hass) { + return isComponentLoaded(hass, "conversation"); + } + + _showVoiceCommandDialog() { + showVoiceCommandDialog(this); + } + areTabsHidden(views, showTabs) { return !views || !views.length || !showTabs; } diff --git a/src/translations/en.json b/src/translations/en.json index c843d728d8..a4d67a2193 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -554,6 +554,12 @@ } }, "dialogs": { + "voice_command": { + "did_not_hear": "Home Assistant did not hear anything", + "how_can_i_help": "How can I help?", + "label": "Type a question and press ", + "label_voice": "Type and press or tap the microphone icon to speak" + }, "confirmation": { "cancel": "Cancel", "ok": "OK", @@ -1698,7 +1704,7 @@ "shopping-list": { "clear_completed": "Clear completed", "add_item": "Add item", - "microphone_tip": "Tap the microphone on the top right and say “Add candy to my shopping list”" + "microphone_tip": "Tap the microphone on the top right and say or type “Add candy to my shopping list”" }, "page-authorize": { "initializing": "Initializing", diff --git a/yarn.lock b/yarn.lock index 77ee588941..bd5bb940c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2037,6 +2037,11 @@ dependencies: "@types/node" "*" +"@types/webspeechapi@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/webspeechapi/-/webspeechapi-0.0.29.tgz#8f3c6b31b779df7a9bbac7f89acfce0c3bcb1972" + integrity sha512-AYEhqEJLdR08YPBOwYa73IHTiGU4DdngbKbtZdW+bzuM7s8LzKBed0Fwgl/a3oMqMY227qOT+3Lpr5A0WSmm+A== + "@types/whatwg-url@^6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"