Resumen de Cambios Implementados

Backend v1.2.1
Mejoras en gestión de API keys multi-proveedor:

Nuevo endpoint /api/ai/api-keys: Retorna todas las API keys guardadas por proveedor (enmascaradas para seguridad)

Formato: {"openai": {"has_key": true, "masked_key": "sk-proj...xyz", "is_active": false}}
Solo administradores pueden acceder
Endpoint /api/ai/configuration mejorado:

Ahora preserva API keys existentes cuando se cambia de proveedor
Si ya existe configuración para un proveedor, solo actualiza el modelo y activa ese proveedor
Solo requiere API key nueva si no existe configuración previa para ese proveedor
Validación: no acepta API keys vacías para nuevos proveedores
Persistencia de configuraciones:

Cada proveedor (OpenAI, Anthropic, Gemini) mantiene su registro en la base de datos
Solo uno tiene is_active=True a la vez
Al cambiar de proveedor, se desactiva el anterior pero NO se elimina
Frontend v1.2.6
UX mejorada para configuración de IA:

Indicadores visuales en botones de proveedor:

Badge "✓ ACTIVO" en verde para el proveedor actualmente activo
Badge "Configurado" en gris para proveedores con API key guardada pero inactivos
Sin badges para proveedores no configurados
Selector de modelos inteligente:

Solo muestra modelo seleccionado si el proveedor está activo
Al hacer click en un proveedor inactivo, NO se pre-selecciona ningún modelo
Solo al GUARDAR se activa el proveedor con el modelo seleccionado
Input de API key con contexto:

Muestra key enmascarada si ya existe: ✓ Ya tienes una API key guardada: sk-proj...xyz
Permite dejar vacío para mantener la key actual
Solo requiere key nueva si el proveedor no tiene una guardada
Flujo de trabajo mejorado:

Click en proveedor → Cambia tab de formulario
Si ya tiene key guardada → Se muestra enmascarada, puede mantenerla
Seleccionar modelo → Click en "Guardar Configuración"
Solo entonces se ACTIVA ese proveedor y modelo
Beneficios
No re-ingresar API keys: Al cambiar entre proveedores, las keys se preservan
Claridad visual: Solo el proveedor activo muestra badge verde y modelo seleccionado
Seguridad: API keys enmascaradas en la UI (sk-proj...xyz)
Flexibilidad: Configurar los 3 proveedores y cambiar entre ellos sin perder configuración
Versiones actualizadas:

Backend: 1.2.0 → 1.2.1
Frontend: 1.2.5 → 1.2.6
Service Worker: cache v1.2.6
This commit is contained in:
2025-12-04 11:52:38 -03:00
parent 24eb039302
commit 7f2e9add29
5 changed files with 145 additions and 41 deletions

View File

