Compare commits

...
1 Commits
Author SHA1 Message Date
Maarten LakerveldandGitHub 5a0db4a796 Show Assist greeting in the assistant's language (#53707)
The greeting sits in the assistant's chat bubble, so render it in the pipeline's language instead of the interface language. Falls back to the interface language when no translation is available (issue #53703).
2026-08-20 12:49:40 +02:00
2 changed files with 95 additions and 10 deletions
+63 -9
View File
@@ -12,6 +12,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { transform } from "../common/decorators/transform";
import { supportsFeature } from "../common/entity/supports-feature";
import type { LocalizeFunc } from "../common/translations/localize";
import {
@@ -24,6 +25,7 @@ import {
import {
configContext,
connectionContext,
internationalizationContext,
statesContext,
} from "../data/context";
import { ConversationEntityFeature } from "../data/conversation";
@@ -33,8 +35,13 @@ import type {
HomeAssistant,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantInternationalization,
} from "../types";
import { AudioRecorder } from "../util/audio-recorder";
import {
findAvailableLanguage,
getTranslation,
} from "../util/common-translation";
import { documentationUrl } from "../util/documentation-url";
import "./ha-alert";
import "./ha-markdown";
@@ -67,6 +74,17 @@ export const assistPipelineChanged = (
current: AssistPipeline | undefined
): boolean => previous?.id !== current?.id;
export const greetingTranslationLanguage = (
pipelineLanguage: string | undefined,
interfaceLanguage: string | undefined
): string | undefined => {
if (!pipelineLanguage || pipelineLanguage === interfaceLanguage) {
return undefined;
}
const language = findAvailableLanguage(pipelineLanguage);
return language && language !== interfaceLanguage ? language : undefined;
};
@customElement("ha-assist-chat")
export class HaAssistChat extends LitElement {
@property({ attribute: false }) public pipeline?: AssistPipeline;
@@ -101,6 +119,13 @@ export class HaAssistChat extends LitElement {
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, string>({
transformer: ({ language }) => language,
})
private _language!: string;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HomeAssistant["states"];
@@ -115,6 +140,8 @@ export class HaAssistChat extends LitElement {
private _conversationId: string | null = null;
private _greetingLoadToken = 0;
private _initialPromptSubmitted = false;
private _audioRecorder?: AudioRecorder;
@@ -131,17 +158,44 @@ export class HaAssistChat extends LitElement {
(changedProperties.has("pipeline") &&
assistPipelineChanged(changedProperties.get("pipeline"), this.pipeline))
) {
this._conversation = [
{
who: "hass",
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
thinking: "",
tool_calls: {},
},
];
this._conversation = [];
this._loadGreeting();
}
}
private async _loadGreeting(): Promise<void> {
const token = ++this._greetingLoadToken;
const language = greetingTranslationLanguage(
this.pipeline?.language,
this._language
);
let greeting: string | undefined;
if (language) {
try {
const result = await getTranslation(null, language, false);
if (result.language === language) {
greeting = result.data["ui.dialogs.voice_command.how_can_i_help"];
}
} catch (_err) {
// Translation failed to load; fall back to the interface language.
}
}
if (token !== this._greetingLoadToken) {
// The pipeline changed while loading; a newer load owns the greeting.
return;
}
this._conversation = [
{
who: "hass",
text:
greeting || this._localize("ui.dialogs.voice_command.how_can_i_help"),
thinking: "",
tool_calls: {},
},
...this._conversation,
];
}
protected firstUpdated(changedProperties: PropertyValues<this>): void {
super.firstUpdated(changedProperties);
if (
@@ -157,7 +211,7 @@ export class HaAssistChat extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_conversation")) {
if (changedProps.has("_conversation") && this._conversation.length) {
this._scrollMessagesBottom();
}
if (
+32 -1
View File
@@ -1,10 +1,19 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import type { AssistPipeline } from "../../src/data/assist_pipeline";
import {
assistPipelineChanged,
greetingTranslationLanguage,
initialPromptToSubmit,
} from "../../src/components/ha-assist-chat";
// common-translation depends on build-time defines and generated translation
// metadata that are not available in unit tests.
vi.mock("../../src/util/common-translation", () => ({
findAvailableLanguage: (language: string) =>
({ en: "en", "en-US": "en", nl: "nl", pl: "pl" })[language],
getTranslation: vi.fn(),
}));
describe("initialPromptToSubmit", () => {
it("returns a trimmed prompt when submission is requested", () => {
expect(initialPromptToSubmit(" Turn on the lights ", true)).toBe(
@@ -36,3 +45,25 @@ describe("ha-assist-chat pipeline updates", () => {
).toBe(true);
});
});
describe("greetingTranslationLanguage", () => {
it("returns the pipeline language when it differs from the interface language", () => {
expect(greetingTranslationLanguage("pl", "en")).toBe("pl");
});
it("returns undefined when the pipeline language matches the interface language", () => {
expect(greetingTranslationLanguage("nl", "nl")).toBeUndefined();
});
it("returns undefined when the pipeline language resolves to the interface language", () => {
expect(greetingTranslationLanguage("en-US", "en")).toBeUndefined();
});
it("returns undefined when there is no pipeline language", () => {
expect(greetingTranslationLanguage(undefined, "en")).toBeUndefined();
});
it("returns undefined when the pipeline language has no available translation", () => {
expect(greetingTranslationLanguage("xx", "en")).toBeUndefined();
});
});