Spaces:
Running
Running
| import React, { useEffect, useRef, useState } from 'react' | |
| import { api, getAuthToken, setAuthToken } from './api' | |
| import AvaliacaoTab from './components/AvaliacaoTab' | |
| import ElaboracaoTab from './components/ElaboracaoTab' | |
| import InicioTab from './components/InicioTab' | |
| import PesquisaTab from './components/PesquisaTab' | |
| import RepositorioTab from './components/RepositorioTab' | |
| const LOGS_PAGE_SIZE = 30 | |
| const TABS = [ | |
| { key: 'Pesquisa/Visualização', label: 'Pesquisa/Visualização' }, | |
| { key: 'Elaboração/Edição', label: 'Elaboração/Edição' }, | |
| { key: 'Avaliação', label: 'Avaliação de Imóveis' }, | |
| { key: 'Repositório de Modelos', label: 'Repositório de Modelos' }, | |
| ] | |
| export default function App() { | |
| const [activeTab, setActiveTab] = useState('') | |
| const [showStartupIntro, setShowStartupIntro] = useState(true) | |
| const [sessionId, setSessionId] = useState('') | |
| const [bootError, setBootError] = useState('') | |
| const [avaliacaoQuickLoad, setAvaliacaoQuickLoad] = useState(null) | |
| const [authLoading, setAuthLoading] = useState(true) | |
| const [authUser, setAuthUser] = useState(null) | |
| const [authError, setAuthError] = useState('') | |
| const [loginLoading, setLoginLoading] = useState(false) | |
| const [usuario, setUsuario] = useState('') | |
| const [matricula, setMatricula] = useState('') | |
| const [logsStatus, setLogsStatus] = useState(null) | |
| const [logsStatusLoading, setLogsStatusLoading] = useState(false) | |
| const [logsOpen, setLogsOpen] = useState(false) | |
| const [logsEvents, setLogsEvents] = useState([]) | |
| const [logsLoading, setLogsLoading] = useState(false) | |
| const [logsError, setLogsError] = useState('') | |
| const [logsScope, setLogsScope] = useState('') | |
| const [logsUsuario, setLogsUsuario] = useState('') | |
| const [logsPage, setLogsPage] = useState(1) | |
| const [settingsOpen, setSettingsOpen] = useState(false) | |
| const [showScrollHomeBtn, setShowScrollHomeBtn] = useState(false) | |
| const [scrollHomeBtnLeft, setScrollHomeBtnLeft] = useState(8) | |
| const headerRef = useRef(null) | |
| const settingsMenuRef = useRef(null) | |
| const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin' | |
| const logsEnabled = Boolean(logsStatus?.enabled) | |
| const logsDisabledReason = String(logsStatus?.reason || 'Logs indisponíveis') | |
| const logsViewActive = Boolean(authUser && isAdmin && logsOpen) | |
| const logsTotalPages = Math.max(1, Math.ceil(logsEvents.length / LOGS_PAGE_SIZE)) | |
| const logsCurrentPage = Math.min(logsPage, logsTotalPages) | |
| const logsStartIndex = logsEvents.length ? (logsCurrentPage - 1) * LOGS_PAGE_SIZE : 0 | |
| const logsVisibleEvents = logsEvents.slice(logsStartIndex, logsStartIndex + LOGS_PAGE_SIZE) | |
| const logsEndIndex = logsEvents.length ? Math.min(logsStartIndex + LOGS_PAGE_SIZE, logsEvents.length) : 0 | |
| function resetToLogin(message = '') { | |
| setAuthToken('') | |
| setAuthUser(null) | |
| setAuthLoading(false) | |
| setLoginLoading(false) | |
| setSessionId('') | |
| setBootError('') | |
| setLogsStatus(null) | |
| setLogsOpen(false) | |
| setLogsEvents([]) | |
| setLogsError('') | |
| setLogsPage(1) | |
| setActiveTab('') | |
| setShowStartupIntro(true) | |
| setAuthError(message) | |
| } | |
| useEffect(() => { | |
| let mounted = true | |
| async function bootstrapAuth() { | |
| const token = getAuthToken() | |
| if (!token) { | |
| if (mounted) setAuthLoading(false) | |
| return | |
| } | |
| try { | |
| const resp = await api.authMe() | |
| if (!mounted) return | |
| setAuthUser(resp?.usuario || null) | |
| } catch { | |
| setAuthToken('') | |
| if (!mounted) return | |
| setAuthUser(null) | |
| } finally { | |
| if (mounted) setAuthLoading(false) | |
| } | |
| } | |
| void bootstrapAuth() | |
| return () => { | |
| mounted = false | |
| } | |
| }, []) | |
| useEffect(() => { | |
| function onAuthRequired(event) { | |
| const message = String(event?.detail?.message || '').trim() || 'Sessão expirada. Faça login novamente.' | |
| resetToLogin(message) | |
| } | |
| if (typeof window !== 'undefined') { | |
| window.addEventListener('mesa:auth-required', onAuthRequired) | |
| return () => window.removeEventListener('mesa:auth-required', onAuthRequired) | |
| } | |
| return undefined | |
| }, []) | |
| useEffect(() => { | |
| let mounted = true | |
| if (!authUser) { | |
| setSessionId('') | |
| setBootError('') | |
| return () => { | |
| mounted = false | |
| } | |
| } | |
| setBootError('') | |
| api.createSession() | |
| .then((resp) => { | |
| if (!mounted) return | |
| setSessionId(resp.session_id) | |
| setBootError('') | |
| }) | |
| .catch((err) => { | |
| if (!mounted) return | |
| if (Number(err?.status) === 401) return | |
| setBootError(err.message || 'Falha ao criar sessão') | |
| }) | |
| return () => { | |
| mounted = false | |
| } | |
| }, [authUser]) | |
| useEffect(() => { | |
| if (!authUser || !isAdmin) { | |
| setLogsStatus(null) | |
| setLogsOpen(false) | |
| setLogsEvents([]) | |
| setLogsError('') | |
| setLogsPage(1) | |
| setLogsStatusLoading(false) | |
| setLogsLoading(false) | |
| return | |
| } | |
| void carregarLogsStatus() | |
| }, [authUser, isAdmin]) | |
| useEffect(() => { | |
| if (!logsEvents.length && logsPage !== 1) { | |
| setLogsPage(1) | |
| return | |
| } | |
| if (logsPage > logsTotalPages) { | |
| setLogsPage(logsTotalPages) | |
| } | |
| }, [logsEvents.length, logsPage, logsTotalPages]) | |
| useEffect(() => { | |
| if (!settingsOpen) return undefined | |
| function onPointerDown(event) { | |
| if (!settingsMenuRef.current) return | |
| if (!settingsMenuRef.current.contains(event.target)) { | |
| setSettingsOpen(false) | |
| } | |
| } | |
| document.addEventListener('mousedown', onPointerDown) | |
| return () => document.removeEventListener('mousedown', onPointerDown) | |
| }, [settingsOpen]) | |
| useEffect(() => { | |
| if (!authUser) { | |
| setSettingsOpen(false) | |
| } | |
| }, [authUser]) | |
| useEffect(() => { | |
| if (typeof window === 'undefined') return undefined | |
| if (!authUser) { | |
| setShowScrollHomeBtn(false) | |
| setScrollHomeBtnLeft(8) | |
| return undefined | |
| } | |
| function resolveHomeButtonLeft() { | |
| const buttonSize = 42 | |
| const navAnchor = document.querySelector('.elaboracao-side-nav-item') | |
| if (navAnchor && typeof navAnchor.getBoundingClientRect === 'function') { | |
| const rect = navAnchor.getBoundingClientRect() | |
| return Math.max(8, rect.left + ((rect.width - buttonSize) / 2)) | |
| } | |
| const shell = document.querySelector('.app-shell') | |
| if (shell && typeof shell.getBoundingClientRect === 'function') { | |
| const rect = shell.getBoundingClientRect() | |
| return Math.max(8, rect.left) | |
| } | |
| return 8 | |
| } | |
| function updateScrollHomeVisibility() { | |
| const headerEl = headerRef.current | |
| if (!headerEl) { | |
| setShowScrollHomeBtn(false) | |
| return | |
| } | |
| const rect = headerEl.getBoundingClientRect() | |
| const shouldShow = rect.bottom <= 0 | |
| const nextLeft = resolveHomeButtonLeft() | |
| setShowScrollHomeBtn((current) => (current === shouldShow ? current : shouldShow)) | |
| setScrollHomeBtnLeft((current) => (Math.abs(current - nextLeft) < 0.5 ? current : nextLeft)) | |
| } | |
| updateScrollHomeVisibility() | |
| window.addEventListener('scroll', updateScrollHomeVisibility, { passive: true }) | |
| window.addEventListener('resize', updateScrollHomeVisibility) | |
| return () => { | |
| window.removeEventListener('scroll', updateScrollHomeVisibility) | |
| window.removeEventListener('resize', updateScrollHomeVisibility) | |
| } | |
| }, [authUser]) | |
| async function onSubmitLogin(event) { | |
| event.preventDefault() | |
| setAuthError('') | |
| setBootError('') | |
| setLoginLoading(true) | |
| try { | |
| const resp = await api.authLogin(usuario, matricula) | |
| setAuthToken(resp?.token || '') | |
| setAuthUser(resp?.usuario || null) | |
| setUsuario('') | |
| setMatricula('') | |
| } catch (err) { | |
| setAuthError(err.message || 'Falha no login') | |
| setAuthUser(null) | |
| setAuthToken('') | |
| } finally { | |
| setLoginLoading(false) | |
| setAuthLoading(false) | |
| } | |
| } | |
| async function onLogout() { | |
| setSettingsOpen(false) | |
| try { | |
| await api.authLogout() | |
| } catch { | |
| // Ignore falha de logout remoto para garantir limpeza local. | |
| } | |
| resetToLogin('') | |
| } | |
| async function carregarLogsStatus() { | |
| if (!isAdmin) return | |
| setLogsStatusLoading(true) | |
| try { | |
| const resp = await api.logsStatus() | |
| setLogsStatus(resp || null) | |
| } catch (err) { | |
| setLogsStatus({ | |
| enabled: false, | |
| backend: 'disabled', | |
| reason: err.message || 'Falha ao carregar status de logs.', | |
| }) | |
| } finally { | |
| setLogsStatusLoading(false) | |
| } | |
| } | |
| async function carregarEventosLogs() { | |
| if (!isAdmin || !logsEnabled) return | |
| setLogsLoading(true) | |
| setLogsError('') | |
| try { | |
| const resp = await api.logsEvents({ scope: logsScope, usuario: logsUsuario, limit: 1000 }) | |
| setLogsEvents(Array.isArray(resp?.events) ? resp.events : []) | |
| setLogsPage(1) | |
| } catch (err) { | |
| setLogsError(err.message || 'Falha ao carregar logs.') | |
| setLogsEvents([]) | |
| setLogsPage(1) | |
| } finally { | |
| setLogsLoading(false) | |
| } | |
| } | |
| async function onToggleLogs() { | |
| if (!isAdmin) return | |
| if (logsOpen) { | |
| setLogsOpen(false) | |
| return | |
| } | |
| setLogsOpen(true) | |
| if (!logsEnabled) return | |
| await carregarEventosLogs() | |
| } | |
| function onCloseLogsView() { | |
| setLogsOpen(false) | |
| } | |
| function onUsarModeloEmAvaliacao(modelo) { | |
| const modeloId = String(modelo?.id || '').trim() | |
| if (!modeloId) return | |
| setAvaliacaoQuickLoad({ | |
| requestKey: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | |
| modeloId, | |
| modeloArquivo: String(modelo?.arquivo || '').trim(), | |
| nomeModelo: String(modelo?.nome_modelo || modelo?.arquivo || modeloId), | |
| }) | |
| setActiveTab('Avaliação') | |
| setLogsOpen(false) | |
| setShowStartupIntro(false) | |
| } | |
| function onScrollToHeader() { | |
| if (typeof window === 'undefined') return | |
| const headerEl = headerRef.current | |
| if (!headerEl) return | |
| const top = Math.max(0, window.scrollY + headerEl.getBoundingClientRect().top - 8) | |
| window.scrollTo({ top, behavior: 'smooth' }) | |
| } | |
| return ( | |
| <div className="app-shell"> | |
| <header ref={headerRef} className={authUser ? 'app-header app-header-logged' : 'app-header app-header-logo-only'}> | |
| <div className="brand-mark" aria-hidden="true"> | |
| <img src={`${import.meta.env.BASE_URL}logo_mesa.png`} alt="MESA" /> | |
| </div> | |
| {authUser ? ( | |
| <div className="app-top-actions"> | |
| <nav className="tabs" aria-label="Navegação principal"> | |
| {TABS.map((tab) => { | |
| const active = tab.key === activeTab | |
| return ( | |
| <button | |
| key={tab.key} | |
| className={active ? 'tab-pill active' : 'tab-pill'} | |
| onClick={() => { | |
| setActiveTab(tab.key) | |
| setShowStartupIntro(false) | |
| setLogsOpen(false) | |
| setSettingsOpen(false) | |
| }} | |
| type="button" | |
| > | |
| <strong>{tab.label}</strong> | |
| </button> | |
| ) | |
| })} | |
| </nav> | |
| <div ref={settingsMenuRef} className={`settings-menu${settingsOpen ? ' is-open' : ''}`}> | |
| <button | |
| type="button" | |
| className="settings-gear-btn" | |
| aria-haspopup="menu" | |
| aria-expanded={settingsOpen} | |
| aria-label="Abrir configurações" | |
| onClick={() => setSettingsOpen((prev) => !prev)} | |
| title="Configurações" | |
| > | |
| ⚙ | |
| </button> | |
| {settingsOpen ? ( | |
| <div className="settings-menu-panel" role="menu" aria-label="Configurações do usuário"> | |
| <div className="settings-user-summary"> | |
| Usuário: <strong>{authUser.nome || authUser.usuario}</strong> ({authUser.perfil || 'viewer'}) | |
| </div> | |
| <div className="settings-menu-actions"> | |
| {isAdmin ? ( | |
| <button | |
| type="button" | |
| className="settings-menu-btn" | |
| onClick={() => { | |
| void onToggleLogs() | |
| setSettingsOpen(false) | |
| }} | |
| disabled={logsStatusLoading || (!logsEnabled && !logsOpen)} | |
| title={logsOpen ? 'Fechar visualização de logs' : !logsEnabled ? logsDisabledReason : 'Abrir leitura de logs'} | |
| > | |
| {logsOpen ? 'Fechar logs' : 'Abrir logs'} | |
| </button> | |
| ) : null} | |
| <button | |
| type="button" | |
| className="settings-menu-btn settings-menu-btn-danger" | |
| onClick={() => void onLogout()} | |
| > | |
| Sair | |
| </button> | |
| </div> | |
| </div> | |
| ) : null} | |
| </div> | |
| </div> | |
| ) : null} | |
| </header> | |
| {authUser && showScrollHomeBtn ? ( | |
| <button | |
| type="button" | |
| className="scroll-home-btn" | |
| style={{ left: `${scrollHomeBtnLeft}px` }} | |
| onClick={onScrollToHeader} | |
| aria-label="Voltar ao cabeçalho" | |
| title="Voltar ao cabeçalho" | |
| > | |
| <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"> | |
| <path d="M11.49 2.26a.75.75 0 0 1 1.02 0l9 8.25a.75.75 0 0 1-1.02 1.1L20 11.12V20a2 2 0 0 1-2 2h-3.75a.75.75 0 0 1-.75-.75V16a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25v5.25a.75.75 0 0 1-.75.75H6a2 2 0 0 1-2-2v-8.88l-.49.49a.75.75 0 0 1-1.02-1.1l9-8.25Z" /> | |
| </svg> | |
| </button> | |
| ) : null} | |
| {authLoading ? <div className="status-line">Validando autenticação...</div> : null} | |
| {!authLoading && !authUser ? ( | |
| <section className="auth-card"> | |
| <h3>Entrar na MESA</h3> | |
| <p>Usuário: primeiro nome sem acentos e em minúsculo. Senha: matrícula sem dígito.</p> | |
| <form onSubmit={onSubmitLogin} className="auth-form"> | |
| <div className="auth-field"> | |
| <label htmlFor="usuario">Usuário</label> | |
| <input | |
| id="usuario" | |
| type="text" | |
| placeholder="ex.: david (minúsculo, sem acento)" | |
| value={usuario} | |
| onChange={(event) => setUsuario(event.target.value)} | |
| autoComplete="off" | |
| disabled={loginLoading} | |
| /> | |
| </div> | |
| <div className="auth-field"> | |
| <label htmlFor="matricula">Senha</label> | |
| <input | |
| id="matricula" | |
| type="text" | |
| placeholder="matrícula sem dígito" | |
| value={matricula} | |
| onChange={(event) => setMatricula(event.target.value)} | |
| autoComplete="off" | |
| disabled={loginLoading} | |
| /> | |
| </div> | |
| <button className="auth-submit" type="submit" disabled={loginLoading || !usuario.trim() || !matricula.trim()}> | |
| {loginLoading ? 'Entrando...' : 'Entrar'} | |
| </button> | |
| </form> | |
| {authError ? <div className="error-line">{authError}</div> : null} | |
| </section> | |
| ) : null} | |
| {authUser ? ( | |
| logsViewActive ? ( | |
| <section className="logs-panel logs-panel-dedicated"> | |
| <div className="logs-panel-head"> | |
| <div className="logs-panel-head-main"> | |
| <h3>Logs</h3> | |
| <p className="logs-close-hint">Para voltar ao app, clique em "Fechar logs".</p> | |
| </div> | |
| <div className="logs-panel-head-actions"> | |
| <div className="logs-panel-meta"> | |
| <span>{logsStatus?.backend === 'hf_dataset' ? 'Origem: Dataset HF' : 'Origem: indisponível'}</span> | |
| {logsStatus?.revision ? <span>Revisão: {String(logsStatus.revision).slice(0, 8)}</span> : null} | |
| </div> | |
| <button type="button" className="logs-close-btn" onClick={onCloseLogsView}> | |
| Fechar logs | |
| </button> | |
| </div> | |
| </div> | |
| {!logsEnabled ? ( | |
| <div className="section1-empty-hint">{logsDisabledReason}</div> | |
| ) : ( | |
| <> | |
| <div className="logs-filters"> | |
| <div className="logs-field"> | |
| <label htmlFor="logs-scope">Escopo</label> | |
| <select id="logs-scope" value={logsScope} onChange={(event) => setLogsScope(event.target.value)}> | |
| <option value="">Todos</option> | |
| <option value="auth">Auth</option> | |
| <option value="repositorio">Repositório</option> | |
| <option value="elaboracao">Elaboração</option> | |
| <option value="visualizacao">Visualização</option> | |
| </select> | |
| </div> | |
| <div className="logs-field"> | |
| <label htmlFor="logs-usuario">Usuário</label> | |
| <input | |
| id="logs-usuario" | |
| type="text" | |
| value={logsUsuario} | |
| onChange={(event) => setLogsUsuario(event.target.value)} | |
| placeholder="filtrar por usuário" | |
| autoComplete="off" | |
| /> | |
| </div> | |
| <button type="button" onClick={() => void carregarEventosLogs()} disabled={logsLoading}> | |
| Atualizar | |
| </button> | |
| </div> | |
| {logsError ? <div className="error-line">{logsError}</div> : null} | |
| <div className="table-container"> | |
| <table className="logs-table"> | |
| <thead> | |
| <tr> | |
| <th>Timestamp</th> | |
| <th>Usuário</th> | |
| <th>Perfil</th> | |
| <th>Escopo</th> | |
| <th>Ação</th> | |
| <th>Status</th> | |
| <th>Detalhes</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {logsVisibleEvents.map((item) => ( | |
| <tr key={item.event_id || `${item.ts}-${item.action}`}> | |
| <td>{item.ts || '-'}</td> | |
| <td>{item.usuario || '-'}</td> | |
| <td>{item.perfil || '-'}</td> | |
| <td>{item.scope || '-'}</td> | |
| <td>{item.action || '-'}</td> | |
| <td>{item.status || '-'}</td> | |
| <td className="logs-table-details">{JSON.stringify(item.details || {})}</td> | |
| </tr> | |
| ))} | |
| {!logsEvents.length ? ( | |
| <tr> | |
| <td colSpan={7}>{logsLoading ? 'Carregando logs...' : 'Nenhum log encontrado para os filtros.'}</td> | |
| </tr> | |
| ) : null} | |
| </tbody> | |
| </table> | |
| </div> | |
| <div className="logs-pagination"> | |
| <span>{logsEvents.length ? `Exibindo ${logsStartIndex + 1}-${logsEndIndex} de ${logsEvents.length}` : 'Exibindo 0 de 0'}</span> | |
| <div className="logs-pagination-actions"> | |
| <button | |
| type="button" | |
| onClick={() => setLogsPage((prev) => Math.max(1, prev - 1))} | |
| disabled={logsLoading || logsCurrentPage <= 1} | |
| > | |
| Anterior | |
| </button> | |
| <span>Página {logsCurrentPage} de {logsTotalPages}</span> | |
| <button | |
| type="button" | |
| onClick={() => setLogsPage((prev) => Math.min(logsTotalPages, prev + 1))} | |
| disabled={logsLoading || logsCurrentPage >= logsTotalPages} | |
| > | |
| Próxima | |
| </button> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </section> | |
| ) : ( | |
| <> | |
| {bootError ? <div className="error-line">Falha ao criar sessão: {bootError}</div> : null} | |
| {showStartupIntro ? ( | |
| <div className="tab-pane"> | |
| <InicioTab /> | |
| </div> | |
| ) : null} | |
| <div className="tab-pane" hidden={activeTab !== 'Pesquisa/Visualização'}> | |
| <PesquisaTab sessionId={sessionId} onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao} /> | |
| </div> | |
| <div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}> | |
| <ElaboracaoTab sessionId={sessionId} /> | |
| </div> | |
| <div className="tab-pane" hidden={activeTab !== 'Repositório de Modelos'}> | |
| <RepositorioTab authUser={authUser} sessionId={sessionId} /> | |
| </div> | |
| <div className="tab-pane" hidden={activeTab !== 'Avaliação'}> | |
| <AvaliacaoTab sessionId={sessionId} quickLoadRequest={avaliacaoQuickLoad} /> | |
| </div> | |
| </> | |
| ) | |
| ) : null} | |
| </div> | |
| ) | |
| } | |