Logo feature 1.0.27r

This commit is contained in:
2025-11-25 05:55:45 -03:00
parent 33b134e838
commit 14073db2d9
2 changed files with 248 additions and 206 deletions

View File

@@ -1,3 +1,46 @@
# ============= LOGO CONFIGURABLE =============
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/api/config/logo", response_model=dict)
async def upload_logo(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Sube un logo y lo guarda en MinIO, actualiza la configuración."""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden cambiar el logo")
# Subir imagen a MinIO
file_extension = file.filename.split(".")[-1]
now = datetime.now()
folder = f"logo"
file_name = f"logo_{now.strftime('%Y%m%d_%H%M%S')}.{file_extension}"
s3_key = f"{folder}/{file_name}"
s3_client.upload_fileobj(file.file, S3_IMAGE_BUCKET, s3_key, ExtraArgs={"ContentType": file.content_type})
logo_url = f"{S3_ENDPOINT}/{S3_IMAGE_BUCKET}/{s3_key}"
# Guardar en configuración (puedes tener una tabla Config o usar AIConfiguration)
config = db.query(models.AIConfiguration).filter(models.AIConfiguration.is_active == True).first()
if config:
config.logo_url = logo_url
db.commit()
db.refresh(config)
# Si no hay config, solo retorna la url
return {"logo_url": logo_url}
# Endpoint para obtener el logo
@app.get("/api/config/logo", response_model=dict)
def get_logo_url(
db: Session = Depends(get_db)
):
config = db.query(models.AIConfiguration).filter(models.AIConfiguration.is_active == True).first()
if config and getattr(config, "logo_url", None):
return {"logo_url": config.logo_url}
# Default logo (puedes poner una url por defecto)
return {"logo_url": f"{S3_ENDPOINT}/{S3_IMAGE_BUCKET}/logo/default_logo.png"}
from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

View File

@@ -45,6 +45,20 @@ function LoginPage({ setUser }) {
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [logoUrl, setLogoUrl] = useState(null);
useEffect(() => {
const fetchLogo = async () => {
try {
const API_URL = import.meta.env.VITE_API_URL || '';
const res = await fetch(`${API_URL}/api/config/logo`);
if (res.ok) {
const data = await res.json();
setLogoUrl(data.logo_url);
}
} catch {}
};
fetchLogo();
}, []);
const handleLogin = async (e) => { const handleLogin = async (e) => {
e.preventDefault() e.preventDefault()
@@ -86,22 +100,11 @@ function LoginPage({ setUser }) {
{/* Header con Logo */} {/* Header con Logo */}
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 px-8 py-10 text-center"> <div className="bg-gradient-to-r from-indigo-600 to-purple-600 px-8 py-10 text-center">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
{/* Logo S de Syntria */} {logoUrl ? (
<div className="w-24 h-24 bg-white rounded-2xl flex items-center justify-center shadow-lg transform hover:scale-105 transition-transform"> <img src={logoUrl} alt="Logo" className="w-24 h-24 object-contain bg-white rounded-2xl shadow-lg" />
<svg viewBox="0 0 100 100" className="w-16 h-16"> ) : (
<path <div className="w-24 h-24 bg-white rounded-2xl flex items-center justify-center shadow-lg text-gray-400">Sin logo</div>
d="M 30 25 Q 20 25 20 35 Q 20 45 30 45 L 50 45 Q 60 45 60 55 Q 60 65 50 65 L 30 65 Q 20 65 20 75 Q 20 85 30 85 L 70 85 Q 80 85 80 75 Q 80 65 70 65 L 50 65 Q 40 65 40 55 Q 40 45 50 45 L 70 45 Q 80 45 80 35 Q 80 25 70 25 Z" )}
fill="url(#gradient)"
stroke="none"
/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: '#6366f1', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: '#a855f7', stopOpacity: 1 }} />
</linearGradient>
</defs>
</svg>
</div>
</div> </div>
<h1 className="text-4xl font-bold text-white mb-2">Syntria</h1> <h1 className="text-4xl font-bold text-white mb-2">Syntria</h1>
<p className="text-indigo-100 text-sm">Sistema Inteligente de Inspecciones</p> <p className="text-indigo-100 text-sm">Sistema Inteligente de Inspecciones</p>
@@ -174,6 +177,20 @@ function DashboardPage({ user, setUser }) {
const [activeTab, setActiveTab] = useState('checklists') const [activeTab, setActiveTab] = useState('checklists')
const [activeInspection, setActiveInspection] = useState(null) const [activeInspection, setActiveInspection] = useState(null)
const [sidebarOpen, setSidebarOpen] = useState(true) const [sidebarOpen, setSidebarOpen] = useState(true)
const [logoUrl, setLogoUrl] = useState(null);
useEffect(() => {
const fetchLogo = async () => {
try {
const API_URL = import.meta.env.VITE_API_URL || '';
const res = await fetch(`${API_URL}/api/config/logo`);
if (res.ok) {
const data = await res.json();
setLogoUrl(data.logo_url);
}
} catch {}
};
fetchLogo();
}, []);
useEffect(() => { useEffect(() => {
loadData() loadData()
@@ -273,21 +290,11 @@ function DashboardPage({ user, setUser }) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Logo y Nombre del Sistema */} {/* Logo y Nombre del Sistema */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center shadow-lg"> {logoUrl ? (
<svg viewBox="0 0 100 100" className="w-8 h-8"> <img src={logoUrl} alt="Logo" className="w-12 h-12 object-contain bg-white rounded-xl shadow-lg" />
<path ) : (
d="M 30 25 Q 20 25 20 35 Q 20 45 30 45 L 50 45 Q 60 45 60 55 Q 60 65 50 65 L 30 65 Q 20 65 20 75 Q 20 85 30 85 L 70 85 Q 80 85 80 75 Q 80 65 70 65 L 50 65 Q 40 65 40 55 Q 40 45 50 45 L 70 45 Q 80 45 80 35 Q 80 25 70 25 Z" <div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center shadow-lg text-gray-400">Sin logo</div>
fill="url(#headerGradient)" )}
stroke="none"
/>
<defs>
<linearGradient id="headerGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: '#6366f1', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: '#a855f7', stopOpacity: 1 }} />
</linearGradient>
</defs>
</svg>
</div>
<div> <div>
<h1 className="text-2xl font-bold text-white">Syntria</h1> <h1 className="text-2xl font-bold text-white">Syntria</h1>
<p className="text-xs text-indigo-200">Sistema Inteligente de Inspecciones</p> <p className="text-xs text-indigo-200">Sistema Inteligente de Inspecciones</p>
@@ -363,65 +370,106 @@ function DashboardPage({ user, setUser }) {
} }
function SettingsTab({ user }) { function SettingsTab({ user }) {
const [aiConfig, setAiConfig] = useState(null) // Estado para el logo
const [availableModels, setAvailableModels] = useState([]) const [logoUrl, setLogoUrl] = useState(null);
const [loading, setLoading] = useState(true) const [logoUploading, setLogoUploading] = useState(false);
const [saving, setSaving] = useState(false) const [aiConfig, setAiConfig] = useState(null);
const [availableModels, setAvailableModels] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
provider: 'openai', provider: 'openai',
api_key: '', api_key: '',
model_name: 'gpt-4o' model_name: 'gpt-4o'
}) });
useEffect(() => { useEffect(() => {
loadSettings() const fetchLogo = async () => {
}, []) try {
const API_URL = import.meta.env.VITE_API_URL || '';
const token = localStorage.getItem('token');
const res = await fetch(`${API_URL}/api/config/logo`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const data = await res.json();
setLogoUrl(data.logo_url);
}
} catch {}
};
fetchLogo();
}, []);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => { const loadSettings = async () => {
try { try {
const token = localStorage.getItem('token') const token = localStorage.getItem('token');
const API_URL = import.meta.env.VITE_API_URL || '' const API_URL = import.meta.env.VITE_API_URL || '';
// Cargar modelos disponibles // Cargar modelos disponibles
const modelsRes = await fetch(`${API_URL}/api/ai/models`, { const modelsRes = await fetch(`${API_URL}/api/ai/models`, {
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
}) });
if (modelsRes.ok) { if (modelsRes.ok) {
const models = await modelsRes.json() const models = await modelsRes.json();
setAvailableModels(models) setAvailableModels(models);
} }
// Cargar configuración actual // Cargar configuración actual
const configRes = await fetch(`${API_URL}/api/ai/configuration`, { const configRes = await fetch(`${API_URL}/api/ai/configuration`, {
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
}) });
if (configRes.ok) { if (configRes.ok) {
const config = await configRes.json() const config = await configRes.json();
setAiConfig(config) setAiConfig(config);
setFormData({ setFormData({
provider: config.provider, provider: config.provider,
api_key: config.api_key, api_key: config.api_key,
model_name: config.model_name model_name: config.model_name
}) });
} }
setLoading(false);
setLoading(false)
} catch (error) { } catch (error) {
console.error('Error loading settings:', error) console.error('Error loading settings:', error);
setLoading(false) setLoading(false);
} }
} };
const handleLogoUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setLogoUploading(true);
try {
const API_URL = import.meta.env.VITE_API_URL || '';
const token = localStorage.getItem('token');
const formDataLogo = new FormData();
formDataLogo.append('file', file);
const res = await fetch(`${API_URL}/api/config/logo`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formDataLogo
});
if (res.ok) {
const data = await res.json();
setLogoUrl(data.logo_url);
alert('Logo actualizado correctamente');
} else {
alert('Error al subir el logo');
}
} catch {
alert('Error al subir el logo');
} finally {
setLogoUploading(false);
}
};
const handleSave = async (e) => { const handleSave = async (e) => {
e.preventDefault() e.preventDefault();
setSaving(true) setSaving(true);
try { try {
const token = localStorage.getItem('token') const token = localStorage.getItem('token');
const API_URL = import.meta.env.VITE_API_URL || '' const API_URL = import.meta.env.VITE_API_URL || '';
const response = await fetch(`${API_URL}/api/ai/configuration`, { const response = await fetch(`${API_URL}/api/ai/configuration`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -429,168 +477,119 @@ function SettingsTab({ user }) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(formData), body: JSON.stringify(formData),
}) });
if (response.ok) { if (response.ok) {
alert('Configuración guardada correctamente') alert('Configuración guardada correctamente');
loadSettings() loadSettings();
} else { } else {
alert('Error al guardar configuración') alert('Error al guardar configuración');
} }
} catch (error) { } catch (error) {
console.error('Error:', error) console.error('Error:', error);
alert('Error al guardar configuración') alert('Error al guardar configuración');
} finally { } finally {
setSaving(false) setSaving(false);
} }
} };
const filteredModels = availableModels.filter(m => m.provider === formData.provider) const filteredModels = availableModels.filter(m => m.provider === formData.provider);
return ( return (
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="mb-6"> <form onSubmit={handleSave}>
<h2 className="text-xl font-bold text-gray-900">Configuración de IA</h2> <div className="mb-6">
<p className="text-sm text-gray-600 mt-1"> <h2 className="text-xl font-bold text-gray-900">Logo del Sistema</h2>
Configura el proveedor y modelo de IA para análisis de imágenes <div className="flex items-center gap-6 mt-2">
</p> {logoUrl ? (
</div> <img src={logoUrl} alt="Logo" className="h-20 w-auto rounded-xl border shadow" />
) : (
{loading ? ( <div className="h-20 w-20 bg-gray-200 rounded-xl flex items-center justify-center text-gray-400">Sin logo</div>
<div className="text-center py-12"> )}
<div className="text-gray-500">Cargando configuración...</div>
</div>
) : (
<form onSubmit={handleSave} className="space-y-6">
{/* Provider Selection */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Proveedor de IA</h3>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => {
setFormData({ ...formData, provider: 'openai', model_name: 'gpt-4o' })
}}
className={`p-4 border-2 rounded-lg transition ${
formData.provider === 'openai'
? 'border-blue-500 bg-blue-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>
</button>
<button
type="button"
onClick={() => {
setFormData({ ...formData, provider: 'gemini', model_name: 'gemini-1.5-pro' })
}}
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>
</button>
</div>
</div>
{/* API Key */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">API Key</h3>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <input type="file" accept="image/*" onChange={handleLogoUpload} disabled={logoUploading} />
{formData.provider === 'openai' ? 'OpenAI API Key' : 'Google AI API Key'} {logoUploading && <span className="ml-2 text-blue-600">Subiendo...</span>}
</label>
<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-...' : 'AIza...'}
required
/>
<p className="text-xs text-gray-500 mt-2">
{formData.provider === 'openai' ? (
<>Obtén tu API key en <a href="https://platform.openai.com/api-keys" target="_blank" className="text-blue-600 hover:underline">OpenAI Platform</a></>
) : (
<>Obtén tu API key en <a href="https://makersuite.google.com/app/apikey" target="_blank" className="text-blue-600 hover:underline">Google AI Studio</a></>
)}
</p>
</div> </div>
</div> </div>
<p className="text-xs text-gray-500 mt-2">El logo se mostrará en el login y en la página principal.</p>
{/* Model Selection */} </div>
<div className="bg-white border border-gray-200 rounded-lg p-6"> <div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Modelo de IA</h3> <h2 className="text-xl font-bold text-gray-900">Configuración de IA</h2>
<p className="text-sm text-gray-600 mt-1">Configura el proveedor y modelo de IA para análisis de imágenes</p>
<div className="space-y-3"> <div className="flex gap-4 mt-4">
{filteredModels.map((model) => (
<label
key={model.id}
className={`flex items-start p-4 border-2 rounded-lg cursor-pointer transition ${
formData.model_name === model.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="model"
value={model.id}
checked={formData.model_name === model.id}
onChange={(e) => setFormData({ ...formData, model_name: e.target.value })}
className="mt-1 mr-3"
/>
<div className="flex-1">
<div className="font-semibold text-gray-900">{model.name}</div>
<div className="text-sm text-gray-600 mt-1">{model.description}</div>
</div>
</label>
))}
</div>
</div>
{/* Current Status */}
{aiConfig && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<span className="text-green-600 text-xl"></span>
<div>
<div className="font-semibold text-green-900">Configuración Activa</div>
<div className="text-sm text-green-700 mt-1">
Proveedor: <strong className="capitalize">{aiConfig.provider}</strong> |
Modelo: <strong>{aiConfig.model_name}</strong>
</div>
<div className="text-xs text-green-600 mt-1">
Configurado el {new Date(aiConfig.created_at).toLocaleDateString('es-ES')}
</div>
</div>
</div>
</div>
)}
{/* Save Button */}
<div className="flex gap-3">
<button <button
type="submit" type="button"
className="flex-1 px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:transform-none" onClick={() => setFormData({ ...formData, provider: 'openai', model_name: 'gpt-4o' })}
disabled={saving} className={`p-4 border-2 rounded-lg transition ${formData.provider === 'openai' ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-gray-400'}`}
> >
{saving ? 'Guardando...' : 'Guardar Configuración'} <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>
</button>
<button
type="button"
onClick={() => setFormData({ ...formData, provider: 'gemini', model_name: 'gemini-1.5-pro' })}
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>
</button> </button>
</div> </div>
</form> </div>
)} <div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">API Key</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{formData.provider === 'openai' ? 'OpenAI API Key' : 'Google AI API Key'}
</label>
<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-...' : 'AIza...'}
required
/>
<p className="text-xs text-gray-500 mt-2">
{formData.provider === 'openai' ? (
<>Obtén tu API key en <a href="https://platform.openai.com/api-keys" target="_blank" className="text-blue-600 hover:underline">OpenAI Platform</a></>
) : (
<>Obtén tu API key en <a href="https://makersuite.google.com/app/apikey" target="_blank" className="text-blue-600 hover:underline">Google AI Studio</a></>
)}
</p>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Modelo de IA</h3>
<div className="space-y-3">
{filteredModels.map((model) => (
<label key={model.model_name} className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition ${formData.model_name === model.model_name ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-gray-400'}`}>
<input
type="radio"
name="model_name"
value={model.model_name}
checked={formData.model_name === model.model_name}
onChange={() => setFormData({ ...formData, model_name: model.model_name })}
className="form-radio text-indigo-600"
/>
<span className="font-semibold">{model.model_name}</span>
<span className="text-xs text-gray-500">{model.description}</span>
</label>
))}
</div>
</div>
<div className="flex gap-3">
<button
type="submit"
className="flex-1 px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:transform-none"
disabled={saving}
>
{saving ? 'Guardando...' : 'Guardar Configuración'}
</button>
</div>
</form>
</div> </div>
) );
} }
function APITokensTab({ user }) { function APITokensTab({ user }) {