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:
parent
6cc4472feb
commit
c0d2fed17a
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user