feat: add support chat widget demo and state management
- Implemented SupportWidgetDemo component for configuring and mounting the support chat widget. - Introduced Zustand store for managing support chat state, including message handling and socket connection. - Created support host bridge for communication between the widget and parent window. - Added utility functions for JWT token generation and local storage management. - Enhanced user experience with notifications for new messages and chat status updates.
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
import { translateCurrentMessage } from "@/i18n/messages"
|
||||
import { readKefuChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||
import { readSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||
import { generateUUID } from "@/lib/utils"
|
||||
|
||||
export type Paging = {
|
||||
@@ -159,7 +159,7 @@ export function getGuestId() {
|
||||
}
|
||||
|
||||
function getRuntimeImConfig() {
|
||||
const widgetConfig = readKefuChatRuntimeConfig()
|
||||
const widgetConfig = readSupportChatRuntimeConfig()
|
||||
const baseUrl = (widgetConfig.apiBaseUrl || widgetConfig.baseUrl || API_BASE_URL)
|
||||
.trim()
|
||||
.replace(/\/$/, "")
|
||||
|
||||
@@ -51,12 +51,12 @@ export function renderIMMessageHTML(message: {
|
||||
asset.filename || "image"
|
||||
)}"></p>`
|
||||
}
|
||||
return `<p>${escapeHTML(t("kefu.imageSummary"))}</p>`
|
||||
return `<p>${escapeHTML(t("supportChat.imageSummary"))}</p>`
|
||||
}
|
||||
|
||||
if (message.messageType === "attachment") {
|
||||
if (asset?.url) {
|
||||
const title = escapeHTML(asset.filename || message.content || t("kefu.attachmentSummary"))
|
||||
const title = escapeHTML(asset.filename || message.content || t("supportChat.attachmentSummary"))
|
||||
const meta = formatFileSize(asset.fileSize ?? 0)
|
||||
const metaHTML = meta ? `<div class="im-attachment-meta">${escapeHTML(meta)}</div>` : ""
|
||||
return `<div class="im-attachment"><a href="${escapeHTMLAttr(
|
||||
@@ -65,7 +65,7 @@ export function renderIMMessageHTML(message: {
|
||||
asset.filename || ""
|
||||
)}" class="im-attachment-link"><span class="im-attachment-icon" aria-hidden="true">${getAttachmentIconSVG()}</span><span class="im-attachment-content"><span class="im-attachment-title">${title}</span>${metaHTML}</span></a></div>`
|
||||
}
|
||||
return `<p>${escapeHTML(message.content || t("kefu.attachmentSummary"))}</p>`
|
||||
return `<p>${escapeHTML(message.content || t("supportChat.attachmentSummary"))}</p>`
|
||||
}
|
||||
|
||||
return renderTextMessageHTML(message.content || "")
|
||||
@@ -77,13 +77,13 @@ export function summarizeIMMessage(message: {
|
||||
payload?: string
|
||||
}) {
|
||||
if (message.messageType === "image") {
|
||||
return t("kefu.imageSummary")
|
||||
return t("supportChat.imageSummary")
|
||||
}
|
||||
if (message.messageType === "attachment") {
|
||||
const asset = parseMessageAssetPayload(message.payload)
|
||||
return asset?.filename?.trim()
|
||||
? `${t("kefu.attachmentSummary")} ${asset.filename.trim()}`
|
||||
: t("kefu.attachmentSummary")
|
||||
? `${t("supportChat.attachmentSummary")} ${asset.filename.trim()}`
|
||||
: t("supportChat.attachmentSummary")
|
||||
}
|
||||
if (message.messageType === "html") {
|
||||
const text = extractTextFromHTML(message.content)
|
||||
@@ -91,11 +91,11 @@ export function summarizeIMMessage(message: {
|
||||
return text.substring(0, 100)
|
||||
}
|
||||
if (message.content.includes("<img")) {
|
||||
return t("kefu.imageSummary")
|
||||
return t("supportChat.imageSummary")
|
||||
}
|
||||
return t("kefu.messageSummary")
|
||||
return t("supportChat.messageSummary")
|
||||
}
|
||||
return message.content?.substring(0, 100) || t("kefu.messageSummary")
|
||||
return message.content?.substring(0, 100) || t("supportChat.messageSummary")
|
||||
}
|
||||
|
||||
export function formatFileSize(size: number) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createWebSocketBaseUrl } from "@/lib/api/websocket"
|
||||
import { getCustomerSessionToken, type ImMessage } from "@/lib/api/im"
|
||||
import { readKefuChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||
import { readSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||
import type {
|
||||
RealtimeConversationPatch,
|
||||
RealtimeMessageCreatedPayload,
|
||||
@@ -22,7 +22,7 @@ export type ImRealtimeEnvelope = {
|
||||
}
|
||||
|
||||
export function createImRealtimeConnection() {
|
||||
const config = readKefuChatRuntimeConfig()
|
||||
const config = readSupportChatRuntimeConfig()
|
||||
const apiBaseUrl = (config.apiBaseUrl || "").trim()
|
||||
const baseUrl = apiBaseUrl
|
||||
? apiBaseUrl.replace(/^http/, "ws").replace(/\/$/, "")
|
||||
|
||||
@@ -16,7 +16,7 @@ export type CSAgentConfig = {
|
||||
width?: string
|
||||
}
|
||||
|
||||
export type KefuChatRuntimeConfig = Omit<CSAgentConfig, "getUserToken"> & {
|
||||
export type SupportChatRuntimeConfig = Omit<CSAgentConfig, "getUserToken"> & {
|
||||
/** Used only by /support/chat to exchange for a chat token; not part of CSAgentConfig. */
|
||||
userToken?: string
|
||||
}
|
||||
@@ -33,7 +33,7 @@ declare global {
|
||||
interface Window {
|
||||
CSAgentConfig?: CSAgentConfig
|
||||
CSAgentWidget?: CSAgentWidget
|
||||
__CS_AGENT_WIDGET_CONFIG__?: KefuChatRuntimeConfig
|
||||
__CS_AGENT_WIDGET_CONFIG__?: SupportChatRuntimeConfig
|
||||
__CS_AGENT_WIDGET_STATE__?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
CSAgentConfig,
|
||||
CSAgentWidget,
|
||||
KefuChatRuntimeConfig,
|
||||
SupportChatRuntimeConfig,
|
||||
} from "./config-types"
|
||||
|
||||
type NormalizedCSAgentConfig = CSAgentConfig & {
|
||||
@@ -24,7 +24,7 @@ type WidgetState = {
|
||||
frameHideTimer: number | null
|
||||
frameDestroyTimer: number | null
|
||||
config: NormalizedCSAgentConfig | null
|
||||
frameConfig: KefuChatRuntimeConfig | null
|
||||
frameConfig: SupportChatRuntimeConfig | null
|
||||
frameUrl: URL | null
|
||||
animationDuration: number
|
||||
listenerBound?: boolean
|
||||
@@ -57,7 +57,7 @@ function getLauncherText() {
|
||||
}
|
||||
|
||||
type FrameMessage =
|
||||
| { type: "cs-agent:init"; payload: KefuChatRuntimeConfig }
|
||||
| { type: "cs-agent:init"; payload: SupportChatRuntimeConfig }
|
||||
| { type: "cs-agent:open" }
|
||||
| { type: "cs-agent:minimize" }
|
||||
| { type: "cs-agent:maximized"; payload: { isMaximized: boolean } }
|
||||
@@ -135,7 +135,7 @@ type FrameMessage =
|
||||
function createFrameConfig(
|
||||
config: NormalizedCSAgentConfig,
|
||||
userToken: string
|
||||
): KefuChatRuntimeConfig {
|
||||
): SupportChatRuntimeConfig {
|
||||
const { getUserToken: _getUserToken, ...payload } = config
|
||||
if (userToken) {
|
||||
return { ...payload, userToken }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KefuChatRuntimeConfig } from "@/lib/sdk/config-types"
|
||||
import type { SupportChatRuntimeConfig } from "@/lib/sdk/config-types"
|
||||
|
||||
export function readKefuChatRuntimeConfig(): KefuChatRuntimeConfig {
|
||||
export function readSupportChatRuntimeConfig(): SupportChatRuntimeConfig {
|
||||
if (typeof window === "undefined") {
|
||||
return {
|
||||
channelId: "",
|
||||
@@ -10,7 +10,7 @@ export function readKefuChatRuntimeConfig(): KefuChatRuntimeConfig {
|
||||
}
|
||||
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const fallback: KefuChatRuntimeConfig = {
|
||||
const fallback: SupportChatRuntimeConfig = {
|
||||
channelId:
|
||||
query.get("channelId") ??
|
||||
process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() ??
|
||||
@@ -46,7 +46,7 @@ export function readKefuChatRuntimeConfig(): KefuChatRuntimeConfig {
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function setKefuChatRuntimeConfig(config: KefuChatRuntimeConfig) {
|
||||
export function setSupportChatRuntimeConfig(config: SupportChatRuntimeConfig) {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ import { summarizeIMMessage } from "@/lib/im-message"
|
||||
import { createRealtimeConnectionManager } from "@/lib/realtime-connection"
|
||||
import { generateUUID } from "@/lib/utils"
|
||||
import {
|
||||
readKefuChatRuntimeConfig,
|
||||
setKefuChatRuntimeConfig,
|
||||
readSupportChatRuntimeConfig,
|
||||
setSupportChatRuntimeConfig,
|
||||
} from "@/lib/sdk/runtime-config"
|
||||
import { translateCurrentMessage } from "@/i18n/messages"
|
||||
|
||||
@@ -107,7 +107,7 @@ function markConversationReadMessages(
|
||||
return next
|
||||
}
|
||||
|
||||
export type KefuChatStore = {
|
||||
export type SupportChatStore = {
|
||||
title: string
|
||||
subtitle: string
|
||||
themeColor: string
|
||||
@@ -149,7 +149,7 @@ function t(key: string) {
|
||||
return translateCurrentMessage(key)
|
||||
}
|
||||
|
||||
export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
export const useSupportChatStore = create<SupportChatStore>((set, get) => {
|
||||
const realtime = createRealtimeConnectionManager({
|
||||
createSocket: createImRealtimeConnection,
|
||||
canReconnect: () => Boolean(get().isOpen && get().conversation?.id),
|
||||
@@ -204,7 +204,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
document.visibilityState !== "visible"
|
||||
) {
|
||||
const state = get()
|
||||
showNotification(t("kefu.newMessage"), getNotificationBody(message), () => {
|
||||
showNotification(t("supportChat.newMessage"), getNotificationBody(message), () => {
|
||||
state.setIsOpen(true)
|
||||
state.setIsVisible(true)
|
||||
})
|
||||
@@ -236,7 +236,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
}
|
||||
|
||||
return {
|
||||
title: t("kefu.title"),
|
||||
title: t("supportChat.title"),
|
||||
subtitle: "",
|
||||
themeColor: "#2563eb",
|
||||
conversation: null,
|
||||
@@ -285,15 +285,15 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
}
|
||||
|
||||
if (widgetConfig.channelId) {
|
||||
setKefuChatRuntimeConfig({
|
||||
...readKefuChatRuntimeConfig(),
|
||||
setSupportChatRuntimeConfig({
|
||||
...readSupportChatRuntimeConfig(),
|
||||
channelId:
|
||||
widgetConfig.channelId || readKefuChatRuntimeConfig().channelId,
|
||||
widgetConfig.channelId || readSupportChatRuntimeConfig().channelId,
|
||||
})
|
||||
}
|
||||
|
||||
set({
|
||||
title: widgetConfig.title || t("kefu.title"),
|
||||
title: widgetConfig.title || t("supportChat.title"),
|
||||
subtitle: widgetConfig.subtitle || "",
|
||||
themeColor: widgetConfig.themeColor || "#2563eb",
|
||||
})
|
||||
@@ -324,7 +324,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
}
|
||||
set({
|
||||
status: "disconnected",
|
||||
error: error instanceof Error ? error.message : t("kefu.initFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.initFailed"),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -355,7 +355,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
})
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : t("kefu.loadMessagesFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.loadMessagesFailed"),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -391,7 +391,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
})
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : t("kefu.syncMessagesFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.syncMessagesFailed"),
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -434,7 +434,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
} catch (error) {
|
||||
set({
|
||||
messagesLoadingMore: false,
|
||||
error: error instanceof Error ? error.message : t("kefu.loadHistoryFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.loadHistoryFailed"),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -496,7 +496,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
conversationId,
|
||||
messageType: "html",
|
||||
content,
|
||||
clientMsgId: `kefu_html_${generateUUID()}`,
|
||||
clientMsgId: `support_chat_html_${generateUUID()}`,
|
||||
})
|
||||
set((state) => ({
|
||||
sending: false,
|
||||
@@ -519,7 +519,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
} catch (error) {
|
||||
set({
|
||||
sending: false,
|
||||
error: error instanceof Error ? error.message : t("kefu.sendMessageFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.sendMessageFailed"),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -540,7 +540,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
return await uploadImImage(conversationId, file)
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : t("kefu.uploadImageFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.uploadImageFailed"),
|
||||
})
|
||||
return null
|
||||
} finally {
|
||||
@@ -562,7 +562,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
messageType: "attachment",
|
||||
content: asset.filename,
|
||||
payload: JSON.stringify({ assetId: asset.assetId }),
|
||||
clientMsgId: `kefu_attachment_${generateUUID()}`,
|
||||
clientMsgId: `support_chat_attachment_${generateUUID()}`,
|
||||
})
|
||||
set((state) => ({
|
||||
uploadingAsset: false,
|
||||
@@ -585,7 +585,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
} catch (error) {
|
||||
set({
|
||||
uploadingAsset: false,
|
||||
error: error instanceof Error ? error.message : t("kefu.sendAttachmentFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.sendAttachmentFailed"),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -614,7 +614,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
} catch (error) {
|
||||
set({
|
||||
closingConversation: false,
|
||||
error: error instanceof Error ? error.message : t("kefu.closeConversationFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.closeConversationFailed"),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -634,7 +634,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
||||
} catch (error) {
|
||||
set({
|
||||
status: "disconnected",
|
||||
error: error instanceof Error ? error.message : t("kefu.refreshFailed"),
|
||||
error: error instanceof Error ? error.message : t("supportChat.refreshFailed"),
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setKefuChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||
import type { KefuChatRuntimeConfig } from "@/lib/sdk/config-types"
|
||||
import { setSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||
import type { SupportChatRuntimeConfig } from "@/lib/sdk/config-types"
|
||||
|
||||
type HostBridgeOptions = {
|
||||
onInit?: () => void
|
||||
@@ -17,7 +17,7 @@ const REQUEST_MINIMIZE_MESSAGE_TYPE = "cs-agent:request-minimize"
|
||||
const REQUEST_CLOSE_MESSAGE_TYPE = "cs-agent:request-close"
|
||||
const REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE = "cs-agent:request-toggle-maximize"
|
||||
|
||||
export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
||||
export function bindSupportHostBridge(options: HostBridgeOptions = {}) {
|
||||
if (typeof window === "undefined") {
|
||||
return () => undefined
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
||||
const data = event.data as
|
||||
| {
|
||||
type?: string
|
||||
payload?: KefuChatRuntimeConfig | { isMaximized?: boolean }
|
||||
payload?: SupportChatRuntimeConfig | { isMaximized?: boolean }
|
||||
}
|
||||
| undefined
|
||||
if (!data?.type) {
|
||||
@@ -38,7 +38,7 @@ export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
||||
}
|
||||
|
||||
if (data.type === INIT_MESSAGE_TYPE && data.payload) {
|
||||
setKefuChatRuntimeConfig(data.payload as KefuChatRuntimeConfig)
|
||||
setSupportChatRuntimeConfig(data.payload as SupportChatRuntimeConfig)
|
||||
options.onInit?.()
|
||||
return
|
||||
}
|
||||
@@ -72,14 +72,14 @@ function postToParent(type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function requestKefuHostMinimize() {
|
||||
export function requestSupportHostMinimize() {
|
||||
postToParent(REQUEST_MINIMIZE_MESSAGE_TYPE)
|
||||
}
|
||||
|
||||
export function requestKefuHostClose() {
|
||||
export function requestSupportHostClose() {
|
||||
postToParent(REQUEST_CLOSE_MESSAGE_TYPE)
|
||||
}
|
||||
|
||||
export function requestKefuHostToggleMaximize() {
|
||||
export function requestSupportHostToggleMaximize() {
|
||||
postToParent(REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE)
|
||||
}
|
||||
Reference in New Issue
Block a user