diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/AIChat.tsx b/app/client/packages/design-system/widgets/src/components/AIChat/src/AIChat.tsx index d6975d9750a2..40a50d43754f 100644 --- a/app/client/packages/design-system/widgets/src/components/AIChat/src/AIChat.tsx +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/AIChat.tsx @@ -14,6 +14,7 @@ const _AIChat = (props: AIChatProps, ref: ForwardedRef) => { // assistantName, chatTitle, isWaitingForResponse = false, + onApplyAssistantSuggestion, onPromptChange, onSubmit, prompt, @@ -56,7 +57,12 @@ const _AIChat = (props: AIChatProps, ref: ForwardedRef) => { diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/AssistantSuggestionButton.tsx b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/AssistantSuggestionButton.tsx new file mode 100644 index 000000000000..e28f98c1228e --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/AssistantSuggestionButton.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/index.ts b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/index.ts new file mode 100644 index 000000000000..c9075961d613 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/index.ts @@ -0,0 +1,2 @@ +export * from "./AssistantSuggestionButton"; +export * from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/styles.module.css b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/styles.module.css new file mode 100644 index 000000000000..6aa30119ff76 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/styles.module.css @@ -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); + } +} diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/types.ts b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/types.ts new file mode 100644 index 000000000000..20186f75d246 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/AssistantSuggestionButton/types.ts @@ -0,0 +1,5 @@ +import type { PropsWithChildren } from "react"; +import type { ButtonProps as HeadlessButtonProps } from "react-aria-components"; + +export interface AssistantSuggestionButtonProps + extends PropsWithChildren {} diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/ThreadMessage.tsx b/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/ThreadMessage.tsx index 37ea1f1c7923..7b827d3f576d 100644 --- a/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/ThreadMessage.tsx +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/ThreadMessage.tsx @@ -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"; @@ -12,6 +13,8 @@ export const ThreadMessage = ({ className, content, isAssistant, + onApplyAssistantSuggestion, + promptSuggestions = [], username, ...rest }: ThreadMessageProps) => { @@ -50,6 +53,25 @@ export const ThreadMessage = ({ {content} + + {promptSuggestions.length > 0 && ( + + {promptSuggestions.map((suggestion) => ( + onApplyAssistantSuggestion?.(suggestion)} + > + {suggestion} + + ))} + + )} ) : ( <> diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/types.ts b/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/types.ts index 8935dfe1e5c4..4459c37a5a94 100644 --- a/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/types.ts +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/ThreadMessage/types.ts @@ -4,4 +4,6 @@ export interface ThreadMessageProps extends HTMLProps { content: string; isAssistant: boolean; username: string; + promptSuggestions?: string[]; + onApplyAssistantSuggestion?: (suggestion: string) => void; } diff --git a/app/client/packages/design-system/widgets/src/components/AIChat/src/types.ts b/app/client/packages/design-system/widgets/src/components/AIChat/src/types.ts index 69572c5d6f83..e27a804b91cf 100644 --- a/app/client/packages/design-system/widgets/src/components/AIChat/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/AIChat/src/types.ts @@ -2,6 +2,7 @@ export interface ChatMessage { id: string; content: string; isAssistant: boolean; + promptSuggestions?: string[]; } export interface AIChatProps { @@ -15,4 +16,5 @@ export interface AIChatProps { isWaitingForResponse?: boolean; onPromptChange: (prompt: string) => void; onSubmit?: () => void; + onApplyAssistantSuggestion?: (suggestion: string) => void; } diff --git a/app/client/packages/rts/src/instrumentation.ts b/app/client/packages/rts/src/instrumentation.ts index cdc892a3b9eb..083c5ca6663d 100644 --- a/app/client/packages/rts/src/instrumentation.ts +++ b/app/client/packages/rts/src/instrumentation.ts @@ -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"; diff --git a/app/client/src/components/propertyControls/ArrayComponent.tsx b/app/client/src/components/propertyControls/ArrayComponent.tsx new file mode 100644 index 000000000000..ec98932273b9 --- /dev/null +++ b/app/client/src/components/propertyControls/ArrayComponent.tsx @@ -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( + items: Array, + 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([]); + const [typing, setTyping] = useState(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 ( + + updateKey(index, value)} + onFocus={onInputFocus} + value={item} + /> + + + + + ); +} diff --git a/app/client/src/components/propertyControls/ArrayControl.tsx b/app/client/src/components/propertyControls/ArrayControl.tsx new file mode 100644 index 000000000000..8dab080e041a --- /dev/null +++ b/app/client/src/components/propertyControls/ArrayControl.tsx @@ -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 { + render() { + return ( + + ); + } + + 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; diff --git a/app/client/src/components/propertyControls/index.ts b/app/client/src/components/propertyControls/index.ts index 6683cd30246b..cf85e900e04b 100644 --- a/app/client/src/components/propertyControls/index.ts +++ b/app/client/src/components/propertyControls/index.ts @@ -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, diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/defaultConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/defaultConfig.ts index 6d618658a411..77e0374f4092 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/defaultConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/defaultConfig.ts @@ -7,4 +7,6 @@ export const defaultsConfig = { widgetType: "AI_CHAT", version: 1, responsiveBehavior: ResponsiveBehavior.Fill, + initialAssistantMessage: "", + initialAssistantSuggestions: [], } as unknown as WidgetDefaultProps; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/propertyPaneContent.ts b/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/propertyPaneContent.ts index ac4abc2c8b17..8fff6583b8d7 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/propertyPaneContent.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/config/propertyPaneContent.ts @@ -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", diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/index.tsx index f94fb5935f71..0c2d773abae5 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSAIChatWidget/widget/index.tsx @@ -27,10 +27,12 @@ import { export interface WDSAIChatWidgetProps extends ContainerWidgetProps {} + export interface Message { id: string; content: string; role: "assistant" | "user" | "system"; + promptSuggestions?: string[]; } interface State extends WidgetState { @@ -43,24 +45,7 @@ class WDSAIChatWidget extends BaseWidget { static type = "WDS_AI_CHAT_WIDGET"; state = { - messages: [ - { - id: "1", - content: "Hello! How can I help you?", - role: "assistant" as const, - }, - { - id: "2", - content: "Find stuck support requests", - role: "user" as const, - }, - { - id: "3", - content: - "I'm finding these customer support requests that have been waiting for a response for over a day:", - role: "assistant" as const, - }, - ], + messages: [] as Message[], prompt: "", isWaitingForResponse: false, }; @@ -123,13 +108,85 @@ class WDSAIChatWidget extends BaseWidget { return {}; } - adaptMessages(messages: Message[]): ChatMessage[] { - return messages.map((message) => ({ - ...message, - isAssistant: message.role === "assistant", - })); + 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] : [], + })); + } } + updatePrompt = (prompt: string) => { + this.setState({ prompt }); + }; + + adaptMessages = (messages: Message[]): ChatMessage[] => { + const chatMessages: ChatMessage[] = messages.map((message) => { + if (message.role === "assistant") { + return { + id: message.id, + content: message.content, + isAssistant: true, + promptSuggestions: message.promptSuggestions || [], + }; + } + + return { + id: message.id, + content: message.content, + isAssistant: false, + }; + }); + + return chatMessages; + }; + handleMessageSubmit = (event?: FormEvent) => { event?.preventDefault(); @@ -148,18 +205,7 @@ class WDSAIChatWidget extends BaseWidget { }), () => { 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", @@ -182,6 +228,8 @@ class WDSAIChatWidget extends BaseWidget { id: Math.random().toString(), content: this.props.queryData.choices[0].message.content, role: "assistant", + // TODO: Add prompt suggestions from the query data, if any + promptSuggestions: [], }, ], isWaitingForResponse: false, @@ -190,7 +238,11 @@ class WDSAIChatWidget extends BaseWidget { }; handlePromptChange = (prompt: string) => { - this.setState({ prompt }); + this.updatePrompt(prompt); + }; + + handleApplyAssistantSuggestion = (suggestion: string) => { + this.updatePrompt(suggestion); }; getWidgetView(): ReactNode { @@ -199,6 +251,7 @@ class WDSAIChatWidget extends BaseWidget { assistantName={this.props.assistantName} chatTitle={this.props.chatTitle} isWaitingForResponse={this.state.isWaitingForResponse} + onApplyAssistantSuggestion={this.handleApplyAssistantSuggestion} onPromptChange={this.handlePromptChange} onSubmit={this.handleMessageSubmit} prompt={this.state.prompt}