Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fe40649
feat: inbox window + master data settings + contacts backend (camadas…
May 26, 2026
3707811
feat(contacts): refactor /contacts table para 12 colunas pixel-perfec…
May 26, 2026
c047f15
feat(contacts): adicionar merge contacts dialog UI com select primary
May 26, 2026
6188826
feat(contacts): adicionar sidebar com contatos bloqueados no rodape
May 26, 2026
fbe1895
feat(contacts): sidebar fixo com ciclo de vida + contatos bloqueados …
May 26, 2026
83e43b9
feat(contacts): adicionar avatar 28px circular na coluna nome (pixel-…
May 26, 2026
670b9bd
feat(contacts): drawer 'Detalhes do contato' ao clicar na linha (350p…
May 26, 2026
8022329
feat: WhatsApp E2E camada 4 + Dados Mestres recovery parcial
May 27, 2026
66b2ea5
feat(contacts): adicionar campos assigneeUserId/tagIds/customFields/a…
May 27, 2026
feda586
feat(sidebar): reordenar nav principal — contatos antes de ai/flows
May 27, 2026
c6fee31
feat(contacts): sidebar contatos espelha inbox (caixas + times + life…
May 27, 2026
d719bd9
feat(contacts): drawer novo contato pixel-perfect com tags + custom f…
May 27, 2026
d0c9b4e
feat(contacts): filtrar lista por caixas/times via lookup cross-table…
May 27, 2026
9aa1dd1
feat(contacts): popular chatStore ao clicar na linha + ContactNameCel…
May 27, 2026
d8b75e7
feat(manage): restaurar item Integrações no sidebar /manage
May 27, 2026
f16e367
feat(account): activity status interativo (Online/Busy/Offline) + fix…
May 27, 2026
e9cfd01
refactor(settings): renomear sidebar group rolesAndTeams → workspace
May 27, 2026
935a031
feat(c2): quick wins Tags+Lifecycle paridade Respond.io (gaps #13+#14)
May 27, 2026
5a3acda
feat(c2): custom fields tipos list+url+time + visibility 3-state (gap…
May 27, 2026
fa1c5e4
feat(c2): custom fields UI tipos novos + visibility 3-state (gap #10 …
May 27, 2026
09fa0eb
feat(c2): custom fields drawer Inbox render por tipo + hideWhenEmpty …
May 27, 2026
32906b1
feat(c2): custom fields slug user-facing + defaults read-only (gap #11)
May 27, 2026
7e10280
feat(c2): snippets schema completo + limite 5000/workspace (gap #12)
May 27, 2026
bcb27d6
feat(c2): closing notes backend — schemas + workspace mode + audit (g…
May 27, 2026
9d21171
feat(c2): closing notes settings UI (gap #15 Fase B)
May 27, 2026
6577a90
feat(closing-notes): modal de fechamento no Inbox respeita modo do wo…
May 27, 2026
19b7dbe
feat(closing-notes): renderiza nota de fechamento no timeline do cont…
May 27, 2026
c5245db
fix(database): closingNotes precisa from/to explícito no Drizzle v2 e…
May 27, 2026
c0f8580
feat(contacts): delete numérico + columns picker (#20.2 + #20.3)
May 27, 2026
074f94f
feat(contacts): unmerge — desfazer fusão de contatos (#17 soft-delete)
May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
724 changes: 713 additions & 11 deletions apps/builder/messages/en.json

Large diffs are not rendered by default.

2,774 changes: 2,774 additions & 0 deletions apps/builder/messages/pt-BR.json

Large diffs are not rendered by default.

711 changes: 700 additions & 11 deletions apps/builder/messages/vi.json

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions apps/builder/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const withNextIntl = createNextIntlPlugin({
const nextConfig: NextConfig = {
reactStrictMode: true,
output: "standalone",
// Desliga o popup "Route/Bundler/Turbopack" do Next.js que aparece em dev.
// Pedro reclamou 2026-05-24 que o popup atrapalha. Não afeta o app.
devIndicators: false,
images: {
// dangerouslyAllowLocalIP: true,
remotePatterns: [
Expand Down Expand Up @@ -48,6 +51,27 @@ const nextConfig: NextConfig = {
},
]
},
async redirects() {
return [
// Camada 2 — Dados Mestres movidos pra /settings/* (Respond.io match).
// Mantém redirects pras URLs antigas pra não quebrar bookmarks.
{
source: "/space/:workspaceId/tags",
destination: "/space/:workspaceId/settings/tags",
permanent: true,
},
{
source: "/space/:workspaceId/custom-fields",
destination: "/space/:workspaceId/settings/contact-fields",
permanent: true,
},
{
source: "/space/:workspaceId/bot-fields",
destination: "/space/:workspaceId/settings/bot-fields",
permanent: true,
},
]
},
headers() {
return [
{
Expand Down
4 changes: 3 additions & 1 deletion apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"dot-prop": "^10.1.0",
"emoji-picker-react": "^4.19.1",
"html-to-text": "^9.0.5",
"iconsax-reactjs": "^0.0.8",
"ky": "^2.0.2",
"libphonenumber-js": "^1.12.42",
"lodash.unset": "^4.18.0",
Expand All @@ -93,7 +94,7 @@
"next": "16.2.4",
"next-intl": "^4.11.0",
"next-safe-action": "^8.5.2",
"next-themes": "^0.4.6",
"next-themes": "0.4.6",
"nuqs": "^2.8.9",
"partysocket": "1.1.18",
"pretty-bytes": "^7.1.0",
Expand Down Expand Up @@ -131,6 +132,7 @@
"@types/react-dom": "19.2.3",
"@types/swagger-ui-react": "^5.18.0",
"babel-plugin-react-compiler": "^1.0.0",
"click-to-react-component": "^1.1.3",
"dotenv-cli": "^11.0.0",
"pg": "^8.20.0",
"pino-pretty": "^13.1.3",
Expand Down
5 changes: 5 additions & 0 deletions apps/builder/public/brand/icon-ai-agent.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/builder/public/brand/icon-flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/builder/public/brand/x1_black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/builder/public/brand/x1_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 89 additions & 0 deletions apps/builder/public/respond-avatars/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Respond.io avatars (156)</title>
<style>
body {
padding: 24px;
font: 13px system-ui;
color: #fff;
background: #0e1116;
}
h1 {
margin: 0 0 4px;
}
.meta {
margin-bottom: 24px;
color: #888;
}
.row {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.label {
width: 200px;
font-family: ui-monospace, monospace;
font-size: 12px;
color: #bbb;
}
.imgs {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.imgs img {
width: 60px;
height: 60px;
background: #1a1d23;
border-radius: 50%;
}
</style>
</head>
<body>
<h1>Respond.io — Galeria de avatares (156)</h1>
<div class="meta">
13 cores × 12 expressões — extraídos de cdn.respond.io
</div>
<div id="grid"></div>
<script>
"use strict"
const colors = [
"red-lighten-2",
"pink-lighten-2",
"purple-lighten-2",
"deep-purple-lighten-2",
"indigo-lighten-2",
"blue-darken-1",
"teal-lighten-2",
"light-green-darken-2",
"lime-darken-2",
"amber-lighten-1",
"orange-lighten-1",
"deep-orange-lighten-1",
"brown-lighten-2",
]
const grid = document.getElementById("grid")
for (const c of colors) {
const row = document.createElement("div")
row.className = "row"
const label = document.createElement("div")
label.className = "label"
label.textContent = c
const imgs = document.createElement("div")
imgs.className = "imgs"
for (let n = 1; n <= 12; n++) {
const img = document.createElement("img")
img.src = `${c}/${n}.webp`
img.alt = `${c}-${n}`
imgs.appendChild(img)
}
row.appendChild(label)
row.appendChild(imgs)
grid.appendChild(row)
}
</script>
</body>
</html>
Binary file added apps/builder/public/respond-icons/iconfont.ttf
Binary file not shown.
Binary file added apps/builder/public/respond-icons/iconfont.woff
Binary file not shown.
Binary file added apps/builder/public/respond-icons/iconfont.woff2
Binary file not shown.
59 changes: 59 additions & 0 deletions apps/builder/src/app/api/audit-logs/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { auditLogService } from "@chatbotx.io/business"
import { NextResponse } from "next/server"
import { assertCurrentUserCanAccessChatbot } from "@/lib/auth/utils"

// Regex top-level (biome lint useTopLevelRegex).
const CSV_NEEDS_QUOTING = /[",\n\r]/
const CSV_QUOTE = /"/g

function escapeCsv(value: string | null | undefined): string {
const s = (value ?? "").toString()
if (CSV_NEEDS_QUOTING.test(s)) {
return `"${s.replace(CSV_QUOTE, '""')}"`
}
return s
}

export async function GET(req: Request) {
const url = new URL(req.url)
const workspaceId = url.searchParams.get("workspaceId")
if (!workspaceId) {
return NextResponse.json({ error: "workspaceId required" }, { status: 400 })
}

await assertCurrentUserCanAccessChatbot(workspaceId)

const userId = url.searchParams.get("userId") || undefined
const action = url.searchParams.get("action") || undefined

const { data } = await auditLogService.listByWorkspaceId({
workspaceId,
userId,
action,
limit: 10_000,
offset: 0,
})

const header = ["Data/Hora", "Ação", "Usuário", "Detalhe"]
const lines = [header.join(",")]

for (const row of data) {
lines.push(
[
escapeCsv(new Date(row.createdAt).toISOString()),
escapeCsv(row.action),
escapeCsv(row.userId ?? "Sistema"),
escapeCsv(row.detail),
].join(","),
)
}

const csv = `${lines.join("\n")}`

return new NextResponse(csv, {
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="audit-logs.csv"`,
},
})
}
2 changes: 1 addition & 1 deletion apps/builder/src/app/api/scalar-ui/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export async function GET() {
<!doctype html>
<html>
<head>
<title>My Client</title>
<title>Meu Cliente</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="https://orpc.dev/icon.svg" />
Expand Down
72 changes: 72 additions & 0 deletions apps/builder/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "@chatbotx.io/ui/styles/default.css";
@import "./respond-icons.css";

/**
* Tailwind CSS official document:
Expand All @@ -9,3 +10,74 @@
* you can always add it with the @source directive.
*/
@source "../../node_modules/@chatbotx.io/ui";

/* Esconder scrollbar visível em TODOS os scrollables — Pedro 2026-05-25:
Respond.io não mostra scrollbar em nenhum painel do inbox. Mantém
funcionalidade de rolar (apenas oculta visualmente). Funciona em
Firefox (scrollbar-width: none), IE/Edge (-ms-overflow-style: none)
e WebKit (Chrome/Safari) via pseudo-element ::-webkit-scrollbar. */
* {
-ms-overflow-style: none;
scrollbar-width: none;
}
*::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}

/* Esconder scrollbar visível em TODOS os scrollables — Pedro 2026-05-25:
Respond.io não mostra scrollbar em nenhum painel do inbox. Mantém
funcionalidade de rolar (apenas oculta visualmente). Funciona em
Firefox (scrollbar-width: none), IE/Edge (-ms-overflow-style: none)
e WebKit (Chrome/Safari) via pseudo-element ::-webkit-scrollbar. */
* {
-ms-overflow-style: none;
scrollbar-width: none;
}
*::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}

/* Scrubber de áudio pixel-perfect Respond.io (Pedro 2026-05-26):
trilha cinza fina (3 px) + thumb branco pequeno (10 px). Substitui
o input[type=range] default do navegador que aparecia azul-system.
Webkit (Chrome/Safari) e Firefox precisam de pseudo-classes
separadas — sem fallback global, então repete a estilização. */
.audio-scrubber {
height: 14px;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
background: transparent;
}
.audio-scrubber::-webkit-slider-runnable-track {
height: 3px;
background: rgba(255, 255, 255, 0.16);
border-radius: 2px;
}
.audio-scrubber::-webkit-slider-thumb {
width: 10px;
height: 10px;
margin-top: -3.5px;
-webkit-appearance: none;
appearance: none;
background: #ffffff;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
.audio-scrubber::-moz-range-track {
height: 3px;
background: rgba(255, 255, 255, 0.16);
border-radius: 2px;
}
.audio-scrubber::-moz-range-thumb {
width: 10px;
height: 10px;
background: #ffffff;
border: none;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const handleCallback = async (
: await workspaceService.create({
data: {
organizationId: organization.id,
name: "New Workspace",
name: "Novo Workspace",
},
organization,
createdBy: userId,
Expand Down
10 changes: 5 additions & 5 deletions apps/builder/src/app/integrations/[...integration]/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const handleWebhook = async (
if (!orgSettings?.[integrationType as keyof OrganizationSettings]) {
logger.debug(`Integration ${integrationType} is not configured`)
return new Response(
JSON.stringify({ message: "Integration is not configured" }),
JSON.stringify({ message: "Integração não configurada" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
Expand All @@ -38,7 +38,7 @@ export const handleWebhook = async (
const integration = integrations[integrationType as IntegrationKey]
if (!integration?.handleRequest) {
return new Response(
JSON.stringify({ message: "Method is not implemented" }),
JSON.stringify({ message: "Método não implementado" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
Expand Down Expand Up @@ -93,22 +93,22 @@ const handleTelegramWebhook = async (req: NextRequest) => {
const botId = req.nextUrl.searchParams.get("botId")
if (!botId) {
return new Response(
JSON.stringify({ message: "Missing botId query param" }),
JSON.stringify({ message: "Parâmetro botId ausente" }),
{ status: 400, headers: { "Content-Type": "application/json" } },
)
}

const integration = integrations.telegram
if (!integration?.handleRequest) {
return new Response(
JSON.stringify({ message: "Method is not implemented" }),
JSON.stringify({ message: "Método não implementado" }),
{ status: 400, headers: { "Content-Type": "application/json" } },
)
}

const integrationTelegram = await findIntegrationTelegramByBotId({ botId })
if (!integrationTelegram) {
return new Response(JSON.stringify({ message: "Bot not found" }), {
return new Response(JSON.stringify({ message: "Bot não encontrado" }), {
status: 404,
headers: { "Content-Type": "application/json" },
})
Expand Down
Loading