diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 50f34544a62..98905fa5126 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -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", +}; diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index fbfc8e0a2ca..4196cf40f46 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -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; @@ -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 = { diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/ChatBot/ChatBot.tsx b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/ChatBot/ChatBot.tsx new file mode 100644 index 00000000000..ae3c98e9c82 --- /dev/null +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/ChatBot/ChatBot.tsx @@ -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(null); + const lastUpdateFromBot = useRef(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 ( +