# Gemini — Views e UX

> O módulo **não tem layout próprio**. Tudo acontece dentro das telas já existentes de
> Notícias e Publicações (CMS/AdminLTE) e na tela de Configurações.

---

## 1. Visão geral das telas

| Tela (CMS) | Arquivo | O que o módulo adiciona |
|---|---|---|
| Notícias — criar/editar | `v5/resources/views/prefecture/contents/noticias/form.blade.php` | botão "Preencher com IA" + modal + script |
| Publicações — criar | `v5/resources/views/prefecture/contents/publicacoes/create.blade.php` | botão "Preencher com IA" (o modal/script vêm do partial `form.blade.php`) |
| Publicações — form (partial) | `v5/resources/views/prefecture/contents/publicacoes/form.blade.php` | `@include` do modal + `ia-modal.js` + `route('gemini.ia')` |
| Configurações → aba "Opções" | `v5/resources/views/prefecture/configurations/configuracoes/form.blade.php` | o campo de texto da chave da API |

> **O cidadão (portal) não vê nenhuma tela do módulo.** O efeito do caminho B
> (mascaramento) é invisível: o texto exibido nos relatórios públicos já vem mascarado.

---

## 2. Componente Blade do modal

Arquivo: `v5/resources/views/prefecture/contents/components/ia-modal/index.blade.php`
(55 linhas). É um modal Bootstrap 4 com `id="modal-banner"`.

```blade
<div class="modal fade" id="modal-banner" tabindex="-1" role="dialog" aria-labelledby="modal-banner" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h3 class="modal-title col-md-11" id="title-modal">Envie o prompt</h3>
                <button type="button" class="close col-md-1 mt-3" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>

        <div id="ia-flash-message" aria-live="polite">
            @if ($errors->any())
                <div class="alert alert-danger mt-2">
                    <ul class="mb-0">
                        @foreach ($errors->all() as $error)
                            <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif
        </div>

        <div class="modal-body p-0 m-4">
            <div class="form-group">
                <textarea id="promptInput" name="prompt" class="form-control" placeholder="Digite o prompt desejado para gerar o conteúdo"></textarea>
            </div>

            <div class="w-100 text-right mt-3">
                <button type="button" id="btnCancelarIA" data-dismiss="modal" class="btn btn-danger btn-sm py-3">
                    Cancelar
                </button>
                <button type="button" id="btnGerarIA" class="btn btn-success btn-sm py-3 ml-2">
                    Gerar conteúdo
                </button>
            </div>
        </div>

            <div class="modal-footer"></div>
        </div>
    </div>
</div>
```

Observações de UX/HTML:

- O componente **não recebe parâmetros** — é um include estático, sem `@props`.
- O `id="modal-banner"` **colide** com o componente
  `v5/resources/views/prefecture/contents/components/banner-modal/index.blade.php:11`,
  que usa exatamente o mesmo `id`. Hoje os dois não coexistem na mesma página (o
  `banner-modal` não é incluído em `noticias/` nem em `publicacoes/`), mas qualquer
  inclusão futura quebraria o `show`/`hide` do modal (o jQuery seleciona o primeiro match).
- Carrega o CSS do FontAwesome 5.10 por CDN (`https://pro.fontawesome.com/...`) — dependência
  externa de front-end.
- O bloco `#ia-flash-message` já renderiza `$errors` do servidor — mas o POST é feito por
  **AJAX**, então esse bloco quase nunca é usado (o JS injeta o alerta dinamicamente).

---

## 3. Botão "Preencher com IA"

### Notícias — `v5/resources/views/prefecture/contents/noticias/form.blade.php:61`

```blade
                <button type="button" id="abrirModalIA" class="btn btn-primary btn-sm ml-2">
                    <i class="fa fa-star"></i> Preencher com IA
                </button>
```

### Publicações — `v5/resources/views/prefecture/contents/publicacoes/create.blade.php:11`

