import logging import re import streamlit as st from modules.config import client_anthropic from modules.utils import sanitizar, extrair_texto_arquivo, aprimorar_prompt from modules.docgen import criar_docx, _gerar_com_continuacao from modules.memoria import ler_arquivo_memoria from modules.video_presentation import gerar_video_apresentacao from modules.pptx_generator import ( gerar_pptx, salvar_apresentacao_drive, listar_apresentacoes_drive, carregar_apresentacao_drive, ) # Detecta URLs do Drive/Docs/Sheets/Slides em texto livre. Captura o ID # em qualquer um dos 5 formatos comuns. Usado pra permitir que o usuario # cole links no campo de instrucoes e o agente busque o conteudo automatico. _RE_DRIVE_URLS = re.compile( r'https?://(?:' r'docs\.google\.com/document/d/([a-zA-Z0-9_-]+)' r'|docs\.google\.com/spreadsheets/d/([a-zA-Z0-9_-]+)' r'|docs\.google\.com/presentation/d/([a-zA-Z0-9_-]+)' r'|drive\.google\.com/file/d/([a-zA-Z0-9_-]+)' r'|drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)' r')' ) def _carregar_arquivos_drive_de_urls(texto: str) -> tuple[str, list[str]]: """Detecta URLs de Drive/Docs/Sheets/Slides em texto livre, busca o conteudo de cada arquivo via Drive API, e retorna (conteudo_concatenado, lista_de_nomes). Limita cada arquivo a 15k chars pra nao estourar contexto. """ if not texto: return "", [] ids_vistos: set[str] = set() ids: list[str] = [] for match in _RE_DRIVE_URLS.finditer(texto): for g in match.groups(): if g and g not in ids_vistos: ids_vistos.add(g) ids.append(g) if not ids: return "", [] from modules.drive_utils import conectar_drive, _ler_conteudo_drive_arquivo svc = conectar_drive() if not svc: return "", [] partes: list[str] = [] nomes: list[str] = [] for file_id in ids: try: meta = svc.files().get(fileId=file_id, fields="id,name,mimeType").execute() nome = meta.get("name", file_id) mime = meta.get("mimeType", "") conteudo = _ler_conteudo_drive_arquivo(svc, file_id, mime, nome) if conteudo and conteudo.strip(): if len(conteudo) > 15000: conteudo = conteudo[:15000] + "\n[...TRUNCADO...]" partes.append(f"### Arquivo do Drive: {nome}\n{conteudo}") nomes.append(nome) else: nomes.append(f"{nome} (sem conteudo extraivel)") except Exception as e: logging.warning(f"[Docs] Erro ao ler arquivo Drive {file_id}: {e}") nomes.append(f"{file_id} (erro de acesso)") return "\n\n".join(partes), nomes # --------------- Enhancer UI helper --------------- def _enhancer_ui(key_prefix: str) -> str | None: """Show enhancer approval UI. Returns final prompt or None if still waiting.""" estado_key = f"_enh_{key_prefix}_state" original_key = f"_enh_{key_prefix}_orig" aprimorado_key = f"_enh_{key_prefix}_enh" estado = st.session_state.get(estado_key) if estado != "awaiting": return None original = st.session_state.get(original_key, "") aprimorado = st.session_state.get(aprimorado_key, "") st.markdown("---") with st.expander("Seu prompt original", expanded=False): st.markdown(original) st.markdown("**Prompt aprimorado:**") st.info(aprimorado) col1, col2, col3 = st.columns(3) with col1: if st.button("Usar aprimorado", type="primary", key=f"btn_enh_usar_{key_prefix}"): st.session_state[estado_key] = None st.session_state[f"_enh_{key_prefix}_run"] = aprimorado st.rerun() with col2: if st.button("Regenerar", key=f"btn_enh_regen_{key_prefix}"): with st.spinner("Regenerando..."): st.session_state[aprimorado_key] = aprimorar_prompt(original) st.rerun() with col3: if st.button("Usar original", key=f"btn_enh_orig_{key_prefix}"): st.session_state[estado_key] = None st.session_state[f"_enh_{key_prefix}_run"] = original st.rerun() return None def _trigger_enhancer(texto: str, key_prefix: str): """Start the enhancer flow: enhance + set awaiting state.""" st.session_state[f"_enh_{key_prefix}_orig"] = texto with st.spinner("Aprimorando seu prompt..."): st.session_state[f"_enh_{key_prefix}_enh"] = aprimorar_prompt(texto) st.session_state[f"_enh_{key_prefix}_state"] = "awaiting" st.rerun() def _pop_enhancer_result(key_prefix: str) -> str | None: """Pop and return the enhancer result if available.""" return st.session_state.pop(f"_enh_{key_prefix}_run", None) # --------------- Gamma prompt --------------- _GAMMA_PROMPT = """Voce e um especialista em criar apresentacoes executivas interativas para a plataforma Gamma.app. Recebeu o conteudo extraido de uma apresentacao PowerPoint. Sua tarefa e transformar esse conteudo em um ROTEIRO COMPLETO para uma apresentacao Gamma.app animada e narrada. CONTEUDO DO POWERPOINT: {conteudo_ppt} INSTRUCOES ADICIONAIS DO USUARIO: {instrucoes} CONTEXTO ADICIONAL: {contexto} Gere o roteiro seguindo EXATAMENTE este formato para cada slide/card do Gamma: --- ## Card [N]: [Titulo do Card] **Visual:** [Descreva o layout ideal: hero image, split layout, grid, quote, etc.] **Icones/Ilustracoes:** [Sugira icones ou ilustracoes no estilo sketch/hand-drawn que combinam com o tema] **Texto principal:** [Texto que aparece no card — curto, impactante, com palavras-chave em **destaque**] **Narração (texto que o apresentador fala):** [Texto mais longo e explicativo que complementa o que esta no card. Deve soar natural, como alguem apresentando para a diretoria. Use linguagem executiva mas acessivel.] **Animacao sugerida:** [fade-in, slide-left, zoom, typewriter, etc.] **Duracao estimada:** [X segundos] --- REGRAS: 1. Cada slide do PPT pode virar 1 ou mais cards no Gamma (quebre slides densos em multiplos cards) 2. O primeiro card deve ser uma capa impactante com titulo e subtitulo 3. O ultimo card deve ser um encerramento com call-to-action ou agradecimento 4. Use frases curtas e impactantes nos cards — a narração complementa 5. Sugira destaques visuais (palavras com background colorido, como no exemplo) 6. Para tabelas/dados, sugira visualizacoes (graficos, progress bars, KPIs side-by-side) 7. Mantenha o tom profissional mas envolvente 8. Toda narração em portugues brasileiro 9. Inclua transicoes suaves entre cards 10. Para dados numericos, destaque os numeros grandes e as variacoes (%, +/-) Ao final, inclua uma secao: ## Resumo para Gamma.app - Total de cards: [N] - Duracao estimada: [X minutos] - Estilo visual sugerido: [ex: Sketch/Hand-drawn, Minimalista, Corporativo] - Paleta de cores sugerida: [baseada no conteudo] - Instrucoes para importar no Gamma: [passo a passo rapido] Gere o roteiro COMPLETO agora.""" # --------------- PPTX generation logic --------------- def _executar_geracao_pptx(instrucao_pptx: str, auto_ctx: bool): """Executa a geracao do PPTX (separado da UI para reuso).""" contexto_auto = "" if auto_ctx: with st.spinner("Carregando contexto completo (OKRs, roadmap, metricas, iniciativas, PM OS)..."): partes = [] try: from modules.contexto import carregar_contexto_sob_demanda ctx_base, _ctx_log = carregar_contexto_sob_demanda( "okrs roadmap metricas iniciativas squad cora estrategia" ) if ctx_base: partes.append(f"## Contexto Base (Roadmap, Squad, Empresa)\n{ctx_base}") except Exception as e: logging.warning(f"[Docs] Erro ao carregar contexto base: {e}") try: from modules.pm_os import pm_os_carregar_contexto_outputs ctx_pm_os = pm_os_carregar_contexto_outputs() if ctx_pm_os: partes.append(f"## Contexto PM OS (OKRs, PRDs, Outputs)\n{ctx_pm_os}") except Exception as e: logging.warning(f"[Docs] Erro ao carregar PM OS: {e}") try: from modules.pm_os import PM_OS_DRIVE, _pm_os_listar_com_svc from modules.drive_utils import conectar_drive, _ler_conteudo_drive_arquivo svc = conectar_drive() if svc: prds = _pm_os_listar_com_svc(svc, PM_OS_DRIVE.get("outputs_prds", "")) prd_parts = [] for arq in prds: if arq.get("mimeType") == "application/vnd.google-apps.folder": continue conteudo = _ler_conteudo_drive_arquivo(svc, arq["id"], arq["mimeType"], arq["name"]) if conteudo.strip(): prd_parts.append(f"### PRD: {arq['name']}\n{conteudo[:5000]}") if prd_parts: partes.append(f"## PRDs das Iniciativas\n" + "\n\n".join(prd_parts)) except Exception as e: logging.warning(f"[Docs] Erro ao carregar PRDs: {e}") try: from modules.contexto_fraude import carregar_inteligencia_fraude ctx_fraude = carregar_inteligencia_fraude() if ctx_fraude: partes.append(f"## Inteligencia de Fraude\n{ctx_fraude[:15000]}") except Exception as e: logging.warning(f"[Docs] Erro ao carregar contexto fraude: {e}") contexto_auto = "\n\n---\n\n".join(partes) # Contexto de documentos selecionados mem_docs = st.session_state.get("memoria_arquivos") or [] docs_selecionados = st.session_state.get("pptx_docs_ctx", []) for doc in mem_docs: if doc["name"] in docs_selecionados: contexto_auto += f"\n--- {doc['name']} ---\n{ler_arquivo_memoria(doc['id'])}\n" # Carrega arquivos linkados no campo de instrucoes (Docs, Sheets, Slides, # Drive files). Permite o usuario colar URL e o agente puxa o conteudo. with st.spinner("Verificando links do Drive na instrucao..."): drive_conteudo, drive_nomes = _carregar_arquivos_drive_de_urls(instrucao_pptx) if drive_conteudo: contexto_auto += "\n\n---\n\n## Arquivos do Drive linkados na instrucao\n\n" + drive_conteudo if drive_nomes: st.info("Arquivos do Drive carregados: " + ", ".join(drive_nomes)) progress_bar = st.progress(0) status_text = st.empty() def update_progress(step, total, msg): progress_bar.progress(step / total) status_text.info(f"**Passo {step}/{total}:** {msg}") try: pptx_bytes, n_slides, slides_html = gerar_pptx( tema=instrucao_pptx, contexto=contexto_auto, progress_callback=update_progress, ) progress_bar.empty() status_text.empty() st.session_state["_pptx_slides"] = slides_html st.session_state["_pptx_bytes"] = pptx_bytes nome_apres = instrucao_pptx.strip()[:50] or "apresentacao" fid = salvar_apresentacao_drive(slides_html, nome_apres) if fid: st.session_state["_pptx_drive_id"] = fid st.success(f"Apresentacao gerada com sucesso! {n_slides} slides." + (" Salva no Drive." if fid else "")) st.download_button( f"Baixar PPTX ({n_slides} slides)", data=pptx_bytes, file_name="apresentacao_cora.pptx", mime="application/vnd.openxmlformats-officedocument.presentationml.presentation", key="dl_pptx", ) except Exception as e: progress_bar.empty() status_text.empty() st.error(f"Erro ao gerar apresentacao: {e}") def _executar_edicao_slides(edit_instrucao: str, saved_slides: list): """Executa a edicao dos slides.""" from modules.pptx_generator import editar_slides_lote progress_bar = st.progress(0) status_text = st.empty() def update_edit_progress(step, total, msg): progress_bar.progress(step / total) status_text.info(f"**Passo {step}/{total}:** {msg}") try: pptx_bytes, n_slides, slides_atualizados = editar_slides_lote( slides=saved_slides, instrucao=edit_instrucao, progress_callback=update_edit_progress, ) progress_bar.empty() status_text.empty() st.session_state["_pptx_slides"] = slides_atualizados st.session_state["_pptx_bytes"] = pptx_bytes salvar_apresentacao_drive(slides_atualizados, st.session_state.get("pptx_instrucao", "apresentacao")[:50]) st.success(f"Edicao aplicada com sucesso! {n_slides} slides.") st.download_button( f"Baixar PPTX editado ({n_slides} slides)", data=pptx_bytes, file_name="apresentacao_cora.pptx", mime="application/vnd.openxmlformats-officedocument.presentationml.presentation", key="dl_pptx_edited", ) except Exception as e: progress_bar.empty() status_text.empty() st.error(f"Erro ao editar slides: {e}") # --------------- Main render --------------- def render_docs_tab(): """Renderiza a aba Documentos.""" st.header("Gerador de Documentos") enhancer_on = st.session_state.get("prompt_enhancer_ativo", False) tipo_doc = st.selectbox("Tipo de documento:", [ "Apresentacao Executiva (PPTX)", "Video Executivo (MP4)", "Apresentacao Interativa (Gamma.app)", "Roteiro de Apresentacao", "Analise de Risco", "Email Executivo", "Documento Livre", ]) # --- Apresentacao Executiva PPTX --- if tipo_doc == "Apresentacao Executiva (PPTX)": st.markdown(""" Gera uma **apresentacao PowerPoint com visual rico** — igual ao video executivo. **Como funciona:** 1. Descreva o tema (ex: "Racional de priorizacao das iniciativas por KR") 2. **Cole links do Drive/Docs/Sheets/Slides** direto no campo abaixo — o agente extrai o conteudo automaticamente 3. O agente carrega contexto automaticamente (OKRs, metricas, iniciativas) se a opcao estiver marcada 4. Claude Opus cria slides HTML com design visual rico (gradientes, cards, icones) 5. Playwright renderiza cada slide em imagem Full HD (1920x1080) 6. Monta o PPTX com imagens — visual identico ao video executivo """) auto_ctx = st.checkbox("Carregar contexto automaticamente (OKRs, roadmap, metricas)", value=True, key="pptx_auto_ctx") instrucao_pptx = st.text_area("Tema e instrucoes da apresentacao:", height=150, placeholder=( "Ex: Racional de atingimento dos OKRs — como as iniciativas do roadmap impactam cada KR. " "Tom executivo para o CPO.\n\n" "Voce pode colar links do Drive aqui (Docs, Sheets, Slides, PDFs, DOCX...) que o agente " "le o conteudo automaticamente. Ex: https://docs.google.com/document/d/ABC123/edit" ), key="pptx_instrucao") # Check if enhancer returned a result enh_result = _pop_enhancer_result("pptx_gen") if enh_result: _executar_geracao_pptx(enh_result, auto_ctx) # Show enhancer UI if awaiting elif st.session_state.get("_enh_pptx_gen_state") == "awaiting": _enhancer_ui("pptx_gen") # Button elif st.button("Gerar Apresentacao PPTX", type="primary", key="btn_pptx"): if not instrucao_pptx.strip(): st.warning("Descreva o tema da apresentacao.") elif enhancer_on: _trigger_enhancer(instrucao_pptx, "pptx_gen") else: _executar_geracao_pptx(instrucao_pptx, auto_ctx) # --- Carregar apresentacao salva do Drive --- st.divider() with st.expander("Carregar apresentacao salva"): if st.button("Listar apresentacoes", key="btn_list_pptx"): with st.spinner("Buscando no Drive..."): st.session_state["_pptx_lista_drive"] = listar_apresentacoes_drive() lista_drive = st.session_state.get("_pptx_lista_drive", []) if lista_drive: opcoes_drive = {} opcoes_meta = {} for a in lista_drive: name = a["name"] display = name.replace("pptx_", "").replace(".json", "") is_json = name.endswith(".json") tipo = "editavel" if is_json else "importavel" label = f"{display} ({a.get('modifiedTime', '')[:10]}) [{tipo}]" opcoes_drive[label] = a["id"] opcoes_meta[label] = name selecionada = st.selectbox("Apresentacao:", list(opcoes_drive.keys()), key="pptx_load_select") is_pptx_import = not opcoes_meta.get(selecionada, "").endswith(".json") if is_pptx_import: st.caption("Este arquivo sera importado: Claude vai recriar os slides como HTML editavel. Pode levar ~1 min por slide.") if st.button("Carregar", key="btn_load_pptx"): spinner_msg = "Importando PPTX (Claude recriando slides)..." if is_pptx_import else "Carregando apresentacao..." with st.spinner(spinner_msg): slides_carregados = carregar_apresentacao_drive( opcoes_drive[selecionada], file_name=opcoes_meta.get(selecionada, ""), ) if slides_carregados: st.session_state["_pptx_slides"] = slides_carregados if is_pptx_import: nome_save = opcoes_meta.get(selecionada, "import").rsplit(".", 1)[0] fid = salvar_apresentacao_drive(slides_carregados, nome_save) if fid: st.success(f"Importado e salvo! {len(slides_carregados)} slides. Edite abaixo.") else: st.success(f"Importado: {len(slides_carregados)} slides. Edite abaixo.") else: st.success(f"Apresentacao carregada: {len(slides_carregados)} slides. Edite abaixo.") st.rerun() else: st.error("Erro ao carregar apresentacao.") elif st.session_state.get("_pptx_lista_drive") is not None: st.caption("Nenhuma apresentacao salva encontrada.") # --- Edicao de slides --- saved_slides = st.session_state.get("_pptx_slides") if saved_slides: st.divider() st.subheader("Editar slides") with st.expander(f"Ver slides atuais ({len(saved_slides)})"): for i, s in enumerate(saved_slides): st.caption(f"**Slide {i+1}:** {s.get('titulo', 'Sem titulo')}") st.caption("Descreva as alteracoes referenciando os slides por numero. " "Slides nao mencionados serao mantidos identicos.") edit_instrucao = st.text_area("Alteracoes:", height=150, placeholder="Ex:\nSlide 1: Trocar titulo para 'Novo Titulo'\n" "Slide 3: Remover a parte sobre migração\n" "Slide 5: Adicionar bullet sobre modelo de ML", key="pptx_edit_instrucao") # Check if enhancer returned a result for edit enh_edit_result = _pop_enhancer_result("pptx_edit") if enh_edit_result: _executar_edicao_slides(enh_edit_result, saved_slides) elif st.session_state.get("_enh_pptx_edit_state") == "awaiting": _enhancer_ui("pptx_edit") elif st.button("Aplicar edicoes", type="primary", key="btn_pptx_edit"): if not edit_instrucao.strip(): st.warning("Descreva o que deseja alterar.") elif enhancer_on: _trigger_enhancer(edit_instrucao, "pptx_edit") else: _executar_edicao_slides(edit_instrucao, saved_slides) # Documentos adicionais de contexto mem_docs = st.session_state.get("memoria_arquivos") or [] if mem_docs: st.multiselect("Documentos adicionais de contexto (opcional):", [d["name"] for d in mem_docs], key="pptx_docs_ctx") return # Nao renderizar o resto da aba # --- Upload de PPT (para Video ou Gamma) --- ppt_conteudo = "" ppt_file_ref = None needs_ppt = tipo_doc in ("Video Executivo (MP4)", "Apresentacao Interativa (Gamma.app)") if tipo_doc == "Video Executivo (MP4)": st.markdown(""" Transforme sua apresentacao (PowerPoint ou PDF) em um **video profissional narrado** — com slides redesenhados na identidade visual Cora, narracao feminina em portugues, animacoes e transicoes suaves. **Como funciona:** 1. Faca upload do seu arquivo abaixo (PPTX ou PDF) 2. Adicione instrucoes especificas (opcional) 3. O agente redesenha os slides, gera narracao e monta o video MP4 """) ppt_file = st.file_uploader("Upload da apresentação (.pptx ou .pdf):", type=["pptx", "pdf"], key="ppt_video") if ppt_file: ppt_file_ref = ppt_file with st.spinner("Extraindo conteudo do PowerPoint..."): ppt_conteudo = extrair_texto_arquivo(ppt_file) if ppt_conteudo: n_slides = ppt_conteudo.count("--- SLIDE") st.success(f"PowerPoint carregado: {n_slides} slides extraidos") with st.expander("Ver conteudo extraido"): st.text(ppt_conteudo[:3000] + ("..." if len(ppt_conteudo) > 3000 else "")) else: st.warning("Nao foi possivel extrair conteudo do PPT.") elif tipo_doc == "Apresentacao Interativa (Gamma.app)": st.markdown(""" Transforme sua apresentacao (PowerPoint ou PDF) em um **roteiro completo para o Gamma.app** — com narração, animações, e sugestões visuais para criar uma apresentação interativa e animada. **Como funciona:** 1. Faca upload do seu arquivo abaixo (PPTX ou PDF) 2. O agente gera o roteiro completo com narração para cada card 3. Copie o conteudo no [Gamma.app](https://gamma.app) e ele cria a apresentação animada """) ppt_file = st.file_uploader("Upload da apresentação (.pptx ou .pdf):", type=["pptx", "pdf"], key="ppt_gamma") if ppt_file: with st.spinner("Extraindo conteudo do PowerPoint..."): ppt_conteudo = extrair_texto_arquivo(ppt_file) if ppt_conteudo: n_slides = ppt_conteudo.count("--- SLIDE") st.success(f"PowerPoint carregado: {n_slides} slides extraidos") with st.expander("Ver conteudo extraido"): st.text(ppt_conteudo[:3000] + ("..." if len(ppt_conteudo) > 3000 else "")) else: st.warning("Nao foi possivel extrair conteudo do PPT.") mem_docs_tab = st.session_state.memoria_arquivos or [] docs_ctx = st.multiselect("Documentos de contexto (memoria):", [d["name"] for d in mem_docs_tab], key="docs_ctx") instrucao = st.text_area("Descreva o que precisa:", height=150, placeholder="Ex: Foco nos resultados, tom executivo para diretoria, destacar KPIs..." if needs_ppt else "Ex: Roteiro de apresentacao sobre os KPIs do trimestre...") if st.button("Gerar Documento", type="primary"): if needs_ppt and not ppt_conteudo: st.warning("Faca upload do arquivo (PPTX ou PDF) primeiro.") elif not instrucao.strip() and not needs_ppt: st.warning("Descreva o documento que precisa.") else: contexto_docs = "" for doc in mem_docs_tab: if doc["name"] in docs_ctx: contexto_docs += f"\n--- {doc['name']} ---\n{ler_arquivo_memoria(doc['id'])}\n" if tipo_doc == "Video Executivo (MP4)": placeholder = st.empty() ppt_file_ref.seek(0) video_bytes = gerar_video_apresentacao( ppt_file_ref, instrucao, contexto_docs, placeholder ) if video_bytes: video_bytes.seek(0) st.video(video_bytes, format="video/mp4") video_bytes.seek(0) st.download_button( "Baixar Video (MP4)", data=video_bytes, file_name="apresentacao_executiva.mp4", mime="video/mp4", key="dl_video", ) elif tipo_doc == "Apresentacao Interativa (Gamma.app)": with st.spinner("Gerando apresentacao interativa..."): prompt_doc = _GAMMA_PROMPT.format( conteudo_ppt=ppt_conteudo, instrucoes=instrucao or "Nenhuma instrucao adicional.", contexto=contexto_docs or "Nenhum contexto adicional.", ) placeholder = st.empty() resposta = _gerar_com_continuacao(client_anthropic, st.session_state.modelo, prompt_doc, placeholder) st.download_button( "Baixar em Word (.docx)", data=criar_docx(resposta), file_name=f"{sanitizar(tipo_doc)}.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", key="dl_doc" ) else: with st.spinner("Gerando..."): prompt_doc = f"""Voce e um PM Senior. Gere um {tipo_doc} completo e profissional em portugues. SOLICITACAO: {instrucao} CONTEXTO ADICIONAL: {contexto_docs or "Nenhum contexto adicional fornecido."} Use formatacao Markdown com titulos, subtitulos e listas bem estruturadas.""" placeholder = st.empty() resposta = _gerar_com_continuacao(client_anthropic, st.session_state.modelo, prompt_doc, placeholder) st.download_button( "Baixar em Word (.docx)", data=criar_docx(resposta), file_name=f"{sanitizar(tipo_doc)}.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", key="dl_doc_other" )