30. Listado de fuentes de conocimientos

 

30: Listado de Fuentes de Conocimiento

En este tutorial, aprenderemos a crear un listado de fuentes de conocimiento para un chatbot específico. Implementaremos un componente reutilizable para mostrar cada fuente y organizaremos la interfaz de usuario para visualizar todas las fuentes asociadas a un chatbot.


1. Cargar las Fuentes de Conocimiento desde el Controlador

1.1. Modificar el ChatbotController

Necesitamos cargar la relación knowledgeSources antes de pasar el chatbot a la vista.

app/Http/Controllers/ChatbotController.php

php
<?php

namespace App\Http\Controllers;

use App\Models\Chatbot;
use Illuminate\Http\Request;
use Inertia\Inertia;

class ChatbotController extends Controller
{
    /**
     * Muestra los detalles de un chatbot específico.
     */
    public function show(Chatbot $chatbot)
    {
        $this->authorize('view', $chatbot);

        // ✅ Cargar las fuentes de conocimiento del chatbot
        $chatbot->load('knowledgeSources');

        return Inertia::render('Chatbots/Show', [
            'chatbot' => $chatbot,
        ]);
    }

    // ... otros métodos
}

1.2. Verificar la Estructura de Datos

Si hacemos un dd() o inspeccionamos los datos en el frontend, veremos:

javascript
// Datos disponibles en la vista
{
    chatbot: {
        id: 'uuid',
        name: 'Mi Chatbot',
        description: 'Descripción del chatbot',
        // ... otros campos
        knowledge_sources: [
            {
                id: 'uuid-1',
                name: 'Manual de Usuario',
                type: 'pdf',
                path: 'knowledge-sources/.../manual.pdf',
                status: 'processed',
                created_at: '2024-01-01 12:00:00',
            },
            {
                id: 'uuid-2',
                name: 'Documentación API',
                type: 'website',
                path: 'https://api.ejemplo.com/docs',
                status: 'pending',
                created_at: '2024-01-02 12:00:00',
            },
            // ... más fuentes
        ]
    }
}

2. Actualizar la Vista Show

2.1. Estructura del Listado

resources/js/Pages/Chatbots/Show.vue

vue
<template>
    <AppLayout>
        <template #header>
            <div class="flex items-center justify-between">
                <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
                    {{ chatbot.name }}
                </h2>
                <button 
                    @click="openModal"
                    class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition"
                >
                    <BookIcon class="w-5 h-5 mr-2" />
                    Gestionar Conocimiento
                </button>
            </div>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <!-- Información del chatbot -->
                <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-6">
                    <div class="p-6 text-gray-900 dark:text-gray-100">
                        <div class="space-y-4">
                            <div>
                                <h3 class="text-lg font-medium">Descripción</h3>
                                <p class="text-gray-600 dark:text-gray-400">
                                    {{ chatbot.description || 'Sin descripción' }}
                                </p>
                            </div>
                            <div>
                                <h3 class="text-lg font-medium">Modelo</h3>
                                <p class="text-gray-600 dark:text-gray-400">
                                    {{ chatbot.model }}
                                </p>
                            </div>
                            <div>
                                <h3 class="text-lg font-medium">System Prompt</h3>
                                <pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-md text-sm whitespace-pre-wrap">
                                    {{ chatbot.system_prompt || 'No definido' }}
                                </pre>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- ✅ Listado de fuentes de conocimiento -->
                <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6">
                        <div class="flex items-center justify-between mb-4">
                            <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
                                Fuentes de Conocimiento
                            </h3>
                            <span class="text-sm text-gray-500 dark:text-gray-400">
                                {{ chatbot.knowledge_sources?.length || 0 }} fuentes
                            </span>
                        </div>

                        <!-- Listado -->
                        <div v-if="chatbot.knowledge_sources && chatbot.knowledge_sources.length > 0">
                            <div class="space-y-2">
                                <KnowledgeSourceListItem
                                    v-for="source in chatbot.knowledge_sources"
                                    :key="source.id"
                                    :knowledge-source="source"
                                />
                            </div>
                        </div>

                        <!-- Estado vacío -->
                        <div v-else class="text-center py-8">
                            <div class="text-gray-400 dark:text-gray-500">
                                <BookIcon class="w-12 h-12 mx-auto mb-3 opacity-50" />
                                <p class="text-sm">No hay fuentes de conocimiento</p>
                                <p class="text-xs mt-1">Agrega una fuente para mejorar las respuestas del chatbot</p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Modal para crear fuentes -->
        <KnowledgeSourceCreateModal 
            :show="showModal"
            @close="closeModal"
            :chatbot-id="chatbot.id"
        />
    </AppLayout>
