Improve ticket timeline readability for storefront transcripts.

Render storefront escalation transcript lines as individual conversation bubbles and keep sorting/expand controls working against the expanded timeline so reps can follow context clearly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Saad Mohsin 2026-05-14 00:18:15 +05:00
parent 6cc4472feb
commit c0d2fed17a

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { useParams, useRouter } from "next/navigation" import { useParams, useRouter } from "next/navigation"
import { import {
ArrowLeft, ArrowLeft,
@ -37,6 +37,10 @@ interface TicketData {
messages: TicketMessage[] messages: TicketMessage[]
} }
interface TimelineMessage extends TicketMessage {
senderOverride?: "CUSTOMER" | "ASSISTANT"
}
type AskSupplierPreviewGroup = { type AskSupplierPreviewGroup = {
supplierUserId: string supplierUserId: string
companyName: string companyName: string
@ -310,6 +314,40 @@ export default function TicketDetailPage() {
const cleanDisplayText = (text: string): string => const cleanDisplayText = (text: string): string =>
text.replace(/^Storefront escalation from visitor [^:]+:\s*/i, "").trim() text.replace(/^Storefront escalation from visitor [^:]+:\s*/i, "").trim()
const timelineMessages = useMemo<TimelineMessage[]>(() => {
if (!ticket) return []
return ticket.messages.flatMap((message) => {
if (message.trigger !== "STOREFRONT_TICKET") return [{ ...message }]
const raw = cleanDisplayText(message.bodyPreview || "")
if (!raw) return [{ ...message }]
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
const parsed = lines
.map((line, idx) => {
const match = line.match(/^\[(.+?)\]\s+(CUSTOMER|ASSISTANT):\s*(.+)$/i)
if (!match) return null
const sentAtMaybe = new Date(match[1])
const sentAt = Number.isNaN(sentAtMaybe.getTime())
? message.sentAt
: sentAtMaybe.toISOString()
return {
id: `${message.id}-part-${idx}`,
subject: message.subject,
bodyPreview: match[3],
status: message.status,
trigger: message.trigger,
sentAt,
senderOverride:
match[2].toUpperCase() === "CUSTOMER" ? ("CUSTOMER" as const) : ("ASSISTANT" as const),
} satisfies TimelineMessage
})
.filter((row): row is TimelineMessage => Boolean(row))
return parsed.length >= 2 ? parsed : [{ ...message }]
})
}, [ticket])
const sourceLabel = (trigger: string | null): string => { const sourceLabel = (trigger: string | null): string => {
if (trigger === "GMAIL_INCOMING") return "Email inbound" if (trigger === "GMAIL_INCOMING") return "Email inbound"
if (trigger === "TICKET_CUSTOMER_REPLY") return "Vendor email" if (trigger === "TICKET_CUSTOMER_REPLY") return "Vendor email"
@ -431,7 +469,7 @@ export default function TicketDetailPage() {
type="button" type="button"
onClick={() => onClick={() =>
setExpandedMessages( setExpandedMessages(
Object.fromEntries((ticket.messages || []).map((m) => [m.id, true])), Object.fromEntries((timelineMessages || []).map((m) => [m.id, true])),
) )
} }
className={cn( className={cn(
@ -829,9 +867,11 @@ export default function TicketDetailPage() {
{/* Conversation timeline - Full width corners chat style */} {/* Conversation timeline - Full width corners chat style */}
<div className="flex-1 overflow-y-auto min-h-0 pr-4 -mr-4 space-y-8 pb-10 custom-scrollbar"> <div className="flex-1 overflow-y-auto min-h-0 pr-4 -mr-4 space-y-8 pb-10 custom-scrollbar">
{(chronological ? [...ticket.messages].reverse() : ticket.messages).map((message) => { {(chronological ? [...timelineMessages].reverse() : timelineMessages).map((message) => {
const isIncoming = const isIncoming =
message.trigger === "GMAIL_INCOMING" || message.status === "RECEIVED" message.senderOverride === "CUSTOMER" ||
message.trigger === "GMAIL_INCOMING" ||
message.status === "RECEIVED"
const fullText = cleanDisplayText(message.bodyPreview || "") const fullText = cleanDisplayText(message.bodyPreview || "")
const lines = fullText.split(/\r?\n/) const lines = fullText.split(/\r?\n/)
@ -868,7 +908,13 @@ export default function TicketDetailPage() {
? (isLight ? "text-indigo-600" : "text-indigo-400") ? (isLight ? "text-indigo-600" : "text-indigo-400")
: "text-indigo-100" : "text-indigo-100"
)}> )}>
{isIncoming ? "Customer" : "JUNO"} {message.senderOverride === "ASSISTANT"
? "JUNO"
: message.senderOverride === "CUSTOMER"
? "Customer"
: isIncoming
? "Customer"
: "JUNO"}
</span> </span>
<span <span
className={cn( className={cn(
@ -949,7 +995,7 @@ export default function TicketDetailPage() {
) )
})} })}
{ticket.messages.length === 0 && ( {timelineMessages.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-slate-500"> <div className="flex flex-col items-center justify-center py-20 text-slate-500">
<Ticket className="w-12 h-12 mb-4 opacity-10" /> <Ticket className="w-12 h-12 mb-4 opacity-10" />
<p className="text-sm font-medium">No messages logged yet for this ticket.</p> <p className="text-sm font-medium">No messages logged yet for this ticket.</p>