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.
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
<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
<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
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
| Tipo | Campos | Descripción |
|---|---|---|
pdf_file | Archivo PDF a subir | |
| Website | website_url | URL del website a scrapear |
4. Estilos y Layout del Formulario
4.1. Clases CSS Utilizadas
/* 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
<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 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 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
<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
Usuario hace clic en "Gestionar Conocimiento"
Modal se abre con el formulario
Usuario selecciona el tipo (PDF o Website)
Formulario muestra los campos correspondientes
Usuario completa los campos
Usuario hace clic en "Guardar"
Validación en el frontend
Envío a través de Inertia con FormData
Validación en el servidor
Archivo se almacena (si es PDF)
Registro se crea en la base de datos
Job se dispara para procesar la fuente
Modal se cierra automáticamente
Mensaje de éxito se muestra al usuario
8.2. Estructura de Archivos
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.vue9. Próximos Pasos
9.1. Mejoras Sugeridas
Soporte para más tipos: Agregar CSV, Excel, JSON, Texto
Drag & Drop: Permitir arrastrar archivos
Previsualización: Mostrar contenido del archivo
Progreso de subida: Indicador de progreso
Validación en tiempo real: Validar campos mientras se escriben
9.2. Extensión del Formulario
// 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
Publicar un comentario