</template>

<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import { ref } from 'vue';
import BookIcon from '@/Components/Icons/BookIcon.vue';
import KnowledgeSourceCreateModal from '@/Components/KnowledgeSources/KnowledgeSourceCreateModal.vue';
import KnowledgeSourceListItem from '@/Components/KnowledgeSources/KnowledgeSourceListItem.vue';

const props = defineProps({
    chatbot: Object,
});

const showModal = ref(false);

const openModal = () => {
    showModal.value = true;
};

const closeModal = () => {
    showModal.value = false;
};
</script>

3. Crear el Componente KnowledgeSourceListItem

3.1. Estructura del Componente

resources/js/Components/KnowledgeSources/KnowledgeSourceListItem.vue

vue
<template>
    <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 hover:border-indigo-300 dark:hover:border-indigo-500 transition-colors">
        <!-- Información de la fuente -->
        <div class="flex items-center space-x-3 min-w-0">
            <!-- Icono según el tipo -->
            <div :class="getTypeIconClass(knowledgeSource.type)" class="flex-shrink-0 p-2 rounded-lg">
                <component :is="getTypeIcon(knowledgeSource.type)" class="w-5 h-5" />
            </div>

            <div class="min-w-0">
                <!-- Nombre -->
                <div class="flex items-center gap-2">
                    <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
                        {{ knowledgeSource.name }}
                    </span>
                    <!-- Badge de estado -->
                    <span :class="getStatusBadgeClass(knowledgeSource.status)" class="flex-shrink-0 px-2 py-0.5 text-xs rounded-full">
                        {{ getStatusLabel(knowledgeSource.status) }}
                    </span>
                </div>
                
                <!-- Metadatos -->
                <div class="flex items-center gap-3 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
                    <span>{{ knowledgeSource.type.toUpperCase() }}</span>
                    <span>•</span>
                    <span>{{ formatDate(knowledgeSource.created_at) }}</span>
                    <span v-if="knowledgeSource.metadata" class="flex items-center gap-1">
                        •
                        <span v-if="knowledgeSource.metadata.pages">
                            {{ knowledgeSource.metadata.pages }} páginas
                        </span>
                        <span v-else-if="knowledgeSource.metadata.rows">
                            {{ knowledgeSource.metadata.rows }} registros
                        </span>
                    </span>
                </div>
            </div>
        </div>

        <!-- Acciones -->
        <div class="flex items-center gap-2 flex-shrink-0">
            <!-- Botón de reprocesar (solo si no está procesado) -->
            <button
                v-if="knowledgeSource.status !== 'processed'"
                @click="reprocessSource"
                class="p-1.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
                title="Reprocesar fuente"
            >
                <ArrowPathIcon class="w-4 h-4" />
            </button>

            <!-- Botón de ver detalles -->
            <button
                @click="viewSource"
                class="p-1.5 text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
                title="Ver detalles"
            >
                <EyeIcon class="w-4 h-4" />
            </button>

            <!-- Botón de eliminar -->
            <button
                @click="deleteSource"
                class="p-1.5 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 rounded-md hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
                title="Eliminar fuente"
            >
                <TrashIcon class="w-4 h-4" />
            </button>
        </div>
    </div>
</template>

<script setup>
import { computed } from 'vue';
import { router } from '@inertiajs/vue3';
import {
    DocumentIcon,
    GlobeIcon,
    DocumentTextIcon,
    TableCellsIcon,
    DocumentChartBarIcon,
    CodeBracketIcon,
} from '@heroicons/vue/24/outline';
import {
    ArrowPathIcon,
    EyeIcon,
    TrashIcon,
} from '@heroicons/vue/24/outline';

const props = defineProps({
    knowledgeSource: {
        type: Object,
        required: true,
    },
});

// Obtener el icono según el tipo
const getTypeIcon = (type) => {
    const icons = {
        pdf: DocumentIcon,
        website: GlobeIcon,
        text: DocumentTextIcon,
        csv: TableCellsIcon,
        excel: DocumentChartBarIcon,
        json: CodeBracketIcon,
    };
    return icons[type] || DocumentIcon;
};

