"use client" import { forwardRef, type HTMLAttributes } from "react" import { CheckCircleIcon, InfoCircleIcon } from "../icons-v2-generated" import { AiAssistantInfo } from "./ai-assistant-info" import { TICKET_EVENT_KIND, type TicketEventSegment } from "./types/message.types" /** Actor types the backend uses for the AI agent. Open set on the wire — * compared case-insensitively; anything else is treated as a human. */ const AI_ACTOR_TYPES = new Set(["AI", "AGENT", "ASSISTANT", "BOT"]) function isAiActor(actorType?: string): boolean { return !!actorType && AI_ACTOR_TYPES.has(actorType.toUpperCase()) } /** `TICKET_ON_HOLD` → `Ticket On Hold` — neutral title for kinds this build * does not know, which must still render (the vocabulary is open). */ function humanizeKind(kind: string): string { return kind .split(/[_\s-]+/) .filter(Boolean) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(" ") } export interface TicketEventMessageProps extends HTMLAttributes { data: TicketEventSegment['data'] /** Timestamp of the message row this block came in on. */ timestamp?: Date } /** Where the ticket reopened INTO → the "who picks it up next" line. * Kind-tokens per the lifecycle statuses; the vocabulary is open, so an * unrecognized target falls back to the actor heuristic below. */ const REOPEN_TARGET_COPY: Record = { AI_ASSISTANCE: "The AI assistant will continue helping you in this conversation.", TECH_REQUIRED: "A technician will reply when available.", } /** * Ticket lifecycle receipt — resolved / reopened / an unknown future kind. * Figma `ai-assistant-info` (type=resolved-fae | resolved-tech | * reopened-fae | reopened-tech | reopened-reason). * * Copy is composed client-side from the event's fields (unlike * `ticket_escalated`, whose body the backend authors): * RESOLVED → "Resolved by {actorName}." under a green check; * REOPENED → the reason when the wire carries one; otherwise who picks the * conversation up next — `targetStatusKind` decides deterministically, * with the `actorType` heuristic as the fallback for older backends * that don't send the target; * unknown kind → a neutral info line: humanized kind as the title, the * reason or actor as the body. Never dropped — the vocabulary is open. */ const TicketEventMessage = forwardRef( ({ data, timestamp, ...props }, ref) => { const { kind, actorName, actorType, reason, targetStatusKind } = data const resolved = kind === TICKET_EVENT_KIND.RESOLVED const reopened = kind === TICKET_EVENT_KIND.REOPENED const title = resolved ? "Ticket Resolved" : reopened ? "Ticket Reopened" : humanizeKind(kind) let body: string | undefined if (resolved) { body = actorName ? `Resolved by ${actorName}.` : "The ticket has been resolved." } else if (reopened) { body = (reason ? `Reason: ${reason}` : undefined) ?? (targetStatusKind ? REOPEN_TARGET_COPY[targetStatusKind] : undefined) ?? (isAiActor(actorType) ? `${actorName || "Fae"} will continue assisting in this conversation.` : "A technician will reply when available.") } else { body = reason || (actorName ? `By ${actorName}.` : undefined) } const icon = resolved ? ( ) : ( ) return }, ) TicketEventMessage.displayName = "TicketEventMessage" export { TicketEventMessage }