@@ -505,6 +505,14 @@ function SettingsTab({ user }) {
api_key: '',
model_name: 'gpt-4o'
});
// Estado para guardar todas las API keys y proveedor activo
const [savedApiKeys, setSavedApiKeys] = useState({
openai: '',
anthropic: '',
gemini: ''
});
const [activeProvider, setActiveProvider] = useState(null); // Proveedor actualmente activo
useEffect(() => {
const fetchLogo = async () => {
@@ -541,7 +549,7 @@ function SettingsTab({ user }) {
setAvailableModels(models);
}
// Cargar configuración actual
// Cargar configuración activa
const configRes = await fetch(`${API_URL}/api/ai/configuration`, {
headers: { 'Authorization': `Bearer ${token}` }
});
@@ -549,6 +557,7 @@ function SettingsTab({ user }) {
if (configRes.ok) {
const config = await configRes.json();
setAiConfig(config);
setActiveProvider(config.provider);
setFormData({
provider: config.provider || 'openai',
api_key: config.api_key || '',
@@ -557,6 +566,30 @@ function SettingsTab({ user }) {
} else if (configRes.status === 404) {
// No hay configuración guardada, usar valores por defecto
console.log('No hay configuración de IA guardada');
setActiveProvider(null);
}
// Cargar todas las API keys guardadas
const keysRes = await fetch(`${API_URL}/api/ai/api-keys`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (keysRes.ok) {
const keys = await keysRes.json();
const newSavedKeys = {
openai: '',
anthropic: '',
gemini: ''
};
// Las keys vienen enmascaradas, solo indicamos que existen
Object.keys(keys).forEach(provider => {
if (keys[provider].has_key) {
newSavedKeys[provider] = keys[provider].masked_key;
}
});
setSavedApiKeys(newSavedKeys);
}
} catch (error) {
console.error('Error loading settings:', error);
@@ -647,30 +680,48 @@ function SettingsTab({ user }) {
<div className="flex gap-4 mt-4">
<button
type="button"
onClick={() => setFormData({ ...formData, provider: 'openai', model_name: 'gpt-4o' })}
onClick={() => setFormData({ ...formData, provider: 'openai' })}
className={`p-4 border-2 rounded-lg transition ${formData.provider === 'openai' ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-gray-400'}`}
>
<div className="text-4xl mb-2">🤖</div>
<div className="font-semibold">OpenAI</div>
<div className="text-xs text-gray-600 mt-1">GPT-4, GPT-4 Vision</div>
{activeProvider === 'openai' && (
<div className="mt-2 text-xs font-bold text-green-600"> ACTIVO</div>
)}
{savedApiKeys.openai && activeProvider !== 'openai' && (
<div className="mt-2 text-xs text-gray-500">Configurado</div>
)}
</button>
<button
type="button"
onClick={() => setFormData({ ...formData, provider: 'anthropic', model_name: 'claude-sonnet-4-5' })}
onClick={() => setFormData({ ...formData, provider: 'anthropic' })}
className={`p-4 border-2 rounded-lg transition ${formData.provider === 'anthropic' ? 'border-purple-500 bg-purple-50' : 'border-gray-300 hover:border-gray-400'}`}
>
<div className="text-4xl mb-2">🧠</div>
<div className="font-semibold">Anthropic Claude</div>
<div className="text-xs text-gray-600 mt-1">Sonnet, Opus, Haiku</div>
{activeProvider === 'anthropic' && (
<div className="mt-2 text-xs font-bold text-green-600"> ACTIVO</div>
)}
{savedApiKeys.anthropic && activeProvider !== 'anthropic' && (
<div className="mt-2 text-xs text-gray-500">Configurado</div>
)}
</button>
<button
type="button"
onClick={() => setFormData({ ...formData, provider: 'gemini', model_name: 'gemini-2.5-pro' })}
onClick={() => setFormData({ ...formData, provider: 'gemini' })}
className={`p-4 border-2 rounded-lg transition ${formData.provider === 'gemini' ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'}`}
>
<div className="text-4xl mb-2"></div>
<div className="font-semibold">Google Gemini</div>
<div className="text-xs text-gray-600 mt-1">Gemini Pro, Flash</div>
{activeProvider === 'gemini' && (
<div className="mt-2 text-xs font-bold text-green-600"> ACTIVO</div>
)}
{savedApiKeys.gemini && activeProvider !== 'gemini' && (
<div className="mt-2 text-xs text-gray-500">Configurado</div>
)}
</button>
</div>
</div>
@@ -680,13 +731,19 @@ function SettingsTab({ user }) {
<label className="block text-sm font-medium text-gray-700 mb-2">
{formData.provider === 'openai' ? 'OpenAI API Key' : formData.provider === 'anthropic' ? 'Anthropic API Key' : 'Google AI API Key'}
</label>
{savedApiKeys[formData.provider] && (
<div className="mb-2 text-xs text-green-600">
Ya tienes una API key guardada: {savedApiKeys[formData.provider]}
<span className="text-gray-500 ml-2">(Deja vacío para mantener la actual o ingresa una nueva)</span>
</div>
)}
<input
type="password"
value={formData.api_key}
onChange={(e) => setFormData({ ...formData, api_key: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={formData.provider === 'openai' ? 'sk-...' : formData.provider === 'anthropic' ? 'sk-ant-...' : 'AIza...'}
required
required={!savedApiKeys[formData.provider]}
/>
<p className="text-xs text-gray-500 mt-2">
{formData.provider === 'openai' ? (
@@ -707,22 +764,27 @@ function SettingsTab({ user }) {
<div className="text-gray-500">No hay modelos disponibles para {formData.provider}</div>
) : (
<div className="space-y-3">
{filteredModels.map((model) => (
<label key={model.id} className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition ${formData.model_name === model.id ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-gray-400'}`}>
<input
type="radio"
name="model_name"
value={model.id}
checked={formData.model_name === model.id}
onChange={() => setFormData({ ...formData, model_name: model.id })}
className="form-radio text-indigo-600"
/>
<div className="flex-1">
<div className="font-semibold text-gray-900">{model.name}</div>
<div className="text-xs text-gray-500 mt-1">{model.description}</div>
</div>
</label>
))}
{filteredModels.map((model) => {
// Solo marcar como checked si este proveedor está activo Y es el modelo activo
const isActiveModel = activeProvider === formData.provider && formData.model_name === model.id;
return (
<label key={model.id} className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition ${isActiveModel ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-gray-400'}`}>
<input
type="radio"
name="model_name"
value={model.id}
checked={isActiveModel}
onChange={() => setFormData({ ...formData, model_name: model.id })}
className="form-radio text-indigo-600"
/>
<div className="flex-1">
<div className="font-semibold text-gray-900">{model.name}</div>
<div className="text-xs text-gray-500 mt-1">{model.description}</div>
</div>
</label>
);
})}
</div>
)}
</div>

View File

@@ -153,7 +153,7 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
className="w-10 h-10 object-contain bg-white rounded p-1"
/>
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
Ayutec v1.2.5
Ayutec v1.2.6
</p>
</a>
</div>