feat: Complete all 10 legal flows, OpenRouter/Gemini integration, and tests

- Switch LLM provider to OpenRouter with Gemini 2.5 Flash as default
- Add 4 trabalhista flows: rescisão indireta, dano moral, acúmulo de
  função, contestação trabalhista
- Add 5 cível flows: cobrança, indenização, obrigação de fazer,
  contestação cível, contrato (revisão)
- Register all 10 flows in the flow registry
- Write 36 unit tests (sanitize, parse-flow-state, prompt-builder,
  url-builder, flow-engine, llm-client)
- Write 24 integration tests (all routes, deep links, step flow)
- All 60 tests passing, all 12 routes verified at runtime

https://claude.ai/code/session_01CvrcMDqfCKWV2hC3xpRbx3
This commit is contained in:
Claude 2026-03-09 00:12:08 +00:00
parent 001bd7de0a
commit e966049169
No known key found for this signature in database
24 changed files with 2340 additions and 26 deletions

View File

@ -1,7 +1,11 @@
# LLM Provider (openai or anthropic)
LLM_PROVIDER=openai
LLM_API_KEY=sk-your-api-key
LLM_MODEL=gpt-4o-mini
# LLM Provider (openrouter, openai, or anthropic)
LLM_PROVIDER=openrouter
LLM_API_KEY=sk-or-v1-your-openrouter-key
LLM_MODEL=google/gemini-2.5-flash
LLM_BASE_URL=https://openrouter.ai/api/v1
# Alternative: use OPENROUTER_API_KEY directly
# OPENROUTER_API_KEY=sk-or-v1-your-openrouter-key
# Server
PORT=3000

View File

@ -5,12 +5,12 @@
"type": "module",
"scripts": {
"dev": "concurrently \"npm run dev:server\" \"npm run dev:css\"",
"dev:server": "tsx watch src/server.ts",
"dev:server": "tsx watch --env-file=.env src/server.ts",
"dev:css": "npx @tailwindcss/cli -i src/styles/input.css -o src/public/css/app.css --watch",
"build": "npm run build:css && npm run build:server",
"build:server": "tsc",
"build:css": "npx @tailwindcss/cli -i src/styles/input.css -o src/public/css/app.css --minify",
"start": "node dist/server.js",
"start": "node --env-file=.env dist/server.js",
"test": "vitest run",
"test:watch": "vitest"
},

View File

