Skip to content

Commit

Permalink
Merge pull request #111 from Cognigy/fix/#87752-message-collation-iss…
Browse files Browse the repository at this point in the history
…ue-for-data-only-messages

fix: #87752 message collation logic overhaul using react context and object oriented approach
  • Loading branch information
vj-venkatesan authored Feb 13, 2025
2 parents fa8d3c4 + 7f594c6 commit 2244e80
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 7 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ jobs:
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install ESLint
run: |
npm install [email protected]
npm install @microsoft/eslint-formatter-sarif@2.1.7
npm install @microsoft/eslint-formatter-sarif
- name: Run ESLint
run: npx eslint .
Expand Down
14 changes: 12 additions & 2 deletions src/messages/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { IStreamingMessage, IWebchatConfig, IWebchatTheme, MessageSender } from

import "src/theme.css";
import classes from "./Message.module.css";
import { isEventMessage, isMessageCollatable } from "../utils";
import { CollateMessage, isEventMessage } from "../utils";
import { IMessage } from "@cognigy/socket-client";
import { useCollation } from "./hooks";

export interface MessageProps {
action?: MessageSender;
Expand All @@ -35,6 +36,8 @@ export interface MessageProps {
) => void;
}

const defaultCollate = new CollateMessage();

const Message: FC<MessageProps> = props => {
const {
action,
Expand All @@ -51,7 +54,14 @@ const Message: FC<MessageProps> = props => {
plugins,
prevMessage,
} = props;
const shouldCollate = isMessageCollatable(message, prevMessage);

// Get the collation instance from the context
const collate = useCollation();

// If it is not in the context, use the default collation instance
const shouldCollate = collate
? collate.isMessageCollatable(message, config, plugins, prevMessage)
: defaultCollate.isMessageCollatable(message, config, plugins, prevMessage);

const showHeader = !shouldCollate && !isFullscreen && !isEventMessage(message);

Expand Down
20 changes: 20 additions & 0 deletions src/messages/collation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { createContext } from "react";
import { CollateMessage } from "src/utils";

// Create the context with undefined as initial value
const CollationContext = createContext<CollateMessage | undefined>(undefined);

// Provider props type
interface CollationProviderProps {
children: React.ReactNode;
sessionId?: string;
}

// Provider component
function CollationProvider({ children }: CollationProviderProps) {
const instance = new CollateMessage();

return <CollationContext.Provider value={instance}>{children}</CollationContext.Provider>;
}

export { CollationContext, CollationProvider };
11 changes: 9 additions & 2 deletions src/messages/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useContext, useState } from "react";
import { MessageContext } from "./context.tsx";
import { getRandomId } from "src/utils.ts";
import { CollateMessage, getRandomId } from "src/utils.ts";
import { CollationContext } from "./collation.tsx";

function useMessageContext() {
const state = useContext(MessageContext);
Expand All @@ -18,4 +19,10 @@ const useRandomId = (prefix = "") => {
return id;
};

export { useMessageContext, useRandomId };
// Custom hook for using the collation context
function useCollation(): CollateMessage | undefined {
const context = useContext(CollationContext);
return context ?? undefined;
}

export { useMessageContext, useRandomId, useCollation };
66 changes: 66 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IMessage, IWebchatMessage } from "@cognigy/socket-client";
import { IWebchatConfig } from "./messages/types";
import { ActionButtonsProps } from "./common/ActionButtons/ActionButtons";
import { match, MessagePlugin } from "./matcher";

/**
* Decides between _webchat and _facebook payload.
Expand Down Expand Up @@ -35,6 +36,71 @@ export const getWebchatButtonLabel: getWebchatButtonLabel = button => {
return title;
};

export class CollateMessage {
private firstBotMessageMap: Map<string, IMessage> = new Map();
private readonly COLLATION_LIMIT: number = 1000 * 60; // 60 sec
private SESSION_ID: string = "default";

private isMessageValid(
plugins: MessagePlugin[] | undefined,
config: IWebchatConfig | undefined,
message: IMessage | undefined,
) {
if (!message) return false;

const matchedPlugins = match(message, config, plugins);

if (!matchedPlugins.length) return false;

return true;
}

isMessageCollatable(
message: IMessage,
config?: IWebchatConfig,
plugins?: MessagePlugin[],
prevMessage?: IMessage,
) {
const difference = Number(message?.timestamp) - Number(prevMessage?.timestamp);

if (config?.initialSessionId && !this.SESSION_ID) {
this.SESSION_ID = config.initialSessionId;
}

// XAppSubmitMessages is a pill, and should always be collated
if (message?.data?._plugin?.type === "x-app-submit") return true;

if (message.source !== "bot") this.firstBotMessageMap.delete(this.SESSION_ID);

// if the previous message was a rating message that displays an event status pill, don't collate
if (
prevMessage?.source === "user" &&
prevMessage?.data?._cognigy?.controlCommands?.[0]?.type === "setRating" &&
prevMessage?.data?._cognigy?.controlCommands?.[0]?.parameters?.showRatingStatus === true
)
return false;

const isMessageValid = this.isMessageValid(plugins, config, message);

// If this is the first valid bot message don't collate
if (
!this.firstBotMessageMap.get(this.SESSION_ID) &&
isMessageValid &&
message.source === "bot"
) {
this.firstBotMessageMap.set(this.SESSION_ID, message);
return false;
}

return (
prevMessage &&
isNaN(difference) === false &&
difference < this.COLLATION_LIMIT &&
prevMessage?.source === message?.source
);
}
}

export const isMessageCollatable = (message: IMessage, prevMessage?: IMessage) => {
const COLLATION_LIMIT = 1000 * 60; // 60 sec

Expand Down
59 changes: 59 additions & 0 deletions test/Collation.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,63 @@ describe("Collation", () => {
const messageHeaders = getAllByTestId("message-header");
expect(messageHeaders).toHaveLength(1);
});

it("handles collation with empty messages correctly", () => {
const botMessage1: IMessage = {
text: "First message always has a header",
source: "bot",
timestamp: "1701163314138",
};
const botMessage2: IMessage = {
text: "This message does not have a header",
source: "bot",
timestamp: "1701163319138",
};
const botMessage3: IMessage = {
text: "This message has a new a header",
source: "agent",
timestamp: "1701163314138",
};
const botMessage4: IMessage = {
text: "Has header",
source: "bot",
timestamp: "1701163319000",
};

const botMessage5: IMessage = {
text: "",
//@ts-expect-error
data: { _some: "" },
source: "bot",
timestamp: "1701163319111",
};

const botMessage6: IMessage = {
text: "Has NO header",
//@ts-expect-error
data: { _some: "" },
source: "bot",
timestamp: "1701163319222",
};
const userMessage1: IMessage = {
text: "Help me find the id",
source: "user",
timestamp: String(Date.now()),
};

const { getAllByTestId } = render(
<>
<Message message={botMessage1} />
<Message message={botMessage2} prevMessage={botMessage1} />
<Message message={botMessage3} prevMessage={botMessage2} />
<Message message={botMessage4} prevMessage={botMessage3} />
<Message message={userMessage1} prevMessage={botMessage4} />
<Message message={botMessage5} prevMessage={userMessage1} />
<Message message={botMessage6} prevMessage={botMessage5} />
</>,
);

const messageHeaders = getAllByTestId("message-header");
expect(messageHeaders).toHaveLength(5);
});
});
61 changes: 61 additions & 0 deletions test/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,67 @@ const screens: TScreen[] = [
timestamp: "1701163314138",
},
},
{
message: {
text: "Has header",
source: "bot",
timestamp: "1701163319000",
},
},
{
message: {
text: "",
data: { _some: "" },
source: "bot",
timestamp: "1701163319111",
},
prevMessage: {
source: "bot",
timestamp: "1701163314000",
},
},
{
message: {
text: "Has NO header",
data: { _some: "" },
source: "bot",
timestamp: "1701163319222",
},
prevMessage: {
source: "bot",
timestamp: "1701163314111",
},
},
{
message: {
text: "This message is from user",
source: "user",
timestamp: "1701163314138",
},
},
{
message: {
text: "",
data: { DoesNotRender: "" },
source: "bot",
timestamp: "1701163319888",
},
prevMessage: {
source: "user",
timestamp: "1701163314138",
},
},
{
message: {
text: "Has header",
source: "bot",
timestamp: "170116331999",
},
prevMessage: {
source: "bot",
timestamp: "1701163319888",
},
},
],
},
{
Expand Down
1 change: 0 additions & 1 deletion test/matcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ describe("Message Matcher", () => {
},
];
const matchResult = match(message, undefined, config);
console.log(matchResult);
expect(matchResult[0]?.name).toBe("component");
});
});

0 comments on commit 2244e80

Please sign in to comment.