mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-22 11:06:55 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69fa1b1963 | |||
| 29a548d610 | |||
| b92d81e510 | |||
| e3d5f796a1 | |||
| 8f020afba6 | |||
| b48c044573 | |||
| b1622a8273 | |||
| 4be7a32148 |
@@ -1,36 +0,0 @@
|
||||
name: Prepare dependencies
|
||||
description: Install and cache the complete dependency tree
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Check for complete dependency tree
|
||||
id: dependencies
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Setup Node and install
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
cache: false
|
||||
|
||||
- name: Save complete dependency tree
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
@@ -30,8 +30,34 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Prepare dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
- name: Check for complete dependency tree
|
||||
id: dependencies
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
lookup-only: true
|
||||
- name: Setup Node and install
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
cache: false
|
||||
- name: Save complete dependency tree
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
|
||||
lint:
|
||||
name: Lint and check format
|
||||
|
||||
@@ -22,40 +22,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare-dependencies:
|
||||
name: Prepare dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
|
||||
prepare-container-dependencies:
|
||||
name: Prepare container dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
options: --user 1001
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
|
||||
# ── Build the demo once and share it across test jobs via artifact ──────────
|
||||
build-demo:
|
||||
name: Build demo
|
||||
needs: prepare-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
@@ -63,10 +32,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build demo
|
||||
uses: ./.github/actions/build
|
||||
@@ -86,7 +53,6 @@ jobs:
|
||||
# ── Build the e2e test app and share it via artifact ────────────────────────
|
||||
build-e2e-test-app:
|
||||
name: Build e2e test app
|
||||
needs: prepare-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
@@ -94,10 +60,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build e2e test app
|
||||
uses: ./.github/actions/build
|
||||
@@ -117,7 +81,6 @@ jobs:
|
||||
# ── Build the gallery and share it via artifact ─────────────────────────────
|
||||
build-gallery:
|
||||
name: Build gallery
|
||||
needs: prepare-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
@@ -125,10 +88,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build gallery
|
||||
uses: ./.github/actions/build
|
||||
@@ -149,7 +110,6 @@ jobs:
|
||||
name: E2E demo (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
needs:
|
||||
- build-demo
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
@@ -172,10 +132,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -200,7 +158,6 @@ jobs:
|
||||
name: E2E app (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
needs:
|
||||
- build-e2e-test-app
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
@@ -225,10 +182,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -253,7 +208,6 @@ jobs:
|
||||
name: E2E gallery (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
needs:
|
||||
- build-gallery
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
@@ -278,10 +232,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -319,10 +271,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Download demo blob reports
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
|
||||
@@ -57,6 +57,16 @@ interface AssistMessage {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export const initialPromptToSubmit = (
|
||||
prompt: string | undefined,
|
||||
submit: boolean
|
||||
): string | undefined => (submit ? prompt?.trim() || undefined : undefined);
|
||||
|
||||
export const assistPipelineChanged = (
|
||||
previous: AssistPipeline | undefined,
|
||||
current: AssistPipeline | undefined
|
||||
): boolean => previous?.id !== current?.id;
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
@@ -67,6 +77,12 @@ export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public startListening?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public initialPrompt?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public submitInitialPrompt = false;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaInput;
|
||||
|
||||
@query(".message:last-child")
|
||||
@@ -99,6 +115,8 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _initialPromptSubmitted = false;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
|
||||
private _audioBuffer?: Int16Array[];
|
||||
@@ -108,7 +126,11 @@ export class HaAssistChat extends LitElement {
|
||||
private _stt_binary_handler_id?: number | null;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (!this.hasUpdated || changedProperties.has("pipeline")) {
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
(changedProperties.has("pipeline") &&
|
||||
assistPipelineChanged(changedProperties.get("pipeline"), this.pipeline))
|
||||
) {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
@@ -138,6 +160,20 @@ export class HaAssistChat extends LitElement {
|
||||
if (changedProps.has("_conversation")) {
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
if (
|
||||
!this._initialPromptSubmitted &&
|
||||
(changedProps.has("initialPrompt") ||
|
||||
changedProps.has("submitInitialPrompt"))
|
||||
) {
|
||||
const prompt = initialPromptToSubmit(
|
||||
this.initialPrompt,
|
||||
this.submitInitialPrompt
|
||||
);
|
||||
if (prompt) {
|
||||
this._initialPromptSubmitted = true;
|
||||
this._processText(prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import { mdiCommentProcessingOutline, mdiDevices } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -61,6 +61,7 @@ import { isIosApp } from "../../util/is_ios";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||
import { showVoiceCommandDialog } from "../voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import {
|
||||
effectiveQuickBarMode,
|
||||
type QuickBarParams,
|
||||
@@ -69,6 +70,18 @@ import {
|
||||
|
||||
const SEPARATOR = "________";
|
||||
|
||||
interface AssistComboBoxItem extends PickerComboBoxItem {
|
||||
action: "assist";
|
||||
assistPrompt: string;
|
||||
}
|
||||
|
||||
type QuickBarComboBoxItem =
|
||||
| NavigationComboBoxItem
|
||||
| ActionCommandComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
| AssistComboBoxItem;
|
||||
|
||||
@customElement("ha-quick-bar")
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -290,13 +303,7 @@ export class QuickBar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow = (
|
||||
item:
|
||||
| NavigationComboBoxItem
|
||||
| ActionCommandComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
private _renderRow = (item: QuickBarComboBoxItem) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -412,8 +419,8 @@ export class QuickBar extends LitElement {
|
||||
}: {
|
||||
firstIndex: number;
|
||||
lastIndex: number;
|
||||
firstItem: PickerComboBoxItem | string;
|
||||
secondItem: PickerComboBoxItem | string;
|
||||
firstItem: QuickBarComboBoxItem | string;
|
||||
secondItem: QuickBarComboBoxItem | string;
|
||||
itemsCount: number;
|
||||
}) => {
|
||||
if (
|
||||
@@ -461,7 +468,23 @@ export class QuickBar extends LitElement {
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
const items: (string | QuickBarComboBoxItem)[] = [];
|
||||
const prompt = filter?.trim();
|
||||
const assistItem =
|
||||
prompt &&
|
||||
(!section || section === "command") &&
|
||||
isComponentLoaded(this.hass.config, "conversation") &&
|
||||
!this.hass.auth.external?.config.hasAssist
|
||||
? ({
|
||||
id: "ask-assist",
|
||||
action: "assist",
|
||||
primary: this.hass.localize("ui.dialogs.quick-bar.ask_assist", {
|
||||
query: prompt,
|
||||
}),
|
||||
icon_path: mdiCommentProcessingOutline,
|
||||
assistPrompt: prompt,
|
||||
} satisfies AssistComboBoxItem)
|
||||
: undefined;
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -486,10 +509,12 @@ export class QuickBar extends LitElement {
|
||||
items.push(...navigateItems);
|
||||
}
|
||||
|
||||
if (this.hass.user?.is_admin && (!section || section === "command")) {
|
||||
let commandItems = this._generateActionCommandsMemoized(this.hass).sort(
|
||||
this._sortBySortingLabel
|
||||
);
|
||||
if (!section || section === "command") {
|
||||
let commandItems = this.hass.user?.is_admin
|
||||
? this._generateActionCommandsMemoized(this.hass).sort(
|
||||
this._sortBySortingLabel
|
||||
)
|
||||
: [];
|
||||
|
||||
if (filter) {
|
||||
commandItems = this._filterGroup(
|
||||
@@ -499,12 +524,15 @@ export class QuickBar extends LitElement {
|
||||
) as ActionCommandComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!section && commandItems.length) {
|
||||
if (!section && (commandItems.length || assistItem)) {
|
||||
// show group title
|
||||
items.push(this.hass.localize("ui.dialogs.quick-bar.commands_title"));
|
||||
}
|
||||
|
||||
items.push(...commandItems);
|
||||
if (assistItem) {
|
||||
items.push(assistItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (!section || section === "entity") {
|
||||
@@ -723,10 +751,21 @@ export class QuickBar extends LitElement {
|
||||
const { index, newTab } = ev.detail;
|
||||
const item = this._comboBox.virtualizerElement.items[
|
||||
index
|
||||
] as PickerComboBoxItem;
|
||||
] as QuickBarComboBoxItem;
|
||||
|
||||
this._itemSelected = true;
|
||||
|
||||
if (item && "assistPrompt" in item) {
|
||||
this.closeDialog();
|
||||
showVoiceCommandDialog(this, this.hass, {
|
||||
pipeline_id: "last_used",
|
||||
start_listening: false,
|
||||
prompt: item.assistPrompt,
|
||||
submit: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// entity selected
|
||||
if (item && "stateObj" in item) {
|
||||
this.closeDialog();
|
||||
|
||||
@@ -58,9 +58,11 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
private _startListening = false;
|
||||
|
||||
public async showDialog(
|
||||
params: Required<VoiceCommandDialogParams>
|
||||
): Promise<void> {
|
||||
private _prompt?: string;
|
||||
|
||||
private _submitPrompt = false;
|
||||
|
||||
public async showDialog(params: VoiceCommandDialogParams): Promise<void> {
|
||||
await this._loadPipelines();
|
||||
const pipelinesIds = this._pipelines?.map((pipeline) => pipeline.id) || [];
|
||||
if (
|
||||
@@ -77,7 +79,9 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
this._pipelineId = this._preferredPipeline;
|
||||
}
|
||||
|
||||
this._startListening = params.start_listening;
|
||||
this._startListening = params.start_listening ?? false;
|
||||
this._prompt = params.prompt;
|
||||
this._submitPrompt = params.submit ?? false;
|
||||
this._dialogOpen = true;
|
||||
this._open = true;
|
||||
}
|
||||
@@ -189,6 +193,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.pipeline=${this._pipeline}
|
||||
.startListening=${this._startListening}
|
||||
.initialPrompt=${this._prompt}
|
||||
.submitInitialPrompt=${this._submitPrompt}
|
||||
>
|
||||
</ha-assist-chat>
|
||||
`
|
||||
|
||||
@@ -6,6 +6,8 @@ const loadVoiceCommandDialog = () => import("./ha-voice-command-dialog");
|
||||
export interface VoiceCommandDialogParams {
|
||||
pipeline_id: "last_used" | "preferred" | string;
|
||||
start_listening?: boolean;
|
||||
prompt?: string;
|
||||
submit?: boolean;
|
||||
}
|
||||
|
||||
export const showVoiceCommandDialog = (
|
||||
@@ -31,6 +33,8 @@ export const showVoiceCommandDialog = (
|
||||
pipeline_id: dialogParams.pipeline_id,
|
||||
// Don't start listening by default for web
|
||||
start_listening: dialogParams.start_listening ?? false,
|
||||
prompt: dialogParams.prompt,
|
||||
submit: dialogParams.submit ?? false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -297,6 +297,15 @@ export class HuiEnergyUsageGraphCard
|
||||
to_battery: {},
|
||||
};
|
||||
|
||||
// Grid sources can be import-only or export-only; assign color indices by
|
||||
// position in the user's grid sources config so this card matches the
|
||||
// energy sources table, which uses positional indices.
|
||||
const colorIndices: Record<string, Record<string, number>> = {};
|
||||
Object.keys(colorPropertyMap).forEach((key) => {
|
||||
colorIndices[key] = {};
|
||||
});
|
||||
let gridIdx = 0;
|
||||
|
||||
for (const source of energyData.prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
if (statIds.solar) {
|
||||
@@ -335,6 +344,7 @@ export class HuiEnergyUsageGraphCard
|
||||
} else {
|
||||
statIds.from_grid = [gridSource.stat_energy_from];
|
||||
}
|
||||
colorIndices.from_grid[gridSource.stat_energy_from] = gridIdx;
|
||||
if (gridSource.name) {
|
||||
statLabels.from_grid[gridSource.stat_energy_from] =
|
||||
gridSource.stat_energy_to
|
||||
@@ -351,6 +361,7 @@ export class HuiEnergyUsageGraphCard
|
||||
} else {
|
||||
statIds.to_grid = [gridSource.stat_energy_to];
|
||||
}
|
||||
colorIndices.to_grid[gridSource.stat_energy_to] = gridIdx;
|
||||
if (gridSource.name) {
|
||||
statLabels.to_grid[gridSource.stat_energy_to] =
|
||||
gridSource.stat_energy_from
|
||||
@@ -361,17 +372,18 @@ export class HuiEnergyUsageGraphCard
|
||||
: gridSource.name;
|
||||
}
|
||||
}
|
||||
gridIdx++;
|
||||
}
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
const colorIndices: Record<string, Record<string, number>> = {};
|
||||
Object.keys(colorPropertyMap).forEach((key) => {
|
||||
colorIndices[key] = {};
|
||||
if (
|
||||
key === "used_grid" ||
|
||||
key === "used_solar" ||
|
||||
key === "used_battery"
|
||||
key === "used_battery" ||
|
||||
key === "from_grid" ||
|
||||
key === "to_grid"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1495,6 +1495,7 @@
|
||||
"revert_error": "Failed to revert the configuration: {error}"
|
||||
},
|
||||
"quick-bar": {
|
||||
"ask_assist": "Ask Assist: {query}",
|
||||
"commands_title": "Commands",
|
||||
"navigate_title": "Navigate",
|
||||
"commands": {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AssistPipeline } from "../../src/data/assist_pipeline";
|
||||
import {
|
||||
assistPipelineChanged,
|
||||
initialPromptToSubmit,
|
||||
} from "../../src/components/ha-assist-chat";
|
||||
|
||||
describe("initialPromptToSubmit", () => {
|
||||
it("returns a trimmed prompt when submission is requested", () => {
|
||||
expect(initialPromptToSubmit(" Turn on the lights ", true)).toBe(
|
||||
"Turn on the lights"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not return a prompt when submission is not requested", () => {
|
||||
expect(initialPromptToSubmit("Turn on the lights", false)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not return an empty prompt", () => {
|
||||
expect(initialPromptToSubmit(" ", true)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ha-assist-chat pipeline updates", () => {
|
||||
it("preserves the conversation when the same pipeline is reloaded", () => {
|
||||
const pipeline = { id: "pipeline-id" } as AssistPipeline;
|
||||
expect(assistPipelineChanged(pipeline, { ...pipeline })).toBe(false);
|
||||
});
|
||||
|
||||
it("resets the conversation when the pipeline changes", () => {
|
||||
expect(
|
||||
assistPipelineChanged(
|
||||
{ id: "first" } as AssistPipeline,
|
||||
{ id: "second" } as AssistPipeline
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { showVoiceCommandDialog } from "../../../src/dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
describe("showVoiceCommandDialog", () => {
|
||||
let element: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement("div");
|
||||
});
|
||||
|
||||
it("passes an initial prompt to the web dialog", () => {
|
||||
const hass = {
|
||||
auth: { external: undefined },
|
||||
} as HomeAssistant;
|
||||
const listener = vi.fn();
|
||||
element.addEventListener("show-dialog", listener);
|
||||
|
||||
showVoiceCommandDialog(element, hass, {
|
||||
pipeline_id: "last_used",
|
||||
prompt: "Turn on the lights",
|
||||
submit: true,
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener.mock.calls[0][0].detail.dialogParams).toEqual({
|
||||
pipeline_id: "last_used",
|
||||
start_listening: false,
|
||||
prompt: "Turn on the lights",
|
||||
submit: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the native Assist payload compatible", () => {
|
||||
const fireMessage = vi.fn();
|
||||
const hass = {
|
||||
auth: {
|
||||
external: {
|
||||
config: { hasAssist: true },
|
||||
fireMessage,
|
||||
},
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
showVoiceCommandDialog(element, hass, {
|
||||
pipeline_id: "last_used",
|
||||
prompt: "Turn on the lights",
|
||||
submit: true,
|
||||
});
|
||||
|
||||
expect(fireMessage).toHaveBeenCalledWith({
|
||||
type: "assist/show",
|
||||
payload: {
|
||||
pipeline_id: "last_used",
|
||||
start_listening: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,45 @@ test.describe("App shell", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Quick search", () => {
|
||||
test("starts an Assist conversation with the search query", async ({
|
||||
page,
|
||||
}) => {
|
||||
await goToPanel(page, "/?scenario=quick-search-assist#/lovelace");
|
||||
|
||||
await page.keyboard.press("Control+K");
|
||||
|
||||
const quickBar = page.locator("ha-quick-bar");
|
||||
await expect(quickBar).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await quickBar
|
||||
.locator("ha-input-search >> input")
|
||||
.fill("Turn on the lights");
|
||||
|
||||
const assistItem = quickBar
|
||||
.locator(".combo-box-row")
|
||||
.filter({ hasText: "Ask Assist: Turn on the lights" });
|
||||
await expect(assistItem).toBeVisible({ timeout: QUICK_TIMEOUT });
|
||||
await assistItem.click();
|
||||
|
||||
const voiceCommandDialog = page.locator("ha-voice-command-dialog");
|
||||
await expect(voiceCommandDialog).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(voiceCommandDialog.locator("ha-assist-chat")).toBeAttached({
|
||||
timeout: QUICK_TIMEOUT,
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__assistRun))
|
||||
.toMatchObject({
|
||||
type: "assist_pipeline/run",
|
||||
start_stage: "intent",
|
||||
input: { text: "Turn on the lights" },
|
||||
end_stage: "intent",
|
||||
pipeline: "test-pipeline",
|
||||
conversation_id: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
defineRouteSmokeTests(appRouteSmokeGroups);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -90,6 +90,7 @@ const MEDIA_BROWSER_ROOT = {
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__assistRun?: unknown;
|
||||
__mockHass: MockHomeAssistant;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ExtEntityRegistryEntry } from "../../../../../src/data/entity/entity_registry";
|
||||
import type { AssistPipeline } from "../../../../../src/data/assist_pipeline";
|
||||
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
|
||||
|
||||
export type Scenario = (hass: MockHomeAssistant) => Promise<void> | void;
|
||||
@@ -89,6 +90,40 @@ const lightMoreInfoScenario: Scenario = async (hass) => {
|
||||
hass.mockWS("config/entity_registry/get", () => registryEntry);
|
||||
};
|
||||
|
||||
const quickSearchAssistScenario: Scenario = async (hass) => {
|
||||
const pipeline: AssistPipeline = {
|
||||
id: "test-pipeline",
|
||||
name: "Test Assist",
|
||||
language: "en",
|
||||
conversation_engine: "conversation.home_assistant",
|
||||
conversation_language: "en",
|
||||
stt_engine: null,
|
||||
stt_language: null,
|
||||
tts_engine: null,
|
||||
tts_language: null,
|
||||
tts_voice: null,
|
||||
wake_word_entity: null,
|
||||
wake_word_id: null,
|
||||
};
|
||||
|
||||
hass.updateHass({
|
||||
config: {
|
||||
...hass.config,
|
||||
components: [...hass.config.components, "conversation"],
|
||||
},
|
||||
enableShortcuts: true,
|
||||
});
|
||||
hass.mockWS("assist_pipeline/pipeline/list", () => ({
|
||||
pipelines: [pipeline],
|
||||
preferred_pipeline: pipeline.id,
|
||||
}));
|
||||
hass.mockWS("assist_pipeline/pipeline/get", () => pipeline);
|
||||
hass.mockWS("assist_pipeline/run", (message) => {
|
||||
window.__assistRun = message;
|
||||
return () => undefined;
|
||||
});
|
||||
};
|
||||
|
||||
// ── Registry ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const scenarios: Record<string, Scenario> = {
|
||||
@@ -97,4 +132,5 @@ export const scenarios: Record<string, Scenario> = {
|
||||
"dark-theme": darkThemeScenario,
|
||||
"custom-theme": customThemeScenario,
|
||||
"light-more-info": lightMoreInfoScenario,
|
||||
"quick-search-assist": quickSearchAssistScenario,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user