Spaces:
Sleeping
Sleeping
| import React, { useEffect, useMemo, useState } from 'react' | |
| import { api, downloadBlob } from '../api' | |
| function normalizarChave(value) { | |
| return String(value || '') | |
| .normalize('NFD') | |
| .replace(/[\u0300-\u036f]/g, '') | |
| .toLowerCase() | |
| .trim() | |
| .replace(/\.dai$/i, '') | |
| } | |
| function resolverModeloId(chave, modelos) { | |
| const normalizada = normalizarChave(chave) | |
| if (!normalizada) return '' | |
| for (const modelo of modelos || []) { | |
| const candidatos = [modelo?.id, modelo?.arquivo, modelo?.nome_modelo] | |
| .map(normalizarChave) | |
| .filter(Boolean) | |
| if (candidatos.includes(normalizada)) return String(modelo.id || '') | |
| } | |
| return '' | |
| } | |
| function formatarFonte(fonte) { | |
| if (!fonte || typeof fonte !== 'object') return '' | |
| if (String(fonte.provider || '').toLowerCase() === 'hf_dataset') { | |
| const repo = String(fonte.repo_id || '').trim() | |
| const revisao = String(fonte.revision || '').trim() | |
| return `HF Dataset${repo ? ` (${repo})` : ''}${revisao ? ` | revisão ${revisao.slice(0, 8)}` : ''}` | |
| } | |
| return 'Pasta local' | |
| } | |
| export default function AnexosModal({ | |
| open, | |
| defaultModeloId = '', | |
| avaliacao = null, | |
| lockModelo = false, | |
| title = 'Gerar anexos', | |
| onClose, | |
| }) { | |
| const [modelos, setModelos] = useState([]) | |
| const [fonte, setFonte] = useState(null) | |
| const [modeloSelecionado, setModeloSelecionado] = useState('') | |
| const [colunas, setColunas] = useState([]) | |
| const [loadingModelos, setLoadingModelos] = useState(false) | |
| const [loadingColunas, setLoadingColunas] = useState(false) | |
| const [generating, setGenerating] = useState(false) | |
| const [downloading, setDownloading] = useState(false) | |
| const [error, setError] = useState('') | |
| const [result, setResult] = useState(null) | |
| const colunasSelecionadas = useMemo( | |
| () => colunas.filter((item) => item.selected).map((item) => item.name), | |
| [colunas], | |
| ) | |
| const busy = loadingModelos || loadingColunas || generating || downloading | |
| const blocking = generating || downloading | |
| const canGenerate = Boolean(modeloSelecionado && colunasSelecionadas.length && !busy) | |
| useEffect(() => { | |
| if (!open) return undefined | |
| let active = true | |
| setLoadingModelos(true) | |
| setError('') | |
| setResult(null) | |
| setColunas([]) | |
| setModeloSelecionado('') | |
| api.anexosModelos() | |
| .then((response) => { | |
| if (!active) return | |
| const lista = Array.isArray(response?.modelos) ? response.modelos : [] | |
| const resolvido = resolverModeloId(defaultModeloId, lista) | |
| setModelos(lista) | |
| setFonte(response?.fonte || null) | |
| if (lockModelo && defaultModeloId && !resolvido) { | |
| setModeloSelecionado('') | |
| setError('O modelo usado nesta avaliação não foi encontrado no repositório configurado.') | |
| return | |
| } | |
| setModeloSelecionado(resolvido || String(lista[0]?.id || '')) | |
| }) | |
| .catch((err) => { | |
| if (!active) return | |
| setModelos([]) | |
| setModeloSelecionado('') | |
| setError(err?.message || 'Não foi possível carregar os modelos.') | |
| }) | |
| .finally(() => { | |
| if (active) setLoadingModelos(false) | |
| }) | |
| return () => { | |
| active = false | |
| } | |
| }, [open, defaultModeloId, lockModelo]) | |
| useEffect(() => { | |
| if (!open || !modeloSelecionado) { | |
| setColunas([]) | |
| return undefined | |
| } | |
| let active = true | |
| setLoadingColunas(true) | |
| setError('') | |
| setResult(null) | |
| api.anexosModeloColunas(modeloSelecionado) | |
| .then((response) => { | |
| if (!active) return | |
| setColunas((response?.columns || []).map((name) => ({ name, selected: true }))) | |
| }) | |
| .catch((err) => { | |
| if (!active) return | |
| setColunas([]) | |
| setError(err?.message || 'Não foi possível carregar as colunas do modelo.') | |
| }) | |
| .finally(() => { | |
| if (active) setLoadingColunas(false) | |
| }) | |
| return () => { | |
| active = false | |
| } | |
| }, [open, modeloSelecionado]) | |
| if (!open) return null | |
| function toggleColuna(nome) { | |
| setColunas((current) => current.map((item) => ( | |
| item.name === nome ? { ...item, selected: !item.selected } : item | |
| ))) | |
| setResult(null) | |
| } | |
| function moverColuna(index, direction) { | |
| setColunas((current) => { | |
| const destino = index + direction | |
| if (destino < 0 || destino >= current.length) return current | |
| const next = [...current] | |
| ;[next[index], next[destino]] = [next[destino], next[index]] | |
| return next | |
| }) | |
| setResult(null) | |
| } | |
| async function gerar(event) { | |
| event.preventDefault() | |
| if (!canGenerate) return | |
| setGenerating(true) | |
| setError('') | |
| setResult(null) | |
| try { | |
| const response = await api.anexosGerar( | |
| modeloSelecionado, | |
| colunasSelecionadas, | |
| avaliacao, | |
| ) | |
| setResult(response) | |
| } catch (err) { | |
| setError(err?.message || 'Falha ao gerar os anexos.') | |
| } finally { | |
| setGenerating(false) | |
| } | |
| } | |
| async function baixar() { | |
| const filename = String(result?.filename || '').trim() | |
| if (!filename) return | |
| setDownloading(true) | |
| setError('') | |
| try { | |
| const blob = await api.anexosDownload(filename) | |
| downloadBlob(blob, filename) | |
| } catch (err) { | |
| setError(err?.message || 'Falha ao baixar os anexos.') | |
| } finally { | |
| setDownloading(false) | |
| } | |
| } | |
| function fechar() { | |
| if (!blocking && typeof onClose === 'function') onClose() | |
| } | |
| return ( | |
| <div className="pesquisa-modal-backdrop" onClick={(event) => { | |
| if (event.target === event.currentTarget) fechar() | |
| }}> | |
| <div className="pesquisa-modal anexos-modal" role="dialog" aria-modal="true" aria-label={title}> | |
| <div className="pesquisa-modal-head"> | |
| <div> | |
| <h4>{title}</h4> | |
| {fonte ? <p>Fonte: {formatarFonte(fonte)}</p> : null} | |
| </div> | |
| <button type="button" className="pesquisa-modal-close" onClick={fechar} disabled={blocking}> | |
| Fechar | |
| </button> | |
| </div> | |
| <form className="pesquisa-modal-body anexos-modal-body" onSubmit={gerar}> | |
| <label className="anexos-modal-model-field"> | |
| Modelo de avaliação | |
| <select | |
| value={modeloSelecionado} | |
| onChange={(event) => setModeloSelecionado(event.target.value)} | |
| disabled={busy || lockModelo || !modelos.length} | |
| > | |
| {!modelos.length ? <option value="">Nenhum modelo encontrado</option> : null} | |
| {modelos.map((modelo) => ( | |
| <option key={String(modelo.id)} value={String(modelo.id)}> | |
| {modelo.nome_modelo || modelo.arquivo || modelo.id} | |
| </option> | |
| ))} | |
| </select> | |
| </label> | |
| <section className="anexos-columns-section"> | |
| <div className="anexos-columns-head"> | |
| <div> | |
| <strong>Colunas do banco de dados</strong> | |
| <span>{colunasSelecionadas.length} selecionadas</span> | |
| </div> | |
| <div className="anexos-columns-actions"> | |
| <button | |
| type="button" | |
| onClick={() => setColunas((current) => current.map((item) => ({ ...item, selected: true })))} | |
| disabled={busy} | |
| > | |
| Todas | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setColunas((current) => current.map((item) => ({ ...item, selected: false })))} | |
| disabled={busy} | |
| > | |
| Nenhuma | |
| </button> | |
| </div> | |
| </div> | |
| {loadingColunas ? ( | |
| <div className="section1-empty-hint">Carregando colunas...</div> | |
| ) : ( | |
| <div className="anexos-columns-list"> | |
| {colunas.map((coluna, index) => ( | |
| <div className="anexos-column-row" key={coluna.name}> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={coluna.selected} | |
| onChange={() => toggleColuna(coluna.name)} | |
| disabled={busy} | |
| /> | |
| <span>{coluna.name}</span> | |
| </label> | |
| <div className="anexos-reorder-actions"> | |
| <button | |
| type="button" | |
| title="Mover para cima" | |
| aria-label={`Mover ${coluna.name} para cima`} | |
| onClick={() => moverColuna(index, -1)} | |
| disabled={busy || index === 0} | |
| > | |
| ↑ | |
| </button> | |
| <button | |
| type="button" | |
| title="Mover para baixo" | |
| aria-label={`Mover ${coluna.name} para baixo`} | |
| onClick={() => moverColuna(index, 1)} | |
| disabled={busy || index === colunas.length - 1} | |
| > | |
| ↓ | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </section> | |
| {error ? <div className="error-line inline-error">{error}</div> : null} | |
| {result?.statistical_warnings?.length ? ( | |
| <div className="anexos-warning-panel"> | |
| <strong>Atenção</strong> | |
| <ul> | |
| {result.statistical_warnings.map((warning) => <li key={warning}>{warning}</li>)} | |
| </ul> | |
| </div> | |
| ) : null} | |
| {result ? ( | |
| <div className="anexos-result-panel"> | |
| <div> | |
| <strong>{result.message}</strong> | |
| <span>{result.filename}</span> | |
| </div> | |
| <button type="button" onClick={() => void baixar()} disabled={downloading}> | |
| {downloading ? 'Baixando...' : 'Baixar DOCX'} | |
| </button> | |
| </div> | |
| ) : null} | |
| <div className="anexos-modal-footer"> | |
| <button type="submit" disabled={!canGenerate}> | |
| {generating ? 'Gerando...' : 'Gerar DOCX'} | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| ) | |
| } | |