mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Migrate voice command dialog (#4150)
* Migrate voice command dialog * Cleanup * Correct types * Added animation when listening and we should talk back right? :'-) * Set recognition to english * Comments * Update on change of hass
This commit is contained in:
parent
12be2a9775
commit
46f5224e70
@ -122,6 +122,7 @@
|
|||||||
"@types/leaflet": "^1.4.3",
|
"@types/leaflet": "^1.4.3",
|
||||||
"@types/memoize-one": "4.1.0",
|
"@types/memoize-one": "4.1.0",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^5.2.6",
|
||||||
|
"@types/webspeechapi": "^0.0.29",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"copy-webpack-plugin": "^5.0.2",
|
"copy-webpack-plugin": "^5.0.2",
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
/** Return if a component is loaded. */
|
/** Return if a component is loaded. */
|
||||||
export default function isComponentLoaded(
|
export const isComponentLoaded = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component: string
|
component: string
|
||||||
): boolean {
|
): boolean => hass && hass.config.components.indexOf(component) !== -1;
|
||||||
return hass && hass.config.components.indexOf(component) !== -1;
|
|
||||||
}
|
|
||||||
|
@ -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`
|
|
||||||
<paper-icon-button
|
|
||||||
aria-label="Start conversation"
|
|
||||||
icon="hass:microphone"
|
|
||||||
hidden$="[[!canListen]]"
|
|
||||||
on-click="handleListenClick"
|
|
||||||
></paper-icon-button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
14
src/data/conversation.ts
Normal file
14
src/data/conversation.ts
Normal file
@ -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<ProcessResults> =>
|
||||||
|
hass.callApi("POST", "conversation/process", { text });
|
@ -9,7 +9,7 @@ import "./more-info/more-info-controls";
|
|||||||
import "./more-info/more-info-settings";
|
import "./more-info/more-info-settings";
|
||||||
|
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
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";
|
import DialogMixin from "../mixins/dialog-mixin";
|
||||||
|
|
||||||
|
@ -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`
|
|
||||||
<style include="paper-dialog-shared-styles">
|
|
||||||
iron-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon paper-icon-button {
|
|
||||||
height: 52px;
|
|
||||||
width: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interimTranscript {
|
|
||||||
color: darkgrey;
|
|
||||||
}
|
|
||||||
|
|
||||||
[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: scroll;
|
|
||||||
border-bottom-left-radius: 0px;
|
|
||||||
border-bottom-right-radius: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages {
|
|
||||||
max-height: 68vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<div class="messages" id="messages">
|
|
||||||
<template is="dom-repeat" items="[[_conversation]]" as="message">
|
|
||||||
<div class$="[[_computeMessageClasses(message)]]">
|
|
||||||
[[message.text]]
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[results]]">
|
|
||||||
<div class="messages">
|
|
||||||
<div class="message user">
|
|
||||||
<span>{{results.final}}</span>
|
|
||||||
<span class="interimTranscript">[[results.interim]]</span> …
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="icon" hidden$="[[results]]">
|
|
||||||
<paper-icon-button
|
|
||||||
icon="hass:text-to-speech"
|
|
||||||
on-click="startListening"
|
|
||||||
></paper-icon-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = "<Home Assistant did not hear anything>";
|
|
||||||
}
|
|
||||||
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);
|
|
@ -11,7 +11,7 @@ import "../../../components/ha-paper-dropdown-menu";
|
|||||||
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
|
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
|
||||||
|
|
||||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
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 { EventsMixin } from "../../../mixins/events-mixin";
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||||
|
@ -13,7 +13,7 @@ import "./controls/more-info-content";
|
|||||||
|
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
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 { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
|
||||||
import { EventsMixin } from "../../mixins/events-mixin";
|
import { EventsMixin } from "../../mixins/events-mixin";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
425
src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
Normal file
425
src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
Normal file
@ -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<void> {
|
||||||
|
this._opened = true;
|
||||||
|
if (SpeechRecognition) {
|
||||||
|
this._startListening();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-paper-dialog
|
||||||
|
with-backdrop
|
||||||
|
.opened=${this._opened}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<div class="messages" id="messages">
|
||||||
|
${this._conversation.map(
|
||||||
|
(message) => html`
|
||||||
|
<div class="${this._computeMessageClasses(message)}">
|
||||||
|
${message.text}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${this.results
|
||||||
|
? html`
|
||||||
|
<div class="messages">
|
||||||
|
<div class="message user">
|
||||||
|
<span
|
||||||
|
class=${classMap({
|
||||||
|
interimTranscript: !this.results.final,
|
||||||
|
})}
|
||||||
|
>${this.results.transcript}</span
|
||||||
|
>${!this.results.final ? "…" : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<paper-input
|
||||||
|
@keyup=${this._handleKeyUp}
|
||||||
|
label="${this.hass!.localize(
|
||||||
|
`ui.dialogs.voice_command.${
|
||||||
|
SpeechRecognition ? "label_voice" : "label"
|
||||||
|
}`
|
||||||
|
)}"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
${SpeechRecognition
|
||||||
|
? html`
|
||||||
|
<span suffix="" slot="suffix">
|
||||||
|
${this.results
|
||||||
|
? html`
|
||||||
|
<div class="bouncer">
|
||||||
|
<div class="double-bounce1"></div>
|
||||||
|
<div class="double-bounce2"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<paper-icon-button
|
||||||
|
.active=${Boolean(this.results)}
|
||||||
|
icon="hass:microphone"
|
||||||
|
@click=${this._toggleListening}
|
||||||
|
>
|
||||||
|
</paper-icon-button>
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</paper-input>
|
||||||
|
</div>
|
||||||
|
</ha-paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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: {},
|
||||||
|
});
|
||||||
|
};
|
@ -9,7 +9,7 @@ import "../../../resources/ha-style";
|
|||||||
|
|
||||||
import "../ha-config-section";
|
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 LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
|
|
||||||
import "./ha-config-name-form";
|
import "./ha-config-name-form";
|
||||||
|
@ -14,7 +14,7 @@ import "../../../components/ha-icon-next";
|
|||||||
import "../ha-config-section";
|
import "../ha-config-section";
|
||||||
import "./ha-config-navigation";
|
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 LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import "@polymer/iron-icon/iron-icon";
|
|||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
import "@polymer/paper-item/paper-item";
|
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-card";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { property, PropertyValues, customElement } from "lit-element";
|
import { property, PropertyValues, customElement } from "lit-element";
|
||||||
import "../../layouts/hass-loading-screen";
|
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 { HomeAssistant } from "../../types";
|
||||||
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
||||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||||
|
@ -222,7 +222,7 @@ class DialogPersonDetail extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||||
if (!(ev.detail as any).value) {
|
if (ev.detail.value) {
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import "../../../resources/ha-style";
|
|||||||
|
|
||||||
import "../ha-config-section";
|
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 LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -22,7 +22,7 @@ import scrollToTarget from "../../common/dom/scroll-to-target";
|
|||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../types";
|
import { HomeAssistant, Route } from "../../types";
|
||||||
import { navigate } from "../../common/navigate";
|
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")
|
@customElement("ha-panel-developer-tools")
|
||||||
class PanelDeveloperTools extends LitElement {
|
class PanelDeveloperTools extends LitElement {
|
||||||
|
@ -24,7 +24,6 @@ import "@polymer/paper-tabs/paper-tabs";
|
|||||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||||
|
|
||||||
import "../../layouts/ha-app-layout";
|
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-next";
|
||||||
import "../../components/ha-paper-icon-button-arrow-prev";
|
import "../../components/ha-paper-icon-button-arrow-prev";
|
||||||
import "../../components/ha-icon";
|
import "../../components/ha-icon";
|
||||||
@ -49,9 +48,12 @@ import { afterNextRender } from "../../common/util/render-status";
|
|||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import { loadLovelaceResources } from "./common/load-resources";
|
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 {
|
class HUIRoot extends LitElement {
|
||||||
@property() public hass?: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
@property() public lovelace?: Lovelace;
|
@property() public lovelace?: Lovelace;
|
||||||
@property() public columns?: number;
|
@property() public columns?: number;
|
||||||
@property() public narrow?: boolean;
|
@property() public narrow?: boolean;
|
||||||
@ -62,6 +64,10 @@ class HUIRoot extends LitElement {
|
|||||||
|
|
||||||
private _debouncedConfigChanged: () => void;
|
private _debouncedConfigChanged: () => void;
|
||||||
|
|
||||||
|
private _conversation = memoizeOne((_components) =>
|
||||||
|
isComponentLoaded(this.hass, "conversation")
|
||||||
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// The view can trigger a re-render when it knows that certain
|
// The view can trigger a re-render when it knows that certain
|
||||||
@ -161,9 +167,15 @@ class HUIRoot extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
></ha-menu-button>
|
></ha-menu-button>
|
||||||
<div main-title>${this.config.title || "Home Assistant"}</div>
|
<div main-title>${this.config.title || "Home Assistant"}</div>
|
||||||
<ha-start-voice-button
|
${this._conversation(this.hass.config.components)
|
||||||
.hass="${this.hass}"
|
? html`
|
||||||
></ha-start-voice-button>
|
<paper-icon-button
|
||||||
|
aria-label="Start conversation"
|
||||||
|
icon="hass:microphone"
|
||||||
|
@click=${this._showVoiceCommandDialog}
|
||||||
|
></paper-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
<paper-menu-button
|
<paper-menu-button
|
||||||
no-animations
|
no-animations
|
||||||
horizontal-align="right"
|
horizontal-align="right"
|
||||||
@ -546,6 +558,10 @@ class HUIRoot extends LitElement {
|
|||||||
ev.target.selected = null;
|
ev.target.selected = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _showVoiceCommandDialog(): void {
|
||||||
|
showVoiceCommandDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
private _handleHelp(): void {
|
private _handleHelp(): void {
|
||||||
window.open("https://www.home-assistant.io/lovelace/", "_blank");
|
window.open("https://www.home-assistant.io/lovelace/", "_blank");
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import "@polymer/iron-label/iron-label";
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import isComponentLoaded from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { pushSupported } from "../../components/ha-push-notifications-toggle";
|
import { pushSupported } from "../../components/ha-push-notifications-toggle";
|
||||||
|
|
||||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||||
|
@ -13,9 +13,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import "../../components/ha-start-voice-button";
|
|
||||||
import "../../components/ha-card";
|
import "../../components/ha-card";
|
||||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||||
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin LocalizeMixin
|
||||||
@ -72,10 +73,14 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
|
|||||||
narrow="[[narrow]]"
|
narrow="[[narrow]]"
|
||||||
></ha-menu-button>
|
></ha-menu-button>
|
||||||
<div main-title>[[localize('panel.shopping_list')]]</div>
|
<div main-title>[[localize('panel.shopping_list')]]</div>
|
||||||
<ha-start-voice-button
|
|
||||||
hass="[[hass]]"
|
<paper-icon-button
|
||||||
can-listen="{{canListen}}"
|
hidden$="[[!conversation]]"
|
||||||
></ha-start-voice-button>
|
aria-label="Start conversation"
|
||||||
|
icon="hass:microphone"
|
||||||
|
on-click="_showVoiceCommandDialog"
|
||||||
|
></paper-icon-button>
|
||||||
|
|
||||||
<paper-menu-button
|
<paper-menu-button
|
||||||
horizontal-align="right"
|
horizontal-align="right"
|
||||||
horizontal-offset="-5"
|
horizontal-offset="-5"
|
||||||
@ -131,7 +136,7 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
|
|||||||
</paper-icon-item>
|
</paper-icon-item>
|
||||||
</template>
|
</template>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
<div class="tip" hidden$="[[!canListen]]">
|
<div class="tip" hidden$="[[!conversation]]">
|
||||||
[[localize('ui.panel.shopping-list.microphone_tip')]]
|
[[localize('ui.panel.shopping-list.microphone_tip')]]
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -143,7 +148,10 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
|
|||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
narrow: Boolean,
|
narrow: Boolean,
|
||||||
canListen: Boolean,
|
conversation: {
|
||||||
|
type: Boolean,
|
||||||
|
computed: "_computeConversation(hass)",
|
||||||
|
},
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
value: [],
|
value: [],
|
||||||
@ -207,6 +215,14 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_computeConversation(hass) {
|
||||||
|
return isComponentLoaded(hass, "conversation");
|
||||||
|
}
|
||||||
|
|
||||||
|
_showVoiceCommandDialog() {
|
||||||
|
showVoiceCommandDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
_saveEdit(ev) {
|
_saveEdit(ev) {
|
||||||
const { index, item } = ev.model;
|
const { index, item } = ev.model;
|
||||||
const name = ev.target.value;
|
const name = ev.target.value;
|
||||||
|
@ -12,7 +12,6 @@ import "@polymer/paper-tabs/paper-tabs";
|
|||||||
import "../../components/ha-cards";
|
import "../../components/ha-cards";
|
||||||
import "../../components/ha-icon";
|
import "../../components/ha-icon";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import "../../components/ha-start-voice-button";
|
|
||||||
|
|
||||||
import "../../layouts/ha-app-layout";
|
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 computeLocationName from "../../common/config/location_name";
|
||||||
import NavigateMixin from "../../mixins/navigate-mixin";
|
import NavigateMixin from "../../mixins/navigate-mixin";
|
||||||
import { EventsMixin } from "../../mixins/events-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 DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||||
const ALWAYS_SHOW_DOMAIN = ["persistent_notification", "configurator"];
|
const ALWAYS_SHOW_DOMAIN = ["persistent_notification", "configurator"];
|
||||||
@ -72,7 +73,12 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
|||||||
<div main-title="">
|
<div main-title="">
|
||||||
[[computeTitle(views, defaultView, locationName)]]
|
[[computeTitle(views, defaultView, locationName)]]
|
||||||
</div>
|
</div>
|
||||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
<paper-icon-button
|
||||||
|
hidden$="[[!conversation]]"
|
||||||
|
aria-label="Start conversation"
|
||||||
|
icon="hass:microphone"
|
||||||
|
on-click="_showVoiceCommandDialog"
|
||||||
|
></paper-icon-button>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
|
|
||||||
<div sticky="" hidden$="[[areTabsHidden(views, showTabs)]]">
|
<div sticky="" hidden$="[[areTabsHidden(views, showTabs)]]">
|
||||||
@ -174,6 +180,11 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
|||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
conversation: {
|
||||||
|
type: Boolean,
|
||||||
|
computed: "_computeConversation(hass)",
|
||||||
|
},
|
||||||
|
|
||||||
locationName: {
|
locationName: {
|
||||||
type: String,
|
type: String,
|
||||||
value: "",
|
value: "",
|
||||||
@ -241,6 +252,14 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_computeConversation(hass) {
|
||||||
|
return isComponentLoaded(hass, "conversation");
|
||||||
|
}
|
||||||
|
|
||||||
|
_showVoiceCommandDialog() {
|
||||||
|
showVoiceCommandDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
areTabsHidden(views, showTabs) {
|
areTabsHidden(views, showTabs) {
|
||||||
return !views || !views.length || !showTabs;
|
return !views || !views.length || !showTabs;
|
||||||
}
|
}
|
||||||
|
@ -554,6 +554,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dialogs": {
|
"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 <Enter>",
|
||||||
|
"label_voice": "Type and press <Enter> or tap the microphone icon to speak"
|
||||||
|
},
|
||||||
"confirmation": {
|
"confirmation": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
@ -1698,7 +1704,7 @@
|
|||||||
"shopping-list": {
|
"shopping-list": {
|
||||||
"clear_completed": "Clear completed",
|
"clear_completed": "Clear completed",
|
||||||
"add_item": "Add item",
|
"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": {
|
"page-authorize": {
|
||||||
"initializing": "Initializing",
|
"initializing": "Initializing",
|
||||||
|
@ -2037,6 +2037,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/whatwg-url@^6.4.0":
|
||||||
version "6.4.0"
|
version "6.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"
|
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user