```blade
            <button type="button" id="abrirModalIA" class="btn btn-primary btn-sm">
                <i class="fa fa-star"></i> Preencher com IA
            </button>
```

O botão é **renderizado incondicionalmente** — não há verificação de permissão nem de
existência da chave da API. Um operador sem chave configurada só descobrirá o problema ao
clicar em "Gerar conteúdo" (aí recebe o 500 "Erro de Configuração", que, por causa do
bug descrito no item 5, **não é exibido**).

---

## 4. JavaScript — `ia-modal.js`

Arquivo: `v5/public/assets/js/custom/utilities/helpers/ia-modal.js`

### 4.1 Inclusão

Em `v5/resources/views/prefecture/contents/noticias/form.blade.php:287-294`:

```blade
    <!-- Preenchimento dos campos Título e Texto com IA -->
    <script src="{{ asset('assets/js/custom/utilities/helpers/ia-modal.js') }}"></script>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <script>
        $(function () {
            setupIAModal({
                routeUrl: '{{ route('gemini.ia') }}'
            });
        });
    </script>
```

Em `v5/resources/views/prefecture/contents/publicacoes/form.blade.php:153-161` — bloco
idêntico.

> A view de **Publicações — editar** (`v5/resources/views/prefecture/contents/publicacoes/edit.blade.php:15`)
> **não** contém o botão `#abrirModalIA` — ele só existe em `create.blade.php`. Porém o
> `edit.blade.php` inclui o partial `form.blade.php`, que traz o `@include` do modal e o
> `ia-modal.js`: o **modal e o script são carregados** na tela de edição, apenas **sem
> botão disparador** (o `setupIAModal` liga o handler de `#abrirModalIA`, que não existe
> ali — a inicialização é inofensiva, apenas não faz nada).

### 4.2 Contrato da função

```js
function setupIAModal({
  btnSelector = '#btnGerarIA',
  inputSelector = '#promptInput',
  titleSelector = '#title',
  textSelector = '#text',
  modalSelector = '#modal-banner',
  openModalSelector = '#abrirModalIA',
  routeUrl,
}) {
```

| Parâmetro | Padrão | Papel |
|---|---|---|
| `btnSelector` | `#btnGerarIA` | botão "Gerar conteúdo" |
| `inputSelector` | `#promptInput` | textarea do prompt |
| `titleSelector` | `#title` | input de título do conteúdo (**id literal `#title`**) |
| `textSelector` | `#text` | textarea Summernote do corpo |
| `modalSelector` | `#modal-banner` | o modal |
| `openModalSelector` | `#abrirModalIA` | botão que abre o modal |
| `routeUrl` | — | URL da rota `gemini.ia` |

### 4.3 Fluxo do clique

```js
  $btnGerarIA.on('click', function (e) {
    e.preventDefault();
    const promptValue = $promptInput.val();

    if (!promptValue.trim()) {
      const errorMessage = 'O campo de prompt não pode estar vazio.';
      $('#ia-flash-message').html(`
        <div class="alert alert-danger mt-2" role="alert">${errorMessage}</div>
      `);
      return;
    }

    $btnGerarIA.prop('disabled', true);
    $btnGerarIA.html('<i class="fa fa-spinner fa-spin"></i> Gerando...');

    $.ajax({
      url: routeUrl,
      method: 'POST',
      headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
      },
      contentType: 'application/json',
      data: JSON.stringify({ prompt: promptValue }),
    })
      .done(function (data) {
        if (data.success === false) {
          $('#ia-flash-message').html(`
            <div class="alert alert-danger mt-2" role="alert">
                Ocorreu um erro ao gerar o conteúdo. O Prompt é obrigatório e deve conter no mínimo 5 caracteres.
            </div>
          `);
          return;
        }

        $(titleSelector).val(data.title || '');
        $(textSelector).summernote('code', data.text || '');

        $modal.modal('hide');
        $promptInput.val('');
      })
      .fail(function (jqXHR) {
        let errorMessage =
          'Ocorreu um erro ao gerar o conteúdo. Verifique sua chave API e tente novamente.<br>';
        if (
          jqXHR.status === 422 &&
          jqXHR.responseJSON &&
          jqXHR.responseJSON.errors
        ) {
          errorMessage = Object.values(jqXHR.responseJSON.errors)
            .map(arr => arr.join('<br>'))
            .join('<br>');
        }
        $('#ia-flash-message').html(`
          <div class="alert alert-danger mt-2" role="alert">${errorMessage}</div>
      `);
      })
      .always(function () {
        $btnGerarIA.prop('disabled', false);
        $btnGerarIA.html(originalBtnHtml);
      });
  });
```

