mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Improve voice dialog (#15084)
* Improve voice dialog * Improve scrolling and dialog size * Align messages to bottom for better keyboard support * Add send button * Simplify label
This commit is contained in:
parent
207380d0da
commit
1d20d6979e
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable lit/prefer-static-styles */
|
/* eslint-disable lit/prefer-static-styles */
|
||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { mdiClose, mdiMicrophone } from "@mdi/js";
|
import { mdiClose, mdiMicrophone, mdiSend } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -56,7 +56,11 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
@state() private _agentInfo?: AgentInfo;
|
@state() private _agentInfo?: AgentInfo;
|
||||||
|
|
||||||
@query("ha-dialog", true) private _dialog!: HaDialog;
|
@state() private _showSendButton = false;
|
||||||
|
|
||||||
|
@query("#scroll-container") private _scrollContainer!: HaDialog;
|
||||||
|
|
||||||
|
@query("#message-input") private _messageInput!: HaTextField;
|
||||||
|
|
||||||
private recognition!: SpeechRecognition;
|
private recognition!: SpeechRecognition;
|
||||||
|
|
||||||
@ -64,10 +68,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
public async showDialog(): Promise<void> {
|
public async showDialog(): Promise<void> {
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
if (SpeechRecognition) {
|
|
||||||
this._startListening();
|
|
||||||
}
|
|
||||||
this._agentInfo = await getAgentInfo(this.hass);
|
this._agentInfo = await getAgentInfo(this.hass);
|
||||||
|
this._scrollMessagesBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async closeDialog(): Promise<void> {
|
public async closeDialog(): Promise<void> {
|
||||||
@ -83,9 +85,17 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog open @closed=${this.closeDialog}>
|
<ha-dialog
|
||||||
<div slot="heading" class="heading">
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||||
|
flexContent
|
||||||
|
>
|
||||||
|
<div slot="heading">
|
||||||
<ha-header-bar>
|
<ha-header-bar>
|
||||||
|
<span slot="title">
|
||||||
|
${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||||
|
</span>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
dialogAction="cancel"
|
dialogAction="cancel"
|
||||||
@ -94,62 +104,77 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="messages">
|
||||||
${this._agentInfo && this._agentInfo.onboarding
|
<div class="messages-container" id="scroll-container">
|
||||||
? html`
|
${this._agentInfo && this._agentInfo.onboarding
|
||||||
<div class="onboarding">
|
? html`
|
||||||
${this._agentInfo.onboarding.text}
|
<div class="onboarding">
|
||||||
<div class="side-by-side" @click=${this._completeOnboarding}>
|
${this._agentInfo.onboarding.text}
|
||||||
<a
|
<div
|
||||||
class="button"
|
class="side-by-side"
|
||||||
href=${this._agentInfo.onboarding.url}
|
@click=${this._completeOnboarding}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
><mwc-button unelevated
|
|
||||||
>${this.hass.localize("ui.common.yes")}!</mwc-button
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
<mwc-button outlined
|
|
||||||
>${this.hass.localize("ui.common.no")}</mwc-button
|
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
href=${this._agentInfo.onboarding.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
><mwc-button unelevated
|
||||||
|
>${this.hass.localize("ui.common.yes")}!</mwc-button
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
<mwc-button outlined
|
||||||
|
>${this.hass.localize("ui.common.no")}</mwc-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._conversation.map(
|
||||||
|
(message) => html`
|
||||||
|
<div class=${this._computeMessageClasses(message)}>
|
||||||
|
${message.text}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
)}
|
||||||
${this._conversation.map(
|
${this.results
|
||||||
(message) => html`
|
? html`
|
||||||
<div class=${this._computeMessageClasses(message)}>
|
<div class="message user">
|
||||||
${message.text}
|
<span
|
||||||
</div>
|
class=${classMap({
|
||||||
`
|
interimTranscript: !this.results.final,
|
||||||
)}
|
})}
|
||||||
${this.results
|
>${this.results.transcript}</span
|
||||||
? html`
|
>${!this.results.final ? "…" : ""}
|
||||||
<div class="message user">
|
</div>
|
||||||
<span
|
`
|
||||||
class=${classMap({
|
: ""}
|
||||||
interimTranscript: !this.results.final,
|
</div>
|
||||||
})}
|
|
||||||
>${this.results.transcript}</span
|
|
||||||
>${!this.results.final ? "…" : ""}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="input" slot="primaryAction">
|
<div class="input" slot="primaryAction">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
|
id="message-input"
|
||||||
@keyup=${this._handleKeyUp}
|
@keyup=${this._handleKeyUp}
|
||||||
.label=${this.hass.localize(
|
@input=${this._handleInput}
|
||||||
`ui.dialogs.voice_command.${
|
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||||
SpeechRecognition ? "label_voice" : "label"
|
|
||||||
}`
|
|
||||||
)}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
iconTrailing
|
iconTrailing
|
||||||
>
|
>
|
||||||
${SpeechRecognition
|
<span slot="trailingIcon">
|
||||||
? html`
|
${this._showSendButton
|
||||||
<span slot="trailingIcon">
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
class="listening-icon"
|
||||||
|
.path=${mdiSend}
|
||||||
|
@click=${this._handleSendMessage}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.send_text"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: SpeechRecognition
|
||||||
|
? html`
|
||||||
${this.results
|
${this.results
|
||||||
? html`
|
? html`
|
||||||
<div class="bouncer">
|
<div class="bouncer">
|
||||||
@ -159,13 +184,17 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
class="listening-icon"
|
||||||
.path=${mdiMicrophone}
|
.path=${mdiMicrophone}
|
||||||
@click=${this._toggleListening}
|
@click=${this._toggleListening}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.start_listening"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
</ha-icon-button>
|
||||||
</span>
|
`
|
||||||
`
|
: ""}
|
||||||
: ""}
|
</span>
|
||||||
</ha-textfield>
|
</ha-textfield>
|
||||||
${this._agentInfo && this._agentInfo.attribution
|
${this._agentInfo && this._agentInfo.attribution
|
||||||
? html`
|
? html`
|
||||||
@ -209,6 +238,24 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
if (ev.keyCode === 13 && input.value) {
|
if (ev.keyCode === 13 && input.value) {
|
||||||
this._processText(input.value);
|
this._processText(input.value);
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
this._showSendButton = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleInput(ev: InputEvent) {
|
||||||
|
const value = (ev.target as HaTextField).value;
|
||||||
|
if (value && !this._showSendButton) {
|
||||||
|
this._showSendButton = true;
|
||||||
|
} else if (!value && this._showSendButton) {
|
||||||
|
this._showSendButton = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSendMessage() {
|
||||||
|
if (this._messageInput.value) {
|
||||||
|
this._processText(this._messageInput.value);
|
||||||
|
this._messageInput.value = "";
|
||||||
|
this._showSendButton = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +268,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
this.recognition = new SpeechRecognition();
|
this.recognition = new SpeechRecognition();
|
||||||
this.recognition.interimResults = true;
|
this.recognition.interimResults = true;
|
||||||
this.recognition.lang = this.hass.language;
|
this.recognition.lang = this.hass.language;
|
||||||
|
this.recognition.continuous = false;
|
||||||
|
|
||||||
this.recognition.addEventListener("start", () => {
|
this.recognition.addEventListener("start", () => {
|
||||||
this.results = {
|
this.results = {
|
||||||
@ -319,7 +367,13 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
if (!this.results) {
|
if (!this.results) {
|
||||||
this._startListening();
|
this._startListening();
|
||||||
} else {
|
} else {
|
||||||
this.recognition!.stop();
|
this._stopListening();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopListening() {
|
||||||
|
if (this.recognition) {
|
||||||
|
this.recognition.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +394,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _scrollMessagesBottom() {
|
private _scrollMessagesBottom() {
|
||||||
this._dialog.scrollToPos(0, 99999);
|
this._scrollContainer.scrollTo(0, 99999);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeMessageClasses(message: Message) {
|
private _computeMessageClasses(message: Message) {
|
||||||
@ -351,7 +405,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-icon-button {
|
ha-icon-button.listening-icon {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
margin-right: -24px;
|
margin-right: -24px;
|
||||||
margin-inline-end: -24px;
|
margin-inline-end: -24px;
|
||||||
@ -359,7 +413,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon-button[active] {
|
ha-icon-button.listening-icon[active] {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,21 +421,19 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
--primary-action-button-flex: 1;
|
--primary-action-button-flex: 1;
|
||||||
--secondary-action-button-flex: 0;
|
--secondary-action-button-flex: 0;
|
||||||
--mdc-dialog-max-width: 450px;
|
--mdc-dialog-max-width: 450px;
|
||||||
|
--mdc-dialog-max-height: 500px;
|
||||||
|
--dialog-content-padding: 0;
|
||||||
}
|
}
|
||||||
ha-header-bar {
|
ha-header-bar {
|
||||||
display: none;
|
--mdc-theme-on-primary: var(--primary-text-color);
|
||||||
}
|
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
display: flex;
|
||||||
ha-header-bar {
|
flex-shrink: 0;
|
||||||
--mdc-theme-on-primary: var(--primary-text-color);
|
|
||||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
display: block;
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
a.button {
|
a.button {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -403,6 +455,25 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
.attribution {
|
.attribution {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
.messages {
|
||||||
|
display: block;
|
||||||
|
height: 300px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
.messages {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.messages-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
left: 0px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
.message {
|
.message {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
@ -3,13 +3,13 @@ import "@material/mwc-list/mwc-list-item";
|
|||||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||||
import {
|
import {
|
||||||
mdiCodeBraces,
|
mdiCodeBraces,
|
||||||
|
mdiCommentProcessingOutline,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiFileMultiple,
|
mdiFileMultiple,
|
||||||
mdiFormatListBulletedTriangle,
|
mdiFormatListBulletedTriangle,
|
||||||
mdiHelp,
|
mdiHelp,
|
||||||
mdiHelpCircle,
|
mdiHelpCircle,
|
||||||
mdiMagnify,
|
mdiMagnify,
|
||||||
mdiMicrophone,
|
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiRefresh,
|
mdiRefresh,
|
||||||
@ -302,9 +302,9 @@ class HUIRoot extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${this.hass!.localize(
|
.label=${this.hass!.localize(
|
||||||
"ui.panel.lovelace.menu.start_conversation"
|
"ui.panel.lovelace.menu.assist"
|
||||||
)}
|
)}
|
||||||
.path=${mdiMicrophone}
|
.path=${mdiCommentProcessingOutline}
|
||||||
@click=${this._showVoiceCommandDialog}
|
@click=${this._showVoiceCommandDialog}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
`
|
`
|
||||||
@ -324,7 +324,7 @@ class HUIRoot extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
@request-selected=${this._showQuickBar}
|
@request-selected=${this._handleShowQuickBar}
|
||||||
>
|
>
|
||||||
${this.hass!.localize(
|
${this.hass!.localize(
|
||||||
"ui.panel.lovelace.menu.search"
|
"ui.panel.lovelace.menu.search"
|
||||||
@ -343,15 +343,15 @@ class HUIRoot extends LitElement {
|
|||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
@request-selected=${this
|
@request-selected=${this
|
||||||
._showVoiceCommandDialog}
|
._handleShowVoiceCommandDialog}
|
||||||
>
|
>
|
||||||
${this.hass!.localize(
|
${this.hass!.localize(
|
||||||
"ui.panel.lovelace.menu.start_conversation"
|
"ui.panel.lovelace.menu.assist"
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
.path=${mdiMicrophone}
|
.path=${mdiCommentProcessingOutline}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
`
|
`
|
||||||
@ -711,6 +711,13 @@ class HUIRoot extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleShowQuickBar(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||||
|
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._showQuickBar();
|
||||||
|
}
|
||||||
|
|
||||||
private _showQuickBar(): void {
|
private _showQuickBar(): void {
|
||||||
showQuickBar(this, {
|
showQuickBar(this, {
|
||||||
commandMode: false,
|
commandMode: false,
|
||||||
@ -762,6 +769,15 @@ class HUIRoot extends LitElement {
|
|||||||
navigate(`${this.route?.prefix}/hass-unused-entities`);
|
navigate(`${this.route?.prefix}/hass-unused-entities`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleShowVoiceCommandDialog(
|
||||||
|
ev: CustomEvent<RequestSelectedDetail>
|
||||||
|
): void {
|
||||||
|
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._showVoiceCommandDialog();
|
||||||
|
}
|
||||||
|
|
||||||
private _showVoiceCommandDialog(): void {
|
private _showVoiceCommandDialog(): void {
|
||||||
showVoiceCommandDialog(this);
|
showVoiceCommandDialog(this);
|
||||||
}
|
}
|
||||||
|
@ -803,13 +803,15 @@
|
|||||||
"nothing_found": "Nothing found!"
|
"nothing_found": "Nothing found!"
|
||||||
},
|
},
|
||||||
"voice_command": {
|
"voice_command": {
|
||||||
|
"title": "Assistant",
|
||||||
"did_not_hear": "Home Assistant did not hear anything",
|
"did_not_hear": "Home Assistant did not hear anything",
|
||||||
"did_not_understand": "Didn't quite get that",
|
"did_not_understand": "Didn't quite get that",
|
||||||
"found": "I found the following for you:",
|
"found": "I found the following for you:",
|
||||||
"error": "Oops, an error has occurred",
|
"error": "Oops, an error has occurred",
|
||||||
"how_can_i_help": "How can I help?",
|
"how_can_i_help": "How can I help?",
|
||||||
"label": "Type a question and press 'Enter'",
|
"input_label": "Enter a request",
|
||||||
"label_voice": "Type and press 'Enter' or tap the microphone to speak"
|
"send_text": "Send text",
|
||||||
|
"start_listening": "Start listening"
|
||||||
},
|
},
|
||||||
"generic": {
|
"generic": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
@ -3856,7 +3858,7 @@
|
|||||||
"configure_ui": "Edit Dashboard",
|
"configure_ui": "Edit Dashboard",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"start_conversation": "Start conversation",
|
"assist": "Assist",
|
||||||
"reload_resources": "Reload resources",
|
"reload_resources": "Reload resources",
|
||||||
"exit_edit_mode": "Done",
|
"exit_edit_mode": "Done",
|
||||||
"close": "Close"
|
"close": "Close"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user