mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
AI Task automation save improvements (#26140)
* also assign category * Extract Suggest AI button * Add sick animation * Show AI Task pref but disabled if not done loading * Lint * Update progress wording * Define better interface * Add My panel * Adjust instructions to params.domain * Mention sentence capitalization * Update label when failure * Keep width during suggestion
This commit is contained in:
parent
c3bf1d8770
commit
ed8c9f5ce5
201
src/components/ha-suggest-with-ai-button.ts
Normal file
201
src/components/ha-suggest-with-ai-button.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import type { PropertyValues } from "lit";
|
||||||
|
import { html, css, LitElement, nothing } from "lit";
|
||||||
|
import { mdiStarFourPoints } from "@mdi/js";
|
||||||
|
|
||||||
|
import { customElement, state, property } from "lit/decorators";
|
||||||
|
import type {
|
||||||
|
AITaskPreferences,
|
||||||
|
GenDataTask,
|
||||||
|
GenDataTaskResult,
|
||||||
|
} from "../data/ai_task";
|
||||||
|
import { fetchAITaskPreferences, generateDataAITask } from "../data/ai_task";
|
||||||
|
import "./chips/ha-assist-chip";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
suggestion: GenDataTaskResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuggestWithAIGenerateTask {
|
||||||
|
type: "data";
|
||||||
|
task: GenDataTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-suggest-with-ai-button")
|
||||||
|
export class HaSuggestWithAIButton extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: "task-type" })
|
||||||
|
public taskType!: "data";
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
generateTask!: () => SuggestWithAIGenerateTask;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _aiPrefs?: AITaskPreferences;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _state: {
|
||||||
|
status: "idle" | "suggesting" | "error" | "done";
|
||||||
|
suggestionIndex: 1 | 2 | 3;
|
||||||
|
} = {
|
||||||
|
status: "idle",
|
||||||
|
suggestionIndex: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _minWidth?: string;
|
||||||
|
|
||||||
|
private _intervalId?: number;
|
||||||
|
|
||||||
|
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(_changedProperties);
|
||||||
|
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||||
|
this._aiPrefs = prefs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this._aiPrefs || !this._aiPrefs.gen_data_entity_id) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
let label: string;
|
||||||
|
switch (this._state.status) {
|
||||||
|
case "error":
|
||||||
|
label = this.hass.localize("ui.components.suggest_with_ai.error");
|
||||||
|
break;
|
||||||
|
case "done":
|
||||||
|
label = this.hass.localize("ui.components.suggest_with_ai.done");
|
||||||
|
break;
|
||||||
|
case "suggesting":
|
||||||
|
label = this.hass.localize(
|
||||||
|
`ui.components.suggest_with_ai.suggesting_${this._state.suggestionIndex}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
label = this.hass.localize("ui.components.suggest_with_ai.label");
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-assist-chip
|
||||||
|
@click=${this._suggest}
|
||||||
|
.label=${label}
|
||||||
|
?active=${this._state.status === "suggesting"}
|
||||||
|
class=${this._state.status === "error"
|
||||||
|
? "error"
|
||||||
|
: this._state.status === "done"
|
||||||
|
? "done"
|
||||||
|
: ""}
|
||||||
|
style=${this._minWidth ? `min-width: ${this._minWidth}` : ""}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiStarFourPoints}></ha-svg-icon>
|
||||||
|
</ha-assist-chip>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _suggest() {
|
||||||
|
if (!this.generateTask || this._state.status === "suggesting") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture current width before changing state
|
||||||
|
const chip = this.shadowRoot?.querySelector("ha-assist-chip");
|
||||||
|
if (chip) {
|
||||||
|
this._minWidth = `${chip.offsetWidth}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to suggesting state
|
||||||
|
this._state = {
|
||||||
|
status: "suggesting",
|
||||||
|
suggestionIndex: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start cycling through suggestion texts
|
||||||
|
this._intervalId = window.setInterval(() => {
|
||||||
|
this._state = {
|
||||||
|
...this._state,
|
||||||
|
suggestionIndex: ((this._state.suggestionIndex % 3) + 1) as 1 | 2 | 3,
|
||||||
|
};
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const info = await this.generateTask();
|
||||||
|
let result: GenDataTaskResult;
|
||||||
|
if (info.type === "data") {
|
||||||
|
result = await generateDataAITask(this.hass, info.task);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported task type");
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "suggestion", result);
|
||||||
|
|
||||||
|
// Show success state
|
||||||
|
this._state = {
|
||||||
|
...this._state,
|
||||||
|
status: "done",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Error generating AI suggestion:", error);
|
||||||
|
|
||||||
|
this._state = {
|
||||||
|
...this._state,
|
||||||
|
status: "error",
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (this._intervalId) {
|
||||||
|
clearInterval(this._intervalId);
|
||||||
|
this._intervalId = undefined;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this._state = {
|
||||||
|
...this._state,
|
||||||
|
status: "idle",
|
||||||
|
};
|
||||||
|
this._minWidth = undefined;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-assist-chip[active] {
|
||||||
|
animation: pulse-glow 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-assist-chip.error {
|
||||||
|
box-shadow: 0 0 12px 4px rgba(var(--rgb-error-color), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-assist-chip.done {
|
||||||
|
box-shadow: 0 0 12px 4px rgba(var(--rgb-primary-color), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(var(--rgb-primary-color), 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 8px 2px rgba(var(--rgb-primary-color), 0.6);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(var(--rgb-primary-color), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-suggest-with-ai-button": HaSuggestWithAIButton;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,13 @@ export interface AITaskPreferences {
|
|||||||
gen_data_entity_id: string | null;
|
gen_data_entity_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GenDataTask {
|
||||||
|
task_name: string;
|
||||||
|
entity_id?: string;
|
||||||
|
instructions: string;
|
||||||
|
structure?: AITaskStructure;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GenDataTaskResult<T = string> {
|
export interface GenDataTaskResult<T = string> {
|
||||||
conversation_id: string;
|
conversation_id: string;
|
||||||
data: T;
|
data: T;
|
||||||
@ -34,12 +41,7 @@ export const saveAITaskPreferences = (
|
|||||||
|
|
||||||
export const generateDataAITask = async <T = string>(
|
export const generateDataAITask = async <T = string>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
task: {
|
task: GenDataTask
|
||||||
task_name: string;
|
|
||||||
entity_id?: string;
|
|
||||||
instructions: string;
|
|
||||||
structure?: AITaskStructure;
|
|
||||||
}
|
|
||||||
): Promise<GenDataTaskResult<T>> => {
|
): Promise<GenDataTaskResult<T>> => {
|
||||||
const result = await hass.callService<GenDataTaskResult<T>>(
|
const result = await hass.callService<GenDataTaskResult<T>>(
|
||||||
"ai_task",
|
"ai_task",
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { mdiClose, mdiPlus, mdiStarFourPoints } from "@mdi/js";
|
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||||
import { dump } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-domain-icon";
|
import "../../../../components/ha-domain-icon";
|
||||||
import "../../../../components/ha-icon-picker";
|
import "../../../../components/ha-icon-picker";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
import "../../../../components/ha-textarea";
|
import "../../../../components/ha-textarea";
|
||||||
import "../../../../components/ha-textfield";
|
import "../../../../components/ha-textfield";
|
||||||
import "../../../../components/ha-labels-picker";
|
import "../../../../components/ha-labels-picker";
|
||||||
|
import "../../../../components/ha-suggest-with-ai-button";
|
||||||
|
import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button";
|
||||||
import "../../category/ha-category-picker";
|
import "../../category/ha-category-picker";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-expansion-panel";
|
||||||
import "../../../../components/chips/ha-chip-set";
|
import "../../../../components/chips/ha-chip-set";
|
||||||
@ -25,14 +28,12 @@ import type {
|
|||||||
SaveDialogParams,
|
SaveDialogParams,
|
||||||
} from "./show-dialog-automation-save";
|
} from "./show-dialog-automation-save";
|
||||||
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
||||||
import {
|
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||||
fetchAITaskPreferences,
|
|
||||||
generateDataAITask,
|
|
||||||
} from "../../../../data/ai_task";
|
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
|
||||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||||
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||||
|
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||||
|
import { fetchCategoryRegistry } from "../../../../data/category_registry";
|
||||||
|
|
||||||
@customElement("ha-dialog-automation-save")
|
@customElement("ha-dialog-automation-save")
|
||||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||||
@ -46,8 +47,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
|||||||
|
|
||||||
@state() private _entryUpdates!: EntityRegistryUpdate;
|
@state() private _entryUpdates!: EntityRegistryUpdate;
|
||||||
|
|
||||||
@state() private _canSuggest = false;
|
|
||||||
|
|
||||||
private _params!: SaveDialogParams;
|
private _params!: SaveDialogParams;
|
||||||
|
|
||||||
@state() private _newName?: string;
|
@state() private _newName?: string;
|
||||||
@ -92,15 +91,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
|
||||||
super.firstUpdated(changedProperties);
|
|
||||||
if (isComponentLoaded(this.hass, "ai_task")) {
|
|
||||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
|
||||||
this._canSuggest = prefs.gen_data_entity_id !== null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _renderOptionalChip(id: string, label: string) {
|
protected _renderOptionalChip(id: string, label: string) {
|
||||||
if (this._visibleOptionals.includes(id)) {
|
if (this._visibleOptionals.includes(id)) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -270,21 +260,12 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
|||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<span slot="title">${this._params.title || title}</span>
|
<span slot="title">${this._params.title || title}</span>
|
||||||
${this._canSuggest
|
<ha-suggest-with-ai-button
|
||||||
? html`
|
slot="actionItems"
|
||||||
<ha-assist-chip
|
.hass=${this.hass}
|
||||||
id="suggest"
|
.generateTask=${this._generateTask}
|
||||||
slot="actionItems"
|
@suggestion=${this._handleSuggestion}
|
||||||
@click=${this._suggest}
|
></ha-suggest-with-ai-button>
|
||||||
label=${this.hass.localize("ui.common.suggest_ai")}
|
|
||||||
>
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="icon"
|
|
||||||
.path=${mdiStarFourPoints}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</ha-assist-chip>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
${this._error
|
${this._error
|
||||||
? html`<ha-alert alert-type="error"
|
? html`<ha-alert alert-type="error"
|
||||||
@ -348,27 +329,43 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _suggest() {
|
private _getSuggestData() {
|
||||||
const labels = await subscribeOne(
|
return Promise.all([
|
||||||
this.hass.connection,
|
subscribeOne(this.hass.connection, subscribeLabelRegistry).then((labs) =>
|
||||||
subscribeLabelRegistry
|
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||||
).then((labs) =>
|
),
|
||||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
subscribeOne(this.hass.connection, subscribeEntityRegistry).then((ents) =>
|
||||||
);
|
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||||
const automationInspiration: string[] = [];
|
),
|
||||||
|
fetchCategoryRegistry(this.hass.connection, "automation").then((cats) =>
|
||||||
|
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
for (const automation of Object.values(this.hass.states)) {
|
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> => {
|
||||||
const entityEntry = this.hass.entities[automation.entity_id];
|
const [labels, entities, categories] = await this._getSuggestData();
|
||||||
|
const inspirations: string[] = [];
|
||||||
|
|
||||||
|
const domain = this._params.domain;
|
||||||
|
|
||||||
|
for (const entity of Object.values(this.hass.states)) {
|
||||||
|
const entityEntry = entities[entity.entity_id];
|
||||||
if (
|
if (
|
||||||
computeStateDomain(automation) !== "automation" ||
|
computeStateDomain(entity) !== domain ||
|
||||||
automation.attributes.restored ||
|
entity.attributes.restored ||
|
||||||
!automation.attributes.friendly_name ||
|
!entity.attributes.friendly_name ||
|
||||||
!entityEntry
|
!entityEntry
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inspiration = `- ${automation.attributes.friendly_name}`;
|
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||||
|
|
||||||
|
const category = categories[entityEntry.categories.automation];
|
||||||
|
if (category) {
|
||||||
|
inspiration += ` (category: ${category})`;
|
||||||
|
}
|
||||||
|
|
||||||
if (entityEntry.labels.length) {
|
if (entityEntry.labels.length) {
|
||||||
inspiration += ` (labels: ${entityEntry.labels
|
inspiration += ` (labels: ${entityEntry.labels
|
||||||
@ -376,59 +373,88 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
|||||||
.join(", ")})`;
|
.join(", ")})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
automationInspiration.push(inspiration);
|
inspirations.push(inspiration);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await generateDataAITask<{
|
const term = this._params.domain === "script" ? "script" : "automation";
|
||||||
name: string;
|
|
||||||
description: string | undefined;
|
|
||||||
labels: string[] | undefined;
|
|
||||||
}>(this.hass, {
|
|
||||||
task_name: "frontend:automation:save",
|
|
||||||
instructions: `Suggest in language "${this.hass.language}" a name, description, and labels for the following Home Assistant automation.
|
|
||||||
|
|
||||||
The name should be relevant to the automation's purpose.
|
return {
|
||||||
|
type: "data",
|
||||||
|
task: {
|
||||||
|
task_name: `frontend:${term}:save`,
|
||||||
|
instructions: `Suggest in language "${this.hass.language}" a name, description, category and labels for the following Home Assistant ${term}.
|
||||||
|
|
||||||
|
The name should be relevant to the ${term}'s purpose.
|
||||||
${
|
${
|
||||||
automationInspiration.length
|
inspirations.length
|
||||||
? `The name should be in same style as existing automations.
|
? `The name should be in same style and sentence capitalization as existing ${term}s.
|
||||||
Suggest labels if relevant to the automation's purpose.
|
Suggest a category and labels if relevant to the ${term}'s purpose.
|
||||||
Only suggest labels that are already used by existing automations.`
|
Only suggest category and labels that are already used by existing ${term}s.`
|
||||||
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
|
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
|
||||||
}
|
}
|
||||||
If the automation contains 5+ steps, include a short description.
|
If the ${term} contains 5+ steps, include a short description.
|
||||||
|
|
||||||
For inspiration, here are existing automations:
|
For inspiration, here are existing ${term}s:
|
||||||
${automationInspiration.join("\n")}
|
${inspirations.join("\n")}
|
||||||
|
|
||||||
|
The ${term} configuration is as follows:
|
||||||
|
|
||||||
The automation configuration is as follows:
|
|
||||||
${dump(this._params.config)}
|
${dump(this._params.config)}
|
||||||
`,
|
`,
|
||||||
structure: {
|
structure: {
|
||||||
name: {
|
name: {
|
||||||
description: "The name of the automation",
|
description: "The name of the automation",
|
||||||
required: true,
|
required: true,
|
||||||
selector: {
|
selector: {
|
||||||
text: {},
|
text: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
description: {
|
||||||
description: {
|
description: "A short description of the automation",
|
||||||
description: "A short description of the automation",
|
required: false,
|
||||||
required: false,
|
selector: {
|
||||||
selector: {
|
text: {},
|
||||||
text: {},
|
},
|
||||||
},
|
},
|
||||||
},
|
labels: {
|
||||||
labels: {
|
description: "Labels for the automation",
|
||||||
description: "Labels for the automation",
|
required: false,
|
||||||
required: false,
|
selector: {
|
||||||
selector: {
|
text: {
|
||||||
text: {
|
multiple: true,
|
||||||
multiple: true,
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
description: "The category of the automation",
|
||||||
|
required: false,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
options: Object.entries(categories).map(([id, name]) => ({
|
||||||
|
value: id,
|
||||||
|
label: name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _handleSuggestion(
|
||||||
|
event: CustomEvent<
|
||||||
|
GenDataTaskResult<{
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
|
labels?: string[];
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const result = event.detail;
|
||||||
|
const [labels, _entities, categories] = await this._getSuggestData();
|
||||||
|
|
||||||
this._newName = result.data.name;
|
this._newName = result.data.name;
|
||||||
if (result.data.description) {
|
if (result.data.description) {
|
||||||
this._newDescription = result.data.description;
|
this._newDescription = result.data.description;
|
||||||
@ -436,6 +462,21 @@ ${dump(this._params.config)}
|
|||||||
this._visibleOptionals = [...this._visibleOptionals, "description"];
|
this._visibleOptionals = [...this._visibleOptionals, "description"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (result.data.category) {
|
||||||
|
// We get back category name, convert it to ID
|
||||||
|
const categoryId = Object.entries(categories).find(
|
||||||
|
([, name]) => name === result.data.category
|
||||||
|
)?.[0];
|
||||||
|
if (categoryId) {
|
||||||
|
this._entryUpdates = {
|
||||||
|
...this._entryUpdates,
|
||||||
|
category: categoryId,
|
||||||
|
};
|
||||||
|
if (!this._visibleOptionals.includes("category")) {
|
||||||
|
this._visibleOptionals = [...this._visibleOptionals, "category"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (result.data.labels?.length) {
|
if (result.data.labels?.length) {
|
||||||
// We get back label names, convert them to IDs
|
// We get back label names, convert them to IDs
|
||||||
const newLabels: Record<string, undefined | string> = Object.fromEntries(
|
const newLabels: Record<string, undefined | string> = Object.fromEntries(
|
||||||
@ -535,7 +576,7 @@ ${dump(this._params.config)}
|
|||||||
--mdc-theme-primary: var(--error-color);
|
--mdc-theme-primary: var(--error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#suggest {
|
ha-suggest-with-ai-button {
|
||||||
margin: 8px 16px;
|
margin: 8px 16px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-settings-row";
|
import "../../../components/ha-settings-row";
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
type AITaskPreferences,
|
type AITaskPreferences,
|
||||||
} from "../../../data/ai_task";
|
} from "../../../data/ai_task";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
|
||||||
@customElement("ai-task-pref")
|
@customElement("ai-task-pref")
|
||||||
export class AITaskPref extends LitElement {
|
export class AITaskPref extends LitElement {
|
||||||
@ -25,16 +26,15 @@ export class AITaskPref extends LitElement {
|
|||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||||
this._prefs = prefs;
|
this._prefs = prefs;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._prefs) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card outlined>
|
<ha-card outlined>
|
||||||
<h1 class="card-header">
|
<h1 class="card-header">
|
||||||
@ -84,7 +84,9 @@ export class AITaskPref extends LitElement {
|
|||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
data-name="gen_data_entity_id"
|
data-name="gen_data_entity_id"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._prefs.gen_data_entity_id}
|
.disabled=${this._prefs === undefined &&
|
||||||
|
isComponentLoaded(this.hass, "ai_task")}
|
||||||
|
.value=${this._prefs?.gen_data_entity_id}
|
||||||
.includeDomains=${["ai_task"]}
|
.includeDomains=${["ai_task"]}
|
||||||
@value-changed=${this._handlePrefChange}
|
@value-changed=${this._handlePrefChange}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import { UNIT_C } from "../../../common/const";
|
import { UNIT_C } from "../../../common/const";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
@ -26,9 +26,9 @@ import { saveCoreConfig } from "../../../data/core";
|
|||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
import "./ai-task-pref";
|
import "./ai-task-pref";
|
||||||
|
import type { AITaskPref } from "./ai-task-pref";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|
||||||
|
|
||||||
@customElement("ha-config-section-general")
|
@customElement("ha-config-section-general")
|
||||||
class HaConfigSectionGeneral extends LitElement {
|
class HaConfigSectionGeneral extends LitElement {
|
||||||
@ -58,6 +58,8 @@ class HaConfigSectionGeneral extends LitElement {
|
|||||||
|
|
||||||
@state() private _updateUnits?: boolean;
|
@state() private _updateUnits?: boolean;
|
||||||
|
|
||||||
|
@query("ai-task-pref") private _aiTaskPref!: AITaskPref;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const canEdit = ["storage", "default"].includes(
|
const canEdit = ["storage", "default"].includes(
|
||||||
this.hass.config.config_source
|
this.hass.config.config_source
|
||||||
@ -267,12 +269,10 @@ class HaConfigSectionGeneral extends LitElement {
|
|||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
${isComponentLoaded(this.hass, "ai_task")
|
<ai-task-pref
|
||||||
? html`<ai-task-pref
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
.narrow=${this.narrow}
|
||||||
.narrow=${this.narrow}
|
></ai-task-pref>
|
||||||
></ai-task-pref>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
</hass-subpage>
|
</hass-subpage>
|
||||||
`;
|
`;
|
||||||
@ -293,6 +293,12 @@ class HaConfigSectionGeneral extends LitElement {
|
|||||||
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
|
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
|
||||||
this._name = this.hass.config.location_name;
|
this._name = this.hass.config.location_name;
|
||||||
this._updateUnits = true;
|
this._updateUnits = true;
|
||||||
|
|
||||||
|
if (window.location.hash === "#ai-task") {
|
||||||
|
this._aiTaskPref.updateComplete.then(() => {
|
||||||
|
this._aiTaskPref.scrollIntoView();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
@ -123,6 +123,9 @@ export const getMyRedirects = (): Redirects => ({
|
|||||||
component: "bluetooth",
|
component: "bluetooth",
|
||||||
redirect: "/config/bluetooth/visualization",
|
redirect: "/config/bluetooth/visualization",
|
||||||
},
|
},
|
||||||
|
config_ai_task: {
|
||||||
|
redirect: "/config/general/#ai-task",
|
||||||
|
},
|
||||||
config_bluetooth: {
|
config_bluetooth: {
|
||||||
component: "bluetooth",
|
component: "bluetooth",
|
||||||
redirect: "/config/bluetooth",
|
redirect: "/config/bluetooth",
|
||||||
|
@ -436,8 +436,7 @@
|
|||||||
"replace": "Replace",
|
"replace": "Replace",
|
||||||
"append": "Append",
|
"append": "Append",
|
||||||
"supports_markdown": "Supports {markdown_help_link}",
|
"supports_markdown": "Supports {markdown_help_link}",
|
||||||
"markdown": "Markdown",
|
"markdown": "Markdown"
|
||||||
"suggest_ai": "Suggest with AI"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"components": {
|
"components": {
|
||||||
@ -1219,6 +1218,14 @@
|
|||||||
},
|
},
|
||||||
"combo-box": {
|
"combo-box": {
|
||||||
"no_match": "No matching items found"
|
"no_match": "No matching items found"
|
||||||
|
},
|
||||||
|
"suggest_with_ai": {
|
||||||
|
"label": "Suggest with AI",
|
||||||
|
"suggesting_1": "Analyzing…",
|
||||||
|
"suggesting_2": "Suggesting…",
|
||||||
|
"suggesting_3": "Enchanting…",
|
||||||
|
"done": "Done!",
|
||||||
|
"error": "Fail!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user