Skip to content

Commit

Permalink
feat: add initial assistant message (#36798)
Browse files Browse the repository at this point in the history
## Description

![image](https://github.com/user-attachments/assets/bb8cf448-6bfe-485a-9e19-d222ae3d8411)



Fixes #36776  

> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.Sanity"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/11275055683>
> Commit: a8f1554
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11275055683&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Thu, 10 Oct 2024 14:29:09 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Introduced `AssistantSuggestionButton` for enhanced user interaction
in the AI chat.
- Added support for displaying and applying assistant suggestions in
chat threads.
	- Implemented an editable array component for managing string pairs.
- Enhanced configuration options with new properties for initial
assistant messages and suggestions.

- **Improvements**
	- Improved state management for dynamic messages in the AI chat widget.
- Updated rendering logic for conditional display of suggestions in chat
messages.
- Added new props to facilitate better interaction and suggestion
handling in chat components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
znamenskii-ilia authored Oct 10, 2024
1 parent 5fadce5 commit a0814e1
Show file tree
Hide file tree
Showing 15 changed files with 388 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const _AIChat = (props: AIChatProps, ref: ForwardedRef<HTMLDivElement>) => {
// assistantName,
chatTitle,
isWaitingForResponse = false,
onApplyAssistantSuggestion,
onPromptChange,
onSubmit,
prompt,
Expand Down Expand Up @@ -56,7 +57,12 @@ const _AIChat = (props: AIChatProps, ref: ForwardedRef<HTMLDivElement>) => {

<ul className={styles.thread} data-testid="t--aichat-thread">
{thread.map((message: ChatMessage) => (
<ThreadMessage {...message} key={message.id} username={username} />
<ThreadMessage
{...message}
key={message.id}
onApplyAssistantSuggestion={onApplyAssistantSuggestion}
username={username}
/>
))}
</ul>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Text } from "@appsmith/wds";
import { clsx } from "clsx";
import React from "react";
import { Button as HeadlessButton } from "react-aria-components";
import styles from "./styles.module.css";
import type { AssistantSuggestionButtonProps } from "./types";

export const AssistantSuggestionButton = ({
children,
className,
...rest
}: AssistantSuggestionButtonProps) => {
return (
<HeadlessButton className={clsx(styles.root, className)} {...rest}>
<Text>{children}</Text>
</HeadlessButton>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./AssistantSuggestionButton";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.root {
height: 30px;
padding: 0 var(--inner-spacing-4);
background-color: var(--bg-neutral-subtle-alt, #e7e8e8);
border-radius: var(--radius-inner-button, 1.8px);

&:hover {
background-color: var(--bg-neutral-subtle-alt-hover, #f0f1f1);
}

&:focus-visible {
box-shadow:
0 0 0 2px var(--color-bg),
0 0 0 4px var(--color-bd-focus);
}

&:active {
background-color: var(--bg-neutral-subtle-alt-active, #e1e2e2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { PropsWithChildren } from "react";
import type { ButtonProps as HeadlessButtonProps } from "react-aria-components";

export interface AssistantSuggestionButtonProps
extends PropsWithChildren<HeadlessButtonProps> {}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Text } from "@appsmith/wds";
import { Flex, Text } from "@appsmith/wds";
import { clsx } from "clsx";
import React from "react";
import Markdown from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter";
import { monokai } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import { AssistantSuggestionButton } from "../AssistantSuggestionButton";
import { UserAvatar } from "../UserAvatar";
import styles from "./styles.module.css";
import type { ThreadMessageProps } from "./types";
Expand All @@ -12,6 +13,8 @@ export const ThreadMessage = ({
className,
content,
isAssistant,
onApplyAssistantSuggestion,
promptSuggestions = [],
username,
...rest
}: ThreadMessageProps) => {
Expand Down Expand Up @@ -50,6 +53,25 @@ export const ThreadMessage = ({
{content}
</Markdown>
</Text>

{promptSuggestions.length > 0 && (
<Flex
className={styles.suggestions}
gap="var(--inner-spacing-5)"
paddingTop="spacing-4"
wrap="wrap"
>
{promptSuggestions.map((suggestion) => (
<AssistantSuggestionButton
key={suggestion}
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
onPress={() => onApplyAssistantSuggestion?.(suggestion)}
>
{suggestion}
</AssistantSuggestionButton>
))}
</Flex>
)}
</div>
) : (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export interface ThreadMessageProps extends HTMLProps<HTMLLIElement> {
content: string;
isAssistant: boolean;
username: string;
promptSuggestions?: string[];
onApplyAssistantSuggestion?: (suggestion: string) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ChatMessage {
id: string;
content: string;
isAssistant: boolean;
promptSuggestions?: string[];
}

export interface AIChatProps {
Expand All @@ -15,4 +16,5 @@ export interface AIChatProps {
isWaitingForResponse?: boolean;
onPromptChange: (prompt: string) => void;
onSubmit?: () => void;
onApplyAssistantSuggestion?: (suggestion: string) => void;
}
1 change: 0 additions & 1 deletion app/client/packages/rts/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ATTR_SERVICE_INSTANCE_ID,
} from "@opentelemetry/semantic-conventions/incubating";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
Expand Down
152 changes: 152 additions & 0 deletions app/client/src/components/propertyControls/ArrayComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 UpdateItemsFunction = (
items: string[],
isUpdatedViaKeyboard?: boolean,
) => void;

interface ArrayComponentProps {
items: string[];
updateItems: UpdateItemsFunction;
addLabel?: string;
}

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

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

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

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

items.length !== 0 && !typing && setRenderItems(items);
}, [props, items.length, renderItems.length, typing]);

const debouncedUpdateItems = useCallback(
debounce((updatedItems: string[]) => {
props.updateItems(updatedItems, true);
}, 200),
[props.updateItems],
);

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

items = Array.isArray(items) ? items : [];
const updatedItems = updateOptionLabel(items, index, updatedKey);
const updatedRenderItems = updateOptionLabel(
renderItems,
index,
updatedKey,
);

setRenderItems(updatedRenderItems);
debouncedUpdateItems(updatedItems);
}

function deleteItem(index: number, isUpdatedViaKeyboard = false) {
let { items } = props;

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

const newItems = items.filter((o, i) => i !== index);
const newRenderItems = renderItems.filter((o, i) => i !== index);

setRenderItems(newRenderItems);
props.updateItems(newItems, isUpdatedViaKeyboard);
}

function addItem(e: React.MouseEvent) {
let { items } = props;

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

items.push("");

const updatedRenderItems = renderItems.slice();

updatedRenderItems.push("");

setRenderItems(updatedRenderItems);
props.updateItems(items, e.detail === 0);
}

function onInputFocus() {
setTyping(true);
}

function onInputBlur() {
setTyping(false);
}

return (
<>
{renderItems.map((item: string, index) => {
return (
<ControlWrapper key={index} orientation={"HORIZONTAL"}>
<StyledInputGroup
dataType={"text"}
onBlur={onInputBlur}
onChange={(value: string) => updateKey(index, value)}
onFocus={onInputFocus}
value={item}
/>
<StyledBox />
<Button
isIconButton
kind="tertiary"
onClick={(e: React.MouseEvent) =>
deleteItem(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={addItem}
size="sm"
startIcon="plus"
>
{props.addLabel || "Add suggestion"}
</Button>
</div>
</>
);
}
48 changes: 48 additions & 0 deletions app/client/src/components/propertyControls/ArrayControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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
items={this.props.propertyValue}
updateItems={this.updateItems}
/>
);
}

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

static getControlType() {
return "ARRAY_INPUT";
}

static canDisplayValueInUI(_config: ControlData, value: string): boolean {
if (isDynamicValue(value)) return false;

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

for (const x of items) {
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,6 @@ export const defaultsConfig = {
widgetType: "AI_CHAT",
version: 1,
responsiveBehavior: ResponsiveBehavior.Fill,
initialAssistantMessage: "",
initialAssistantSuggestions: [],
} as unknown as WidgetDefaultProps;
Loading

0 comments on commit a0814e1

Please sign in to comment.