Skip to content

Commit

Permalink
configure assistant initial message
Browse files Browse the repository at this point in the history
  • Loading branch information
znamenskii-ilia committed Oct 10, 2024
1 parent b96e900 commit 0fac236
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 18 deletions.
154 changes: 154 additions & 0 deletions app/client/src/components/propertyControls/ArrayComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Button } from "@appsmith/ads";
import { debounce } from "lodash";
import React, { useCallback, useEffect, useState } from "react";
import styled from "styled-components";
import { ControlWrapper, InputGroup } from "./StyledControls";

function updateOptionLabel<T>(
items: Array<T>,
index: number,
updatedLabel: string,
) {
return items.map((option: T, optionIndex) => {
if (index !== optionIndex) {
return option;
}

return updatedLabel;
});
}

const StyledBox = styled.div`
width: 10px;
`;

type UpdatePairFunction = (
pair: string[],
isUpdatedViaKeyboard?: boolean,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => any;

interface ArrayComponentProps {
pairs: string[];
updatePairs: UpdatePairFunction;
addLabel?: string;
}

const StyledInputGroup = styled(InputGroup)`
> .ads-v2-input__input-section > div {
flex: 1;
min-width: 0px;
}
`;

export function ArrayComponent(props: ArrayComponentProps) {
const [renderPairs, setRenderPairs] = useState<string[]>([]);
const [typing, setTyping] = useState<boolean>(false);
const { pairs } = props;

useEffect(() => {
let { pairs } = props;

pairs = Array.isArray(pairs) ? pairs.slice() : [];

pairs.length !== 0 && !typing && setRenderPairs(pairs);
}, [props, pairs.length, renderPairs.length, typing]);

const debouncedUpdatePairs = useCallback(
debounce((updatedPairs: string[]) => {
props.updatePairs(updatedPairs, true);
}, 200),
[props.updatePairs],
);

function updateKey(index: number, updatedKey: string) {
let { pairs } = props;

pairs = Array.isArray(pairs) ? pairs : [];
const updatedPairs = updateOptionLabel(pairs, index, updatedKey);
const updatedRenderPairs = updateOptionLabel(
renderPairs,
index,
updatedKey,
);

setRenderPairs(updatedRenderPairs);
debouncedUpdatePairs(updatedPairs);
}

function deletePair(index: number, isUpdatedViaKeyboard = false) {
let { pairs } = props;

pairs = Array.isArray(pairs) ? pairs : [];

const newPairs = pairs.filter((o, i) => i !== index);
const newRenderPairs = renderPairs.filter((o, i) => i !== index);

setRenderPairs(newRenderPairs);
props.updatePairs(newPairs, isUpdatedViaKeyboard);
}

function addPair(e: React.MouseEvent) {
let { pairs } = props;

pairs = Array.isArray(pairs) ? pairs.slice() : [];

pairs.push("");

const updatedRenderPairs = renderPairs.slice();

updatedRenderPairs.push("");

setRenderPairs(updatedRenderPairs);
props.updatePairs(pairs, e.detail === 0);
}

function onInputFocus() {
setTyping(true);
}

function onInputBlur() {
setTyping(false);
}

return (
<>
{renderPairs.map((pair: string, index) => {
return (
<ControlWrapper key={index} orientation={"HORIZONTAL"}>
<StyledInputGroup
dataType={"text"}
onBlur={onInputBlur}
onChange={(value: string) => updateKey(index, value)}
onFocus={onInputFocus}
value={pair}
/>
<StyledBox />
<Button
isIconButton
kind="tertiary"
onClick={(e: React.MouseEvent) =>
deletePair(index, e.detail === 0)
}
size="sm"
startIcon="delete-bin-line"
/>
</ControlWrapper>
);
})}

<div className="flex flex-row-reverse mt-1">
<Button
className="t--property-control-options-add"
kind="tertiary"
onClick={addPair}
size="sm"
startIcon="plus"
>
{props.addLabel || "Add suggestion"}
</Button>
</div>
</>
);
}
50 changes: 50 additions & 0 deletions app/client/src/components/propertyControls/ArrayControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { objectKeys } from "@appsmith/utils";
import type { DropdownOption } from "components/constants";
import React from "react";
import { isDynamicValue } from "utils/DynamicBindingUtils";
import { ArrayComponent } from "./ArrayComponent";
import type { ControlData, ControlProps } from "./BaseControl";
import BaseControl from "./BaseControl";

class ArrayControl extends BaseControl<ControlProps> {
render() {
return (
<ArrayComponent
pairs={this.props.propertyValue}
updatePairs={this.updateItems}
/>
);
}

updateItems = (items: string[], isUpdatedViaKeyboard = false) => {
this.updateProperty(this.props.propertyName, items, isUpdatedViaKeyboard);
};

static getControlType() {
return "ARRAY_INPUT";
}

// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static canDisplayValueInUI(config: ControlData, value: any): boolean {
if (isDynamicValue(value)) return false;

try {
const pairs: DropdownOption[] = JSON.parse(value);

for (const x of pairs) {
const keys = objectKeys(x);

if (!keys.includes("label") || !keys.includes("value")) {
return false;
}
}
} catch {
return false;
}

return true;
}
}