// Obtener clase CSS para el icono según el tipo
const getTypeIconClass = (type) => {
    const classes = {
        pdf: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400',
        website: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
        text: 'bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400',
        csv: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400',
        excel: 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400',
        json: 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400',
    };
    return classes[type] || 'bg-gray-100 text-gray-600';
};

// Obtener clase CSS para el badge de estado
const getStatusBadgeClass = (status) => {
    const classes = {
        pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
        processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
        processed: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
        failed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
    };
    return classes[status] || 'bg-gray-100 text-gray-800';
};

// Obtener etiqueta del estado
const getStatusLabel = (status) => {
    const labels = {
        pending: 'Pendiente',
        processing: 'Procesando',
        processed: 'Procesado',
        failed: 'Falló',
    };
    return labels[status] || status;
};

// Formatear fecha
const formatDate = (date) => {
    if (!date) return '';
    return new Date(date).toLocaleDateString('es-ES', {
        day: '2-digit',
        month: 'short',
        year: 'numeric',
    });
};

// Acciones
const viewSource = () => {
    router.get(route('chatbots.knowledge-sources.show', {
        chatbot: props.knowledgeSource.chatbot_id,
        knowledgeSource: props.knowledgeSource.id,
    }));
};

const reprocessSource = () => {
    if (confirm(`¿Quieres reprocesar "${props.knowledgeSource.name}"?`)) {
        router.post(route('chatbots.knowledge-sources.reprocess', {
            chatbot: props.knowledgeSource.chatbot_id,
            knowledgeSource: props.knowledgeSource.id,
        }));
    }
};

const deleteSource = () => {
    if (confirm(`¿Estás seguro de que quieres eliminar "${props.knowledgeSource.name}"?`)) {
        router.delete(route('chatbots.knowledge-sources.destroy', {
            chatbot: props.knowledgeSource.chatbot_id,
            knowledgeSource: props.knowledgeSource.id,
        }));
    }
};
</script>

4. Mejoras Adicionales

4.1. Filtrado y Búsqueda

Podemos agregar un filtro para buscar fuentes de conocimiento:

vue
<template>
    <!-- Barra de búsqueda -->
    <div class="mb-4">
        <input
            v-model="searchQuery"
            type="text"
            placeholder="Buscar fuentes..."
            class="w-full sm:w-64 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"
        />
    </div>

    <!-- Listado filtrado -->
    <div v-if="filteredSources.length > 0">
        <KnowledgeSourceListItem
            v-for="source in filteredSources"
            :key="source.id"
            :knowledge-source="source"
        />
    </div>
</template>

<script setup>
import { computed, ref } from 'vue';

const searchQuery = ref('');

const filteredSources = computed(() => {
    if (!searchQuery.value) return props.chatbot.knowledge_sources;
    
    const query = searchQuery.value.toLowerCase();
    return props.chatbot.knowledge_sources.filter(source =>
        source.name.toLowerCase().includes(query) ||
        source.type.toLowerCase().includes(query)
    );
});
</script>

4.2. Paginación

Para muchos elementos, podemos agregar paginación:

vue
<template>
    <!-- Listado paginado -->
    <div class="space-y-2">
        <KnowledgeSourceListItem
            v-for="source in paginatedSources"
            :key="source.id"
            :knowledge-source="source"
        />
    </div>

    <!-- Paginación -->
    <div class="mt-4 flex items-center justify-between">
        <span class="text-sm text-gray-500 dark:text-gray-400">
            Mostrando {{ (currentPage - 1) * itemsPerPage + 1 }} - 
            {{ Math.min(currentPage * itemsPerPage, chatbot.knowledge_sources.length) }} 
            de {{ chatbot.knowledge_sources.length }} fuentes
        </span>
        <div class="flex gap-2">
            <button
                @click="currentPage--"
                :disabled="currentPage === 1"
                class="px-3 py-1 border rounded-md disabled:opacity-50"
            >
                Anterior
            </button>
            <button
                @click="currentPage++"
                :disabled="currentPage === totalPages"
                class="px-3 py-1 border rounded-md disabled:opacity-50"
            >
                Siguiente
            </button>
        </div>
    </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const currentPage = ref(1);
const itemsPerPage = 5;

const paginatedSources = computed(() => {
    const start = (currentPage.value - 1) * itemsPerPage;
    const end = start + itemsPerPage;
    return props.chatbot.knowledge_sources.slice(start, end);
});

