Skip to content

Commit

Permalink
ad function calling config form
Browse files Browse the repository at this point in the history
  • Loading branch information
znamenskii-ilia committed Feb 14, 2025
1 parent 1a60eaa commit 989b568
Show file tree
Hide file tree
Showing 10 changed files with 477 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
font-size: 12px;
width: 100px;
position: relative;
background-color: var(--ads-v2-color-white);
}

.rc-select-disabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ControlType } from "constants/PropertyControlConstants";
import React from "react";
import { FieldArray } from "redux-form";
import type { ControlProps } from "../BaseControl";
import BaseControl from "../BaseControl";
import { FunctionCallingConfigForm } from "./components/FunctionCallingConfigForm";

/**
* This component is used to configure the function calling for the AI assistant.
* It allows the user to add, edit and delete functions that can be used by the AI assistant.
*/
export default class FunctionCallingConfigControl extends BaseControl<ControlProps> {
render() {
return (
<FieldArray
component={FunctionCallingConfigForm}
key={this.props.configProperty}
name={this.props.configProperty}
props={{
formName: this.props.formName,
configProperty: this.props.configProperty,
}}
rerenderOnEveryChange={false}
/>
);
}

getControlType(): ControlType {
return "FUNCTION_CALLING_CONFIG_FORM";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Button, Text } from "@appsmith/ads";
import { default as React, useCallback } from "react";
import type { FieldArrayFieldsProps } from "redux-form";
import styled from "styled-components";
import { v4 as uuid } from "uuid";
import type { FunctionCallingConfigFormToolField } from "../types";
import { FunctionCallingConfigToolField } from "./FunctionCallingConfigToolField";
import { FunctionCallingEmpty } from "./FunctionCallingEmpty";

export interface FunctionCallingConfigFormProps {
formName: string;
fields: FieldArrayFieldsProps<FunctionCallingConfigFormToolField>;
}

const Header = styled.div`
display: flex;
gap: var(--ads-v2-spaces-4);
justify-content: space-between;
margin-bottom: var(--ads-v2-spaces-4);
`;

const ConfigItems = styled.div`
display: flex;
flex-direction: column;
gap: var(--ads-v2-spaces-4);
`;

export const FunctionCallingConfigForm = ({
fields,
formName,
}: FunctionCallingConfigFormProps) => {
const handleAddFunctionButtonClick = useCallback(() => {
fields.push({
id: uuid(),
description: "",
entityId: "",
requiresApproval: false,
entityType: "Query",
});
}, [fields]);

const handleRemoveToolButtonClick = useCallback(
(index: number) => {
fields.remove(index);
},
[fields],
);

return (
<div>
<Header>
<div>
<Text isBold kind="heading-s" renderAs="p">
Function Calls
</Text>
<Text renderAs="p">
Add functions for the model to execute dynamically.
</Text>
</div>

<Button
kind="secondary"
onClick={handleAddFunctionButtonClick}
startIcon="plus"
>
Add Function
</Button>
</Header>

{fields.length === 0 ? (
<FunctionCallingEmpty />
) : (
<ConfigItems>
{fields.map((field, index) => {
return (
<FunctionCallingConfigToolField
fieldPath={field}
formName={formName}
index={index}
key={field}
onRemove={handleRemoveToolButtonClick}
/>
);
})}
</ConfigItems>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Button, Text } from "@appsmith/ads";
import type { AppState } from "ee/reducers";
import FormControl from "pages/Editor/FormControl";
import React, { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { change, formValueSelector } from "redux-form";
import styled from "styled-components";
import type { DropDownGroupedOptions } from "../../DropDownControl";
import type {
FunctionCallingEntityTypeOption,
FunctionCallingEntityType,
} from "../types";
import { selectEntityOptions } from "./selectors";

interface FunctionCallingConfigToolFieldProps {
fieldPath: string;
formName: string;
index: number;
onRemove: (index: number) => void;
}

const ConfigItemRoot = styled.div`
display: flex;
flex-direction: column;
padding: var(--ads-v2-spaces-3);
border-radius: var(--ads-v2-border-radius);
border: 1px solid var(--ads-v2-color-border);
background: var(--colors-semantics-bg-inset, #f8fafc);
`;

const ConfigItemRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: var(--ads-v2-spaces-4);
justify-content: space-between;
margin-bottom: var(--ads-v2-spaces-4);
`;

const ConfigItemSelectWrapper = styled.div`
width: 50%;
min-width: 160px;
`;

export const FunctionCallingConfigToolField = ({
index,
onRemove,
...props
}: FunctionCallingConfigToolFieldProps) => {
const dispatch = useDispatch();
const fieldValue = useSelector((state: AppState) =>
formValueSelector(props.formName)(state, props.fieldPath),
);
const entityOptions = useSelector(selectEntityOptions);

const handleRemoveButtonClick = useCallback(() => {
onRemove(index);
}, [onRemove, index]);

const findEntityOption = useCallback(
(entityId: string, items: FunctionCallingEntityTypeOption[]): boolean => {
return items.find(({ value }) => value === entityId) !== undefined;
},
[entityOptions],
);

const findEntityType = useCallback(
(entityId: string): string => {
switch (true) {
case findEntityOption(entityId, entityOptions.Query):
return "Query";
case findEntityOption(entityId, entityOptions.JSFunction):
return "JSFunction";
case findEntityOption(entityId, entityOptions.SystemFunction):
return "SystemFunction";
}

return "";
},
[fieldValue.entityId, entityOptions],
);

useEffect(
// entityType is dependent on entityId
// Every time entityId changes, we need to find the new entityType
function handleEntityTypeChange() {
const entityType = findEntityType(fieldValue.entityId);

dispatch(
change(props.formName, `${props.fieldPath}.entityType`, entityType),
);
},
[fieldValue.entityId],
);

return (
<ConfigItemRoot>
<ConfigItemRow>
<Text isBold renderAs="p">
Function
</Text>
<Button
isIconButton
kind="tertiary"
onClick={handleRemoveButtonClick}
startIcon="trash"
/>
</ConfigItemRow>
<ConfigItemRow>
<ConfigItemSelectWrapper>
<FormControl
config={{
controlType: "DROP_DOWN",
configProperty: `${props.fieldPath}.entityId`,
formName: props.formName,
id: props.fieldPath,
label: "",
isValid: true,
// FormControl component has incomplete TypeScript definitions for some valid properties
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
isSearchable: true,
options: [
...entityOptions.Query,
...entityOptions.JSFunction,
...entityOptions.SystemFunction,
],
optionGroupConfig: {
Query: {
label: "Queries",
children: [],
},
JSFunction: {
label: "JS Functions",
children: [],
},
SystemFunction: {
label: "System Functions",
children: [],
},
} satisfies Record<
FunctionCallingEntityType,
DropDownGroupedOptions
>,
}}
formName={props.formName}
/>
</ConfigItemSelectWrapper>
<FormControl
config={{
controlType: "SWITCH",
configProperty: `${props.fieldPath}.requiresApproval`,
formName: props.formName,
id: props.fieldPath,
label: "Requires approval",
isValid: true,
}}
formName={props.formName}
/>
</ConfigItemRow>
<FormControl
config={{
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
configProperty: `${props.fieldPath}.description`,
formName: props.formName,
id: props.fieldPath,
placeholderText: "Describe how this function should be used...",
label: "Description",
isValid: true,
}}
formName={props.formName}
/>
</ConfigItemRoot>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Text } from "@appsmith/ads";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import React, { memo } from "react";
import styled from "styled-components";

const Root = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--ads-v2-spaces-4);
height: 300px;
`;

export const FunctionCallingEmpty = memo(() => {
return (
<Root>
<img alt="" src={getAssetUrl(`${ASSETS_CDN_URL}/empty-state.svg`)} />
<Text>Function Calls will be displayed here</Text>
</Root>
);
});
Loading

0 comments on commit 989b568

Please sign in to comment.