From 517a0a62231938a99ee32556ca843f3de7ebb786 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jul 2025 13:03:38 +0000 Subject: [PATCH] Extract Suggest AI button --- src/components/ha-suggest-with-ai-button.ts | 86 +++++++++++++++++++ src/data/ai_task.ts | 14 +-- .../dialog-automation-save.ts | 70 ++++++++------- src/translations/en.json | 3 +- 4 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 src/components/ha-suggest-with-ai-button.ts diff --git a/src/components/ha-suggest-with-ai-button.ts b/src/components/ha-suggest-with-ai-button.ts new file mode 100644 index 0000000000..ff63e5f36e --- /dev/null +++ b/src/components/ha-suggest-with-ai-button.ts @@ -0,0 +1,86 @@ +import type { PropertyValues } from "lit"; +import { html, 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 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; + } +} + +@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!: () => GenDataTask; + + @state() + private _aiPrefs?: AITaskPreferences; + + @state() + private _suggesting = false; + + 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; + } + return html` + + + + `; + } + + private async _suggest() { + if (!this.generateTask || this._suggesting) { + return; + } + try { + this._suggesting = true; + const task = await this.generateTask(); + const result = await generateDataAITask(this.hass, task); + fireEvent(this, "suggestion", result); + } finally { + this._suggesting = false; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-suggest-with-ai-button": HaSuggestWithAIButton; + } +} diff --git a/src/data/ai_task.ts b/src/data/ai_task.ts index c89f6607fe..83f0293e7a 100644 --- a/src/data/ai_task.ts +++ b/src/data/ai_task.ts @@ -5,6 +5,13 @@ export interface AITaskPreferences { gen_data_entity_id: string | null; } +export interface GenDataTask { + task_name: string; + entity_id?: string; + instructions: string; + structure?: AITaskStructure; +} + export interface GenDataTaskResult { conversation_id: string; data: T; @@ -34,12 +41,7 @@ export const saveAITaskPreferences = ( export const generateDataAITask = async ( hass: HomeAssistant, - task: { - task_name: string; - entity_id?: string; - instructions: string; - structure?: AITaskStructure; - } + task: GenDataTask ): Promise> => { const result = await hass.callService>( "ai_task", diff --git a/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts b/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts index c888f0956d..7eb1b3f89e 100644 --- a/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts +++ b/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts @@ -11,6 +11,7 @@ import "../../../../components/ha-icon-picker"; import "../../../../components/ha-textarea"; import "../../../../components/ha-textfield"; import "../../../../components/ha-labels-picker"; +import "../../../../components/ha-suggest-with-ai-button"; import "../../category/ha-category-picker"; import "../../../../components/ha-expansion-panel"; import "../../../../components/chips/ha-chip-set"; @@ -27,6 +28,8 @@ import type { import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support"; import { fetchAITaskPreferences, + GenDataTask, + GenDataTaskResult, generateDataAITask, } from "../../../../data/ai_task"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; @@ -48,8 +51,6 @@ class DialogAutomationSave extends LitElement implements HassDialog { @state() private _entryUpdates!: EntityRegistryUpdate; - @state() private _canSuggest = false; - private _params!: SaveDialogParams; @state() private _newName?: string; @@ -94,15 +95,6 @@ class DialogAutomationSave extends LitElement implements HassDialog { 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) { if (this._visibleOptionals.includes(id)) { return nothing; @@ -272,21 +264,12 @@ class DialogAutomationSave extends LitElement implements HassDialog { .path=${mdiClose} > ${this._params.title || title} - ${this._canSuggest - ? html` - - - - ` - : nothing} + ${this._error ? html` Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name])) ), @@ -362,6 +345,10 @@ class DialogAutomationSave extends LitElement implements HassDialog { Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name])) ), ]); + } + + private _generateTask = async (): Promise => { + const [labels, entities, categories] = await this._getSuggestData(); const automationInspiration: string[] = []; for (const automation of Object.values(this.hass.states)) { @@ -391,12 +378,7 @@ class DialogAutomationSave extends LitElement implements HassDialog { automationInspiration.push(inspiration); } - const result = await generateDataAITask<{ - name: string; - description: string | undefined; - labels: string[] | undefined; - category: string | undefined; - }>(this.hass, { + return { task_name: "frontend:automation:save", instructions: `Suggest in language "${this.hass.language}" a name, description, category and labels for the following Home Assistant automation. @@ -454,7 +436,22 @@ ${dump(this._params.config)} }, }, }, - }); + }; + }; + + 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; if (result.data.description) { this._newDescription = result.data.description; @@ -463,6 +460,7 @@ ${dump(this._params.config)} } } if (result.data.category) { + // TODO search up category ID this._entryUpdates = { ...this._entryUpdates, category: result.data.category, @@ -570,7 +568,7 @@ ${dump(this._params.config)} --mdc-theme-primary: var(--error-color); } - #suggest { + ha-suggest-with-ai-button { margin: 8px 16px; } `, diff --git a/src/translations/en.json b/src/translations/en.json index 4d556e38d7..c999d3242e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -437,7 +437,8 @@ "append": "Append", "supports_markdown": "Supports {markdown_help_link}", "markdown": "Markdown", - "suggest_ai": "Suggest with AI" + "suggest_ai": "Suggest with AI", + "suggesting_ai": "Suggesting…" }, "components": {