const totalPages = computed(() => {
    return Math.ceil(props.chatbot.knowledge_sources.length / itemsPerPage);
});
</script>

5. Estilos del Listado

5.1. CSS para el Listado

css
/* resources/css/components/knowledge-sources.css */

.knowledge-sources-list {
    @apply space-y-2;
}

.knowledge-sources-list .list-item {
    @apply flex items-center justify-between p-4;
    @apply bg-gray-50 dark:bg-gray-700/50;
    @apply rounded-lg border border-gray-200 dark:border-gray-600;
    @apply hover:border-indigo-300 dark:hover:border-indigo-500;
    @apply transition-colors;
}

.knowledge-sources-list .list-item .type-icon {
    @apply flex-shrink-0 p-2 rounded-lg;
}

.knowledge-sources-list .list-item .type-icon.pdf {
    @apply bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400;
}

.knowledge-sources-list .list-item .type-icon.website {
    @apply bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400;
}

.knowledge-sources-list .list-item .type-icon.text {
    @apply bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400;
}

.knowledge-sources-list .list-item .status-badge {
    @apply px-2 py-0.5 text-xs rounded-full;
}

.knowledge-sources-list .list-item .status-badge.pending {
    @apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
}

.knowledge-sources-list .list-item .status-badge.processing {
    @apply bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400;
}

.knowledge-sources-list .list-item .status-badge.processed {
    @apply bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400;
}

.knowledge-sources-list .list-item .status-badge.failed {
    @apply bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
}

6. Actualización en Tiempo Real

6.1. Refrescar el Listado después de Crear

vue
<script setup>
// En KnowledgeSourceCreateModal.vue
const handleSubmit = () => {
    form.post(route('chatbots.knowledge-sources.store', props.chatbotId), {
        data: formData,
        forceFormData: true,
        preserveScroll: true,
        onSuccess: () => {
            handleClose();
            form.reset();
            // ✅ Emitir evento de actualización
            emit('created');
        },
    });
};
</script>

<!-- En Show.vue -->
<KnowledgeSourceCreateModal 
    :show="showModal"
    @close="closeModal"
    @created="refreshSources"
    :chatbot-id="chatbot.id"
/>

6.2. Función para Refrescar

javascript
// En Show.vue
const refreshSources = () => {
    // Recargar la página para obtener los datos actualizados
    router.reload({
        only: ['chatbot'],
    });
};

7. Resumen del Listado

7.1. Características Implementadas

✅ Carga de fuentes desde el controlador con load()
✅ Listado visual con diseño consistente
✅ Iconos por tipo (PDF, Website, etc.)
✅ Badges de estado (Pendiente, Procesando, Procesado, Falló)
✅ Acciones (Ver, Reprocesar, Eliminar)
✅ Estado vacío cuando no hay fuentes
✅ Fecha de creación formateada

7.2. Estructura de Datos

javascript
// KnowledgeSource
{
    id: 'uuid',
    chatbot_id: 'uuid',
    name: 'Manual de Usuario',
    type: 'pdf', // pdf, website, text, csv, excel, json
    path: 'knowledge-sources/.../manual.pdf',
    status: 'processed', // pending, processing, processed, failed
    extracted_content: '...',
    metadata: {
        pages: 10,
        size: 2048,
    },
    created_at: '2024-01-01 12:00:00',
    updated_at: '2024-01-01 12:00:00',
}

8. Código Completo del Componente ListItem

