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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user