25. Formulario para crear fuentes de conocimientos
Tutorial 25: Formulario para Crear Fuentes de Conocimiento - Parte 2
En este tutorial, continuaremos con la implementación del formulario para crear fuentes de conocimiento, agregando los botones de acción, configurando el envío del formulario y estableciendo las rutas necesarias.
1. Agregar los Botones del Modal
1.1. Estructura de los Botones
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> <!-- ... contenido del formulario ... --> </template> <!-- Footer con botones --> <template #footer> <div class="flex justify-between w-full"> <!-- Botón Cerrar (lado izquierdo) --> <SecondaryButton @click="handleClose"> Cerrar </SecondaryButton> <!-- Botón Guardar (lado derecho) --> <PrimaryButton @click="handleSubmit" :disabled="form.processing" > <span v-if="form.processing" class="flex items-center"> <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Guardando... </span> <span v-else>Guardar</span> </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 { useForm } from '@inertiajs/vue3'; import { watch } from 'vue'; // Props 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', description: '', tags: '', // PDF pdf_file: null, // Website website_url: '', // Texto content: '', // CSV csv_file: null, csv_has_header: true, csv_delimiter: ',', // Excel excel_file: null, excel_has_header: true, excel_sheet: '0', // JSON json_file: null, json_is_array: true, }); // Manejar el envío del formulario const handleSubmit = () => { console.log('Datos del formulario:', form.data()); // Aquí se enviará el formulario // form.post(route('chatbots.knowledge-sources.store', props.chatbotId), { // preserveScroll: true, // onSuccess: () => { // handleClose(); // }, // }); }; // Manejar el cierre del modal const handleClose = () => { emit('close'); }; // Resetear el formulario cuando se cierra el modal watch( () => props.show, (newValue) => { if (!newValue) { form.reset(); } } ); </script>
2. Configurar las Rutas
2.1. Definir las Rutas Anidadas
routes/web.php
<?php use App\Http\Controllers\ChatbotController; use App\Http\Controllers\KnowledgeSourceController; use Illuminate\Support\Facades\Route; Route::middleware(['auth'])->group(function () { // Rutas principales de chatbots Route::resource('chatbots', ChatbotController::class); // Rutas anidadas para fuentes de conocimiento Route::prefix('chatbots/{chatbot}')->group(function () { Route::get('/knowledge-sources', [KnowledgeSourceController::class, 'index']) ->name('chatbots.knowledge-sources.index'); Route::get('/knowledge-sources/create', [KnowledgeSourceController::class, 'create']) ->name('chatbots.knowledge-sources.create'); Route::post('/knowledge-sources', [KnowledgeSourceController::class, 'store']) ->name('chatbots.knowledge-sources.store'); Route::get('/knowledge-sources/{knowledgeSource}', [KnowledgeSourceController::class, 'show']) ->name('chatbots.knowledge-sources.show'); Route::get('/knowledge-sources/{knowledgeSource}/edit', [KnowledgeSourceController::class, 'edit']) ->name('chatbots.knowledge-sources.edit'); Route::put('/knowledge-sources/{knowledgeSource}', [KnowledgeSourceController::class, 'update']) ->name('chatbots.knowledge-sources.update'); Route::delete('/knowledge-sources/{knowledgeSource}', [KnowledgeSourceController::class, 'destroy']) ->name('chatbots.knowledge-sources.destroy'); Route::post('/knowledge-sources/{knowledgeSource}/reprocess', [KnowledgeSourceController::class, 'reprocess']) ->name('chatbots.knowledge-sources.reprocess'); }); });
2.2. Verificar las Rutas
php artisan route:list --name=knowledge
Resultado esperado:
GET|HEAD chatbots/{chatbot}/knowledge-sources ....................... chatbots.knowledge-sources.index
GET|HEAD chatbots/{chatbot}/knowledge-sources/create ................ chatbots.knowledge-sources.create
POST chatbots/{chatbot}/knowledge-sources ........................ chatbots.knowledge-sources.store
GET|HEAD chatbots/{chatbot}/knowledge-sources/{knowledgeSource} ..... chatbots.knowledge-sources.show
GET|HEAD chatbots/{chatbot}/knowledge-sources/{knowledgeSource}/edit chatbots.knowledge-sources.edit
PUT|PATCH chatbots/{chatbot}/knowledge-sources/{knowledgeSource} ..... chatbots.knowledge-sources.update
DELETE chatbots/{chatbot}/knowledge-sources/{knowledgeSource} ..... chatbots.knowledge-sources.destroy
POST chatbots/{chatbot}/knowledge-sources/{knowledgeSource}/reprocess chatbots.knowledge-sources.reprocess3. Configurar el Controlador
3.1. Actualizar el Controlador
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\Http\Request; use Illuminate\Support\Facades\Storage; use Inertia\Inertia; class KnowledgeSourceController extends Controller { /** * Muestra la lista de fuentes de conocimiento. */ public function index(Chatbot $chatbot) { $this->authorize('view', $chatbot); $knowledgeSources = $chatbot->knowledgeSources() ->orderBy('created_at', 'desc') ->paginate(10); return Inertia::render('KnowledgeSources/Index', [ 'chatbot' => $chatbot, 'knowledgeSources' => $knowledgeSources, ]); } /** * Muestra el formulario para crear una nueva fuente. */ public function create(Chatbot $chatbot) { $this->authorize('update', $chatbot); return Inertia::render('KnowledgeSources/Create', [ 'chatbot' => $chatbot, ]); } /** * Almacena una nueva fuente de conocimiento. */ public function store(KnowledgeSourceRequest $request, Chatbot $chatbot) { $this->authorize('update', $chatbot); $validated = $request->validated(); // Procesar archivo si existe $filePath = null; if ($request->hasFile('file')) { $filePath = $this->uploadFile($request->file('file'), $chatbot); } // Preparar datos para la base de datos $data = [ 'name' => $validated['name'], 'type' => $validated['type'], 'description' => $validated['description'] ?? null, 'tags' => $validated['tags'] ?? null, 'path' => $filePath, 'status' => 'pending', 'metadata' => $this->prepareMetadata($validated), ]; // Para tipo texto, el contenido ya está disponible if ($validated['type'] === 'text') { $data['extracted_content'] = $validated['content']; $data['status'] = 'processed'; } // Para website, guardamos la URL if ($validated['type'] === 'website') { $data['path'] = $validated['url']; } // Crear la fuente de conocimiento $knowledgeSource = $chatbot->knowledgeSources()->create($data); // Si no es texto, disparar job de procesamiento if ($validated['type'] !== 'text') { // dispatch(new ProcessKnowledgeSource($knowledgeSource)); } return redirect() ->route('chatbots.knowledge-sources.index', $chatbot) ->with('success', 'Fuente de conocimiento creada exitosamente.'); } /** * Muestra una fuente de conocimiento específica. */ public function show(Chatbot $chatbot, KnowledgeSource $knowledgeSource) { $this->authorize('view', $chatbot); return Inertia::render('KnowledgeSources/Show', [ 'chatbot' => $chatbot, 'knowledgeSource' => $knowledgeSource, ]); } /** * Muestra el formulario para editar una fuente. */ public function edit(Chatbot $chatbot, KnowledgeSource $knowledgeSource) { $this->authorize('update', $chatbot); return Inertia::render('KnowledgeSources/Edit', [ 'chatbot' => $chatbot, 'knowledgeSource' => $knowledgeSource, ]); } /** * Actualiza una fuente de conocimiento. */ public function update(KnowledgeSourceRequest $request, Chatbot $chatbot, KnowledgeSource $knowledgeSource) { $this->authorize('update', $chatbot); $validated = $request->validated(); $knowledgeSource->update([ 'name' => $validated['name'], 'description' => $validated['description'] ?? $knowledgeSource->description, 'tags' => $validated['tags'] ?? $knowledgeSource->tags, ]); return redirect() ->route('chatbots.knowledge-sources.index', $chatbot) ->with('success', 'Fuente de conocimiento actualizada exitosamente.'); } /** * Elimina una fuente de conocimiento. */ public function destroy(Chatbot $chatbot, KnowledgeSource $knowledgeSource) { $this->authorize('delete', $chatbot); // Eliminar archivo si existe if ($knowledgeSource->path && Storage::exists($knowledgeSource->path)) { Storage::delete($knowledgeSource->path); } $knowledgeSource->delete(); return redirect() ->route('chatbots.knowledge-sources.index', $chatbot) ->with('success', 'Fuente de conocimiento eliminada exitosamente.'); } /** * Reprocesa una fuente de conocimiento. */ public function reprocess(Chatbot $chatbot, KnowledgeSource $knowledgeSource) { $this->authorize('update', $chatbot); $knowledgeSource->update([ 'status' => 'pending', 'extracted_content' => null, ]); // dispatch(new ProcessKnowledgeSource($knowledgeSource)); return back()->with('success', 'La fuente de conocimiento está siendo reprocesada.'); } /** * Sube un archivo al almacenamiento. */ private function uploadFile($file, Chatbot $chatbot): string { $path = $file->store("knowledge-sources/{$chatbot->id}", 'public'); return $path; } /** * Prepara los metadatos según el tipo de fuente. */ private function prepareMetadata(array $data): array { $metadata = []; switch ($data['type']) { case 'pdf': // No hay metadatos adicionales por ahora break; case 'website': $metadata['url'] = $data['url']; break; case 'csv': $metadata['has_header'] = $data['has_header'] ?? true; $metadata['delimiter'] = $data['delimiter'] ?? ','; break; case 'excel': $metadata['has_header'] = $data['has_header'] ?? true; $metadata['sheet'] = $data['sheet'] ?? 0; break; case 'json': $metadata['is_array'] = $data['is_array'] ?? true; break; case 'text': $metadata['char_count'] = strlen($data['content'] ?? ''); $metadata['word_count'] = str_word_count($data['content'] ?? ''); break; } return $metadata; } }
4. Actualizar el Request de Validación
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; // Cambiar a true para permitir la autorización } public function rules(): array { $rules = [ 'name' => 'required|string|max:255', 'type' => ['required', Rule::in(['pdf', 'website', 'text', 'csv', 'excel', 'json'])], 'description' => 'nullable|string|max:1000', 'tags' => 'nullable|string|max:255', ]; // 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; case 'text': $rules['content'] = 'required|string|min:10|max:50000'; break; case 'csv': $rules['file'] = 'required|file|mimes:csv,txt|max:5120'; $rules['has_header'] = 'sometimes|boolean'; $rules['delimiter'] = ['sometimes', 'string', Rule::in([',', ';', "\t"])]; break; case 'excel': $rules['file'] = 'required|file|mimes:xlsx,xls|max:5120'; $rules['has_header'] = 'sometimes|boolean'; $rules['sheet'] = 'sometimes|integer|min:0'; break; case 'json': $rules['file'] = 'required|file|mimes:json|max:5120'; $rules['is_array'] = 'sometimes|boolean'; 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', 'file.file' => 'El campo debe ser un archivo válido', 'file.mimes' => 'El archivo debe ser de tipo :values', 'file.max' => 'El archivo no puede ser mayor a :max KB', 'url.required' => 'La URL del website es obligatoria', 'url.url' => 'La URL debe ser válida', 'content.required' => 'El contenido es obligatorio', 'content.min' => 'El contenido debe tener al menos 10 caracteres', 'content.max' => 'El contenido no puede tener más de 50000 caracteres', ]; } }
5. Implementar el Envío del Formulario
5.1. Actualizar el Modal con Envío Funcional
resources/js/Components/KnowledgeSources/KnowledgeSourceCreateModal.vue
<template> <DialogModal :show="show" @close="handleClose"> <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> <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. El contenido será procesado para mejorar las respuestas del chatbot. </p> <!-- Formulario reutilizable --> <KnowledgeSourceForm :form="form" /> </div> </template> <template #footer> <div class="flex justify-between w-full"> <SecondaryButton @click="handleClose"> Cerrar </SecondaryButton> <PrimaryButton @click="handleSubmit" :disabled="form.processing" > <span v-if="form.processing" class="flex items-center"> <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Guardando... </span> <span v-else>Guardar</span> </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', description: '', tags: '', // PDF pdf_file: null, // Website website_url: '', // Texto content: '', // CSV csv_file: null, csv_has_header: true, csv_delimiter: ',', // Excel excel_file: null, excel_has_header: true, excel_sheet: '0', // JSON json_file: null, json_is_array: true, }); // Manejar el envío del formulario const handleSubmit = () => { // Crear FormData para enviar archivos const data = new FormData(); // Campos comunes data.append('name', form.name); data.append('type', form.type); data.append('description', form.description); data.append('tags', form.tags); // Campos según el tipo switch (form.type) { case 'pdf': if (form.pdf_file) { data.append('file', form.pdf_file); } break; case 'website': data.append('url', form.website_url); break; case 'text': data.append('content', form.content); break; case 'csv': if (form.csv_file) { data.append('file', form.csv_file); data.append('has_header', form.csv_has_header ? '1' : '0'); data.append('delimiter', form.csv_delimiter); } break; case 'excel': if (form.excel_file) { data.append('file', form.excel_file); data.append('has_header', form.excel_has_header ? '1' : '0'); data.append('sheet', form.excel_sheet); } break; case 'json': if (form.json_file) { data.append('file', form.json_file); data.append('is_array', form.json_is_array ? '1' : '0'); } break; } // Enviar el formulario form.post(route('chatbots.knowledge-sources.store', props.chatbotId), { data: data, forceFormData: true, preserveScroll: true, onSuccess: () => { // Cerrar el modal en caso de éxito handleClose(); form.reset(); }, onError: (errors) => { console.error('Errores de validación:', errors); // Mostrar mensaje de error if (Object.keys(errors).length > 0) { const firstError = Object.values(errors)[0]; alert(firstError); } }, }); }; // Manejar el cierre del modal const handleClose = () => { emit('close'); }; // Resetear el formulario cuando se cierra el modal watch( () => props.show, (newValue) => { if (!newValue) { form.reset(); } } ); </script>
6. Verificar la Autorización
6.1. Crear Política de Autorización
app/Policies/KnowledgeSourcePolicy.php
<?php namespace App\Policies; use App\Models\User; use App\Models\KnowledgeSource; class KnowledgeSourcePolicy { public function view(User $user, KnowledgeSource $knowledgeSource): bool { return $user->id === $knowledgeSource->chatbot->user_id; } public function create(User $user, $chatbotId): bool { // Verificar que el usuario sea dueño del chatbot return $user->chatbots()->where('id', $chatbotId)->exists(); } public function update(User $user, KnowledgeSource $knowledgeSource): bool { return $user->id === $knowledgeSource->chatbot->user_id; } public function delete(User $user, KnowledgeSource $knowledgeSource): bool { return $user->id === $knowledgeSource->chatbot->user_id; } }
6.2. Registrar la Política
app/Providers/AuthServiceProvider.php
<?php namespace App\Providers; use App\Models\KnowledgeSource; use App\Policies\KnowledgeSourcePolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { protected $policies = [ KnowledgeSource::class => KnowledgeSourcePolicy::class, ]; public function boot(): void { $this->registerPolicies(); } }
7. Resumen del Flujo Complet
7.1. Flujo de Usuario
Usuario navega a la página de detalles del chatbot
Usuario hace clic en "Gestionar Conocimiento"
Modal se abre con el formulario
Usuario selecciona el tipo de fuente
Usuario completa los campos según el tipo
Usuario hace clic en "Guardar"
Formulario se envía mediante Inertia
Validación en el servidor
Archivo se procesa y almacena
Modal se cierra automáticamente
Mensaje de éxito se muestra al usuario
7.2. Estructura de Datos
KnowledgeSource ├── id (UUID) ├── chatbot_id (UUID) ├── name (string) ├── type (enum: pdf, website, text, csv, excel, json) ├── description (string, nullable) ├── tags (string, nullable) ├── path (string, nullable) - Ruta del archivo o URL ├── extracted_content (longText, nullable) ├── status (enum: pending, processing, processed, failed) ├── metadata (json, nullable) ├── created_at (timestamp) └── updated_at (timestamp)
8. Próximos Pasos
Procesamiento en Background: Implementar Jobs para procesar archivos y URLs
Extracción de Contenido: Implementar la lógica de extracción para cada tipo
Embeddings: Generar embeddings del contenido para búsqueda semántica
Interfaz de Usuario: Mostrar el progreso del procesamiento
Edición: Implementar la edición de fuentes existentes
Búsqueda: Buscar dentro del contenido de las fuentes
9. Resumen
En este tutorial hemos completado la implementación del formulario para crear fuentes de conocimiento:
✅ Botones de acción con diseño consistente
✅ Envío del formulario con Inertia
✅ Configuración de rutas anidadas
✅ Controlador con todas las operaciones CRUD
✅ Validación completa del lado del servidor
✅ Manejo de archivos y FormData
✅ Cierre automático del modal tras éxito
✅ Políticas de autorización
¡Excelente trabajo! Ahora tienes un sistema completo para gestionar fuentes de conocimiento para tus chatbots
Comentarios
Publicar un comentario