Ajusta player de audio e permite seek
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 5s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 4m9s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
Quality Checks / Lint, Test and Build (push) Successful in 5m16s

This commit is contained in:
rever-tecnologia 2025-12-19 16:08:00 -03:00
parent d125930cf6
commit bc67dc01ef
4 changed files with 80 additions and 33 deletions

View file

@ -133,7 +133,7 @@ function AudioWaveform({
}) { }) {
const playedBars = Math.round(progress * peaks.length) const playedBars = Math.round(progress * peaks.length)
return ( return (
<div className="flex h-8 items-center gap-[2px]"> <div className="flex h-8 items-center gap-[2px] overflow-hidden">
{peaks.map((value, index) => { {peaks.map((value, index) => {
const height = Math.max(4, Math.round(value * 24)) const height = Math.max(4, Math.round(value * 24))
const played = index <= playedBars const played = index <= playedBars
@ -191,7 +191,7 @@ function AudioAttachmentPlayer({
const decoded = await audioContext.decodeAudioData(buffer) const decoded = await audioContext.decodeAudioData(buffer)
await audioContext.close() await audioContext.close()
if (!cancelled) { if (!cancelled) {
setPeaks(extractPeaks(decoded)) setPeaks(extractPeaks(decoded, 36))
} }
} catch (error) { } catch (error) {
console.error("Falha ao gerar waveform:", error) console.error("Falha ao gerar waveform:", error)
@ -246,10 +246,11 @@ function AudioAttachmentPlayer({
const progress = duration > 0 ? currentTime / duration : 0 const progress = duration > 0 ? currentTime / duration : 0
const sizeLabel = formatAttachmentSize(size) const sizeLabel = formatAttachmentSize(size)
const canSeek = duration > 0
return ( return (
<div <div
className={`flex items-center gap-3 rounded-2xl border px-3 py-2 ${ className={`flex w-full max-w-[280px] items-center gap-3 rounded-2xl border px-3 py-2 ${
isAgent ? "border-white/10 bg-white/10 text-white" : "border-slate-200 bg-white text-slate-900" isAgent ? "border-white/10 bg-white/10 text-white" : "border-slate-200 bg-white text-slate-900"
}`} }`}
> >
@ -264,14 +265,34 @@ function AudioAttachmentPlayer({
{isPlaying ? <Pause className="size-4" /> : <Play className="size-4" />} {isPlaying ? <Pause className="size-4" /> : <Play className="size-4" />}
</button> </button>
<div className="flex-1"> <div className="flex-1 min-w-0">
{isLoadingWaveform ? ( <button
<div className={`h-8 rounded-lg ${isAgent ? "bg-white/10" : "bg-slate-100"}`} /> type="button"
) : peaks.length > 0 ? ( onPointerDown={(event) => {
<AudioWaveform peaks={peaks} progress={progress} isAgent={isAgent} /> if (!audioRef.current || duration <= 0) return
) : ( const rect = event.currentTarget.getBoundingClientRect()
<div className={`h-8 rounded-lg ${isAgent ? "bg-white/10" : "bg-slate-100"}`} /> if (rect.width <= 0) return
)} const ratio = (event.clientX - rect.left) / rect.width
const clamped = Math.min(1, Math.max(0, ratio))
const targetTime = clamped * duration
audioRef.current.currentTime = targetTime
setCurrentTime(targetTime)
}}
aria-label="Buscar posição do áudio"
className={[
"w-full rounded-lg bg-transparent p-0 text-left transition-colors focus-visible:outline-none",
canSeek ? "cursor-pointer" : "cursor-default",
canSeek ? (isAgent ? "hover:bg-white/10" : "hover:bg-slate-100") : "",
].join(" ")}
>
{isLoadingWaveform ? (
<div className={`h-8 rounded-lg ${isAgent ? "bg-white/10" : "bg-slate-100"}`} />
) : peaks.length > 0 ? (
<AudioWaveform peaks={peaks} progress={progress} isAgent={isAgent} />
) : (
<div className={`h-8 rounded-lg ${isAgent ? "bg-white/10" : "bg-slate-100"}`} />
)}
</button>
<div className={`mt-1 flex items-center justify-between text-[10px] ${isAgent ? "text-white/60" : "text-slate-400"}`}> <div className={`mt-1 flex items-center justify-between text-[10px] ${isAgent ? "text-white/60" : "text-slate-400"}`}>
<span>{formatDuration(currentTime)}</span> <span>{formatDuration(currentTime)}</span>
<span className="truncate">{sizeLabel ?? formatDuration(duration)}</span> <span className="truncate">{sizeLabel ?? formatDuration(duration)}</span>
@ -282,7 +303,9 @@ function AudioAttachmentPlayer({
type="button" type="button"
onClick={handleDownload} onClick={handleDownload}
className={`flex size-8 items-center justify-center rounded-md ${ className={`flex size-8 items-center justify-center rounded-md ${
isAgent ? "text-white/70 hover:text-white" : "text-slate-500 hover:text-slate-700" isAgent
? "text-white/70 hover:bg-white/10 hover:text-white"
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700"
}`} }`}
aria-label="Baixar áudio" aria-label="Baixar áudio"
> >
@ -1147,12 +1170,12 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
return ( return (
<div <div
key={att.storageId} key={att.storageId}
className="flex items-center gap-2 rounded-lg bg-slate-100 px-2 py-1 text-xs" className="flex w-full items-center gap-2 rounded-lg bg-slate-100 px-2 py-1 text-xs"
> >
<audio controls src={att.previewUrl} className="h-7 w-40" /> <audio controls src={att.previewUrl} className="h-8 w-full min-w-0" />
<button <button
onClick={() => handleRemoveAttachment(att.storageId)} onClick={() => handleRemoveAttachment(att.storageId)}
className="flex size-6 items-center justify-center rounded-full text-slate-500 hover:bg-slate-200" className="flex size-6 shrink-0 items-center justify-center rounded-full text-slate-500 hover:bg-slate-200"
aria-label="Remover áudio" aria-label="Remover áudio"
> >
<X className="size-3" /> <X className="size-3" />

View file

@ -42,7 +42,7 @@ function AudioWaveform({
}) { }) {
const playedBars = Math.round(progress * peaks.length) const playedBars = Math.round(progress * peaks.length)
return ( return (
<div className="flex h-9 items-center gap-[2px]"> <div className="flex h-9 items-center gap-[2px] overflow-hidden">
{peaks.map((value, index) => { {peaks.map((value, index) => {
const height = Math.max(4, Math.round(value * 28)) const height = Math.max(4, Math.round(value * 28))
const played = index <= playedBars const played = index <= playedBars
@ -100,7 +100,7 @@ function AudioAttachmentPlayer({
const decoded = await audioContext.decodeAudioData(buffer) const decoded = await audioContext.decodeAudioData(buffer)
await audioContext.close() await audioContext.close()
if (!cancelled) { if (!cancelled) {
setPeaks(extractPeaks(decoded)) setPeaks(extractPeaks(decoded, 36))
} }
} catch (error) { } catch (error) {
console.error("Falha ao gerar waveform:", error) console.error("Falha ao gerar waveform:", error)
@ -155,11 +155,22 @@ function AudioAttachmentPlayer({
const progress = duration > 0 ? currentTime / duration : 0 const progress = duration > 0 ? currentTime / duration : 0
const sizeLabel = formatAttachmentSize(size) const sizeLabel = formatAttachmentSize(size)
const canSeek = duration > 0
const handleSeek = (event: { currentTarget: HTMLButtonElement; clientX: number }) => {
if (!audioRef.current || duration <= 0) return
const rect = event.currentTarget.getBoundingClientRect()
if (rect.width <= 0) return
const ratio = (event.clientX - rect.left) / rect.width
const clamped = Math.min(1, Math.max(0, ratio))
const targetTime = clamped * duration
audioRef.current.currentTime = targetTime
setCurrentTime(targetTime)
}
return ( return (
<div <div
className={cn( className={cn(
"flex items-center gap-3 rounded-2xl border px-3 py-2", "flex w-full max-w-[280px] items-center gap-3 rounded-2xl border px-3 py-2",
tone === "dark" tone === "dark"
? "border-white/10 bg-white/10 text-white" ? "border-white/10 bg-white/10 text-white"
: "border-slate-200 bg-white text-slate-900" : "border-slate-200 bg-white text-slate-900"
@ -179,14 +190,25 @@ function AudioAttachmentPlayer({
{isPlaying ? <Pause className="size-4" /> : <Play className="size-4" />} {isPlaying ? <Pause className="size-4" /> : <Play className="size-4" />}
</Button> </Button>
<div className="flex-1"> <div className="flex-1 min-w-0">
{isLoadingWaveform ? ( <button
<div className={cn("h-9 rounded-lg", tone === "dark" ? "bg-white/10" : "bg-slate-100")} /> type="button"
) : peaks.length > 0 ? ( onPointerDown={handleSeek}
<AudioWaveform peaks={peaks} progress={progress} tone={tone} /> aria-label="Buscar posição do áudio"
) : ( className={cn(
<div className={cn("h-9 rounded-lg", tone === "dark" ? "bg-white/10" : "bg-slate-100")} /> "w-full rounded-lg bg-transparent p-0 text-left transition-colors focus-visible:outline-none",
)} canSeek ? "cursor-pointer" : "cursor-default",
canSeek && (tone === "dark" ? "hover:bg-white/10" : "hover:bg-slate-100")
)}
>
{isLoadingWaveform ? (
<div className={cn("h-9 rounded-lg", tone === "dark" ? "bg-white/10" : "bg-slate-100")} />
) : peaks.length > 0 ? (
<AudioWaveform peaks={peaks} progress={progress} tone={tone} />
) : (
<div className={cn("h-9 rounded-lg", tone === "dark" ? "bg-white/10" : "bg-slate-100")} />
)}
</button>
<div <div
className={cn( className={cn(
"mt-1 flex items-center justify-between text-[10px]", "mt-1 flex items-center justify-between text-[10px]",
@ -204,7 +226,9 @@ function AudioAttachmentPlayer({
size="icon" size="icon"
className={cn( className={cn(
"size-8", "size-8",
tone === "dark" ? "text-white/70 hover:text-white" : "text-slate-500 hover:text-slate-700" tone === "dark"
? "text-white/70 hover:bg-white/10 hover:text-white"
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700"
)} )}
onClick={handleDownload} onClick={handleDownload}
aria-label="Baixar áudio" aria-label="Baixar áudio"

View file

@ -826,12 +826,12 @@ export function ChatWidget() {
return ( return (
<div <div
key={index} key={index}
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-2 py-1" className="flex w-full items-center gap-2 rounded-lg border border-slate-200 bg-white px-2 py-1"
> >
<audio controls src={file.previewUrl} className="h-8 w-40" /> <audio controls src={file.previewUrl} className="h-9 w-full min-w-0" />
<button <button
onClick={() => removeAttachment(index)} onClick={() => removeAttachment(index)}
className="flex size-6 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100" className="flex size-6 shrink-0 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
aria-label="Remover áudio" aria-label="Remover áudio"
> >
<X className="size-3" /> <X className="size-3" />

View file

@ -547,8 +547,8 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) {
)} )}
{pendingAudio && ( {pendingAudio && (
<div className="mb-2 flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-2 py-1"> <div className="mb-2 flex w-full items-center gap-2 rounded-lg border border-slate-200 bg-white px-2 py-1">
<audio controls src={pendingAudio.previewUrl} className="h-8 w-44" /> <audio controls src={pendingAudio.previewUrl} className="h-9 w-full min-w-0" />
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -557,7 +557,7 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) {
} }
setPendingAudio(null) setPendingAudio(null)
}} }}
className="flex size-7 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100" className="flex size-7 shrink-0 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
aria-label="Remover áudio" aria-label="Remover áudio"
> >
<X className="size-3.5" /> <X className="size-3.5" />