# Guia LEORAD — organizar máscaras e autotextos com IA

## Para você, médico(a): como usar

**Jeito mais rápido:** em [leorad.com.br/organizar-mascaras](https://leorad.com.br/organizar-mascaras) ou no LEORAD (biblioteca → botão **+** → **Organizar acervo com IA**), escolha a IA que você usa (ChatGPT, Claude ou Gemini) — ela abre já lendo este guia, e você só anexa o seu material. Ali também dá para organizar sem sair do LEORAD, na aba **Aqui no LEO**.

**Sem o botão:**

1. Abra o **ChatGPT**, o **Claude** ou o **Gemini** numa conversa nova.
2. Anexe **este arquivo** e o seu material: documentos do Word, PDFs, planilhas, export de outro sistema ou texto colado.
3. Escreva: **"Organize minhas máscaras e autotextos seguindo o guia."**

Nos dois casos, a IA vai fazer algumas perguntas, mostrar a organização proposta e, depois do seu ok, gerar um arquivo **`.json`**. No LEORAD: **Biblioteca → botão + → Importar arquivo .leo** e escolha o `.json` (ele aceita os dois).

Dicas:

- Comece com um lote pequeno, de 5 a 10 máscaras, para conferir como fica. A importação cria tudo de uma vez, e desfazer é apagar item por item.
- **Não envie laudos de pacientes reais.** Use só modelos, sem nome, data de nascimento ou número de exame.
- Acervo grande? Peça um arquivo por região ou modalidade (um de Crânio, outro de Abdome…) e importe um de cada vez.

---

## Para a IA: instruções

Você vai ajudar um(a) radiologista a transformar as máscaras (modelos de laudo) e os autotextos (frases prontas) dele(a) num arquivo `.json` que o LEORAD importa direto. Siga este guia à risca: o formato é estrito, e alguns erros fazem itens **sumirem sem aviso** na importação.

### Regras de ouro

1. **Organize, não reescreva.** Você pode criar pastas, padronizar nomes de máscara, criar atalhos e apontar duplicatas. O **texto do laudo** fica literal: não corrija, não resuma, não "melhore" termos médicos, não troque sinônimos. Se algo parecer errado (erro de digitação, lateralidade estranha, medida incoerente), **aponte ao usuário** e mantenha como está. Só edite o texto se o usuário pedir explicitamente.
2. **Preserve campos de preenchimento.** Marcadores como `___`, `***`, `[ ]`, `( )`, `xx mm` ou `<<medida>>` ficam exatamente como vieram.
3. **Nada de dados de paciente.** Se o material tiver nome de paciente, data de nascimento, número de prontuário/atendimento ou assinatura com CRM de laudo real, **pare e avise**. Pergunte se o usuário quer que você remova esses trechos ou se ele prefere mandar só os modelos.
4. **Mostre antes de gerar.** Nunca gere o arquivo final sem o usuário aprovar a estrutura.
5. **Não peça senha, token, login nem chave.** Nada disso é necessário.

### Fluxo da conversa

**Etapa 0 — Se ainda não há material.** Muitas vezes o usuário chega por um link, e você lê este guia antes de ele anexar qualquer coisa. Nesse caso, não resuma o guia nem explique o formato. Responda em no máximo 4 linhas, em português e com tom cordial:

> Oi! Vou organizar suas máscaras e autotextos para importar no LEORAD.
> Anexe aqui o seu material (Word, PDF, planilha ou texto colado). Pode ser tudo de uma vez.
> Só peço que sejam modelos, sem dados de pacientes.

E espere o material.

**Etapa 1 — Entender o material.** Leia tudo o que foi anexado e responda com um resumo curto: quantos textos encontrou, de que tipo e como parecem organizados. Pergunte só o que não der para deduzir:

- O que é **máscara** (laudo completo) e o que é **autotexto/frase** (trecho para inserir no meio do laudo)? Se a separação estiver óbvia, proponha em vez de perguntar.
- Como ele prefere as pastas: por **região** (Crânio, Tórax, Abdome…), por **modalidade** (TC, RM, US…) ou pela organização que já existe no material?
- Qual nome dar à pasta principal (ex.: "Minhas máscaras", "Clínica X")?

**Etapa 2 — Propor a estrutura.** Mostre a árvore em texto, com o nome e o atalho de cada item, assim:

```
Máscaras — Dra. Ana
├── Crânio
│   ├── TC CRÂNIO NORMAL            (tccranio)
│   └── RM CRÂNIO NORMAL            (rmcranio)
└── Abdome
    └── US ABDOME TOTAL NORMAL      (usabdtotal)

Frases — Dra. Ana
└── Fígado
    └── ESTEATOSE LEVE              (esteatoseleve)
```

Liste também: duplicatas ou quase-duplicatas encontradas, textos que você não soube classificar e qualquer coisa que pareça errada no conteúdo. Espere o ok.

**Etapa 3 — Gerar o arquivo.** Monte o `.json` no formato abaixo, rode o **validador** (ou o checklist manual, se você não puder executar código) e corrija tudo o que ele apontar. Entregue:

- o arquivo para download, com um nome como `biblioteca-cranio.json`;
- se você **não conseguir gerar arquivo**, o JSON completo num único bloco de código, com a instrução: "copie, cole no Bloco de Notas e salve como `biblioteca.json`". No Bloco de Notas, o usuário deve escolher "Todos os arquivos" em Tipo e a codificação UTF-8;
- um resumo de uma linha: quantas pastas, quantas máscaras e quantas frases.

Se o acervo for grande (mais de ~40 textos), gere **um arquivo por pasta principal** em vez de um arquivo gigante, para não cortar o conteúdo no meio.

### Formato do arquivo

Um **array JSON plano**, sem aninhamento. A hierarquia é dada pelo campo `parent`, que aponta para o `uuid` de outro item **do mesmo arquivo**.

**Pasta:**

```json
{ "uuid": "p-cranio", "name": "Crânio", "type": "mask", "order": 0, "parent": "raiz-mascaras" }
```

A pasta principal (raiz) **não tem** o campo `parent`.

**Documento (máscara ou frase):**

```json
{
  "uuid": "d-tc-cranio",
  "name": "TC CRÂNIO NORMAL",
  "type": "mask",
  "order": 0,
  "parent": "p-cranio",
  "isLeaf": true,
  "text": "<p><strong>TOMOGRAFIA COMPUTADORIZADA DO CRÂNIO</strong></p><p><strong>Técnica:</strong></p><p>Aquisição volumétrica sem contraste.</p><p><strong>Análise:</strong></p><p>Parênquima encefálico com atenuação preservada.</p><p><strong>Impressão:</strong></p><p>Exame dentro dos padrões da normalidade.</p>",
  "instructions": "",
  "speechText": "tomografia de crânio normal",
  "shortcut": "tccranio"
}
```

| campo | regra |
|---|---|
| `uuid` | Identificador **único dentro do arquivo**. Qualquer texto serve (`p1`, `d-tc-cranio`…). O LEORAD gera os definitivos na importação. |
| `name` | Nome que aparece na biblioteca. Máscaras em MAIÚSCULAS no padrão `MODALIDADE REGIÃO ACHADO` costumam ficar mais fáceis de achar. **Não pode repetir dentro da mesma pasta**, nem mudando só maiúscula/minúscula. |
| `type` | `"mask"` para máscara, `"phrase"` para autotexto/frase. Define em qual aba o item entra. |
| `order` | Posição dentro da pasta: 0, 1, 2… |
| `parent` | `uuid` da pasta onde o item fica. Obrigatório em tudo, menos na raiz. |
| `isLeaf` | `true` em documentos. Pastas **não** têm esse campo. |
| `text` | Corpo do texto em **HTML**: um `<p>…</p>` por parágrafo e `<strong>` nos títulos de seção. Pode usar `<em>` e `<u>`. **Não use `\n`**, porque quebra de linha não aparece. Não use tabelas, estilos, classes nem cores. Nunca vazio. |
| `shortcut` | Atalho para chamar o texto: minúsculo, sem espaço, sem acento, **até 30 caracteres**, e **sem repetir** no arquivo. |
| `speechText` | Como o médico falaria o nome ao ditar ("tomografia de crânio normal"). Pode ser `""`. |
| `instructions` | Orientação para a IA do LEORAD que **não sai no laudo** (ex.: "sempre citar a medida do maior nódulo"). HTML ou `""`. Só preencha se o material trouxer instruções explícitas. |

**Máscaras e frases ficam em árvores separadas.** Um documento precisa ter o **mesmo `type` da pasta** onde está, e uma pasta o mesmo `type` da pasta-mãe. Por isso, crie uma raiz para máscaras (`"type": "mask"`) e outra para frases (`"type": "phrase"`).

### O que faz item sumir sem aviso

A importação **não mostra erro** nestes casos: o item simplesmente não aparece.

1. `parent` apontando para um `uuid` que não existe no arquivo.
2. Documento sem `parent` (solto na raiz).
3. Documento ou pasta com `type` diferente da pasta onde está.
4. Dois itens com o mesmo `name` na mesma pasta, inclusive quando a diferença é só maiúscula/minúscula.
5. `text` vazio.
6. `shortcut` com mais de 30 caracteres.
7. Mais de **8 níveis** de pasta abaixo da raiz.

### Validador (rode antes de entregar)

Se você pode executar Python, salve o JSON como `biblioteca.json` e rode este script. Se ele terminar com **"Revise antes de importar"**, corrija e rode de novo.

```python
import json, re, sys
from collections import Counter, defaultdict

caminho = sys.argv[1] if len(sys.argv) > 1 else "biblioteca.json"
with open(caminho, encoding="utf-8") as f:
    dados = json.load(f)
nos = dados["jsonContent"] if isinstance(dados, dict) else dados

erros, avisos = [], []
if not isinstance(nos, list):
    sys.exit("O arquivo precisa ser um array JSON [ ... ].")

por_uuid = {}
for n in nos:
    u = n.get("uuid")
    if not u:
        erros.append(f"item sem uuid: {n.get('name')!r}")
    elif u in por_uuid:
        erros.append(f"uuid repetido: {u}")
    por_uuid[u] = n

docs = [n for n in nos if n.get("isLeaf")]
pastas = [n for n in nos if not n.get("isLeaf")]

for n in nos:
    nome = n.get("name") or "(sem nome)"
    if not n.get("name"):
        erros.append(f"item sem name (uuid {n.get('uuid')})")
    if n.get("type") not in ("mask", "phrase"):
        erros.append(f"{nome}: type deve ser 'mask' ou 'phrase'")
    pai = n.get("parent")
    if pai and pai not in por_uuid:
        erros.append(f"{nome}: parent '{pai}' não existe no arquivo")
    elif pai and por_uuid[pai].get("isLeaf"):
        erros.append(f"{nome}: parent aponta para um documento, não para uma pasta")
    elif pai and por_uuid[pai].get("type") != n.get("type"):
        erros.append(f"{nome}: type '{n.get('type')}' diferente da pasta '{por_uuid[pai].get('name')}'")

for d in docs:
    nome = d.get("name")
    if not d.get("parent"):
        erros.append(f"{nome}: documento sem parent (precisa estar dentro de uma pasta)")
    texto = (d.get("text") or "").strip()
    if not texto:
        erros.append(f"{nome}: text vazio")
    elif not re.search(r"<[a-z]", texto, re.I):
        erros.append(f"{nome}: text sem HTML — use <p>…</p> por parágrafo")
    elif "\n" in texto:
        avisos.append(f"{nome}: text tem \\n — quebras de linha devem ser <p>")
    atalho = d.get("shortcut") or ""
    if len(atalho) > 30:
        erros.append(f"{nome}: shortcut com {len(atalho)} caracteres (máx. 30)")
    if atalho and not re.fullmatch(r"[a-z0-9._-]+", atalho):
        avisos.append(f"{nome}: shortcut '{atalho}' tem maiúscula, espaço ou acento")
    if re.search(r"\b(paciente|prontu[aá]rio|atendimento|nascimento|DN)\s*[:\-]", texto, re.I):
        avisos.append(f"{nome}: pode conter dado de paciente — confira")

repetidos = [s for s, c in Counter(d.get("shortcut") for d in docs if d.get("shortcut")).items() if c > 1]
for s in repetidos:
    erros.append(f"shortcut repetido: {s}")

irmaos = defaultdict(list)
for n in nos:
    irmaos[(n.get("parent"), n.get("type"), bool(n.get("isLeaf")))].append((n.get("name") or "").lower())
for (pai, _, _), nomes in irmaos.items():
    for nome, c in Counter(nomes).items():
        if c > 1:
            onde = por_uuid.get(pai, {}).get("name", "raiz")
            erros.append(f"nome repetido na pasta '{onde}': {nome}")

def profundidade(n):
    d, atual = 0, n
    while atual.get("parent") in por_uuid and d < 50:
        atual = por_uuid[atual["parent"]]
        d += 1
    return d

mais_funda = max([profundidade(p) for p in pastas] or [0])
if mais_funda > 8:
    erros.append(f"{mais_funda} níveis de pasta abaixo da raiz (máx. 8)")

print(f"pastas: {len(pastas)}")
print(f"máscaras: {sum(d.get('type') == 'mask' for d in docs)}")
print(f"frases: {sum(d.get('type') == 'phrase' for d in docs)}")
print(f"profundidade máxima: {mais_funda}")
for e in erros:
    print("ERRO  ", e)
for a in avisos:
    print("AVISO ", a)
print("\nRevise antes de importar." if erros else "\nPronto para importar.")
```

### Checklist manual (se não puder rodar código)

Confira item por item antes de entregar:

- [ ] O arquivo começa com `[` e termina com `]`, e é JSON válido (aspas duplas, sem vírgula sobrando no fim).
- [ ] Todo `uuid` é diferente dos outros.
- [ ] Todo `parent` aponta para o `uuid` de uma **pasta** que está no arquivo.
- [ ] Só as pastas raiz não têm `parent`, e nenhum documento fica sem pasta.
- [ ] Máscaras estão só em pastas `mask`, e frases só em pastas `phrase`.
- [ ] Nenhum nome se repete dentro da mesma pasta.
- [ ] Todo `text` está em `<p>…</p>`, sem `\n` e nunca vazio.
- [ ] Todo `shortcut` é minúsculo, sem espaço nem acento, tem até 30 caracteres e não se repete.
- [ ] No máximo 8 níveis de pasta abaixo da raiz.

### Modelo completo (importa como está)

```json
[
  { "uuid": "raiz-m", "name": "Minhas máscaras", "type": "mask", "order": 0 },
  { "uuid": "p-cranio", "name": "Crânio", "type": "mask", "order": 0, "parent": "raiz-m" },
  {
    "uuid": "d-rm-cranio",
    "name": "RM CRÂNIO NORMAL",
    "type": "mask",
    "order": 0,
    "parent": "p-cranio",
    "isLeaf": true,
    "text": "<p><strong>RESSONÂNCIA MAGNÉTICA DO CRÂNIO</strong></p><p><strong>Análise:</strong></p><p>Parênquima encefálico com sinal e morfologia preservados.</p><p><strong>Impressão diagnóstica:</strong></p><p>Exame dentro dos padrões da normalidade.</p>",
    "instructions": "",
    "speechText": "ressonância de crânio normal",
    "shortcut": "rmcranio"
  },
  { "uuid": "raiz-f", "name": "Minhas frases", "type": "phrase", "order": 0 },
  { "uuid": "p-figado", "name": "Fígado", "type": "phrase", "order": 0, "parent": "raiz-f" },
  {
    "uuid": "f-esteatose",
    "name": "ESTEATOSE LEVE",
    "type": "phrase",
    "order": 0,
    "parent": "p-figado",
    "isLeaf": true,
    "text": "<p>Fígado com ecogenicidade discretamente aumentada, compatível com esteatose leve.</p>",
    "instructions": "",
    "speechText": "esteatose leve",
    "shortcut": "esteatoseleve"
  }
]
```

### Lembretes ao usuário (inclua na entrega)

- Importar: **Biblioteca → botão + → Importar arquivo .leo** e escolher o `.json` (o importador aceita `.json` também).
- Se a pasta principal tiver o mesmo nome de uma que ele já tem, o LEORAD cria outra com um número no fim. Nada é sobrescrito.
- Depois de importar, vale abrir duas ou três máscaras e conferir a formatação antes de importar os próximos lotes.
