From 29a3d67e4888a092220e14db72fad2f20c61a7ac Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 28 Jan 2026 15:35:24 +0000 Subject: [PATCH] AI suggestions: Areas (#29090) --- .../areas/dialog-area-registry-detail.ts | 102 ++++++- .../dialog-automation-save.ts | 26 +- .../config/common/suggest-metadata-ai.ts | 278 ++++++++---------- .../config/common/suggest-metadata-helpers.ts | 72 +++++ .../common/suggest-metadata-inspirations.ts | 82 ++++++ .../scene-save-dialog/dialog-scene-save.ts | 26 +- 6 files changed, 409 insertions(+), 177 deletions(-) create mode 100644 src/panels/config/common/suggest-metadata-helpers.ts create mode 100644 src/panels/config/common/suggest-metadata-inspirations.ts diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 60ff671fe7..92162d8877 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -16,8 +16,11 @@ import "../../../components/ha-labels-picker"; import "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import "../../../components/ha-settings-row"; +import "../../../components/ha-suggest-with-ai-button"; +import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button"; import "../../../components/ha-textfield"; import "../../../components/ha-wa-dialog"; +import type { GenDataTaskResult } from "../../../data/ai_task"; import type { AreaRegistryEntry, AreaRegistryEntryMutableParams, @@ -32,6 +35,14 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant, ValueChangedEvent } from "../../../types"; +import { + type MetadataSuggestionInclude, + type MetadataSuggestionResult, + generateMetadataSuggestionTask, + processMetadataSuggestion, +} from "../common/suggest-metadata-ai"; +import { fetchLabels } from "../common/suggest-metadata-helpers"; +import { buildAreaMetadataInspirations } from "../common/suggest-metadata-inspirations"; import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; const cropOptions: CropOptions = { @@ -75,6 +86,12 @@ class DialogAreaDetail @state() private _open = false; + @state() private _suggestionInclude: MetadataSuggestionInclude = { + name: true, + labels: true, + floor: true, + }; + public async showDialog( params: AreaRegistryDetailDialogParams ): Promise { @@ -242,6 +259,76 @@ class DialogAreaDetail `; } + private async _getLabelNames(): Promise { + if (!this._labels.length) { + return []; + } + const labels = await fetchLabels(this.hass.connection); + return this._labels + .map((labelId) => labels[labelId]) + .filter((name): name is string => Boolean(name)); + } + + private _generateTask = async (): Promise => { + this._suggestionInclude = { + ...this._suggestionInclude, + name: this._name.trim() === "", + }; + + return generateMetadataSuggestionTask<{ + name: string; + aliases: string[]; + labels: string[]; + floor: string | null; + temperature_entity: string | null; + humidity_entity: string | null; + }>( + this.hass.connection, + this.hass.language, + "area", + { + name: this._name, + aliases: this._aliases, + labels: await this._getLabelNames(), + floor: this._floor ? this.hass.floors?.[this._floor]?.name : null, + temperature_entity: this._temperatureEntity + ? (this.hass.states[this._temperatureEntity]?.attributes + ?.friendly_name ?? null) + : null, + humidity_entity: this._humidityEntity + ? (this.hass.states[this._humidityEntity]?.attributes + ?.friendly_name ?? null) + : null, + }, + await buildAreaMetadataInspirations(this.hass.connection), + this._suggestionInclude + ); + }; + + private async _handleSuggestion( + event: CustomEvent> + ) { + const result = event.detail; + const processed = await processMetadataSuggestion( + this.hass.connection, + "area", + result, + this._suggestionInclude + ); + + if (processed.name) { + this._name = processed.name; + } + + if (processed.labels?.length) { + this._labels = processed.labels; + } + + if (processed.floor) { + this._floor = processed.floor; + } + } + protected render() { if (!this._params) { return nothing; @@ -259,6 +346,12 @@ class DialogAreaDetail : this.hass.localize("ui.panel.config.areas.editor.create_area")} @closed=${this._dialogClosed} > +
${this._error ? html`${this._error}` @@ -423,13 +516,16 @@ class DialogAreaDetail ha-picture-upload, ha-expansion-panel { display: block; - margin-bottom: 16px; + margin-bottom: var(--ha-space-4); } .content { - padding: 12px; + padding: var(--ha-space-3); } .description { - margin: 0 0 16px 0; + margin: 0 0 var(--ha-space-4) 0; + } + ha-suggest-with-ai-button { + margin: var(--ha-space-2) var(--ha-space-4); } `, ]; 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 5513becc5c..25ed3ec723 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 @@ -33,10 +33,10 @@ import type { } from "./show-dialog-automation-save"; import { type MetadataSuggestionResult, - SUGGESTION_INCLUDE_ALL, generateMetadataSuggestionTask, processMetadataSuggestion, } from "../../common/suggest-metadata-ai"; +import { buildEntityMetadataInspirations } from "../../common/suggest-metadata-inspirations"; @customElement("ha-dialog-automation-save") class DialogAutomationSave extends LitElement implements HassDialog { @@ -341,10 +341,14 @@ class DialogAutomationSave extends LitElement implements HassDialog { } return generateMetadataSuggestionTask( this.hass.connection, - this.hass.states, this.hass.language, this._params.domain, - this._params.config + this._params.config, + await buildEntityMetadataInspirations( + this.hass.connection, + this.hass.states, + this._params.domain + ) ); }; @@ -358,11 +362,12 @@ class DialogAutomationSave extends LitElement implements HassDialog { const processed = await processMetadataSuggestion( this.hass.connection, this._params.domain, - result, - SUGGESTION_INCLUDE_ALL + result ); - this._newName = processed.name; + if (processed.name) { + this._newName = processed.name; + } if (processed.description) { this._newDescription = processed.description; @@ -432,7 +437,8 @@ class DialogAutomationSave extends LitElement implements HassDialog { haStyleDialog, css` ha-wa-dialog { - --dialog-content-padding: 0 24px 24px 24px; + --dialog-content-padding: 0 var(--ha-space-6) var(--ha-space-6) + var(--ha-space-6); } ha-textfield, @@ -448,15 +454,15 @@ class DialogAutomationSave extends LitElement implements HassDialog { ha-labels-picker, ha-area-picker, ha-chip-set:has(> ha-assist-chip) { - margin-top: 16px; + margin-top: var(--ha-space-4); } ha-alert { display: block; - margin-bottom: 16px; + margin-bottom: var(--ha-space-4); } ha-suggest-with-ai-button { - margin: 8px 16px; + margin: var(--ha-space-2) var(--ha-space-4); } `, ]; diff --git a/src/panels/config/common/suggest-metadata-ai.ts b/src/panels/config/common/suggest-metadata-ai.ts index 4ad93246c4..8d8dac5025 100644 --- a/src/panels/config/common/suggest-metadata-ai.ts +++ b/src/panels/config/common/suggest-metadata-ai.ts @@ -1,163 +1,84 @@ import { dump } from "js-yaml"; -import { computeDomain } from "../../../common/entity/compute_domain"; -import { subscribeOne } from "../../../common/util/subscribe-one"; import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task"; -import { fetchCategoryRegistry } from "../../../data/category_registry"; -import { - subscribeEntityRegistry, - type EntityRegistryEntry, -} from "../../../data/entity/entity_registry"; -import { subscribeLabelRegistry } from "../../../data/label/label_registry"; import type { HomeAssistant } from "../../../types"; import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button"; +import { + fetchCategories, + fetchFloors, + fetchLabels, +} from "./suggest-metadata-helpers"; export interface MetadataSuggestionResult { - name: string; + name?: string; description?: string; category?: string; labels?: string[]; + floor?: string; } -export type MetadataSuggestionDomain = "automation" | "script" | "scene"; +export type MetadataSuggestionDomain = + | "automation" + | "script" + | "scene" + | "area"; export interface MetadataSuggestionInclude { + name: boolean; description?: boolean; categories?: boolean; labels?: boolean; + floor?: boolean; } -type Categories = Record; -type Entities = Record; -type Labels = Record; - -export const SUGGESTION_INCLUDE_ALL: MetadataSuggestionInclude = { +export const SUGGESTION_INCLUDE_DEFAULT: MetadataSuggestionInclude = { + name: true, description: true, categories: true, labels: true, } as const; -const tryCatchEmptyObject = (promise: Promise): Promise => - promise.catch((err) => { - // eslint-disable-next-line no-console - console.error("Error fetching data for suggestion: ", err); - return {} as T; - }); - -const fetchCategories = ( - connection: HomeAssistant["connection"], - domain: MetadataSuggestionDomain -): Promise => - tryCatchEmptyObject( - fetchCategoryRegistry(connection, domain).then((cats) => - Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name])) - ) - ); - -const fetchEntities = ( - connection: HomeAssistant["connection"] -): Promise => - tryCatchEmptyObject( - subscribeOne(connection, subscribeEntityRegistry).then((ents) => - Object.fromEntries(ents.map((ent) => [ent.entity_id, ent])) - ) - ); - -const fetchLabels = ( - connection: HomeAssistant["connection"] -): Promise => - tryCatchEmptyObject( - subscribeOne(connection, subscribeLabelRegistry).then((labs) => - Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name])) - ) - ); - -function buildMetadataInspirations( - domain: MetadataSuggestionDomain, - states: HomeAssistant["states"], - entities: Entities, - categories?: Categories, - labels?: Labels -): string[] { - const inspirations: string[] = []; - - for (const entityId of Object.keys(entities)) { - const entityEntry = entities[entityId]; - if (!entityEntry || computeDomain(entityId) !== domain) { - continue; - } - - const entity = states[entityId]; - if ( - !entity || - entity.attributes.restored || - !entity.attributes.friendly_name - ) { - continue; - } - - let inspiration = `- ${entity.attributes.friendly_name}`; - - // Get the category for this domain - if (categories && categories[entityEntry.categories[domain]]) { - inspiration += ` (category: ${categories[entityEntry.categories[domain]]})`; - } - - if (labels && entityEntry.labels.length) { - inspiration += ` (labels: ${entityEntry.labels - .map((label) => labels[label]) - .join(", ")})`; - } - - inspirations.push(inspiration); - } - - return inspirations; -} +// Always English to format lists in the prompt +const PROMPT_LIST_FORMAT = new Intl.ListFormat("en", { + style: "long", + type: "conjunction", +}); /** - * Generates an AI task for suggesting metadata - * for automations or scripts based on their configuration. + * Generates an AI task for suggesting metadata based on their configuration. * * @param connection - Home Assistant connection - * @param states - Current state objects * @param language - User's language preference - * @param domain - The domain to suggest metadata for (automation, script) + * @param domain - The domain to suggest metadata for * @param config - The configuration to suggest metadata for + * @param inspirations - Existing entries to use as inspiration * @param include - The metadata fields to include in the suggestion * @returns Promise resolving to the AI task structure */ export async function generateMetadataSuggestionTask( connection: HomeAssistant["connection"], - states: HomeAssistant["states"], language: HomeAssistant["language"], domain: MetadataSuggestionDomain, config: T, - include = SUGGESTION_INCLUDE_ALL + inspirations: string[] = [], + include = SUGGESTION_INCLUDE_DEFAULT ): Promise { - const [categories, entities, labels] = await Promise.all([ + const [categories, floors] = await Promise.all([ include.categories ? fetchCategories(connection, domain) : Promise.resolve(undefined), - fetchEntities(connection), - include.labels ? fetchLabels(connection) : Promise.resolve(undefined), + include.floor ? fetchFloors(connection) : Promise.resolve(undefined), ]); - const inspirations = buildMetadataInspirations( - domain, - states, - entities, - categories, - labels - ); - const structure: AITaskStructure = { - name: { - description: `The name of the ${domain}`, - required: true, - selector: { - text: {}, + ...(include.name && { + name: { + description: `The name of the ${domain}`, + required: true, + selector: { + text: {}, + }, }, - }, + }), ...(include.description && { description: { description: `A short description of the ${domain}`, @@ -193,49 +114,83 @@ export async function generateMetadataSuggestionTask( }, }, }), + ...(include.floor && + floors && { + floor: { + description: `The floor of the ${domain}`, + required: false, + selector: { + select: { + options: Object.values(floors).map((floor) => ({ + value: floor.floor_id, + label: floor.name, + })), + }, + }, + }, + }), }; - const categoryLabelText: string[] = []; - if (include.categories) { - categoryLabelText.push("category"); - } - if (include.labels) { - categoryLabelText.push("labels"); - } - const categoryLabelString = - categoryLabelText.length > 0 ? `, ${categoryLabelText.join(" and ")}` : ""; + const requestedParts = [ + include.name ? "a name" : null, + include.description ? "a description" : null, + include.categories ? "a category" : null, + include.labels ? "labels" : null, + include.floor ? "a floor" : null, + ].filter((entry): entry is string => entry !== null); + + const categoryLabels: string[] = [ + include.categories ? "category" : null, + include.labels ? "labels" : null, + include.floor ? "floor" : null, + ].filter((entry): entry is string => entry !== null); + + const categoryLabelsText = PROMPT_LIST_FORMAT.format(categoryLabels); + + const requestedPartsText = requestedParts.length + ? PROMPT_LIST_FORMAT.format(requestedParts) + : "suggestions"; return { type: "data", task: { task_name: `frontend__${domain}__save`, - instructions: `Suggest in language "${language}" a name${include.description ? ", description" : ""}${categoryLabelString} for the following Home Assistant ${domain}. - -The name should be relevant to the ${domain}'s purpose. -${ - inspirations.length - ? `The name should be in same style and sentence capitalization as existing ${domain}s.${ - include.categories || include.labels - ? ` -Suggest ${categoryLabelText.join(" and ")} if relevant to the ${domain}'s purpose. -Only suggest ${categoryLabelText.join(" and ")} that are already used by existing ${domain}s.` - : "" - }` - : `The name should be short, descriptive, sentence case, and written in the language ${language}.` -}${ - include.description - ? ` -If the ${domain} contains 5+ steps, include a short description.` - : "" - } - -For inspiration, here are existing ${domain}s: -${inspirations.join("\n")} - -The ${domain} configuration is as follows: - -${dump(config)} -`, + instructions: [ + `Suggest in language "${language}" ${requestedPartsText} for the following Home Assistant ${domain}.`, + "", + include.name + ? `The name should be relevant to the ${domain}'s purpose.` + : `The suggestions should be relevant to the ${domain}'s purpose.`, + ...(inspirations.length + ? [ + ...(include.name + ? [ + `The name should be in same style and sentence capitalization as existing ${domain}s.`, + ] + : []), + ...(include.categories || include.labels || include.floor + ? [ + `Suggest ${categoryLabelsText} if relevant to the ${domain}'s purpose.`, + `Only suggest ${categoryLabelsText} that are already used by existing ${domain}s.`, + ] + : []), + ] + : include.name + ? [ + `The name should be short, descriptive, sentence case, and written in the language ${language}.`, + ] + : []), + ...(include.description + ? [`If the ${domain} contains 5+ steps, include a short description.`] + : []), + "", + `For inspiration, here are existing ${domain}s:`, + inspirations.join("\n"), + "", + `The ${domain} configuration is as follows:`, + "", + `${dump(config)}`, + ].join("\n"), structure, }, }; @@ -243,7 +198,7 @@ ${dump(config)} /** * Processes the result of an AI task for suggesting metadata - * for automations or scripts based on their configuration. + * based on their configuration. * * @param connection - Home Assistant connection * @param domain - The domain of the ${domain} @@ -255,17 +210,18 @@ export async function processMetadataSuggestion( connection: HomeAssistant["connection"], domain: MetadataSuggestionDomain, result: GenDataTaskResult, - include: MetadataSuggestionInclude + include = SUGGESTION_INCLUDE_DEFAULT ): Promise { - const [categories, labels] = await Promise.all([ + const [categories, labels, floors] = await Promise.all([ include.categories ? fetchCategories(connection, domain) : Promise.resolve(undefined), include.labels ? fetchLabels(connection) : Promise.resolve(undefined), + include.floor ? fetchFloors(connection) : Promise.resolve(undefined), ]); const processed: MetadataSuggestionResult = { - name: result.data.name, + name: include.name ? result.data.name : undefined, description: include.description ? result.data.description : undefined, }; @@ -302,5 +258,17 @@ export async function processMetadataSuggestion( } } + if (include.floor && floors && result.data.floor) { + const floorId = + result.data.floor in floors + ? result.data.floor + : Object.entries(floors).find( + ([, floor]) => floor.name === result.data.floor + )?.[0]; + if (floorId) { + processed.floor = floorId; + } + } + return processed; } diff --git a/src/panels/config/common/suggest-metadata-helpers.ts b/src/panels/config/common/suggest-metadata-helpers.ts new file mode 100644 index 0000000000..c45865f970 --- /dev/null +++ b/src/panels/config/common/suggest-metadata-helpers.ts @@ -0,0 +1,72 @@ +import { subscribeOne } from "../../../common/util/subscribe-one"; +import { subscribeAreaRegistry } from "../../../data/area/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; +import { fetchCategoryRegistry } from "../../../data/category_registry"; +import { + subscribeEntityRegistry, + type EntityRegistryEntry, +} from "../../../data/entity/entity_registry"; +import { subscribeFloorRegistry } from "../../../data/ws-floor_registry"; +import type { FloorRegistryEntry } from "../../../data/floor_registry"; +import { subscribeLabelRegistry } from "../../../data/label/label_registry"; +import type { HomeAssistant } from "../../../types"; +import type { MetadataSuggestionDomain } from "./suggest-metadata-ai"; + +export type Categories = Record; +export type Entities = Record; +export type Labels = Record; +export type Floors = Record; +export type Areas = Record; + +const tryCatchEmptyObject = (promise: Promise): Promise => + promise.catch((err) => { + // eslint-disable-next-line no-console + console.error("Error fetching data for suggestion: ", err); + return {} as T; + }); + +export const fetchCategories = ( + connection: HomeAssistant["connection"], + domain: MetadataSuggestionDomain +): Promise => + tryCatchEmptyObject( + fetchCategoryRegistry(connection, domain).then((cats) => + Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name])) + ) + ); + +export const fetchLabels = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeLabelRegistry).then((labs) => + Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name])) + ) + ); + +export const fetchFloors = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeFloorRegistry).then((floors) => + Object.fromEntries(floors.map((floor) => [floor.floor_id, floor])) + ) + ); + +export const fetchAreas = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeAreaRegistry).then((areas) => + Object.fromEntries(areas.map((area) => [area.area_id, area])) + ) + ); + +export const fetchEntities = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeEntityRegistry).then((ents) => + Object.fromEntries(ents.map((ent) => [ent.entity_id, ent])) + ) + ); diff --git a/src/panels/config/common/suggest-metadata-inspirations.ts b/src/panels/config/common/suggest-metadata-inspirations.ts new file mode 100644 index 0000000000..fd711e6528 --- /dev/null +++ b/src/panels/config/common/suggest-metadata-inspirations.ts @@ -0,0 +1,82 @@ +import { computeDomain } from "../../../common/entity/compute_domain"; +import type { HomeAssistant } from "../../../types"; +import type { MetadataSuggestionDomain } from "./suggest-metadata-ai"; +import { + fetchAreas, + fetchCategories, + fetchEntities, + fetchFloors, + fetchLabels, +} from "./suggest-metadata-helpers"; + +export const buildEntityMetadataInspirations = async ( + connection: HomeAssistant["connection"], + states: HomeAssistant["states"], + domain: MetadataSuggestionDomain +): Promise => { + const [categories, entities, labels] = await Promise.all([ + fetchCategories(connection, domain), + fetchEntities(connection), + fetchLabels(connection), + ]); + + return Object.values(entities).reduce((inspirations, entry) => { + if (!entry || computeDomain(entry.entity_id) !== domain) { + return inspirations; + } + + const entity = states[entry.entity_id]; + if ( + !entity || + entity.attributes.restored || + !entity.attributes.friendly_name + ) { + return inspirations; + } + + let inspiration = `- ${entity.attributes.friendly_name}`; + + const category = entry.categories[domain]; + if (category && categories[category]) { + inspiration += ` (category: ${categories[category]})`; + } + + if (entry.labels.length) { + const labelNames = entry.labels + .map((labelId) => labels[labelId]) + .filter(Boolean); + if (labelNames.length) { + inspiration += ` (labels: ${labelNames.join(", ")})`; + } + } + + inspirations.push(inspiration); + return inspirations; + }, []); +}; + +export const buildAreaMetadataInspirations = async ( + connection: HomeAssistant["connection"] +): Promise => { + const [labels, floors, areas] = await Promise.all([ + fetchLabels(connection), + fetchFloors(connection), + fetchAreas(connection), + ]); + + return Object.values(areas).reduce((inspirations, area) => { + if (!area.floor_id) { + return inspirations; + } + + const floorName = floors[area.floor_id]?.name; + const labelNames = area.labels + .map((labelId) => labels[labelId]) + .filter(Boolean); + + inspirations.push( + `- ${area.name} (${floorName ? `floor: ${floorName}` : "no floor"}${labelNames.length ? `, labels: ${labelNames.join(", ")}` : ""})` + ); + return inspirations; + }, []); +}; diff --git a/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts b/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts index 4ebfbd1e89..1c21c818bd 100644 --- a/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts +++ b/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts @@ -33,9 +33,11 @@ import { generateMetadataSuggestionTask, processMetadataSuggestion, } from "../../common/suggest-metadata-ai"; +import { buildEntityMetadataInspirations } from "../../common/suggest-metadata-inspirations"; +import type { SceneConfig } from "../../../../data/scene"; -const SUGGESTION_CONFIG: MetadataSuggestionInclude = { - description: false, +const SUGGESTION_INCLUDE: MetadataSuggestionInclude = { + name: true, categories: true, labels: true, }; @@ -281,13 +283,17 @@ class DialogSceneSave extends LitElement { } private _generateTask = async (): Promise => - generateMetadataSuggestionTask( + generateMetadataSuggestionTask( this.hass.connection, - this.hass.states, this.hass.language, "scene", this._params.config, - SUGGESTION_CONFIG + await buildEntityMetadataInspirations( + this.hass.connection, + this.hass.states, + "scene" + ), + SUGGESTION_INCLUDE ); private async _handleSuggestion( @@ -298,12 +304,14 @@ class DialogSceneSave extends LitElement { this.hass.connection, "scene", result, - SUGGESTION_CONFIG + SUGGESTION_INCLUDE ); - this._newName = processed.name; - if (this._error && this._newName.trim()) { - this._error = false; + if (processed.name) { + this._newName = processed.name; + if (this._error && this._newName.trim()) { + this._error = false; + } } if (processed.category) {