24. Formulario para crear fuentes de conocimientos

 

24: Formulario para Crear Fuentes de Conocimiento

En este tutorial, crearemos un formulario completo para agregar fuentes de conocimiento a los chatbots. Aprenderemos a crear un componente reutilizable con campos dinámicos que se adaptan según el tipo de fuente seleccionada.


1. Crear un Componente Dedicado para el Formulario

1.1. Estructura de Carpetas

Vamos a crear un componente dedicado dentro de la carpeta knowledge sources para mantener el código organizado y reutilizable.

text
resources/js/
├── Components/
│   ├── KnowledgeSources/
│   │   ├── KnowledgeSourceForm.vue  ← Nuevo componente
│   │   └── KnowledgeSourceCreateModal.vue
│   ├── TextInput.vue
│   ├── SelectInput.vue
│   └── ...

1.2. Crear el Componente del Formulario

resources/js/Components/KnowledgeSources/KnowledgeSourceForm.vue

vue
<template>
    <div class="space-y-4">
        <!-- Nombre de la fuente -->
        <div class="col-span-6 sm:col-span-4">
            <InputLabel for="name" value="Nombre" />
            <TextInput
                id="name"
                v-model="form.name"
                type="text"
                class="mt-1 block w-full"
                placeholder="Ej: Manual de Usuario"
                :error="form.errors.name"
            />
            <InputError :message="form.errors.name" class="mt-2" />
        </div>

        <!-- Tipo de fuente -->
        <div class="col-span-6 sm:col-span-4">
            <InputLabel for="type" value="Tipo de Fuente" />
            <SelectInput
                id="type"
                v-model="form.type"
                :options="sourceTypes"
                class="mt-1 block w-full"
                placeholder="Selecciona un tipo"
                :error="form.errors.type"
            />
            <InputError :message="form.errors.type" class="mt-2" />
        </div>

        <!-- Campo PDF (condicional) -->
        <div v-if="form.type === 'pdf'" class="col-span-6 sm:col-span-4">
            <InputLabel for="pdf_file" value="PDF" />
            <TextInput
                id="pdf_file"
                type="file"
                accept=".pdf"
                class="mt-1 block w-full"
                @change="handleFileUpload('pdf', $event)"
                :error="form.errors.pdf_file"
            />
            <p v-if="form.pdf_file" class="mt-1 text-xs text-gray-500">
                Archivo seleccionado: {{ form.pdf_file.name }}
            </p>
            <InputError :message="form.errors.pdf_file" class="mt-2" />
        </div>

        <!-- Campo Website (condicional) -->
        <div v-if="form.type === 'website'" class="col-span-6 sm:col-span-4">
            <InputLabel for="website_url" value="Website" />
            <TextInput
                id="website_url"
                v-model="form.website_url"
                type="url"
                class="mt-1 block w-full"
                placeholder="https://ejemplo.com"
                :error="form.errors.website_url"
            />
            <InputError :message="form.errors.website_url" class="mt-2" />
        </div>

        <!-- Espaciado vertical -->
        <div class="mt-4"></div>
    </div>
</template>

<script setup>
import InputLabel from '@/Components/InputLabel.vue';
import TextInput from '@/Components/TextInput.vue';
import SelectInput from '@/Components/SelectInput.vue';
import InputError from '@/Components/InputError.vue';

// Props
const props = defineProps({
    form: {
        type: Object,
        required: true,
    },
});

// Tipos de fuentes disponibles
const sourceTypes = [
    { value: 'pdf', label: '📄 PDF' },
    { value: 'website', label: '🌐 Website' },
];

// Manejar la subida de archivos
const handleFileUpload = (type, event) => {
    const file = event.target.files[0];
    if (file) {
        const key = `${type}_file`;
        props.form[key] = file;
        
        // Si el nombre está vacío, usar el nombre del archivo
        if (!props.form.name) {
            props.form.name = file.name.replace(/\.[^/.]+$/, '');
        }
    }
};
</script>

2. Integrar el Formulario en el Modal

2.1. Actualizar el Modal

resources/js/Components/KnowledgeSources/KnowledgeSourceCreateModal.vue

