Fix lint errors and improve chat UI
Lint fixes: - Move HIDDEN_EVENT_TYPES constant outside component to fix useMemo dependency - Add eslint-disable comments for img elements using blob URLs Chat widget improvements: - Add view and download buttons with loading and success indicators - Click image to open in new tab, download button to save file - Show check icon after successful download Chat history fixes: - Fix title to "Histórico de chat" with proper accents - Change agent icon from Headphones to MessageCircle - Change agent icon background from primary to gray 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f0882c612f
commit
ebeda62cfb
3 changed files with 74 additions and 23 deletions
|
|
@ -1,5 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useAction, useMutation, useQuery } from "convex/react"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
|
|
@ -30,6 +31,8 @@ import {
|
|||
Image as ImageIcon,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
Check,
|
||||
} from "lucide-react"
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 4000
|
||||
|
|
@ -122,9 +125,17 @@ function MessageAttachment({ attachment }: { attachment: ChatAttachment }) {
|
|||
}, [attachment.storageId, getFileUrl])
|
||||
|
||||
const isImage = attachment.type?.startsWith("image/")
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [downloaded, setDownloaded] = useState(false)
|
||||
|
||||
const handleView = () => {
|
||||
if (!url) return
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!url) return
|
||||
if (!url || downloading) return
|
||||
setDownloading(true)
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
|
|
@ -136,8 +147,12 @@ function MessageAttachment({ attachment }: { attachment: ChatAttachment }) {
|
|||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
setDownloaded(true)
|
||||
setTimeout(() => setDownloaded(false), 2000)
|
||||
} catch (error) {
|
||||
toast.error("Erro ao baixar arquivo")
|
||||
} finally {
|
||||
setDownloading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,31 +166,67 @@ function MessageAttachment({ attachment }: { attachment: ChatAttachment }) {
|
|||
|
||||
if (isImage && url) {
|
||||
return (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="group relative overflow-hidden rounded-lg border border-slate-200"
|
||||
>
|
||||
<div className="group relative overflow-hidden rounded-lg border border-slate-200">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={url}
|
||||
alt={attachment.name}
|
||||
className="size-16 object-cover"
|
||||
className="size-16 cursor-pointer object-cover"
|
||||
onClick={handleView}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Download className="size-4 text-white" />
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-1 bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
onClick={handleView}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-white/20 hover:bg-white/30"
|
||||
title="Visualizar"
|
||||
>
|
||||
<Eye className="size-3.5 text-white" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-white/20 hover:bg-white/30"
|
||||
title="Baixar"
|
||||
>
|
||||
{downloading ? (
|
||||
<Spinner className="size-3 text-white" />
|
||||
) : downloaded ? (
|
||||
<Check className="size-3.5 text-emerald-400" />
|
||||
) : (
|
||||
<Download className="size-3.5 text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-2 py-1.5 text-xs hover:bg-slate-100"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 rounded-lg border border-slate-200 bg-slate-50 px-2 py-1.5 text-xs">
|
||||
<FileText className="size-4 text-slate-500" />
|
||||
<span className="max-w-[80px] truncate text-slate-700">{attachment.name}</span>
|
||||
<Download className="size-3 text-slate-400" />
|
||||
<button
|
||||
onClick={handleView}
|
||||
className="rounded p-0.5 hover:bg-slate-200"
|
||||
title="Visualizar"
|
||||
>
|
||||
<Eye className="size-3 text-slate-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
className="rounded p-0.5 hover:bg-slate-200"
|
||||
title="Baixar"
|
||||
>
|
||||
{downloading ? (
|
||||
<Spinner className="size-3 text-slate-400" />
|
||||
) : downloaded ? (
|
||||
<Check className="size-3 text-emerald-500" />
|
||||
) : (
|
||||
<Download className="size-3 text-slate-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -598,6 +649,7 @@ export function ChatWidget() {
|
|||
{attachments.map((file, index) => (
|
||||
<div key={index} className="group relative">
|
||||
{file.type?.startsWith("image/") && file.previewUrl ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img
|
||||
src={file.previewUrl}
|
||||
alt={file.name}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
ChevronDown,
|
||||
ChevronRight,
|
||||
User,
|
||||
Headphones,
|
||||
Clock,
|
||||
Download,
|
||||
FileText,
|
||||
|
|
@ -183,10 +182,10 @@ function ChatSessionCard({ session, isExpanded, onToggle }: { session: ChatSessi
|
|||
<div
|
||||
className={cn(
|
||||
"flex size-7 shrink-0 items-center justify-center rounded-full",
|
||||
isAgent ? "bg-primary/10 text-primary" : "bg-slate-200 text-slate-600"
|
||||
isAgent ? "bg-slate-100 text-slate-600" : "bg-slate-200 text-slate-600"
|
||||
)}
|
||||
>
|
||||
{isAgent ? <Headphones className="size-3.5" /> : <User className="size-3.5" />}
|
||||
{isAgent ? <MessageCircle className="size-3.5" /> : <User className="size-3.5" />}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -277,9 +276,9 @@ export function TicketChatHistory({ ticketId }: ChatHistoryProps) {
|
|||
<CardHeader className="border-b border-slate-100 bg-slate-50/50 px-4 py-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold text-slate-900">
|
||||
<MessageCircle className="size-4" />
|
||||
Historico de Chat
|
||||
Histórico de chat
|
||||
<span className="ml-auto text-xs font-normal text-slate-500">
|
||||
{chatHistory.sessions.length} {chatHistory.sessions.length === 1 ? "sessao" : "sessoes"} - {chatHistory.totalMessages} mensagens
|
||||
{chatHistory.sessions.length} {chatHistory.sessions.length === 1 ? "sessão" : "sessões"} - {chatHistory.totalMessages} mensagens
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ const timelineIcons: Record<string, ComponentType<{ className?: string }>> = {
|
|||
|
||||
const timelineLabels: Record<string, string> = TICKET_TIMELINE_LABELS
|
||||
|
||||
// Tipos de eventos que não devem aparecer na timeline
|
||||
const HIDDEN_EVENT_TYPES = ["CHAT_MESSAGE_ADDED"]
|
||||
|
||||
interface TicketTimelineProps {
|
||||
ticket: TicketWithDetails
|
||||
}
|
||||
|
|
@ -82,9 +85,6 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
|||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
// Tipos de eventos que não devem aparecer na timeline
|
||||
const HIDDEN_EVENT_TYPES = ["CHAT_MESSAGE_ADDED"]
|
||||
|
||||
const sortedTimeline = useMemo(
|
||||
() => [...ticket.timeline]
|
||||
.filter((event) => !HIDDEN_EVENT_TYPES.includes(event.type))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue