mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +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/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",
|
||||
|
@ -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;
|
||||
|
@ -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 { 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";
|
||||
|
||||
|
@ -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 { 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";
|
||||
|
@ -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";
|
||||
|
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 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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -222,7 +222,7 @@ class DialogPersonDetail extends LitElement {
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||
if (!(ev.detail as any).value) {
|
||||
if (ev.detail.value) {
|
||||
this._params = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
/*
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
></ha-menu-button>
|
||||
<div main-title>${this.config.title || "Home Assistant"}</div>
|
||||
<ha-start-voice-button
|
||||
.hass="${this.hass}"
|
||||
></ha-start-voice-button>
|
||||
${this._conversation(this.hass.config.components)
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label="Start conversation"
|
||||
icon="hass:microphone"
|
||||
@click=${this._showVoiceCommandDialog}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
@ -546,6 +558,10 @@ class HUIRoot extends LitElement {
|
||||
ev.target.selected = null;
|
||||
}
|
||||
|
||||
private _showVoiceCommandDialog(): void {
|
||||
showVoiceCommandDialog(this);
|
||||
}
|
||||
|
||||
private _handleHelp(): void {
|
||||
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 { 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 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 "../../components/ha-menu-button";
|
||||
import "../../components/ha-start-voice-button";
|
||||
import "../../components/ha-card";
|
||||
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
|
||||
@ -72,10 +73,14 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
|
||||
narrow="[[narrow]]"
|
||||
></ha-menu-button>
|
||||
<div main-title>[[localize('panel.shopping_list')]]</div>
|
||||
<ha-start-voice-button
|
||||
hass="[[hass]]"
|
||||
can-listen="{{canListen}}"
|
||||
></ha-start-voice-button>
|
||||
|
||||
<paper-icon-button
|
||||
hidden$="[[!conversation]]"
|
||||
aria-label="Start conversation"
|
||||
icon="hass:microphone"
|
||||
on-click="_showVoiceCommandDialog"
|
||||
></paper-icon-button>
|
||||
|
||||
<paper-menu-button
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
@ -131,7 +136,7 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
|
||||
</paper-icon-item>
|
||||
</template>
|
||||
</ha-card>
|
||||
<div class="tip" hidden$="[[!canListen]]">
|
||||
<div class="tip" hidden$="[[!conversation]]">
|
||||
[[localize('ui.panel.shopping-list.microphone_tip')]]
|
||||
</div>
|
||||
</div>
|
||||
@ -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;
|
||||
|
@ -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)) {
|
||||
<div main-title="">
|
||||
[[computeTitle(views, defaultView, locationName)]]
|
||||
</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>
|
||||
|
||||
<div sticky="" hidden$="[[areTabsHidden(views, showTabs)]]">
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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 <Enter>",
|
||||
"label_voice": "Type and press <Enter> 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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user