vue
<template>
    <DialogModal :show="show" @close="handleClose">
        <!-- Título -->
        <template #title>
            <div class="flex items-center">
                <BookIcon class="w-6 h-6 mr-2 text-indigo-600" />
                <span>Agregar Fuente de Conocimiento</span>
            </div>
        </template>

        <!-- Contenido -->
        <template #content>
            <div class="space-y-4">
                <p class="text-sm text-gray-600 dark:text-gray-400">
                    Agrega una nueva fuente de conocimiento para tu chatbot.
                </p>

                <!-- Formulario reutilizable -->
                <KnowledgeSourceForm :form="form" />
            </div>
        </template>

        <!-- Footer -->
        <template #footer>
            <div class="flex justify-between w-full">
                <SecondaryButton @click="handleClose">
                    Cerrar
                </SecondaryButton>
                <PrimaryButton 
                    @click="handleSubmit"
                    :disabled="form.processing"
                >
                    Guardar
                </PrimaryButton>
            </div>
        </template>
    </DialogModal>
</template>

<script setup>
import DialogModal from '@/Components/DialogModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import BookIcon from '@/Components/Icons/BookIcon.vue';
import KnowledgeSourceForm from './KnowledgeSourceForm.vue';
import { useForm } from '@inertiajs/vue3';
import { watch } from 'vue';

const props = defineProps({
    show: {
        type: Boolean,
        default: false,
    },
    chatbotId: {
        type: String,
        required: true,
    },
});

const emit = defineEmits(['close']);

// Estado del formulario
const form = useForm({
    name: '',
    type: 'pdf',
    pdf_file: null,
    website_url: '',
});

// Manejar el envío
const handleSubmit = () => {
    // Crear FormData para enviar archivos
    const data = new FormData();
    data.append('name', form.name);
    data.append('type', form.type);

    if (form.type === 'pdf' && form.pdf_file) {
        data.append('file', form.pdf_file);
    }

    if (form.type === 'website' && form.website_url) {
        data.append('url', form.website_url);
    }

    form.post(route('chatbots.knowledge-sources.store', props.chatbotId), {
        data: data,
        forceFormData: true,
        preserveScroll: true,
        onSuccess: () => {
            handleClose();
            form.reset();
        },
    });
};

// Cerrar modal
const handleClose = () => {
    emit('close');
};

// Resetear formulario al cerrar
watch(
    () => props.show,
    (newValue) => {
        if (!newValue) {
            form.reset();
        }
    }
);
</script>

3. Definir los Campos del Formulario

3.1. Estructura del Objeto Form

javascript
const form = useForm({
    name: '',           // Nombre de la fuente
    type: 'pdf',        // Tipo: 'pdf' o 'website'
    pdf_file: null,     // Archivo PDF (cuando type='pdf')
    website_url: '',    // URL del website (cuando type='website')
});

3.2. Campos por Tipo

TipoCamposDescripción
PDFpdf_fileArchivo PDF a subir
Websitewebsite_urlURL del website a scrapear

4. Estilos y Layout del Formulario

4.1. Clases CSS Utilizadas

css
/* Grid responsive */
.col-span-6 {
    grid-column: span 6 / span 6;
}

.sm:col-span-4 {
    @media (min-width: 640px) {
        grid-column: span 4 / span 4;
    }
}

/* Espaciado vertical */
.space-y-4 > * + * {
    margin-top: 1rem;
}

.mt-4 {
    margin-top: 1rem;
}

4.2. Estructura del Layout

html
<div class="space-y-4">
    <!-- Grid con 6 columnas -->
    <div class="grid grid-cols-6 gap-4">
        <!-- Cada campo ocupa 6 columnas en móvil y 4 en desktop -->
        <div class="col-span-6 sm:col-span-4">
            <!-- Campo del formulario -->
        </div>
    </div>
</div>

5. Validación del Lado del Servidor

5.1. Actualizar el Request

app/Http/Requests/KnowledgeSourceRequest.php

php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class KnowledgeSourceRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        $rules = [
            'name' => 'required|string|max:255',
            'type' => ['required', Rule::in(['pdf', 'website'])],
        ];

        // Reglas según el tipo
        switch ($this->input('type')) {
            case 'pdf':
                $rules['file'] = 'required|file|mimes:pdf|max:10240';
                break;
            case 'website':
                $rules['url'] = 'required|url|max:2048';
                break;
        }

        return $rules;
    }

    public function messages(): array
    {
        return [
            'name.required' => 'El nombre de la fuente es obligatorio',
            'type.required' => 'El tipo de fuente es obligatorio',
            'type.in' => 'El tipo de fuente seleccionado no es válido',
            'file.required' => 'Debes seleccionar un archivo PDF',
            'file.mimes' => 'El archivo debe ser un PDF',
            'file.max' => 'El PDF no puede ser mayor a 10MB',
            'url.required' => 'La URL del website es obligatoria',
            'url.url' => 'La URL debe ser válida',
        ];
    }
}

