diff --git a/copilot-widget/lib/chat-sdk/chat.ts b/copilot-widget/lib/chat-sdk/chat.ts new file mode 100644 index 000000000..7cb34b13e --- /dev/null +++ b/copilot-widget/lib/chat-sdk/chat.ts @@ -0,0 +1,51 @@ +import { AxiosInstance } from "axios"; +import { Massenger } from "."; +import { HistoryMessage, SessionIdType, getInstance } from "./data"; +import { radnom } from "./rand"; +import get from 'lodash.get'; + +type ChatOptionsType = { + copilotToken: string; + baseUrl: string; + inject?: { + user?: Record; + headers?: Record; + } +} + +export class Chat { + private sessionId?: SessionIdType; + history: HistoryMessage[] = []; + options: ChatOptionsType; + messenger: Massenger = new Massenger(this); + axiosInstance: AxiosInstance | null = null; + + constructor(options: ChatOptionsType) { + this.options = options; + this.initAxios(); + } + + initAxios() { + this.axiosInstance = getInstance({ + baseUrl: get(this.options, "baseUrl"), + copilotToken: get(this.options, "copilotToken"), + sessionId: this.getSessionId, + }); + } + + get getSessionId(): SessionIdType { + if (!this.sessionId) { + this.setSessionId = this.generateSessionId(); + } + return this.sessionId!; + } + + set setSessionId(sessionId: SessionIdType) { + this.sessionId = sessionId; + } + + generateSessionId(): SessionIdType { + return (radnom() + ":" + this.options.copilotToken) as SessionIdType; + } + +} \ No newline at end of file diff --git a/copilot-widget/lib/chat-sdk/data/index.ts b/copilot-widget/lib/chat-sdk/data/index.ts new file mode 100644 index 000000000..d0eafe9aa --- /dev/null +++ b/copilot-widget/lib/chat-sdk/data/index.ts @@ -0,0 +1,60 @@ +import axios, { AxiosInstance } from "axios"; + +export type SessionIdType = `${string}:${string}`; +const SESSION_ID_HEADER = "X-Session-Id"; +const COPILOT_TOKEN_HEADER = "X-Bot-Token"; + +type InstanceOptions = { + copilotToken: string; + sessionId: SessionIdType; + baseUrl: string; +} + +// only one instance per copilotToken +let instances = new Map(); + +export function getInstance( + { + copilotToken, + sessionId, + baseUrl, + }: InstanceOptions, +) { + // make sure we only have one instance + if (!instances.has(copilotToken)) { + const instance = axios.create({ + baseURL: baseUrl, + headers: { + [SESSION_ID_HEADER]: sessionId, + [COPILOT_TOKEN_HEADER]: copilotToken, + }, + }); + instances.set(copilotToken, instance); + } + return instances.get(copilotToken)!; +} + +export function getInitialData(instance: AxiosInstance) { + return instance.get("/chat/init"); +} + +export type HistoryMessage = { + chatbot_id: string; + created_at: string; + from_user: boolean; + id: number; + message: string; + session_id: string; + updated_at: string; +}; + +export interface InitialDataType { + bot_name: string; + logo: string; + history: HistoryMessage[]; + sound_effects: { + submit: string; + response: string; + }; + inital_questions: string[]; +} diff --git a/copilot-widget/lib/chat-sdk/index.ts b/copilot-widget/lib/chat-sdk/index.ts new file mode 100644 index 000000000..5a6a191c9 --- /dev/null +++ b/copilot-widget/lib/chat-sdk/index.ts @@ -0,0 +1 @@ +export { Chat } from './chat'; \ No newline at end of file diff --git a/copilot-widget/lib/chat-sdk/messages.ts b/copilot-widget/lib/chat-sdk/messages.ts new file mode 100644 index 000000000..67406d470 --- /dev/null +++ b/copilot-widget/lib/chat-sdk/messages.ts @@ -0,0 +1,96 @@ +import get from "lodash.get"; +import { radnom } from "./rand"; + +type Actor = "user" | "bot"; +type IdType = string | number; +type TimeStampType = number | Date; + +type UserMessageOptions = { + timestamp?: TimeStampType; + id?: IdType; + from?: Actor; + content: string; +} + +export type UserMessagePayload = { + message: string; + id?: IdType; + timestamp?: TimeStampType; +} +export class UserMessage { + id: IdType; + timestamp?: TimeStampType; + from: Actor = "user"; + content: string; + + constructor( + content: string, + options?: UserMessageOptions + ) { + this.content = content; + this.id = get(options, "id", radnom()); + this.timestamp = get(options, "timestamp", Date.now()); + } + + public get payload(): { + content: string; + id?: IdType; + timestamp?: TimeStampType; + } { + return { + content: this.content, + id: this.id, + timestamp: this.timestamp, + } + } +} + +type CopilotMessageTypes = "text"; + +class CopilotResponse { + id: IdType; + timestamp: TimeStampType; + from: Actor = "bot"; + type: CopilotMessageTypes; + private userMessageId: IdType | null = null; + + constructor({ + type, + id, + timestamp = Date.now(), + }: { type: CopilotMessageTypes, id: IdType, timestamp?: TimeStampType }) { + this.type = type; + this.id = id; + this.timestamp = timestamp; + + } + public set setUserMessageId(id: IdType) { + this.userMessageId = id; + } + public get getUserMessageId() { + return this.userMessageId; + } +} +export class CopilotTextResponse extends CopilotResponse { + response: string; + + constructor(response: string, id?: IdType) { + super({ type: "text", id: id || radnom() }); + this.response = response; + } + + public get payload(): { + response: string; + id?: IdType; + timestamp?: TimeStampType; + } { + return { + response: this.response, + id: this.id, + timestamp: this.timestamp, + } + } +} + + +export type MessageType = CopilotTextResponse | UserMessage; \ No newline at end of file diff --git a/copilot-widget/lib/chat-sdk/messenger.ts b/copilot-widget/lib/chat-sdk/messenger.ts new file mode 100644 index 000000000..c35ab67bd --- /dev/null +++ b/copilot-widget/lib/chat-sdk/messenger.ts @@ -0,0 +1,67 @@ +import { Chat } from "./chat"; +import { CopilotTextResponse, MessageType, UserMessage } from "./messages"; + +export class Massenger { + // responsible for sending,storing messages + private messages: Set = new Set(); + + chat: Chat; + + constructor(chat: Chat) { + this.chat = chat; + } + + private get axios() { + return this.chat.axiosInstance + } + + private set addMessage(message: MessageType) { + this.messages.add(message) + } + + public get getMessages() { + return this.messages; + } + + public clearMessages() { + this.messages.clear(); + } + + public queryMessage(id: string | number) { + // query message by id + return [...this.messages].find((message) => message.id === id); + } + + async handleMessage(message: UserMessage) { + if (!this.axios) { + throw new Error("axios is not initialized"); + } + // Currently we only support text messages + const { data, status } = await this.axios.post<{ + type: "text", + response: { + text: string | null + } + }>("/chat/send", message.payload); + + if (status !== 200) { + throw new Error("Failed to send message"); + } + + if (data.type === 'text') { + const copilotResp = new CopilotTextResponse(data.response.text || 'null') + copilotResp.setUserMessageId = message.id; + this.addMessage = copilotResp; + } + } + + async sendMessage(message: string) { + const userMessage = new UserMessage(message); + this.addMessage = userMessage; + await this.handleMessage(userMessage); + } + toArray() { + return [...this.messages] + } + +} diff --git a/copilot-widget/lib/chat-sdk/rand.ts b/copilot-widget/lib/chat-sdk/rand.ts new file mode 100644 index 000000000..521497a97 --- /dev/null +++ b/copilot-widget/lib/chat-sdk/rand.ts @@ -0,0 +1,3 @@ +export function radnom() { + return Math.random().toString(36).substring(2, 15) +} \ No newline at end of file diff --git a/copilot-widget/package.json b/copilot-widget/package.json index d5876c85e..87f0ddd20 100644 --- a/copilot-widget/package.json +++ b/copilot-widget/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6", "@tailwindcss/typography": "^0.5.9", + "@types/lodash.get": "^4.4.9", "@types/node": "^20.4.7", "@types/react": "^18.x", "@types/react-dom": "^18.x", @@ -33,6 +34,7 @@ "eslint": "^8.38.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.3.4", + "lodash.get": "^4.4.2", "postcss": "^8.4.31", "postcss-prefix-selector": "^1.16.0", "prettier": "^2.8.8", diff --git a/copilot-widget/pnpm-lock.yaml b/copilot-widget/pnpm-lock.yaml index 79df6b1e3..6c30ced64 100644 --- a/copilot-widget/pnpm-lock.yaml +++ b/copilot-widget/pnpm-lock.yaml @@ -17,6 +17,9 @@ devDependencies: '@tailwindcss/typography': specifier: ^0.5.9 version: 0.5.9(tailwindcss@3.3.3) + '@types/lodash.get': + specifier: ^4.4.9 + version: 4.4.9 '@types/node': specifier: ^20.4.7 version: 20.5.6 @@ -50,6 +53,9 @@ devDependencies: eslint-plugin-react-refresh: specifier: ^0.3.4 version: 0.3.5(eslint@8.48.0) + lodash.get: + specifier: ^4.4.2 + version: 4.4.2 postcss: specifier: ^8.4.31 version: 8.4.31 @@ -1514,6 +1520,16 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/lodash.get@4.4.9: + resolution: {integrity: sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==} + dependencies: + '@types/lodash': 4.14.202 + dev: true + + /@types/lodash@4.14.202: + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + dev: true + /@types/mdast@3.0.12: resolution: {integrity: sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==} dependencies: @@ -3758,6 +3774,10 @@ packages: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} dev: true + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: true + /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} dev: true