Feature: Agregar botón para crear webhook de debug automáticamente
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s

- Agregar botón "Crear Webhook de Debug" en WebhookReceiverSection
- Detectar si ya existe un webhook apuntando al receptor de debug
- Permitir eliminar el webhook de debug
- Incluir todos los eventos disponibles al crear el webhook
- También incluye mejoras previas de manejo de media y mensajes
This commit is contained in:
2025-12-02 21:21:33 -06:00
parent 71593b25e9
commit 80d0042c7e
21 changed files with 3722 additions and 112 deletions

View File

@@ -0,0 +1,172 @@
<template>
<div class="space-y-2">
<!-- Files grid -->
<div class="flex flex-wrap gap-2">
<div
v-for="(file, index) in files"
:key="index"
class="relative group"
>
<!-- Image preview -->
<div
v-if="isImage(file)"
class="relative"
>
<img
:src="getPreviewUrl(file)"
class="h-20 w-20 object-cover rounded-lg"
/>
<button
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
@click="$emit('remove', index)"
>
<UIcon name="i-lucide-x" class="w-3 h-3" />
</button>
</div>
<!-- Video preview -->
<div
v-else-if="isVideo(file)"
class="relative"
>
<div class="h-20 w-20 bg-gray-800 rounded-lg flex items-center justify-center">
<video
:src="getPreviewUrl(file)"
class="h-full w-full object-cover rounded-lg"
/>
<div class="absolute inset-0 flex items-center justify-center">
<UIcon name="i-lucide-play-circle" class="w-8 h-8 text-white/80" />
</div>
</div>
<button
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
@click="$emit('remove', index)"
>
<UIcon name="i-lucide-x" class="w-3 h-3" />
</button>
</div>
<!-- Audio preview -->
<div
v-else-if="isAudio(file)"
class="relative flex items-center gap-2 px-3 py-2 bg-gray-800 rounded-lg"
>
<UIcon name="i-lucide-music" class="w-5 h-5 text-[var(--wa-green-light)]" />
<span class="text-sm text-white truncate max-w-[100px]">{{ file.name }}</span>
<button
class="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center"
@click="$emit('remove', index)"
>
<UIcon name="i-lucide-x" class="w-3 h-3" />
</button>
</div>
<!-- Document preview -->
<div
v-else
class="relative flex items-center gap-2 px-3 py-2 bg-gray-800 rounded-lg"
>
<UIcon :name="getDocIcon(file)" class="w-5 h-5 text-[var(--wa-blue)]" />
<div class="flex flex-col min-w-0">
<span class="text-sm text-white truncate max-w-[100px]">{{ file.name }}</span>
<span class="text-xs text-gray-400">{{ formatSize(file.size) }}</span>
</div>
<button
class="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center flex-shrink-0"
@click="$emit('remove', index)"
>
<UIcon name="i-lucide-x" class="w-3 h-3" />
</button>
</div>
</div>
</div>
<!-- Caption input -->
<div v-if="showCaption && files.length > 0">
<UInput
:model-value="caption"
placeholder="Agregar descripción..."
size="sm"
class="bg-[var(--wa-bg-light)]"
@update:model-value="$emit('update:caption', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
files: File[]
caption?: string
showCaption?: boolean
}
withDefaults(defineProps<Props>(), {
caption: '',
showCaption: true
})
defineEmits<{
remove: [index: number]
'update:caption': [value: string]
}>()
// Preview URL cache
const previewUrls = new Map<File, string>()
const getPreviewUrl = (file: File): string => {
if (!previewUrls.has(file)) {
previewUrls.set(file, URL.createObjectURL(file))
}
return previewUrls.get(file)!
}
// Cleanup URLs on unmount
onUnmounted(() => {
previewUrls.forEach(url => URL.revokeObjectURL(url))
previewUrls.clear()
})
const isImage = (file: File): boolean => {
return file.type.startsWith('image/')
}
const isVideo = (file: File): boolean => {
return file.type.startsWith('video/')
}
const isAudio = (file: File): boolean => {
return file.type.startsWith('audio/')
}
const getDocIcon = (file: File): string => {
const type = file.type
const name = file.name.toLowerCase()
if (type.includes('pdf') || name.endsWith('.pdf')) {
return 'i-lucide-file-text'
}
if (type.includes('spreadsheet') || name.match(/\.(xlsx?|csv)$/)) {
return 'i-lucide-file-spreadsheet'
}
if (type.includes('word') || name.match(/\.(docx?)$/)) {
return 'i-lucide-file-text'
}
if (type.includes('presentation') || name.match(/\.(pptx?)$/)) {
return 'i-lucide-file-presentation'
}
if (type.includes('zip') || name.match(/\.(zip|rar|7z)$/)) {
return 'i-lucide-file-archive'
}
return 'i-lucide-file'
}
const formatSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
</script>