Files
frontend/src/components/ha-suggest-with-ai-button.ts
Paulus Schoutsen ed8c9f5ce5 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
2025-07-16 08:20:37 +03:00

202 lines
5.1 KiB
TypeScript

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;
}
}