Skip to content

Commit

Permalink
feat(chat): add draft action buttons flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Magomed-Elbi Dzhukalaev committed Dec 19, 2024
1 parent e6803f4 commit bf1f7a5
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 36 deletions.
5 changes: 4 additions & 1 deletion apps/chat/src/components/Chat/ChatInput/ChatInputMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DialFile, DialLink } from '@/src/types/files';
import { Prompt } from '@/src/types/prompt';
import { Translation } from '@/src/types/translation';

import { ChatSelectors } from '@/src/store/chat/chat.reducer';
import {
ConversationsActions,
ConversationsSelectors,
Expand Down Expand Up @@ -115,6 +116,7 @@ export const ChatInputMessage = ({
);
const isModelsLoaded = useAppSelector(ModelsSelectors.selectIsModelsLoaded);
const isChatFullWidth = useAppSelector(UISelectors.selectIsChatFullWidth);
const chatFormValue = useAppSelector(ChatSelectors.selectChatFormOptions);

const shouldRegenerate =
isLastMessageError || (isLastAssistantMessageEmpty && !messageIsStreaming);
Expand Down Expand Up @@ -223,7 +225,7 @@ export const ChatInputMessage = ({

onSend({
role: Role.User,
content,
content: chatFormValue ? JSON.stringify(chatFormValue) : content,
custom_content: getUserCustomContent(
selectedFiles,
selectedFolders,
Expand All @@ -244,6 +246,7 @@ export const ChatInputMessage = ({
isSendDisabled,
dispatch,
onSend,
chatFormValue,
content,
selectedFiles,
selectedFolders,
Expand Down
18 changes: 6 additions & 12 deletions apps/chat/src/components/Chat/ChatMessage/ChatMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
MessageUserButtons,
} from '@/src/components/Chat/ChatMessage/MessageButtons';
import { MessageFormSchema } from '@/src/components/Chat/ChatMessage/MessageFormSchema/MessageFormSchema';
import { UserMessageContent } from '@/src/components/Chat/ChatMessage/UserMessageContent';
import { MessageAttachments } from '@/src/components/Chat/MessageAttachments';
import { MessageStages } from '@/src/components/Chat/MessageStages';
import { ModelIcon } from '@/src/components/Chatbar/ModelIcon';
Expand Down Expand Up @@ -578,18 +579,11 @@ export const ChatMessageContent = ({
<>
<div className="relative mr-2 flex w-full flex-col gap-5">
{message.content && (
<div
className={classNames(
'prose min-w-full flex-1 whitespace-pre-wrap',
{
'max-w-none': isChatFullWidth,
'text-sm': isOverlay,
'leading-[150%]': isMobileOrOverlay,
},
)}
>
{message.content}
</div>
<UserMessageContent
message={message}
messageIndex={messageIndex}
allMessages={conversation.messages}
/>
)}
<MessageAttachments
attachments={message.custom_content?.attachments}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,124 @@
import { ChatActions } from '@/src/store/chat/chat.reducer';
import { useAppDispatch } from '@/src/store/hooks';
import { useCallback } from 'react';

import { DialMessageFormSchema } from '@epam/ai-dial-shared';
import classNames from 'classnames';

interface MessageFormSchemaProps {
schema: DialMessageFormSchema;
import { ChatActions, ChatSelectors } from '@/src/store/chat/chat.reducer';
import { useAppDispatch, useAppSelector } from '@/src/store/hooks';

import {
DialMessageFormSchema,
DialWidgets,
FormSchemaButtonOption,
} from '@epam/ai-dial-shared';

interface ButtonWidgetProps {
constValue: number;
title: string;
widgetOptions: FormSchemaButtonOption['dial:widgetOptions'];
isLastMessage: boolean;
property: string;
}

export const MessageFormSchema = ({ schema }: MessageFormSchemaProps) => {
const ButtonWidget = ({
title,
constValue,
widgetOptions,
isLastMessage,
property,
}: ButtonWidgetProps) => {
const dispatch = useAppDispatch();

const formOptions = useAppSelector(ChatSelectors.selectChatFormOptions);

const isSelected = isLastMessage && formOptions?.[property] === constValue;

const handleClick = useCallback(() => {
if (isLastMessage && widgetOptions?.populateText && !widgetOptions.submit) {
dispatch(
ChatActions.setFormOptions({
content: widgetOptions.populateText,
property,
value: constValue,
}),
);
}
}, [isLastMessage, widgetOptions, dispatch, property, constValue]);

return (
<div>
{Object.entries(schema.properties).map(([key, property]) => (
<div key={key} className="flex flex-col gap-2">
<button
onClick={handleClick}
className={classNames(
'button button-secondary',
isSelected && '!border-accent-primary',
)}
disabled={!isLastMessage}
>
{title}
</button>
);
};

interface PropertiesMapperProps {
property: string;
isLastMessage: boolean;
widget?: DialWidgets;
description?: string;
oneOf?: FormSchemaButtonOption[];
}

const PropertiesMapper = ({
property,
isLastMessage,
widget,
description,
oneOf,
}: PropertiesMapperProps) => {
if (widget === DialWidgets.buttons)
return (
<div className="flex flex-col gap-2">
{description && (
<p className="mt-2 border-t border-tertiary py-2 text-sm text-primary">
{property?.description}
{description}
</p>
<div className="flex items-center gap-2">
{property?.oneOf?.map((item) => (
<button
onClick={() => {
dispatch(ChatActions.setInputContent(item.title));
}}
key={item.const}
className="button button-secondary"
>
{item.title}
</button>
))}
</div>
)}
<div className="flex items-center gap-2">
{oneOf?.map((item) => (
<ButtonWidget
key={item.const}
constValue={item.const}
title={item.title}
widgetOptions={item['dial:widgetOptions']}
isLastMessage={isLastMessage}
property={property}
/>
))}
</div>
</div>
);

return null;
};

interface MessageFormSchemaProps {
schema: DialMessageFormSchema;
isLastMessage: boolean;
}

export const MessageFormSchema = ({
schema,
isLastMessage,
}: MessageFormSchemaProps) => {
return (
<div>
{Object.entries(schema.properties).map(([key, property]) => (
<PropertiesMapper
key={key}
property={key}
isLastMessage={isLastMessage}
widget={property['dial:widget']}
oneOf={property.oneOf}
description={property.description}
/>
))}
</div>
);
Expand Down
105 changes: 105 additions & 0 deletions apps/chat/src/components/Chat/ChatMessage/UserMessageContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import classNames from 'classnames';

import { isSmallScreen } from '@/src/utils/app/mobile';

import { useAppSelector } from '@/src/store/hooks';
import { SettingsSelectors } from '@/src/store/settings/settings.reducers';
import { UISelectors } from '@/src/store/ui/ui.reducers';

import {
DialMessageFormSchema,
DialWidgets,
FormSchemaPropertyValue,
Message,
} from '@epam/ai-dial-shared';

const isFormActionReply = (
message: Message,
index: number,
allMessages: Message[],
) => {
if (index === 0 || !allMessages[index - 1]?.custom_content?.form_schema)
return false;

try {
if (JSON.parse(message.content)) return true;
} catch {
return false;
}
};

const getFormActionReplyWidgets = (
message: Message,
index: number,
allMessages: Message[],
) => {
if (!isFormActionReply(message, index, allMessages)) return [];
const schema = allMessages[index - 1]?.custom_content
?.form_schema as DialMessageFormSchema;
const parsedReply: Record<string, FormSchemaPropertyValue> = JSON.parse(
message.content,
);

return Object.entries(schema.properties).map(([key, value]) => ({
property: key,
widget: value['dial:widget'],
value: parsedReply[key],
label: value.oneOf?.find((option) => option.const === parsedReply[key])
?.title,
}));
};

interface UserMessageContentProps {
message: Message;
messageIndex: number;
allMessages: Message[];
}

export const UserMessageContent = ({
message,
messageIndex,
allMessages,
}: UserMessageContentProps) => {
const isChatFullWidth = useAppSelector(UISelectors.selectIsChatFullWidth);
const isOverlay = useAppSelector(SettingsSelectors.selectIsOverlay);
const isMobileOrOverlay = isSmallScreen() || isOverlay;

const isFormActionReplyMsg = isFormActionReply(
message,
messageIndex,
allMessages,
);
const userReplyProperties = getFormActionReplyWidgets(
message,
messageIndex,
allMessages,
);

return (
<div
className={classNames('prose min-w-full flex-1 whitespace-pre-wrap', {
'max-w-none': isChatFullWidth,
'text-sm': isOverlay,
'leading-[150%]': isMobileOrOverlay,
})}
>
{isFormActionReplyMsg ? (
<div className="flex items-center gap-2">
{userReplyProperties
.filter(({ widget }) => widget === DialWidgets.buttons)
.map((property) => (
<button
key={property.property}
className="button button-secondary"
disabled
>
{property.label ?? property.value}
</button>
))}
</div>
) : (
message.content
)}
</div>
);
};
26 changes: 26 additions & 0 deletions apps/chat/src/store/chat/chat.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { PayloadAction, createSelector, createSlice } from '@reduxjs/toolkit';

import { RootState } from '..';

import { FormSchemaPropertyValue } from '@epam/ai-dial-shared';

export interface ChatState {
inputContent: string;
formOptions?: Record<string, FormSchemaPropertyValue>;
}

const initialState: ChatState = {
Expand All @@ -16,6 +19,23 @@ export const chatSlice = createSlice({
reducers: {
setInputContent: (state, { payload }: PayloadAction<string>) => {
state.inputContent = payload;
if (state.formOptions) state.formOptions = undefined;
},
setFormOptions(
state,
{
payload,
}: PayloadAction<{
property: string;
value: FormSchemaPropertyValue;
content?: string;
}>,
) {
state.inputContent = payload.content ?? '';
state.formOptions = {
...(state.formOptions || {}),
[payload.property]: payload.value,
};
},
},
});
Expand All @@ -27,8 +47,14 @@ export const selectInputContent = createSelector(
(state) => state.inputContent,
);

export const selectChatFormOptions = createSelector(
[rootSelector],
(state) => state.formOptions,
);

export const ChatActions = chatSlice.actions;

export const ChatSelectors = {
selectInputContent,
selectChatFormOptions,
};
2 changes: 2 additions & 0 deletions libs/shared/src/types/dial-message-form-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface FormSchemaButtonOption {
};
}

export type FormSchemaPropertyValue = string | number | boolean;

export type FormSchemaPropertyType =
| 'array'
| 'boolean'
Expand Down

0 comments on commit bf1f7a5

Please sign in to comment.