Skip to content

Commit

Permalink
feat: Display message data (#154)
Browse files Browse the repository at this point in the history
* Display message data

---------

Co-authored-by: Hyungu Kang | Airen <[email protected]>
  • Loading branch information
liamcho and bang9 authored Apr 29, 2024
1 parent 1ee76aa commit a0d57c0
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 10 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?

3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ChatAiWidget from './components/ChatAiWidget'; //import { ChatAiWidget } from "@sendbird/chat-ai-widget";
import { Constant } from './const';
import { SendbirdChatAICallbacks } from './interfaces';

interface Props extends Partial<Constant> {
applicationId?: string;
Expand All @@ -10,6 +11,7 @@ interface Props extends Partial<Constant> {
accentColor: string;
isOpen: boolean;
}) => React.ReactElement;
callbacks?: SendbirdChatAICallbacks;
}

const App = (props: Props) => {
Expand Down Expand Up @@ -40,6 +42,7 @@ const App = (props: Props) => {
autoOpen={props.autoOpen}
renderWidgetToggleButton={props.renderWidgetToggleButton}
serviceName={props.serviceName}
callbacks={props.callbacks}
/>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/BotMessageWithBodyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import Label, {
LabelTypography,
LabelColors,
} from '@sendbird/uikit-react/ui/Label';
import { ReactNode } from 'react';
import React, { ReactNode } from 'react';
import styled from 'styled-components';

import BotMessageFeedback from './BotMessageFeedback';
import BotProfileImage from './BotProfileImage';
import { SentTime, BodyContainer } from './MessageComponent';
import MessageDataContent from './MessageDataContent';
import { useConstantState } from '../context/ConstantContext';
import { formatCreatedAtToAMPM } from '../utils';
import { isLastMessageInStreaming } from '../utils/messages';
Expand Down
29 changes: 22 additions & 7 deletions src/components/CustomChannelComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SendableMessage } from '@sendbird/chat/lib/__definition';
import { SendingStatus } from '@sendbird/chat/message';
import { SendingStatus, UserMessage } from '@sendbird/chat/message';
import ChannelUI from '@sendbird/uikit-react/GroupChannel/components/GroupChannelUI';
import { Message } from '@sendbird/uikit-react/GroupChannel/components/Message';
import { useGroupChannelContext } from '@sendbird/uikit-react/GroupChannel/context';
Expand All @@ -12,11 +12,11 @@ import ChatBottom from './ChatBottom';
import CustomChannelHeader from './CustomChannelHeader';
import CustomMessage from './CustomMessage';
import DynamicRepliesPanel from './DynamicRepliesPanel';
import MessageDataContent from './MessageDataContent';
import StaticRepliesPanel from './StaticRepliesPanel';
import { useConstantState } from '../context/ConstantContext';
import useAutoDismissMobileKyeboardHandler from '../hooks/useAutoDismissMobileKyeboardHandler';
import { useScrollOnStreaming } from '../hooks/useScrollOnStreaming';
import useWidgetLocalStorage from '../hooks/useWidgetLocalStorage';
import { hideChatBottomBanner, isIOSMobile } from '../utils';
import {
getBotWelcomeMessages,
Expand Down Expand Up @@ -72,7 +72,7 @@ const Root = styled.div<RootStyleProps>`
font-size: ${isIOSMobile ? 16 : 14}px;
font-family: 'Roboto', sans-serif;
line-height: 20px;
color: ${({ theme }) => theme.textColor.messageInput};
color: ${({ theme }) => theme.textColor.messageInput}; // FIXME: messageInput does not exist
resize: none;
border: none;
outline: none;
Expand Down Expand Up @@ -109,16 +109,28 @@ const Root = styled.div<RootStyleProps>`
}
`;

function isForSendbirdDashboard(data: object | undefined) {
return (
data &&
'chat-ai-widget-preview' in data &&
data['chat-ai-widget-preview'] === 'True'
);
}

export function CustomChannelComponent() {
const { suggestedMessageContent, botId, enableEmojiFeedback } =
useConstantState();
const {
suggestedMessageContent,
botId,
enableEmojiFeedback,
customUserAgentParam,
} = useConstantState();
const {
messages: allMessages,
currentChannel: channel,
scrollToBottom,
refresh,
} = useGroupChannelContext();
const { userId } = useWidgetLocalStorage();

const botUser = channel?.members.find((member) => member.userId === botId);

const lastMessageRef = useRef<HTMLDivElement>(null);
Expand All @@ -139,7 +151,7 @@ export function CustomChannelComponent() {
?.suggested_replies ?? []) as string[];

const isStaticReplyVisible = getStaticMessageVisibility(
lastMessage ?? null,
(lastMessage as UserMessage) ?? null,
botUser?.userId,
suggestedMessageContent,
enableEmojiFeedback
Expand Down Expand Up @@ -231,6 +243,9 @@ export function CustomChannelComponent() {
) : isStaticReplyVisible ? (
<StaticRepliesPanel botUser={botUser} />
) : null)}
{isForSendbirdDashboard(customUserAgentParam) && message.data && (
<MessageDataContent messageData={message.data} />
)}
</Message>
);
}}
Expand Down
237 changes: 237 additions & 0 deletions src/components/MessageDataContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import styled from 'styled-components';

import { useConstantState } from '../context/ConstantContext';
import { ReactComponent as ChevronRightIcon } from '../icons/icon-chevron-right.svg';
import { ReactComponent as EllipsisIcon } from '../icons/icon-ellipsis.svg';
import { ReactComponent as MessageBubbleIcon } from '../icons/icon-message-bubble.svg';
import { FunctionCallData } from '../interfaces';
import { noop } from '../utils';

const Text = styled.div`
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.1px;
`;

const ViewDetails = styled.div`
display: flex;
align-items: center;
color: #6210cc;
path {
fill: #6210cc;
}
&:hover {
color: #4e11a1;
cursor: pointer;
path {
fill: #4e11a1;
}
}
&:focus {
border: 1px solid #6210cc;
}
&:active {
color: #0d0d0d;
path {
fill: #0d0d0d;
}
}
&:disabled {
color: #a6a6a6;
path {
fill: #a6a6a6;
}
}
`;

const WorkFlowType = styled.div`
border-radius: 2px;
border: 1px solid #ccc;
font-size: 12px;
font-weight: 400;
line-height: 16px;
padding: 0 4px;
`;

const Root = styled.div`
display: flex;
justify-content: flex-start;
margin-top: 16px;
margin-left: 36px;
`;

const SideBar = styled.div`
width: 4px;
border-radius: 100px;
background-color: #e0e0e0;
margin-left: 8px;
`;

const DataContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 4px;
width: 100%;
margin-left: 16px;
`;

const DataRow = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
`;

const AdditionalInfo = styled.div`
color: #858585;
font-size: 12px;
font-weight: 400;
line-height: 16px;
margin-top: 5px;
`;

interface MessageDataContentProps {
messageData: string;
}

enum IntentPayload {
EXACT_MATCHED = 'exact_question_match',
KEYWORD_MATCHED = 'keyword_match',
SIMILAR_MATCHED = 'similar_question_match',
}

interface WorkflowObject {
name: string;
intent_type: IntentPayload;
}

interface MessageDataObject {
function_calls: object[];
workflow: WorkflowObject;
}

interface FunctionCallRenderData {
name: string;
onClick: () => void;
}

interface WorkflowData {
name: string;
type: string;
}

const INTENT_MAP = {
exact_question_match: 'Exact match',
keyword_match: 'Keyword',
similar_question_match: 'Intent classified',
};

function isObjectOfViewDetailData(object: any): object is FunctionCallData {
const { name, request, response_text, status_code } = object ?? {};
return (
typeof name === 'string' &&
typeof request === 'object' &&
typeof response_text === 'string' &&
typeof status_code === 'number'
);
}

function isValidFunctionCalls(
functionCallsData: object | undefined
): functionCallsData is FunctionCallData[] {
return (
Array.isArray(functionCallsData) &&
functionCallsData.length > 0 &&
functionCallsData.every((functionCallData) =>
isObjectOfViewDetailData(functionCallData)
)
);
}

function isValidWorkflowData(object: any): object is WorkflowObject {
const { name, intent_type } = object ?? {};
return (
typeof name === 'string' &&
Object.values(IntentPayload).includes(intent_type)
);
}

export default function MessageDataContent({
messageData,
}: MessageDataContentProps) {
const { callbacks } = useConstantState();
const onViewDetailClick = callbacks?.onViewDetailClick;

function getMessageContentData(): [
WorkflowData | null,
FunctionCallRenderData[]
] {
let newWorkflow: WorkflowData | null = null;
const newFunctionCalls: FunctionCallRenderData[] = [];

try {
const messageDataObject: MessageDataObject = JSON.parse(messageData);
const functionCallsData = messageDataObject?.function_calls;
if (
Array.isArray(functionCallsData) &&
functionCallsData.length > 0 &&
isValidFunctionCalls(functionCallsData)
) {
functionCallsData.forEach((functionCallData) => {
if (functionCallData.name) {
const functionCall =
typeof onViewDetailClick === 'function'
? onViewDetailClick
: noop;
newFunctionCalls.push({
name: functionCallData.name,
onClick: () => functionCall(functionCallData),
});
}
});
}
const workflowData = messageDataObject?.workflow;
if (isValidWorkflowData(workflowData)) {
newWorkflow = {
name: workflowData.name,
type: INTENT_MAP[workflowData.intent_type],
} as WorkflowData;
}
return [newWorkflow, newFunctionCalls];
} catch (e) {
return [newWorkflow, newFunctionCalls];
}
}
const [workflow, functionCalls] = getMessageContentData();

if (!workflow && functionCalls.length === 0) return null;

return (
<Root>
<SideBar />
<DataContainer>
{workflow && (
<DataRow>
<MessageBubbleIcon id="aichatbot-widget-ellipsis-icon" />
<Text>{workflow.name}</Text>
<WorkFlowType>{workflow.type}</WorkFlowType>
</DataRow>
)}
{functionCalls.map((renderData, index) => (
<DataRow key={index}>
<EllipsisIcon id="aichatbot-widget-message-bubble-icon" />
<Text>{renderData.name}</Text>
<ViewDetails onClick={renderData.onClick}>
<Text>View details</Text>
<ChevronRightIcon id="aichatbot-widget-chevron-right-icon" />
</ViewDetails>
</DataRow>
))}
<AdditionalInfo>Only visible in the dashboard widget</AdditionalInfo>
</DataContainer>
</Root>
);
}
3 changes: 3 additions & 0 deletions src/context/ConstantContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { LabelStringSet } from '@sendbird/uikit-react/ui/Label';
import { createContext, useContext, useMemo } from 'react';

import { type Constant, DEFAULT_CONSTANT } from '../const';
import { SendbirdChatAICallbacks } from '../interfaces';

const initialState = DEFAULT_CONSTANT;

interface ConstantContextProps extends Constant {
applicationId: string | null;
botId: string | null;
callbacks?: SendbirdChatAICallbacks;
}
const ConstantContext = createContext<ConstantContextProps>({
applicationId: null,
Expand Down Expand Up @@ -35,6 +37,7 @@ export const ConstantStateProvider = (props: ProviderProps) => {
props.suggestedMessageContent?.messageFilterList ??
initialState.suggestedMessageContent.messageFilterList,
},
callbacks: props.callbacks,
firstMessageData: props.firstMessageData ?? [],
createGroupChannelParams:
props.createGroupChannelParams ?? initialState.createGroupChannelParams,
Expand Down
5 changes: 5 additions & 0 deletions src/icons/icon-chevron-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a0d57c0

Please sign in to comment.