diff --git a/src/components/ha-form/ha-form-expandable.ts b/src/components/ha-form/ha-form-expandable.ts index 86c108caac..2c4892efdd 100644 --- a/src/components/ha-form/ha-form-expandable.ts +++ b/src/components/ha-form/ha-form-expandable.ts @@ -71,6 +71,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement { display: block; --expansion-panel-content-padding: 0; border-radius: 6px; + --ha-card-border-radius: 6px; } ha-svg-icon, ha-icon { diff --git a/src/data/voice_assistant.ts b/src/data/voice_assistant.ts index 5500effdf7..5092509609 100644 --- a/src/data/voice_assistant.ts +++ b/src/data/voice_assistant.ts @@ -3,6 +3,23 @@ import type { ConversationResult } from "./conversation"; import type { ResolvedMediaSource } from "./media_source"; import type { SpeechMetadata } from "./stt"; +export interface VoiceAssistantPipeline { + id: string; + conversation_engine: string; + language: string; + name: string; + stt_engine: string; + tts_engine: string; +} + +export interface VoiceAssistantPipelineMutableParams { + conversation_engine: string; + language: string; + name: string; + stt_engine: string; + tts_engine: string; +} + interface PipelineEventBase { timestamp: string; } @@ -202,3 +219,37 @@ export const runVoiceAssistantPipeline = ( return unsubProm; }; + +export const fetchVoiceAssistantPipelines = (hass: HomeAssistant) => + hass.callWS({ + type: "voice_assistant/pipeline/list", + }); + +export const createVoiceAssistantPipeline = ( + hass: HomeAssistant, + pipeline: VoiceAssistantPipelineMutableParams +) => + hass.callWS({ + type: "voice_assistant/pipeline/create", + ...pipeline, + }); + +export const updateVoiceAssistantPipeline = ( + hass: HomeAssistant, + pipelineId: string, + pipeline: Partial +) => + hass.callWS({ + type: "voice_assistant/pipeline/update", + pipeline_id: pipelineId, + ...pipeline, + }); + +export const deleteVoiceAssistantPipeline = ( + hass: HomeAssistant, + pipelineId: string +) => + hass.callWS({ + type: "voice_assistant/pipeline/delete", + pipeline_id: pipelineId, + }); diff --git a/src/panels/config/voice-assistants/assist-pref.ts b/src/panels/config/voice-assistants/assist-pref.ts new file mode 100644 index 0000000000..e08e801def --- /dev/null +++ b/src/panels/config/voice-assistants/assist-pref.ts @@ -0,0 +1,188 @@ +import "@material/mwc-list/mwc-list"; +import { mdiHelpCircle, mdiPlus } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { property, state } from "lit/decorators"; +import "../../../components/ha-alert"; +import "../../../components/ha-card"; +import "../../../components/ha-icon-next"; +import "../../../components/ha-list-item"; +import "../../../components/ha-switch"; +import "../../../components/ha-button"; +import { + createVoiceAssistantPipeline, + deleteVoiceAssistantPipeline, + fetchVoiceAssistantPipelines, + updateVoiceAssistantPipeline, + VoiceAssistantPipeline, +} from "../../../data/voice_assistant"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../../../types"; +import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail"; + +export class AssistPref extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _pipelines: VoiceAssistantPipeline[] = []; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + fetchVoiceAssistantPipelines(this.hass).then((pipelines) => { + this._pipelines = pipelines; + }); + } + + protected render() { + return html` + +

Assist

+
+ + + +
+
+ + ${this._pipelines.map( + (pipeline) => html` + + ${pipeline.name} + ${pipeline.language} + + + ` + )} + + + ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.add_assistant" + )} + + +
+ +
+ `; + } + + private _editPipeline(ev) { + const id = ev.currentTarget.id as string; + + const pipeline = this._pipelines.find((res) => res.id === id); + this._openDialog(pipeline); + } + + private _addPipeline() { + this._openDialog(); + } + + private async _openDialog(pipeline?: VoiceAssistantPipeline): Promise { + showVoiceAssistantPipelineDetailDialog(this, { + pipeline, + createPipeline: async (values) => { + const created = await createVoiceAssistantPipeline(this.hass!, values); + this._pipelines = this._pipelines!.concat(created); + }, + updatePipeline: async (values) => { + const updated = await updateVoiceAssistantPipeline( + this.hass!, + pipeline!.id, + values + ); + this._pipelines = this._pipelines!.map((res) => + res === pipeline ? updated : res + ); + }, + deletePipeline: async () => { + if ( + !(await showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.delete.confirm_title", + { name: pipeline!.name } + ), + text: this.hass!.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.delete.confirm_text", + { name: pipeline!.name } + ), + confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, + })) + ) { + return false; + } + + try { + await deleteVoiceAssistantPipeline(this.hass!, pipeline!.id); + this._pipelines = this._pipelines!.filter((res) => res !== pipeline); + return true; + } catch (err: any) { + return false; + } + }, + }); + } + + static get styles(): CSSResultGroup { + return css` + a { + color: var(--primary-color); + } + .header-actions { + position: absolute; + right: 0px; + top: 24px; + display: flex; + flex-direction: row; + } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } + .card-actions { + display: flex; + } + .card-actions a { + text-decoration: none; + } + .card-header { + display: flex; + align-items: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "assist-pref": AssistPref; + } +} + +customElements.define("assist-pref", AssistPref); 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 new file mode 100644 index 0000000000..c40b80f2fd --- /dev/null +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -0,0 +1,204 @@ +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-form/ha-form"; +import { SchemaUnion } from "../../../components/ha-form/types"; +import { + VoiceAssistantPipeline, + VoiceAssistantPipelineMutableParams, +} from "../../../data/voice_assistant"; +import { haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail"; + +@customElement("dialog-voice-assistant-pipeline-detail") +export class DialogVoiceAssistantPipelineDetail extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: VoiceAssistantPipelineDetailsDialogParams; + + @state() private _data?: Partial; + + @state() private _error?: Record; + + @state() private _submitting = false; + + public showDialog(params: VoiceAssistantPipelineDetailsDialogParams): void { + this._params = params; + this._error = undefined; + if (this._params.pipeline) { + this._data = this._params.pipeline; + } else { + this._data = {}; + } + } + + public closeDialog(): void { + this._params = undefined; + this._data = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params || !this._data) { + return nothing; + } + + return html` + +
+ +
+ ${this._params.pipeline?.id + ? html` + + ${this.hass.localize("ui.common.delete")} + + ` + : nothing} + + ${this._params.pipeline?.id + ? this.hass.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.detail.update_assistant_action" + ) + : this.hass.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.detail.add_assistant_action" + )} + +
+ `; + } + + private _schema = memoizeOne( + () => + [ + { + name: "name", + required: true, + selector: { + text: {}, + }, + }, + { + name: "conversation_engine", + required: true, + selector: { + text: {}, + }, + }, + { + name: "language", + required: true, + selector: { + text: {}, + }, + }, + { + name: "stt_engine", + required: true, + selector: { + text: {}, + }, + }, + { + name: "tts_engine", + required: true, + selector: { + text: {}, + }, + }, + ] as const + ); + + private _computeLabel = ( + schema: SchemaUnion> + ): string => + this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` + ); + + private _valueChanged(ev: CustomEvent) { + this._error = undefined; + const value = ev.detail.value; + this._data = value; + } + + private async _updatePipeline() { + this._submitting = true; + try { + if (this._params!.pipeline?.id) { + const values: Partial = { + name: this._data!.name, + conversation_engine: this._data!.conversation_engine, + language: this._data!.language, + stt_engine: this._data!.stt_engine, + tts_engine: this._data!.tts_engine, + }; + await this._params!.updatePipeline(values); + } else { + await this._params!.createPipeline( + this._data as VoiceAssistantPipelineMutableParams + ); + } + this.closeDialog(); + } catch (err: any) { + this._error = { base: err?.message || "Unknown error" }; + } finally { + this._submitting = false; + } + } + + private async _deletePipeline() { + this._submitting = true; + try { + if (await this._params!.deletePipeline()) { + this.closeDialog(); + } + } finally { + this._submitting = false; + } + } + + static get styles(): CSSResultGroup { + return [haStyleDialog, css``]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-voice-assistant-pipeline-detail": DialogVoiceAssistantPipelineDetail; + } +} diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts index 191fe9f8b5..ca720066ec 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts @@ -1,14 +1,15 @@ +import "@polymer/paper-item/paper-item"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { CloudStatus } from "../../../data/cloud"; import "../../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../types"; +import "./assist-pref"; import "./cloud-alexa-pref"; import "./cloud-google-pref"; import { voiceAssistantTabs } from "./ha-config-voice-assistants"; -import "@polymer/paper-item/paper-item"; -import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @customElement("ha-config-voice-assistants-assistants") export class HaConfigVoiceAssistantsAssistants extends LitElement { @@ -36,6 +37,7 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement { .tabs=${voiceAssistantTabs} >
+ ${this.cloudStatus?.logged_in ? html` Promise; + updatePipeline: ( + updates: Partial + ) => Promise; + deletePipeline: () => Promise; +} + +export const loadVoiceAssistantPipelineDetailDialog = () => + import("./dialog-voice-assistant-pipeline-detail"); + +export const showVoiceAssistantPipelineDetailDialog = ( + element: HTMLElement, + dialogParams: VoiceAssistantPipelineDetailsDialogParams +) => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-voice-assistant-pipeline-detail", + dialogImport: loadVoiceAssistantPipelineDetailDialog, + dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index e3e1bfe853..0cc9035a69 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2001,7 +2001,27 @@ }, "voice_assistants": { "assistants": { - "caption": "Assistants" + "caption": "Assistants", + "pipeline": { + "add_assistant": "Add assistant", + "manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]", + "delete": { + "confirm_title": "Delete {name}?", + "confirm_text": "{name} will be permanently deleted." + }, + "detail": { + "update_assistant_action": "Update", + "add_assistant_title": "Add assistant", + "add_assistant_action": "Create", + "form": { + "name": "Name", + "conversation_engine": "Conversation agent", + "language": "Language", + "stt_engine": "Speech to text", + "tts_engine": "Text to speech" + } + } + } }, "expose": { "caption": "Expose",