feat: Created component for ai signposting (#25187)

## 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
This commit is contained in:
Druthi Polisetty 2023-07-13 17:36:38 +05:30 committed by GitHub
parent 0bc1213795
commit 6dcb996bbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 179 additions and 4 deletions

View File

@ -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 (
<BindingPrompt
editorTheme={props.editorTheme}
isAIEnabled={props.isAIEnabled}
isOpen={props.isOpen || false}
promptMessage={props.promptMessage}
showLightningMenu={props.showLightningMenu}
/>
);
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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",

View File

@ -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 (
<Container className="py-2 px-3 rounded">
<div className="flex items-start">
<Icon
className="font-semibold mr-2 flex items-start"
color="var(--ads-v2-color-fg-information)"
name={props.iconName}
size="md"
/>
</div>
{props.content}
</Container>
);
}
export default SignPostingBanner;

View File

@ -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 = (

View File

@ -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<typeof mapStateToProps>;
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
@ -489,6 +491,10 @@ class CodeEditor extends Component<Props, State> {
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<Props, State> {
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<Props, State> {
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}
>
<EditorWrapper
AIEnabled
border={border}
borderLess={borderLess}
className={`${className} ${replayHighlightClass} ${
@ -1575,6 +1585,7 @@ class CodeEditor extends Component<Props, State> {
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<Props, State> {
ref={this.codeEditorTarget}
tabIndex={0}
>
<BindingPrompt
<CodeEditorSignPosting
editorTheme={this.props.theme}
forComp="editor"
isAIEnabled={this.AIEnabled}
isOpen={this.isBindingPromptOpen()}
mode={this.props.mode}
promptMessage={this.props.promptMessage}
showLightningMenu={this.props.showLightningMenu}
/>

View File

@ -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 {

View File

@ -0,0 +1 @@
export * from "ce/components/editorComponents/CodeEditorSignPosting";

View File

@ -0,0 +1 @@
export * from "ce/components/editorComponents/EditorFormSignPosting";

View File

@ -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,

View File

@ -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}
>
<EditorFormSignPosting
isAIEnabled={isAIEnabledForPosting}
mode={editorMode}
/>
<SettingsWrapper>
{editorConfig && editorConfig.length > 0 ? (
renderConfig(editorConfig)

View File

@ -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<SlashCommandPayload>) {
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: {

View File

@ -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<string, any> | null = await store.getItem(