Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Custom Widget Editor integration with AI #37257

Merged
merged 24 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a3a1a59
fix: API Body format context lost
hetunandu Oct 31, 2024
ff0a0a7
API and GraphQL
hetunandu Nov 4, 2024
9b60d95
Merge branch 'release' into fix/paddings-and-scrolls
hetunandu Nov 4, 2024
0e7e723
Merge branch 'release' into fix/paddings-and-scrolls
hetunandu Nov 5, 2024
efcc7ef
fix for other query fields
hetunandu Nov 5, 2024
0abd134
Merge branch 'release' into fix/paddings-and-scrolls
hetunandu Nov 5, 2024
9d04a85
API method and url
hetunandu Nov 5, 2024
1398c9c
[Plugin Action Editor] Query Form evaluation
hetunandu Nov 5, 2024
1e40efa
Merge branch 'release' into chore/plugin-action-editor-query-init
hetunandu Nov 5, 2024
585c787
fix: Add changes
hetunandu Nov 6, 2024
eb956da
Merge branch 'release' into feat/custom-widget-tab-edit
hetunandu Nov 6, 2024
46ecf58
Keep tabs mounted always
hetunandu Nov 6, 2024
9444c94
Add flag for default AI tab
hetunandu Nov 6, 2024
2d41e2a
Merge branch 'release' into feat/custom-widget-tab-edit
hetunandu Nov 7, 2024
8e9107b
updates for iframe url
hetunandu Nov 7, 2024
7d9cb19
use uncompled Src doc
hetunandu Nov 8, 2024
31aeb2f
Merge branch 'release' into feat/custom-widget-tab-edit
hetunandu Nov 8, 2024
da40c16
add url origin on iframe
hetunandu Nov 8, 2024
95bd4ad
sending context post CHAT_INTIALISED message
hetunandu Nov 8, 2024
dc633f4
Merge branch 'release' into feat/custom-widget-tab-edit
hetunandu Nov 8, 2024
80d412c
clean up
hetunandu Nov 8, 2024
a824609
Fix bug with outdated code posting
hetunandu Nov 11, 2024
cffb595
Only show AI tab if feature flag exists
hetunandu Nov 11, 2024
f48efee
address review comments
hetunandu Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2520,3 +2520,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 || "",
});
}
Comment on lines +68 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type validation for message data.

Consider adding type validation to ensure message data is properly structured:

+ const isValidMessageData = (
+   data: unknown
+ ): data is { html_code?: string; css_code?: string; js_code?: string } => {
+   if (!isObject(data)) return false;
+   const { html_code, css_code, js_code } = data as any;
+   return (
+     (html_code === undefined || typeof html_code === "string") &&
+     (css_code === undefined || typeof css_code === "string") &&
+     (js_code === undefined || typeof js_code === "string")
+   );
+ };

- if (isObject(event.data)) {
+ if (isValidMessageData(event.data)) {
    lastUpdateFromBot.current = Date.now();
    bulkUpdate({
      html: event.data.html_code || "",
      css: event.data.css_code || "",
      js: event.data.js_code || "",
    });
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isObject(event.data)) {
lastUpdateFromBot.current = Date.now();
bulkUpdate({
html: event.data.html_code || "",
css: event.data.css_code || "",
js: event.data.js_code || "",
});
}
const isValidMessageData = (
data: unknown
): data is { html_code?: string; css_code?: string; js_code?: string } => {
if (!isObject(data)) return false;
const { html_code, css_code, js_code } = data as any;
return (
(html_code === undefined || typeof html_code === "string") &&
(css_code === undefined || typeof css_code === "string") &&
(js_code === undefined || typeof js_code === "string")
);
};
if (isValidMessageData(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]);
hetunandu marked this conversation as resolved.
Show resolved Hide resolved

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%" />
);
};
hetunandu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading