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:
@@ -188,7 +188,7 @@ flowchart TD
|
|||||||
│ └── ai/ # LLM / RAG / MCP related logic
|
│ └── ai/ # LLM / RAG / MCP related logic
|
||||||
├── web/ # unified Next.js frontend
|
├── web/ # unified Next.js frontend
|
||||||
│ ├── app/dashboard/ # admin dashboard
|
│ ├── app/dashboard/ # admin dashboard
|
||||||
│ ├── app/kefu/ # customer service entry and chat pages
|
│ ├── app/support/ # customer service entry and chat pages
|
||||||
│ ├── components/ # React components
|
│ ├── components/ # React components
|
||||||
│ ├── lib/ # API client, SDK source and utilities
|
│ ├── lib/ # API client, SDK source and utilities
|
||||||
│ ├── public/sdk/ # built embeddable SDK assets
|
│ ├── public/sdk/ # built embeddable SDK assets
|
||||||
@@ -255,8 +255,8 @@ make web-dev
|
|||||||
|
|
||||||
- 管理后台:`http://localhost:3000/dashboard`
|
- 管理后台:`http://localhost:3000/dashboard`
|
||||||
- 客服工作台:`http://localhost:3000/dashboard/conversations`
|
- 客服工作台:`http://localhost:3000/dashboard/conversations`
|
||||||
- 客户侧 Web 接入示例:`http://localhost:3000/kefu`
|
- 客户侧 Web 接入示例:`http://localhost:3000/support/demo`
|
||||||
- 客户侧聊天页:`http://localhost:3000/kefu/chat`
|
- 客户侧聊天页:`http://localhost:3000/support/chat`
|
||||||
|
|
||||||
生产构建时,前端统一由 `web` 工程构建,静态产物输出到 `web/out`,后端会从 `web/out` 提供静态资源。
|
生产构建时,前端统一由 `web` 工程构建,静态产物输出到 `web/out`,后端会从 `web/out` 提供静态资源。
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ docker run --rm -p 8083:8083 \
|
|||||||
|
|
||||||
- 管理后台:负责 AI Agent、知识库、客服组、工单与运营配置
|
- 管理后台:负责 AI Agent、知识库、客服组、工单与运营配置
|
||||||
- 客服工作台:负责接管会话、处理消息与人工服务
|
- 客服工作台:负责接管会话、处理消息与人工服务
|
||||||
- 客户侧 Web 接入:通过 `/kefu`、`/kefu/chat` 和 `web/public/sdk` 中的嵌入式脚本承接用户咨询入口
|
- 客户侧 Web 接入:通过 `/support/demo`、`/support/chat` 和 `web/public/sdk` 中的嵌入式脚本承接用户咨询入口
|
||||||
|
|
||||||
这使得`贝壳AI客服`可以同时覆盖:
|
这使得`贝壳AI客服`可以同时覆盖:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
server:
|
server:
|
||||||
port: 8083
|
port: 8083
|
||||||
cors:
|
cors:
|
||||||
# 浏览器跨域白名单。生产环境必须改为实际前端/嵌入站点域名,例如 https://kefu.example.com。
|
# 浏览器跨域白名单。生产环境必须改为实际前端/嵌入站点域名,例如 https://support.example.com。
|
||||||
# 留空表示不允许跨域请求,只支持同源或非浏览器调用。
|
# 留空表示不允许跨域请求,只支持同源或非浏览器调用。
|
||||||
allowedOrigins:
|
allowedOrigins:
|
||||||
- http://127.0.0.1:8083
|
- http://127.0.0.1:8083
|
||||||
|
|||||||
+1
-1
Submodule docs updated: e0dc72852c...a86a0d2c03
@@ -7,7 +7,7 @@ import { z } from "zod/v4"
|
|||||||
import { CopyIcon, ExternalLinkIcon } from "lucide-react"
|
import { CopyIcon, ExternalLinkIcon } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { getWidgetDemoPath } from "@/components/kefu/demo-navigation"
|
import { getWidgetDemoPath } from "@/components/support-chat/demo-navigation"
|
||||||
import { OptionCombobox } from "@/components/option-combobox"
|
import { OptionCombobox } from "@/components/option-combobox"
|
||||||
import { ProjectDialog } from "@/components/project-dialog"
|
import { ProjectDialog } from "@/components/project-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export default function Page() {
|
|||||||
<CheckCircle2Icon className="size-7" />
|
<CheckCircle2Icon className="size-7" />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<h1 className="text-lg font-semibold text-foreground">{t("kefu.closedTitle")}</h1>
|
<h1 className="text-lg font-semibold text-foreground">{t("supportChat.closedTitle")}</h1>
|
||||||
<p className="text-sm leading-6 text-muted-foreground">
|
<p className="text-sm leading-6 text-muted-foreground">
|
||||||
{t("kefu.closedDescription")}
|
{t("supportChat.closedDescription")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { KefuChatShell } from "@/components/kefu/chat-shell"
|
import { SupportChatShell } from "@/components/support-chat/chat-shell"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <KefuChatShell />
|
return <SupportChatShell />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { KefuWidgetDemo } from "@/components/kefu/widget-demo"
|
import { SupportWidgetDemo } from "@/components/support-chat/widget-demo"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <KefuWidgetDemo />
|
return <SupportWidgetDemo />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export const KEFU_CLOSED_PATH = "/support/chat/closed"
|
|
||||||
|
|
||||||
export function getStandaloneClosedUrl() {
|
|
||||||
return KEFU_CLOSED_PATH
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const KEFU_DEMO_PATH = "/support/demo"
|
|
||||||
|
|
||||||
export function getWidgetDemoPath() {
|
|
||||||
return KEFU_DEMO_PATH
|
|
||||||
}
|
|
||||||
@@ -20,20 +20,20 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import { useShallow } from "zustand/react/shallow"
|
import { useShallow } from "zustand/react/shallow"
|
||||||
|
|
||||||
import { KefuConnectionStatus } from "@/components/kefu/connection-status"
|
import { SupportChatConnectionStatus } from "@/components/support-chat/connection-status"
|
||||||
import { getStandaloneClosedUrl } from "@/components/kefu/close-navigation"
|
import { getStandaloneClosedUrl } from "@/components/support-chat/close-navigation"
|
||||||
import { CustomerMessageEditor } from "@/components/kefu/customer-message-editor"
|
import { CustomerMessageEditor } from "@/components/support-chat/customer-message-editor"
|
||||||
import {
|
import {
|
||||||
KefuMessageList,
|
SupportChatMessageList,
|
||||||
type KefuMessageListHandle,
|
type SupportChatMessageListHandle,
|
||||||
} from "@/components/kefu/message-list"
|
} from "@/components/support-chat/message-list"
|
||||||
import {
|
import {
|
||||||
bindKefuHostBridge,
|
bindSupportHostBridge,
|
||||||
requestKefuHostClose,
|
requestSupportHostClose,
|
||||||
requestKefuHostMinimize,
|
requestSupportHostMinimize,
|
||||||
requestKefuHostToggleMaximize,
|
requestSupportHostToggleMaximize,
|
||||||
} from "@/lib/kefu-host-bridge"
|
} from "@/lib/support-host-bridge"
|
||||||
import { useKefuChatStore } from "@/lib/stores/kefu-chat"
|
import { useSupportChatStore } from "@/lib/stores/support-chat"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -80,7 +80,7 @@ function getMobileStatusDotClass(status: string) {
|
|||||||
return "bg-muted-foreground shadow-[0_0_0_3px_rgba(148,163,184,0.14)]"
|
return "bg-muted-foreground shadow-[0_0_0_3px_rgba(148,163,184,0.14)]"
|
||||||
}
|
}
|
||||||
|
|
||||||
function useKefuSystemTheme() {
|
function useSupportChatSystemTheme() {
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return
|
return
|
||||||
@@ -119,11 +119,11 @@ function isEmbeddedInHost() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KefuChatShell() {
|
export function SupportChatShell() {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
useKefuSystemTheme()
|
useSupportChatSystemTheme()
|
||||||
|
|
||||||
const messageListRef = useRef<KefuMessageListHandle | null>(null)
|
const messageListRef = useRef<SupportChatMessageListHandle | null>(null)
|
||||||
const [isEmbedded, setIsEmbedded] = useState(false)
|
const [isEmbedded, setIsEmbedded] = useState(false)
|
||||||
const [isMaximized, setIsMaximized] = useState(false)
|
const [isMaximized, setIsMaximized] = useState(false)
|
||||||
const [isCloseDialogOpen, setIsCloseDialogOpen] = useState(false)
|
const [isCloseDialogOpen, setIsCloseDialogOpen] = useState(false)
|
||||||
@@ -152,7 +152,7 @@ export function KefuChatShell() {
|
|||||||
disconnectSocket,
|
disconnectSocket,
|
||||||
markConversationRead,
|
markConversationRead,
|
||||||
closeConversation,
|
closeConversation,
|
||||||
} = useKefuChatStore(
|
} = useSupportChatStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
title: state.title,
|
title: state.title,
|
||||||
subtitle: state.subtitle,
|
subtitle: state.subtitle,
|
||||||
@@ -192,12 +192,12 @@ export function KefuChatShell() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
void markConversationRead().catch((readError) => {
|
void markConversationRead().catch((readError) => {
|
||||||
console.error("Failed to mark kefu conversation read", readError)
|
console.error("Failed to mark support chat conversation read", readError)
|
||||||
})
|
})
|
||||||
}, [conversation?.id, isVisible, markConversationRead])
|
}, [conversation?.id, isVisible, markConversationRead])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return bindKefuHostBridge({
|
return bindSupportHostBridge({
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
setIsVisible(true)
|
setIsVisible(true)
|
||||||
@@ -250,11 +250,11 @@ export function KefuChatShell() {
|
|||||||
|
|
||||||
function handleMinimize() {
|
function handleMinimize() {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
requestKefuHostMinimize()
|
requestSupportHostMinimize()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleMaximize() {
|
function handleToggleMaximize() {
|
||||||
requestKefuHostToggleMaximize()
|
requestSupportHostToggleMaximize()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmCloseConversation() {
|
async function confirmCloseConversation() {
|
||||||
@@ -268,12 +268,12 @@ export function KefuChatShell() {
|
|||||||
}
|
}
|
||||||
setIsCloseDialogOpen(false)
|
setIsCloseDialogOpen(false)
|
||||||
if (isEmbedded) {
|
if (isEmbedded) {
|
||||||
requestKefuHostClose()
|
requestSupportHostClose()
|
||||||
} else {
|
} else {
|
||||||
window.location.replace(getStandaloneClosedUrl())
|
window.location.replace(getStandaloneClosedUrl())
|
||||||
}
|
}
|
||||||
} catch (closeError) {
|
} catch (closeError) {
|
||||||
window.alert(closeError instanceof Error ? closeError.message : t("kefu.closeConversationFailed"))
|
window.alert(closeError instanceof Error ? closeError.message : t("supportChat.closeConversationFailed"))
|
||||||
} finally {
|
} finally {
|
||||||
setIsClosingConversation(false)
|
setIsClosingConversation(false)
|
||||||
}
|
}
|
||||||
@@ -326,8 +326,8 @@ export function KefuChatShell() {
|
|||||||
{!isEmbedded && status !== "connected" ? (
|
{!isEmbedded && status !== "connected" ? (
|
||||||
<WindowActionButton
|
<WindowActionButton
|
||||||
onClick={retry}
|
onClick={retry}
|
||||||
aria-label={t("kefu.retry")}
|
aria-label={t("supportChat.retry")}
|
||||||
title={t("kefu.retry")}
|
title={t("supportChat.retry")}
|
||||||
>
|
>
|
||||||
<RotateCwIcon className="size-4" />
|
<RotateCwIcon className="size-4" />
|
||||||
</WindowActionButton>
|
</WindowActionButton>
|
||||||
@@ -335,18 +335,18 @@ export function KefuChatShell() {
|
|||||||
{isEmbedded ? (
|
{isEmbedded ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
render={<WindowActionButton aria-label={t("kefu.moreActions")} title={t("kefu.moreActions")} />}
|
render={<WindowActionButton aria-label={t("supportChat.moreActions")} title={t("supportChat.moreActions")} />}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<MoreHorizontalIcon className="size-4" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-36">
|
<DropdownMenuContent align="end" className="w-36">
|
||||||
<DropdownMenuItem onClick={retry}>
|
<DropdownMenuItem onClick={retry}>
|
||||||
<RotateCwIcon className="size-4" />
|
<RotateCwIcon className="size-4" />
|
||||||
{t("kefu.retry")}
|
{t("supportChat.retry")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleMinimize}>
|
<DropdownMenuItem onClick={handleMinimize}>
|
||||||
<MinusIcon className="size-4" />
|
<MinusIcon className="size-4" />
|
||||||
{t("kefu.minimize")}
|
{t("supportChat.minimize")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleToggleMaximize}>
|
<DropdownMenuItem onClick={handleToggleMaximize}>
|
||||||
{isMaximized ? (
|
{isMaximized ? (
|
||||||
@@ -354,22 +354,22 @@ export function KefuChatShell() {
|
|||||||
) : (
|
) : (
|
||||||
<Maximize2Icon className="size-4" />
|
<Maximize2Icon className="size-4" />
|
||||||
)}
|
)}
|
||||||
{isMaximized ? t("kefu.restoreWindow") : t("kefu.maximize")}
|
{isMaximized ? t("supportChat.restoreWindow") : t("supportChat.maximize")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => setIsCloseDialogOpen(true)}
|
onClick={() => setIsCloseDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
{t("kefu.closeWindow")}
|
{t("supportChat.closeWindow")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<WindowActionButton
|
<WindowActionButton
|
||||||
onClick={() => setIsCloseDialogOpen(true)}
|
onClick={() => setIsCloseDialogOpen(true)}
|
||||||
aria-label={t("kefu.closeChatWindow")}
|
aria-label={t("supportChat.closeChatWindow")}
|
||||||
title={t("kefu.closeChatWindow")}
|
title={t("supportChat.closeChatWindow")}
|
||||||
className="hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
className="hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
||||||
>
|
>
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
@@ -378,13 +378,13 @@ export function KefuChatShell() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="hidden shrink-0 items-center gap-1 sm:flex sm:gap-2">
|
<div className="hidden shrink-0 items-center gap-1 sm:flex sm:gap-2">
|
||||||
{status !== "connected" ? (
|
{status !== "connected" ? (
|
||||||
<KefuConnectionStatus status={status} />
|
<SupportChatConnectionStatus status={status} />
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex items-center gap-0.5 rounded-lg bg-background/55 p-0.5 shadow-sm ring-1 ring-border/70 dark:bg-background/25 dark:ring-white/10">
|
<div className="flex items-center gap-0.5 rounded-lg bg-background/55 p-0.5 shadow-sm ring-1 ring-border/70 dark:bg-background/25 dark:ring-white/10">
|
||||||
<WindowActionButton
|
<WindowActionButton
|
||||||
onClick={retry}
|
onClick={retry}
|
||||||
aria-label={t("kefu.retry")}
|
aria-label={t("supportChat.retry")}
|
||||||
title={t("kefu.retry")}
|
title={t("supportChat.retry")}
|
||||||
>
|
>
|
||||||
<RotateCwIcon className="size-4" />
|
<RotateCwIcon className="size-4" />
|
||||||
</WindowActionButton>
|
</WindowActionButton>
|
||||||
@@ -392,15 +392,15 @@ export function KefuChatShell() {
|
|||||||
<>
|
<>
|
||||||
<WindowActionButton
|
<WindowActionButton
|
||||||
onClick={handleMinimize}
|
onClick={handleMinimize}
|
||||||
aria-label={t("kefu.minimize")}
|
aria-label={t("supportChat.minimize")}
|
||||||
title={t("kefu.minimize")}
|
title={t("supportChat.minimize")}
|
||||||
>
|
>
|
||||||
<MinusIcon className="size-4" />
|
<MinusIcon className="size-4" />
|
||||||
</WindowActionButton>
|
</WindowActionButton>
|
||||||
<WindowActionButton
|
<WindowActionButton
|
||||||
onClick={handleToggleMaximize}
|
onClick={handleToggleMaximize}
|
||||||
aria-label={isMaximized ? t("kefu.restoreWindow") : t("kefu.maximizeWindow")}
|
aria-label={isMaximized ? t("supportChat.restoreWindow") : t("supportChat.maximizeWindow")}
|
||||||
title={isMaximized ? t("kefu.restoreWindow") : t("kefu.maximizeWindow")}
|
title={isMaximized ? t("supportChat.restoreWindow") : t("supportChat.maximizeWindow")}
|
||||||
>
|
>
|
||||||
{isMaximized ? (
|
{isMaximized ? (
|
||||||
<Minimize2Icon className="size-4" />
|
<Minimize2Icon className="size-4" />
|
||||||
@@ -412,8 +412,8 @@ export function KefuChatShell() {
|
|||||||
) : null}
|
) : null}
|
||||||
<WindowActionButton
|
<WindowActionButton
|
||||||
onClick={() => setIsCloseDialogOpen(true)}
|
onClick={() => setIsCloseDialogOpen(true)}
|
||||||
aria-label={t("kefu.closeChatWindow")}
|
aria-label={t("supportChat.closeChatWindow")}
|
||||||
title={t("kefu.closeChatWindow")}
|
title={t("supportChat.closeChatWindow")}
|
||||||
className="hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
className="hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
||||||
>
|
>
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
@@ -424,7 +424,7 @@ export function KefuChatShell() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] overflow-hidden bg-muted/60 dark:bg-muted/30">
|
<div className="grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] overflow-hidden bg-muted/60 dark:bg-muted/30">
|
||||||
<KefuMessageList
|
<SupportChatMessageList
|
||||||
ref={messageListRef}
|
ref={messageListRef}
|
||||||
messages={safeMessages}
|
messages={safeMessages}
|
||||||
onNearBottomVisible={maybeMarkConversationRead}
|
onNearBottomVisible={maybeMarkConversationRead}
|
||||||
@@ -459,9 +459,9 @@ export function KefuChatShell() {
|
|||||||
>
|
>
|
||||||
<DialogContent className="max-w-[320px]" showCloseButton={!isClosingConversation}>
|
<DialogContent className="max-w-[320px]" showCloseButton={!isClosingConversation}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("kefu.closeDialogTitle")}</DialogTitle>
|
<DialogTitle>{t("supportChat.closeDialogTitle")}</DialogTitle>
|
||||||
<DialogDescription className="text-xs leading-5">
|
<DialogDescription className="text-xs leading-5">
|
||||||
{t("kefu.closeDialogDescription")}
|
{t("supportChat.closeDialogDescription")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -471,7 +471,7 @@ export function KefuChatShell() {
|
|||||||
disabled={isClosingConversation}
|
disabled={isClosingConversation}
|
||||||
onClick={() => setIsCloseDialogOpen(false)}
|
onClick={() => setIsCloseDialogOpen(false)}
|
||||||
>
|
>
|
||||||
{t("kefu.continueConversation")}
|
{t("supportChat.continueConversation")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -479,7 +479,7 @@ export function KefuChatShell() {
|
|||||||
disabled={isClosingConversation}
|
disabled={isClosingConversation}
|
||||||
onClick={() => void confirmCloseConversation()}
|
onClick={() => void confirmCloseConversation()}
|
||||||
>
|
>
|
||||||
{isClosingConversation ? t("kefu.closing") : t("kefu.confirmClose")}
|
{isClosingConversation ? t("supportChat.closing") : t("supportChat.confirmClose")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const SUPPORT_CHAT_CLOSED_PATH = "/support/chat/closed"
|
||||||
|
|
||||||
|
export function getStandaloneClosedUrl() {
|
||||||
|
return SUPPORT_CHAT_CLOSED_PATH
|
||||||
|
}
|
||||||
+3
-3
@@ -4,11 +4,11 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useI18n } from "@/i18n/provider"
|
import { useI18n } from "@/i18n/provider"
|
||||||
|
|
||||||
type KefuConnectionStatusProps = {
|
type SupportChatConnectionStatusProps = {
|
||||||
status: "connecting" | "connected" | "disconnected"
|
status: "connecting" | "connected" | "disconnected"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KefuConnectionStatus({ status }: KefuConnectionStatusProps) {
|
export function SupportChatConnectionStatus({ status }: SupportChatConnectionStatusProps) {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toneClass =
|
const toneClass =
|
||||||
status === "connected"
|
status === "connected"
|
||||||
@@ -32,7 +32,7 @@ export function KefuConnectionStatus({ status }: KefuConnectionStatusProps) {
|
|||||||
: "bg-muted-foreground shadow-[0_0_0_4px_rgba(148,163,184,0.14)]"
|
: "bg-muted-foreground shadow-[0_0_0_4px_rgba(148,163,184,0.14)]"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{t(`kefu.${status}`)}</span>
|
<span>{t(`supportChat.${status}`)}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const SUPPORT_DEMO_PATH = "/support/demo"
|
||||||
|
|
||||||
|
export function getWidgetDemoPath() {
|
||||||
|
return SUPPORT_DEMO_PATH
|
||||||
|
}
|
||||||
+11
-11
@@ -20,7 +20,7 @@ import { renderIMMessageHTML } from "@/lib/im-message"
|
|||||||
import { cn, formatDateTime } from "@/lib/utils"
|
import { cn, formatDateTime } from "@/lib/utils"
|
||||||
import { useI18n } from "@/i18n/provider"
|
import { useI18n } from "@/i18n/provider"
|
||||||
|
|
||||||
type KefuMessageListProps = {
|
type SupportChatMessageListProps = {
|
||||||
messages?: ImMessage[] | null
|
messages?: ImMessage[] | null
|
||||||
onNearBottomVisible?: () => void
|
onNearBottomVisible?: () => void
|
||||||
hasMoreOlder?: boolean
|
hasMoreOlder?: boolean
|
||||||
@@ -28,7 +28,7 @@ type KefuMessageListProps = {
|
|||||||
onLoadOlder?: () => Promise<void>
|
onLoadOlder?: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KefuMessageListHandle = {
|
export type SupportChatMessageListHandle = {
|
||||||
scrollToBottom: () => void
|
scrollToBottom: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ function getTimelineLabel(
|
|||||||
t: (key: string, values?: Record<string, string | number>) => string
|
t: (key: string, values?: Record<string, string | number>) => string
|
||||||
) {
|
) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return t("kefu.justNow")
|
return t("supportChat.justNow")
|
||||||
}
|
}
|
||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
@@ -62,13 +62,13 @@ function getTimelineLabel(
|
|||||||
date.getMinutes()
|
date.getMinutes()
|
||||||
).padStart(2, "0")}`
|
).padStart(2, "0")}`
|
||||||
if (currentDayKey === todayDayKey) {
|
if (currentDayKey === todayDayKey) {
|
||||||
return t("kefu.todayAt", { time: timeText })
|
return t("supportChat.todayAt", { time: timeText })
|
||||||
}
|
}
|
||||||
return `${currentDayKey} ${timeText}`
|
return `${currentDayKey} ${timeText}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KefuMessageList = forwardRef<KefuMessageListHandle, KefuMessageListProps>(
|
export const SupportChatMessageList = forwardRef<SupportChatMessageListHandle, SupportChatMessageListProps>(
|
||||||
function KefuMessageList(
|
function SupportChatMessageList(
|
||||||
{
|
{
|
||||||
messages,
|
messages,
|
||||||
onNearBottomVisible,
|
onNearBottomVisible,
|
||||||
@@ -232,14 +232,14 @@ export const KefuMessageList = forwardRef<KefuMessageListHandle, KefuMessageList
|
|||||||
onClick={() => void handleLoadOlder()}
|
onClick={() => void handleLoadOlder()}
|
||||||
className="h-7 rounded-full bg-background/90 text-xs text-muted-foreground shadow-sm hover:bg-background hover:text-sky-700 dark:hover:text-sky-400"
|
className="h-7 rounded-full bg-background/90 text-xs text-muted-foreground shadow-sm hover:bg-background hover:text-sky-700 dark:hover:text-sky-400"
|
||||||
>
|
>
|
||||||
{loadingOlder ? t("kefu.loadingOlder") : t("kefu.loadOlder")}
|
{loadingOlder ? t("supportChat.loadingOlder") : t("supportChat.loadOlder")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{safeMessages.length === 0 ? (
|
{safeMessages.length === 0 ? (
|
||||||
<div className="flex min-h-32 items-center justify-center px-3 py-6 text-center text-sm leading-6 text-muted-foreground">
|
<div className="flex min-h-32 items-center justify-center px-3 py-6 text-center text-sm leading-6 text-muted-foreground">
|
||||||
{t("kefu.emptyPrompt")}
|
{t("supportChat.emptyPrompt")}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ const MessageItem = memo(
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const { open } = useImageLightbox()
|
const { open } = useImageLightbox()
|
||||||
const isCustomer = message.senderType === "customer"
|
const isCustomer = message.senderType === "customer"
|
||||||
const senderName = isCustomer ? t("kefu.customerSelf") : message.senderName?.trim() || t("kefu.agentLabel")
|
const senderName = isCustomer ? t("supportChat.customerSelf") : message.senderName?.trim() || t("supportChat.agentLabel")
|
||||||
const avatarSrc =
|
const avatarSrc =
|
||||||
!isCustomer && message.senderAvatar?.trim() ? message.senderAvatar.trim() : undefined
|
!isCustomer && message.senderAvatar?.trim() ? message.senderAvatar.trim() : undefined
|
||||||
const htmlContent = renderIMMessageHTML(message)
|
const htmlContent = renderIMMessageHTML(message)
|
||||||
@@ -301,7 +301,7 @@ const MessageItem = memo(
|
|||||||
<Avatar className="mt-5">
|
<Avatar className="mt-5">
|
||||||
{avatarSrc ? <AvatarImage src={avatarSrc} alt="" /> : null}
|
{avatarSrc ? <AvatarImage src={avatarSrc} alt="" /> : null}
|
||||||
<AvatarFallback className="bg-muted text-muted-foreground">
|
<AvatarFallback className="bg-muted text-muted-foreground">
|
||||||
{fallbackName || t("kefu.customerFallback")}
|
{fallbackName || t("supportChat.customerFallback")}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -316,7 +316,7 @@ const MessageItem = memo(
|
|||||||
<span className="font-medium">{senderName}</span>
|
<span className="font-medium">{senderName}</span>
|
||||||
<span>{formatDateTime(message.sentAt)}</span>
|
<span>{formatDateTime(message.sentAt)}</span>
|
||||||
{isCustomer ? (
|
{isCustomer ? (
|
||||||
<span>{message.agentRead ? t("kefu.agentRead") : t("kefu.agentUnread")}</span>
|
<span>{message.agentRead ? t("supportChat.agentRead") : t("supportChat.agentUnread")}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -115,7 +115,7 @@ async function signUserToken(config: WidgetDemoConfig, t: (key: string) => strin
|
|||||||
.sign(new TextEncoder().encode(secret))
|
.sign(new TextEncoder().encode(secret))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KefuWidgetDemo() {
|
export function SupportWidgetDemo() {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const [config, setConfig] = useState<WidgetDemoConfig>({
|
const [config, setConfig] = useState<WidgetDemoConfig>({
|
||||||
...INITIAL_CONFIG,
|
...INITIAL_CONFIG,
|
||||||
@@ -193,7 +193,7 @@ export function KefuWidgetDemo() {
|
|||||||
const configLines = [` channelId: "${config.channelId || ""}"`]
|
const configLines = [` channelId: "${config.channelId || ""}"`]
|
||||||
if (config.authMode === "jwt") {
|
if (config.authMode === "jwt") {
|
||||||
configLines.push(` async getUserToken() {
|
configLines.push(` async getUserToken() {
|
||||||
const res = await fetch("/api/kefu/user-token", { credentials: "include" });
|
const res = await fetch("/api/support/user-token", { credentials: "include" });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.userToken;
|
return data.userToken;
|
||||||
}`)
|
}`)
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { request } from "@/lib/api/client"
|
import { request } from "@/lib/api/client"
|
||||||
import { translateCurrentMessage } from "@/i18n/messages"
|
import { translateCurrentMessage } from "@/i18n/messages"
|
||||||
import { readKefuChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
import { readSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||||
import { generateUUID } from "@/lib/utils"
|
import { generateUUID } from "@/lib/utils"
|
||||||
|
|
||||||
export type Paging = {
|
export type Paging = {
|
||||||
@@ -159,7 +159,7 @@ export function getGuestId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRuntimeImConfig() {
|
function getRuntimeImConfig() {
|
||||||
const widgetConfig = readKefuChatRuntimeConfig()
|
const widgetConfig = readSupportChatRuntimeConfig()
|
||||||
const baseUrl = (widgetConfig.apiBaseUrl || widgetConfig.baseUrl || API_BASE_URL)
|
const baseUrl = (widgetConfig.apiBaseUrl || widgetConfig.baseUrl || API_BASE_URL)
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\/$/, "")
|
.replace(/\/$/, "")
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ export function renderIMMessageHTML(message: {
|
|||||||
asset.filename || "image"
|
asset.filename || "image"
|
||||||
)}"></p>`
|
)}"></p>`
|
||||||
}
|
}
|
||||||
return `<p>${escapeHTML(t("kefu.imageSummary"))}</p>`
|
return `<p>${escapeHTML(t("supportChat.imageSummary"))}</p>`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.messageType === "attachment") {
|
if (message.messageType === "attachment") {
|
||||||
if (asset?.url) {
|
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 meta = formatFileSize(asset.fileSize ?? 0)
|
||||||
const metaHTML = meta ? `<div class="im-attachment-meta">${escapeHTML(meta)}</div>` : ""
|
const metaHTML = meta ? `<div class="im-attachment-meta">${escapeHTML(meta)}</div>` : ""
|
||||||
return `<div class="im-attachment"><a href="${escapeHTMLAttr(
|
return `<div class="im-attachment"><a href="${escapeHTMLAttr(
|
||||||
@@ -65,7 +65,7 @@ export function renderIMMessageHTML(message: {
|
|||||||
asset.filename || ""
|
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>`
|
)}" 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 || "")
|
return renderTextMessageHTML(message.content || "")
|
||||||
@@ -77,13 +77,13 @@ export function summarizeIMMessage(message: {
|
|||||||
payload?: string
|
payload?: string
|
||||||
}) {
|
}) {
|
||||||
if (message.messageType === "image") {
|
if (message.messageType === "image") {
|
||||||
return t("kefu.imageSummary")
|
return t("supportChat.imageSummary")
|
||||||
}
|
}
|
||||||
if (message.messageType === "attachment") {
|
if (message.messageType === "attachment") {
|
||||||
const asset = parseMessageAssetPayload(message.payload)
|
const asset = parseMessageAssetPayload(message.payload)
|
||||||
return asset?.filename?.trim()
|
return asset?.filename?.trim()
|
||||||
? `${t("kefu.attachmentSummary")} ${asset.filename.trim()}`
|
? `${t("supportChat.attachmentSummary")} ${asset.filename.trim()}`
|
||||||
: t("kefu.attachmentSummary")
|
: t("supportChat.attachmentSummary")
|
||||||
}
|
}
|
||||||
if (message.messageType === "html") {
|
if (message.messageType === "html") {
|
||||||
const text = extractTextFromHTML(message.content)
|
const text = extractTextFromHTML(message.content)
|
||||||
@@ -91,11 +91,11 @@ export function summarizeIMMessage(message: {
|
|||||||
return text.substring(0, 100)
|
return text.substring(0, 100)
|
||||||
}
|
}
|
||||||
if (message.content.includes("<img")) {
|
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) {
|
export function formatFileSize(size: number) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createWebSocketBaseUrl } from "@/lib/api/websocket"
|
import { createWebSocketBaseUrl } from "@/lib/api/websocket"
|
||||||
import { getCustomerSessionToken, type ImMessage } from "@/lib/api/im"
|
import { getCustomerSessionToken, type ImMessage } from "@/lib/api/im"
|
||||||
import { readKefuChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
import { readSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||||
import type {
|
import type {
|
||||||
RealtimeConversationPatch,
|
RealtimeConversationPatch,
|
||||||
RealtimeMessageCreatedPayload,
|
RealtimeMessageCreatedPayload,
|
||||||
@@ -22,7 +22,7 @@ export type ImRealtimeEnvelope = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createImRealtimeConnection() {
|
export function createImRealtimeConnection() {
|
||||||
const config = readKefuChatRuntimeConfig()
|
const config = readSupportChatRuntimeConfig()
|
||||||
const apiBaseUrl = (config.apiBaseUrl || "").trim()
|
const apiBaseUrl = (config.apiBaseUrl || "").trim()
|
||||||
const baseUrl = apiBaseUrl
|
const baseUrl = apiBaseUrl
|
||||||
? apiBaseUrl.replace(/^http/, "ws").replace(/\/$/, "")
|
? apiBaseUrl.replace(/^http/, "ws").replace(/\/$/, "")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export type CSAgentConfig = {
|
|||||||
width?: string
|
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. */
|
/** Used only by /support/chat to exchange for a chat token; not part of CSAgentConfig. */
|
||||||
userToken?: string
|
userToken?: string
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
CSAgentConfig?: CSAgentConfig
|
CSAgentConfig?: CSAgentConfig
|
||||||
CSAgentWidget?: CSAgentWidget
|
CSAgentWidget?: CSAgentWidget
|
||||||
__CS_AGENT_WIDGET_CONFIG__?: KefuChatRuntimeConfig
|
__CS_AGENT_WIDGET_CONFIG__?: SupportChatRuntimeConfig
|
||||||
__CS_AGENT_WIDGET_STATE__?: unknown
|
__CS_AGENT_WIDGET_STATE__?: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
CSAgentConfig,
|
CSAgentConfig,
|
||||||
CSAgentWidget,
|
CSAgentWidget,
|
||||||
KefuChatRuntimeConfig,
|
SupportChatRuntimeConfig,
|
||||||
} from "./config-types"
|
} from "./config-types"
|
||||||
|
|
||||||
type NormalizedCSAgentConfig = CSAgentConfig & {
|
type NormalizedCSAgentConfig = CSAgentConfig & {
|
||||||
@@ -24,7 +24,7 @@ type WidgetState = {
|
|||||||
frameHideTimer: number | null
|
frameHideTimer: number | null
|
||||||
frameDestroyTimer: number | null
|
frameDestroyTimer: number | null
|
||||||
config: NormalizedCSAgentConfig | null
|
config: NormalizedCSAgentConfig | null
|
||||||
frameConfig: KefuChatRuntimeConfig | null
|
frameConfig: SupportChatRuntimeConfig | null
|
||||||
frameUrl: URL | null
|
frameUrl: URL | null
|
||||||
animationDuration: number
|
animationDuration: number
|
||||||
listenerBound?: boolean
|
listenerBound?: boolean
|
||||||
@@ -57,7 +57,7 @@ function getLauncherText() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FrameMessage =
|
type FrameMessage =
|
||||||
| { type: "cs-agent:init"; payload: KefuChatRuntimeConfig }
|
| { type: "cs-agent:init"; payload: SupportChatRuntimeConfig }
|
||||||
| { type: "cs-agent:open" }
|
| { type: "cs-agent:open" }
|
||||||
| { type: "cs-agent:minimize" }
|
| { type: "cs-agent:minimize" }
|
||||||
| { type: "cs-agent:maximized"; payload: { isMaximized: boolean } }
|
| { type: "cs-agent:maximized"; payload: { isMaximized: boolean } }
|
||||||
@@ -135,7 +135,7 @@ type FrameMessage =
|
|||||||
function createFrameConfig(
|
function createFrameConfig(
|
||||||
config: NormalizedCSAgentConfig,
|
config: NormalizedCSAgentConfig,
|
||||||
userToken: string
|
userToken: string
|
||||||
): KefuChatRuntimeConfig {
|
): SupportChatRuntimeConfig {
|
||||||
const { getUserToken: _getUserToken, ...payload } = config
|
const { getUserToken: _getUserToken, ...payload } = config
|
||||||
if (userToken) {
|
if (userToken) {
|
||||||
return { ...payload, 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") {
|
if (typeof window === "undefined") {
|
||||||
return {
|
return {
|
||||||
channelId: "",
|
channelId: "",
|
||||||
@@ -10,7 +10,7 @@ export function readKefuChatRuntimeConfig(): KefuChatRuntimeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
const fallback: KefuChatRuntimeConfig = {
|
const fallback: SupportChatRuntimeConfig = {
|
||||||
channelId:
|
channelId:
|
||||||
query.get("channelId") ??
|
query.get("channelId") ??
|
||||||
process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() ??
|
process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() ??
|
||||||
@@ -46,7 +46,7 @@ export function readKefuChatRuntimeConfig(): KefuChatRuntimeConfig {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setKefuChatRuntimeConfig(config: KefuChatRuntimeConfig) {
|
export function setSupportChatRuntimeConfig(config: SupportChatRuntimeConfig) {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ import { summarizeIMMessage } from "@/lib/im-message"
|
|||||||
import { createRealtimeConnectionManager } from "@/lib/realtime-connection"
|
import { createRealtimeConnectionManager } from "@/lib/realtime-connection"
|
||||||
import { generateUUID } from "@/lib/utils"
|
import { generateUUID } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
readKefuChatRuntimeConfig,
|
readSupportChatRuntimeConfig,
|
||||||
setKefuChatRuntimeConfig,
|
setSupportChatRuntimeConfig,
|
||||||
} from "@/lib/sdk/runtime-config"
|
} from "@/lib/sdk/runtime-config"
|
||||||
import { translateCurrentMessage } from "@/i18n/messages"
|
import { translateCurrentMessage } from "@/i18n/messages"
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ function markConversationReadMessages(
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KefuChatStore = {
|
export type SupportChatStore = {
|
||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
themeColor: string
|
themeColor: string
|
||||||
@@ -149,7 +149,7 @@ function t(key: string) {
|
|||||||
return translateCurrentMessage(key)
|
return translateCurrentMessage(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
export const useSupportChatStore = create<SupportChatStore>((set, get) => {
|
||||||
const realtime = createRealtimeConnectionManager({
|
const realtime = createRealtimeConnectionManager({
|
||||||
createSocket: createImRealtimeConnection,
|
createSocket: createImRealtimeConnection,
|
||||||
canReconnect: () => Boolean(get().isOpen && get().conversation?.id),
|
canReconnect: () => Boolean(get().isOpen && get().conversation?.id),
|
||||||
@@ -204,7 +204,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
document.visibilityState !== "visible"
|
document.visibilityState !== "visible"
|
||||||
) {
|
) {
|
||||||
const state = get()
|
const state = get()
|
||||||
showNotification(t("kefu.newMessage"), getNotificationBody(message), () => {
|
showNotification(t("supportChat.newMessage"), getNotificationBody(message), () => {
|
||||||
state.setIsOpen(true)
|
state.setIsOpen(true)
|
||||||
state.setIsVisible(true)
|
state.setIsVisible(true)
|
||||||
})
|
})
|
||||||
@@ -236,7 +236,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t("kefu.title"),
|
title: t("supportChat.title"),
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
themeColor: "#2563eb",
|
themeColor: "#2563eb",
|
||||||
conversation: null,
|
conversation: null,
|
||||||
@@ -285,15 +285,15 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (widgetConfig.channelId) {
|
if (widgetConfig.channelId) {
|
||||||
setKefuChatRuntimeConfig({
|
setSupportChatRuntimeConfig({
|
||||||
...readKefuChatRuntimeConfig(),
|
...readSupportChatRuntimeConfig(),
|
||||||
channelId:
|
channelId:
|
||||||
widgetConfig.channelId || readKefuChatRuntimeConfig().channelId,
|
widgetConfig.channelId || readSupportChatRuntimeConfig().channelId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
set({
|
set({
|
||||||
title: widgetConfig.title || t("kefu.title"),
|
title: widgetConfig.title || t("supportChat.title"),
|
||||||
subtitle: widgetConfig.subtitle || "",
|
subtitle: widgetConfig.subtitle || "",
|
||||||
themeColor: widgetConfig.themeColor || "#2563eb",
|
themeColor: widgetConfig.themeColor || "#2563eb",
|
||||||
})
|
})
|
||||||
@@ -324,7 +324,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
}
|
}
|
||||||
set({
|
set({
|
||||||
status: "disconnected",
|
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) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
error: error instanceof Error ? error.message : t("kefu.loadMessagesFailed"),
|
error: error instanceof Error ? error.message : t("supportChat.loadMessagesFailed"),
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -391,7 +391,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
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) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
messagesLoadingMore: false,
|
messagesLoadingMore: false,
|
||||||
error: error instanceof Error ? error.message : t("kefu.loadHistoryFailed"),
|
error: error instanceof Error ? error.message : t("supportChat.loadHistoryFailed"),
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -496,7 +496,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
conversationId,
|
conversationId,
|
||||||
messageType: "html",
|
messageType: "html",
|
||||||
content,
|
content,
|
||||||
clientMsgId: `kefu_html_${generateUUID()}`,
|
clientMsgId: `support_chat_html_${generateUUID()}`,
|
||||||
})
|
})
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
sending: false,
|
sending: false,
|
||||||
@@ -519,7 +519,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
sending: false,
|
sending: false,
|
||||||
error: error instanceof Error ? error.message : t("kefu.sendMessageFailed"),
|
error: error instanceof Error ? error.message : t("supportChat.sendMessageFailed"),
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -540,7 +540,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
return await uploadImImage(conversationId, file)
|
return await uploadImImage(conversationId, file)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
error: error instanceof Error ? error.message : t("kefu.uploadImageFailed"),
|
error: error instanceof Error ? error.message : t("supportChat.uploadImageFailed"),
|
||||||
})
|
})
|
||||||
return null
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
@@ -562,7 +562,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
messageType: "attachment",
|
messageType: "attachment",
|
||||||
content: asset.filename,
|
content: asset.filename,
|
||||||
payload: JSON.stringify({ assetId: asset.assetId }),
|
payload: JSON.stringify({ assetId: asset.assetId }),
|
||||||
clientMsgId: `kefu_attachment_${generateUUID()}`,
|
clientMsgId: `support_chat_attachment_${generateUUID()}`,
|
||||||
})
|
})
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
uploadingAsset: false,
|
uploadingAsset: false,
|
||||||
@@ -585,7 +585,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
uploadingAsset: false,
|
uploadingAsset: false,
|
||||||
error: error instanceof Error ? error.message : t("kefu.sendAttachmentFailed"),
|
error: error instanceof Error ? error.message : t("supportChat.sendAttachmentFailed"),
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -614,7 +614,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
closingConversation: false,
|
closingConversation: false,
|
||||||
error: error instanceof Error ? error.message : t("kefu.closeConversationFailed"),
|
error: error instanceof Error ? error.message : t("supportChat.closeConversationFailed"),
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -634,7 +634,7 @@ export const useKefuChatStore = create<KefuChatStore>((set, get) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
status: "disconnected",
|
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 { setSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config"
|
||||||
import type { KefuChatRuntimeConfig } from "@/lib/sdk/config-types"
|
import type { SupportChatRuntimeConfig } from "@/lib/sdk/config-types"
|
||||||
|
|
||||||
type HostBridgeOptions = {
|
type HostBridgeOptions = {
|
||||||
onInit?: () => void
|
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_CLOSE_MESSAGE_TYPE = "cs-agent:request-close"
|
||||||
const REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE = "cs-agent:request-toggle-maximize"
|
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") {
|
if (typeof window === "undefined") {
|
||||||
return () => undefined
|
return () => undefined
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
|||||||
const data = event.data as
|
const data = event.data as
|
||||||
| {
|
| {
|
||||||
type?: string
|
type?: string
|
||||||
payload?: KefuChatRuntimeConfig | { isMaximized?: boolean }
|
payload?: SupportChatRuntimeConfig | { isMaximized?: boolean }
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
if (!data?.type) {
|
if (!data?.type) {
|
||||||
@@ -38,7 +38,7 @@ export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === INIT_MESSAGE_TYPE && data.payload) {
|
if (data.type === INIT_MESSAGE_TYPE && data.payload) {
|
||||||
setKefuChatRuntimeConfig(data.payload as KefuChatRuntimeConfig)
|
setSupportChatRuntimeConfig(data.payload as SupportChatRuntimeConfig)
|
||||||
options.onInit?.()
|
options.onInit?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,14 +72,14 @@ function postToParent(type: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestKefuHostMinimize() {
|
export function requestSupportHostMinimize() {
|
||||||
postToParent(REQUEST_MINIMIZE_MESSAGE_TYPE)
|
postToParent(REQUEST_MINIMIZE_MESSAGE_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestKefuHostClose() {
|
export function requestSupportHostClose() {
|
||||||
postToParent(REQUEST_CLOSE_MESSAGE_TYPE)
|
postToParent(REQUEST_CLOSE_MESSAGE_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestKefuHostToggleMaximize() {
|
export function requestSupportHostToggleMaximize() {
|
||||||
postToParent(REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE)
|
postToParent(REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE)
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
"claiming": "Claiming...",
|
"claiming": "Claiming...",
|
||||||
"confirmClaim": "Claim"
|
"confirmClaim": "Claim"
|
||||||
},
|
},
|
||||||
"kefu": {
|
"supportChat": {
|
||||||
"title": "Support",
|
"title": "Support",
|
||||||
"agentLabel": "Support",
|
"agentLabel": "Support",
|
||||||
"customerSelf": "You",
|
"customerSelf": "You",
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
"claiming": "认领中...",
|
"claiming": "认领中...",
|
||||||
"confirmClaim": "确认认领"
|
"confirmClaim": "确认认领"
|
||||||
},
|
},
|
||||||
"kefu": {
|
"supportChat": {
|
||||||
"title": "在线客服",
|
"title": "在线客服",
|
||||||
"agentLabel": "客服",
|
"agentLabel": "客服",
|
||||||
"customerSelf": "我",
|
"customerSelf": "我",
|
||||||
|
|||||||
Reference in New Issue
Block a user