Skip to content

Commit

Permalink
feat: Custom Widget Editor integration with AI (#37257)
Browse files Browse the repository at this point in the history
## Description

Adds the AI widget builder integration inside the Custom Widget Editor. 
User can prompt the AI on the changes and the bot will update the code
of the widget direclty.

Fixes #37250

## Automation

/ok-to-test tags="@tag.Sanity"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!IMPORTANT]
> 🟣 🟣 🟣 Your tests are running.
> Tests running at:
<https://github.com/appsmithorg/appsmith/actions/runs/11772460894>
> Commit: cffb595
> Workflow: `PR Automation test suite`
> Tags: `@tag.Sanity`
> Spec: ``
> <hr>Mon, 11 Nov 2024 05:03:57 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [x] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
  - Introduced a feature flag for a custom AI widget builder.
- Added a new `ChatBot` component for enhanced interactivity within the
editor.

- **Improvements**
- Simplified layout management by removing the `LayoutControls`
component.
- Enhanced the `TabsLayout` to conditionally display an AI tab based on
the feature flag.
  - Updated the `Editor` component to include the AI tab dynamically.

- **Bug Fixes**
- Streamlined state management for layout selection in the custom widget
builder context.

These updates improve user experience by integrating AI capabilities and
simplifying the interface.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
hetunandu authored Nov 11, 2024
1 parent 5c075e8 commit d8d0d1a
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 243 deletions.
7 changes: 7 additions & 0 deletions app/client/src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2522,3 +2522,10 @@ export const JS_EDITOR_SETTINGS = {
TITLE: () => "Settings",
ON_LOAD_TITLE: () => "Choose the functions to run on page load",
};

export const CUSTOM_WIDGET_BUILDER_TAB_TITLE = {
AI: () => "AI",
HTML: () => "HTML",
STYLE: () => "Style",
JS: () => "Javascript",
};
2 changes: 2 additions & 0 deletions app/client/src/ce/entities/FeatureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const FEATURE_FLAG = {
"release_ide_datasource_selector_enabled",
release_table_custom_loading_state_enabled:
"release_table_custom_loading_state_enabled",
release_custom_widget_ai_builder: "release_custom_widget_ai_builder",
} as const;

export type FeatureFlag = keyof typeof FEATURE_FLAG;
Expand Down Expand Up @@ -77,6 +78,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_ide_animations_enabled: false,
release_ide_datasource_selector_enabled: false,
release_table_custom_loading_state_enabled: false,
release_custom_widget_ai_builder: false,
};

export const AB_TESTING_EVENT_KEYS = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from "react";
import type { ContentProps } from "../CodeEditors/types";
import { CustomWidgetBuilderContext } from "../..";
import {
CUSTOM_WIDGET_AI_BOT_MESSAGE_RESPONSE_DEBOUNCE_TIMEOUT,
CUSTOM_WIDGET_AI_BOT_URL,
CUSTOM_WIDGET_AI_CHAT_TYPE,
CUSTOM_WIDGET_AI_INITIALISED_MESSAGE,
} from "../../constants";
import { isObject } from "lodash";

export const ChatBot = (props: ContentProps) => {
const ref = useRef<HTMLIFrameElement>(null);
const lastUpdateFromBot = useRef<number>(0);
const { bulkUpdate, parentEntityId, uncompiledSrcDoc, widgetId } = useContext(
CustomWidgetBuilderContext,
);

const handleSrcDocUpdates = useCallback(() => {
// Don't send updates back to bot if the last update came from the bot within the last 100ms
if (
Date.now() - lastUpdateFromBot.current <
CUSTOM_WIDGET_AI_BOT_MESSAGE_RESPONSE_DEBOUNCE_TIMEOUT
) {
return;
}

// Send src doc to the chatbot iframe
if (ref.current && ref.current.contentWindow && uncompiledSrcDoc) {
ref.current.contentWindow.postMessage(
{
html_code: uncompiledSrcDoc.html,
css_code: uncompiledSrcDoc.css,
js_code: uncompiledSrcDoc.js,
chatType: CUSTOM_WIDGET_AI_CHAT_TYPE,
},
"*",
);
}
}, [uncompiledSrcDoc]);

const updateContents = useCallback(
(
event: MessageEvent<
string | { html_code?: string; css_code?: string; js_code?: string }
>,
) => {
const iframeWindow =
ref.current?.contentWindow || ref.current?.contentDocument?.defaultView;

// Accept messages only from the current iframe
if (event.source !== iframeWindow) return;

if (event.data === CUSTOM_WIDGET_AI_INITIALISED_MESSAGE) {
handleSrcDocUpdates();

return;
}

if (!bulkUpdate) return;

if (isObject(event.data)) {
lastUpdateFromBot.current = Date.now();

bulkUpdate({
html: event.data.html_code || "",
css: event.data.css_code || "",
js: event.data.js_code || "",
});
}
},
[bulkUpdate, handleSrcDocUpdates],
);

useEffect(
function addEventListenerForBotUpdates() {
// add a listener to update the contents
window.addEventListener("message", updateContents, false);

// clean up
return () => window.removeEventListener("message", updateContents, false);
},
[updateContents],
);

useEffect(handleSrcDocUpdates, [handleSrcDocUpdates]);

const instanceId = `${widgetId}-${parentEntityId}`;

const srcUrl = useMemo(() => {
return CUSTOM_WIDGET_AI_BOT_URL(instanceId);
}, [instanceId]);

return (
<iframe height={`${props.height}px`} ref={ref} src={srcUrl} width="100%" />
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from "react";
import styles from "./styles.module.css";
import WidgetName from "./widgetName";
import LayoutControls from "./layoutControls";
import ReferenceTrigger from "./referenceTrigger";
import { CodeTemplates } from "./CodeTemplates";

Expand All @@ -11,7 +10,6 @@ export default function Header() {
<div className={styles.headerControlsLeft}>
<WidgetName />
<CodeTemplates />
<LayoutControls />
</div>
<div className={styles.headerControlsRight}>
<ReferenceTrigger />
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import styles from "./styles.module.css";
import { Tab, TabPanel, Tabs, TabsList } from "@appsmith/ads";
import type { ContentProps } from "../../CodeEditors/types";
import useLocalStorageState from "utils/hooks/useLocalStorageState";
import classNames from "classnames";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { CUSTOM_WIDGET_BUILDER_TABS } from "../../../constants";

interface Props {
tabs: Array<{
Expand All @@ -17,9 +21,15 @@ const LOCAL_STORAGE_KEYS = "custom-widget-layout-tabs-state";
export default function TabLayout(props: Props) {
const { tabs } = props;

const isDefaultAITab = useFeatureFlag(
FEATURE_FLAG.release_custom_widget_ai_builder,
);

const [selectedTab, setSelectedTab] = useLocalStorageState<string>(
LOCAL_STORAGE_KEYS,
tabs[0].title,
isDefaultAITab
? CUSTOM_WIDGET_BUILDER_TABS.AI
: CUSTOM_WIDGET_BUILDER_TABS.JS,
);

useEffect(() => {
Expand Down Expand Up @@ -88,7 +98,10 @@ export default function TabLayout(props: Props) {
</TabsList>
{tabs.map((tab) => (
<TabPanel
className={styles.tabPanel}
className={classNames(styles.tabPanel, {
"data-[state=inactive]:hidden": selectedTab !== tab.title,
})}
forceMount
key={tab.title}
value={tab.title}
>
Expand Down
Loading

0 comments on commit d8d0d1a

Please sign in to comment.