export default ArrayControl;
2 changes: 2 additions & 0 deletions app/client/src/components/propertyControls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,14 @@ import type { IconSelectControlV2Props } from "./IconSelectControlV2";
import IconSelectControlV2 from "./IconSelectControlV2";
import PrimaryColumnsControlWDS from "./PrimaryColumnsControlWDS";
import ToolbarButtonListControl from "./ToolbarButtonListControl";
import ArrayControl from "./ArrayControl";

export const PropertyControls = {
InputTextControl,
DropDownControl,
SwitchControl,
OptionControl,
ArrayControl,
CodeEditorControl,
DatePickerControl,
ActionSelectorControl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const defaultsConfig = {
widgetType: "AI_CHAT",
version: 1,
responsiveBehavior: ResponsiveBehavior.Fill,
initialAssistantSuggestions: [],
} as unknown as WidgetDefaultProps;
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,27 @@ export const propertyPaneContent = [
defaultValue: "",
},
{
helpText: "Configures a prompt for the assistant",
propertyName: "systemPrompt",
label: "Prompt",
helpText: "Configures an initial assistant message",
propertyName: "initialAssistantMessage",
label: "Initial Assistant Message",
controlType: "INPUT_TEXT",
isJSConvertible: false,
isBindProperty: false,
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
defaultValue: "",
},
{
helpText: "Configures initial assistant suggestions",
propertyName: "initialAssistantSuggestions",
label: "Initial Assistant Suggestions",
controlType: "ARRAY_INPUT",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.ARRAY },
defaultValue: [],
},
{
helpText: "Controls the visibility of the widget",
propertyName: "isVisible",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class WDSAIChatWidget extends BaseWidget<WDSAIChatWidgetProps, State> {
static type = "WDS_AI_CHAT_WIDGET";

state = {
messages: [],
messages: [] as Message[],
prompt: "",
isWaitingForResponse: false,
};
Expand Down Expand Up @@ -108,6 +108,77 @@ class WDSAIChatWidget extends BaseWidget<WDSAIChatWidgetProps, State> {
return {};
}

componentDidMount() {
// Add initial assistant message with suggestions if they were configured
if (this.props.initialAssistantMessage.length > 0) {
this.setState((state) => ({
...state,
messages: [
{
id: Math.random().toString(),
content: this.props.initialAssistantMessage,
role: "assistant",
promptSuggestions: this.props.initialAssistantSuggestions || [],
},
],
}));
}
}

componentDidUpdate(prevProps: WDSAIChatWidgetProps): void {
// Track changes in the widget's properties and update the local state accordingly

// Update the initial assistant message
if (
prevProps.initialAssistantMessage !==
this.props.initialAssistantMessage ||
prevProps.initialAssistantSuggestions !==
this.props.initialAssistantSuggestions
) {
let updatedMessage: Message | null;

//
if (this.props.initialAssistantMessage.length > 0) {
const currentMessage = this.state.messages[0];

updatedMessage = {
// If the initial assistant message is set, update it
// Otherwise, create a new one
...(currentMessage || {
id: Math.random().toString(),
role: "assistant",
}),
content: this.props.initialAssistantMessage,
promptSuggestions: this.props.initialAssistantSuggestions,
};
} else {
updatedMessage = null;
}

this.setState((state) => ({
...state,
messages: updatedMessage ? [updatedMessage] : [],
}));
}
}

addAssistantInitialMessage = (
content: string,
promptSuggestions: string[],
) => {
this.setState((state) => ({
messages: [
...state.messages,
{
id: Math.random().toString(),
content,
role: "assistant",
promptSuggestions,
},
],
}));
};

updatePrompt = (prompt: string) => {
this.setState({ prompt });
};
Expand Down Expand Up @@ -151,18 +222,7 @@ class WDSAIChatWidget extends BaseWidget<WDSAIChatWidgetProps, State> {
}),
() => {
const messages: Message[] = [...this.state.messages];

if (this.props.systemPrompt) {
messages.unshift({
id: String(Date.now()),
content: this.props.systemPrompt,
role: "system",
});
}

const params = {
messages,
};
const params = { messages };

this.executeAction({
triggerPropertyName: "onClick",
Expand Down

0 comments on commit 0fac236

Please sign in to comment.