Files
ai-agent/web/lib/im-realtime-state.ts
T

173 lines
4.8 KiB
TypeScript

import type { AgentConversation, AgentMessage } from "@/lib/api/agent"
import type { ImConversation, ImMessage } from "@/lib/api/im"
import { mergeImMessagesByIdAsc } from "@/lib/im-message-merge"
import { summarizeIMMessage } from "@/lib/im-message"
export type RealtimeMessage = AgentMessage | ImMessage
export type RealtimeConversation = AgentConversation | ImConversation
export type RealtimeMessageCreatedPayload<TMessage extends RealtimeMessage> = {
conversationId?: number
messageId?: number
message?: TMessage
senderType?: string
senderId?: number
senderName?: string
senderAvatar?: string
messageType?: string
content?: string
payload?: string
sendStatus?: number
sentAt?: string
}
export type RealtimeConversationPatch = Partial<RealtimeConversation> & {
conversationId?: number
}
export function normalizeRealtimeMessage<TMessage extends RealtimeMessage>(
payload: RealtimeMessageCreatedPayload<TMessage> | null | undefined
): TMessage | null {
if (!payload) {
return null
}
if (payload.message?.id) {
return payload.message
}
const id = payload.messageId ?? 0
const conversationId = payload.conversationId ?? 0
if (id <= 0 || conversationId <= 0) {
return null
}
return {
id,
conversationId,
senderType: payload.senderType ?? "",
senderId: payload.senderId ?? 0,
senderName: payload.senderName,
senderAvatar: payload.senderAvatar,
messageType: payload.messageType ?? "",
content: payload.content ?? "",
payload: payload.payload,
sendStatus: payload.sendStatus ?? 0,
sentAt: payload.sentAt,
customerRead: false,
agentRead: false,
} as TMessage
}
export function mergeRealtimeMessage<TMessage extends RealtimeMessage>(
messages: TMessage[],
message: TMessage | null | undefined
): TMessage[] {
if (!message) {
return messages
}
return mergeImMessagesByIdAsc(messages, [message])
}
export function patchConversation<TConversation extends RealtimeConversation>(
conversation: TConversation | null,
patch: RealtimeConversationPatch | null | undefined
): TConversation | null {
if (!conversation || !patch) {
return conversation
}
const id = patch.id ?? patch.conversationId
if (!id || conversation.id !== id) {
return conversation
}
const fields = { ...patch }
delete fields.conversationId
return {
...conversation,
...fields,
} as TConversation
}
export function patchConversationList<TConversation extends RealtimeConversation>(
conversations: TConversation[],
patch: RealtimeConversationPatch | null | undefined
): TConversation[] {
if (!patch) {
return conversations
}
const id = patch.id ?? patch.conversationId
if (!id) {
return conversations
}
let changed = false
const next = conversations.map((item) => {
const patched = patchConversation(item, patch)
if (patched !== item) {
changed = true
}
return patched ?? item
})
return changed ? next : conversations
}
export function patchConversationWithMessage<
TConversation extends RealtimeConversation,
TMessage extends RealtimeMessage,
>(conversation: TConversation | null, message: TMessage | null | undefined) {
if (!conversation || !message || conversation.id !== message.conversationId) {
return conversation
}
return {
...conversation,
lastMessageId: message.id,
lastMessageAt: message.sentAt ?? conversation.lastMessageAt,
lastActiveAt: message.sentAt ?? conversation.lastActiveAt,
lastMessageSummary: summarizeIMMessage(message),
} as TConversation
}
export function patchConversationListWithMessage<
TConversation extends RealtimeConversation,
TMessage extends RealtimeMessage,
>(conversations: TConversation[], message: TMessage | null | undefined) {
if (!message) {
return conversations
}
let changed = false
const next = conversations.map((item) => {
const patched = patchConversationWithMessage(item, message)
if (patched !== item) {
changed = true
}
return patched ?? item
})
return changed ? next : conversations
}
export function markMessagesReadToMessageId<TMessage extends RealtimeMessage>(
messages: TMessage[],
messageId: number,
reader: "agent" | "customer",
readAt?: string
): TMessage[] {
if (messageId <= 0) {
return messages
}
let changed = false
const next = messages.map((message) => {
if (message.id > messageId) {
return message
}
if (reader === "agent") {
if (message.agentRead && message.agentReadAt === readAt) {
return message
}
changed = true
return { ...message, agentRead: true, agentReadAt: readAt } as TMessage
}
if (message.customerRead && message.customerReadAt === readAt) {
return message
}
changed = true
return { ...message, customerRead: true, customerReadAt: readAt } as TMessage
})
return changed ? next : messages
}