mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Voice assistants pipeline UI (#16167)
This commit is contained in:
parent
639c120b56
commit
5e352f6194
@ -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 {
|
||||
|
@ -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<VoiceAssistantPipeline[]>({
|
||||
type: "voice_assistant/pipeline/list",
|
||||
});
|
||||
|
||||
export const createVoiceAssistantPipeline = (
|
||||
hass: HomeAssistant,
|
||||
pipeline: VoiceAssistantPipelineMutableParams
|
||||
) =>
|
||||
hass.callWS<VoiceAssistantPipeline>({
|
||||
type: "voice_assistant/pipeline/create",
|
||||
...pipeline,
|
||||
});
|
||||
|
||||
export const updateVoiceAssistantPipeline = (
|
||||
hass: HomeAssistant,
|
||||
pipelineId: string,
|
||||
pipeline: Partial<VoiceAssistantPipelineMutableParams>
|
||||
) =>
|
||||
hass.callWS<VoiceAssistantPipeline>({
|
||||
type: "voice_assistant/pipeline/update",
|
||||
pipeline_id: pipelineId,
|
||||
...pipeline,
|
||||
});
|
||||
|
||||
export const deleteVoiceAssistantPipeline = (
|
||||
hass: HomeAssistant,
|
||||
pipelineId: string
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "voice_assistant/pipeline/delete",
|
||||
pipeline_id: pipelineId,
|
||||
});
|
||||
|
188
src/panels/config/voice-assistants/assist-pref.ts
Normal file
188
src/panels/config/voice-assistants/assist-pref.ts
Normal file
@ -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`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">Assist</h1>
|
||||
<div class="header-actions">
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/assist/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="icon-link"
|
||||
>
|
||||
<ha-icon-button
|
||||
label="Learn how it works"
|
||||
.path=${mdiHelpCircle}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<mwc-list>
|
||||
${this._pipelines.map(
|
||||
(pipeline) => html`
|
||||
<ha-list-item
|
||||
twoline
|
||||
hasMeta
|
||||
role="button"
|
||||
@click=${this._editPipeline}
|
||||
.id=${pipeline.id}
|
||||
>
|
||||
${pipeline.name}
|
||||
<span slot="secondary">${pipeline.language}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
<ha-button @click=${this._addPipeline}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.add_assistant"
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a
|
||||
href="/config/voice-assistants/expose?assistants=conversation&historyBack"
|
||||
>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.manage_entities"
|
||||
)}
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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);
|
@ -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<VoiceAssistantPipeline>;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
|
||||
@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`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.pipeline?.id
|
||||
? this._params.pipeline.name
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.detail.add_assistant_title"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<ha-form
|
||||
.schema=${this._schema()}
|
||||
.data=${this._data}
|
||||
.hass=${this.hass}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
${this._params.pipeline?.id
|
||||
? html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click=${this._deletePipeline}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updatePipeline}
|
||||
.disabled=${Boolean(this._error) || this._submitting}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${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"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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<ReturnType<typeof this._schema>>
|
||||
): 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<VoiceAssistantPipelineMutableParams> = {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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}
|
||||
>
|
||||
<div class="content">
|
||||
<assist-pref .hass=${this.hass}> </assist-pref>
|
||||
${this.cloudStatus?.logged_in
|
||||
? html`<cloud-alexa-pref
|
||||
.hass=${this.hass}
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
VoiceAssistantPipeline,
|
||||
VoiceAssistantPipelineMutableParams,
|
||||
} from "../../../data/voice_assistant";
|
||||
|
||||
export interface VoiceAssistantPipelineDetailsDialogParams {
|
||||
pipeline?: VoiceAssistantPipeline;
|
||||
createPipeline: (
|
||||
values: VoiceAssistantPipelineMutableParams
|
||||
) => Promise<unknown>;
|
||||
updatePipeline: (
|
||||
updates: Partial<VoiceAssistantPipelineMutableParams>
|
||||
) => Promise<unknown>;
|
||||
deletePipeline: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user