vue
<!-- resources/js/Components/KnowledgeSources/KnowledgeSourceListItem.vue -->
<template>
    <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 hover:border-indigo-300 dark:hover:border-indigo-500 transition-colors">
        <div class="flex items-center space-x-3 min-w-0">
            <div :class="getTypeIconClass(knowledgeSource.type)" class="flex-shrink-0 p-2 rounded-lg">
                <component :is="getTypeIcon(knowledgeSource.type)" class="w-5 h-5" />
            </div>
            <div class="min-w-0">
                <div class="flex items-center gap-2">
                    <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
                        {{ knowledgeSource.name }}
                    </span>
                    <span :class="getStatusBadgeClass(knowledgeSource.status)" class="flex-shrink-0 px-2 py-0.5 text-xs rounded-full">
                        {{ getStatusLabel(knowledgeSource.status) }}
                    </span>
                </div>
                <div class="flex items-center gap-3 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
                    <span>{{ knowledgeSource.type.toUpperCase() }}</span>
                    <span>•</span>
                    <span>{{ formatDate(knowledgeSource.created_at) }}</span>
                </div>
            </div>
        </div>
        <div class="flex items-center gap-2 flex-shrink-0">
            <button
                v-if="knowledgeSource.status !== 'processed'"
                @click="reprocessSource"
                class="p-1.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
                title="Reprocesar fuente"
            >
                <ArrowPathIcon class="w-4 h-4" />
            </button>
            <button
                @click="viewSource"
                class="p-1.5 text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
                title="Ver detalles"
            >
                <EyeIcon class="w-4 h-4" />
            </button>
            <button
                @click="deleteSource"
                class="p-1.5 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 rounded-md hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
                title="Eliminar fuente"
            >
                <TrashIcon class="w-4 h-4" />
            </button>
        </div>
    </div>
</template>

<script setup>
import { router } from '@inertiajs/vue3';
import {
    DocumentIcon,
    GlobeIcon,
    DocumentTextIcon,
    TableCellsIcon,
    DocumentChartBarIcon,
    CodeBracketIcon,
} from '@heroicons/vue/24/outline';
import {
    ArrowPathIcon,
    EyeIcon,
    TrashIcon,
} from '@heroicons/vue/24/outline';

const props = defineProps({
    knowledgeSource: {
        type: Object,
        required: true,
    },
});

const getTypeIcon = (type) => {
    const icons = {
        pdf: DocumentIcon,
        website: GlobeIcon,
        text: DocumentTextIcon,
        csv: TableCellsIcon,
        excel: DocumentChartBarIcon,
        json: CodeBracketIcon,
    };
    return icons[type] || DocumentIcon;
};

const getTypeIconClass = (type) => {
    const classes = {
        pdf: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400',
        website: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
        text: 'bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400',
        csv: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400',
        excel: 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400',
        json: 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400',
    };
    return classes[type] || 'bg-gray-100 text-gray-600';
};

const getStatusBadgeClass = (status) => {
    const classes = {
        pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
        processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
        processed: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
        failed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
    };
    return classes[status] || 'bg-gray-100 text-gray-800';
};

const getStatusLabel = (status) => {
    const labels = {
        pending: 'Pendiente',
        processing: 'Procesando',
        processed: 'Procesado',
        failed: 'Falló',
    };
    return labels[status] || status;
};

const formatDate = (date) => {
    if (!date) return '';
    return new Date(date).toLocaleDateString('es-ES', {
        day: '2-digit',
        month: 'short',
        year: 'numeric',
    });
};

const viewSource = () => {
    router.get(route('chatbots.knowledge-sources.show', {
        chatbot: props.knowledgeSource.chatbot_id,
        knowledgeSource: props.knowledgeSource.id,
    }));
};

const reprocessSource = () => {
    if (confirm(`¿Quieres reprocesar "${props.knowledgeSource.name}"?`)) {
        router.post(route('chatbots.knowledge-sources.reprocess', {
            chatbot: props.knowledgeSource.chatbot_id,
            knowledgeSource: props.knowledgeSource.id,
        }));
    }
};

const deleteSource = () => {
    if (confirm(`¿Estás seguro de que quieres eliminar "${props.knowledgeSource.name}"?`)) {
        router.delete(route('chatbots.knowledge-sources.destroy', {
            chatbot: props.knowledgeSource.chatbot_id,
            knowledgeSource: props.knowledgeSource.id,
        }));
    }
};
</script>

9. Resumen

En este tutorial hemos aprendido a:

✅ Cargar relaciones con load() en el controlador
✅ Crear un listado de fuentes de conocimiento
✅ Diseñar un componente reutilizable KnowledgeSourceListItem
✅ Mostrar información con iconos, badges y metadatos
✅ Implementar acciones (ver, reprocesar, eliminar)
✅ Manejar estados (vacío, con datos)
✅ Formatear fechas para mejor visualización

¡Excelente trabajo! Ahora tienes un listado completo y funcional de fuentes de conocimiento para tus chatbots

Comentarios

Entradas más populares de este blog

1. Introducción al curso de Chatbots personalizados con Laravel & OpenAI

2. Prerequisitos para el Curso de Chatbots con Laravel y OpenAI

4. Herramientas de Calidad