Integración de un Asistente de IA (Generar, Editar y Guardar) en un Stack PHP/MySQL/JS
El propósito de esta implementación es añadir una funcionalidad de "Asistente de IA" a una tarjeta de contenido existente. El flujo completo permite al usuario:
Flujo de Datos Clave: El texto para análisis se extrae de la columna desc_todas
. La sugerencia de la IA, una vez editada y aprobada, se guarda en la columna descripcion
.
Insertar un botón en la interfaz que iniciará todo el proceso. Este botón debe contener toda la información necesaria para las operaciones posteriores mediante atributos data-*
.
En el archivo carga_temas_subtemas_ver_ia.php
, dentro del bucle while
que genera las tarjetas, se modifica el `div.card-footer` de la tarjeta "Descripción General" para incluir el siguiente botón:
<!-- Botón que inicia el flujo de IA. Contiene toda la metadata necesaria. -->
<button
class="btn btn-outline-info btn-sm btn-ia-desc"
data-target-id="desc-<?= $id_counter ?>"
data-record-id="<?= $tema['id'] ?>"
data-column-name="descripcion">
IA
</button>
class="btn-ia-desc"
: Un selector CSS único para que JavaScript pueda identificar este botón específico.data-target-id
: Apunta al ID del elemento <p>
que contiene el texto fuente (de la columna desc_todas
).data-record-id
: Almacena la clave primaria (id
) del registro en la tabla temas_curso
.data-column-name
: Especifica la columna de destino para la operación de guardado (descripcion
).Crear un marcador de posición (placeholder) en el DOM donde JavaScript inyectará dinámicamente el formulario de edición (<textarea>
y botón "Grabar").
Justo después del cierre de la tarjeta (</div><!-- FIN DE LA TARJETA -->
), se añade el siguiente div:
<!-- Contenedor vacío que será poblado por JavaScript con el formulario de edición. -->
<div id="response-container-desc-<?= $id_counter ?>" class="ia-response-container mt-3"></div>
avi/chat_ia_desc.php
)
Crear un "agente" de IA. Este script recibe el texto fuente, se comunica con la API de Google Gemini para obtener una sugerencia de mejora y devuelve el resultado al frontend.
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
include("../include/conecta_mysql_otec.php");
define('GOOGLE_AI_API_KEY', 'TU_API_KEY_AQUI');
define('GOOGLE_AI_API_URL', 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' . GOOGLE_AI_API_KEY);
function log_message($message) { /* ... función de log ... */ }
log_message("-> INICIO EJECUCIÓN: chat_ia_desc.php (Conexión a Gemini)");
$response = [ 'success' => false, 'data' => null, 'message' => 'Error' ];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$post_data = file_get_contents('php://input');
$request = json_decode($post_data, true);
if (isset($request['text']) && !empty($request['text'])) {
$texto_a_analizar = $request['text'];
log_message("Texto recibido para analizar: " . substr($texto_a_analizar, 0, 80) . "...");
$system_prompt = "Eres un asistente experto en diseño instruccional...";
$payload = [ /* ... construcción del payload para Gemini ... */ ];
log_message("Llamando a la API de Gemini...");
$ch = curl_init(GOOGLE_AI_API_URL);
curl_setopt_array($ch, [ /* ... opciones de cURL ... */ ]);
$api_response_raw = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
log_message("Respuesta de la API recibida. HTTP Code: " . $http_code);
if ($http_code == 200) {
$api_response = json_decode($api_response_raw, true);
if (isset($api_response['candidates'][0]['content']['parts'][0]['text'])) {
$response['success'] = true;
$response['data'] = nl2br(htmlspecialchars(trim($api_response['candidates'][0]['content']['parts'][0]['text'])));
$response['message'] = 'Análisis de IA completado.';
} else { /* ... manejo de error de API (ej. safety) ... */ }
} else { /* ... manejo de error de conexión cURL ... */ }
}
}
echo json_encode($response);
exit;
?>
guardar_desc.php
)
Gestionar la persistencia de los datos. Este script recibe el texto final del frontend y lo actualiza en la base de datos de forma segura. Es un script dedicado y no genérico.
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
include("include/conecta_mysql_otec.php");
function log_message($message) { /* ... función de log ... */ }
log_message("-> INICIO GUARDADO: guardar_desc.php");
$response = [ 'success' => false, 'message' => 'Solicitud no válida.' ];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$post_data = file_get_contents('php://input');
$request = json_decode($post_data, true);
if (isset($request['record_id']) && isset($request['text_content'])) {
$record_id = filter_var($request['record_id'], FILTER_VALIDATE_INT);
$text_content = $request['text_content'];
// Medida de Seguridad 1: El nombre de la columna está escrito directamente (hardcoded).
// No se puede manipular desde el frontend.
$column_name = 'descripcion';
if ($record_id) {
// Medida de Seguridad 2: Se utiliza una consulta preparada.
$sql = "UPDATE temas_curso SET {$column_name} = ? WHERE id = ?";
if ($stmt = $mysqli->prepare($sql)) {
$stmt->bind_param("si", $text_content, $record_id);
if ($stmt->execute()) { /* ... manejo de éxito ... */ }
$stmt->close();
} else { /* ... manejo de error de preparación ... */ }
}
}
}
echo json_encode($response);
exit;
?>
Manejar el evento de clic en el botón "IA", llamar al agente de IA y renderizar el formulario de edición con la respuesta.
document.addEventListener('DOMContentLoaded', function() {
// ... otros listeners ...
const botonIaDesc = document.querySelector('.btn-ia-desc');
if (botonIaDesc) {
botonIaDesc.addEventListener('click', function(event) {
// ... Recolectar datos de los atributos data-* ...
// ... Mostrar mensaje de "Cargando..." ...
fetch('avi/chat_ia_desc.php', { /* ... opciones de fetch ... */ })
.then(response => response.json())
.then(data => {
if (data.success) {
// Lógica para crear dinámicamente el HTML del formulario
const formHtml = `
<div class="border p-3 rounded bg-light">
<label for="ia-textarea-...">Sugerencia de la IA (editable):</label>
<textarea id="ia-textarea-..." class="form-control">${data.data}</textarea>
<button class="btn btn-primary btn-sm btn-save-ia" ...>Grabar Cambios</button>
<div id="save-status-..."></div>
</div>`;
responseContainer.innerHTML = formHtml;
}
});
});
}
});
Manejar el clic en el botón "Grabar", que fue creado dinámicamente. Para esto, es imprescindible usar la técnica de **delegación de eventos**.
Se añade un listener al contenedor principal (un elemento estático), que intercepta todos los clics y actúa solo si el objetivo es un botón de guardado.
// Dentro de document.addEventListener('DOMContentLoaded', function() { ... });
const mainContainer = document.querySelector('main.container');
if (mainContainer) {
mainContainer.addEventListener('click', function(event) {
// Se verifica si el objetivo del clic tiene la clase '.btn-save-ia'
if (event.target.classList.contains('btn-save-ia')) {
event.preventDefault();
const saveButton = event.target;
saveButton.disabled = true; // Prevenir doble clic
// ... Recolectar datos del botón y del textarea ...
// ... Mostrar mensaje "Guardando..." ...
fetch('guardar_desc.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ /* ... datos a enviar ... */ })
})
.then(response => response.json())
.then(data => {
// ... Mostrar mensaje de éxito o error del backend ...
})
.catch(error => {
// ... Manejar errores de conexión ...
})
.finally(() => {
// Opcionalmente, reactivar el botón
saveButton.disabled = false;
});
}
});
}