mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-28 04:17:48 +00:00
Compare commits
1 Commits
dev
...
gen-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa1678582d |
@@ -1,4 +1,5 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LovelaceRawConfig } from "./config/types";
|
||||
|
||||
export type LovelaceDashboard =
|
||||
| LovelaceYamlDashboard
|
||||
@@ -34,6 +35,11 @@ export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableP
|
||||
mode: "storage";
|
||||
}
|
||||
|
||||
export interface LovelaceAIDashboardResult {
|
||||
conversation_id: string;
|
||||
config: LovelaceRawConfig;
|
||||
}
|
||||
|
||||
export const fetchDashboards = (
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceDashboard[]> =>
|
||||
@@ -66,3 +72,9 @@ export const deleteDashboard = (hass: HomeAssistant, id: string) =>
|
||||
type: "lovelace/dashboards/delete",
|
||||
dashboard_id: id,
|
||||
});
|
||||
|
||||
export const generateDashboardWithAI = (hass: HomeAssistant, prompt: string) =>
|
||||
hass.callWS<LovelaceAIDashboardResult>({
|
||||
type: "lovelace/config/generate",
|
||||
prompt,
|
||||
});
|
||||
|
||||
@@ -171,6 +171,22 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
@click=${this._selected}
|
||||
.config=${defaultConfig}
|
||||
></dashboard-card>
|
||||
<dashboard-card
|
||||
.name=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_ai`
|
||||
)}
|
||||
.description=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_ai_description`
|
||||
)}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
|
||||
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
|
||||
.alt=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_ai`
|
||||
)}
|
||||
@click=${this._selected}
|
||||
.generateWithAI=${true}
|
||||
></dashboard-card>
|
||||
</div>
|
||||
<div class="cards-container">
|
||||
<div class="cards-container-header">
|
||||
@@ -241,6 +257,12 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
const target = ev.currentTarget as any;
|
||||
let config: any = null;
|
||||
|
||||
if (target.generateWithAI) {
|
||||
this._params?.generateWithAI?.();
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.config) {
|
||||
config = target.config;
|
||||
} else if (target.strategy) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
|
||||
export interface NewDashboardDialogParams {
|
||||
selectConfig: (config: LovelaceConfig | undefined) => any;
|
||||
generateWithAI?: () => void;
|
||||
}
|
||||
|
||||
export const loadNewDashboardDialog = () => import("./dialog-new-dashboard");
|
||||
|
||||
@@ -59,6 +59,7 @@ import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboar
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import "./ha-config-lovelace-generate-dashboard";
|
||||
|
||||
export const PANEL_DASHBOARDS = [
|
||||
"home",
|
||||
@@ -365,6 +366,17 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
if (this.route?.path.startsWith("/generate")) {
|
||||
return html`
|
||||
<ha-config-lovelace-generate-dashboard
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
@lovelace-dashboard-created=${this._dashboardCreated}
|
||||
></ha-config-lovelace-generate-dashboard>
|
||||
`;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
|
||||
return html`
|
||||
@@ -490,6 +502,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
private async _addDashboard() {
|
||||
showNewDashboardDialog(this, {
|
||||
generateWithAI: () => navigate("/config/lovelace/dashboards/generate"),
|
||||
selectConfig: async (config) => {
|
||||
if (config && isStrategyDashboard(config)) {
|
||||
const strategyType = config.strategy.type;
|
||||
@@ -514,6 +527,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _dashboardCreated(ev: CustomEvent<{ dashboard: LovelaceDashboard }>) {
|
||||
ev.stopPropagation();
|
||||
const dashboard = ev.detail.dashboard;
|
||||
this._dashboards = this._dashboards!.concat(dashboard).sort((res1, res2) =>
|
||||
stringCompare(res1.url_path, res2.url_path, this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
|
||||
private async _openDetailDialog(
|
||||
dashboard?: LovelaceDashboard,
|
||||
urlPath?: string,
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-fab";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textarea";
|
||||
import type {
|
||||
LovelaceConfig,
|
||||
LovelaceRawConfig,
|
||||
} from "../../../../data/lovelace/config/types";
|
||||
import {
|
||||
isStrategyDashboard,
|
||||
saveConfig,
|
||||
} from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import {
|
||||
createDashboard,
|
||||
generateDashboardWithAI,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { checkLovelaceConfig } from "../../../lovelace/common/check-lovelace-config";
|
||||
import { generateLovelaceDashboardStrategy } from "../../../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../../../lovelace/types";
|
||||
import "../../../lovelace/views/hui-view";
|
||||
import "../../../lovelace/views/hui-view-background";
|
||||
import "../../../lovelace/views/hui-view-container";
|
||||
import "../../automation/sidebar/ha-automation-sidebar-card";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"lovelace-dashboard-created": {
|
||||
dashboard: LovelaceDashboard;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-config-lovelace-generate-dashboard")
|
||||
export class HaConfigLovelaceGenerateDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _prompt = "";
|
||||
|
||||
@state() private _generating = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _generatedConfig?: LovelaceRawConfig;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
public firstUpdated() {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/lovelace/dashboards"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.generate.title"
|
||||
)}
|
||||
>
|
||||
<div class="layout ${classMap({ "has-sidebar": !this.narrow })}">
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}></hui-view-background>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${0}
|
||||
></hui-view>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.generate.empty_title"
|
||||
)}
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.generate.empty_description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
`}
|
||||
${this._generating
|
||||
? html`
|
||||
<div class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</hui-view-container>
|
||||
</div>
|
||||
<div class="fab-positioner">
|
||||
<ha-fab
|
||||
class=${classMap({ dirty: Boolean(this._generatedConfig) })}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${!this._generatedConfig || this._generating}
|
||||
extended
|
||||
@click=${this._saveGeneratedDashboard}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-positioner">
|
||||
<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
>
|
||||
<span slot="title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.generate.sidebar_title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="subtitle">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.generate.sidebar_subtitle"
|
||||
)}
|
||||
</span>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<ha-textarea
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.generate.prompt"
|
||||
)}
|
||||
.value=${this._prompt}
|
||||
?disabled=${this._generating}
|
||||
?autogrow=${true}
|
||||
@input=${this._promptChanged}
|
||||
></ha-textarea>
|
||||
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<ha-button
|
||||
variant="primary"
|
||||
.disabled=${this._generating || !this._prompt.trim()}
|
||||
@click=${this._generateDashboard}
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._generatedConfig
|
||||
? "ui.panel.config.lovelace.dashboards.generate.regenerate"
|
||||
: "ui.panel.config.lovelace.dashboards.generate.generate"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-automation-sidebar-card>
|
||||
</div>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _promptChanged(ev: Event) {
|
||||
this._prompt = (ev.target as HTMLTextAreaElement).value;
|
||||
}
|
||||
|
||||
private _closeSidebar = () => {
|
||||
navigate("/config/lovelace/dashboards");
|
||||
};
|
||||
|
||||
private _createLovelace(
|
||||
rawConfig: LovelaceRawConfig,
|
||||
config: LovelaceConfig
|
||||
): Lovelace {
|
||||
return {
|
||||
config,
|
||||
rawConfig,
|
||||
editMode: false,
|
||||
urlPath: "lovelace-ai-preview",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async _generateDashboard() {
|
||||
if (this._generating) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = this._prompt.trim();
|
||||
if (!prompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._generating = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
const result = await generateDashboardWithAI(this.hass, prompt);
|
||||
const rawConfig = result.config;
|
||||
const generatedConfig = isStrategyDashboard(rawConfig)
|
||||
? await generateLovelaceDashboardStrategy(rawConfig, this.hass)
|
||||
: rawConfig;
|
||||
const config = checkLovelaceConfig(generatedConfig) as LovelaceConfig;
|
||||
this._generatedConfig = rawConfig;
|
||||
this._lovelace = this._createLovelace(rawConfig, config);
|
||||
} catch (err: any) {
|
||||
this._error = err?.message || this.hass.localize("ui.common.error");
|
||||
console.error("Error generating Lovelace dashboard:", err);
|
||||
} finally {
|
||||
this._generating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _saveGeneratedDashboard() {
|
||||
if (!this._generatedConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedConfig = this._generatedConfig;
|
||||
showDashboardDetailDialog(this, {
|
||||
updateDashboard: async () => undefined,
|
||||
removeDashboard: async () => false,
|
||||
createDashboard: async (values) => {
|
||||
const dashboard = await createDashboard(this.hass, values);
|
||||
await saveConfig(this.hass, dashboard.url_path, generatedConfig);
|
||||
fireEvent(this, "lovelace-dashboard-created", { dashboard });
|
||||
navigate(`/${dashboard.url_path}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
--sidebar-width: 0;
|
||||
--sidebar-gap: 0;
|
||||
}
|
||||
|
||||
.layout.has-sidebar {
|
||||
--sidebar-width: min(470px, 42vw);
|
||||
--sidebar-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding-inline-end: calc(var(--sidebar-width) + var(--sidebar-gap));
|
||||
min-height: calc(100vh - var(--header-height) - 48px);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--primary-background-color);
|
||||
min-height: calc(100vh - var(--header-height) - 64px);
|
||||
}
|
||||
|
||||
.content hui-view-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content hui-view {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--ha-space-12);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--card-background-color) 74%,
|
||||
transparent
|
||||
);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.fab-positioner {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.fab-positioner ha-fab {
|
||||
position: fixed;
|
||||
right: unset;
|
||||
left: unset;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
|
||||
.fab-positioner ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.sidebar-positioner {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
ha-automation-sidebar-card {
|
||||
position: fixed;
|
||||
top: calc(var(--header-height) + 16px);
|
||||
height: calc(-81px + 100vh - var(--safe-area-inset-top, 0px));
|
||||
height: calc(-81px + 100dvh - var(--safe-area-inset-top, 0px));
|
||||
width: var(--sidebar-width);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.content-wrapper {
|
||||
padding-inline-end: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: 52vh;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-automation-sidebar-card {
|
||||
position: static;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-lovelace-generate-dashboard": HaConfigLovelaceGenerateDashboard;
|
||||
}
|
||||
}
|
||||
@@ -4189,6 +4189,8 @@
|
||||
"header": "Add dashboard",
|
||||
"create_empty": "New dashboard from scratch",
|
||||
"create_empty_description": "Start with an empty dashboard from scratch",
|
||||
"create_ai": "Generate dashboard with AI",
|
||||
"create_ai_description": "Describe your home and let AI build a dashboard",
|
||||
"default": "Default dashboard",
|
||||
"default_description": "Display your devices grouped by area",
|
||||
"strategy": {
|
||||
@@ -4253,6 +4255,16 @@
|
||||
"remove_default": "Remove as default",
|
||||
"set_default_confirm_title": "Set as default dashboard?",
|
||||
"set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile."
|
||||
},
|
||||
"generate": {
|
||||
"title": "Generate dashboard with AI",
|
||||
"sidebar_title": "Describe your dashboard",
|
||||
"sidebar_subtitle": "Ask AI to generate or regenerate a complete dashboard configuration.",
|
||||
"prompt": "Prompt",
|
||||
"generate": "Generate",
|
||||
"regenerate": "Regenerate",
|
||||
"empty_title": "No dashboard generated yet",
|
||||
"empty_description": "Describe your ideal dashboard in the sidebar and click Generate."
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
|
||||
Reference in New Issue
Block a user