Comportamento observável:

1. O botão fica **desabilitado** enquanto o prompt está vazio (`toggleBtnGerarIA` no evento
   `input` do textarea).
2. Ao clicar, o botão é desabilitado e mostra um spinner + "Gerando...".
3. `POST` JSON `{ "prompt": "..." }` com o header `X-CSRF-TOKEN`.
4. **Sucesso**: `#title` recebe `data.title`; `#text` (Summernote) recebe `data.text`
   via `summernote('code', ...)`; o modal fecha; o prompt é limpo.
5. **Erro 422**: exibe as mensagens de validação do Laravel.
6. **Qualquer outro erro**: exibe a mensagem genérica
   *"Ocorreu um erro ao gerar o conteúdo. Verifique sua chave API e tente novamente."*.
7. `.always()` restaura o botão.

### 4.4 Bug — a mensagem de erro do servidor nunca é exibida

O controller retorna, no caso de chave ausente, HTTP 500 com
`{"title":"Erro de Configuração","text":"A chave da API do Google Gemini não está configurada..."}`.
O `.fail()` do jQuery **ignora** `responseJSON.title`/`responseJSON.text` e mostra
apenas a mensagem genérica. O operador **nunca** vê "Erro de Configuração".

Do mesmo modo, o bloco `.done()` testa `data.success === false` — mas o controller
**nunca** retorna um campo `success` (só `title` e `text`). Esse ramo é **código morto**.

### 4.5 Bug — texto da mensagem não bate com a validação

A mensagem do ramo morto diz *"deve conter no mínimo 5 caracteres"*, mas a regra do
Form Request é `min:4`. O 422 real traz a mensagem correta do Laravel; apenas o texto
fixo está divergente.

### 4.6 Fragilidade — seletores globais

`#title` e `#text` são IDs **genéricos**. O JS é inicializado em todas as páginas que
incluem o partial; se algum outro formulário do CMS usar `#title`/`#text`, o conteúdo
gerado pode ser gravado no campo errado. O ideal seria parametrizar os seletores junto
com `routeUrl` (a função já aceita isso — mas as views não exploram).

### 4.7 Ausência de timeout no cliente

Não há `timeout` no `$.ajax`. Se o servidor ficar pendurado (o `GeminiController` não
configura timeout no Guzzle), o botão fica em "Gerando..." indefinidamente.

---

## 5. Tela de Configurações — onde a chave é digitada

Arquivo: `v5/resources/views/prefecture/configurations/configuracoes/form.blade.php:269-279`

```blade
            <div class="tab-pane fade col-md-12 @if (session('display') == 'options') active in @endif" id="divOptions"
                role="tabpanel" aria-labelledby="config-tab">
                @php
                    $opcoes = $configurations->filter(function($item) {
                        return (
                            (
                                $item->order >= 23 && $item->order <= 34
                            ) || in_array($item->key, ['gemini_api_key', 'geocoding_api_key', 'full_list_requests_disabled', 'quantidade_noticias_ultimas_noticias'])
                        )
                        && !in_array($item->key, ['link_form_artist_registration', 'link_form_equipment_registration', 'relatorio_completolai']);
                    })->sortBy('id')->sortBy('order');
                @endphp

                @foreach ($opcoes as $configuration)
                    @include('prefecture.configurations.configuracoes.loop_config')
                @endforeach
            </div>
```

