From 6dcb996bbb9acec9819b7a28ecee7670002cf5c3 Mon Sep 17 00:00:00 2001 From: Druthi Polisetty Date: Thu, 13 Jul 2023 17:36:38 +0530 Subject: [PATCH] feat: Created component for ai signposting (#25187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description feat: Created component for ai signposting #### PR fixes following issue(s) Fixes # (issue number) > if no issue exists, please create an issue and ask the maintainers about this first > > #### Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video > > #### Type of change > Please delete options that are not relevant. - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (fix or feature that would cause existing functionality to not work as expected) - Chore (housekeeping or task changes that don't impact user perception) - This change requires a documentation update > > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed --- .../CodeEditorSignPosting.tsx | 26 ++++++++++++++ .../EditorFormSignPosting.tsx | 11 ++++++ .../components/editorComponents/GPT/index.tsx | 6 +++- .../src/ce/constants/ReduxActionConstants.tsx | 1 + .../appsmith/SignPostingBanner.tsx | 33 ++++++++++++++++++ .../CodeEditor/EditorConfig.ts | 2 ++ .../editorComponents/CodeEditor/index.tsx | 17 ++++++++-- .../CodeEditor/styledComponents.ts | 3 ++ .../CodeEditorSignPosting.tsx | 1 + .../EditorFormSignPosting.tsx | 1 + .../src/entities/Engine/AppEditorEngine.ts | 11 ++++++ .../Editor/QueryEditor/EditorJSONtoForm.tsx | 21 +++++++++++- app/client/src/sagas/ActionSagas.ts | 16 +++++++++ app/client/src/utils/storage.ts | 34 +++++++++++++++++++ 14 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 app/client/src/ce/components/editorComponents/CodeEditorSignPosting.tsx create mode 100644 app/client/src/ce/components/editorComponents/EditorFormSignPosting.tsx create mode 100644 app/client/src/components/designSystems/appsmith/SignPostingBanner.tsx create mode 100644 app/client/src/ee/components/editorComponents/CodeEditorSignPosting.tsx create mode 100644 app/client/src/ee/components/editorComponents/EditorFormSignPosting.tsx diff --git a/app/client/src/ce/components/editorComponents/CodeEditorSignPosting.tsx b/app/client/src/ce/components/editorComponents/CodeEditorSignPosting.tsx new file mode 100644 index 0000000000..3ca8326c9c --- /dev/null +++ b/app/client/src/ce/components/editorComponents/CodeEditorSignPosting.tsx @@ -0,0 +1,26 @@ +import BindingPrompt from "components/editorComponents/CodeEditor/BindingPrompt"; +import type { + EditorTheme, + TEditorModes, +} from "components/editorComponents/CodeEditor/EditorConfig"; +import React from "react"; + +export function CodeEditorSignPosting(props: { + promptMessage?: React.ReactNode | string; + isOpen?: boolean; + editorTheme?: EditorTheme; + showLightningMenu?: boolean; + isAIEnabled?: boolean; + mode: TEditorModes; + forComp?: string; +}): JSX.Element { + return ( + + ); +} diff --git a/app/client/src/ce/components/editorComponents/EditorFormSignPosting.tsx b/app/client/src/ce/components/editorComponents/EditorFormSignPosting.tsx new file mode 100644 index 0000000000..bc54978c45 --- /dev/null +++ b/app/client/src/ce/components/editorComponents/EditorFormSignPosting.tsx @@ -0,0 +1,11 @@ +import type { TEditorModes } from "components/editorComponents/CodeEditor/EditorConfig"; + +export type Props = { + isAIEnabled: boolean; + mode: TEditorModes; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function EditorFormSignPosting(props: Props) { + return null; +} diff --git a/app/client/src/ce/components/editorComponents/GPT/index.tsx b/app/client/src/ce/components/editorComponents/GPT/index.tsx index 2c5e5f816c..2669630fa5 100644 --- a/app/client/src/ce/components/editorComponents/GPT/index.tsx +++ b/app/client/src/ce/components/editorComponents/GPT/index.tsx @@ -1,5 +1,8 @@ import type { CodeEditorExpected } from "components/editorComponents/CodeEditor"; -import type { TEditorModes } from "components/editorComponents/CodeEditor/EditorConfig"; +import type { + FieldEntityInformation, + TEditorModes, +} from "components/editorComponents/CodeEditor/EditorConfig"; import React from "react"; export type TAIWrapperProps = { @@ -12,6 +15,7 @@ export type TAIWrapperProps = { enableAIAssistance: boolean; dataTreePath?: string; mode: TEditorModes; + entity: FieldEntityInformation; }; export function AIWindow(props: TAIWrapperProps) { diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index f99c25f734..f982830edd 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -834,6 +834,7 @@ const ActionTypes = { BIND_WIDGET_TO_DATASOURCE_ERROR: "BIND_WIDGET_TO_DATASOURCE_ERROR", LOAD_FILE_PICKER_ACTION: "LOAD_FILE_PICKER_ACTION", TOGGLE_AI_WINDOW: "TOGGLE_AI_WINDOW", + UPDATE_AI_TRIGGERED: "UPDATE_AI_TRIGGERED", UPDATE_DATASOURCE_AUTH_STATE: "UPDATE_DATASOURCE_AUTH_STATE", UPDATE_POSITIONS_ON_TAB_CHANGE: "UPDATE_POSITIONS_ON_TAB_CHANGE", RESET_DATA_TREE: "RESET_DATA_TREE", diff --git a/app/client/src/components/designSystems/appsmith/SignPostingBanner.tsx b/app/client/src/components/designSystems/appsmith/SignPostingBanner.tsx new file mode 100644 index 0000000000..ad85380950 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/SignPostingBanner.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import type { ReactNode } from "react"; +import styled from "styled-components"; +import { Icon } from "design-system"; + +export type SignPostingBannerProps = { + iconName: string; + content: ReactNode; +}; + +export const Container = styled.div` + background-color: var(--ads-v2-color-blue-100); + width: 100%; + display: flex; +`; + +function SignPostingBanner(props: SignPostingBannerProps) { + return ( + +
+ +
+ {props.content} +
+ ); +} + +export default SignPostingBanner; diff --git a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts index 1894fa20e4..99f82d2181 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts +++ b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts @@ -6,6 +6,7 @@ import type { EntityNavigationData } from "selectors/navigationSelectors"; import type { ExpectedValueExample } from "utils/validation/common"; import { editorSQLModes } from "./sql/config"; +import type { WidgetType } from "constants/WidgetConstants"; export const EditorModes = { TEXT: "text/plain", @@ -61,6 +62,7 @@ export type FieldEntityInformation = { example?: ExpectedValueExample; mode?: TEditorModes; token?: CodeMirror.Token; + widgetType?: WidgetType; }; export type HintHelper = ( diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index 2a91e694d2..2c15b853a0 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -71,7 +71,7 @@ import { bindingHint, sqlHint, } from "components/editorComponents/CodeEditor/hintHelpers"; -import BindingPrompt from "./BindingPrompt"; + import { showBindingPrompt } from "./BindingPromptHelper"; import { Button } from "design-system"; import "codemirror/addon/fold/brace-fold"; @@ -103,6 +103,7 @@ import { getLintAnnotations, getLintTooltipDirection } from "./lintHelpers"; import { executeCommandAction } from "actions/apiPaneActions"; import { startingEntityUpdate } from "actions/editorActions"; import type { SlashCommandPayload } from "entities/Action"; +import { SlashCommand } from "entities/Action"; import type { Indices } from "constants/Layers"; import { replayHighlightClass } from "globalStyles/portals"; import { @@ -158,6 +159,7 @@ import { PeekOverlayExpressionIdentifier, SourceType } from "@shared/ast"; import type { MultiplexingModeConfig } from "components/editorComponents/CodeEditor/modes"; import { MULTIPLEXING_MODE_CONFIGS } from "components/editorComponents/CodeEditor/modes"; import { getDeleteLineShortcut } from "./utils/deleteLine"; +import { CodeEditorSignPosting } from "@appsmith/components/editorComponents/CodeEditorSignPosting"; type ReduxStateProps = ReturnType; type ReduxDispatchProps = ReturnType; @@ -489,6 +491,10 @@ class CodeEditor extends Component { handleSlashCommandSelection = (...args: any) => { const [command] = args; if (command === APPSMITH_AI) { + this.props.executeCommand({ + actionType: SlashCommand.ASK_AI, + args: {}, + }); this.setState({ showAIWindow: true }); } this.handleAutocompleteVisibility(this.editor); @@ -1244,6 +1250,8 @@ class CodeEditor extends Component { entityInformation.entityId = entity.widgetId; if (isTriggerPath) entityInformation.expectedType = AutocompleteDataType.FUNCTION; + + entityInformation.widgetType = entity.type; } } entityInformation.propertyPath = propertyPath; @@ -1552,12 +1560,14 @@ class CodeEditor extends Component { currentValue={this.props.input.value} dataTreePath={dataTreePath} enableAIAssistance={this.AIEnabled} + entity={entityInformation} isOpen={this.state.showAIWindow} mode={this.props.mode} triggerContext={this.props.expected} update={this.updateValueWithAIResponse} > { isNotHover={this.state.isFocused || this.state.isOpened} isRawView={this.props.isRawView} isReadOnly={this.props.isReadOnly} + mode={this.props.mode} onMouseMove={this.handleLintTooltip} onMouseOver={this.handleMouseMove} ref={this.editorWrapperRef} @@ -1604,10 +1615,12 @@ class CodeEditor extends Component { ref={this.codeEditorTarget} tabIndex={0} > - diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts index 6ec052c0fa..72e70e3d5e 100644 --- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts +++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts @@ -1,5 +1,6 @@ import styled from "styled-components"; import type { CodeEditorBorder } from "components/editorComponents/CodeEditor/EditorConfig"; + import { EditorSize, EditorTheme, @@ -56,6 +57,8 @@ export const EditorWrapper = styled.div<{ codeEditorVisibleOverflow?: boolean; ctrlPressed: boolean; removeHoverAndFocusStyle?: boolean; + AIEnabled?: boolean; + mode: string; }>` // Bottom border was getting clipped .CodeMirror.cm-s-duotone-light.CodeMirror-wrap { diff --git a/app/client/src/ee/components/editorComponents/CodeEditorSignPosting.tsx b/app/client/src/ee/components/editorComponents/CodeEditorSignPosting.tsx new file mode 100644 index 0000000000..117f8b0f6f --- /dev/null +++ b/app/client/src/ee/components/editorComponents/CodeEditorSignPosting.tsx @@ -0,0 +1 @@ +export * from "ce/components/editorComponents/CodeEditorSignPosting"; diff --git a/app/client/src/ee/components/editorComponents/EditorFormSignPosting.tsx b/app/client/src/ee/components/editorComponents/EditorFormSignPosting.tsx new file mode 100644 index 0000000000..62e1e3d375 --- /dev/null +++ b/app/client/src/ee/components/editorComponents/EditorFormSignPosting.tsx @@ -0,0 +1 @@ +export * from "ce/components/editorComponents/EditorFormSignPosting"; diff --git a/app/client/src/entities/Engine/AppEditorEngine.ts b/app/client/src/entities/Engine/AppEditorEngine.ts index 8d4e4031d8..22657a827e 100644 --- a/app/client/src/entities/Engine/AppEditorEngine.ts +++ b/app/client/src/entities/Engine/AppEditorEngine.ts @@ -58,6 +58,7 @@ import { } from "@appsmith/sagas/userSagas"; import { getFirstTimeUserOnboardingComplete } from "selectors/onboardingSelectors"; import { isAirgapped } from "@appsmith/utils/airgapHelpers"; +import { getAIPromptTriggered } from "utils/storage"; export default class AppEditorEngine extends AppEngine { constructor(mode: APP_MODE) { @@ -214,6 +215,16 @@ export default class AppEditorEngine extends AppEngine { payload: [], }); } + + const noOfTimesAIPromptTriggered: number = yield getAIPromptTriggered(); + + yield put({ + type: ReduxActionTypes.UPDATE_AI_TRIGGERED, + payload: { + value: noOfTimesAIPromptTriggered, + }, + }); + yield call(waitForWidgetConfigBuild); yield put({ type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index dcbbfa588c..bc32098e2c 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -131,8 +131,14 @@ import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole"; import { DocsLink, openDoc } from "../../../constants/DocumentationLinks"; import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView"; import { CloseDebugger } from "components/editorComponents/Debugger/DebuggerTabs"; +import { isAIEnabled } from "@appsmith/components/editorComponents/GPT/trigger"; +import { editorSQLModes } from "components/editorComponents/CodeEditor/sql/config"; +import { EditorFormSignPosting } from "@appsmith/components/editorComponents/EditorFormSignPosting"; import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer"; -import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors"; +import { + selectFeatureFlagCheck, + selectFeatureFlags, +} from "@appsmith/selectors/featureFlagsSelectors"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; const QueryFormContainer = styled.form` @@ -875,6 +881,14 @@ export function EditorJSONtoForm(props: Props) { dispatch(setDebuggerSelectedTab(tabKey)); }, []); + const featureFlags = useSelector(selectFeatureFlags); + const editorMode = + currentActionPluginName === PluginName.POSTGRES + ? editorSQLModes.POSTGRESQL_WITH_BINDING + : editorSQLModes.MYSQL_WITH_BINDING; + + const isAIEnabledForPosting = isAIEnabled(featureFlags, editorMode); + // close the debugger //TODO: move this to a common place const onClose = () => dispatch(showDebugger(false)); @@ -980,6 +994,11 @@ export function EditorJSONtoForm(props: Props) { className="tab-panel" value={EDITOR_TABS.QUERY} > + + {editorConfig && editorConfig.length > 0 ? ( renderConfig(editorConfig) diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 50048056b4..fccfaaf893 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -127,6 +127,7 @@ import { DEFAULT_GRAPHQL_ACTION_CONFIG } from "constants/ApiEditorConstants/Grap import { DEFAULT_API_ACTION_CONFIG } from "constants/ApiEditorConstants/ApiEditorConstants"; import { createNewApiName, createNewQueryName } from "utils/AppsmithUtils"; import { fetchDatasourceStructure } from "actions/datasourceActions"; +import { setAIPromptTriggered } from "utils/storage"; export function* createDefaultActionPayload( pageId: string, @@ -942,6 +943,21 @@ function* executeCommandSaga(actionPayload: ReduxAction) { break; case SlashCommand.ASK_AI: { const context = get(actionPayload, "payload.args", {}); + + const noOfTimesAIPromptTriggered: number = yield select( + (state) => state.ai.noOfTimesAITriggered, + ); + + if (noOfTimesAIPromptTriggered < 5) { + const currentValue: number = yield setAIPromptTriggered(); + yield put({ + type: ReduxActionTypes.UPDATE_AI_TRIGGERED, + payload: { + value: currentValue, + }, + }); + } + yield put({ type: ReduxActionTypes.TOGGLE_AI_WINDOW, payload: { diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index b53e7c5b36..0098a2d57b 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -23,6 +23,7 @@ export const STORAGE_KEYS: { FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY: "FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY", SIGNPOSTING_APP_STATE: "SIGNPOSTING_APP_STATE", + AI_TRIGGERED: "AI_TRIGGERED", FEATURE_WALKTHROUGH: "FEATURE_WALKTHROUGH", USER_SIGN_UP: "USER_SIGN_UP", }; @@ -421,6 +422,39 @@ export const setFirstTimeUserOnboardingTelemetryCalloutVisibility = async ( } }; +export const setAIPromptTriggered = async () => { + try { + let noOfTimesAITriggered: number = await getAIPromptTriggered(); + + if (noOfTimesAITriggered >= 5) { + return noOfTimesAITriggered; + } + + noOfTimesAITriggered += 1; + await store.setItem(STORAGE_KEYS.AI_TRIGGERED, noOfTimesAITriggered); + + return noOfTimesAITriggered; + } catch (error) { + log.error("An error occurred while setting AI_TRIGGERED"); + log.error(error); + + return 0; + } +}; + +export const getAIPromptTriggered = async () => { + try { + const flag: number | null = await store.getItem(STORAGE_KEYS.AI_TRIGGERED); + + if (flag === null) return 0; + + return flag; + } catch (error) { + log.error("An error occurred while fetching AI_TRIGGERED"); + log.error(error); + return 0; + } +}; export const setFeatureFlagShownStatus = async (key: string, value: any) => { try { let flagsJSON: Record | null = await store.getItem(