@ -0,0 +1,233 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import Fastify from "fastify";
import fastifyView from "@fastify/view";
import fastifyFormbody from "@fastify/formbody";
import nunjucks from "nunjucks";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { homeRoutes } from "../routes/home.js";
import { flowRoutes } from "../routes/flow.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const templatesDir = join(__dirname, "..", "templates");
async function buildApp() {
const app = Fastify({ logger: false });
await app.register(fastifyFormbody);
await app.register(fastifyView, {
engine: { nunjucks },
templates: templatesDir,
});
await app.register(homeRoutes);
await app.register(flowRoutes);
return app;
}
describe("Routes Integration", () => {
let app: Awaited<ReturnType<typeof buildApp>>;
beforeAll(async () => {
app = await buildApp();
await app.ready();
});
afterAll(async () => {
await app.close();
});
describe("GET /", () => {
it("returns 200", async () => {
const response = await app.inject({ method: "GET", url: "/" });
expect(response.statusCode).toBe(200);
});
it("contains page content", async () => {
const response = await app.inject({ method: "GET", url: "/" });
expect(response.body).toContain("Jus IA");
});
});
describe("GET /health", () => {
it("returns ok status", async () => {
const response = await app.inject({ method: "GET", url: "/health" });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ status: "ok" });
});
});
describe("POST /selecionar", () => {
it("returns 200 for trabalhista", async () => {
const response = await app.inject({
method: "POST",
url: "/selecionar",
payload: { tipo_tarefa: "peticao-inicial", area: "trabalhista" },
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("Horas Extras");
});
it("returns 200 for civel", async () => {
const response = await app.inject({
method: "POST",
url: "/selecionar",
payload: { tipo_tarefa: "peticao-inicial", area: "civel" },
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("Cobrança");
});
it("returns not-available for unknown area", async () => {
const response = await app.inject({
method: "POST",
url: "/selecionar",
payload: { tipo_tarefa: "peticao-inicial", area: "penal" },
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("disponível");
});
});
describe("GET /:area/:subtipo (deep links)", () => {
it("returns 200 for trabalhista/horas-extras", async () => {
const response = await app.inject({
method: "GET",
url: "/trabalhista/horas-extras",
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("Dados do Caso");
});
it("returns 200 for civel/cobranca", async () => {
const response = await app.inject({
method: "GET",
url: "/civel/cobranca",
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("Dados da Dívida");
});
it("returns not-available for unknown flow", async () => {
const response = await app.inject({
method: "GET",
url: "/trabalhista/nao-existe",
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("disponível");
});
});
describe("POST /:area/iniciar", () => {
it("starts horas-extras flow", async () => {
const response = await app.inject({
method: "POST",
url: "/trabalhista/iniciar",
payload: { subtipo: "horas-extras", _tipo_tarefa: "peticao-inicial" },
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("Dados do Caso");
});
it("starts cobranca flow", async () => {
const response = await app.inject({
method: "POST",
url: "/civel/iniciar",
payload: { subtipo: "cobranca", _tipo_tarefa: "peticao-inicial" },
});
expect(response.statusCode).toBe(200);
});
it("returns not-available for unknown subtipo", async () => {
const response = await app.inject({
method: "POST",
url: "/trabalhista/iniciar",
payload: { subtipo: "nao-existe" },
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("disponível");
});
});
describe("POST /:area/:subtipo/step/:stepNumber (step submission)", () => {
it("advances from step 1 to step 2 in horas-extras", async () => {
const response = await app.inject({
method: "POST",
url: "/trabalhista/horas-extras/step/1",
payload: {
_area: "trabalhista",
_subtipo: "horas-extras",
_tipo_tarefa: "peticao-inicial",
_step: "1",
_total_steps: "5",
_responses: "{}",
empregador_tipo: "Pessoa Jurídica (empresa)",
regime: "CLT",
jornada_contratual: "44h semanais",
data_inicio: "2020-01-01",
data_fim: "2024-01-01",
ainda_empregado: "Não",
horas_extras_semana: "5 a 10 horas",
banco_horas: "Não",
},
});
expect(response.statusCode).toBe(200);
expect(response.body).toContain("Detalhes do Caso");
});
it("shows preview after final step in horas-extras", async () => {
const responses = JSON.stringify({
empregador_tipo: "Pessoa Jurídica (empresa)",
regime: "CLT",
jornada_contratual: "44h semanais",
data_inicio: "2020-01-01",
data_fim: "2024-01-01",
ainda_empregado: "Não",
horas_extras_semana: "5 a 10 horas",
banco_horas: "Não",
});
const response = await app.inject({
method: "POST",
url: "/trabalhista/horas-extras/step/2",
payload: {
_area: "trabalhista",
_subtipo: "horas-extras",
_tipo_tarefa: "peticao-inicial",
_step: "2",
_total_steps: "5",
_responses: responses,
registro_ponto: "Sim, eletrônico",
testemunhas: "Sim",
pagamento_parcial: "Não, nenhum pagamento",
},
});
expect(response.statusCode).toBe(200);
// Should contain the assembled prompt with CLT reference
expect(response.body).toContain("CLT");
});
});
describe("All registered flows have valid deep links", () => {
const flows = [
"/trabalhista/horas-extras",
"/trabalhista/rescisao-indireta",
"/trabalhista/dano-moral",
"/trabalhista/acumulo-funcao",
"/trabalhista/contestacao",
"/civel/cobranca",
"/civel/indenizacao",
"/civel/obrigacao-fazer",
"/civel/contestacao",
"/civel/contrato",
];
for (const flowUrl of flows) {
it(`GET ${flowUrl} returns 200`, async () => {
const response = await app.inject({ method: "GET", url: flowUrl });
expect(response.statusCode).toBe(200);
});
}
});
});

View File

@ -1,5 +1,16 @@
import type { FlowConfig } from "../flows/types.js";
// Trabalhista flows
import { horasExtrasFlow } from "../flows/trabalhista/horas-extras.js";
import { rescisaoIndiretaFlow } from "../flows/trabalhista/rescisao-indireta.js";
import { danoMoralFlow } from "../flows/trabalhista/dano-moral.js";
import { acumuloFuncaoFlow } from "../flows/trabalhista/acumulo-funcao.js";
import { contestacaoTrabalhistaFlow } from "../flows/trabalhista/contestacao.js";
// Cível flows
import { cobrancaFlow } from "../flows/civel/cobranca.js";
import { indenizacaoFlow } from "../flows/civel/indenizacao.js";
import { obrigacaoFazerFlow } from "../flows/civel/obrigacao-fazer.js";
import { contestacaoCivelFlow } from "../flows/civel/contestacao.js";
import { contratoRevisaoFlow } from "../flows/civel/contrato.js";
/** Registry of all available flows */
const flowRegistry: Map<string, FlowConfig> = new Map();
@ -9,9 +20,19 @@ function registerFlow(flow: FlowConfig): void {
flowRegistry.set(key, flow);
}
// Register all flows
// Register all trabalhista flows
registerFlow(horasExtrasFlow);
// TODO: Register remaining 9 flows as they are built
registerFlow(rescisaoIndiretaFlow);
registerFlow(danoMoralFlow);
registerFlow(acumuloFuncaoFlow);
registerFlow(contestacaoTrabalhistaFlow);
// Register all cível flows
registerFlow(cobrancaFlow);
registerFlow(indenizacaoFlow);
registerFlow(obrigacaoFazerFlow);
registerFlow(contestacaoCivelFlow);
registerFlow(contratoRevisaoFlow);
/** Get a flow by area/subtipo */
export function getFlow(area: string, subtipo: string): FlowConfig | undefined {

View File

@ -3,9 +3,13 @@ export const config = {
host: process.env.HOST || "0.0.0.0",
nodeEnv: process.env.NODE_ENV || "development",
llm: {
provider: process.env.LLM_PROVIDER || "openai",
apiKey: process.env.LLM_API_KEY || "",
model: process.env.LLM_MODEL || "gpt-4o-mini",
provider: (process.env.LLM_PROVIDER || "openrouter") as
| "openrouter"
| "openai"
| "anthropic",
apiKey: process.env.LLM_API_KEY || process.env.OPENROUTER_API_KEY || "",
model: process.env.LLM_MODEL || "google/gemini-2.5-flash",
baseUrl: process.env.LLM_BASE_URL || "https://openrouter.ai/api/v1",
},
analyticsId: process.env.ANALYTICS_ID || "",
sentryDsn: process.env.SENTRY_DSN || "",

View File

@ -0,0 +1,179 @@
import type { FlowConfig } from "../types.js";
export const cobrancaFlow: FlowConfig = {
area: "civel",
areaLabel: "Cível",
subtipo: "cobranca",
subtipoLabel: "Cobrança",
tipoTarefa: "peticao-inicial",
steps: [
{
stepNumber: 1,
title: "Dados da Dívida",
requiresLlm: false,
groups: [
{
title: "Credor e Devedor",
questions: [
{
id: "relacao_partes",
text: "Qual a relação entre as partes?",
type: "select",
options: [
"Contrato de prestação de serviço",
"Empréstimo pessoal",
"Venda de produto",
"Aluguel",
"Cheque devolvido",
"Nota promissória",
"Outro",
],
required: true,
},
{
id: "tipo_devedor",
text: "O devedor é pessoa física ou jurídica?",
type: "select",
options: ["PF", "PJ"],
required: true,
},
],
},
{
title: "Valor e Prazo",
questions: [
{
id: "faixa_valor",
text: "Qual a faixa de valor da dívida?",
type: "select",
options: [
"Até R$ 5 mil",
"R$ 5 a 20 mil",
"R$ 20 a 50 mil",
"Acima de R$ 50 mil",
],
required: true,
},
{
id: "data_vencimento",
text: "Data de vencimento da dívida",
type: "date",
required: true,
},
{
id: "tentou_cobranca",
text: "Já tentou cobrar antes?",
type: "select",
options: [
"Sim, extrajudicialmente",
"Sim, com protesto",
"Não",
],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Documentação",
requiresLlm: true,
groups: [
{
title: "Provas da Dívida",
questions: [
{
id: "documentos",
text: "Quais documentos possui?",
type: "multiselect",
options: [
"Contrato assinado",
"Nota promissória",
"Cheque",
"Notas fiscais",
"Comprovantes de transferência",
"Mensagens",
"E-mails",
"Nenhum documento formal",
],
required: true,
},
{
id: "divida_reconhecida",
text: "A dívida é reconhecida pelo devedor?",
type: "select",
options: [
"Sim, devedor reconhece",
"Parcialmente",
"Não, devedor contesta",
],
required: true,
},
],
},
{
title: "Situação Atual",
questions: [
{
id: "parcelas_pagas",
text: "Houve pagamento parcial?",
type: "select",
options: [
"Nenhuma",
"Algumas parcelas",
"Maioria paga, faltam poucas",
],
required: true,
},
{
id: "possui_garantia",
text: "Há garantia vinculada à dívida?",
type: "select",
options: [
"Sim, com garantia real",
"Sim, com fiador",
"Não",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial de ação de cobrança com os seguintes dados:
**Partes:**
- Credor: [a ser preenchido pelo advogado]
- Devedor: {{tipo_devedor}}
**Relação entre as partes:**
- Origem: {{relacao_partes}}
- Faixa de valor: {{faixa_valor}}
- Data de vencimento: {{data_vencimento}}
- Cobrança prévia: {{tentou_cobranca}}
**Documentação e provas:**
- Documentos disponíveis: {{documentos}}
- Reconhecimento da dívida: {{divida_reconhecida}}
- Parcelas pagas: {{parcelas_pagas}}
- Garantia: {{possui_garantia}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 318 do CPC (procedimento comum)
- Art. 319 do CPC (requisitos da petição inicial)
- Art. 784 do CPC (títulos executivos extrajudiciais)
- Art. 397 do CC (mora)
Inclua pedidos de: pagamento do valor principal, juros de mora, correção monetária, custas processuais e honorários advocatícios.`,
legalReferences: [
"art. 318 CPC",
"art. 319 CPC",
"art. 784 CPC",
"art. 397 CC",
],
};

View File

@ -0,0 +1,182 @@
import type { FlowConfig } from "../types.js";
export const contestacaoCivelFlow: FlowConfig = {
area: "civel",
areaLabel: "Cível",
subtipo: "contestacao",
subtipoLabel: "Contestação Cível",
tipoTarefa: "contestacao",
steps: [
{
stepNumber: 1,
title: "Dados do Processo",
requiresLlm: false,
groups: [
{
title: "Processo",
questions: [
{
id: "numero_processo",
text: "Número do processo",
type: "text",
required: true,
},
{
id: "vara_tribunal",
text: "Vara e tribunal",
type: "text",
required: true,
},
{
id: "tipo_acao_autor",
text: "Qual o tipo de ação movida pelo autor?",
type: "select",
options: [
"Cobrança",
"Indenização",
"Obrigação de fazer",
"Revisional",
"Consignação",
"Outra",
],
required: true,
},
],
},
{
title: "Partes",
questions: [
{
id: "posicao_cliente",
text: "Posição do cliente no processo",
type: "select",
options: [
"Réu pessoa física",
"Réu pessoa jurídica",
],
required: true,
},
{
id: "valor_causa",
text: "Valor da causa",
type: "select",
options: [
"Até R$ 20 mil",
"R$ 20 a 50 mil",
"R$ 50 a 100 mil",
"Acima de R$ 100 mil",
],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Estratégia de Defesa",
requiresLlm: true,
groups: [
{
title: "Preliminares",
questions: [
{
id: "preliminares",
text: "Quais preliminares deseja alegar?",
type: "multiselect",
options: [
"Inépcia da inicial",
"Ilegitimidade passiva",
"Falta de interesse de agir",
"Incompetência",
"Litispendência",
"Coisa julgada",
"Prescrição",
"Decadência",
"Nenhuma",
],
required: true,
},
{
id: "merito_defesa",
text: "Qual a tese principal de defesa no mérito?",
type: "select",
options: [
"Fato não ocorreu",
"Fato ocorreu diferente",
"Inexistência de dano",
"Culpa exclusiva do autor",
"Caso fortuito ou força maior",
"Pagamento já realizado",
],
required: true,
},
],
},
{
title: "Provas",
questions: [
{
id: "documentos_defesa",
text: "Quais documentos possui para defesa?",
type: "multiselect",
options: [
"Contrato",
"Comprovantes de pagamento",
"E-mails",
"Protocolo",
"Fotos",
"Testemunhas",
"Perícia",
"Nenhum",
],
required: true,
},
{
id: "pedido_reconvencao",
text: "Deseja fazer pedido reconvencional?",
type: "select",
options: ["Sim", "Não"],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma contestação cível com os seguintes dados:
**Processo:**
- Número: {{numero_processo}}
- Vara/Tribunal: {{vara_tribunal}}
- Tipo de ação: {{tipo_acao_autor}}
- Valor da causa: {{valor_causa}}
**Partes:**
- Contestante: {{posicao_cliente}}
**Estratégia de defesa:**
- Preliminares: {{preliminares}}
- Tese de mérito: {{merito_defesa}}
**Provas e pedidos:**
- Documentos: {{documentos_defesa}}
- Reconvenção: {{pedido_reconvencao}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 335 do CPC (prazo para contestação)
- Art. 336 do CPC (ônus de impugnar especificamente)
- Art. 337 do CPC (preliminares de contestação)
- Art. 343 do CPC (reconvenção)
Inclua: preliminares aplicáveis, impugnação específica dos fatos, defesa de mérito, requerimento de provas e pedidos finais.`,
legalReferences: [
"art. 335 CPC",
"art. 336 CPC",
"art. 337 CPC",
"art. 343 CPC",
],
};

View File

@ -0,0 +1,196 @@
import type { FlowConfig } from "../types.js";
export const contratoRevisaoFlow: FlowConfig = {
area: "civel",
areaLabel: "Cível",
subtipo: "contrato",
subtipoLabel: "Contrato (Revisão)",
tipoTarefa: "contrato",
steps: [
{
stepNumber: 1,
title: "Dados do Contrato",
requiresLlm: false,
groups: [
{
title: "Tipo de Contrato",
questions: [
{
id: "tipo_contrato",
text: "Qual o tipo de contrato?",
type: "select",
options: [
"Aluguel",
"Prestação de serviço",
"Financiamento",
"Empréstimo",
"Seguro",
"Outro",
],
required: true,
},
{
id: "partes_contrato",
text: "Quais as partes do contrato?",
type: "select",
options: ["PF x PF", "PF x PJ", "PJ x PJ"],
required: true,
},
{
id: "data_contrato",
text: "Data de assinatura do contrato",
type: "date",
required: true,
},
{
id: "vigencia",
text: "Situação do contrato",
type: "select",
options: ["Em vigor", "Encerrado", "Rescindido"],
required: true,
},
],
},
{
title: "Problema",
questions: [
{
id: "problema_principal",
text: "Qual o problema principal do contrato?",
type: "select",
options: [
"Cláusula abusiva",
"Reajuste abusivo",
"Descumprimento",
"Vício de consentimento",
"Onerosidade excessiva",
"Outro",
],
required: true,
},
{
id: "tentou_negociar",
text: "Tentou negociar com a outra parte?",
type: "select",
options: ["Sim, sem sucesso", "Não"],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Detalhes da Revisão",
requiresLlm: true,
groups: [
{
title: "Cláusulas",
questions: [
{
id: "clausulas_contestadas",
text: "Cláusulas que deseja revisar",
type: "text",
placeholder: "Quais cláusulas deseja revisar?",
required: true,
},
{
id: "valor_atual",
text: "Valor atual do contrato",
type: "text",
placeholder: "Valor mensal ou total atual",
required: true,
},
{
id: "valor_pretendido",
text: "Valor pretendido",
type: "text",
placeholder: "Valor que considera justo",
required: true,
},
{
id: "fundamentacao",
text: "Fundamentação legal principal",
type: "select",
options: [
"CDC - relação de consumo",
"CC - onerosidade excessiva",
"CC - lesão",
"Lei do Inquilinato",
"Outro",
],
required: true,
},
],
},
{
title: "Documentação",
questions: [
{
id: "documentos",
text: "Quais documentos possui?",
type: "multiselect",
options: [
"Contrato original",
"Aditivos",
"Comprovantes de pagamento",
"Notificações",
"Extratos",
"Nenhum",
],
required: true,
},
{
id: "pedido_tutela",
text: "Deseja pedir tutela de urgência?",
type: "select",
options: [
"Sim, com tutela de urgência",
"Não",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial de ação revisional de contrato com os seguintes dados:
**Contrato:**
- Tipo: {{tipo_contrato}}
- Partes: {{partes_contrato}}
- Data: {{data_contrato}}
- Vigência: {{vigencia}}
**Problema:**
- Problema principal: {{problema_principal}}
- Tentativa de negociação: {{tentou_negociar}}
**Revisão pretendida:**
- Cláusulas contestadas: {{clausulas_contestadas}}
- Valor atual: {{valor_atual}}
- Valor pretendido: {{valor_pretendido}}
- Fundamentação: {{fundamentacao}}
**Documentação e pedidos:**
- Documentos: {{documentos}}
- Tutela de urgência: {{pedido_tutela}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 317 do CC (correção do valor da prestação)
- Art. 478 do CC (resolução por onerosidade excessiva)
- Art. 51 do CDC (nulidade de cláusulas abusivas)
- Art. 6º, V do CDC (modificação de cláusulas desproporcionais)
Inclua pedidos de: revisão das cláusulas abusivas, adequação dos valores, tutela de urgência (se aplicável), custas processuais e honorários advocatícios.`,
legalReferences: [
"art. 317 CC",
"art. 478 CC",
"art. 51 CDC",
"art. 6º, V CDC",
],
};

View File

@ -0,0 +1,192 @@
import type { FlowConfig } from "../types.js";
export const indenizacaoFlow: FlowConfig = {
area: "civel",
areaLabel: "Cível",
subtipo: "indenizacao",
subtipoLabel: "Indenização",
tipoTarefa: "peticao-inicial",
steps: [
{
stepNumber: 1,
title: "Dados do Fato",
requiresLlm: false,
groups: [
{
title: "Tipo de Dano",
questions: [
{
id: "tipo_indenizacao",
text: "Qual o tipo de indenização pretendida?",
type: "select",
options: [
"Dano material",
"Dano moral",
"Danos estéticos",
"Lucros cessantes",
"Dano material e moral",
],
required: true,
},
{
id: "origem",
text: "Qual a origem do dano?",
type: "select",
options: [
"Acidente de trânsito",
"Erro médico",
"Relação de consumo",
"Relação contratual",
"Ato ilícito",
"Outro",
],
required: true,
},
{
id: "data_fato",
text: "Data do fato",
type: "date",
required: true,
},
],
},
{
title: "Partes",
questions: [
{
id: "tipo_responsavel",
text: "O responsável é pessoa física, jurídica ou poder público?",
type: "select",
options: ["PF", "PJ", "Poder Público"],
required: true,
},
{
id: "relacao_com_responsavel",
text: "Qual a relação com o responsável?",
type: "select",
options: [
"Consumidor",
"Contratante",
"Terceiro",
"Paciente",
"Outro",
],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Detalhes e Provas",
requiresLlm: true,
groups: [
{
title: "Extensão do Dano",
questions: [
{
id: "descricao_dano",
text: "Descrição do dano sofrido",
type: "text",
placeholder: "Descreva brevemente o dano sofrido",
required: true,
},
{
id: "valor_estimado",
text: "Valor estimado da indenização",
type: "select",
options: [
"Até R$ 10 mil",
"R$ 10 a 50 mil",
"R$ 50 a 100 mil",
"Acima de R$ 100 mil",
],
required: true,
},
{
id: "consequencias",
text: "Quais as consequências do dano?",
type: "multiselect",
options: [
"Gastos médicos",
"Perda de renda",
"Dano psicológico",
"Dano estético",
"Perda de bem material",
"Outro",
],
required: true,
},
],
},
{
title: "Provas",
questions: [
{
id: "provas",
text: "Quais provas possui?",
type: "multiselect",
options: [
"Boletim de ocorrência",
"Laudos médicos",
"Fotos",
"Vídeos",
"Testemunhas",
"Notas fiscais",
"Orçamentos",
"Contratos",
"Nenhuma",
],
required: true,
},
{
id: "tentou_acordo",
text: "Tentou acordo extrajudicial?",
type: "select",
options: ["Sim, sem sucesso", "Não"],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial de ação de indenização por responsabilidade civil com os seguintes dados:
**Partes:**
- Autor: [a ser preenchido pelo advogado]
- Réu: {{tipo_responsavel}}
- Relação: {{relacao_com_responsavel}}
**Fato gerador:**
- Origem: {{origem}}
- Data do fato: {{data_fato}}
- Tipo de indenização: {{tipo_indenizacao}}
**Extensão do dano:**
- Descrição: {{descricao_dano}}
- Valor estimado: {{valor_estimado}}
- Consequências: {{consequencias}}
**Provas disponíveis:**
- Provas: {{provas}}
- Tentativa de acordo: {{tentou_acordo}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 186 do CC (ato ilícito)
- Art. 927 do CC (obrigação de reparar o dano)
- Art. 944 do CC (indenização mede-se pela extensão do dano)
- Art. 14 do CDC (responsabilidade do fornecedor)
Inclua pedidos de: indenização por danos materiais e/ou morais, juros de mora, correção monetária, custas processuais e honorários advocatícios.`,
legalReferences: [
"art. 186 CC",
"art. 927 CC",
"art. 944 CC",
"art. 14 CDC",
],
};

View File

@ -2,10 +2,10 @@ export const civelArea = {
value: "civel",
label: "Cível",
subtipos: [
{ value: "cobranca", label: "Cobrança", available: false },
{ value: "indenizacao", label: "Indenização", available: false },
{ value: "obrigacao-fazer", label: "Obrigação de Fazer", available: false },
{ value: "contestacao", label: "Contestação Cível", available: false },
{ value: "contrato", label: "Contrato (Revisão)", available: false },
{ value: "cobranca", label: "Cobrança" },
{ value: "indenizacao", label: "Indenização" },
{ value: "obrigacao-fazer", label: "Obrigação de Fazer" },
{ value: "contestacao", label: "Contestação Cível" },
{ value: "contrato", label: "Contrato (Revisão)" },
],
};

View File

@ -0,0 +1,182 @@
import type { FlowConfig } from "../types.js";
export const obrigacaoFazerFlow: FlowConfig = {
area: "civel",
areaLabel: "Cível",
subtipo: "obrigacao-fazer",
subtipoLabel: "Obrigação de Fazer",
tipoTarefa: "peticao-inicial",
steps: [
{
stepNumber: 1,
title: "Dados da Obrigação",
requiresLlm: false,
groups: [
{
title: "Origem",
questions: [
{
id: "origem_obrigacao",
text: "Qual a origem da obrigação?",
type: "select",
options: [
"Contrato",
"Decisão judicial anterior",
"Lei ou regulamento",
"Relação de consumo",
"Outro",
],
required: true,
},
{
id: "tipo_obrigante",
text: "Quem deveria cumprir a obrigação?",
type: "select",
options: ["PF", "PJ", "Poder Público"],
required: true,
},
],
},
{
title: "Obrigação",
questions: [
{
id: "descricao_obrigacao",
text: "Descrição da obrigação",
type: "text",
placeholder: "O que deveria ser feito ou entregue?",
required: true,
},
{
id: "prazo_original",
text: "Prazo original para cumprimento",
type: "date",
required: true,
},
{
id: "urgencia",
text: "Qual o grau de urgência?",
type: "select",
options: [
"Urgente - risco de dano irreparável",
"Moderada",
"Baixa - pode aguardar rito normal",
],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Detalhes",
requiresLlm: true,
groups: [
{
title: "Inadimplência",
questions: [
{
id: "notificou",
text: "Notificou o obrigado?",
type: "select",
options: [
"Sim, com AR",
"Sim, por e-mail",
"Sim, verbal",
"Não",
],
required: true,
},
{
id: "motivo_recusa",
text: "Qual o motivo da recusa ou inadimplência?",
type: "select",
options: [
"Alega impossibilidade",
"Ignora pedidos",
"Contesta obrigação",
"Desconhecido",
],
required: true,
},
{
id: "prejuizo",
text: "Qual o prejuízo causado?",
type: "text",
placeholder: "Qual o prejuízo pela não realização?",
required: true,
},
],
},
{
title: "Provas",
questions: [
{
id: "documentos",
text: "Quais documentos possui?",
type: "multiselect",
options: [
"Contrato",
"Notificação",
"E-mails",
"Mensagens",
"Protocolo de atendimento",
"Fotos",
"Nenhum",
],
required: true,
},
{
id: "pedido_tutela",
text: "Deseja pedir tutela de urgência?",
type: "select",
options: [
"Sim, com tutela de urgência",
"Não, apenas rito normal",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial de ação de obrigação de fazer com os seguintes dados:
**Partes:**
- Autor: [a ser preenchido pelo advogado]
- Réu: {{tipo_obrigante}}
**Obrigação:**
- Origem: {{origem_obrigacao}}
- Descrição: {{descricao_obrigacao}}
- Prazo original: {{prazo_original}}
- Urgência: {{urgencia}}
**Inadimplência:**
- Notificação: {{notificou}}
- Motivo da recusa: {{motivo_recusa}}
- Prejuízo: {{prejuizo}}
**Provas e pedidos:**
- Documentos: {{documentos}}
- Tutela de urgência: {{pedido_tutela}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 497 do CPC (tutela específica das obrigações de fazer)
- Art. 536 do CPC (cumprimento de sentença de obrigação de fazer)
- Art. 537 do CPC (multa periódica - astreintes)
- Art. 300 do CPC (tutela de urgência)
Inclua pedidos de: cumprimento da obrigação de fazer, fixação de astreintes em caso de descumprimento, tutela de urgência (se aplicável), custas processuais e honorários advocatícios.`,
legalReferences: [
"art. 497 CPC",
"art. 536 CPC",
"art. 537 CPC",
"art. 300 CPC",
],
};

View File

@ -0,0 +1,187 @@
import type { FlowConfig } from "../types.js";
export const acumuloFuncaoFlow: FlowConfig = {
area: "trabalhista",
areaLabel: "Trabalhista",
subtipo: "acumulo-funcao",
subtipoLabel: "Acúmulo de Função",
tipoTarefa: "peticao-inicial",
steps: [
{
stepNumber: 1,
title: "Dados do Vínculo",
requiresLlm: false,
groups: [
{
title: "Contrato",
questions: [
{
id: "cargo_registrado",
text: "Qual o cargo registrado em carteira?",
type: "text",
placeholder: "Ex: Auxiliar Administrativo",
required: true,
},
{
id: "cargo_exercido",
text: "Qual o cargo efetivamente exercido?",
type: "text",
placeholder: "Ex: Auxiliar Administrativo + Financeiro",
required: true,
},
{
id: "regime",
text: "Qual o regime de trabalho?",
type: "select",
options: ["CLT", "PJ", "Autônomo", "Temporário"],
required: true,
},
{
id: "tempo_servico",
text: "Quanto tempo de serviço?",
type: "select",
options: [
"Menos de 1 ano",
"1 a 3 anos",
"3 a 5 anos",
"Mais de 5 anos",
],
required: true,
},
],
},
{
title: "Remuneração",
questions: [
{
id: "faixa_salarial",
text: "Qual a faixa salarial?",
type: "select",
options: [
"Até 2 SM",
"2 a 5 SM",
"5 a 10 SM",
"Acima de 10 SM",
],
required: true,
},
{
id: "recebeu_adicional",
text: "Recebeu algum adicional pelo acúmulo?",
type: "select",
options: ["Sim", "Não"],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Detalhes do Acúmulo",
requiresLlm: true,
groups: [
{
title: "Funções Acumuladas",
questions: [
{
id: "funcoes_extras",
text: "Descreva as funções exercidas além do cargo contratado",
type: "text",
placeholder: "Descreva as funções exercidas além do cargo contratado",
required: true,
},
{
id: "inicio_acumulo",
text: "Quando começou o acúmulo de função?",
type: "select",
options: [
"Desde a contratação",
"Após promoção",
"Após saída de colega",
"Após reestruturação",
],
required: true,
},
{
id: "frequencia_acumulo",
text: "Com que frequência exerce as funções extras?",
type: "select",
options: [
"Diariamente",
"Várias vezes por semana",
"Eventualmente",
],
required: true,
},
],
},
{
title: "Provas",
questions: [
{
id: "provas_disponiveis",
text: "Quais provas estão disponíveis?",
type: "multiselect",
options: [
"E-mails com atribuições",
"Testemunhas",
"Descrição de cargo",
"Contracheques",
"Prints de sistemas",
"Nenhuma",
],
required: true,
},
{
id: "outros_exercem",
text: "Há outro funcionário específico para a função acumulada?",
type: "select",
options: [
"Sim, há funcionário específico",
"Sim, havia antes",
"Não sei",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial trabalhista de adicional por acúmulo de função com os seguintes dados:
**Partes:**
- Reclamante: [a ser preenchido pelo advogado]
- Reclamada: [a ser preenchido pelo advogado]
**Vínculo empregatício:**
- Cargo registrado: {{cargo_registrado}}
- Cargo efetivamente exercido: {{cargo_exercido}}
- Regime: {{regime}}
- Tempo de serviço: {{tempo_servico}}
- Faixa salarial: {{faixa_salarial}}
- Recebeu adicional: {{recebeu_adicional}}
**Detalhes do acúmulo:**
- Funções extras exercidas: {{funcoes_extras}}
- Início do acúmulo: {{inicio_acumulo}}
- Frequência: {{frequencia_acumulo}}
**Provas:**
- Provas disponíveis: {{provas_disponiveis}}
- Existência de funcionário específico para a função: {{outros_exercem}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 456, parágrafo único da CLT (condição do contrato de trabalho)
- Art. 468 da CLT (alteração contratual lesiva)
Inclua pedidos de: pagamento de adicional por acúmulo de função com reflexos em férias + 1/3, 13º salário, FGTS, DSR, e honorários advocatícios.`,
legalReferences: [
"art. 456, parágrafo único CLT",
"art. 468 CLT",
],
};

View File

@ -0,0 +1,181 @@
import type { FlowConfig } from "../types.js";
export const contestacaoTrabalhistaFlow: FlowConfig = {
area: "trabalhista",
areaLabel: "Trabalhista",
subtipo: "contestacao",
subtipoLabel: "Contestação Trabalhista",
tipoTarefa: "contestacao",
steps: [
{
stepNumber: 1,
title: "Dados do Processo",
requiresLlm: false,
groups: [
{
title: "Processo",
questions: [
{
id: "numero_processo",
text: "Qual o número do processo?",
type: "text",
placeholder: "0000000-00.0000.0.00.0000",
required: true,
},
{
id: "vara_tribunal",
text: "Qual a vara e tribunal?",
type: "text",
placeholder: "Ex: 1ª Vara do Trabalho de São Paulo",
required: true,
},
{
id: "tipo_acao",
text: "Qual o tipo de ação?",
type: "select",
options: [
"Reclamatória trabalhista",
"Ação de consignação",
"Inquérito para apuração de falta grave",
"Outra",
],
required: true,
},
],
},
{
title: "Partes",
questions: [
{
id: "porte_empresa",
text: "Qual o porte da empresa reclamada?",
type: "select",
options: [
"Microempresa",
"Pequena empresa",
"Média empresa",
"Grande empresa",
],
required: true,
},
{
id: "segmento",
text: "Qual o segmento da empresa?",
type: "text",
placeholder: "Ex: Comércio, Indústria, Serviços",
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Pedidos do Reclamante",
requiresLlm: true,
groups: [
{
title: "Pedidos a Contestar",
questions: [
{
id: "pedidos_principais",
text: "Quais os principais pedidos do reclamante?",
type: "multiselect",
options: [
"Horas extras",
"Verbas rescisórias",
"Dano moral",
"Vínculo empregatício",
"Equiparação salarial",
"Adicional de insalubridade",
"Desvio de função",
"Outro",
],
required: true,
},
{
id: "valor_causa",
text: "Qual o valor da causa?",
type: "select",
options: [
"Até R$ 20 mil",
"R$ 20 a 50 mil",
"R$ 50 a 100 mil",
"Acima de R$ 100 mil",
],
required: true,
},
],
},
{
title: "Defesa",
questions: [
{
id: "teses_defesa",
text: "Quais teses de defesa serão utilizadas?",
type: "multiselect",
options: [
"Prescrição",
"Ausência de provas",
"Acordo coletivo válido",
"Justa causa comprovada",
"Inexistência de vínculo",
"Pagamento correto",
"Culpa do reclamante",
],
required: true,
},
{
id: "documentos_empresa",
text: "Quais documentos a empresa possui?",
type: "multiselect",
options: [
"Controle de ponto",
"Contracheques",
"Contrato de trabalho",
"TRCT",
"Acordo coletivo",
"Regulamento interno",
"Nenhum",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma contestação trabalhista com os seguintes dados:
**Processo:**
- Número: {{numero_processo}}
- Vara/Tribunal: {{vara_tribunal}}
- Tipo de ação: {{tipo_acao}}
**Reclamada:**
- Porte: {{porte_empresa}}
- Segmento: {{segmento}}
**Pedidos do reclamante a contestar:**
- Pedidos principais: {{pedidos_principais}}
- Valor da causa: {{valor_causa}}
**Teses de defesa:**
- Teses: {{teses_defesa}}
- Documentos disponíveis: {{documentos_empresa}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 818 da CLT (ônus da prova)
- Art. 373 do CPC (distribuição do ônus da prova)
- Art. 769 da CLT (aplicação subsidiária do CPC)
Inclua preliminares pertinentes, conteste cada pedido individualmente com fundamentos fáticos e jurídicos, e formule pedidos finais de improcedência total dos pedidos do reclamante, com condenação em honorários advocatícios sucumbenciais.`,
legalReferences: [
"art. 818 CLT",
"art. 373 CPC",
"art. 769 CLT",
],
};

View File

@ -0,0 +1,202 @@
import type { FlowConfig } from "../types.js";
export const danoMoralFlow: FlowConfig = {
area: "trabalhista",
areaLabel: "Trabalhista",
subtipo: "dano-moral",
subtipoLabel: "Dano Moral",
tipoTarefa: "peticao-inicial",
steps: [
{
stepNumber: 1,
title: "Dados do Vínculo",
requiresLlm: false,
groups: [
{
title: "Relação de Trabalho",
questions: [
{
id: "empregador_tipo",
text: "O empregador é pessoa jurídica ou física?",
type: "select",
options: ["Pessoa Jurídica (empresa)", "Pessoa Física"],
required: true,
},
{
id: "regime",
text: "Qual o regime de trabalho?",
type: "select",
options: ["CLT", "PJ", "Autônomo", "Temporário"],
required: true,
},
{
id: "cargo",
text: "Qual o cargo exercido?",
type: "text",
required: true,
},
{
id: "tempo_servico",
text: "Quanto tempo de serviço?",
type: "select",
options: [
"Menos de 1 ano",
"1 a 3 anos",
"3 a 5 anos",
"Mais de 5 anos",
],
required: true,
},
],
},
{
title: "Remuneração",
questions: [
{
id: "faixa_salarial",
text: "Qual a faixa salarial?",
type: "select",
options: [
"Até 2 SM",
"2 a 5 SM",
"5 a 10 SM",
"Acima de 10 SM",
],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Detalhes do Dano",
requiresLlm: true,
groups: [
{
title: "Caracterização do Dano",
questions: [
{
id: "tipo_dano",
text: "Qual o tipo de dano sofrido?",
type: "select",
options: [
"Assédio moral",
"Assédio sexual",
"Discriminação",
"Acidente de trabalho",
"Exposição indevida",
"Revista íntima",
"Outro",
],
required: true,
},
{
id: "frequencia",
text: "Qual a frequência das ocorrências?",
type: "select",
options: [
"Episódio único",
"Recorrente",
"Sistemático",
],
required: true,
},
{
id: "periodo_ocorrencias",
text: "Por quanto tempo ocorreram os fatos?",
type: "select",
options: [
"Menos de 3 meses",
"3 a 6 meses",
"6 a 12 meses",
"Mais de 1 ano",
],
required: true,
},
],
},
{
title: "Impacto e Provas",
questions: [
{
id: "consequencias",
text: "Quais consequências o dano causou?",
type: "multiselect",
options: [
"Afastamento médico",
"Tratamento psicológico",
"Perda salarial",
"Danos à reputação",
"Nenhuma consequência formal",
],
required: true,
},
{
id: "provas_disponiveis",
text: "Quais provas estão disponíveis?",
type: "multiselect",
options: [
"Mensagens",
"E-mails",
"Testemunhas",
"Laudos médicos",
"Câmeras",
"Documentos",
"Nenhuma",
],
required: true,
},
{
id: "registrou_ocorrencia",
text: "Registrou a ocorrência formalmente?",
type: "select",
options: [
"Sim, BO",
"Sim, RH ou ouvidoria",
"Não",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial trabalhista de indenização por dano moral com os seguintes dados:
**Partes:**
- Reclamante: [a ser preenchido pelo advogado]
- Reclamada: {{empregador_tipo}}
**Vínculo empregatício:**
- Regime: {{regime}}
- Cargo: {{cargo}}
- Tempo de serviço: {{tempo_servico}}
- Faixa salarial: {{faixa_salarial}}
**Caracterização do dano:**
- Tipo de dano: {{tipo_dano}}
- Frequência: {{frequencia}}
- Período das ocorrências: {{periodo_ocorrencias}}
**Impacto e provas:**
- Consequências: {{consequencias}}
- Provas disponíveis: {{provas_disponiveis}}
- Registro de ocorrência: {{registrou_ocorrencia}}
{{#refinement_context}}
**Fundamente com base em:**
- Arts. 223-A a 223-G da CLT (dano extrapatrimonial nas relações de trabalho)
- Art. 186 do Código Civil (ato ilícito)
- Art. 927 do Código Civil (obrigação de reparar o dano)
Inclua pedidos de: indenização por dano moral com arbitramento judicial do valor, considerando a gravidade da ofensa, a condição econômica das partes e o caráter pedagógico, além de honorários advocatícios.`,
legalReferences: [
"art. 223-A a 223-G CLT",
"art. 186 CC",
"art. 927 CC",
],
};

View File

@ -3,9 +3,9 @@ export const trabalhistaArea = {
label: "Trabalhista",
subtipos: [
{ value: "horas-extras", label: "Horas Extras" },
{ value: "rescisao-indireta", label: "Rescisão Indireta", available: false },
{ value: "dano-moral", label: "Dano Moral", available: false },
{ value: "acumulo-funcao", label: "Acúmulo de Função", available: false },
{ value: "contestacao", label: "Contestação Trabalhista", available: false },
{ value: "rescisao-indireta", label: "Rescisão Indireta" },
{ value: "dano-moral", label: "Dano Moral" },
{ value: "acumulo-funcao", label: "Acúmulo de Função" },
{ value: "contestacao", label: "Contestação Trabalhista" },
],
};

View File

@ -0,0 +1,192 @@
import type { FlowConfig } from "../types.js";
export const rescisaoIndiretaFlow: FlowConfig = {
area: "trabalhista",
areaLabel: "Trabalhista",
subtipo: "rescisao-indireta",
subtipoLabel: "Rescisão Indireta",
tipoTarefa: "peticao-inicial",
steps: [
{
stepNumber: 1,
title: "Dados do Vínculo",
requiresLlm: false,
groups: [
{
title: "Relação de Trabalho",
questions: [
{
id: "regime",
text: "Qual o regime de trabalho?",
type: "select",
options: ["CLT", "PJ", "Autônomo", "Temporário"],
required: true,
},
{
id: "cargo",
text: "Qual o cargo exercido?",
type: "text",
required: true,
},
{
id: "tempo_servico",
text: "Quanto tempo de serviço?",
type: "select",
options: [
"Menos de 1 ano",
"1 a 3 anos",
"3 a 5 anos",
"Mais de 5 anos",
],
required: true,
},
],
},
{
title: "Remuneração",
questions: [
{
id: "salario_tipo",
text: "Qual o tipo de salário?",
type: "select",
options: ["Fixo", "Comissão", "Misto"],
required: true,
},
{
id: "faixa_salarial",
text: "Qual a faixa salarial?",
type: "select",
options: [
"Até 2 SM",
"2 a 5 SM",
"5 a 10 SM",
"Acima de 10 SM",
],
required: true,
},
],
},
],
},
{
stepNumber: 2,
title: "Motivos da Rescisão",
requiresLlm: true,
groups: [
{
title: "Falta Grave do Empregador",
questions: [
{
id: "motivo_principal",
text: "Qual o motivo principal da rescisão indireta?",
type: "select",
options: [
"Atraso reiterado de salários",
"Não recolhimento de FGTS",
"Assédio moral",
"Desvio de função",
"Redução salarial",
"Condições de trabalho inadequadas",
],
required: true,
},
{
id: "motivos_adicionais",
text: "Há motivos adicionais?",
type: "multiselect",
options: [
"Atraso reiterado de salários",
"Não recolhimento de FGTS",
"Assédio moral",
"Desvio de função",
"Redução salarial",
"Condições de trabalho inadequadas",
],
required: false,
},
{
id: "inicio_problemas",
text: "Há quanto tempo os problemas começaram?",
type: "select",
options: [
"Menos de 3 meses",
"3 a 6 meses",
"6 a 12 meses",
"Mais de 1 ano",
],
required: true,
},
],
},
{
title: "Evidências",
questions: [
{
id: "provas_disponiveis",
text: "Quais provas estão disponíveis?",
type: "multiselect",
options: [
"Contracheques",
"Extratos FGTS",
"Mensagens",
"Testemunhas",
"E-mails",
"Fotos ou vídeos",
"Documentos médicos",
"Nenhuma",
],
required: true,
},
{
id: "comunicou_empregador",
text: "Comunicou o empregador sobre os problemas?",
type: "select",
options: [
"Sim, formalmente",
"Sim, verbalmente",
"Não",
],
required: true,
},
],
},
],
},
],
promptTemplate: `Elabore uma petição inicial trabalhista de rescisão indireta do contrato de trabalho com os seguintes dados:
**Partes:**
- Reclamante: [a ser preenchido pelo advogado]
- Reclamada: [a ser preenchido pelo advogado]
**Vínculo empregatício:**
- Regime: {{regime}}
- Cargo: {{cargo}}
- Tempo de serviço: {{tempo_servico}}
- Tipo de salário: {{salario_tipo}}
- Faixa salarial: {{faixa_salarial}}
**Motivos da rescisão indireta:**
- Motivo principal: {{motivo_principal}}
- Motivos adicionais: {{motivos_adicionais}}
- Início dos problemas: {{inicio_problemas}}
**Evidências:**
- Provas disponíveis: {{provas_disponiveis}}
- Comunicação ao empregador: {{comunicou_empregador}}
{{#refinement_context}}
**Fundamente com base em:**
- Art. 483 da CLT (hipóteses de rescisão indireta)
- Art. 487 da CLT (aviso prévio)
- Súmula 13 do TST (mora salarial)
Inclua pedidos de: reconhecimento da rescisão indireta, pagamento de verbas rescisórias como dispensa sem justa causa (saldo de salário, aviso prévio, 13º proporcional, férias + 1/3, FGTS + 40%), guias para seguro-desemprego, e honorários advocatícios.`,
legalReferences: [
"art. 483 CLT",
"art. 487 CLT",
"Súmula 13 TST",
],
};

View File

@ -0,0 +1,70 @@
import { describe, it, expect } from "vitest";
import { calculateTotalSteps, getVisualStep, isStepComplete } from "../flow-engine.js";
import type { FlowConfig, FlowStep } from "../../flows/types.js";
const mockFlow: FlowConfig = {
area: "trabalhista",
areaLabel: "Trabalhista",
subtipo: "horas-extras",
subtipoLabel: "Horas Extras",
tipoTarefa: "peticao-inicial",
steps: [
{ stepNumber: 1, title: "Step 1", groups: [], requiresLlm: false },
{ stepNumber: 2, title: "Step 2", groups: [], requiresLlm: true },
],
promptTemplate: "",
legalReferences: [],
};
describe("calculateTotalSteps", () => {
it("includes selection screens when no deep link", () => {
expect(calculateTotalSteps(mockFlow, false)).toBe(5); // 2 selection + 2 flow + 1 preview
});
it("excludes selection screens with deep link", () => {
expect(calculateTotalSteps(mockFlow, true)).toBe(3); // 0 selection + 2 flow + 1 preview
});
});
describe("getVisualStep", () => {
it("adds offset for non-deep-link", () => {
expect(getVisualStep(mockFlow, 1, false)).toBe(3); // offset 2 + step 1
});
it("no offset for deep link", () => {
expect(getVisualStep(mockFlow, 1, true)).toBe(1);
});
});
describe("isStepComplete", () => {
const step: FlowStep = {
stepNumber: 1,
title: "Test",
requiresLlm: false,
groups: [
{
title: "Group 1",
questions: [
{ id: "q1", text: "Question 1", type: "select", options: ["A", "B"], required: true },
{ id: "q2", text: "Question 2", type: "text", required: false },
],
},
],
};
it("returns true when all required questions answered", () => {
expect(isStepComplete(step, { q1: "A" })).toBe(true);
});
it("returns false when required question missing", () => {
expect(isStepComplete(step, {})).toBe(false);
});
it("returns false when required answer is empty", () => {
expect(isStepComplete(step, { q1: "" })).toBe(false);
});
it("returns true when optional question missing", () => {
expect(isStepComplete(step, { q1: "A" })).toBe(true);
});
});

View File

@ -0,0 +1,29 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getRefinementQuestions } from "../llm-client.js";
import type { FlowState } from "../../flows/types.js";
const mockState: FlowState = {
area: "trabalhista",
subtipo: "horas-extras",
tipoTarefa: "peticao-inicial",
currentStep: 1,
totalSteps: 5,
responses: { regime: "CLT" },
};
describe("getRefinementQuestions", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("returns empty array when no API key configured", async () => {
const result = await getRefinementQuestions(mockState, "Trabalhista", "Horas Extras");
expect(result).toEqual([]);
});
it("returns empty array on fetch failure (graceful fallback)", async () => {
// Even if somehow config had a key, network failure should return []
const result = await getRefinementQuestions(mockState, "Trabalhista", "Horas Extras");
expect(Array.isArray(result)).toBe(true);
});
});

View File

@ -0,0 +1,86 @@
import { describe, it, expect } from "vitest";
import { buildPrompt } from "../prompt-builder.js";
import type { FlowConfig, FlowState } from "../../flows/types.js";
const mockFlow: FlowConfig = {
area: "trabalhista",
areaLabel: "Trabalhista",
subtipo: "horas-extras",
subtipoLabel: "Horas Extras",
tipoTarefa: "peticao-inicial",
steps: [],
promptTemplate: "Regime: {{regime}}, Jornada: {{jornada_contratual}}, Extra: {{horas_extras_semana}}",
legalReferences: ["art. 59 CLT"],
};
const mockState: FlowState = {
area: "trabalhista",
subtipo: "horas-extras",
tipoTarefa: "peticao-inicial",
currentStep: 2,
totalSteps: 5,
responses: {
regime: "CLT",
jornada_contratual: "44h semanais",
horas_extras_semana: "5 a 10 horas",
},
};
describe("buildPrompt", () => {
it("interpolates template variables", () => {
const result = buildPrompt(mockFlow, mockState);
expect(result.text).toContain("CLT");
expect(result.text).toContain("44h semanais");
expect(result.text).toContain("5 a 10 horas");
expect(result.text).not.toContain("{{");
});
it("returns legal references", () => {
const result = buildPrompt(mockFlow, mockState);
expect(result.legalReferences).toEqual(["art. 59 CLT"]);
});
it("calculates char count", () => {
const result = buildPrompt(mockFlow, mockState);
expect(result.charCount).toBe(result.text.length);
});
it("determines fitsInUrl correctly for short prompt", () => {
const result = buildPrompt(mockFlow, mockState);
expect(result.fitsInUrl).toBe(true);
expect(result.encodedUrl).toBeDefined();
});
it("determines fitsInUrl correctly for long prompt", () => {
const longFlow = {
...mockFlow,
promptTemplate: "A".repeat(2000),
};
const result = buildPrompt(longFlow, mockState);
expect(result.fitsInUrl).toBe(false);
expect(result.encodedUrl).toBeUndefined();
});
it("removes unreplaced variables", () => {
const flowWithExtra = {
...mockFlow,
promptTemplate: "{{regime}} - {{campo_inexistente}}",
};
const result = buildPrompt(flowWithExtra, mockState);
expect(result.text).not.toContain("{{campo_inexistente}}");
expect(result.text).toContain("CLT");
});
it("handles array responses by joining with comma", () => {
const stateWithArray = {
...mockState,
responses: { ...mockState.responses, provas: ["Doc1", "Doc2"] },
};
const flowWithArray = {
...mockFlow,
promptTemplate: "Provas: {{provas}}",
};
const result = buildPrompt(flowWithArray, stateWithArray);
expect(result.text).toContain("Doc1, Doc2");
});
});

View File

@ -0,0 +1,22 @@
import { describe, it, expect } from "vitest";
import { buildRedirectUrl, getJusIaDirectUrl } from "../url-builder.js";
describe("buildRedirectUrl", () => {
it("builds correct URL with encoded prompt", () => {
const url = buildRedirectUrl("teste prompt");
expect(url).toContain("https://ia.jusbrasil.com.br/conversa?q=");
expect(url).toContain("teste%20prompt");
expect(url.endsWith("&send")).toBe(true);
});
it("encodes special characters", () => {
const url = buildRedirectUrl("art. 59 da CLT (limite)");
expect(url).toContain(encodeURIComponent("art. 59 da CLT (limite)"));
});
});
describe("getJusIaDirectUrl", () => {
it("returns base Jus IA URL", () => {
expect(getJusIaDirectUrl()).toBe("https://ia.jusbrasil.com.br/conversa");
});
});

View File

@ -67,13 +67,26 @@ Gere perguntas de refinamento para capturar nuances deste caso.`;
signal: controller.signal,
});
} else {
// Default: OpenAI-compatible
response = await fetch("https://api.openai.com/v1/chat/completions", {
// OpenRouter (default) and OpenAI use the same OpenAI-compatible API
const baseUrl =
config.llm.provider === "openrouter"
? config.llm.baseUrl
: "https://api.openai.com/v1";
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${config.llm.apiKey}`,
};
// OpenRouter-specific headers
if (config.llm.provider === "openrouter") {
headers["HTTP-Referer"] = "https://jus-ia-start-kit.app";
headers["X-Title"] = "Jus IA Start Kit";
}
response = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.llm.apiKey}`,
},
headers,
body: JSON.stringify({
model: config.llm.model,
messages: [
@ -99,6 +112,7 @@ Gere perguntas de refinamento para capturar nuances deste caso.`;
if (config.llm.provider === "anthropic") {
content = data.content?.[0]?.text || "{}";
} else {
// OpenRouter and OpenAI both use the same response format
content = data.choices?.[0]?.message?.content || "{}";
}

View File

@ -0,0 +1,93 @@
import { describe, it, expect } from "vitest";
import { parseFlowState, serializeFlowState } from "../parse-flow-state.js";
describe("parseFlowState", () => {
it("parses basic form body", () => {
const state = parseFlowState({
_area: "trabalhista",
_subtipo: "horas-extras",
_tipo_tarefa: "peticao-inicial",
_step: "1",
_total_steps: "5",
_responses: "{}",
regime: "CLT",
});
expect(state.area).toBe("trabalhista");
expect(state.subtipo).toBe("horas-extras");
expect(state.currentStep).toBe(1);
expect(state.responses.regime).toBe("CLT");
});
it("merges existing responses with new ones", () => {
const state = parseFlowState({
_area: "trabalhista",
_subtipo: "horas-extras",
_step: "2",
_total_steps: "5",
_responses: JSON.stringify({ regime: "CLT", jornada: "44h" }),
testemunhas: "Sim",
});
expect(state.responses.regime).toBe("CLT");
expect(state.responses.jornada).toBe("44h");
expect(state.responses.testemunhas).toBe("Sim");
});
it("handles invalid JSON in _responses", () => {
const state = parseFlowState({
_area: "civel",
_subtipo: "cobranca",
_step: "1",
_total_steps: "4",
_responses: "invalid-json",
campo: "valor",
});
expect(state.responses.campo).toBe("valor");
});
it("handles empty body gracefully", () => {
const state = parseFlowState({});
expect(state.area).toBe("");
expect(state.currentStep).toBe(1);
expect(Object.keys(state.responses)).toHaveLength(0);
});
it("sanitizes input values", () => {
const state = parseFlowState({
_area: "trabalhista",
_subtipo: "test",
_step: "1",
_total_steps: "3",
_responses: "{}",
campo: "<script>alert('xss')</script>Hello",
});
expect(state.responses.campo).toBe("alert('xss')Hello");
});
it("handles array values", () => {
const state = parseFlowState({
_area: "trabalhista",
_subtipo: "test",
_step: "1",
_total_steps: "3",
_responses: "{}",
provas: ["Doc1", "Doc2"],
});
expect(state.responses.provas).toEqual(["Doc1", "Doc2"]);
});
});
describe("serializeFlowState", () => {
it("serializes responses to JSON", () => {
const json = serializeFlowState({
area: "trabalhista",
subtipo: "horas-extras",
tipoTarefa: "peticao-inicial",
currentStep: 1,
totalSteps: 5,
responses: { regime: "CLT", jornada: "44h" },
});
const parsed = JSON.parse(json);
expect(parsed.regime).toBe("CLT");
expect(parsed.jornada).toBe("44h");
});
});

View File

@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import { sanitizeText, validateSelect } from "../sanitize.js";
describe("sanitizeText", () => {
it("strips HTML tags", () => {
expect(sanitizeText("<b>bold</b> text")).toBe("bold text");
});
it("trims whitespace", () => {
expect(sanitizeText(" hello ")).toBe("hello");
});
it("caps at MAX_INPUT_LENGTH", () => {
const long = "a".repeat(600);
expect(sanitizeText(long).length).toBe(500);
});
it("handles empty string", () => {
expect(sanitizeText("")).toBe("");
});
it("passes through clean text", () => {
expect(sanitizeText("Texto limpo")).toBe("Texto limpo");
});
it("strips nested HTML", () => {
expect(sanitizeText("<div><p>test</p></div>")).toBe("test");
});
});
describe("validateSelect", () => {
const options = ["CLT", "PJ", "Autônomo"];
it("returns true for valid option", () => {
expect(validateSelect("CLT", options)).toBe(true);
});
it("returns false for invalid option", () => {
expect(validateSelect("Outro", options)).toBe(false);
});
it("returns false for empty string", () => {
expect(validateSelect("", options)).toBe(false);
});
});

View File

@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
},
});