diff --git a/src/components/ha-conversation-agent-picker.ts b/src/components/ha-conversation-agent-picker.ts index 8e3fdc4aeb..0e3e66fbcc 100644 --- a/src/components/ha-conversation-agent-picker.ts +++ b/src/components/ha-conversation-agent-picker.ts @@ -37,7 +37,15 @@ export class HaConversationAgentPicker extends LitElement { if (!this._agents) { return nothing; } - const value = this.value ?? (this.required ? "homeassistant" : NONE); + const value = + this.value ?? + (this.required && + (!this.language || + this._agents + .find((agent) => agent.id === "homeassistant") + ?.supported_languages?.includes(this.language)) + ? "homeassistant" + : NONE); return html` this._updateAgents(), 500); private async _updateAgents() { - const { agents } = await listAgents(this.hass, this.language); + const { agents } = await listAgents( + this.hass, + this.language, + this.hass.config.country || undefined + ); this._agents = agents; diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index d0c0ae8596..54327ae35e 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -7,7 +7,7 @@ import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; import type { HomeAssistant } from "../types"; import "./ha-icon-button"; -const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"]; +const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"]; export const createCloseHeading = ( hass: HomeAssistant, diff --git a/src/components/ha-form/ha-form-grid.ts b/src/components/ha-form/ha-form-grid.ts index 2ccb082c8a..f74377c12a 100644 --- a/src/components/ha-form/ha-form-grid.ts +++ b/src/components/ha-form/ha-form-grid.ts @@ -33,6 +33,11 @@ export class HaFormGrid extends LitElement implements HaFormElement { @property() public computeHelper?: (schema: HaFormSchema) => string; + public async focus() { + await this.updateComplete; + this.renderRoot.querySelector("ha-form")?.focus(); + } + protected updated(changedProps: PropertyValues): void { super.updated(changedProps); if (changedProps.has("schema")) { diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 12b1fa9cd4..3062c2317d 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -4,6 +4,7 @@ import { html, LitElement, PropertyValues, + ReactiveElement, TemplateResult, } from "lit"; import { customElement, property } from "lit/decorators"; @@ -56,13 +57,18 @@ export class HaForm extends LitElement implements HaFormElement { @property() public localizeValue?: (key: string) => string; - public focus() { - const root = this.shadowRoot?.querySelector(".root"); + public async focus() { + await this.updateComplete; + const root = this.renderRoot.querySelector(".root"); if (!root) { return; } for (const child of root.children) { if (child.tagName !== "HA-ALERT") { + if (child instanceof ReactiveElement) { + // eslint-disable-next-line no-await-in-loop + await child.updateComplete; + } (child as HTMLElement).focus(); break; } diff --git a/src/components/ha-language-picker.ts b/src/components/ha-language-picker.ts index 2bda976cd4..49c5b419bb 100644 --- a/src/components/ha-language-picker.ts +++ b/src/components/ha-language-picker.ts @@ -1,17 +1,11 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; import { formatLanguageCode } from "../common/language/format_language"; import { caseInsensitiveStringCompare } from "../common/string/compare"; +import { FrontendLocaleData } from "../data/translation"; import { HomeAssistant } from "../types"; import "./ha-list-item"; import "./ha-select"; @@ -35,13 +29,39 @@ export class HaLanguagePicker extends LitElement { @state() _defaultLanguages: string[] = []; + @query("ha-select") private _select!: HaSelect; + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._computeDefaultLanguageOptions(); } + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (changedProperties.has("languages") || changedProperties.has("value")) { + this._select.layoutOptions(); + if (this._select.value !== this.value) { + fireEvent(this, "value-changed", { value: this._select.value }); + } + if (!this.value) { + return; + } + const languageOptions = this._getLanguagesOptions( + this.languages ?? this._defaultLanguages, + this.hass.locale, + this.nativeName + ); + const selectedItem = languageOptions.find( + (option) => option.value === this.value + ); + if (!selectedItem) { + this.value = undefined; + } + } + } + private _getLanguagesOptions = memoizeOne( - (languages: string[], language: string, nativeName: boolean) => { + (languages: string[], locale: FrontendLocaleData, nativeName: boolean) => { let options: { label: string; value: string }[] = []; if (nativeName) { @@ -53,12 +73,12 @@ export class HaLanguagePicker extends LitElement { } else { options = languages.map((lang) => ({ value: lang, - label: formatLanguageCode(lang, this.hass.locale), + label: formatLanguageCode(lang, locale), })); } options.sort((a, b) => - caseInsensitiveStringCompare(a.label, b.label, language) + caseInsensitiveStringCompare(a.label, b.label, locale.language) ); return options; } @@ -75,21 +95,19 @@ export class HaLanguagePicker extends LitElement { } protected render() { - const value = this.value; - const languageOptions = this._getLanguagesOptions( this.languages ?? this._defaultLanguages, - this.hass.locale.language, + this.hass.locale, this.nativeName ); - if (languageOptions.length === 0) { - return nothing; - } + const value = + this.value ?? (this.required ? languageOptions[0]?.value : this.value); return html` - ${languageOptions.map( - (option) => html` - ${option.label} - ` - )} + ${languageOptions.length === 0 + ? html`${this.hass.localize( + "ui.components.language-picker.no_languages" + )}` + : languageOptions.map( + (option) => html` + ${option.label} + ` + )} `; } @@ -117,6 +143,9 @@ export class HaLanguagePicker extends LitElement { private _changed(ev): void { const target = ev.target as HaSelect; + if (!this.hass || target.value === "" || target.value === this.value) { + return; + } this.value = target.value; fireEvent(this, "value-changed", { value: this.value }); } diff --git a/src/components/ha-selector/ha-selector-text.ts b/src/components/ha-selector/ha-selector-text.ts index afaa3a1c0e..fb8033790a 100644 --- a/src/components/ha-selector/ha-selector-text.ts +++ b/src/components/ha-selector/ha-selector-text.ts @@ -30,6 +30,13 @@ export class HaTextSelector extends LitElement { @state() private _unmaskedPassword = false; + public async focus() { + await this.updateComplete; + ( + this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement + )?.focus(); + } + protected render() { if (this.selector.text?.multiline) { return html`; - public focus() { - this.shadowRoot?.getElementById("selector")?.focus(); + public async focus() { + await this.updateComplete; + (this.renderRoot.querySelector("#selector") as HTMLElement)?.focus(); } private get _type() { diff --git a/src/components/ha-stt-picker.ts b/src/components/ha-stt-picker.ts index d01f637a24..f425e41a47 100644 --- a/src/components/ha-stt-picker.ts +++ b/src/components/ha-stt-picker.ts @@ -96,7 +96,13 @@ export class HaSTTPicker extends LitElement { private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500); private async _updateEngines() { - this._engines = (await listSTTEngines(this.hass, this.language)).providers; + this._engines = ( + await listSTTEngines( + this.hass, + this.language, + this.hass.config.country || undefined + ) + ).providers; if (!this.value) { return; diff --git a/src/components/ha-tts-picker.ts b/src/components/ha-tts-picker.ts index bde9196e49..d9dfed597b 100644 --- a/src/components/ha-tts-picker.ts +++ b/src/components/ha-tts-picker.ts @@ -96,7 +96,13 @@ export class HaTTSPicker extends LitElement { private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500); private async _updateEngines() { - this._engines = (await listTTSEngines(this.hass, this.language)).providers; + this._engines = ( + await listTTSEngines( + this.hass, + this.language, + this.hass.config.country || undefined + ) + ).providers; if (!this.value) { return; diff --git a/src/components/ha-tts-voice-picker.ts b/src/components/ha-tts-voice-picker.ts index 7afc737c17..ace393379d 100644 --- a/src/components/ha-tts-voice-picker.ts +++ b/src/components/ha-tts-voice-picker.ts @@ -10,7 +10,7 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; import { debounce } from "../common/util/debounce"; -import { listTTSVoices } from "../data/tts"; +import { listTTSVoices, TTSVoice } from "../data/tts"; import { HomeAssistant } from "../types"; import "./ha-list-item"; import "./ha-select"; @@ -34,13 +34,14 @@ export class HaTTSVoicePicker extends LitElement { @property({ type: Boolean }) public required = false; - @state() _voices?: string[] | null; + @state() _voices?: TTSVoice[] | null; protected render() { if (!this._voices) { return nothing; } - const value = this.value ?? (this.required ? this._voices[0] : NONE); + const value = + this.value ?? (this.required ? this._voices[0]?.voice_id : NONE); return html` ` : nothing} ${this._voices.map( - (voice) => html` - ${voice} + (voice) => html` + ${voice.name} ` )} @@ -94,7 +95,10 @@ export class HaTTSVoicePicker extends LitElement { return; } - if (!this._voices || !this._voices.includes(this.value)) { + if ( + !this._voices || + !this._voices.find((voice) => voice.voice_id === this.value) + ) { this.value = undefined; fireEvent(this, "value-changed", { value: this.value }); } diff --git a/src/data/conversation.ts b/src/data/conversation.ts index 904fc55c95..f8427f779d 100644 --- a/src/data/conversation.ts +++ b/src/data/conversation.ts @@ -78,11 +78,13 @@ export const processConversationInput = ( export const listAgents = ( hass: HomeAssistant, - language?: string + language?: string, + country?: string ): Promise<{ agents: Agent[] }> => hass.callWS({ type: "conversation/agent/list", language, + country, }); export const getAgentInfo = ( diff --git a/src/data/stt.ts b/src/data/stt.ts index e7d652563c..ac1d9006ad 100644 --- a/src/data/stt.ts +++ b/src/data/stt.ts @@ -25,9 +25,11 @@ export interface STTEngine { export const listSTTEngines = ( hass: HomeAssistant, - language?: string + language?: string, + country?: string ): Promise<{ providers: STTEngine[] }> => hass.callWS({ type: "stt/engine/list", language, + country, }); diff --git a/src/data/tts.ts b/src/data/tts.ts index 5f2175c135..cf70d0eda7 100644 --- a/src/data/tts.ts +++ b/src/data/tts.ts @@ -31,18 +31,20 @@ export const getProviderFromTTSMediaSource = (mediaContentId: string) => export const listTTSEngines = ( hass: HomeAssistant, - language?: string + language?: string, + country?: string ): Promise<{ providers: TTSEngine[] }> => hass.callWS({ type: "tts/engine/list", language, + country, }); export const listTTSVoices = ( hass: HomeAssistant, engine_id: string, language: string -): Promise<{ voices: string[] | null }> => +): Promise<{ voices: TTSVoice[] | null }> => hass.callWS({ type: "tts/engine/voices", engine_id, diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts index 68011ef5c9..f393ee9cd6 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts @@ -13,6 +13,12 @@ export class AssistPipelineDetailConfig extends LitElement { @property() public supportedLanguages?: string[]; + public async focus() { + await this.updateComplete; + const input = this.renderRoot?.querySelector("ha-form"); + input?.focus(); + } + private _schema = memoizeOne( (supportedLanguages?: string[]) => [ @@ -84,11 +90,8 @@ export class AssistPipelineDetailConfig extends LitElement { margin-bottom: 4px; } p { - font-weight: normal; color: var(--secondary-text-color); - font-size: 16px; - line-height: 24px; - letter-spacing: 0.5px; + font-size: var(--mdc-typography-body2-font-size, 0.875rem); margin-top: 0; margin-bottom: 0; } diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts index 5257e234cd..4fc2c49610 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts @@ -1,9 +1,10 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { SchemaUnion } from "../../../../components/ha-form/types"; +import { LocalizeKeys } from "../../../../common/translations/localize"; import { AssistPipeline } from "../../../../data/assist_pipeline"; import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-form/ha-form"; @customElement("assist-pipeline-detail-conversation") export class AssistPipelineDetailConversation extends LitElement { @@ -29,24 +30,26 @@ export class AssistPipelineDetailConversation extends LitElement { }, }, }, - { - name: "conversation_language", - required: true, - selector: { - language: { languages: supportedLanguages ?? [] }, - }, - }, + supportedLanguages?.length + ? { + name: "conversation_language", + required: true, + selector: { + language: { languages: supportedLanguages }, + }, + } + : { name: "", type: "constant" }, ] as const, }, ] as const ); - private _computeLabel = ( - schema: SchemaUnion> - ): string => - this.hass.localize( - `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` - ); + private _computeLabel = (schema): string => + schema.name + ? this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys + ) + : ""; protected render() { return html` @@ -92,11 +95,8 @@ export class AssistPipelineDetailConversation extends LitElement { margin-bottom: 4px; } p { - font-weight: normal; color: var(--secondary-text-color); - font-size: 16px; - line-height: 24px; - letter-spacing: 0.5px; + font-size: var(--mdc-typography-body2-font-size, 0.875rem); margin-top: 0; margin-bottom: 0; } diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-stt.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-stt.ts index 0675af69ff..e0f9fad801 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-stt.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-stt.ts @@ -1,9 +1,10 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { SchemaUnion } from "../../../../components/ha-form/types"; +import { LocalizeKeys } from "../../../../common/translations/localize"; import { AssistPipeline } from "../../../../data/assist_pipeline"; import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-form/ha-form"; @customElement("assist-pipeline-detail-stt") export class AssistPipelineDetailSTT extends LitElement { @@ -28,24 +29,26 @@ export class AssistPipelineDetailSTT extends LitElement { }, }, }, - { - name: "stt_language", - required: true, - selector: { - language: { languages: supportedLanguages ?? [] }, - }, - }, + supportedLanguages?.length + ? { + name: "stt_language", + required: true, + selector: { + language: { languages: supportedLanguages }, + }, + } + : { name: "", type: "constant" }, ] as const, }, ] as const ); - private _computeLabel = ( - schema: SchemaUnion> - ): string => - this.hass.localize( - `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` - ); + private _computeLabel = (schema): string => + schema.name + ? this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys + ) + : ""; protected render() { return html` @@ -91,11 +94,8 @@ export class AssistPipelineDetailSTT extends LitElement { margin-bottom: 4px; } p { - font-weight: normal; color: var(--secondary-text-color); - font-size: 16px; - line-height: 24px; - letter-spacing: 0.5px; + font-size: var(--mdc-typography-body2-font-size, 0.875rem); margin-top: 0; margin-bottom: 0; } diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-tts.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-tts.ts index abb567289e..605600176c 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-tts.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-tts.ts @@ -1,9 +1,10 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { SchemaUnion } from "../../../../components/ha-form/types"; +import { LocalizeKeys } from "../../../../common/translations/localize"; import { AssistPipeline } from "../../../../data/assist_pipeline"; import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-form/ha-form"; @customElement("assist-pipeline-detail-tts") export class AssistPipelineDetailTTS extends LitElement { @@ -28,13 +29,17 @@ export class AssistPipelineDetailTTS extends LitElement { }, }, }, - { - name: "tts_language", - selector: { - language: { languages: supportedLanguages ?? [] }, - }, - required: true, - }, + supportedLanguages?.length + ? { + name: "tts_language", + required: true, + selector: { + language: { languages: supportedLanguages }, + }, + } + : { name: "", type: "constant" }, + + { name: "", type: "constant" }, { name: "tts_voice", selector: { @@ -48,12 +53,12 @@ export class AssistPipelineDetailTTS extends LitElement { ] as const ); - private _computeLabel = ( - schema: SchemaUnion> - ): string => - this.hass.localize( - `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` - ); + private _computeLabel = (schema): string => + schema.name + ? this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys + ) + : ""; protected render() { return html` @@ -100,11 +105,8 @@ export class AssistPipelineDetailTTS extends LitElement { margin-bottom: 4px; } p { - font-weight: normal; color: var(--secondary-text-color); - font-size: 16px; - line-height: 24px; - letter-spacing: 0.5px; + font-size: var(--mdc-typography-body2-font-size, 0.875rem); margin-top: 0; margin-bottom: 0; } diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index 6e6878e31d..2a2be60a4f 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -41,7 +41,13 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { this._data = this._params.pipeline; this._preferred = this._params.preferred; } else { - this._data = {}; + this._data = { + language: ( + this.hass.config.language || this.hass.locale.language + ).substring(0, 2), + stt_engine: "cloud", + tts_engine: "cloud", + }; } } @@ -88,21 +94,26 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { .hass=${this.hass} .data=${this._data} .supportedLanguages=${this._supportedLanguages} + keys="name,language" @value-changed=${this._valueChanged} + dialogInitialFocus > @@ -151,31 +162,35 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { private _valueChanged(ev: CustomEvent) { this._error = undefined; - const value = ev.detail.value; - this._data = value; + const value = {}; + (ev.currentTarget as any) + .getAttribute("keys") + .split(",") + .forEach((key) => { + value[key] = ev.detail.value[key]; + }); + this._data = { ...this._data, ...value }; } private async _updatePipeline() { this._submitting = true; try { + const data = this._data!; + const values: AssistPipelineMutableParams = { + name: data.name!, + language: data.language!, + conversation_engine: data.conversation_engine!, + conversation_language: data.conversation_language ?? null, + stt_engine: data.stt_engine ?? null, + stt_language: data.stt_language ?? null, + tts_engine: data.tts_engine ?? null, + tts_language: data.tts_language ?? null, + tts_voice: data.tts_voice ?? null, + }; if (this._params!.pipeline?.id) { - const data = this._data!; - const values: AssistPipelineMutableParams = { - name: data.name!, - language: data.language!, - conversation_engine: data.conversation_engine!, - conversation_language: data.conversation_language ?? null, - stt_engine: data.stt_engine ?? null, - stt_language: data.stt_language ?? null, - tts_engine: data.tts_engine ?? null, - tts_language: data.tts_language ?? null, - tts_voice: data.tts_voice ?? null, - }; await this._params!.updatePipeline(values); } else { - await this._params!.createPipeline( - this._data as AssistPipelineMutableParams - ); + await this._params!.createPipeline(values); } this.closeDialog(); } catch (err: any) { diff --git a/src/translations/en.json b/src/translations/en.json index 651faa3f21..1da760f2c0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -413,6 +413,10 @@ "theme": "Theme", "no_theme": "No theme" }, + "language-picker": { + "language": "Language", + "no_languages": "No languages available" + }, "tts-picker": { "tts": "Text to Speech", "none": "None"