6. Implementar el Controlador

6.1. Método Store

app/Http/Controllers/KnowledgeSourceController.php

php
<?php

namespace App\Http\Controllers;

use App\Models\Chatbot;
use App\Models\KnowledgeSource;
use App\Http\Requests\KnowledgeSourceRequest;
use Illuminate\Support\Facades\Storage;

class KnowledgeSourceController extends Controller
{
    public function store(KnowledgeSourceRequest $request, Chatbot $chatbot)
    {
        $this->authorize('update', $chatbot);

        $validated = $request->validated();

        // Preparar datos
        $data = [
            'name' => $validated['name'],
            'type' => $validated['type'],
            'status' => 'pending',
        ];

        // Procesar según el tipo
        if ($validated['type'] === 'pdf') {
            $file = $request->file('file');
            $path = $file->store("knowledge-sources/{$chatbot->id}", 'public');
            $data['path'] = $path;
        }

        if ($validated['type'] === 'website') {
            $data['path'] = $validated['url'];
        }

        // Crear la fuente
        $knowledgeSource = $chatbot->knowledgeSources()->create($data);

        // Disparar job de procesamiento
        // dispatch(new ProcessKnowledgeSource($knowledgeSource));

        return redirect()
            ->route('chatbots.knowledge-sources.index', $chatbot)
            ->with('success', 'Fuente de conocimiento creada exitosamente.');
    }
}

7. Ejemplo de Uso en la Vista Show

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 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"
                >
                    <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">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 text-gray-900">
                        <!-- Contenido del chatbot -->
                    </div>
                </div>
            </div>
        </div>

        <!-- Modal -->
        <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';

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

const showModal = ref(false);

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

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

8. Resumen del Flujo

8.1. Flujo de Datos

  1. Usuario hace clic en "Gestionar Conocimiento"

  2. Modal se abre con el formulario

  3. Usuario selecciona el tipo (PDF o Website)

  4. Formulario muestra los campos correspondientes

  5. Usuario completa los campos

  6. Usuario hace clic en "Guardar"

  7. Validación en el frontend

  8. Envío a través de Inertia con FormData

  9. Validación en el servidor

  10. Archivo se almacena (si es PDF)

  11. Registro se crea en la base de datos

  12. Job se dispara para procesar la fuente

  13. Modal se cierra automáticamente

  14. Mensaje de éxito se muestra al usuario

8.2. Estructura de Archivos

text
resources/js/
├── Components/
│   ├── KnowledgeSources/
│   │   ├── KnowledgeSourceForm.vue      ← Formulario reutilizable
│   │   └── KnowledgeSourceCreateModal.vue ← Modal con el formulario
│   ├── InputLabel.vue
│   ├── TextInput.vue
│   ├── SelectInput.vue
│   └── InputError.vue
├── Pages/
│   └── Chatbots/
│       └── Show.vue                     ← Donde se usa el modal
└── Layouts/
    └── AppLayout.vue

9. Próximos Pasos

9.1. Mejoras Sugeridas

  1. Soporte para más tipos: Agregar CSV, Excel, JSON, Texto

  2. Drag & Drop: Permitir arrastrar archivos

  3. Previsualización: Mostrar contenido del archivo

  4. Progreso de subida: Indicador de progreso

  5. Validación en tiempo real: Validar campos mientras se escriben

9.2. Extensión del Formulario

javascript
// Agregar más tipos
const sourceTypes = [
    { value: 'pdf', label: '📄 PDF' },
    { value: 'website', label: '🌐 Website' },
    { value: 'text', label: '📝 Texto Manual' },
    { value: 'csv', label: '📊 CSV' },
    { value: 'excel', label: '📈 Excel' },
    { value: 'json', label: '🔗 JSON' },
];

// Agregar más campos
const form = useForm({
    name: '',
    type: 'pdf',
    pdf_file: null,
    website_url: '',
    content: '',        // Para tipo 'text'
    csv_file: null,     // Para tipo 'csv'
    excel_file: null,   // Para tipo 'excel'
    json_file: null,    // Para tipo 'json'
});

10. Resumen

En este tutorial hemos completado el formulario para crear fuentes de conocimiento:

✅ Componente KnowledgeSourceForm reutilizable
✅ Campos dinámicos según el tipo seleccionado
✅ SelectInput para elegir el tipo
✅ Manejo de archivos con input de tipo file
✅ Validación en frontend y backend
✅ Integración con modal
✅ Envío de formulario con Inertia y FormData

¡Excelente trabajo! Ahora tienes un formulario funcional y reutilizable para crear 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