A chave aparece na **aba "Opções"** graças ao `in_array($item->key, ['gemini_api_key', ...])`.

O campo é renderizado pelo partial
`v5/resources/views/prefecture/configurations/configuracoes/loop_config.blade.php`, no
ramo genérico (tipo `text`):

```blade
            @elseif($configuration->key != 'fornecedor')
                <input class="config-input form-control @if($configuration->key == 'telefone' || $configuration->key == 'telefone_2' ) advanced-phone @endif" placeholder="{{ $configuration->placeholder }}"
                    maxlength="{{ $configuration->max_input }}" minlength="{{ $configuration->min_input }}"
                    @if ($configuration->notnull == true) required @endif type="{{ $configuration->type }}"
                    id="{{$configuration->key}}" name="{{ $configuration->key }}" @if (isset($configuration->value) && $configuration->value != '')
                    value="{{ $configuration->value }}" @endif accept="image/*">
```

Características da renderização:

| Aspecto | Comportamento |
|---|---|
| Tipo do input | `text` (vem de `configurations.type`) — **não** é `password` |
| Valor exibido | `value="{{ $configuration->value }}"` → **a chave aparece em claro na tela** |
| `maxlength` | 255 (`configurations.max_input`) |
| `required` | não (porque `notnull = false`) |
| Máscara JS | nenhuma (`vanilla-masker` é carregado na página, mas não se aplica a este campo) |
| Rótulo | "Chave da API do Gemini", com `?` de tooltip exibindo a `description` |

> **Risco:** a chave é exibida em texto claro para qualquer operador com acesso à tela de
> Configurações (apenas `check.logged` na rota `configuracoes/editar/{slug}`), e — como
> visto em `02-MODELAGEM.md` — o valor é gravado em claro no `activity_log` a cada
> edição.

### 5.1 Equivalente na v6

`v6/resources/views/prefecture/configurations/configuracoes/form.blade.php:157` mantém o
campo, com filtro equivalente:

```php
$opcoes = $configurations->filter(function($item) {
    return ($item->order >= 24 && $item->order <= 37) || $item->key === 'full_list_requests_disabled' || $item->key === 'information_request_recaptcha'
    || $item->key === 'gemini_api_key' || $item->key === 'diario_oficial_segregacao_funcoes'; })->sortBy('id')->sortBy('order');
```

---

## 6. Layouts e assets

| Item | Valor |
|---|---|
| Layout do CMS | `admin.layouts.app` (AdminLTE 1.22) — `v5/resources/views/prefecture/contents/publicacoes/create.blade.php:1` |
| Layout do portal | **não aplicável** — o módulo não tem tela pública |
| JS do modal | `v5/public/assets/js/custom/utilities/helpers/ia-modal.js` (depende de **jQuery** + **Bootstrap 4 modal** + **Summernote**) |
| Editor WYSIWYG do corpo | Summernote (`v5/public/assets/plugins/summernote/summernote.min.js`) |
| Ícone do botão | FontAwesome `fa-star` |
| CSS do modal | inline no próprio Blade + FontAwesome 5.10 por CDN |

Não há exportação (Excel/PDF/CSV) associada ao módulo.

---

## 7. Referências

- `v5/resources/views/prefecture/contents/components/ia-modal/index.blade.php`
- `v5/resources/views/prefecture/contents/noticias/form.blade.php:61, 273, 287-294`
- `v5/resources/views/prefecture/contents/publicacoes/create.blade.php:11`
- `v5/resources/views/prefecture/contents/publicacoes/form.blade.php:135, 153-161`
- `v5/public/assets/js/custom/utilities/helpers/ia-modal.js`
- `v5/resources/views/prefecture/configurations/configuracoes/form.blade.php:269-279`
- `v5/resources/views/prefecture/configurations/configuracoes/loop_config.blade.php`
- `v6/resources/views/prefecture/configurations/configuracoes/form.blade.php:157`
