POP / app.py
Radioterapia-AI's picture
Update app.py
ce72860 verified
#!/usr/bin/env python3
"""
# ══════════════════════════════════════════════════════════════
# ☢︎ RADIOTERAPIA.AI — POP de elite
# app.py — Hugging Face Spaces
# por: Braga, HF.
# ══════════════════════════════════════════════════════════════
"""
import json, os, tempfile, io
from datetime import datetime
try:
import graphviz as gv
HAS_GRAPHVIZ = True
except ImportError:
HAS_GRAPHVIZ = False
from docx import Document
from docx.shared import Pt, Cm, Emu, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
# ============================================================
# SISTEMA DE CORES DINÂMICO
# ============================================================
def hex_to_rgb(h):
h = h.lstrip("#")
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def rgb_to_hex(r, g, b):
return f"{min(255,max(0,int(r))):02X}{min(255,max(0,int(g))):02X}{min(255,max(0,int(b))):02X}"
def lighten(hex_color, pct):
"""Clareia uma cor por pct% misturando com branco."""
r, g, b = hex_to_rgb(hex_color)
return rgb_to_hex(r + (255-r)*pct, g + (255-g)*pct, b + (255-b)*pct)
def darken(hex_color, pct):
r, g, b = hex_to_rgb(hex_color)
return rgb_to_hex(r*(1-pct), g*(1-pct), b*(1-pct))
def build_palette(primary_hex):
"""Gera paleta completa a partir de uma cor primária."""
p = primary_hex.lstrip("#").upper()
return {
"primary": p, # H1, bullets, table headers
"primary_dark": darken(p, 0.15), # Texto sobre fundo claro
"secondary": lighten(p, 0.45), # H2, SmartArt — +10% mais claro que v4.0
"tertiary": lighten(p, 0.70), # Footer header, zebra — +10% mais claro
"quaternary": lighten(p, 0.85), # Zebra alternada mais clara
"text_on_primary": "FFFFFF", # Texto sobre cor primária
"text_on_secondary": darken(p, 0.30), # Texto sobre cor secundária
"text_body": "1A1A1A",
"header_text": "000000", # PRETO automático para cabeçalho
"border": darken(p, 0.0),
"border_light": lighten(p, 0.20),
"risk_red": "8B1A1A",
"barrier_green": "1B5E20",
"gray_border": "7F8C9A",
"annex_border": "000000",
}
def parse_color_input(val):
"""Parseia qualquer formato de cor do Gradio ColorPicker → hex 6 chars."""
if not val:
return DEFAULT_PRIMARY
val = str(val).strip()
# Formato rgb(R, G, B) ou rgba(R, G, B, A) — COM FLOATS
if val.startswith("rgb"):
import re
# Capturar números com decimais: 248.402, 79.543, etc.
nums = re.findall(r'[\d]+\.?[\d]*', val)
if len(nums) >= 3:
r = min(255, max(0, int(float(nums[0]))))
g = min(255, max(0, int(float(nums[1]))))
b = min(255, max(0, int(float(nums[2]))))
return rgb_to_hex(r, g, b)
return DEFAULT_PRIMARY
# Formato hex
c = val.lstrip("#")
c = ''.join(ch for ch in c if ch in '0123456789abcdefABCDEF')
if len(c) >= 6:
return c[:6].upper()
if len(c) == 3:
return (c[0]*2 + c[1]*2 + c[2]*2).upper()
return DEFAULT_PRIMARY
DEFAULT_PRIMARY = "283264"
FONTE = "Calibri"
TAM_CORPO = Pt(11)
TAM_H1 = Pt(12)
TAM_H2 = Pt(11)
TAM_SMALL = Pt(9)
TAM_HEADER = Pt(9)
TAM_TINY = Pt(8)
# Página A4
MARGEM_SUP = Cm(6.0) # 6cm — espaço uniforme header↔corpo em todas as páginas
MARGEM_INF = Cm(3.0)
MARGEM_ESQ = Cm(2.5)
MARGEM_DIR = Cm(2.0)
LARGURA_DXA = int((21 - 2.5 - 2.0) * 567)
# Indentação hierárquica
IND_H2 = 0.5
IND_BODY = 0.5
IND_ITEM = 1.0
IND_SUBITEM = 1.5
# ============================================================
# UTILITÁRIOS DOCX
# ============================================================
def set_shading(cell, color):
cell._tc.get_or_add_tcPr().append(
parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}" w:val="clear"/>'))
def set_borders(cell, top=None, bottom=None, left=None, right=None):
tcPr = cell._tc.get_or_add_tcPr()
borders = parse_xml(f'<w:tcBorders {nsdecls("w")}></w:tcBorders>')
for side, val in [("top",top),("bottom",bottom),("left",left),("right",right)]:
if val:
c, s = val if isinstance(val, tuple) else (val, "4")
borders.append(parse_xml(
f'<w:{side} {nsdecls("w")} w:val="single" w:sz="{s}" w:space="0" w:color="{c}"/>'))
tcPr.append(borders)
def set_valign(cell, v="center"):
cell._tc.get_or_add_tcPr().append(parse_xml(f'<w:vAlign {nsdecls("w")} w:val="{v}"/>'))
def set_width(cell, w):
cell._tc.get_or_add_tcPr().append(
parse_xml(f'<w:tcW {nsdecls("w")} w:w="{w}" w:type="dxa"/>'))
def set_margins(cell, t=0, b=0, l=80, r=80):
cell._tc.get_or_add_tcPr().append(parse_xml(
f'<w:tcMar {nsdecls("w")}><w:top w:w="{t}" w:type="dxa"/>'
f'<w:left w:w="{l}" w:type="dxa"/><w:bottom w:w="{b}" w:type="dxa"/>'
f'<w:right w:w="{r}" w:type="dxa"/></w:tcMar>'))
def fmt(p, text, bold=False, italic=False, size=None, color=None, font=FONTE, caps=False):
run = p.add_run(text)
run.bold = bold; run.italic = italic
if size: run.font.size = size
if color: run.font.color.rgb = RGBColor.from_string(color)
run.font.name = font
if caps: run.font.all_caps = True
return run
def _add_highlight_to_run(run, highlight_color="cyan"):
"""Aplica highlight (fundo colorido) a um run via XML.
Aceita nomes Word (cyan, yellow...) ou hex 6-char para shading."""
rPr = run._r.get_or_add_rPr()
if len(highlight_color) == 6 and all(c in '0123456789ABCDEFabcdef' for c in highlight_color):
# Hex color → usar shading (aceita qualquer cor)
rPr.append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="{highlight_color}" w:val="clear"/>'))
else:
# Nome Word predefinido (cyan, yellow, etc)
rPr.append(parse_xml(f'<w:highlight {nsdecls("w")} w:val="{highlight_color}"/>'))
def fmt_with_meta_badges(p, text, size=None, color="1A1A1A", font=FONTE, pal=None):
"""Renderiza texto com badges visuais para Metas OMS (Meta 1..Meta 6).
Metas ficam em bold + highlight com cor terciária da paleta."""
import re
pattern = re.compile(r'(Meta\s+[1-6])', re.IGNORECASE)
parts = pattern.split(text)
badge_color = pal["tertiary"] if pal else "cyan"
badge_text_color = pal["primary_dark"] if pal else "1A3A6E"
for part in parts:
if pattern.match(part):
run = p.add_run(f" {part} ")
run.bold = True
run.font.name = font
if size: run.font.size = size
run.font.color.rgb = RGBColor.from_string(badge_text_color)
_add_highlight_to_run(run, badge_color)
else:
if part:
run = p.add_run(part)
run.font.name = font
if size: run.font.size = size
if color: run.font.color.rgb = RGBColor.from_string(color)
def spacing(p, before=0, after=0, line=1.15):
pf = p.paragraph_format
pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line
def p_shading(p, color):
p._p.get_or_add_pPr().append(
parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}" w:val="clear"/>'))
def repeat_header(row):
row._tr.get_or_add_trPr().append(parse_xml(f'<w:tblHeader {nsdecls("w")}/>'))
def page_break(doc):
p = doc.add_paragraph()
p.add_run()._r.append(parse_xml(f'<w:br {nsdecls("w")} w:type="page"/>'))
# ============================================================
# HEADER + FOOTER
# ============================================================
def build_header(section, meta, pal, logo_bytes=None):
header = section.header
header.is_linked_to_previous = False
for p in header.paragraphs: p.clear()
tbl = header.add_table(rows=4, cols=3, width=Cm(16.5))
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
cw = [1700, 5800, 2900]
brd = (pal["gray_border"], "4")
for row in tbl.rows:
for i, cell in enumerate(row.cells):
set_width(cell, cw[i]); set_valign(cell)
set_margins(cell, 30, 30, 80, 80)
set_borders(cell, brd, brd, brd, brd)
# Logo
logo_cell = tbl.cell(0,0).merge(tbl.cell(3,0))
logo_cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
if logo_bytes:
from docx.shared import Inches as In
run = logo_cell.paragraphs[0].add_run()
run.add_picture(io.BytesIO(logo_bytes), height=Cm(1.8))
else:
fmt(logo_cell.paragraphs[0], "[LOGO]", bold=True, size=Pt(10), color="888888")
# Título — PRETO
tc = tbl.cell(0,1).merge(tbl.cell(1,1))
tc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(tc.paragraphs[0], "PROCEDIMENTO OPERACIONAL PADRÃO",
bold=True, size=Pt(13), color="000000")
# Nome do processo — PRETO
pc = tbl.cell(2,1).merge(tbl.cell(3,1))
pc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(pc.paragraphs[0], meta.get("titulo_processo","").upper(),
bold=True, size=Pt(12), color="000000")
# Metadados — PRETO
for i, (lbl, val) in enumerate([
("Código: ", meta.get("codigo","POP-XXX-001")),
("Elaborado em: ", meta.get("data_elaboracao","")),
("Revisado em: ", meta.get("data_revisao","—")),
("Válido até: ", meta.get("validade","")),
]):
c = tbl.cell(i, 2)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], lbl, bold=True, size=TAM_HEADER, color="000000")
fmt(c.paragraphs[0], val or "—", size=TAM_HEADER, color="000000")
def build_footer(section, meta, pal):
footer = section.footer
footer.is_linked_to_previous = False
for p in footer.paragraphs: p.clear()
footer.add_paragraph() # separator
tbl = footer.add_table(rows=2, cols=3, width=Cm(16.5))
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
cw = [3500, 3500, 3400]
brd = (pal["gray_border"], "4")
for row in tbl.rows:
for i, cell in enumerate(row.cells):
set_width(cell, cw[i]); set_valign(cell)
set_margins(cell, 30, 30, 60, 60)
set_borders(cell, brd, brd, brd, brd)
for i, h in enumerate(["Elaborado por:", "Validado por:", "Aprovado por:"]):
c = tbl.cell(0, i)
set_shading(c, pal["tertiary"]) # Cor terciária no header do rodapé
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color=pal["primary_dark"])
for i, key in enumerate(["elaborado_por", "validado_por", "aprovado_por"]):
c = tbl.cell(1, i)
person = meta.get(key, {})
nome = person.get("nome","") if isinstance(person, dict) else ""
cargo = person.get("cargo","") if isinstance(person, dict) else ""
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
if nome:
fmt(c.paragraphs[0], nome, bold=True, size=TAM_SMALL, color=pal["text_body"])
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(p2, cargo, size=TAM_TINY, color="666666")
else:
fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA")
# Página X de Y
pp = footer.add_paragraph()
pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
spacing(pp, 2, 0)
fmt(pp, "Página ", size=TAM_TINY, color="888888")
# Campo PAGE (número atual)
for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>',
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
pp.add_run()._r.append(parse_xml(x))
r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888")
pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
# " de " + NUMPAGES (total)
fmt(pp, " de ", size=TAM_TINY, color="888888")
for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
f'<w:instrText {nsdecls("w")} xml:space="preserve"> NUMPAGES </w:instrText>',
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
pp.add_run()._r.append(parse_xml(x))
r2 = pp.add_run("1"); r2.font.size = TAM_TINY; r2.font.color.rgb = RGBColor.from_string("888888")
pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}",
size=TAM_TINY, color="888888")
# ============================================================
# ELEMENTOS DE CONTEÚDO
# ============================================================
def add_h1(doc, num, titulo, pal):
p = doc.add_paragraph(); spacing(p, 16, 8)
p_shading(p, pal["primary"])
pf = p.paragraph_format; pf.left_indent = Cm(0.3); pf.right_indent = Cm(0.3)
fmt(p, f" {num}. {titulo.upper()}", bold=True, size=TAM_H1, color=pal["text_on_primary"])
def add_h2(doc, num, titulo, pal):
p = doc.add_paragraph(); spacing(p, 12, 6)
p_shading(p, pal["secondary"])
pf = p.paragraph_format; pf.left_indent = Cm(IND_H2 + 0.3); pf.right_indent = Cm(0.3)
fmt(p, f" {num}. {titulo.upper()}", bold=True, size=TAM_H2, color=pal["text_on_secondary"])
def add_body(doc, text, indent=IND_BODY):
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
spacing(p, 2, 5, 1.15); p.paragraph_format.left_indent = Cm(indent)
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
def add_bullet(doc, text, bold_prefix=None, indent=IND_ITEM, pal=None):
c = pal["primary"] if pal else DEFAULT_PRIMARY
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
spacing(p, 1, 3, 1.15)
p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.4)
fmt(p, "● ", size=Pt(9), color=c, bold=True)
if bold_prefix:
fmt(p, bold_prefix, bold=True, size=TAM_CORPO, color=c)
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
else:
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
def add_num(doc, n, text, indent=IND_SUBITEM, pal=None, critico=False):
c = pal["primary"] if pal else DEFAULT_PRIMARY
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
spacing(p, 1, 4, 1.15)
p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5)
if critico:
fmt(p, f"\u26a0 ", bold=True, size=TAM_CORPO, color="C05000")
fmt(p, f"{n}. [PASSO CR\u00cdTICO] ", bold=True, size=TAM_CORPO, color="C05000")
else:
fmt(p, f"{n}. ", bold=True, size=TAM_CORPO, color=c)
# Texto com badges de Meta OMS
fmt_with_meta_badges(p, text, size=TAM_CORPO, pal=pal)
def add_def_item(doc, termo, defn, pal):
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
spacing(p, 2, 5, 1.15)
p.paragraph_format.left_indent = Cm(IND_ITEM); p.paragraph_format.first_line_indent = Cm(-0.4)
fmt(p, "● ", size=Pt(9), color=pal["primary"], bold=True)
fmt(p, f"{termo}: ", bold=True, size=TAM_CORPO, color=pal["primary"])
fmt(p, defn, size=TAM_CORPO, color="1A1A1A")
def add_risk(doc, risco, barreira, pal):
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
spacing(p, 4, 3, 1.15)
p.paragraph_format.left_indent = Cm(IND_ITEM); p.paragraph_format.first_line_indent = Cm(-0.5)
fmt(p, "⚠ ", size=TAM_CORPO, color=pal["risk_red"], bold=True)
fmt(p, "Risco: ", bold=True, size=TAM_CORPO, color=pal["risk_red"])
fmt(p, risco, size=TAM_CORPO, color="1A1A1A")
p2 = doc.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
spacing(p2, 0, 8, 1.15); p2.paragraph_format.left_indent = Cm(IND_SUBITEM)
fmt(p2, "↳ Barreira de Segurança: ", bold=True, size=TAM_CORPO, color=pal["barrier_green"])
fmt(p2, barreira, size=TAM_CORPO, color="1A1A1A")
# ============================================================
# TABELAS
# ============================================================
def add_table(doc, headers, rows, col_widths=None, pal=None):
nc = len(headers)
if not col_widths: col_widths = [LARGURA_DXA // nc] * nc
tbl = doc.add_table(rows=1+len(rows), cols=nc)
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
brd = (pal["border_light"], "4") if pal else ("3B6AA0", "4")
brd2 = (pal["gray_border"], "2") if pal else ("7F8C9A", "2")
for i, h in enumerate(headers):
c = tbl.cell(0, i); set_shading(c, pal["primary"] if pal else DEFAULT_PRIMARY)
set_width(c, col_widths[i]); set_valign(c)
set_margins(c, 50, 50, 80, 80); set_borders(c, brd, brd, brd, brd)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF")
repeat_header(tbl.rows[0])
for ri, rd in enumerate(rows):
for ci, val in enumerate(rd):
c = tbl.cell(ri+1, ci); set_width(c, col_widths[ci]); set_valign(c)
set_margins(c, 40, 40, 80, 80); set_borders(c, brd2, brd2, brd2, brd2)
a = WD_ALIGN_PARAGRAPH.LEFT if ci == 0 and nc > 2 else WD_ALIGN_PARAGRAPH.CENTER
c.paragraphs[0].alignment = a
fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A")
if ri % 2 == 0:
for ci in range(nc):
set_shading(tbl.cell(ri+1, ci), pal["tertiary"] if pal else "E8EFF7")
doc.add_paragraph()
# ============================================================
# SMARTART
# ============================================================
def add_process(doc, titulo, etapas, pal):
if not etapas: return
p = doc.add_paragraph(); spacing(p, 8, 6)
p.paragraph_format.left_indent = Cm(IND_BODY)
fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
n = len(etapas); total = n*2-1; aw = 400
bw = (LARGURA_DXA - aw*(n-1)) // n
tbl = doc.add_table(rows=1, cols=total)
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
colors = [pal["primary"], pal["primary"], pal["primary"], pal["primary"]]
for ci in range(total):
c = tbl.cell(0, ci)
if ci % 2 == 0:
ei = ci // 2
set_width(c, bw); set_shading(c, pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15))
bg = pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15)
set_borders(c, (bg,"6"), (bg,"6"), (bg,"6"), (bg,"6"))
set_valign(c); set_margins(c, 60, 60, 80, 80)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], f"Etapa {ei+1}", bold=True, size=TAM_TINY, color="FFFFFF")
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(p2, etapas[ei], size=TAM_SMALL, color="FFFFFF")
else:
set_width(c, aw)
set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"))
set_valign(c); c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], "→", bold=True, size=Pt(16), color=pal["primary"])
doc.add_paragraph()
def add_checklist(doc, titulo, itens, pal):
if not itens: return
p = doc.add_paragraph(); spacing(p, 8, 6)
p.paragraph_format.left_indent = Cm(IND_BODY)
fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
tbl = doc.add_table(rows=len(itens), cols=2)
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
chw = 600; tw = LARGURA_DXA - chw
for ri, item in enumerate(itens):
cc = tbl.cell(ri, 0); set_width(cc, chw); set_valign(cc)
set_margins(cc, 40, 40, 40, 40); set_shading(cc, pal["primary"])
set_borders(cc,("FFFFFF","2"),("FFFFFF","2"),(pal["primary"],"4"),("FFFFFF","2"))
cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(cc.paragraphs[0], "✓", bold=True, size=Pt(14), color="FFFFFF")
ct = tbl.cell(ri, 1); set_width(ct, tw); set_valign(ct)
set_margins(ct, 50, 50, 120, 80)
bg = pal["quaternary"] if ri % 2 == 0 else "FFFFFF"
set_shading(ct, bg)
set_borders(ct,(pal["gray_border"],"2"),(pal["gray_border"],"2"),("FFFFFF","0"),(pal["gray_border"],"2"))
ct.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT
fmt(ct.paragraphs[0], item, size=TAM_CORPO, color="1A1A1A")
doc.add_paragraph()
def add_cycle(doc, titulo, etapas, pal):
if not etapas: return
p = doc.add_paragraph(); spacing(p, 8, 6)
p.paragraph_format.left_indent = Cm(IND_BODY)
fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
n = len(etapas); cpr = min(n, 4); rows_n = (n+cpr-1)//cpr
bw = LARGURA_DXA // cpr
tbl = doc.add_table(rows=rows_n, cols=cpr)
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
for idx, et in enumerate(etapas):
r, c_idx = idx//cpr, idx%cpr
c = tbl.cell(r, c_idx); set_width(c, bw)
bg = pal["primary"] if idx % 2 == 0 else lighten(pal["primary"], 0.15)
set_shading(c, bg)
set_borders(c,("FFFFFF","4"),("FFFFFF","4"),("FFFFFF","4"),("FFFFFF","4"))
set_valign(c); set_margins(c, 60, 60, 80, 80)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
arrow = " →" if idx < n-1 else " ⟳"
fmt(c.paragraphs[0], f"{idx+1}{arrow}", bold=True, size=TAM_TINY, color="FFFFFF")
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(p2, et, size=TAM_SMALL, color="FFFFFF")
for idx in range(n, rows_n*cpr):
c = tbl.cell(idx//cpr, idx%cpr)
set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"))
doc.add_paragraph()
# ============================================================
# ANEXOS COM BORDA — 1 PÁGINA POR ANEXO, BORDA FULL-HEIGHT
# ============================================================
# Altura da caixa bordada em DXA (~14.5cm para caber com H1+descrição na mesma página)
ANNEX_BOX_HEIGHT_DXA = 8200
def _set_row_height(row, height_dxa):
"""Define altura mínima de uma linha de tabela."""
trPr = row._tr.get_or_add_trPr()
trPr.append(parse_xml(
f'<w:trHeight {nsdecls("w")} w:val="{height_dxa}" w:hRule="atLeast"/>'))
def _fill_cell_blank_lines(cell, n=8):
"""Adiciona linhas em branco ao final de uma célula para preenchimento vertical."""
for _ in range(n):
p = cell.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
def start_annex_border(doc, pal):
"""Cria container bordado com altura mínima full-page."""
tbl = doc.add_table(rows=1, cols=1)
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
c = tbl.cell(0, 0)
brd = (pal["annex_border"], "6")
set_borders(c, brd, brd, brd, brd)
set_width(c, LARGURA_DXA)
set_margins(c, 150, 150, 250, 250)
_set_row_height(tbl.rows[0], ANNEX_BOX_HEIGHT_DXA)
return c, tbl
# ============================================================
# GERADOR DE DIAGRAMAS VIA GRAPHVIZ (imagens PNG)
# ============================================================
def gerar_diagrama_png(tipo, conteudo, titulo, pal, context="annex"):
"""Gera PNG de diagrama profissional via Graphviz. Retorna path ou None.
context: 'inline' (no corpo do procedimento) ou 'annex' (página de anexo)."""
if not HAS_GRAPHVIZ:
return None
try:
dot = gv.Digraph(format='png')
dot.attr(dpi='120', bgcolor='transparent', margin='0.15', nodesep='0.4', ranksep='0.5')
node_style = {
'style': 'filled,rounded', 'shape': 'box', 'fontname': 'Arial',
'fontsize': '11', 'fillcolor': f'#{pal["primary"]}',
'fontcolor': '#FFFFFF', 'color': f'#{pal["primary_dark"]}', 'penwidth': '1.2',
'height': '0.5', 'margin': '0.12,0.06'
}
node_light = {**node_style, 'fillcolor': f'#{pal["secondary"]}',
'fontcolor': f'#{pal["text_on_secondary"]}'}
node_ter = {**node_style, 'fillcolor': f'#{pal["tertiary"]}',
'fontcolor': f'#{pal["primary_dark"]}'}
edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'}
node_decision = {**node_style, 'shape': 'diamond', 'height': '0.7', 'width': '1.2'}
# Mapear formas
forma_map = {'elipse': 'ellipse', 'losango': 'diamond', 'retangulo': 'box',
'ellipse': 'ellipse', 'diamond': 'diamond', 'box': 'box'}
def _parse_etapa(et, idx=0, total=1):
if isinstance(et, dict):
txt = et.get('texto', et.get('titulo', str(et)))[:30]
frm = forma_map.get(et.get('forma', 'retangulo'), 'box')
return txt, frm
txt = str(et)[:30]
if idx == 0 or idx == total - 1:
return txt, 'ellipse'
if '?' in str(et):
return txt, 'diamond'
return txt, 'box'
if tipo in ("processo", "fluxograma"):
etapas_raw = conteudo if isinstance(conteudo, list) else [conteudo]
has_decision = any(
(isinstance(e, dict) and e.get('forma') in ('losango','diamond'))
or (isinstance(e, str) and '?' in e)
for e in etapas_raw
)
if context == "inline" and not has_decision:
# ── INLINE TIPO 1: Linear simples, max 5 etapas ──
etapas = etapas_raw[:5]
dot.attr(rankdir='LR', size='8,2!')
for i, et in enumerate(etapas):
txt, frm = _parse_etapa(et, i, len(etapas))
st = {**(node_style if i % 2 == 0 else node_light), 'shape': frm}
dot.node(f'n{i}', txt, **st)
if i > 0:
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
elif context == "inline" and has_decision:
# ── INLINE TIPO 2: 2 sequenciais + decisão + split ──
# Layout: n0 → n1 (mesma linha)
# ↓
# n2 (losango)
# ↙ ↘
# n3 n4
etapas = etapas_raw[:5]
dot.attr(rankdir='TB', size='7,4!')
for i, et in enumerate(etapas):
txt, frm = _parse_etapa(et, i, len(etapas))
if i == 2 or frm == 'diamond':
st = {**node_decision}
elif i < 2:
st = {**node_style, 'shape': frm}
else:
st = {**node_light, 'shape': frm}
dot.node(f'n{i}', txt, **st)
# Edges: 0→1, 1→2, 2→3(left), 2→4(right)
if len(etapas) >= 2: dot.edge('n0', 'n1', **edge_style)
if len(etapas) >= 3: dot.edge('n1', 'n2', **edge_style)
if len(etapas) >= 4: dot.edge('n2', 'n3', **edge_style, label=' N\u00e3o', fontsize='9', fontcolor=f'#{pal["primary"]}')
if len(etapas) >= 5: dot.edge('n2', 'n4', **edge_style, label=' Sim', fontsize='9', fontcolor=f'#{pal["primary"]}')
# Ranks
dot.body.append(' { rank=same; n0 n1 }')
if len(etapas) >= 4:
rank_bottom = 'n3'
if len(etapas) >= 5: rank_bottom += ' n4'
dot.body.append(f' {{ rank=same; {rank_bottom} }}')
else:
# ── ANEXO: Layout 3 colunas com splits em decisões ──
etapas = etapas_raw[:10]
n = len(etapas)
dot.attr(rankdir='TB', size='7,4!')
# Criar todos os nós
for i, et in enumerate(etapas):
txt, frm = _parse_etapa(et, i, n)
if frm == 'diamond':
st = {**node_decision}
elif i % 2 == 0:
st = {**node_style, 'shape': frm}
else:
st = {**node_light, 'shape': frm}
dot.node(f'n{i}', txt, **st)
# Edges inteligentes: detectar diamonds e criar splits
diamonds = [i for i in range(n) if _parse_etapa(etapas[i], i, n)[1] == 'diamond']
if diamonds:
# Edges sequenciais até o primeiro diamond
d1 = diamonds[0]
for i in range(d1):
dot.edge(f'n{i}', f'n{i+1}', **edge_style)
# Primeiro split
left_idx = d1 + 1 if d1 + 1 < n else None
right_idx = d1 + 2 if d1 + 2 < n else None
if left_idx is not None:
dot.edge(f'n{d1}', f'n{left_idx}', **edge_style,
label=' N\u00e3o', fontsize='9', fontcolor=f'#{pal["primary"]}')
if right_idx is not None:
dot.edge(f'n{d1}', f'n{right_idx}', **edge_style,
label=' Sim', fontsize='9', fontcolor=f'#{pal["primary"]}')
# Ranks: centro, split esquerdo/direito
if left_idx and right_idx:
dot.body.append(f' {{ rank=same; n{left_idx} n{right_idx} }}')
# Continuação após split
used = set(range(d1 + 1)) | {d1, left_idx, right_idx}
remaining = [i for i in range(n) if i not in used and i is not None]
# Left path
if left_idx is not None and remaining:
left_next = remaining[0]
dot.edge(f'n{left_idx}', f'n{left_next}', **edge_style)
remaining = remaining[1:]
# Right path: sequencial
prev_right = right_idx
for ri in remaining:
if prev_right is not None:
frm_ri = _parse_etapa(etapas[ri], ri, n)[1]
if frm_ri == 'diamond' and ri + 1 < n and ri + 2 < n:
# Segundo split
dot.edge(f'n{prev_right}', f'n{ri}', **edge_style)
dot.edge(f'n{ri}', f'n{ri+1}', **edge_style,
label=' N\u00e3o', fontsize='9', fontcolor=f'#{pal["primary"]}')
dot.edge(f'n{ri}', f'n{ri+2}', **edge_style,
label=' Sim', fontsize='9', fontcolor=f'#{pal["primary"]}')
dot.body.append(f' {{ rank=same; n{ri+1} n{ri+2} }}')
break
else:
dot.edge(f'n{prev_right}', f'n{ri}', **edge_style)
prev_right = ri
else:
# Sem diamonds: zigzag simples
per_row = 3
for i in range(n):
if i > 0:
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
for row_start in range(0, n, per_row):
row_nodes = ' '.join(f'n{j}' for j in range(row_start, min(row_start + per_row, n)))
dot.body.append(f' {{ rank=same; {row_nodes} }}')
elif tipo == "hierarquia":
dot.attr(rankdir='TB', size='8,4.5!')
items = conteudo if isinstance(conteudo, list) else [conteudo]
for i, item in enumerate(items):
if isinstance(item, dict):
lbl = item.get('titulo', '')[:30]
dot.node(f'g{i}', lbl, **node_style)
for j, sub in enumerate(item.get('subitens', [])):
dot.node(f'g{i}s{j}', sub[:25], **node_light)
dot.edge(f'g{i}', f'g{i}s{j}', **edge_style)
else:
dot.node(f'g{i}', str(item)[:30], **node_style)
if i > 0:
dot.edge(f'g{i-1}', f'g{i}', **edge_style)
elif tipo == "piramide":
dot.attr(rankdir='TB', size='7,5!')
items = conteudo if isinstance(conteudo, list) else [conteudo]
for i, item in enumerate(items):
w = str(max(1.5, 3.5 - i * 0.4))
st = {**node_style, 'width': w, 'fixedsize': 'true', 'height': '0.45'}
if i > len(items) // 2:
st = {**node_ter, 'width': w, 'fixedsize': 'true', 'height': '0.45'}
dot.node(f'p{i}', str(item)[:30], **st)
if i > 0:
dot.edge(f'p{i-1}', f'p{i}', **edge_style, style='invis')
for i in range(len(items)):
dot.body.append(f' {{ rank=same; p{i} }}')
elif tipo == "ciclo":
dot.attr(rankdir='LR', size='8,3!') # Horizontal e compacto
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
n = len(etapas)
for i, et in enumerate(etapas):
st = node_style if i % 2 == 0 else node_light
dot.node(f'c{i}', et[:30], **st)
for i in range(n):
dot.edge(f'c{i}', f'c{(i+1)%n}', **edge_style)
else:
return None
out = os.path.join(tempfile.gettempdir(), f"diag_{tipo}_{id(conteudo)}")
return dot.render(out, cleanup=True)
except Exception:
return None
def _inserir_diagrama_na_celula(cell, img_path):
"""Insere imagem PNG do diagrama dentro de uma célula do docx.
Limita largura a 15cm E altura a 10cm para caber na caixa do anexo."""
if not img_path or not os.path.exists(img_path):
return False
try:
from PIL import Image as PILImage
img = PILImage.open(img_path)
w_px, h_px = img.size
# Calcular dimensões respeitando limites
max_w = Cm(15)
max_h = Cm(10)
ratio = w_px / h_px
width = max_w
height = int(width / ratio) if ratio > 0 else max_h
if height > max_h:
height = max_h
width = int(height * ratio)
p = cell.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
run.add_picture(img_path, width=width)
return True
except Exception:
try:
p = cell.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
run.add_picture(img_path, width=Cm(12))
return True
except:
return False
def _inserir_diagrama_no_corpo(doc, img_path, legenda="", pal=None):
"""Insere imagem PNG de diagrama no corpo do documento (fora de tabela/célula).
Centralizado, com legenda em itálico abaixo."""
if not img_path or not os.path.exists(img_path):
return
try:
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(8)
p.paragraph_format.space_after = Pt(4)
run = p.add_run()
run.add_picture(img_path, width=Cm(14))
if legenda:
pl = doc.add_paragraph()
pl.alignment = WD_ALIGN_PARAGRAPH.CENTER
pl.paragraph_format.space_before = Pt(2)
pl.paragraph_format.space_after = Pt(8)
fmt(pl, legenda, italic=True, size=Pt(9),
color=pal["primary"] if pal else "333333")
except Exception:
pass
def _render_passo(doc, num, item, pal):
"""Renderiza um item de procedimento: string, dict com texto/prioridade, ou dict com diagrama.
Formatos aceitos:
- "texto simples"
- {"texto": "...", "prioridade": "critica"}
- {"diagrama": {"tipo":..., "titulo":..., "conteudo":...}}
- {"tipo":..., "titulo":..., "conteudo":...}
"""
if isinstance(item, str):
add_num(doc, num, item, pal=pal)
elif isinstance(item, dict):
# Formato 1: {"texto": "...", "prioridade": "critica"}
if "texto" in item and "diagrama" not in item and "tipo" not in item:
texto = item.get("texto", "")
critico = item.get("prioridade", "").lower() == "critica"
add_num(doc, num, texto, pal=pal, critico=critico)
return
# Formato 2: diagrama (com ou sem wrapper "diagrama")
diag = item.get("diagrama", None)
if diag is None and "tipo" in item:
diag = item
if diag is None:
# Dict não reconhecido — texto fallback
add_num(doc, num, str(item), pal=pal)
return
tipo = diag.get("tipo", "processo")
titulo = diag.get("titulo", "Diagrama")
conteudo = diag.get("conteudo", [])
img_path = gerar_diagrama_png(tipo, conteudo, titulo, pal, context="inline")
if img_path:
_inserir_diagrama_no_corpo(doc, img_path, titulo, pal)
else:
items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo)
add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal)
def render_annex(doc, anexo, num, pal):
page_break(doc)
titulo = anexo.get("titulo", anexo) if isinstance(anexo, dict) else str(anexo)
add_h1(doc, f"ANEXO {num}", titulo.upper(), pal)
if isinstance(anexo, str):
add_body(doc, f"Conteúdo do {titulo} — a ser inserido pela instituição.")
return
if anexo.get("descricao"):
add_body(doc, anexo["descricao"])
tipo = anexo.get("tipo", "texto")
conteudo = anexo.get("conteudo", "")
# Criar container bordado com altura full-page
bc, border_tbl = start_annex_border(doc, pal)
# Para tipos de diagrama: tentar Graphviz primeiro
graphviz_ok = False
if tipo in ("processo", "fluxograma", "hierarquia", "piramide", "ciclo"):
img_path = gerar_diagrama_png(tipo, conteudo, titulo, pal)
if img_path:
_inserir_diagrama_na_celula(bc, img_path)
graphviz_ok = True
_fill_cell_blank_lines(bc, 6)
# === CHECKLIST ===
if graphviz_ok:
pass # Graphviz renderizou — pular SmartArt de tabela
elif tipo == "checklist":
itens = conteudo if isinstance(conteudo, list) else [conteudo]
pt = bc.paragraphs[0]; pt.alignment = WD_ALIGN_PARAGRAPH.LEFT
fmt(pt, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
inner = bc.add_table(rows=len(itens), cols=2)
chw = 500; tw = LARGURA_DXA - 900
for ri, item in enumerate(itens):
cc = inner.cell(ri, 0); set_width(cc, chw); set_valign(cc)
set_margins(cc, 30, 30, 30, 30); set_shading(cc, pal["primary"])
set_borders(cc,("FFFFFF","2"),("FFFFFF","2"),(pal["primary"],"4"),("FFFFFF","2"))
cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(cc.paragraphs[0], "✓", bold=True, size=Pt(12), color="FFFFFF")
ct = inner.cell(ri, 1); set_width(ct, tw); set_valign(ct)
set_margins(ct, 40, 40, 100, 60)
set_shading(ct, pal["quaternary"] if ri % 2 == 0 else "FFFFFF")
set_borders(ct,(pal["gray_border"],"1"),(pal["gray_border"],"1"),("FFFFFF","0"),(pal["gray_border"],"1"))
fmt(ct.paragraphs[0], item, size=TAM_SMALL, color="1A1A1A")
_fill_cell_blank_lines(bc, max(1, 12 - len(itens)))
# === ESCALA / TABELA ===
elif tipo in ("escala", "tabela", "referencia", "referência"):
headers = anexo.get("colunas", [])
rows_data = anexo.get("linhas", [])
# Handle Gemini format: conteudo = [{colunas: [...], linhas: [...]}]
if not headers and isinstance(conteudo, list) and len(conteudo) > 0 and isinstance(conteudo[0], dict):
tbl_obj = conteudo[0]
headers = tbl_obj.get("colunas", [])
rows_data = tbl_obj.get("linhas", [])
# Fallback: if conteudo is list of lists (actual row data) and no linhas at annexo level
if not rows_data and isinstance(conteudo, list) and len(conteudo) > 0 and isinstance(conteudo[0], list):
rows_data = conteudo
if headers and rows_data:
bc.paragraphs[0].text = ""
nc = len(headers); cw_each = (LARGURA_DXA - 900) // nc
inner = bc.add_table(rows=1+len(rows_data), cols=nc)
for i, h in enumerate(headers):
c = inner.cell(0, i); set_shading(c, pal["primary"])
set_width(c, cw_each); set_valign(c); set_margins(c, 40, 40, 60, 60)
set_borders(c,(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"))
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF")
for ri, rd in enumerate(rows_data):
for ci, val in enumerate(rd):
c = inner.cell(ri+1, ci); set_width(c, cw_each); set_valign(c)
set_margins(c, 30, 30, 60, 60)
set_borders(c,(pal["gray_border"],"1"),(pal["gray_border"],"1"),
(pal["gray_border"],"1"),(pal["gray_border"],"1"))
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A")
if ri % 2 == 0:
for ci in range(nc): set_shading(inner.cell(ri+1, ci), pal["tertiary"])
_fill_cell_blank_lines(bc, max(1, 10 - len(rows_data)))
# === PROCESSO / FLUXOGRAMA ===
elif tipo in ("processo", "fluxograma"):
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
etapas = etapas[:6]
fmt(bc.paragraphs[0], f"▸ Fluxo do Processo", bold=True, size=TAM_CORPO, color=pal["primary"])
n = len(etapas); total = n*2-1; aw = 350
bw = (LARGURA_DXA - 900 - aw*(n-1)) // n
inner = bc.add_table(rows=1, cols=total)
for ci in range(total):
c = inner.cell(0, ci)
if ci % 2 == 0:
ei = ci // 2; set_width(c, bw)
bg = pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15)
set_shading(c, bg); set_borders(c,(bg,"4"),(bg,"4"),(bg,"4"),(bg,"4"))
set_valign(c); set_margins(c, 50, 50, 60, 60)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], f"Etapa {ei+1}", bold=True, size=TAM_TINY, color="FFFFFF")
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(p2, etapas[ei], size=Pt(8), color="FFFFFF")
else:
set_width(c, aw)
set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"))
set_valign(c); c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], "\u2192", bold=True, size=Pt(14), color=pal["primary"])
_fill_cell_blank_lines(bc, 15)
# === HIERARQUIA (organograma / classificação em níveis) ===
elif tipo == "hierarquia":
items = conteudo if isinstance(conteudo, list) else [conteudo]
fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
for item in items:
if isinstance(item, dict):
# Nível 1: box colorido
p1 = bc.add_paragraph()
p1.paragraph_format.space_before = Pt(6)
p1_shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{pal["primary"]}" w:val="clear"/>')
p1._p.get_or_add_pPr().append(p1_shading)
fmt(p1, f" {item.get('titulo', '')}", bold=True, size=TAM_CORPO, color="FFFFFF")
# Nível 2: subitens indentados com barra lateral
for sub in item.get("subitens", []):
p2 = bc.add_paragraph()
p2.paragraph_format.left_indent = Cm(1.0)
p2.paragraph_format.space_before = Pt(2)
p2_shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{pal["tertiary"]}" w:val="clear"/>')
p2._p.get_or_add_pPr().append(p2_shading)
fmt(p2, f" \u25b9 {sub}", size=TAM_SMALL, color=pal["primary_dark"])
elif isinstance(item, str):
p1 = bc.add_paragraph()
p1_shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{pal["secondary"]}" w:val="clear"/>')
p1._p.get_or_add_pPr().append(p1_shading)
fmt(p1, f" {item}", bold=True, size=TAM_SMALL, color=pal["primary_dark"])
_fill_cell_blank_lines(bc, max(1, 8 - len(items)))
# === PIRÂMIDE (de cima para baixo, estreito→largo) ===
elif tipo == "piramide":
items = conteudo if isinstance(conteudo, list) else [conteudo]
fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
n = len(items)
max_w = LARGURA_DXA - 900
for i, item in enumerate(items):
# Cada nível fica mais largo (proporção crescente)
level_w = int(max_w * (0.4 + 0.6 * i / max(n-1, 1)))
margin_l = (max_w - level_w) // 2
inner = bc.add_table(rows=1, cols=1)
inner.alignment = WD_TABLE_ALIGNMENT.CENTER
c = inner.cell(0, 0)
set_width(c, level_w)
# Gradiente de cores: topo=primário, base=terciário
frac = i / max(n-1, 1)
r1, g1, b1 = hex_to_rgb(pal["primary"])
r2, g2, b2 = hex_to_rgb(pal["tertiary"])
bg = rgb_to_hex(r1+(r2-r1)*frac, g1+(g2-g1)*frac, b1+(b2-b1)*frac)
set_shading(c, bg)
brd = (bg, "4")
set_borders(c, brd, brd, brd, brd)
set_valign(c); set_margins(c, 40, 40, 80, 80)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
txt_color = "FFFFFF" if frac < 0.5 else pal["primary_dark"]
fmt(c.paragraphs[0], str(item), bold=True, size=TAM_SMALL, color=txt_color)
_fill_cell_blank_lines(bc, max(1, 10 - n))
# === CICLO (PDCA, melhoria contínua) ===
elif tipo == "ciclo":
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
n = len(etapas)
# Layout em grid 2x2 ou linear
if n == 4:
inner = bc.add_table(rows=2, cols=2)
inner.alignment = WD_TABLE_ALIGNMENT.CENTER
positions = [(0,0),(0,1),(1,1),(1,0)] # Sentido horário
colors = [pal["primary"], lighten(pal["primary"],0.15),
lighten(pal["primary"],0.30), lighten(pal["primary"],0.45)]
bw = (LARGURA_DXA - 900) // 2
for idx, (r,c_idx) in enumerate(positions):
c = inner.cell(r, c_idx); set_width(c, bw); set_valign(c)
set_shading(c, colors[idx])
brd = (colors[idx], "4")
set_borders(c, brd, brd, brd, brd)
set_margins(c, 50, 50, 80, 80)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], etapas[idx], bold=True, size=TAM_SMALL, color="FFFFFF")
# Setas no centro
p_arrow = bc.add_paragraph()
p_arrow.alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(p_arrow, "\u21bb Ciclo Cont\u00ednuo", bold=True, size=TAM_SMALL, color=pal["primary"])
else:
cpr = min(n, 4); rows_n = (n+cpr-1)//cpr
bw = (LARGURA_DXA - 900) // cpr
inner = bc.add_table(rows=rows_n, cols=cpr)
inner.alignment = WD_TABLE_ALIGNMENT.CENTER
for idx, et in enumerate(etapas):
r, ci2 = divmod(idx, cpr)
c = inner.cell(r, ci2); set_width(c, bw); set_valign(c)
bg = pal["primary"] if idx % 2 == 0 else lighten(pal["primary"], 0.20)
set_shading(c, bg)
brd = (bg, "4")
set_borders(c, brd, brd, brd, brd)
set_margins(c, 40, 40, 60, 60)
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], et, bold=True, size=TAM_TINY, color="FFFFFF")
_fill_cell_blank_lines(bc, 10)
# === MISTO ===
elif tipo == "misto":
bc.paragraphs[0].text = ""
elementos = conteudo if isinstance(conteudo, list) else [conteudo]
for elem in elementos:
if isinstance(elem, str):
pe = bc.add_paragraph(); fmt(pe, elem, size=TAM_CORPO, color="1A1A1A")
elif isinstance(elem, dict):
st = elem.get("tipo", "texto")
if st == "texto":
pe = bc.add_paragraph(); fmt(pe, elem.get("conteudo",""), size=TAM_CORPO, color="1A1A1A")
elif st == "checklist":
pe = bc.add_paragraph()
fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"])
for it in elem.get("itens", []):
pi = bc.add_paragraph(); pi.paragraph_format.left_indent = Cm(0.5)
fmt(pi, "\u2713 ", bold=True, size=TAM_SMALL, color=pal["primary"])
fmt(pi, it, size=TAM_SMALL, color="1A1A1A")
elif st == "processo":
pe = bc.add_paragraph()
fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"])
for i, et in enumerate(elem.get("etapas",[]), 1):
pi = bc.add_paragraph(); pi.paragraph_format.left_indent = Cm(0.5)
fmt(pi, f"{i}. ", bold=True, size=TAM_SMALL, color=pal["primary"])
fmt(pi, et, size=TAM_SMALL, color="1A1A1A")
elif st == "tabela":
pe = bc.add_paragraph()
fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"])
hdrs = elem.get("colunas",[]); rws = elem.get("linhas",[])
if hdrs and rws:
nc2 = len(hdrs); cw2 = (LARGURA_DXA - 900) // nc2
it2 = bc.add_table(rows=1+len(rws), cols=nc2)
for i, h in enumerate(hdrs):
cc2 = it2.cell(0,i); set_shading(cc2, pal["primary"])
set_width(cc2,cw2); set_valign(cc2); set_margins(cc2,30,30,50,50)
set_borders(cc2,(pal["primary"],"3"),(pal["primary"],"3"),
(pal["primary"],"3"),(pal["primary"],"3"))
cc2.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(cc2.paragraphs[0], h, bold=True, size=TAM_TINY, color="FFFFFF")
for ri2, rd2 in enumerate(rws):
for ci2, v2 in enumerate(rd2):
cc2 = it2.cell(ri2+1,ci2); set_width(cc2,cw2); set_valign(cc2)
set_margins(cc2,25,25,50,50)
set_borders(cc2,(pal["gray_border"],"1"),(pal["gray_border"],"1"),
(pal["gray_border"],"1"),(pal["gray_border"],"1"))
cc2.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(cc2.paragraphs[0], str(v2), size=TAM_TINY, color="1A1A1A")
if ri2 % 2 == 0:
for ci2 in range(nc2): set_shading(it2.cell(ri2+1,ci2), pal["tertiary"])
_fill_cell_blank_lines(bc, 4)
# === GLOSSÁRIO / SÍMBOLOS ===
elif tipo in ("glossario", "glossário", "simbolos", "símbolos", "glossario_visual"):
bc.paragraphs[0].text = ""
if isinstance(conteudo, list):
if len(conteudo) > 0 and isinstance(conteudo[0], dict):
# Formato tabela: [{colunas:..., linhas:...}] ou [{simbolo:..., nome:...}]
tbl_obj = conteudo[0]
if "colunas" in tbl_obj:
headers = tbl_obj.get("colunas", [])
rows_data = tbl_obj.get("linhas", [])
if headers and rows_data:
nc = len(headers); cw_each = (LARGURA_DXA - 900) // nc
inner = bc.add_table(rows=1+len(rows_data), cols=nc)
for i, h in enumerate(headers):
c = inner.cell(0, i); set_shading(c, pal["primary"])
set_width(c, cw_each); set_valign(c); set_margins(c, 40, 40, 60, 60)
set_borders(c,(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"))
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF")
for ri, rd in enumerate(rows_data):
for ci, val in enumerate(rd):
c = inner.cell(ri+1, ci); set_width(c, cw_each); set_valign(c)
set_margins(c, 30, 30, 60, 60)
set_borders(c,(pal["gray_border"],"1"),(pal["gray_border"],"1"),
(pal["gray_border"],"1"),(pal["gray_border"],"1"))
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A")
if ri % 2 == 0:
for ci in range(nc): set_shading(inner.cell(ri+1, ci), pal["tertiary"])
else:
# Formato dict simples: [{simbolo: "⚠", nome: "Atenção"}, ...]
for item in conteudo:
pi = bc.add_paragraph()
pi.paragraph_format.left_indent = Cm(0.5)
simb = item.get("simbolo", item.get("icone", "●"))
nome = item.get("nome", item.get("descricao", str(item)))
fmt(pi, f"{simb} {nome}", size=TAM_CORPO, color="1A1A1A")
else:
# Formato lista simples de strings: "⚠ Atenção" (símbolo + 2 espaços + nome)
for item in conteudo:
pi = bc.add_paragraph()
pi.paragraph_format.left_indent = Cm(0.5)
fmt(pi, str(item), size=TAM_CORPO, color="1A1A1A")
_fill_cell_blank_lines(bc, max(1, 10 - len(conteudo) if isinstance(conteudo, list) else 10))
# === TEXTO / FALLBACK ===
else:
if isinstance(conteudo, str):
fmt(bc.paragraphs[0], conteudo, size=TAM_CORPO, color="1A1A1A")
elif isinstance(conteudo, list):
bc.paragraphs[0].text = ""
for item in conteudo:
pi = bc.add_paragraph(); fmt(pi, f"\u25cf {item}", size=TAM_CORPO, color="1A1A1A")
_fill_cell_blank_lines(bc, 12)
# ============================================================
# GERADOR PRINCIPAL
# ============================================================
def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overrides=None):
data = json.loads(json_str)
meta = data.get("metadata", {})
secoes = data.get("secoes", {})
pal = build_palette(parse_color_input(primary_color))
# Aplicar overrides individuais de cor
if palette_overrides:
for k, v in palette_overrides.items():
if v and v.strip():
pal[k] = parse_color_input(v)
doc = Document()
style = doc.styles["Normal"]
style.font.name = FONTE; style.font.size = TAM_CORPO
style.font.color.rgb = RGBColor.from_string("1A1A1A")
style.paragraph_format.space_after = Pt(4); style.paragraph_format.line_spacing = 1.15
sec = doc.sections[0]
sec.page_width = Cm(21); sec.page_height = Cm(29.7)
sec.top_margin = MARGEM_SUP; sec.bottom_margin = MARGEM_INF
sec.left_margin = MARGEM_ESQ; sec.right_margin = MARGEM_DIR
build_header(sec, meta, pal, logo_bytes)
build_footer(sec, meta, pal)
if doc.paragraphs and doc.paragraphs[0].text == "":
doc.paragraphs[0]._element.getparent().remove(doc.paragraphs[0]._element)
# Seções
sn = 1
add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal)
# Remover space_before do primeiro H1 (margem superior já cuida do espaço)
doc.paragraphs[0].paragraph_format.space_before = Pt(0)
add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]"))
sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal)
add_body(doc, secoes.get("campo_aplicacao", "[Campo não fornecido]"))
sn += 1; add_h1(doc, sn, "CONCEITOS E DEFINIÇÕES", pal)
for it in secoes.get("conceitos", []):
if isinstance(it, dict): add_def_item(doc, it.get("termo",""), it.get("definicao",""), pal)
else: add_bullet(doc, str(it), pal=pal)
sn += 1; add_h1(doc, sn, "RESPONSABILIDADES E COMPETÊNCIAS", pal)
for it in secoes.get("responsabilidades", []):
if isinstance(it, dict):
add_bullet(doc, it.get("acoes",""), bold_prefix=f"{it.get('papel','')}: ", pal=pal)
else: add_bullet(doc, str(it), pal=pal)
sn += 1; add_h1(doc, sn, "RECURSOS NECESSÁRIOS (MATERIAIS E EQUIPAMENTOS)", pal)
for it in secoes.get("recursos", []):
add_bullet(doc, str(it), pal=pal)
sn += 1; add_h1(doc, sn, "DESCRIÇÃO DO PROCEDIMENTO (PASSO A PASSO)", pal)
proc = secoes.get("procedimento", {})
sub = 1
add_h2(doc, f"{sn}.{sub}", "Ações Iniciais e Preparo", pal)
for i, p in enumerate(proc.get("acoes_iniciais",[]), 1): _render_passo(doc, i, p, pal)
sub += 1; add_h2(doc, f"{sn}.{sub}", "Execução Técnica", pal)
for i, p in enumerate(proc.get("execucao_tecnica",[]), 1): _render_passo(doc, i, p, pal)
sub += 1; add_h2(doc, f"{sn}.{sub}", "Ações Finais e Organização", pal)
for i, p in enumerate(proc.get("acoes_finais",[]), 1): _render_passo(doc, i, p, pal)
sn += 1; add_h1(doc, sn, "GERENCIAMENTO DE RISCO E PONTOS CRÍTICOS", pal)
riscos = secoes.get("riscos", {})
add_h2(doc, f"{sn}.1", "Riscos Assistenciais", pal)
# Ordenar riscos: Crítico primeiro, Moderado depois, demais por último
riscos_list = riscos.get("assistenciais", [])
def _risk_sort_key(it):
r = it.get("risco","").lower() if isinstance(it, dict) else str(it).lower()
if "crít" in r or "crit" in r: return 0
if "moder" in r: return 1
return 2
riscos_list = sorted(riscos_list, key=_risk_sort_key)
for it in riscos_list:
if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal)
else: add_bullet(doc, str(it), pal=pal)
add_h2(doc, f"{sn}.2", "Plano de Contingência", pal)
# Caixa de destaque com borda lateral vermelha
cont_raw = riscos.get("contingencia","[Contingência não fornecida]")
# Normalizar: str → lista de 1 item
if isinstance(cont_raw, str):
cont_items = [cont_raw]
elif isinstance(cont_raw, list):
cont_items = cont_raw
else:
cont_items = [str(cont_raw)]
cont_tbl = doc.add_table(rows=1, cols=1)
cont_tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
cc = cont_tbl.cell(0, 0)
set_width(cc, LARGURA_DXA - 200)
set_margins(cc, 80, 80, 200, 200)
set_borders(cc, (pal["risk_red"],"12"), (pal["gray_border"],"1"), (pal["gray_border"],"1"), (pal["gray_border"],"1"))
set_shading(cc, "FFF3E0")
# Título
pt = cc.paragraphs[0]
pt.paragraph_format.space_after = Pt(8)
fmt(pt, "\u26a0 ATEN\u00c7\u00c3O \u2014 Plano de Conting\u00eancia", bold=True, size=TAM_CORPO, color=pal["risk_red"])
# Itens numerados: conceito bold + ":" + ações com seta
for idx, item_c in enumerate(cont_items, 1):
pi = cc.add_paragraph()
pi.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
pi.paragraph_format.line_spacing = 1.15
pi.paragraph_format.left_indent = Cm(0.5)
pi.paragraph_format.space_before = Pt(3)
pi.paragraph_format.space_after = Pt(3)
# Número bold + cor risk_red
fmt(pi, f"{idx}. ", bold=True, size=TAM_SMALL, color=pal["risk_red"])
# Limpar texto: remover prefixos "→ " ou "1. " que o Gemini pode ter adicionado
txt = str(item_c).strip()
import re
txt = re.sub(r'^\d+\.\s*', '', txt) # Remove "1. " prefix
txt = re.sub(r'^[\u2192→]\s*', '', txt) # Remove "→ " prefix
# Separar conceito (antes do primeiro ":") do resto
if ':' in txt:
conceito, resto = txt.split(':', 1)
# Strip markdown bold markers **
conceito = conceito.replace('**', '').strip()
fmt(pi, conceito + ":", bold=True, size=TAM_SMALL, color="1A1A1A")
# Formatar setas com espaçamento
resto = resto.strip()
# Normalizar setas: garantir " → " com espaços
resto = re.sub(r'\s*[\u2192→]\s*', ' \u2192 ', resto)
fmt_with_meta_badges(pi, " " + resto, size=TAM_SMALL, pal=pal)
else:
fmt_with_meta_badges(pi, txt, size=TAM_SMALL, pal=pal)
sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal)
add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser "
"obrigatoriamente documentadas nos seguintes registros para rastreabilidade e auditoria:")
for it in secoes.get("registros",[]): add_bullet(doc, str(it), pal=pal)
sn += 1; add_h1(doc, sn, "INDICADORES DE MONITORAMENTO", pal)
add_body(doc, "A eficácia da adesão a este POP será medida através dos seguintes indicadores institucionais:")
inds = secoes.get("indicadores", [])
if inds and isinstance(inds[0], dict) and "meta" in inds[0]:
add_table(doc, ["Indicador","Meta","Periodicidade"],
[[i.get("nome",""),i.get("meta",""),i.get("periodicidade","")] for i in inds],
[5157,2400,1800], pal)
else:
for it in inds: add_bullet(doc, str(it), pal=pal)
sn += 1; add_h1(doc, sn, "REFERÊNCIAS BIBLIOGRÁFICAS / NORMATIVAS", pal)
for i, ref in enumerate(secoes.get("referencias",[]), 1):
add_num(doc, i, str(ref), indent=IND_ITEM, pal=pal)
sn += 1; add_h1(doc, sn, "ANEXOS / FLUXOGRAMAS", pal)
anexos = secoes.get("anexos", [])
if anexos:
for idx, it in enumerate(anexos, 1):
nome = it.get("titulo", it) if isinstance(it, dict) else str(it)
add_bullet(doc, f"Anexo {idx}: {nome}", pal=pal)
else:
add_body(doc, "Não há anexos vinculados a este POP nesta versão.")
sn += 1; add_h1(doc, sn, "HISTÓRICO DE REVISÕES", pal)
revs = secoes.get("historico_revisoes", [])
if revs:
add_table(doc, ["Versão","Data","Descrição da Alteração","Responsável"],
[[r.get("versao",""),r.get("data",""),r.get("descricao",""),r.get("responsavel","")] for r in revs],
[1000,1200,5557,1600], pal)
# Anexos completos
if anexos:
for idx, anx in enumerate(anexos, 1):
render_annex(doc, anx, idx, pal)
titulo_safe = meta.get("titulo_processo","POP").replace(" ","_").replace("/","-")
codigo = meta.get("codigo","POP-XXX-001")
fp = os.path.join(tempfile.gettempdir(), f"{codigo}_{titulo_safe}.docx")
doc.save(fp)
return fp
# ============================================================
# VALIDADOR
# ============================================================
def validar(json_str):
try: data = json.loads(json_str)
except json.JSONDecodeError as e: return False, f"❌ JSON inválido: {e}", None
erros, avisos = [], []
if "metadata" not in data: erros.append("'metadata' ausente")
else:
for c in ["titulo_processo","codigo","versao","setor"]:
if not data["metadata"].get(c): erros.append(f"metadata.{c} vazio")
if "secoes" not in data: erros.append("'secoes' ausente")
else:
for s in ["objetivo","campo_aplicacao","conceitos","responsabilidades","recursos",
"procedimento","riscos","registros","indicadores","referencias","historico_revisoes"]:
if s not in data["secoes"]: erros.append(f"Seção '{s}' ausente")
if erros:
return False, "❌ ERROS:\n"+"\n".join(f" • {e}" for e in erros), None
msg = "✅ JSON válido!"
if avisos: msg += "\n⚠️ "+"\n".join(avisos)
return True, msg, data
# ============================================================
# GRADIO FRONTEND
# ============================================================
DEFAULT_UI_COLOR = "283264"
def processar(json_text, json_file, c_pri, c_sec, c_ter, c_zeb, logo_file):
json_str = ""
# Prioridade 1: arquivo anexo
if json_file is not None:
try:
fpath = json_file.name if hasattr(json_file, 'name') else json_file
with open(fpath, "r", encoding="utf-8") as f: json_str = f.read()
except Exception as e:
return None, f"\u274c {e}", None
# Prioridade 2: texto colado
if not json_str.strip():
json_str = json_text or ""
if not json_str.strip():
return None, "\u26a0\ufe0f Insira o c\u00f3digo.", None
jc = json_str.strip()
# Remover backticks de codeblock markdown (```json ... ```)
if jc.startswith("```"):
jc = jc.split("\n",1)[1] if "\n" in jc else jc[3:]
jc = jc.strip()
if jc.startswith("json"):
jc = jc[4:].strip()
if jc.endswith("```"):
jc = jc[:-3].strip()
ok, msg, data = validar(jc)
if not ok: return None, msg, None
pri = parse_color_input(c_pri) if c_pri else DEFAULT_UI_COLOR
pal = build_palette(pri)
if c_sec and c_sec.strip(): pal["secondary"] = parse_color_input(c_sec)
if c_ter and c_ter.strip(): pal["tertiary"] = parse_color_input(c_ter)
if c_zeb and c_zeb.strip(): pal["quaternary"] = parse_color_input(c_zeb)
logo_bytes = None
if logo_file is not None:
try:
lp = logo_file if isinstance(logo_file, str) else (logo_file.name if hasattr(logo_file,'name') else None)
if lp and os.path.exists(lp):
with open(lp, "rb") as f: logo_bytes = f.read()
except: pass
try:
overrides = {}
if c_sec and c_sec.strip(): overrides["secondary"] = c_sec
if c_ter and c_ter.strip(): overrides["tertiary"] = c_ter
if c_zeb and c_zeb.strip(): overrides["quaternary"] = c_zeb
fp = gerar_pop_docx(jc, pri, logo_bytes, palette_overrides=overrides)
meta = data.get("metadata",{})
info = "\u2705 POP gerado com sucesso!"
return fp, info, meta
except Exception as e:
return None, f"\u274c {str(e)}", None
def criar_interface():
import gradio as gr
TITLE_COLOR = "#9ab4d2"
init_pal = build_palette(DEFAULT_UI_COLOR)
APP_CSS = f"""
.gradio-container {{ max-width: 1200px !important; }}
#pop-hex-pri, #pop-hex-sec, #pop-hex-ter, #pop-hex-zeb {{
height: 0 !important; overflow: hidden !important;
margin: 0 !important; padding: 0 !important;
}}
#color-state-row {{
height: 0 !important; overflow: hidden !important;
margin: 0 !important; padding: 0 !important; gap: 0 !important;
}}
#json-paste textarea {{
min-height: 120px !important; height: 120px !important; overflow-y: auto !important;
}}
.upload-container span {{ font-size: 10px !important; }}
.upload-container {{ min-height: 130px !important; }}
h1, h3 {{ color: {TITLE_COLOR} !important; }}
.left-col {{ flex-shrink: 0 !important; }}
.step3-row {{ flex-wrap: nowrap !important; }}
.step3-row > div {{ min-width: 0 !important; }}
.landing-title {{ text-align: center; margin-top: 60px; margin-bottom: 0; }}
.landing-title h1 {{ font-size: 2.2em !important; margin-bottom: 0 !important; padding-bottom: 0 !important; }}
.landing-sub {{ text-align: center; color: #9ab4d2 !important; font-size: 0.95em;
margin-top: 8px; position: relative; z-index: 10; }}
.landing-steps {{ margin-top: 40px; }}
.step-card {{ text-align: center; padding: 10px; }}
.step-card button {{ min-height: 80px !important; white-space: normal !important;
line-height: 1.4 !important; padding: 16px 20px !important;
background: #283264 !important; border-color: #283264 !important; color: #fff !important; }}
.step-card button:hover {{ background: #3a4a8a !important; border-color: #3a4a8a !important; }}
.btn-voltar {{ position: absolute; right: 16px; top: 8px; z-index: 100; }}
"""
with gr.Blocks(title="\u2622\ufe0e RADIOTERAPIA.AI - POP de elite") as demo:
# ══════════════════════════════════════
# LANDING PAGE
# ══════════════════════════════════════
with gr.Column(visible=True) as landing_page:
gr.HTML("""
<div style="text-align:center; margin-top:50px;">
<img src="data:image/jpeg;base64,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0t/HPwPpUFjbfa7gL0HVj6Cl1C5FzcfLwijao9qS0VzOWsuUgoooqDQKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAuWMouYDbOepzGfQ1VljMMhVhgg4NIDtNXSP7Wgz/y8IOR/fFX8St1M/hd+hRqWyvZNPuVliba6nioyMGkqU2ndGh1Wkaq8M7X+nKpZlxd2T/cmXvx/hyO1bNu1vrVo91p5Zkj5mgf/W2319V/2h+OK4C1upLKdZImKOvQityw1NNRukngl/s7VEORIhwshr1MNi7rlf3f5f5bMxqUlLU3xS5qCHxHFLIIdUi/s27PSdFzBL7kD7v1XI9hV240+W3hWXAeB/uSxsHjf6MOK9CNpK8df09exyuDi9SHNJmjNGaQBmjOaM5pRQMBSigUZqywzRmjNT2+nS3MLS4WOBPvzSMEjT6seKaTeiAgqS6e30KzS61AsscnMMCf625+nov+0fwyaqS+JIYZTDpcX9pXfed1xBF7hT976tgexrFv9UTTrt7ieY6jqj8tIx3JEf64/T2rGpXhBXWvn0/4L8lp3ZrGHctavqzzzJqGpKoZRizsV4SFe3H+PJPWubvr6TUbpppW3O55PpTbq7kvZzJK5d26k1HXi167qPy/P1/rQ2SsFKBuOKSr1vGNOhE8gzIf9Wp/mawirilKyC4P9m2fkj/Wy8yf7I9Ko06SQyuWJySck02iTuEY2WoUUUUigooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAp0chicMpwR0NNooAvvGurRl0ws6jLL/AHvcVRIwaVJDE4ZSQR0Iq4JI9WGHxFcdm/hf61fxepnrD0KNFSXFs9rJtdSD/Oo6jY0TvqjRsfEcsEPkzqt1b/3JOo+h7Vp6Pfi0lMmk6jLp8r/ehlb5H9j2I9iDXN0V0QxM42vrb7/kxWO4fxRPEP8AiZaQr/8ATezby8+5HK/kBT4PEGjXh4v5rQntc27cfihb+QrjbPWLqw/1M8iD0zx+XSro8WSSj/SLe1uPdo/m/Ou2OPvv+K/VWZm6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/wARXLxrywSI/Mpx/E4UAZPOMV9Yal/wU2+EljpvnxaxqV5LjP2aHTJhJ06ZdVT/AMer5h/a2/4KMan8dNIn8PeGrS40Dw5cfLcvK4+2X6/3W28Ih7qCc92xxXn43IeEcsTxMsW8Q1tTi07vopOOy77foethsxzrF/u/Yqn3k09PRPr959o/soePbH4hfCRbzSIlt9Dtb64sNLiVcbLWB/Ki/EqoJz618N/8FP8A/k66+/7B1r/6Aa6/9i39vjwp+zt8Gv8AhHNe07xHdXa381yj2FvA8YjcLgEvKhzkN27jmvHf2x/jhpX7QvxtufEmi2+o21jNawwBL2NElDIuDwjMMfjXfxdxPgsw4Ww9CNVOteDlFdLJ3+SHlGVVcNmVSbi+TWz+aPvr9gA/8YieD/8ArjP/AOlEtfDn/BQ7/k7vxX9bb/0nir2X9mH/AIKLeCfgr8C9B8Mappfime/0uORZZLS2t2hYtK7jaWmU9GHUCvnL9qn4tad8cfjprXifSYb23sNS8nyo7tFSZdkSIdwVmHVT0JqOMs+y/FcL4PB4eqpVIez5ordWptP7nod2WYKrTxtSrONk7/meeUUUV+NH0YUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB+3v/BmD/yOX7Qn/XloH/oeoV7R/wAHjX/JkHwv/wCx5H/pBdV+V/8AwRz/AOCx17/wSM1jx/d2fgC18df8J3DYQus2sNp/2P7K1wQQRDJv3eefTG3vnjsv+Cvf/BevUP8AgrH8FPDPg27+GVn4HTw3rf8AbK3cOutqBuD5EsPllDBHt/1mc5PTpzQV0Pz4ooooJCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z" alt="radioterapia.ai"
id="rt-landing-logo" style="height:130px; border-radius:12px; display:block; margin:0 auto;">
<div id="rt-pop-sub" style="margin-top:16px; font-size:1.3em; font-weight:600; letter-spacing:3px;">\u2014 POP de Elite \u2014</div>
<div style="margin-top:8px; font-size:0.9em; opacity:0.55;">por: Braga, HF.</div>
</div>
<style>
#rt-landing-logo { filter: invert(0.88) hue-rotate(180deg) brightness(1.5) contrast(0.85); }
#rt-pop-sub { color: #283264; }
.dark #rt-landing-logo { filter: none; }
.dark #rt-pop-sub { color: #9ab4d2; }
</style>
""")
gr.HTML("<div style='height:40px;'></div>")
with gr.Row(elem_classes="landing-steps"):
with gr.Column(scale=1, elem_classes="step-card"):
btn_passo1 = gr.Button(
"\U0001f4ac Passo 1: Clique aqui primeiro, passe sua demanda e receba o conte\u00fado do POP (em c\u00f3digo) com o nosso Agente Conselheiro da Qualidade.",
variant="primary", size="lg")
with gr.Column(scale=1, elem_classes="step-card"):
btn_passo2 = gr.Button(
"\U0001f4c4 Passo 2: Depois de receber o c\u00f3digo, clique aqui para montar o POP e receber o documento final",
variant="primary", size="lg")
# ══════════════════════════════════════
# APP PRINCIPAL (começa oculto)
# ══════════════════════════════════════
with gr.Column(visible=False) as main_app:
with gr.Row():
gr.HTML("""<div>
<img src="data:image/jpeg;base64,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0t/HPwPpUFjbfa7gL0HVj6Cl1C5FzcfLwijao9qS0VzOWsuUgoooqDQKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAuWMouYDbOepzGfQ1VljMMhVhgg4NIDtNXSP7Wgz/y8IOR/fFX8St1M/hd+hRqWyvZNPuVliba6nioyMGkqU2ndGh1Wkaq8M7X+nKpZlxd2T/cmXvx/hyO1bNu1vrVo91p5Zkj5mgf/W2319V/2h+OK4C1upLKdZImKOvQityw1NNRukngl/s7VEORIhwshr1MNi7rlf3f5f5bMxqUlLU3xS5qCHxHFLIIdUi/s27PSdFzBL7kD7v1XI9hV240+W3hWXAeB/uSxsHjf6MOK9CNpK8df09exyuDi9SHNJmjNGaQBmjOaM5pRQMBSigUZqywzRmjNT2+nS3MLS4WOBPvzSMEjT6seKaTeiAgqS6e30KzS61AsscnMMCf625+nov+0fwyaqS+JIYZTDpcX9pXfed1xBF7hT976tgexrFv9UTTrt7ieY6jqj8tIx3JEf64/T2rGpXhBXWvn0/4L8lp3ZrGHctavqzzzJqGpKoZRizsV4SFe3H+PJPWubvr6TUbpppW3O55PpTbq7kvZzJK5d26k1HXi167qPy/P1/rQ2SsFKBuOKSr1vGNOhE8gzIf9Wp/mawirilKyC4P9m2fkj/Wy8yf7I9Ko06SQyuWJySck02iTuEY2WoUUUUigooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAp0chicMpwR0NNooAvvGurRl0ws6jLL/AHvcVRIwaVJDE4ZSQR0Iq4JI9WGHxFcdm/hf61fxepnrD0KNFSXFs9rJtdSD/Oo6jY0TvqjRsfEcsEPkzqt1b/3JOo+h7Vp6Pfi0lMmk6jLp8r/ehlb5H9j2I9iDXN0V0QxM42vrb7/kxWO4fxRPEP8AiZaQr/8ATezby8+5HK/kBT4PEGjXh4v5rQntc27cfihb+QrjbPWLqw/1M8iD0zx+XSro8WSSj/SLe1uPdo/m/Ou2OPvv+K/VWZm6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/wARXLxrywSI/Mpx/E4UAZPOMV9Yal/wU2+EljpvnxaxqV5LjP2aHTJhJ06ZdVT/AMer5h/a2/4KMan8dNIn8PeGrS40Dw5cfLcvK4+2X6/3W28Ih7qCc92xxXn43IeEcsTxMsW8Q1tTi07vopOOy77foethsxzrF/u/Yqn3k09PRPr959o/soePbH4hfCRbzSIlt9Dtb64sNLiVcbLWB/Ki/EqoJz618N/8FP8A/k66+/7B1r/6Aa6/9i39vjwp+zt8Gv8AhHNe07xHdXa381yj2FvA8YjcLgEvKhzkN27jmvHf2x/jhpX7QvxtufEmi2+o21jNawwBL2NElDIuDwjMMfjXfxdxPgsw4Ww9CNVOteDlFdLJ3+SHlGVVcNmVSbi+TWz+aPvr9gA/8YieD/8ArjP/AOlEtfDn/BQ7/k7vxX9bb/0nir2X9mH/AIKLeCfgr8C9B8Mappfime/0uORZZLS2t2hYtK7jaWmU9GHUCvnL9qn4tad8cfjprXifSYb23sNS8nyo7tFSZdkSIdwVmHVT0JqOMs+y/FcL4PB4eqpVIez5ordWptP7nod2WYKrTxtSrONk7/meeUUUV+NH0YUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB+3v/BmD/yOX7Qn/XloH/oeoV7R/wAHjX/JkHwv/wCx5H/pBdV+V/8AwRz/AOCx17/wSM1jx/d2fgC18df8J3DYQus2sNp/2P7K1wQQRDJv3eefTG3vnjsv+Cvf/BevUP8AgrH8FPDPg27+GVn4HTw3rf8AbK3cOutqBuD5EsPllDBHt/1mc5PTpzQV0Pz4ooooJCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z" alt="radioterapia.ai"
id="rt-hdr-logo" style="height:40px; border-radius:6px;">
<br>
<div id="rt-hdr-sub" style="font-size:0.9em; font-weight:600; letter-spacing:1px; margin-top:4px;">\u2014 POP de Elite \u2014</div>
<div style="font-size:0.75em; opacity:0.5; margin-top:2px;">por: Braga, HF.</div>
</div>
<style>
#rt-hdr-logo { filter: invert(0.88) hue-rotate(180deg) brightness(1.5) contrast(0.85); }
#rt-hdr-sub { color: #283264; }
.dark #rt-hdr-logo { filter: none; }
.dark #rt-hdr-sub { color: #9ab4d2; }
</style>""")
btn_voltar = gr.Button("\u2190 Voltar", size="sm", variant="secondary", elem_classes="btn-voltar")
gr.Markdown("---")
with gr.Row():
# ══════════════ COLUNA ESQUERDA ══════════════
with gr.Column(scale=3, min_width=580, elem_classes="left-col"):
# ── ETAPA 1: LOGO (compacto) ──
gr.Markdown("### \U0001f5bc\ufe0e Etapa 2.1 \u2014 Logotipo institucional *(opcional)*")
logo_input = gr.Image(
type="filepath", sources=["upload"],
height=130, show_label=False
)
gr.Markdown("---")
# ── ETAPA 2: CORES (4 boxes clicáveis) ──
gr.Markdown("### \U0001f3a8\ufe0e Etapa 2.2 \u2014 Paleta de cores")
with gr.Row(elem_id="color-state-row"):
color_pri = gr.Textbox(value=f"#{init_pal['primary']}", elem_id="pop-hex-pri", show_label=False, container=False)
color_sec = gr.Textbox(value="", elem_id="pop-hex-sec", show_label=False, container=False)
color_ter = gr.Textbox(value="", elem_id="pop-hex-ter", show_label=False, container=False)
color_zeb = gr.Textbox(value="", elem_id="pop-hex-zeb", show_label=False, container=False)
gr.HTML(f"""
<div style="display:flex; gap:6px; height:50px; position:relative;" id="color-boxes">
<input type="color" id="cpick-pri" value="#{init_pal['primary']}"
style="position:absolute;opacity:0;width:0;height:0;"
oninput="
var v=this.value, hex=v.replace('#','');
document.getElementById('cbox-pri').style.background=v;
document.getElementById('cbox-pri').querySelector('span').innerHTML='Prim\\u00e1ria<br>'+v.toUpperCase();
var t=document.querySelector('#pop-hex-pri textarea')||document.querySelector('#pop-hex-pri input');
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}
var r=parseInt(hex.substr(0,2),16),g=parseInt(hex.substr(2,2),16),b=parseInt(hex.substr(4,2),16);
function mx(c,p){{return Math.min(255,Math.max(0,Math.round(c+(255-c)*p)));}}
function th(r,g,b){{return '#'+[r,g,b].map(x=>x.toString(16).padStart(2,'0')).join('').toUpperCase();}}
var cs=th(mx(r,.45),mx(g,.45),mx(b,.45));
var ct=th(mx(r,.70),mx(g,.70),mx(b,.70));
var cz=th(mx(r,.85),mx(g,.85),mx(b,.85));
[['sec',cs,'Secund\\u00e1ria'],['ter',ct,'Terci\\u00e1ria'],['zeb',cz,'Zebra']].forEach(function(x){{
document.getElementById('cbox-'+x[0]).style.background=x[1];
document.getElementById('cbox-'+x[0]).querySelector('span').innerHTML=x[2]+'<br>'+x[1];
document.getElementById('cpick-'+x[0]).value=x[1];
var t2=document.querySelector('#pop-hex-'+x[0]+' textarea')||document.querySelector('#pop-hex-'+x[0]+' input');
if(t2){{t2.value=x[1];t2.dispatchEvent(new Event('input',{{bubbles:true}}));}}
}});
">
<div id="cbox-pri" onclick="document.getElementById('cpick-pri').click();"
style="flex:1;background:#{init_pal['primary']};border-radius:6px;cursor:pointer;
display:flex;align-items:center;justify-content:center;border:2px solid #444;">
<span style="color:#fff;font-size:9px;font-weight:bold;text-align:center;pointer-events:none;">
Prim\u00e1ria<br>#{init_pal['primary']}</span></div>
<input type="color" id="cpick-sec" value="#{init_pal['secondary']}"
style="position:absolute;opacity:0;width:0;height:0;"
oninput="var v=this.value;document.getElementById('cbox-sec').style.background=v;
document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria<br>'+v.toUpperCase();
var t=document.querySelector('#pop-hex-sec textarea')||document.querySelector('#pop-hex-sec input');
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}">
<div id="cbox-sec" onclick="document.getElementById('cpick-sec').click();"
style="flex:1;background:#{init_pal['secondary']};border-radius:6px;cursor:pointer;
display:flex;align-items:center;justify-content:center;">
<span style="color:#{init_pal['text_on_secondary']};font-size:9px;font-weight:bold;text-align:center;pointer-events:none;">
Secund\u00e1ria<br>#{init_pal['secondary']}</span></div>
<input type="color" id="cpick-ter" value="#{init_pal['tertiary']}"
style="position:absolute;opacity:0;width:0;height:0;"
oninput="var v=this.value;document.getElementById('cbox-ter').style.background=v;
document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria<br>'+v.toUpperCase();
var t=document.querySelector('#pop-hex-ter textarea')||document.querySelector('#pop-hex-ter input');
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}">
<div id="cbox-ter" onclick="document.getElementById('cpick-ter').click();"
style="flex:1;background:#{init_pal['tertiary']};border-radius:6px;cursor:pointer;
display:flex;align-items:center;justify-content:center;">
<span style="color:#{init_pal['primary_dark']};font-size:9px;font-weight:bold;text-align:center;pointer-events:none;">
Terci\u00e1ria<br>#{init_pal['tertiary']}</span></div>
<input type="color" id="cpick-zeb" value="#{init_pal['quaternary']}"
style="position:absolute;opacity:0;width:0;height:0;"
oninput="var v=this.value;document.getElementById('cbox-zeb').style.background=v;
document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra<br>'+v.toUpperCase();
var t=document.querySelector('#pop-hex-zeb textarea')||document.querySelector('#pop-hex-zeb input');
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}">
<div id="cbox-zeb" onclick="document.getElementById('cpick-zeb').click();"
style="flex:1;background:#{init_pal['quaternary']};border-radius:6px;cursor:pointer;
display:flex;align-items:center;justify-content:center;">
<span style="color:#{init_pal['primary_dark']};font-size:9px;text-align:center;pointer-events:none;">
Zebra<br>#{init_pal['quaternary']}</span></div>
</div>
""")
btn_reset_colors = gr.Button("\u21ba restaurar cores padr\u00e3o", size="sm", variant="secondary")
gr.Markdown("---")
# ── ETAPA 3: JSON (lado a lado) ──
gr.Markdown("### \U0001f4dd\ufe0e Etapa 2.3 \u2014 Insira o c\u00f3digo recebido *(upload do arquivo OU cole o c\u00f3digo)*")
with gr.Row(equal_height=True, elem_classes="step3-row"):
with gr.Column(scale=3):
json_file_input = gr.File(
file_types=[".json", ".txt"], type="filepath",
height=130, show_label=False
)
with gr.Column(scale=0, min_width=30):
gr.HTML('<div style="display:flex;align-items:center;justify-content:center;height:100%;font-weight:700;color:#666;font-size:11px;">OU</div>')
with gr.Column(scale=3):
json_text_input = gr.Textbox(
placeholder='Cole o c\u00f3digo aqui',
lines=6, max_lines=6, show_label=False, elem_id="json-paste"
)
# Quando arquivo é carregado, desabilita textbox (mas preserva o valor existente)
def toggle_textbox(file, current_text):
if file is not None:
return gr.Textbox(value=current_text or "", interactive=False,
placeholder='Arquivo carregado \u2014 usando arquivo anexo')
return gr.Textbox(value=current_text or "", interactive=True,
placeholder='Cole o c\u00f3digo aqui')
json_file_input.change(
fn=toggle_textbox,
inputs=[json_file_input, json_text_input],
outputs=[json_text_input]
)
btn_gerar = gr.Button("\U0001f680 GERAR POP (.docx)", variant="primary", size="lg")
btn_novo = gr.Button("\U0001f504 Novo POP \u2014 limpar c\u00f3digo", variant="secondary", size="sm")
# ══════════════ COLUNA DIREITA ══════════════
with gr.Column(scale=2, min_width=280):
result_info = gr.Markdown("")
btn_download = gr.DownloadButton(
"\U0001f4e5 Baixar POP (.docx)", visible=False, variant="primary", size="lg"
)
preview_gallery = gr.Gallery(
label="\u25c4 \u25ba Preview do documento",
columns=1, rows=1, height=320,
object_fit="contain", visible=False
)
# ═══════════ EVENTOS ═══════════
def generate_preview_images(docx_path):
"""Converte .docx → PDF → imagens para preview."""
import subprocess, glob
try:
# Converter docx → pdf via LibreOffice
tmp_dir = tempfile.mkdtemp()
subprocess.run(
["libreoffice", "--headless", "--convert-to", "pdf",
"--outdir", tmp_dir, docx_path],
capture_output=True, timeout=30
)
pdf_files = glob.glob(os.path.join(tmp_dir, "*.pdf"))
if not pdf_files:
return []
pdf_path = pdf_files[0]
# Converter pdf → imagens via pdftoppm
subprocess.run(
["pdftoppm", "-jpeg", "-r", "150", pdf_path,
os.path.join(tmp_dir, "page")],
capture_output=True, timeout=30
)
imgs = sorted(glob.glob(os.path.join(tmp_dir, "page-*.jpg")))
return imgs if imgs else []
except Exception:
return []
def on_generate(json_text, json_file, cpri, csec, cter, czeb, logo):
fp, info, meta = processar(json_text, json_file, cpri, csec, cter, czeb, logo)
if fp:
fname = os.path.basename(fp)
msg = f"\u2705 POP gerado com sucesso!\n\n`{fname}`"
imgs = generate_preview_images(fp)
if imgs:
return (msg, gr.DownloadButton(value=fp, visible=True),
gr.Gallery(value=imgs, visible=True),
gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True))
return (msg, gr.DownloadButton(value=fp, visible=True),
gr.Gallery(visible=False),
gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True))
return (info or "\u26a0\ufe0f Erro", gr.DownloadButton(visible=False),
gr.Gallery(visible=False),
gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True))
def on_start_generate():
return ("\u23f3 *Redigindo o POP, aguarde...*",
gr.DownloadButton(visible=False),
gr.Gallery(visible=False),
gr.Button("\u23f3 Redigindo...", variant="secondary", interactive=False))
btn_gerar.click(
fn=on_start_generate,
outputs=[result_info, btn_download, preview_gallery, btn_gerar]
).then(
fn=on_generate,
inputs=[json_text_input, json_file_input, color_pri, color_sec, color_ter, color_zeb, logo_input],
outputs=[result_info, btn_download, preview_gallery, btn_gerar]
)
def on_novo():
return "", None, "", gr.DownloadButton(visible=False), gr.Gallery(visible=False)
btn_novo.click(
fn=on_novo,
outputs=[json_text_input, json_file_input, result_info, btn_download, preview_gallery]
)
# Reset cores para padrão
def on_reset_colors():
p = build_palette(DEFAULT_UI_COLOR)
return f"#{p['primary']}", "", "", ""
btn_reset_colors.click(
fn=on_reset_colors,
outputs=[color_pri, color_sec, color_ter, color_zeb],
js=f"""() => {{
var p = '{init_pal["primary"]}', s = '{init_pal["secondary"]}',
t = '{init_pal["tertiary"]}', z = '{init_pal["quaternary"]}';
var d = '{init_pal["primary_dark"]}', ts = '{init_pal["text_on_secondary"]}';
document.getElementById('cbox-pri').style.background='#'+p;
document.getElementById('cbox-pri').querySelector('span').innerHTML='Prim\\u00e1ria<br>#'+p;
document.getElementById('cpick-pri').value='#'+p;
document.getElementById('cbox-sec').style.background='#'+s;
document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria<br>#'+s;
document.getElementById('cpick-sec').value='#'+s;
document.getElementById('cbox-ter').style.background='#'+t;
document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria<br>#'+t;
document.getElementById('cpick-ter').value='#'+t;
document.getElementById('cbox-zeb').style.background='#'+z;
document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra<br>#'+z;
document.getElementById('cpick-zeb').value='#'+z;
}}"""
)
# ═══════════ NAVEGA\u00c7\u00c3O LANDING \u2194 APP ═══════════
# Passo 1: abre URL do Gemini Gem em nova aba
btn_passo1.click(
fn=None, inputs=None, outputs=None,
js="() => { window.open('https://gemini.google.com/gem/c86826a9110d', '_blank'); }"
)
# Passo 2: oculta landing, mostra app
def show_app():
return gr.Column(visible=False), gr.Column(visible=True)
btn_passo2.click(fn=show_app, outputs=[landing_page, main_app])
# Voltar: oculta app, mostra landing
def show_landing():
return gr.Column(visible=True), gr.Column(visible=False)
btn_voltar.click(fn=show_landing, outputs=[landing_page, main_app])
return demo, APP_CSS
# ══════════════════════════════════════════════════════════════
# INICIAR APLICAÇÃO
# ══════════════════════════════════════════════════════════════
demo, app_css = criar_interface()
demo.launch(debug=False, show_error=True, css=app_css,
theme=__import__('gradio').themes.Soft(primary_hue="blue"))