@meistrari/minuta-nuxt
v1.1.1
Published
Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser
Maintainers
Keywords
Readme
@meistrari/minuta-nuxt
Embed do editor de minuta do defesa.ai para qualquer app Nuxt: o documento paginado em A4 com edição rica e export DOCX fiel ao preview, renderizado como componente — sem iframe e sem segredos no browser.
Segue a mesma arquitetura do @meistrari/chat-nuxt: o componente fala com
rotas Nitro registradas no próprio app hospedeiro (/api/minuta/*), e o proxy
encaminha para a API do Case Defense Builder com o JWT Tela do usuário
logado — a API valida o token contra a auth central e resolve o workspace.
O pacote mora em packages/minuta-nuxt do repositório
case-defense-builder e é
publicado no npm pelo workflow Publish @meistrari/minuta-nuxt. O próprio
cliente do defesa.ai consome esta mesma versão publicada — a tela /minutas/:id
é só um invólucro do <MinutaEmbed>, então bug corrigido aqui chega nos dois.
Instalação
npm install @meistrari/minuta-nuxt// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@meistrari/minuta-nuxt'],
minutaNuxt: {
// URL da API do Case Defense Builder
cdbApiUrl: process.env.CDB_API_URL, // backend do defesa.ai
// opcional: cookie do host que carrega o JWT Tela do usuário
// (default: event.context.auth.token → cookie `tela-jwt`)
// authTokenCookie: 'meu-cookie-jwt',
},
})Uso
<template>
<MinutaEmbed
minuta-id="94a53f33-…"
style="height: 100vh"
@saved="onSaved"
/>
</template>Criando uma minuta a partir do host (composable auto-importado):
const { uploadMinutaFile, createMinuta } = useMinutaApi()
// arquivos de QUALQUER tipo — sobem pro Vault via proxy
const files = await Promise.all([...pickedFiles].map(f => uploadMinutaFile(f)))
const minuta = await createMinuta({
name: 'Contestação — João da Silva',
files,
// contexto livre sobre a peça (tipo, juízo, orientações do escritório…)
context: 'Contestação para o réu; linha de defesa: culpa exclusiva do consumidor.',
// agente Tela que escreve a minuta ("organizationName/repository")
agentId: 'tela/defesa-minuta-avulsa',
})
// navegue para a tela que renderiza <MinutaEmbed :minuta-id="minuta.id" />Para peça de texto fixo com campos variáveis, sem agente, veja Templates e variáveis.
Templates e variáveis
Nem toda peça precisa de agente. Quando o texto já está decidido e o que muda são alguns campos — petições intermediárias, ofícios, requerimentos de rotina — existe o caminho template: um documento com placeholders, escrito no mesmo editor A4, que vira minuta pronta no mesmo request, sem passar por agente nenhum.
O CDB não guarda template. Quem guarda é o app hospedeiro, que já tem cadastro para isso. O que o embed entrega é o editor e o formato; o que o CDB faz é trocar placeholder por valor.
Escrever o template
<MinutaTemplateEditor
v-model="cadastro.templateHtml"
:variables="CATALOGO"
style="height: 100vh"
@update:used-variables="keys => cadastro.variaveis = keys"
/>O catálogo é um array plano, definido pelo host — o CDB não sabe o que significa nenhuma dessas chaves:
const CATALOGO = [
{ key: 'PROCESSO', label: 'Nº do Processo', description: 'De info_processos.processo', group: 'Processo' },
{ key: 'NOME_REU', label: 'Nome do Réu', group: 'Réu' },
]No documento, o advogado digita / (ou usa o botão { } da barra), busca pelo
rótulo e insere. O placeholder é um nó atômico: seleciona inteiro, apaga
inteiro, e negrito/fonte aplicam nele por completo. Não dá para digitar uma
chave que não existe, e não dá para partir uma pela metade ao formatar —
que é o motivo de ser nó, e não #NOME_VARIAVEL no texto.
update:used-variables devolve as chaves usadas, na ordem do documento e sem
repetição. É com ela que o host sabe o que perguntar; ele nunca precisa ler o
HTML.
Gerar a peça
const minuta = await createMinuta({
name: 'Requerimento — contrato 123',
templateHtml: cadastro.templateHtml,
variables: { PROCESSO: '1234567-89.2026.8.26.0100', NOME_REU: null },
})
minuta.status // 'READY' — sem agente, sem polling
minuta.missingVariables // ['NOME_REU']Só texto nos valores: número e data chegam já formatados pelo host, que é quem
sabe se 1234.5 é R$ 1.234,50. null significa "não resolveu".
Variável sem valor não some. Ela continua sendo um placeholder vivo dentro
do documento, no parágrafo em que está — quem abrir a minuta vê o buraco onde
ele fica, em vez de descobrir que falta algo por um botão desabilitado. O host
decide o que fazer com missingVariables (abrir pendência, bloquear o
protocolo, ou deixar preencher no editor mesmo).
templateHtml e agentId se excluem: mandar os dois é 400.
Props do <MinutaTemplateEditor>
| Prop | Default | Descrição |
|---|---|---|
| modelValue | '' | HTML do template (v-model) |
| variables | [] | catálogo { key, label, description?, group? } |
| editable | true | permite editar |
| showToolbar | true | barra com o resumo das variáveis usadas |
| margins | 2,5cm/3cm | margens da página em px @96dpi |
| chrome | null | cabeçalho/rodapé; v-model:chrome para persistir |
Eventos: update:modelValue, update:usedVariables, update:chrome,
ready(editor). Slots: toolbar-start, toolbar-end.
defineExpose: getHtml(), usedVariables, editor.
Playground: bun run dev e abra /?template.
Props do <MinutaEmbed>
| Prop | Default | Descrição |
|---|---|---|
| minutaId | — | id da minuta avulsa no CDB |
| editable | true | permite editar o documento |
| autoSave | true | salva 2s após a última edição |
| showToolbar | true | barra com nome, estado de save e export |
| margins | 2,5cm/3cm | margens da página em px @96dpi |
| renamable | segue editable | nome editável na própria barra |
| chatAgentId | null | id de plataforma do agente assistente; sem ele, o chat não aparece |
Eventos: ready(editor), saved(minuta), error(message).
Slot: toolbar-start (o host injeta navegação própria, ex.: botão voltar).
defineExpose: saveNow(), exportDocx(), editor.
Dois agentes, dois lugares. O que escreve a minuta vai no agentId da
chamada de criação acima — é decisão de quem cria, e cada app manda o seu (o
mesmo campo existe na Data API, para quem cria do servidor; a env
MINUTA_AVULSA_AGENT do CDB é só o default de quem não mandar nada). O que
edita pelo chat é prop do componente (chatAgentId), porque vive dentro da
tela de edição.
A barra do embed inclui Histórico: versões são criadas automaticamente conforme o documento é editado (snapshot no auto-save, com espaçamento mínimo de 10 min) e podem ser restauradas — o estado atual vira um snapshot antes.
Como funciona
MinutaEmbedbusca a minuta em/api/minuta/:id(rota Nitro do módulo).- O proxy resolve o JWT do usuário (contexto de auth do host ou cookie) e
encaminha para
{cdbApiUrl}/standalone-minuta/:idcomAuthorization: Bearer. - Enquanto
status === 'PROCESSING', o embed faz polling (3s) — o próprio GET no CDB consulta o agente e materializa o resultado. - Com
status === 'READY', o HTML entra no editor paginado (Tiptap + motor de paginação A4 próprio). O export DOCX é 100% client-side, com as quebras de página idênticas ao preview.
Desenvolvimento
npm install
npm run build # nuxt-module-buildO núcleo do editor (MinutaDocxEditor.vue, paginationPlugin, docxExport,
editorExtensions) é portado do Case Defense Builder — a intenção é que o CDB
passe a consumir este package para existir um único editor mantido.
