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