diff --git a/apps/builder/messages/en.json b/apps/builder/messages/en.json index cd4079bc6..fe90b0453 100644 --- a/apps/builder/messages/en.json +++ b/apps/builder/messages/en.json @@ -18,6 +18,8 @@ "archive": "Archive", "unarchive": "Unarchive", "archiveConversation": "Archive Conversation", + "closeConversation": "Close", + "openConversation": "Open", "assign": "Assign", "assignConversation": "Assign Conversation", "back": "Back", @@ -109,15 +111,96 @@ "deactivate": "Deactivate", "onFailure": "On failure", "onSkip": "On skip", - "onSuccess": "On success" + "onSuccess": "On success", + "trigger": "Trigger", + "close": "Close", + "remove": "Remove" }, "states": { "success": "Success", "error": "Error", - "skip": "Skip" + "skip": "Skip", + "closed": "Closed", + "blocked": "Blocked" }, "admins": { - "title": "Admins" + "title": "User settings", + "subtitle": "Manage and invite team members to your Workspace.", + "addUser": "Add User", + "searchPlaceholder": "Search User settings", + "revoke": "Revoke", + "edit": "Edit", + "roles": { + "owner": "Owner", + "manager": "Manager", + "agent": "Agent" + }, + "lastSeen": "Last seen", + "inviteDialog": { + "title": "Add User", + "description": "Enter the email address and select an access level to invite a new user to your Workspace.", + "emailLabel": "Email Address", + "emailPlaceholder": "user@company.com", + "accessLevelLabel": "Access Level", + "submit": "Add" + }, + "editDialog": { + "title": "Edit User", + "description": "To edit this user select a new access level and click \"Edit\". The Access Level defines what the user can do on the workspace.", + "submit": "Update" + }, + "revokeDialog": { + "title": "Revoke Access", + "description": "If you revoke this user all contacts assigned to this user will be marked unassigned. Are you sure you want to revoke this user's access?", + "confirm": "Revoke" + }, + "roleDescriptions": { + "owner": "Owners can access all modules within the workspace and have full control of the settings with no restrictions.", + "manager": "Managers have access to all modules within the workspace including Settings. However, they are not able to invite/delete other managers or delete channels.", + "agent": "Agents only have access to messages within the workspace. Optionally, you can limit visibility in Contacts." + }, + "advancedRestrictions": "Advanced Restrictions", + "managerRestrictions": { + "restrictDataExport": { + "label": "Restrict Data Export", + "description": "This user will not be able to export data from the workspace." + }, + "restrictContactDeletion": { + "label": "Restrict Contact Deletion", + "description": "This user will not be able to delete any contacts from the workspace." + }, + "restrictWorkspaceSettings": { + "label": "Restrict Workspace Settings Access", + "description": "This user will not be able to access the workspace settings." + }, + "restrictIntegrationSettings": { + "label": "Restrict Integration Settings Access", + "description": "This user will not be able to access the integration settings." + } + }, + "agentRestrictions": { + "contactVisibility": { + "label": "Restrict Contacts Visibility", + "description": "This user only view contacts assigned to", + "options": { + "all": "anyone", + "team": "anyone in the user's team", + "self": "user" + } + }, + "restrictCalling": { + "label": "Restrict Calls Capabilities", + "description": "This user will not be able to receive or initiate calls." + }, + "restrictWorkflows": { + "label": "Restrict Workflows Button", + "description": "The Workflows button in the Messages module will be disabled for this user. This includes Shortcuts and stopping a Workflow for a Contact." + }, + "maskPhoneAndEmail": { + "label": "Mask Phone Number and Email Address", + "description": "This user will not be able to view all Contacts' phone numbers and email addresses." + } + } }, "assignAdmin": { "assignConversation": "Assign conversation", @@ -215,6 +298,9 @@ "label": "Automated Responses" } }, + "automation": { + "title": "Automation" + }, "billing": { "title": "Billing" }, @@ -314,9 +400,93 @@ "channels": { "title": "Channels" }, + "organization": { + "manage": "Manage Organization" + }, + "personalSettings": { + "title": "Personal settings", + "account": "Account", + "profile": "Profile", + "notifications": "Notification settings", + "activityStatus": { + "sectionLabel": "My status", + "available": "Available", + "busy": "Busy", + "offline": "Offline", + "updateSuccess": "Status updated to {status}", + "updateError": "Failed to update status" + }, + "profileSection": { + "title": "Profile", + "subtitle": "Manage your profile and login details.", + "changeAvatar": "Change photo", + "avatarUpdated": "Photo updated.", + "firstNameLabel": "First Name", + "lastNameLabel": "Last Name", + "languageLabel": "Language", + "changePassword": "Change password", + "saveProfile": "Save Profile", + "savedSuccess": "Profile updated.", + "twoFA": { + "title": "Two-factor authentication (2FA)", + "toggle": "Enable two-factor authentication (2FA)" + }, + "darkMode": { + "title": "Dark mode", + "system": "Use system settings", + "light": "Off", + "dark": "On" + } + }, + "notificationsSection": { + "title": "Notification settings", + "subtitle": "Choose how you receive workspace alerts." + } + }, + "manageWorkspaces": { + "sidebarTitle": "Workspaces", + "title": "Workspaces", + "subtitle": "Manage your organization's workspaces.", + "addWorkspace": "Add Workspace", + "searchPlaceholder": "Search workspace", + "open": "Open", + "rename": "Rename", + "delete": "Delete", + "empty": "No workspaces created yet.", + "columns": { + "name": "Name", + "members": "Users", + "createdAt": "Created", + "actions": "Actions" + }, + "renameDialog": { + "title": "Rename Workspace", + "description": "Update the workspace name.", + "nameLabel": "Workspace Name", + "submit": "Save", + "success": "Workspace renamed." + }, + "deleteDialog": { + "title": "Delete Workspace", + "description": "Deleting the workspace \"{name}\" permanently removes all contacts, channels, fields and associated data. This action cannot be undone.", + "confirmLabel": "Type \"{name}\" to confirm", + "submit": "Delete", + "success": "Workspace deleted." + } + }, "workspaces": { "list": { "title": "Workspaces List" + }, + "searchPlaceholder": "Search project", + "addWorkspace": "Add Workspace", + "createDialog": { + "title": "Add Workspace", + "description": "Create a new workspace to separate channels, contacts, teams and settings.", + "nameLabel": "Workspace Name", + "namePlaceholder": "Ex: Sales, Collections, Support", + "submit": "Create", + "success": "Workspace \"{name}\" created." } }, "workspaceToken": { @@ -707,11 +877,15 @@ "isDefault": { "label": "Mark as default" }, + "country": { + "label": "Country" + }, "language": { "description": "Contacts are assigned the default language when the contact language is unknown.", "english": "English", "label": "Language", "placeholder": "Select language", + "portuguese": "Português (Brasil)", "vietnamese": "Tiếng Việt" }, "lastName": { @@ -989,6 +1163,12 @@ "datetime": { "label": "Datetime" }, + "time": { + "label": "Time" + }, + "list": { + "label": "List (multi-select)" + }, "boolean": { "label": "Boolean", "placeholder": "Select true or false", @@ -1233,6 +1413,21 @@ }, "webhookUrl": { "label": "Webhook URL" + }, + "addedAt": { + "label": "Date added" + }, + "lastEditedAt": { + "label": "Last edited" + }, + "visibility": { + "label": "Visibility", + "alwaysShow": "Always show", + "alwaysHide": "Always hide", + "hideWhenEmpty": "Hide when empty" + }, + "fieldId": { + "label": "Field ID" } }, "flows": { @@ -1311,7 +1506,8 @@ "sequenceActions": "Sequence Actions", "subscribeSequence": "Subscribe Sequence", "unsubscribeSequence": "Unsubscribe Sequence", - "splitTraffic": "Split Traffic" + "splitTraffic": "Split Traffic", + "updateContactLifecycleStage": "Update Contact Lifecycle Stage" }, "splitTraffic": { "balanceHint": "The total sum must equal 100%.", @@ -1423,7 +1619,59 @@ "whatsappSystemUserTokenRequired": "System User Token is required to configure WhatsApp." } }, + "conversations": { + "searchInConversation": "Search in this conversation", + "searchInConversationPlaceholder": "Search messages…", + "searchNoResults": "No results", + "searchPrevious": "Previous result", + "searchNext": "Next result", + "composerHint": "Use '/' for snippets, '$' for variables, ':' for emoji", + "composerPlaceholder": "Type a message", + "channelPrefix": "to:", + "todayLabel": "Today", + "yesterdayLabel": "Yesterday", + "unassignedAgent": "Unassigned", + "unnamedContact": "Unnamed contact" + }, + "inboxSideRail": { + "details": "Contact details", + "channels": "Channels", + "activities": "Activities", + "attachments": "Attachments" + }, "messages": { + "actions": { + "menu": "More options", + "copy": "Copy text", + "copyLink": "Copy message link", + "reply": "Reply", + "copied": "Message copied", + "linkCopied": "Link copied" + }, + "tooltip": { + "sent": "Sent", + "delivered": "Delivered", + "read": "Read", + "received": "Received", + "failed": "Failed" + }, + "composer": { + "internalComment": "Internal comment", + "addComment": "Add Comment", + "exitInternal": "Back to message", + "internalLabel": "Internal comment", + "internalPlaceholder": "Use @ to mention a teammate. Comments are only visible to your team.", + "internalHint": "Comment visible only to your team", + "channelHint": "Channel used to send the message", + "formLabel": "Message form", + "textareaLabel": "Type your message", + "emoji": "Emoji", + "emojiSearch": "Search", + "attach": "Attach file", + "snippets": "Quick replies", + "send": "Send message" + }, + "runningWorkflows": "Running workflows", "moveFeature": "Move {feature}", "poweredBy": "Powered by {name}", "noGiphyApiKey": "No GIPHY API key found. Please add a GIPHY API key in the organization settings.", @@ -1536,6 +1784,83 @@ "settings": { "title": "Settings" }, + "settingsGroups": { + "general": "General settings", + "workspace": "Workspace", + "apps": "Apps", + "advanced": "Advanced", + "inbox": "Inbox", + "data": "Data" + }, + "lifecycle": { + "title": "Lifecycle", + "subtitle": "Track and manage contacts through each stage of your sales process", + "learnMore": "Learn more", + "toggleTitle": "Show/hide lifecycle stages", + "toggleSubtitle": "Display lifecycle stages in the inbox and contacts modules.", + "configureTitle": "Configure lifecycle stages", + "activeTitle": "Lifecycle stages", + "activeSubtitle": "Organize and track contacts through your active funnel stages.", + "lostTitle": "Lost stages", + "lostSubtitle": "Track where contacts drop out of the funnel.", + "addStage": "Add stage", + "maxStagesReached": "Maximum of {max} stages reached (active + lost).", + "stageNumber": "Stage {n}", + "stageName": "Stage name", + "newStage": "New stage", + "defaultBadge": "Default", + "saved": "Stages saved", + "saveError": "Error saving stages", + "saving": "Saving…", + "emptyNameError": "All stages need a name", + "emojiSearch": "Search", + "emojiCategories": { + "suggested": "Recently used", + "smileys": "Smileys & emotion", + "animals": "Animals & nature", + "food": "Food & drink", + "travel": "Travel & places", + "activities": "Activities", + "objects": "Objects", + "symbols": "Symbols", + "flags": "Flags" + }, + "deleteConfirm": "Delete stage \"{name}\"? Contacts on this stage will be unassigned.", + "selectPlaceholder": "Select stage", + "noStage": "No stage", + "nextStageTooltip": "Next stage: {name}", + "lastStageTooltip": "Already at the last stage of the funnel", + "contactLifecycle": "Contact lifecycle", + "activeStages": "Active stages", + "lostStages": "Lost stages", + "contactUpdated": "Contact stage updated", + "contactUpdateError": "Error updating stage", + "bulkMove": "Move stage", + "bulkMoved": "{count} contact(s) moved", + "deleteModalTitle": "Delete stage \"{name}\"", + "deleteModalDescription": "This stage has {count} contact(s). Choose which stage to move them to before deleting.", + "deleteModalReassignTo": "Move contacts to", + "deleteModalNoContacts": "This stage has no contacts. Confirm to delete.", + "deleteModalNoTarget": "Leave unassigned", + "deleteModalConfirm": "Delete and migrate", + "deleteModalCancel": "Cancel", + "stageBadgeLabel": "Stage", + "activityLog": "Stage history", + "activityLogEmpty": "No stage changes yet", + "menu": { + "setAsDefault": "Set as default", + "delete": "Delete" + }, + "anyStage": "Any stage", + "triggerFromStage": "From (origin)", + "triggerToStage": "To (destination)", + "stepHint": "Update the contact's lifecycle stage to a new stage.", + "stepUpdateAction": "Update stage", + "stepUpdateActionHint": "Update contact stage to another stage", + "stepRemoveAction": "Remove stage", + "stepRemoveActionHint": "Removes the stage from the contact", + "stepStageLabel": "Lifecycle stage" + }, "auth": { "orContinueWith": "Or continue with", "dontHaveAnAccount": "Don't have an account?", @@ -1565,7 +1890,23 @@ "signUpTitle": "Sign up to {name}" }, "tags": { - "title": "Tags" + "title": "Tags", + "subtitle": "Create and manage tags to organize conversations and contacts", + "formDescription": "Configure the tag appearance — pick an emoji, color and description.", + "namePlaceholder": "Ex: Hot lead", + "emojiLabel": "Emoji", + "clearEmoji": "Clear emoji", + "colorLabel": "Color", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Optional — what is this tag for?", + "preview": "Preview", + "addTag": "Add tag", + "searchPlaceholder": "Search tags", + "emptyState": "No tags found", + "delete": { + "usageDescription": "You're about to delete {tagCount, plural, =1 {1 tag} other {# tags}} assigned to {contactCount, plural, =1 {1 contact} other {# contacts}}. This removes the tag from all contacts and cannot be undone.", + "confirmInputLabel": "To confirm, type {count} below:" + } }, "texts": { "or": "or" @@ -1586,6 +1927,10 @@ }, "whatsapp": { "category": { + "authentication": { + "description": "OTP codes, 2-step verification, and authentication messages.", + "label": "Authentication" + }, "marketing": { "description": "Marketing category description", "label": "Marketing" @@ -1595,6 +1940,31 @@ "label": "Utility" } }, + "connectionType": { + "embedded": "Embedded Signup", + "manual": "Manual connection" + }, + "list": { + "empty": "No WhatsApp connected yet." + }, + "templatePicker": { + "empty": "No approved templates available.", + "preview": "Preview", + "sent": "Template sent", + "title": "Send template", + "variables": "Variables" + }, + "webhook": { + "pending": "Webhook pending", + "verified": "Webhook verified" + }, + "windowAlert": { + "description": "Meta does not allow sending messages 24 hours after the contact's last reply. To continue, send an approved message template or wait for a new reply from the contact to resume sending messages normally.", + "learnMore": "Learn more", + "openTemplate": "Open template list", + "sendTemplateButton": "Send message template", + "title": "WhatsApp messaging window closed" + }, "commands": { "description": "These are special keywords that tell the WhatsApp bot what to do", "label": "Commands" @@ -1825,7 +2195,35 @@ "organizationInvitationDescription2": "{userName} has invited you to join {organizationName}." }, "inboxTeams": { - "title": "Inbox Teams" + "title": "Team settings", + "subtitle": "Create and manage teams to group the users in your Workspace.", + "addTeam": "Add Team", + "searchPlaceholder": "Search Team settings", + "manage": "Manage", + "delete": "Delete", + "createDialog": { + "title": "Add Team", + "description": "Create a Team to group users. Teams can be used in place of Users to share resources like Views." + }, + "manageDialog": { + "title": "Manage Team", + "description": "Manage team by renaming it, change its description and adding or removing of team members." + }, + "deleteDialog": { + "title": "Delete Team", + "description": "Are you sure you want to delete the team \"{name}\"? This action cannot be undone. Members will not be deleted.", + "confirm": "Delete" + }, + "fields": { + "name": "Team Name", + "namePlaceholder": "Add team name here e.g. Marketing Team", + "description": "Team Description", + "descriptionPlaceholder": "Add team description here e.g. Manage marketing contacts.", + "members": "Team Members", + "membersPlaceholder": "Add a user here e.g. John." + }, + "membersCount": "{count, plural, =0 {No members} =1 {1 member} other {# members}}", + "save": "Save" }, "webhooks": { "title": "Webhooks" @@ -1880,7 +2278,8 @@ "productRemovedFromCart": "Product Removed From Cart", "productOrdered": "Product Ordered", "contactReferredANewContact": "Contact Referred A New Contact", - "contactReferredExistingContact": "Contact Referred Existing Contact" + "contactReferredExistingContact": "Contact Referred Existing Contact", + "lifecycleStageChanged": "Lifecycle stage changed" }, "actions": { "addTag": "Add Tag", @@ -1898,7 +2297,64 @@ "day": "Day", "hour": "Hour", "minute": "Minute", - "at": "At" + "at": "At", + "shortcutMenu": { + "button": "Shortcuts", + "title": "Fire flow", + "loading": "Loading shortcuts...", + "empty": "No shortcut configured. Create a flow with a Shortcut trigger in Flows.", + "fired": "Shortcut fired", + "fireError": "Failed to fire shortcut" + }, + "flowNode": { + "helperText": "Every flow starts with a trigger. When the configured event happens, this flow is fired automatically for the contact.", + "typeLabel": "Trigger type", + "typePlaceholder": "Select a trigger", + "noFilters": "No filters — fires on any event of this type. Refine using a condition step after the trigger.", + "anyOption": "Any", + "conversationOpened": { + "label": "Conversation Opened", + "hint": "Fires when a conversation is opened with the contact (new or re-opened after archive)." + }, + "conversationClosed": { + "label": "Conversation Closed", + "hint": "Fires when a conversation is closed." + }, + "contactFieldUpdated": { + "label": "Contact Field Updated", + "hint": "Fires when any contact field is changed.", + "fieldsLabel": "Fields that fire the trigger", + "empty": "No custom fields configured in this workspace." + }, + "contactTagUpdated": { + "label": "Contact Tag Updated", + "hint": "Fires when a tag is added or removed from the contact.", + "actionLabel": "Tag action", + "actionAny": "Any (added or removed)", + "actionAdded": "When tag is added", + "actionRemoved": "When tag is removed", + "tagsLabel": "Tags that fire the trigger", + "empty": "No tags configured in this workspace." + }, + "shortcut": { + "label": "Shortcut", + "hint": "Fires when an agent clicks a shortcut inside the Inbox.", + "iconLabel": "Icon (emoji)", + "iconHint": "Shown on the shortcut button inside the Inbox.", + "labelLabel": "Button text", + "labelPlaceholder": "Ex: Resend offer", + "note": "This trigger does not fire automatically. It appears as a button in the Inbox for agents to click when they want to run this flow manually." + }, + "lifecycleStageChanged": { + "label": "Lifecycle Stage Changed", + "hint": "Fires when the contact's lifecycle stage changes.", + "toStagesLabel": "To stages (destination)", + "fromStagesLabel": "From stages (origin)", + "triggerOnCleared": "Also fire when stage is cleared", + "loading": "Loading stages...", + "emptyStages": "No lifecycle stages configured in this workspace." + } + } }, "errorLogs": { "title": "Error Logs" @@ -1908,13 +2364,108 @@ "description": "Generate a token to allow your workspace to access ChatbotX APIs. Keep this token secret and do not share it with anyone." }, "contacts": { - "title": "Contacts" + "title": "Contacts", + "fieldsTitle": "Contact fields", + "admin": "Administrate", + "details": "Contact details", + "channels": "Channels", + "tagsTitle": "Tags", + "ownerLabel": "Administrator", + "noTags": "No tags", + "addTag": "Add tag", + "searchTag": "Search and select tags", + "hiddenFields": "Hidden contact fields", + "delete": { + "usageDescription": "You are about to delete {contactCount, plural, one {# contact} other {# contacts}} with {tagCount, plural, one {# tag} other {# tags}} and {conversationCount, plural, one {# conversation} other {# conversations}} attached. This action also removes the linked messages, notes and events. Type the total to confirm.", + "confirmInputLabel": "Type {count, number} to confirm" + }, + "columnsPicker": { + "button": "Columns", + "title": "Show columns", + "showAll": "Show all", + "hideAll": "Hide all" + }, + "unmerge": { + "loading": "Loading merged contacts…", + "bannerTitle": "{count, plural, one {# contact merged here} other {# contacts merged here}}", + "warning": "Restoring a merged contact recovers the contact card. Conversations, tags and custom fields that were transferred stay with this primary contact — move them back manually if needed.", + "button": "Unmerge", + "successToast": "Contact restored" + }, + "visibility": { + "customize": "Customize view", + "title": "Customize view", + "description": "Customize the visibility of contact fields in the Inbox right panel. The change will apply to all workspace users.", + "fieldHeader": "Contact field", + "visibilityHeader": "Visibility", + "showAlways": "Always show", + "alwaysHide": "Always hide", + "saved": "View updated" + }, + "column": { + "channels": "Channel(s)", + "country": "Country/region", + "language": "Language", + "conversationStatus": "Conversation Status", + "lastMessage": "Last Message", + "statusOpen": "Open", + "statusClosed": "Closed" + }, + "merge": { + "action": "Merge contacts", + "title": "Merge contacts", + "description": "Choose which contact stays as primary. The others get merged under it (tags, custom fields and conversations move to the primary) and then deleted.", + "selectPrimary": "Primary contact", + "unnamed": "unnamed", + "warning": "{count} duplicate(s) will be deleted after merging. This action cannot be undone.", + "confirm": "Merge", + "success": "{count} contact(s) merged" + }, + "sidebar": { + "all": "All contacts", + "blocked": "Blocked contacts" + }, + "drawer": { + "title": "Contact details", + "openInInbox": "Open in Inbox" + }, + "placeholders": { + "addFirstName": "Add a first name", + "addLastName": "Add a last name", + "addEmail": "Add an email", + "selectAssignee": "Select an assignee" + } }, "auditLogs": { - "title": "Audit Logs" + "title": "Activity Logs", + "subtitle": "Full history of actions performed in the workspace.", + "searchPlaceholder": "Search by detail, action or user", + "filterUser": "All users", + "filterUserAll": "All users", + "filterAction": "All actions", + "filterActionAll": "All actions", + "exportCsv": "Export CSV", + "exportSuccess": "Logs exported successfully.", + "exportError": "Failed to export logs.", + "empty": "No activity recorded yet.", + "totalRows": "{count, plural, =0 {No records} =1 {1 record} other {# records}}", + "columns": { + "user": "User", + "action": "Action", + "detail": "Detail", + "when": "When" + } }, "customFields": { - "title": "Custom Fields" + "title": "Custom Fields", + "subtitle": "Create and manage custom fields to enrich your contact profiles", + "addValue": "Add a value for {field}", + "valuesLabel": "Options", + "valuesPlaceholder": "Type and press Enter to add", + "emptyOptions": "No options configured", + "defaultsTitle": "Default contact fields", + "defaultsTooltip": "These 7 fields come from the system and are available on every contact. They can't be edited or removed — only Inbox visibility can be adjusted in \"Customize view\".", + "defaultsLockTooltip": "Default field — not editable" }, "keywords": { "title": "Keywords" @@ -1925,6 +2476,43 @@ "users": { "title": "Users" }, + "organizationUsers": { + "pageTitle": "Admin settings", + "pageSubtitle": "Manage your organization's users here.", + "addButton": "Add User", + "searchPlaceholder": "Search users", + "empty": "No users found.", + "edit": "Edit", + "delete": "Delete", + "addTitle": "Add User", + "addDescription": "Set this user's access to the organization and workspaces.", + "addSubmit": "Add", + "created": "User added.", + "editTitle": "Edit User", + "editDescription": "Update this user's access to the organization and workspaces.", + "editSubmit": "Save", + "updated": "User updated.", + "deleteTitle": "Delete User", + "deleteConfirm": "Are you sure you want to delete {name}? This action cannot be undone.", + "deleteSubmit": "Delete", + "deleted": "User deleted.", + "orgAccess": "Organization Access", + "workspaceAccess": "Workspace access", + "selectWorkspace": "Select workspace", + "selectRole": "Select access", + "addWorkspace": "Add workspace", + "noWorkspaces": "No workspaces assigned.", + "orgRoles": { + "owner": "Owner", + "manager": "Manager", + "agent": "Member" + }, + "wsRoles": { + "owner": "Owner", + "manager": "Manager", + "agent": "Agent" + } + }, "plans": { "title": "Billing Plans" }, @@ -2068,5 +2656,119 @@ "qrCodeGenerator": { "title": "QR Code Generator", "description": "Create and manage your QR codes." + }, + "flowEditor": { + "startBadge": "Start", + "copySuffix": "Copy", + "addNode": "Add block", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "fitView": "Fit to view" + }, + "conversationFilter": { + "all": "All", + "human": "Human", + "bot": "Bot" + }, + "inboxFilters": { + "all": "All", + "mine": "Mine", + "unassigned": "Unassigned", + "teamInbox": "Team Inbox", + "collapseSidebar": "Collapse sidebar", + "expandSidebar": "Expand sidebar", + "newMessageIndicator": "New message", + "blockedContacts": "Blocked Contacts" + }, + "inboxDisplayFilter": { + "showLabel": "Show", + "sortLabel": "Sort by", + "advanced": "Advanced filters", + "noReply": "No reply", + "show": { + "all": "All", + "open": "Open", + "closed": "Closed" + }, + "sort": { + "recent": "Recent", + "oldest": "Oldest" + }, + "searchContact": "Search contact" + }, + "snippets": { + "title": "Snippets", + "subtitle": "Create and manage reusable replies to speed up your conversations", + "feature": "Snippet", + "addButton": "Add Snippet", + "createTitle": "Add Snippet", + "createConfirm": "Create", + "searchPlaceholder": "Search Snippets", + "nameLabel": "Name", + "namePlaceholder": "Name your Snippet (e.g. greeting1)", + "shortcutLabel": "Shortcut", + "shortcutPlaceholder": "e.g. hi", + "topicsLabel": "Topics", + "topicsPlaceholder": "Add topic (e.g. sales, support)", + "topicsLimitReached": "Topic limit reached", + "filesLabel": "Files", + "addFile": "Add file", + "filesLimitReached": "File limit reached", + "messageLabel": "Message", + "messagePlaceholder": "Type the snippet message here", + "formDescription": "Snippets are reusable pre-written replies. Use \"/\" in the composer to insert them in conversations.", + "deleteTitle": "Delete Snippet", + "deleteDescription": "Are you sure you want to delete snippet \"{name}\"? This action cannot be undone." + }, + "botFields": { + "title": "Bot Fields", + "subtitle": "Global workspace variables used in flows and automated messages" + }, + "closingNotes": { + "title": "Closing Notes", + "subtitle": "Configure summary + category that agent fills in when closing a conversation in the Inbox.", + "modeTitle": "Operation mode", + "modeSubtitle": "Decide if agent is required to categorize and/or summarize the conversation before closing.", + "modeSaved": "Mode updated", + "savingMode": "Saving mode…", + "categoriesTitle": "Conversation categories", + "categoriesSubtitle": "List of categories the agent can choose from when closing (e.g.: Resolved, No response, Spam).", + "addCategory": "Add category", + "editCategoryTitle": "Edit category", + "categoryFormDescription": "Category becomes available to the agent in the close-conversation dialog.", + "namePlaceholder": "Ex.: Resolved", + "descriptionPlaceholder": "When to use this category (optional)", + "categoryCreated": "Category created", + "categoryUpdated": "Category updated", + "categoryDeleted": "Category removed", + "emptyCategories": "No categories yet. Add the first one so the agent can categorize conversations when closing.", + "deleteCategoryTitle": "Delete category", + "confirmDelete": "Delete category \"{name}\"? Conversations that used this category will no longer have it.", + "mode": { + "disabled": "Disabled", + "disabledDesc": "Agent closes conversation directly, no dialog.", + "optional": "Optional dialog", + "optionalDesc": "Dialog opens when closing but agent can skip without filling anything.", + "mandatoryDialog": "Category required", + "mandatoryDialogDesc": "Dialog opens, agent must choose a category. Summary optional.", + "mandatoryBoth": "Category + Summary required", + "mandatoryBothDesc": "Dialog opens, agent must choose category AND write conversation summary." + }, + "dialog": { + "title": "Close conversation", + "description": { + "optional": "Categorize and/or summarize this conversation. Both fields are optional.", + "mandatoryDialog": "Choose a category before closing this conversation.", + "mandatoryBoth": "Choose a category and write a short summary before closing." + }, + "categoryLabel": "Category", + "categoryPlaceholder": "Select a category", + "summaryLabel": "Summary", + "summaryPlaceholder": "Briefly describe what happened in this conversation…", + "noCategories": "No categories registered. Ask the admin to add categories in Settings → Closing Notes.", + "skip": "Skip and close", + "confirm": "Close conversation", + "successToast": "Conversation closed" + } } } diff --git a/apps/builder/messages/pt-BR.json b/apps/builder/messages/pt-BR.json new file mode 100644 index 000000000..0d201cefd --- /dev/null +++ b/apps/builder/messages/pt-BR.json @@ -0,0 +1,2774 @@ +{ + "actions": { + "copy": "Copiar", + "copyUrl": "Copiar URL", + "actions": "Ações", + "add": "Adicionar", + "addVariable": "Adicionar Variável", + "addFeature": "Adicionar {feature}", + "addFlowReply": "Adicionar resposta de fluxo", + "addMore": "Adicionar mais", + "addTag": "Adicionar Tag", + "addAction": "Adicionar Ação", + "addCondition": "Adicionar Condição", + "addTextReply": "Adicionar resposta de texto", + "markAsFollowUp": "Marcar como Follow Up", + "removeFromFollowUp": "Remover do Follow Up", + "markAsUnread": "Marcar como não lido", + "archive": "Arquivar", + "unarchive": "Desarquivar", + "archiveConversation": "Arquivar Conversa", + "closeConversation": "Fechar", + "openConversation": "Abrir", + "assign": "Atribuir", + "assignConversation": "Atribuir Conversa", + "back": "Voltar", + "blockContact": "Bloquear Contato", + "unblockContact": "Desbloquear Contato", + "cancel": "Cancelar", + "clear": "Limpar", + "clearCustomField": "Limpar Campo Personalizado", + "confirm": "Confirmar", + "connect": "Conectar", + "continue": "Continuar", + "create": "Criar", + "createFeature": "Criar {feature}", + "delete": "Excluir", + "deleteContact": "Excluir Contato", + "disableBot": "Desativar Bot", + "disconnect": "Desconectar", + "download": "Baixar", + "edit": "Editar", + "enableBot": "Ativar Bot", + "export": "Exportar", + "getEmbedCode": "Obter Código de Incorporação", + "getID": "Obter ID", + "getTools": "Obter Ferramentas", + "import": "Importar", + "exportContacts": "Exportar Contatos", + "filter": "Filtrar", + "selectFile": "Selecionar arquivo", + "insertLink": "Inserir link", + "addNew": "Adicionar novo", + "send": "Enviar", + "loginWithMagicLink": "Entrar com Magic Link", + "manage": "Gerenciar", + "more": "Mais", + "moreOptions": "Mais opções", + "moreSettings": "Mais configurações", + "openMenu": "Abrir menu", + "openWebchat": "Abrir Webchat", + "removeTag": "Remover Tag", + "rename": "Renomear", + "save": "Salvar", + "selectAll": "Selecionar todos", + "selectRow": "Selecionar linha", + "sendFlow": "Enviar Fluxo", + "sendMessage": "Enviar Mensagem", + "setCustomField": "Definir Campo Personalizado", + "synchronize": "Sincronizar", + "testNow": "Testar Agora", + "toggle": "Alternar", + "toggleTheme": "Alternar tema", + "transferConversationToBot": "Transferir Conversa para o Bot", + "update": "Atualizar", + "upgradeToPro": "Fazer Upgrade para Pro", + "uploadFile": "Enviar Arquivo", + "view": "Visualizar", + "duplicate": "Duplicar", + "getDraftLink": "Obter Link do Rascunho", + "getPublishedLink": "Obter Link Publicado", + "analytics": "Análises", + "flowVersions": "Versões do Fluxo", + "revertToPublished": "Reverter para Publicado", + "search": "Buscar", + "signOut": "Sair", + "backToSignIn": "Voltar para entrar", + "connectFeature": "Conectar {feature}", + "scanQRCode": "Escaneie com sua câmera", + "inviteFeature": "Convidar {feature}", + "joinTheTeam": "Entrar no time", + "chooseChannel": "Escolher Canal", + "chooseSubaction": "Escolher Subação", + "resend": "Reenviar", + "publish": "Publicar", + "setStartNode": "Definir como Nó Inicial", + "getNodeId": "Obter ID do Nó", + "setAsDefaultAgent": "Definir como Padrão", + "unsetDefaultAgent": "Desfazer Padrão", + "clickToEdit": "Clique para editar", + "move": "Mover", + "removeAssignee": "Remover Responsável", + "sendMail": "Enviar E-mail", + "wait": "Aguardar", + "landingPage": "Página de Captura", + "generate": "Gerar", + "regenerate": "Regenerar", + "qrCode": "QR Code", + "addSequence": "Adicionar Sequência", + "removeSequence": "Remover Sequência", + "activate": "Ativar", + "deactivate": "Desativar", + "onFailure": "Em caso de falha", + "onSkip": "Em caso de pulo", + "onSuccess": "Em caso de sucesso", + "trigger": "Gatilho", + "close": "Fechar", + "remove": "Remover" + }, + "states": { + "success": "Sucesso", + "error": "Erro", + "skip": "Pular", + "closed": "Fechada", + "blocked": "Bloqueado" + }, + "admins": { + "title": "Configurações do usuário", + "subtitle": "Gerencie e convide membros da equipe para seu espaço de trabalho.", + "addUser": "Adicionar Usuário", + "searchPlaceholder": "Pesquisar Configurações do usuário", + "revoke": "Revogar", + "edit": "Alterar", + "roles": { + "owner": "Proprietário", + "manager": "Administrador", + "agent": "Representante" + }, + "lastSeen": "Última atividade", + "inviteDialog": { + "title": "Adicionar Usuário", + "description": "Digite o endereço de e-mail e selecione um nível de acesso para convidar um novo usuário para seu espaço de trabalho.", + "emailLabel": "Endereço de E-mail", + "emailPlaceholder": "usuario@empresa.com", + "accessLevelLabel": "Nível de Acesso", + "submit": "Adicionar" + }, + "editDialog": { + "title": "Editar Usuário", + "description": "Para editar este usuário, selecione um novo nível de acesso e clique em \"Editar\". O Nível de Acesso define o que o usuário pode fazer no espaço de trabalho.", + "submit": "Atualização" + }, + "revokeDialog": { + "title": "Revogar Acesso", + "description": "Se você revogar este usuário, todos os contatos atribuídos a ele serão marcados como não atribuídos. Tem certeza que deseja revogar o acesso deste usuário?", + "confirm": "Revogar" + }, + "roleDescriptions": { + "owner": "Os proprietários podem acessar todos os módulos do espaço de trabalho e têm controle total das configurações sem restrições.", + "manager": "Os administradores têm acesso a todos os módulos do espaço de trabalho, incluindo as Configurações. No entanto, não podem convidar/excluir outros administradores ou excluir canais.", + "agent": "Os representantes só têm acesso a mensagens dentro do espaço de trabalho. Opcionalmente, você pode limitar a visibilidade em Contatos." + }, + "advancedRestrictions": "Restrições avançadas", + "managerRestrictions": { + "restrictDataExport": { + "label": "Restringir Exportação de Dados", + "description": "Este usuário não será capaz de exportar dados do espaço de trabalho." + }, + "restrictContactDeletion": { + "label": "Restringir exclusão de contato", + "description": "Este usuário não será capaz de excluir nenhum contato do espaço de trabalho." + }, + "restrictWorkspaceSettings": { + "label": "Restringir acesso às configurações do Workspace", + "description": "Este usuário não poderá acessar as configurações da área de trabalho." + }, + "restrictIntegrationSettings": { + "label": "Restringir acesso às configurações de integração", + "description": "Este usuário não poderá acessar as configurações de integração." + } + }, + "agentRestrictions": { + "contactVisibility": { + "label": "Restringir visibilidade de contato", + "description": "Este usuário só vê contatos atribuídos a", + "options": { + "all": "todos", + "team": "equipe do usuário", + "self": "usuário" + } + }, + "restrictCalling": { + "label": "Restringir recursos de chamadas", + "description": "Este usuário não conseguirá receber ou iniciar chamadas." + }, + "restrictWorkflows": { + "label": "Botão de restringir fluxos de trabalho", + "description": "O botão Fluxos de Trabalho no módulo Mensagens ficará desativado para este usuário. Isso inclui os atalhos e a opção de interromper um fluxo de trabalho para um contato." + }, + "maskPhoneAndEmail": { + "label": "Mascaramento do número de telefone e endereço de e-mail", + "description": "Este usuário não conseguirá ver os números de telefone e endereços de e-mail de todos os contatos." + } + } + }, + "assignAdmin": { + "assignConversation": "Atribuir conversa", + "assignedToMe": "Atribuído a mim", + "assignedTo": "Atribuído a {name}", + "unAssigned": "Não atribuído" + }, + "aiAgent": { + "defaultAgent": "Agente Padrão", + "description": "Gerencie e configure Agentes de IA para potencializar seu workspace com automação inteligente e respostas automatizadas.", + "title": "Agentes de IA", + "name": "Agentes" + }, + "aiFiles": { + "description": "Dê aos seus agentes acesso a PDFs, documentos e mais", + "title": "Conhecimento" + }, + "aiFunctions": { + "description": "Configure funções de LLM para tornar seu agente de IA mais inteligente", + "title": "Funções de IA" + }, + "aiMcpServers": { + "description": "Conecte a IA a sistemas externos via MCP", + "title": "Servidores MCP", + "tools": { + "label": "Ferramentas Disponíveis" + } + }, + "analytics": { + "contacts": "Contatos", + "newContacts": "Novos Contatos", + "activeContacts": "Contatos Ativos", + "botMessagesByResult": "Mensagens recebidas pelo bot", + "botMessagesNoResponse": "Mensagens recebidas sem resposta", + "botMessagesWithResponse": "Mensagens recebidas com resposta", + "aiProvider": "Provedor de IA", + "responseCount": "Quantidade de Respostas", + "percentage": "Porcentagem", + "responseTime": "Tempo de Resposta", + "firstResponseTime": "Tempo da Primeira Resposta", + "totalContacts": "Total de Contatos", + "averageResponseTime": "Tempo Médio de Resposta (Minutos)", + "averageResponseTimeHelp": "O tempo médio que um atendente humano leva para responder a mensagem de um cliente.", + "averageFirstResponseTimeByAdmin": "Tempo médio da primeira resposta por atendentes humanos (Minutos)", + "averageFirstResponseTimeByAdminHelp": "O tempo médio que um atendente humano leva para enviar a primeira resposta à mensagem de um cliente.", + "averageResponseTimeByAdmin": "Tempo médio de resposta por atendentes humanos (Minutos)", + "averageDurationOfConversation": "Duração média de uma conversa (Minutos)", + "averageDurationOfConversationHelp": "Tempo médio que uma conversa fica em mãos de um humano antes de ser passada para o bot.", + "success": "Sucesso", + "fallback": "Fallback", + "admins": "Atendentes Humanos", + "messages": "Mensagens", + "conversations": "Conversas", + "assignedConversations": { + "title": "Conversas Atribuídas", + "helpText": "Conversas atribuídas pela caixa de entrada ou pelo bot." + }, + "messagesSentByHumanOrBot": "Mensagens enviadas por humano/bot", + "human": "Humano", + "bot": "Bot", + "conversationsMovedToHumanOrBot": "Conversas transferidas para humano/bot", + "uniqueConversationsByAdmins": "Conversas únicas por atendentes humanos", + "uniqueConversationsByAdminsHelp": "Número de contatos para quem o atendente humano enviou ao menos uma mensagem.", + "messagesSentByAdmins": "Mensagens enviadas por atendentes humanos", + "messagesSentByAdminsHelp": "Mensagens enviadas pela caixa de entrada.", + "allContactsByChannel": "Todos os contatos por canal", + "newContactsByChannel": "Novos contatos por canal", + "newContactsBySource": "Novos contatos por origem", + "assignedConversationsByAdmins": "Conversas atribuídas por atendentes humanos", + "assignedConversationsByAdminsHelp": "Conversas atribuídas pela caixa de entrada ou pelo bot.", + "followUpConversations": "Conversas em follow up", + "followUpConversationsHelp": "Conversas marcadas como follow up pela caixa de entrada ou pelo bot.", + "archivedConversations": "Conversas arquivadas", + "blockedConversations": "Conversas bloqueadas", + "blockedConversationsHelp": "Conversas que sua conta bloqueou.", + "blockedContacts": "Contatos bloqueados", + "blockedContactsHelp": "Contatos que não querem receber mensagens da sua conta", + "newContactsByCountry": "Novos contatos por país" + }, + "autoAssignConversation": { + "rule": { + "allTime": "Distribuição igual de conversas (Todo o período)", + "label": "Regra de Atribuição", + "last24Hours": "Distribuição igual de conversas (Últimas 24 horas)", + "last8Hours": "Distribuição igual de conversas (Últimas 8 horas)", + "lastHour": "Distribuição igual de conversas (Última hora)" + }, + "users": { + "label": "Usuários" + } + }, + "automatedResponse": { + "setting": { + "description": "Descrição das respostas automatizadas", + "label": "Respostas Automatizadas" + } + }, + "automation": { + "title": "Automação" + }, + "billing": { + "title": "Faturamento" + }, + "broadcasts": { + "title": "Transmissões", + "status": { + "scheduled": "Agendado", + "sent": "Enviado", + "sending": "Enviando" + }, + "stats": { + "sent": "Enviado", + "delivered": "Entregue", + "seen": "Visualizado", + "clicked": "Clicado", + "failed": "Falhou", + "noContacts": "Nenhum contato encontrado", + "pageInfo": "Página {page} de {pageCount}" + }, + "messengerList": { + "title": "Lista do Messenger", + "description": "Pode conter promoções e será enviado apenas para pessoas inscritas na sua lista do Messenger." + }, + "messengerActiveContacts": { + "title": "Contatos Ativos do Messenger", + "description": "Pode conter promoções e será enviado apenas para usuários que mandaram mensagem ao seu bot nas últimas 24h." + }, + "messengerAccountUpdate": { + "title": "Atualização de Conta do Messenger", + "description": "Notifica o usuário sobre uma alteração não recorrente em seu aplicativo ou conta." + }, + "messengerConfirmedEventUpdate": { + "title": "Atualização de Evento Confirmado do Messenger", + "description": "Envia lembretes ou atualizações ao usuário sobre um evento para o qual ele se registrou (ex: compra de ingressos). Esta tag pode ser usada para eventos futuros e em andamento." + }, + "messengerPostPurchaseUpdate": { + "title": "Atualização Pós-Compra do Messenger", + "description": "Notifica o usuário sobre uma atualização em uma compra recente. Confirmação de transação como faturas ou recibos, notificações de status de envio como produto em trânsito, despachado, entregue ou atrasado." + }, + "allContacts": { + "title": "Todos os Contatos", + "description": "Envia um fluxo para todos os contatos." + }, + "receiversCount": "Destinatários: {count}", + "whatsappTemplateMessage": { + "title": "Mensagem de Template", + "description": "Envia uma mensagem template do WhatsApp para seus contatos" + }, + "whatsappWithin24Hours": { + "title": "Contatos ativos nas últimas 24 horas", + "description": "Pode conter promoções e será enviado apenas para usuários que mandaram mensagem ao seu bot nas últimas 24h." + }, + "flowType": { + "flow": { + "title": "Fluxo", + "description": "Envia um fluxo para seus contatos" + }, + "template": { + "title": "Template", + "description": "Envia um template para seus contatos" + } + } + }, + "condition": { + "yes": "Sim", + "no": "Não", + "fields": { + "locale": "Idioma", + "fullName": "Nome completo", + "country": "País", + "continent": "Continente", + "gender": "Gênero", + "subscribedToBroadcast": "Inscrito em transmissão", + "contactCreatedDate": "Data de criação do contato", + "contactCreatedDateMinutesAgo": "Data de criação do contato (minutos atrás)", + "source": "Origem", + "conversationTransferredToHuman": "Conversa transferida para humano", + "interactedInLast24H": "Interagiu nas últimas 24 horas", + "isGuestUser": "Usuário visitante", + "existingContact": "Contato existente", + "currentChannel": "Canal atual", + "timezone": "Fuso horário", + "tags": "Tags", + "customFields": "Campos personalizados", + "phone": "Telefone", + "executedFlow": "Fluxo executado", + "email": "E-mail", + "followUp": "Follow up", + "unread": "Não lido", + "archived": "Arquivado", + "blocked": "Bloqueado", + "noAdminReply": "Sem resposta do admin", + "contactCreatedAt": "Contato criado em", + "interactedInLast24h": "Interagiu nas últimas 24 horas" + } + }, + "channels": { + "title": "Canais" + }, + "organization": { + "manage": "Gerenciar Organização" + }, + "personalSettings": { + "title": "Configurações pessoais", + "account": "Conta", + "profile": "Perfil", + "notifications": "Configurações de notificações", + "activityStatus": { + "sectionLabel": "Meu status", + "available": "Disponível", + "busy": "Ocupado", + "offline": "Desconectado", + "updateSuccess": "Status atualizado para {status}", + "updateError": "Falha ao atualizar status" + }, + "profileSection": { + "title": "Perfil", + "subtitle": "Gerencie seu perfil e detalhes de login.", + "changeAvatar": "Alterar foto", + "avatarUpdated": "Foto atualizada.", + "firstNameLabel": "Primeiro Nome", + "lastNameLabel": "Último Nome", + "languageLabel": "Idioma", + "changePassword": "Mudar a senha", + "saveProfile": "Salvar Perfil", + "savedSuccess": "Perfil atualizado.", + "twoFA": { + "title": "Autenticação de dois fatores (2FA)", + "toggle": "Habilitar autenticação de dois fatores (2FA)" + }, + "darkMode": { + "title": "Modo escuro", + "system": "Usar configurações do sistema", + "light": "Desligado", + "dark": "Ligado" + } + }, + "notificationsSection": { + "title": "Configurações de notificações", + "subtitle": "Escolha como receber alertas do seu workspace." + } + }, + "manageWorkspaces": { + "sidebarTitle": "Workspaces", + "title": "Workspaces", + "subtitle": "Gerencie os workspaces da sua organização.", + "addWorkspace": "Adicionar Workspace", + "searchPlaceholder": "Pesquisar workspace", + "open": "Abrir", + "rename": "Renomear", + "delete": "Apagar", + "empty": "Nenhum workspace criado ainda.", + "columns": { + "name": "Nome", + "members": "Usuários", + "createdAt": "Data de criação", + "actions": "Ações" + }, + "renameDialog": { + "title": "Renomear Workspace", + "description": "Atualize o nome do workspace.", + "nameLabel": "Nome do Workspace", + "submit": "Guardar", + "success": "Workspace renomeado." + }, + "deleteDialog": { + "title": "Apagar Workspace", + "description": "Apagar o workspace \"{name}\" remove todos os contatos, canais, campos e dados associados de forma permanente. Esta ação não pode ser desfeita.", + "confirmLabel": "Digite \"{name}\" para confirmar", + "submit": "Apagar", + "success": "Workspace apagado." + } + }, + "workspaces": { + "list": { + "title": "Lista de Workspaces" + }, + "searchPlaceholder": "Pesquisar projeto", + "addWorkspace": "Adicionar Workspace", + "createDialog": { + "title": "Adicionar Workspace", + "description": "Crie um novo workspace para separar canais, contatos, equipes e configurações.", + "nameLabel": "Nome do Workspace", + "namePlaceholder": "Ex: Vendas, Cobrança, Suporte", + "submit": "Criar", + "success": "Workspace \"{name}\" criado." + } + }, + "workspaceToken": { + "title": "Token do Workspace" + }, + "fields": { + "omnichannel": { + "label": "Omnichannel" + }, + "messenger": { + "label": "Messenger" + }, + "instagram": { + "label": "Instagram" + }, + "zalo": { + "label": "Zalo OA" + }, + "telegram": { + "label": "Telegram", + "botToken": "Token do Bot", + "connectInstructions": { + "step1": "Abra @BotFather no Telegram.", + "step2": "Envie /newbot e siga as instruções.", + "step3": "Após criar o bot, você receberá o token da API. Copie e cole abaixo." + } + }, + "matchAll": { + "label": "Atender todas as condições abaixo" + }, + "matchAny": { + "label": "Atender uma das condições abaixo" + }, + "condition": { + "label": "Condição" + }, + "actions": { + "label": "Ações" + }, + "tags": { + "label": "Tags" + }, + "customFields": { + "label": "Campos Personalizados" + }, + "pipelines": { + "label": "Pipelines" + }, + "sequences": { + "label": "Sequências" + }, + "ecommerce": { + "label": "E-commerce" + }, + "entryPointLink": { + "label": "Link de Entrada" + }, + "assignedId": { + "label": "Atribuir a" + }, + "operator": { + "label": "Operador", + "is": "É", + "isNot": "Não é", + "in": "Em", + "notIn": "Não em", + "isEmpty": "Está vazio", + "isNotEmpty": "Não está vazio", + "gt": "Maior que", + "lt": "Menor que", + "gte": "Maior ou igual a", + "lte": "Menor ou igual a", + "contains": "Contém", + "notContains": "Não contém", + "startsWith": "Começa com", + "endsWith": "Termina com", + "isBetween": "Está entre", + "notBetween": "Não está entre" + }, + "contactFilter": { + "label": "Filtro de contato" + }, + "contactNote": { + "label": "Nota do Contato" + }, + "chooseTime": { + "label": "Escolher horário" + }, + "notificationType": { + "label": "Tipo de Notificação", + "notifyAdmin": "Notificar Admin", + "newMessageToHuman": "Nova Mensagem para Humano", + "newOrder": "Novo Pedido" + }, + "notificationChannel": { + "label": "Canal de Notificação", + "messenger": "Messenger", + "email": "E-mail", + "telegram": "Telegram", + "browser": "Navegador" + }, + "accessToken": { + "label": "Token de Acesso" + }, + "botField": { + "label": "Campo do Bot" + }, + "agent": { + "label": "Agente" + }, + "aiAgent": { + "label": "Agente de IA" + }, + "aiFile": { + "label": "Arquivo de IA" + }, + "aiFunction": { + "label": "Função de IA" + }, + "aiTrigger": { + "label": "Gatilho de IA" + }, + "alphanumericLength": { + "label": "Comprimento Alfanumérico" + }, + "analytics": { + "label": "Análises" + }, + "apiKey": { + "label": "API Key" + }, + "auth": { + "label": "Autenticação" + }, + "authorizedDomain": { + "description": "Os domínios ou subdomínios autorizados a acessar seu webchat.", + "label": "Domínio{plural, select, 1 {} other {s}} Autorizado{plural, select, 1 {} other {s}}" + }, + "authToken": { + "label": "Token" + }, + "authType": { + "headers": "Header Personalizado", + "none": "Nenhum", + "token": "Token" + }, + "automatedResponse": { + "label": "Resposta Automatizada" + }, + "avatar": { + "label": "Avatar" + }, + "reflink": { + "label": "Link de Entrada" + }, + "savePayloadToCustomField": { + "label": "Salvar payload em campo personalizado" + }, + "bot": { + "label": "Bot" + }, + "botResponse": { + "label": "Resposta do Bot" + }, + "brandColor": { + "description": "Esta cor é usada em botões para combinar com sua marca na landing page, e-mails e webchat.", + "label": "Cor da Marca" + }, + "broadcast": { + "label": "Transmissão" + }, + "trigger": { + "label": "Gatilho" + }, + "webhook": { + "label": "Webhook" + }, + "button": { + "label": "Botão", + "whenPressed": "Quando esse botão for pressionado" + }, + "buttonLabel": { + "label": "Rótulo do Botão" + }, + "category": { + "label": "Categoria" + }, + "workspace": { + "label": "Workspace" + }, + "workspaceId": { + "description": "Identificador único do seu workspace.", + "label": "ID do Workspace" + }, + "workspaceName": { + "label": "Nome da Conta" + }, + "contact": { + "label": "Contato" + }, + "contacts": { + "label": "Contatos" + }, + "conversation": { + "label": "Conversa" + }, + "conversationStarter": { + "description": "Os iniciadores de conversa são usados para começar uma conversa com um usuário.", + "label": "Iniciador{plural, select, 1 {} other {es}} de Conversa", + "type": { + "openWebsite": "Abrir Site", + "sendFlow": "Enviar Fluxo", + "sendText": "Enviar Texto" + } + }, + "createdAt": { + "label": "Criado em" + }, + "currentTime": { + "label": "Horário Atual" + }, + "customRange": { + "label": "Intervalo personalizado" + }, + "customCss": { + "label": "CSS Personalizado" + }, + "theme": { + "label": "Tema", + "placeholder": "Selecione um tema" + }, + "customJS": { + "label": "JS Personalizado", + "placeholder": "Insira JavaScript personalizado" + }, + "customCSS": { + "label": "CSS Personalizado", + "placeholder": "Insira CSS personalizado" + }, + "customField": { + "label": "Campo Personalizado", + "savePayloadTo": "Salvar resposta em Campo Personalizado", + "set_value": "Definir valor", + "append": "Adicionar ao final", + "prepend": "Adicionar ao início", + "increase": "Aumentar em", + "decrease": "Diminuir em" + }, + "dataCollect": { + "label": "Coleta de Dados" + }, + "defaultReply": { + "description": "É a resposta padrão que seu workspace enviará aos usuários quando o workspace não souber como responder à mensagem do usuário.", + "label": "Resposta Padrão" + }, + "delayType": { + "label": "Tipo de Atraso", + "placeholder": "Tipo de Atraso" + }, + "delayUnit": { + "days": "Dias", + "hours": "Horas", + "minutes": "Minutos", + "seconds": "Segundos" + }, + "description": { + "label": "Descrição", + "placeholder": "Insira a descrição" + }, + "developmentMode": { + "description": "Seu bot funcionará apenas para administradores. Ative essa opção se você está construindo seu bot e não quer que não-admins usem.", + "label": "Modo de Desenvolvimento" + }, + "email": { + "label": "E-mail", + "placeholder": "Insira o e-mail" + }, + "embedCode": { + "description": "Copie e cole esse código no seu site para incorporar o widget de chat.", + "label": "Código de Incorporação", + "securityNote": "Nota de Segurança", + "securityNoteDescription": "Este widget só funcionará em domínios autorizados na configuração do seu webchat. Certifique-se de adicionar seu domínio à lista de domínios autorizados." + }, + "processingStatus": { + "label": "Status do Processamento" + }, + "enable": { + "label": "Ativar" + }, + "file": { + "label": "Arquivo" + }, + "filter": { + "label": "Filtro" + }, + "finalMessage": { + "label": "Mensagem Final" + }, + "firstName": { + "label": "Nome", + "placeholder": "Insira o nome" + }, + "countryCode": { + "label": "Código do País" + }, + "contactId": { + "label": "ID do Contato / ID do Assinante / PSID" + }, + "flow": { + "label": "Fluxo" + }, + "flowId": { + "label": "ID do Fluxo" + }, + "flows": { + "aiGenerateText": { + "label": "{aiName}: Gerar Texto" + }, + "label": "Fluxos", + "placeholder": "Selecione o fluxo" + }, + "steps": { + "label": "Passos", + "placeholder": "Selecione o passo" + }, + "folder": { + "label": "Pasta" + }, + "format": { + "label": "Formato" + }, + "framework": { + "angular": "Angular", + "ember": "Ember", + "react": "React", + "svelte": "Svelte", + "vue": "Vue" + }, + "function": { + "label": "Função" + }, + "gemini": { + "label": "Gemini" + }, + "geminiModel": { + "label": "Modelo Gemini" + }, + "gender": { + "female": "Feminino", + "label": "Gênero", + "male": "Masculino", + "unknown": "Desconhecido" + }, + "googleSheets": { + "label": "Google Sheets" + }, + "host": { + "label": "Host", + "placeholder": "smtp.example.com" + }, + "hideHeader": { + "label": "Mostrar Cabeçalho" + }, + "hideMessageInput": { + "label": "Mostrar Campo de Mensagem" + }, + "inbox": { + "label": "Caixa de Entrada" + }, + "inboxTeam": { + "label": "Time da Caixa de Entrada" + }, + "teamMembers": { + "label": "Membros do Time" + }, + "inboxTeamMember": { + "label": "Membro do Time da Caixa de Entrada" + }, + "inputCustomField": { + "label": "Campo Personalizado de Entrada" + }, + "insertLink": { + "label": "Inserir link" + }, + "instructions": { + "label": "Mensagem do Sistema (Prompt)" + }, + "isDefault": { + "label": "Marcar como padrão" + }, + "country": { + "label": "País" + }, + "language": { + "description": "Os contatos recebem o idioma padrão quando o idioma do contato é desconhecido.", + "english": "Inglês", + "label": "Idioma", + "placeholder": "Selecione o idioma", + "portuguese": "Português (Brasil)", + "vietnamese": "Vietnamita" + }, + "lastName": { + "label": "Sobrenome", + "placeholder": "Insira o sobrenome" + }, + "last30days": { + "label": "Últimos 30 dias" + }, + "last7days": { + "label": "Últimos 7 dias" + }, + "lastInput": { + "label": "Última Entrada" + }, + "lastMonth": { + "label": "Mês passado" + }, + "lifeTime": { + "label": "Vitalício" + }, + "locale": { + "label": "Idioma" + }, + "errorLog": { + "label": "Log de Erros" + }, + "max": { + "label": "Máximo" + }, + "maxOutputTokens": { + "label": "Máximo de Tokens" + }, + "mcpServer": { + "label": "Servidor MCP" + }, + "systemFunction": { + "label": "Função do Sistema" + }, + "min": { + "label": "Mínimo" + }, + "model": { + "label": "Modelo" + }, + "name": { + "label": "Nome", + "placeholder": "Insira o nome", + "searchPlaceholder": "Buscar nome..." + }, + "node": { + "label": "Nó" + }, + "noResults": { + "label": "Nenhum resultado encontrado" + }, + "notes": { + "label": "Notas" + }, + "numericLength": { + "label": "Comprimento Numérico" + }, + "numericValue": { + "label": "Valor Numérico" + }, + "openai": { + "label": "OpenAI" + }, + "operation": { + "label": "Operação" + }, + "outputCustomField": { + "label": "Campo Personalizado de Saída" + }, + "outputMessage": { + "label": "Mensagem de Saída", + "placeholder": "Qual é a mensagem de saída?" + }, + "paymentHistory": { + "label": "Histórico de Pagamento" + }, + "paymentMethods": { + "label": "Métodos de Pagamento" + }, + "persistentMenu": { + "description": "Os menus persistentes são usados para iniciar uma conversa com um usuário.", + "label": "Menu{plural, select, 1 {} other {s}} Persistente{plural, select, 1 {} other {s}}", + "type": { + "openWebsite": "Abrir Site", + "sendFlow": "Enviar Fluxo" + } + }, + "phoneNumber": { + "label": "Número de telefone" + }, + "phoneNumberId": { + "label": "Número de telefone", + "noPhoneNumbersFound": "Nenhum número de telefone encontrado para esta conta" + }, + "port": { + "label": "Porta", + "placeholder": "587" + }, + "provider": { + "label": "Provedor" + }, + "prompt": { + "label": "Prompt", + "placeholder": "Insira o prompt" + }, + "userMessage": { + "label": "Mensagem do usuário" + }, + "pageUserName": { + "label": "Nome de Usuário da Página" + }, + "purpose": { + "label": "Propósito", + "placeholder": "O que esta função faz?" + }, + "question": { + "label": "Pergunta", + "placeholder": "Você está aberto hoje?" + }, + "imageProfileUrl": { + "label": "URL da Imagem de Perfil" + }, + "persona": { + "label": "Persona" + }, + "quickReply": { + "label": "Resposta Rápida" + }, + "search": { + "placeholder": "Buscar..." + }, + "logo": { + "label": "Logo" + }, + "showLogo": { + "label": "Mostrar Logo Pessoal" + }, + "size": { + "label": "Tamanho" + }, + "status": { + "label": "Status", + "pending": "Pendente", + "processing": "Processando", + "success": "Sucesso", + "error": "Erro" + }, + "subtitle": { + "placeholder": "Insira o subtítulo" + }, + "syncToMessenger": { + "label": "Sincronizar com Messenger" + }, + "tag": { + "label": "Etiqueta" + }, + "targetCountry": { + "description": "O país onde a maioria dos seus contatos vive.", + "label": "País Alvo" + }, + "team": { + "label": "Time" + }, + "rememberConversation": { + "label": "Lembrar conversa" + }, + "temperature": { + "label": "Temperatura" + }, + "templateMessages": { + "label": "Mensagens de Template" + }, + "text": { + "label": "Texto", + "placeholder": "Insira o texto" + }, + "thisMonth": { + "label": "Este mês" + }, + "timezone": { + "description": "Os contatos recebem este fuso horário quando o fuso do contato é desconhecido.", + "label": "Fuso Horário" + }, + "title": { + "placeholder": "Insira o título" + }, + "today": { + "label": "Hoje" + }, + "tools": { + "label": "Ferramentas", + "placeholder": "Selecione ferramentas" + }, + "triggerFlowId": { + "label": "ID do Fluxo Gatilho", + "placeholder": "Qual fluxo é disparado?" + }, + "type": { + "label": "Tipo", + "placeholder": "Selecione o tipo" + }, + "lastRead": { + "label": "Última Leitura" + }, + "assignee": { + "label": "Responsável" + }, + "modified": { + "label": "Modificado" + }, + "estimatedContacts": { + "label": "Contatos Estimados" + }, + "scheduledAt": { + "label": "Agendado para" + }, + "channel": { + "label": "Canal" + }, + "updatedAt": { + "label": "Atualizado em" + }, + "url": { + "label": "URL", + "placeholder": "Insira a URL" + }, + "userId": { + "label": "ID do Usuário" + }, + "userTags": { + "label": "Tags do Usuário" + }, + "username": { + "label": "Usuário", + "placeholder": "user@example.com" + }, + "keywords": { + "label": "Palavras-chave" + }, + "value": { + "label": "Valor" + }, + "wabaId": { + "label": "WABA ID" + }, + "webchat": { + "label": "Webchat" + }, + "welcomeFlowId": { + "description": "A mensagem de boas-vindas é enviada automaticamente quando o usuário abre seu webchat.", + "label": "Fluxo de Boas-vindas" + }, + "whatsapp": { + "label": "WhatsApp" + }, + "whatsappFlow": { + "label": "WhatsApp Flow" + }, + "shortText": { + "label": "Texto Curto", + "placeholder": "Insira o texto" + }, + "number": { + "label": "Número", + "placeholder": "Insira o número" + }, + "date": { + "label": "Data" + }, + "datetime": { + "label": "Data e Hora" + }, + "time": { + "label": "Hora" + }, + "list": { + "label": "Lista (multi-seleção)" + }, + "boolean": { + "label": "Booleano", + "placeholder": "Selecione verdadeiro ou falso", + "true": "Verdadeiro", + "false": "Falso" + }, + "longText": { + "label": "Texto Longo" + }, + "replyFormat": { + "label": "Formato da Resposta", + "number": "Número", + "text": "Texto", + "email": "E-mail", + "phone": "Telefone", + "image": "Imagem", + "file": "Arquivo", + "link": "Link", + "date": "Data", + "datetime": "Data e Hora", + "location": "Localização" + }, + "workspaceMember": { + "label": "Membro do Workspace" + }, + "yesterday": { + "label": "Ontem" + }, + "permissions": { + "label": "Permissões", + "superAdmin": "Super Admin", + "analytics": "Análises", + "flows": "Fluxos", + "contacts": "Contatos", + "onlyAssignedContacts": "Apenas Contatos Atribuídos", + "emailAndPhone": "E-mail e Telefone", + "broadcast": "Transmissão", + "ecommerce": "E-commerce" + }, + "member": { + "label": "Membro" + }, + "schedule": { + "label": "Agendar", + "now": "Agora", + "scheduled": "Agendar para depois" + }, + "inputFieldId": { + "label": "Campo Personalizado de Entrada" + }, + "outputFieldId": { + "label": "Salvar resultado em campo personalizado" + }, + "jsonPath": { + "placeholder": "Insira o caminho JSON" + }, + "spreadsheets": { + "label": "Planilhas" + }, + "retryMessage": { + "label": "Mensagem de Nova Tentativa" + }, + "skipButtonLabel": { + "label": "Rótulo do Botão Pular" + }, + "autoSkipTime": { + "label": "Tempo de Pulo Automático" + }, + "autoSkipFailAttempts": { + "label": "Tentativas Falhas para Pulo Automático" + }, + "autoSkip": { + "label": "Pulo Automático" + }, + "spreadsheet": { + "label": "Planilha" + }, + "source": { + "label": "Origem" + }, + "password": { + "label": "Senha", + "placeholder": "••••••••" + }, + "passwordConfirmation": { + "label": "Repita a senha" + }, + "newPassword": { + "label": "Nova Senha" + }, + "developerAccessToken": { + "label": "Token de Acesso da API" + }, + "fullName": { + "label": "Nome Completo" + }, + "botFields": { + "label": "Campos do Bot" + }, + "keyword": { + "label": "Palavra-chave", + "searchPlaceholder": "Buscar palavra-chave..." + }, + "templateId": { + "label": "Template do WhatsApp" + }, + "whatsappChannel": { + "label": "Canal do WhatsApp" + }, + "messages": { + "label": "Mensagens" + }, + "shortcut": { + "label": "Atalho" + }, + "savedReplies": { + "label": "Respostas Salvas" + }, + "role": { + "label": "Função" + }, + "plan": { + "label": "Plano" + }, + "price": { + "label": "Preço" + }, + "annualPrice": { + "label": "Preço Anual" + }, + "limitContacts": { + "label": "Máximo de Contatos" + }, + "marketingFeatures": { + "label": "Lista de recursos de marketing", + "description": "Uma lista de recursos do produto visíveis aos clientes. Exibida nas tabelas de preço." + }, + "currency": { + "label": "Moeda" + }, + "clientId": { + "label": "Client ID" + }, + "clientSecret": { + "label": "Client Secret" + }, + "verifyToken": { + "label": "Token de Verificação do Webhook" + }, + "apiVersion": { + "label": "Versão da API" + }, + "configId": { + "label": "ID de Configuração do App" + }, + "systemUserId": { + "label": "ID do Usuário do Sistema" + }, + "systemUserToken": { + "label": "Token do Usuário do Sistema" + }, + "businessId": { + "label": "ID do Negócio" + }, + "businessName": { + "label": "Nome do Negócio" + }, + "publishableKey": { + "label": "Publishable Key" + }, + "secretKey": { + "label": "Secret Key" + }, + "freeTrial": { + "label": "Teste Grátis", + "suffix": "dias" + }, + "pricing": { + "label": "Preços" + }, + "sequence": { + "label": "Sequência" + }, + "from": { + "label": "De" + }, + "fromAddress": { + "label": "Endereço de Origem", + "placeholder": "noreply@example.com" + }, + "messageTemplate": { + "label": "Template de Mensagem" + }, + "preheader": { + "label": "Pré-cabeçalho" + }, + "subject": { + "label": "Assunto" + }, + "to": { + "label": "Para" + }, + "topicId": { + "label": "ID do Tópico" + }, + "smtpChannel": { + "label": "Canal SMTP" + }, + "magicLink": { + "label": "Magic Link", + "nameHint": "Adicionado depois de /r/{workspaceId}/ na URL pública de rastreamento. Use apenas letras, números, underscore e hífen.", + "urlHint": "Use chaves duplas na URL, ex: https://example.com/page?utm_campaign='{{campaign}}'. Os valores vêm da query string do link de rastreamento." + }, + "appId": { + "label": "ID do App" + }, + "appSecret": { + "label": "App Secret" + }, + "webhookVerifyToken": { + "label": "Token de Verificação do Webhook" + }, + "heading": { + "label": "Título", + "placeholder": "Insira o título" + }, + "image": { + "label": "Imagem" + }, + "line": { + "label": "Linha" + }, + "spacing": { + "label": "Espaçamento" + }, + "code": { + "label": "Código", + "placeholder": "Insira o código" + }, + "authCallbackUrl": { + "label": "URL de Callback de Autenticação" + }, + "webhookUrl": { + "label": "URL do Webhook" + }, + "addedAt": { + "label": "Data de adição" + }, + "lastEditedAt": { + "label": "Última edição em" + }, + "visibility": { + "label": "Visibilidade", + "alwaysShow": "Exibir sempre", + "alwaysHide": "Sempre ocultar", + "hideWhenEmpty": "Ocultar quando vazio" + }, + "fieldId": { + "label": "ID do campo" + } + }, + "flows": { + "title": "Fluxos", + "aiGenerateText": { + "label": "{name}: Gerar Texto com IA" + }, + "actions": { + "addContactNotes": "Adicionar Notas ao Contato", + "addContactTag": "Adicionar Tag ao Contato", + "aiAnalyzeImage": "Analisar Imagem com IA", + "aiGenerateImage": "Gerar Imagem com IA", + "aiGenerateTextAgent": "Gerar Texto com Agente IA", + "aiSpeechToText": "IA: Fala para Texto", + "aiTextToSpeech": "IA: Texto para Fala", + "archiveConversation": "Arquivar Conversa", + "assignConversation": "Atribuir Conversa", + "autoAssignConversation": "Atribuir Conversa Automaticamente", + "blockContact": "Bloquear Contato", + "broadcastActions": "Ações de Transmissão", + "chooseChannel": "Escolher Canal", + "clearCustomField": "Limpar Campo Personalizado", + "contactActions": "Ações do Contato", + "countCharacters": "Contar Caracteres", + "deleteContact": "Excluir Contato", + "emailActions": "Ações de E-mail", + "flowActions": "Ações de Fluxo", + "followConversation": "Seguir Conversa", + "formatDate": "Formatar Data", + "generateCode": "Gerar Código", + "getDataFromJson": "Obter Dados do JSON", + "inboxActions": "Ações da Caixa de Entrada", + "markEmailVerified": "Marcar E-mail como Verificado", + "openWebsite": "Abrir Site", + "optInEmail": "Aceitar E-mail", + "optOutEmail": "Recusar E-mail", + "performAction": "Executar Ação", + "removeContactTag": "Remover Tag do Contato", + "sendAudio": "Enviar Áudio", + "sendCard": "Enviar Card", + "sendExternalFlow": "Iniciar Fluxo Externo", + "sendExternalNode": "Iniciar Nó Externo", + "sendFile": "Enviar Arquivo", + "sendGif": "Enviar Gif", + "sendImage": "Enviar Imagem", + "sendMessage": "Enviar Mensagem", + "sendNode": "Iniciar Outro Nó", + "sendText": "Enviar Texto", + "sendVideo": "Enviar Vídeo", + "sendWaTemplateMessage": "Mensagem de Template", + "whatsappOptionList": "Lista de Opções", + "noTemplatesAvailable": "Nenhum template disponível", + "setCustomField": "Definir Campo Personalizado", + "startAnotherNode": "Iniciar Outro Nó", + "startExternalFlow": "Iniciar Fluxo Externo", + "startExternalNode": "Iniciar Nó Externo", + "startFlow": "Iniciar Fluxo", + "subscribeBroadcast": "Inscrever em Transmissão", + "tools": "Ferramentas", + "transferConversationToBot": "Transferir Conversa para o Bot", + "transferConversationToHuman": "Transferir Conversa para Humano", + "unarchiveConversation": "Desarquivar Conversa", + "unassignConversation": "Desatribuir Conversa", + "unfollowConversation": "Deixar de Seguir Conversa", + "unsubscribeBroadcast": "Cancelar Inscrição da Transmissão", + "getUserData": "Obter Dados do Usuário", + "sendCarousel": "Enviar Carrossel", + "actions": "Ações", + "findGif": "Encontrar Gif", + "spreadsheetGetRow": "Obter Linha", + "spreadsheetUpdateRow": "Atualizar Linha", + "spreadsheetSendData": "Enviar Dados", + "spreadsheetGetRandomRow": "Obter Linha Aleatória", + "spreadsheetClearRow": "Limpar Linha", + "typing": "Digitando", + "sequenceActions": "Ações de Sequência", + "subscribeSequence": "Inscrever em Sequência", + "unsubscribeSequence": "Cancelar Inscrição da Sequência", + "splitTraffic": "Dividir Tráfego", + "updateContactLifecycleStage": "Atualizar Ciclo de Vida do Contato" + }, + "splitTraffic": { + "balanceHint": "A soma total deve ser 100%.", + "addVariation": "Nova Variação" + }, + "whatsappOptionList": { + "buttonLabel": "Rótulo do Botão", + "optionTitle": "Título", + "optionDescription": "Descrição", + "addOption": "Adicionar Nova", + "editButtonTitle": "Editar Botão", + "editOptionTitle": "Editar Opção" + }, + "additionalSteps": "Passos adicionais", + "delayType": { + "datetimeCustomField": "Campo Personalizado de Data e Hora", + "datetimeCustomFieldValue": "{customField}", + "duration": "Duração", + "durationValue": "{duration}", + "date": "Data", + "specificDate": "Data Específica", + "specificDateValue": "{date}", + "random": "Aleatório" + }, + "formatTimezone": { + "timezone": "Fuso Horário da Conta", + "contactTimezone": "Fuso Horário do Contato" + }, + "fields": { + "selectTemplatePlaceholder": "Escolha um template do WhatsApp", + "preview": "Pré-visualização" + }, + "operators": { + "append": "Adicionar ao final", + "prepend": "Adicionar ao início", + "set": "Definir como" + }, + "wait": { + "datetimeTooltip": "Data e hora em que o fluxo será disparado.", + "setInterval": "Definir Intervalo", + "delayTypeLabel": "Este é um", + "fixed": "Fixo", + "randomized": "Aleatório", + "delay": "atraso", + "durationDetailPrefix": "Aguardar", + "dateDetailPrefix": "Aguardar até", + "customFieldDetailPrefix": "Aguardar até", + "randomDetailPrefix": "Aguardar entre", + "randomDetailAnd": "e", + "intervalDetailPrefix": "Ativo entre", + "offsetAdd": "+", + "offsetSubtract": "-", + "durationLabel": "Duração", + "activeHours": "Horas ativas", + "dateTypeLabel": "Tipo de data", + "datetimeLabel": "Data e Hora", + "customFieldLabel": "Campo personalizado", + "offset": "Adicionar deslocamento", + "offsetLabel": "Deslocamento", + "randomRangeLabel": "Intervalo aleatório", + "minLabel": "Mín", + "maxLabel": "Máx", + "unitLabel": "Unidade", + "specific": "Específico", + "dynamic": "Dinâmico" + } + }, + "folders": { + "heading": { + "title": "Pastas" + } + }, + "gemini": { + "autoReply": { + "description": "Ative a resposta automática com IA para responder naturalmente aos usuários com IA potencializada pelos seus dados de negócio.", + "label": "Resposta Automática com IA" + }, + "connect": { + "description": "Responda aos usuários naturalmente com IA potencializada pelos seus dados de negócio.", + "label": "Google Gemini" + }, + "title": "Google Gemini" + }, + "general": { + "title": "Geral" + }, + "googleSheets": { + "setting": { + "label": "Google Sheets", + "description": "Configuração da integração com Google Sheets" + }, + "header": "Cabeçalho da Coluna", + "title": "Google Sheets" + }, + "integrations": { + "title": "Integrações" + }, + "organizationSettings": { + "title": "Configurações da Organização", + "errors": { + "giphyApiKeyRequired": "A API Key é obrigatória para configurar o GIPHY.", + "giphyApiKeyInvalid": "API Key do GIPHY inválida", + "googleClientSecretRequired": "O Client Secret é obrigatório para configurar o Google.", + "googleVerifyTokenRequired": "O Token de Verificação é obrigatório para configurar o Google.", + "messengerAppSecretRequired": "O App Secret é obrigatório para configurar o Messenger.", + "zaloAppSecretRequired": "O App Secret é obrigatório para configurar o Zalo.", + "stripeSecretKeyRequired": "A Secret Key é obrigatória para configurar o Stripe.", + "whatsappAppSecretRequired": "O App Secret é obrigatório para configurar o WhatsApp.", + "whatsappSystemUserTokenRequired": "O Token do Usuário do Sistema é obrigatório para configurar o WhatsApp." + } + }, + "conversations": { + "searchInConversation": "Buscar nesta conversa", + "searchInConversationPlaceholder": "Buscar mensagens…", + "searchNoResults": "Sem resultados", + "searchPrevious": "Resultado anterior", + "searchNext": "Próximo resultado", + "composerHint": "Use '/' para snippets, '$' para variáveis, ':' para emoji", + "composerPlaceholder": "Digite uma mensagem", + "channelPrefix": "para:", + "todayLabel": "Hoje", + "yesterdayLabel": "Ontem", + "unassignedAgent": "Não atribuído", + "unnamedContact": "Contato sem nome" + }, + "inboxSideRail": { + "details": "Detalhes do contato", + "channels": "Canais", + "activities": "Atividades", + "attachments": "Anexos" + }, + "messages": { + "actions": { + "menu": "Mais opções", + "copy": "Copiar texto", + "copyLink": "Copiar link da mensagem", + "reply": "Responder", + "copied": "Mensagem copiada", + "linkCopied": "Link copiado" + }, + "tooltip": { + "sent": "Enviado", + "delivered": "Entregue", + "read": "Lido", + "received": "Recebido", + "failed": "Falha" + }, + "composer": { + "internalComment": "Comentário interno", + "addComment": "Adicionar comentário", + "exitInternal": "Voltar para mensagem", + "internalLabel": "Comentário interno", + "internalPlaceholder": "Use @ para mencionar um colega de equipe. Comentários só são visíveis para sua equipe.", + "internalHint": "Comentário visível apenas para sua equipe", + "channelHint": "Canal de envio da mensagem", + "formLabel": "Formulário de mensagem", + "textareaLabel": "Digite sua mensagem", + "emoji": "Emoji", + "emojiSearch": "Pesquisa", + "attach": "Anexar arquivo", + "snippets": "Atalhos rápidos", + "send": "Enviar mensagem" + }, + "runningWorkflows": "Workflows em andamento", + "moveFeature": "Mover {feature}", + "poweredBy": "Desenvolvido por {name}", + "noGiphyApiKey": "Nenhuma API Key do GIPHY encontrada. Adicione uma API Key do GIPHY nas configurações da organização.", + "connectedSuccess": "{feature} conectado com sucesso", + "connectFailed": "Falha ao conectar {feature}", + "connectSuccess": "{feature} conectado com sucesso", + "success": "Sucesso", + "failed": "Falhou", + "copiedToClipboard": "Copiado para a área de transferência", + "copyFailed": "Falha ao copiar para a área de transferência", + "createdFailed": "Falha na criação de {feature}", + "deletedSuccess": "{feature} excluído com sucesso", + "deleteFileComingSoon": "Funcionalidade de exclusão de arquivo em breve", + "disconnectFeature": "Desconectar {feature}", + "disconnectFeatureDescription": "Tem certeza que deseja desconectar {feature}?", + "disconnectSuccess": "{feature} desconectado com sucesso", + "failedToCopy": "Falha ao copiar", + "failedToPreviewImage": "Falha ao pré-visualizar imagem", + "loadingData": "Carregando dados...", + "leaveEmptyToKeepSecret": "Deixe em branco para manter o segredo atual", + "needToAddSettings": "Você precisa adicionar configurações para usar esta integração", + "noDataAvailable": "Nenhum dado disponível", + "savedSuccessfully": "Salvo com sucesso", + "syncedSuccessfully": "Sincronizado com sucesso", + "nameAlreadyExists": "Já existe um {feature} com este nome", + "unknownError": "Erro desconhecido", + "updatedSuccess": "{feature} atualizado com sucesso", + "updateFileComingSoon": "Funcionalidade de atualização de arquivo em breve", + "videoError": "Erro de vídeo", + "createdSuccess": "{feature} criado com sucesso", + "createFeature": "Criar {feature}", + "noItemsFound": "Nenhum item encontrado", + "editFeature": "Editar {feature}", + "duplicateFeature": "Duplicar {feature}", + "deleteFeature": "Excluir {feature}", + "deleteConfirmation": "Tem certeza absoluta?\nEsta ação não pode ser desfeita.\nIsso vai excluir permanentemente seu {feature} dos nossos servidores.", + "addFeature": "Adicionar {feature}", + "signOutConfirmation": "Tem certeza que deseja sair?", + "copyInvitationUrlSuccess": "Copie o link abaixo e envie para a pessoa que você quer adicionar como admin.", + "noAIIntegrationFound": "Nenhuma integração de IA encontrada. Conecte uma integração de IA para usar agentes de IA.", + "resendFeature": "Reenviar {feature}", + "resendFeatureDescription": "Reenviar {feature} para os contatos que ainda não receberam.", + "resendSuccess": "Reenviado com sucesso", + "duplicateTitle": "Duplicar {feature}", + "duplicateDescription": "Tem certeza que deseja duplicar {feature}?\nEsta ação não pode ser desfeita.", + "duplicateSuccess": "{feature} duplicado com sucesso", + "changeDefault": "Alterar Padrão", + "changeDefaultDescription": "Tem certeza que deseja alterar o agente de IA padrão?", + "unsetDefaultDescription": "Tem certeza que deseja remover o agente de IA padrão?", + "botIsActive": "O bot está ativo", + "selectConversationTitle": "Selecione uma conversa", + "selectConversationDescription": "Escolha uma conversa da lista à esquerda para visualizar as mensagens e começar a conversar.", + "selectConversationContactTitle": "Nenhum contato selecionado", + "selectConversationContactDescription": "Selecione uma conversa para visualizar os detalhes do contato e as informações da caixa de entrada aqui.", + "archiveFeature": "Arquivar {feature}", + "archiveFeatureDescription": "Tem certeza que deseja arquivar {feature}?", + "disableFeature": "Desativar {feature}", + "disableFeatureDescription": "Tem certeza que deseja desativar {feature}?", + "enableFeature": "Ativar {feature}", + "enableFeatureDescription": "Tem certeza que deseja ativar {feature}?", + "exportedSuccess": "{feature} exportado com sucesso", + "createSuccess": "{feature} criado com sucesso", + "deleteFailed": "Falha ao excluir {feature}", + "deleteSuccess": "{feature} excluído com sucesso", + "error": "Erro", + "moveFolderDescription": "Mover {feature} para pasta", + "noData": "Sem dados", + "featureNotFound": "{feature} não encontrado", + "enableSuccess": "{feature} ativado com sucesso", + "userInactive": "Usuário inativo há mais de 7 dias" + }, + "instagram": { + "description": "Esta integração permite criar bots no Instagram.", + "selectInstagramAccount": "Selecionar Conta do Instagram", + "iceBreakers": "Quebra-gelos", + "iceBreakersDescription": "Os quebra-gelos permitem que usuários iniciem uma conversa com seu bot através de uma lista de perguntas frequentes.", + "refreshPermissions": "Atualizar Permissões", + "title": "Instagram" + }, + "messenger": { + "description": "Esta integração permite criar bots no Messenger.", + "welcomeMessage": "Mensagem de Boas-vindas", + "welcomeMessageDescription": "Esta é a primeira mensagem que o usuário receberá do seu bot. É enviada quando o usuário interage pela primeira vez clicando no botão 'Começar'.", + "conversationStarters": "Iniciadores de Conversa", + "conversationStartersDescription": "Os iniciadores de conversa oferecem uma forma do usuário começar uma conversa com seu negócio através de uma lista de perguntas frequentes.", + "persistentMenu": "Menu Persistente", + "persistentMenuDescription": "O menu pode ser configurado para ajudar os usuários a descobrir e acessar recursos do seu bot mais facilmente. O menu está sempre disponível e deve conter as ações principais que os usuários podem executar a qualquer momento. Ele comunica as capacidades básicas do bot tanto para iniciantes quanto para usuários recorrentes.", + "dynamicPersistentMenu": "Menu Persistente Dinâmico", + "dynamicPersistentMenuDescription": "Permite alterar dinamicamente o menu persistente a qualquer momento, em nível de usuário.", + "botProfile": "Perfil do Bot", + "botProfileDescription": "Esta configuração permite definir um nome e imagem diferentes para o seu bot", + "personas": "Personas", + "personasDescription": "Esta configuração permite definir um nome e imagem diferentes para o seu bot", + "defaultPersona": "Persona Padrão", + "refreshPermissions": "Atualizar Permissões", + "reconnect": "Reconectar", + "reconnectDescription": "Sempre reconecte seu bot se o bot do Messenger não estiver respondendo ou se aparecer algum erro de permissão no log de erros.", + "selectFacebookPage": "Selecionar Página do Facebook", + "title": "Facebook Messenger" + }, + "omnichannel": { + "title": "Omnichannel" + }, + "openai": { + "connect": { + "description": "Responda aos usuários naturalmente com IA potencializada pelos seus dados de negócio." + }, + "title": "OpenAI" + }, + "settings": { + "title": "Configurações" + }, + "settingsGroups": { + "general": "Configurações gerais", + "workspace": "Workspace", + "apps": "Aplicativos", + "advanced": "Avançado", + "inbox": "Caixa de entrada", + "data": "Dados" + }, + "lifecycle": { + "title": "Ciclo de vida", + "subtitle": "Acompanhe e gerencie contatos em cada etapa do seu processo de vendas", + "learnMore": "Saiba mais", + "toggleTitle": "Mostrar/ocultar etapas do ciclo de vida", + "toggleSubtitle": "Exibir etapas do ciclo de vida na caixa de entrada e nos módulos de contatos.", + "configureTitle": "Configurar etapas do ciclo de vida", + "activeTitle": "Etapas do ciclo de vida", + "activeSubtitle": "Organize e acompanhe contatos pelas etapas do funil ativo.", + "lostTitle": "Etapas de perda", + "lostSubtitle": "Acompanhe por onde os contatos saem do funil.", + "addStage": "Adicionar etapa", + "maxStagesReached": "Máximo de {max} etapas atingido (ativas + perdidas).", + "stageNumber": "Etapa {n}", + "stageName": "Nome da etapa", + "newStage": "Nova etapa", + "defaultBadge": "Padrão", + "saved": "Etapas salvas", + "saveError": "Erro ao salvar etapas", + "saving": "Salvando…", + "emptyNameError": "Todas as etapas precisam de nome", + "emojiSearch": "Pesquisar", + "emojiCategories": { + "suggested": "Usados recentemente", + "smileys": "Carinhas e emoções", + "animals": "Animais e natureza", + "food": "Comida e bebida", + "travel": "Viagem e lugares", + "activities": "Atividades", + "objects": "Objetos", + "symbols": "Símbolos", + "flags": "Bandeiras" + }, + "deleteConfirm": "Apagar etapa \"{name}\"? Contatos nessa etapa ficarão sem etapa atribuída.", + "selectPlaceholder": "Selecionar etapa", + "noStage": "Sem etapa", + "nextStageTooltip": "Próxima etapa: {name}", + "lastStageTooltip": "Já está na última etapa do funil", + "contactLifecycle": "Etapa do contato", + "activeStages": "Etapas ativas", + "lostStages": "Etapas perdidas", + "contactUpdated": "Etapa do contato atualizada", + "contactUpdateError": "Erro ao atualizar etapa", + "bulkMove": "Mover etapa", + "bulkMoved": "{count} contato(s) movido(s)", + "deleteModalTitle": "Apagar etapa \"{name}\"", + "deleteModalDescription": "Esta etapa tem {count} contato(s). Escolha pra qual etapa migrar antes de apagar.", + "deleteModalReassignTo": "Migrar contatos para", + "deleteModalNoContacts": "Esta etapa não tem contatos. Confirme pra apagar.", + "deleteModalNoTarget": "Deixar sem etapa", + "deleteModalConfirm": "Apagar e migrar", + "deleteModalCancel": "Cancelar", + "stageBadgeLabel": "Etapa", + "activityLog": "Histórico de etapas", + "activityLogEmpty": "Sem mudanças de etapa registradas", + "menu": { + "setAsDefault": "Definir como padrão", + "delete": "Apagar" + }, + "anyStage": "Qualquer estágio", + "triggerFromStage": "De (origem)", + "triggerToStage": "Para (destino)", + "stepHint": "Atualizar a etapa do ciclo de vida do contato para uma nova etapa.", + "stepUpdateAction": "Atualizar etapa", + "stepUpdateActionHint": "Atualizar etapa do contato para outra etapa", + "stepRemoveAction": "Remover etapa", + "stepRemoveActionHint": "Remove a etapa do contato", + "stepStageLabel": "Estágio do ciclo de vida" + }, + "auth": { + "orContinueWith": "Ou continue com", + "dontHaveAnAccount": "Não tem uma conta?", + "alreadyHaveAnAccount": "Já tem uma conta?", + "signUp": "Cadastre-se", + "acceptTermsAndPolicy": "Ao clicar em continuar, você concorda com nossos", + "and": "e", + "privacyPolicy": "Política de Privacidade", + "termsOfService": "Termos de Serviço", + "signInTitle": "Entrar no {name}", + "forgotPasswordTitle": "Esqueceu sua senha?", + "forgotPasswordDescription": "Sem problemas! Vamos enviar um link com instruções para redefinir sua senha.", + "checkYourEmail": "Verifique seu e-mail", + "magicLinkSent": "Enviamos uma URL de verificação para seu e-mail", + "magicLinkSentDescription": "Clique no link no seu e-mail para continuar.", + "didNotReceiveEmail": "Não recebeu o e-mail? Verifique sua caixa de spam.", + "trySigningInAgain": "Você também pode tentar entrar novamente com um e-mail diferente.", + "signIn": "Entrar", + "signUpSuccess": "Cadastro realizado com sucesso. Verifique seu e-mail para confirmação.", + "forgotPassword": "Esqueceu a senha?", + "rememberedPassword": "Lembrou sua senha?", + "forgotPasswordEmailSent": "Enviamos um e-mail com instruções para redefinir sua senha.", + "magicLinkSentTitle": "Magic link enviado", + "passwordResetSuccess": "Senha redefinida com sucesso", + "resetPasswordTitle": "Redefinir sua senha", + "resetPasswordDescription": "Insira sua nova senha abaixo.", + "signUpTitle": "Cadastre-se em {name}" + }, + "tags": { + "title": "Etiquetas", + "subtitle": "Crie e gerencie etiquetas para organizar conversas e contatos", + "formDescription": "Configure a aparência da etiqueta — escolha emoji, cor e descrição.", + "namePlaceholder": "Ex: Lead quente", + "emojiLabel": "Emoji", + "clearEmoji": "Remover emoji", + "colorLabel": "Cor", + "descriptionLabel": "Descrição", + "descriptionPlaceholder": "Opcional — pra que serve essa etiqueta?", + "preview": "Pré-visualização", + "addTag": "Adicionar etiqueta", + "searchPlaceholder": "Pesquisar etiquetas", + "emptyState": "Nenhuma etiqueta encontrada", + "delete": { + "usageDescription": "Você está prestes a excluir {tagCount, plural, =1 {1 etiqueta} other {# etiquetas}} atribuída(s) a {contactCount, plural, =1 {1 contato} other {# contatos}}. Essa ação remove a etiqueta de todos os contatos e não pode ser desfeita.", + "confirmInputLabel": "Para confirmar, digite {count} abaixo:" + } + }, + "texts": { + "or": "ou" + }, + "theme": { + "dark": "Escuro", + "light": "Claro", + "system": "Sistema" + }, + "validation": { + "maxSize": "O tamanho do arquivo excede o limite máximo", + "maxItemsReached": "Máximo de {max} {feature} permitido por workspace", + "invalidApiKey": "API Key inválida" + }, + "webchat": { + "description": "Permita que seus clientes recebam respostas automáticas instantâneas direto do seu site", + "title": "Webchat" + }, + "whatsapp": { + "category": { + "authentication": { + "description": "Códigos OTP, verificação em 2 etapas e mensagens de autenticação.", + "label": "Autenticação" + }, + "marketing": { + "description": "Descrição da categoria Marketing", + "label": "Marketing" + }, + "utility": { + "description": "Descrição da categoria Utilidade", + "label": "Utilidade" + } + }, + "connectionType": { + "embedded": "Embedded Signup", + "manual": "Conexão manual" + }, + "list": { + "empty": "Nenhum WhatsApp conectado ainda." + }, + "templatePicker": { + "empty": "Nenhum template aprovado disponível.", + "preview": "Pré-visualização", + "sent": "Template enviado", + "title": "Enviar template", + "variables": "Variáveis" + }, + "webhook": { + "pending": "Webhook pendente", + "verified": "Webhook verificado" + }, + "windowAlert": { + "description": "A Meta não permite o envio de mensagens após 24 horas da última resposta do contato. Para continuar, envie um modelo de mensagem aprovado ou aguarde uma nova resposta do contato para voltar a enviar mensagens normalmente.", + "learnMore": "Saiba mais", + "openTemplate": "Abrir lista de templates", + "sendTemplateButton": "Enviar modelo de mensagem", + "title": "Janela de mensagens do WhatsApp fechada" + }, + "commands": { + "description": "São palavras-chave especiais que dizem ao bot do WhatsApp o que fazer", + "label": "Comandos" + }, + "connectExisting": "Conectar uma Conta Business do WhatsApp existente", + "icebreakers": { + "description": "São perguntas comuns que as pessoas podem facilmente fazer a você.", + "label": "Quebra-gelos" + }, + "manualConnect": "Conexão manual usando token de acesso do Usuário do Sistema.", + "manualOnboarding": { + "title": "Sua caixa de entrada do WhatsApp está pronta!", + "description": "Configure a URL do webhook e o token de verificação no seu Meta App. Use os valores abaixo.", + "webhookUrl": "URL do Webhook", + "verifyToken": "Token de verificação do webhook", + "qrCodeHint": "Escaneie o QR code para abrir a URL do webhook", + "moreSettings": "Mais configurações", + "goToInbox": "Ir para a caixa de entrada" + }, + "marketingMessageLite": "Marketing Message Lite", + "messageTemplate": { + "carouselImage": { + "label": "Imagem do Carrossel" + }, + "carouselVideo": { + "label": "Vídeo do Carrossel" + }, + "document": { + "label": "Documento" + }, + "image": { + "label": "Imagem" + }, + "location": { + "label": "Localização" + }, + "text": { + "label": "Texto" + }, + "video": { + "label": "Vídeo" + }, + "viewCatalog": { + "label": "Ver Catálogo" + }, + "viewProduct": { + "label": "Ver Produto" + }, + "params": { + "couponCode": "Código de Cupom", + "couponCodePlaceholder": "Insira o código do cupom", + "quickReplyPayload": "Payload de Resposta Rápida", + "quickReplyPayloadPlaceholder": "Insira o payload (opcional)", + "flowToken": "Token do Flow", + "flowTokenPlaceholder": "Insira o token do flow", + "catalogProductId": "ID do Produto", + "catalogProductIdPlaceholder": "Insira o retailer ID do produto", + "carouselCard": "Card {index}", + "limitedTimeOffer": "Hora de Expiração da Oferta", + "limitedTimeOfferPlaceholder": "Insira a hora de expiração em milissegundos", + "limitedTimeOfferHelp": "Timestamp Unix em milissegundos de quando a oferta expira", + "location": "Localização", + "latitude": "Latitude", + "longitude": "Longitude", + "locationName": "Nome do local (opcional)", + "locationAddress": "Endereço (opcional)" + } + }, + "sampleBodyCardContent": { + "label": "Conteúdo de exemplo do corpo do card" + }, + "sampleBodyContent": { + "label": "Conteúdo de exemplo do corpo" + }, + "sampleHeaderContent": { + "label": "Conteúdo de exemplo do cabeçalho" + }, + "showFooter": { + "label": "Mostrar rodapé" + }, + "tabs": { + "accountHealths": "Saúde da Conta", + "automation": "Automação", + "ecommerce": "E-commerce", + "flows": "Fluxos", + "messageTemplates": "Templates de Mensagem", + "profile": "Perfil", + "usefulLinks": "Links Úteis" + }, + "accountHealths": { + "title": "Gerencie sua conta do WhatsApp", + "description": "Revise o status da sua conta WhatsApp, limites de mensagens e qualidade. Atualize configurações ou resolva problemas se necessário", + "goToBusinessManager": "Ir para o Meta Business Manager", + "fields": { + "displayPhoneNumber": { + "label": "Número de telefone exibido", + "tooltip": "O número de telefone mostrado aos clientes quando eles enviam mensagem ao seu negócio." + }, + "businessName": { + "label": "Nome do negócio", + "tooltip": "O nome de exibição verificado do WhatsApp Business." + }, + "displayNameStatus": { + "label": "Status do nome de exibição", + "tooltip": "Status de aprovação do nome de exibição do WhatsApp Business." + }, + "qualityRating": { + "label": "Avaliação de qualidade", + "tooltip": "Avaliação de qualidade baseada no feedback de clientes nos últimos 7 dias." + }, + "messagingLimitTier": { + "label": "Nível de limite de mensagens", + "tooltip": "Número máximo de clientes únicos que seu negócio pode enviar mensagens em uma janela de 24 horas." + }, + "accountMode": { + "label": "Modo da conta", + "tooltip": "Indica se a Conta WhatsApp Business está ativa ou em modo sandbox." + }, + "marketingMessages": { + "label": "Marketing Messages (MM Lite)", + "tooltip": "Status de onboarding da Conta WhatsApp Business na API Marketing Messages Lite.", + "status": { + "INELIGIBLE_ON_BEHALF_OF_WABA": "Inelegível: modelo OBO", + "INELIGIBLE_INACTIVE_OR_RESTRICTED": "Inelegível: inativo ou restrito", + "INELIGIBLE_COUNTRY_NOT_SUPPORTED": "Inelegível: país não suportado", + "INELIGIBLE_USING_WHATSAPP_BUSINESS_APP": "Inelegível: usando WhatsApp Business app", + "ELIGIBLE": "Elegível", + "PENDING_VALID_PAYMENT_METHOD": "Aguardando método de pagamento válido", + "PENDING_INTERNAL_SETUP": "Configuração interna pendente", + "ONBOARDED": "Onboarding concluído" + }, + "description": { + "INELIGIBLE_ON_BEHALF_OF_WABA": "A Conta WhatsApp Business usa o modelo OBO, que não é suportado. Você deve primeiro transferir a propriedade da WABA para o cliente ou fazer onboarding via Intent API.", + "INELIGIBLE_INACTIVE_OR_RESTRICTED": "A Conta WhatsApp Business está inativa ou restrita de enviar mensagens devido a um problema de aplicação de política.", + "INELIGIBLE_COUNTRY_NOT_SUPPORTED": "A Conta WhatsApp Business está em uma região que não suporta a API MM Lite.", + "INELIGIBLE_USING_WHATSAPP_BUSINESS_APP": "O número de telefone do WhatsApp Business está sendo usado dentro do app WhatsApp Business.", + "ELIGIBLE": "A Conta WhatsApp Business é elegível para fazer onboarding e usar a API MM Lite.", + "PENDING_VALID_PAYMENT_METHOD": "A Conta WhatsApp Business precisa configurar um método de pagamento válido.", + "PENDING_INTERNAL_SETUP": "A Conta WhatsApp Business está sendo configurada para usar a API MM Lite e não requer mais nenhuma ação do cliente ou parceiro. O processo automatizado leva menos de dez minutos. Inscreva-se no webhook account_update e ouça o evento AD_ACCOUNT_LINKED. Abra um ticket de suporte se a configuração demorar mais de dez minutos e você não receber um webhook.", + "ONBOARDED": "A Conta WhatsApp Business fez onboarding com sucesso e está pronta para usar a API MM Lite." + } + }, + "webhookConfiguration": { + "label": "Configuração do Webhook", + "tooltip": "Status do webhook configurado para receber eventos do WhatsApp.", + "configured": "Webhook configurado com sucesso", + "notConfigured": "Webhook não configurado" + }, + "phoneNumberHealth": { + "label": "Saúde do Número de Telefone", + "tooltip": "Indica se este número pode enviar mensagens e quais problemas o afetam.", + "headline": "Número de Telefone - {status}" + }, + "wabaHealth": { + "label": "Saúde da Conta WhatsApp Business", + "tooltip": "Indica se a Conta WhatsApp Business pode enviar mensagens e quais problemas a afetam.", + "headline": "Conta WhatsApp Business (WABA ID: {id}) - {status}", + "headlineNoId": "Conta WhatsApp Business - {status}" + } + }, + "messagingLimitTier": { + "TIER_50": "50 clientes por 24h", + "TIER_250": "250 clientes por 24h", + "TIER_1K": "1.000 clientes por 24h", + "TIER_10K": "10.000 clientes por 24h", + "TIER_100K": "100.000 clientes por 24h", + "TIER_UNLIMITED": "Clientes ilimitados por 24h", + "UNKNOWN": "Desconhecido" + }, + "displayNameStatus": { + "APPROVED": "Aprovado", + "AVAILABLE_WITHOUT_REVIEW": "Disponível Sem Revisão", + "DECLINED": "Recusado", + "EXPIRED": "Expirado", + "NONE": "Nenhum", + "PENDING_REVIEW": "Aguardando Revisão" + }, + "accountMode": { + "LIVE": "Ativo", + "SANDBOX": "Sandbox" + }, + "canSendMessage": { + "AVAILABLE": "DISPONÍVEL", + "LIMITED": "LIMITADO", + "BLOCKED": "BLOQUEADO", + "UNKNOWN": "DESCONHECIDO" + }, + "health": { + "label": "Status do Negócio", + "tooltip": "Visão geral da saúde da sua conta WhatsApp e capacidade de envio de mensagens.", + "blockedMessage": "Seu número de telefone foi bloqueado de enviar mensagens.", + "problem": "Problema:", + "possibleSolution": "Solução possível:", + "noIssues": "Nenhum problema detectado", + "entityHeadline": "{type} (ID: {id})", + "entityHeadlineNoId": "{type}", + "entityTooltip": "Indica se este {type} pode enviar mensagens e quais problemas o afetam.", + "entityTypes": { + "PHONE_NUMBER": "Número de Telefone", + "WABA": "Conta WhatsApp Business", + "BUSINESS": "Negócio", + "MESSAGE_TEMPLATE": "Template de Mensagem", + "APP": "App" + } + } + }, + "templateFooter": { + "label": "Rodapé do Template" + }, + "templateHeader": { + "label": "Cabeçalho do Template" + }, + "transferPhoneNumber": "Transferir telefone de outro provedor WhatsApp" + }, + "zalo": { + "title": "Zalo OA", + "refreshPermissions": "Atualizar permissões", + "duplicated": "Esta conta Zalo OA já está conectada a outro workspace." + }, + "telegram": { + "description": "Esta integração permite criar bots no Telegram.", + "title": "Telegram" + }, + "invitation": { + "chatbotInvitationDescription1": "Entre em {chatbotName} em {organizationName}", + "chatbotInvitationDescription2": "{userName} convidou você para entrar em {chatbotName} em {organizationName}.", + "organizationInvitationDescription1": "Entre em {organizationName}", + "organizationInvitationDescription2": "{userName} convidou você para entrar em {organizationName}." + }, + "inboxTeams": { + "title": "Configurações de equipe", + "subtitle": "Criar e gerenciar equipes para agrupar os usuários em seu espaço de trabalho.", + "addTeam": "Adicionar Equipe", + "searchPlaceholder": "Pesquisar Configurações de equipe", + "manage": "Administrar", + "delete": "Apagar", + "createDialog": { + "title": "Adicionar Equipe", + "description": "Crie uma equipe para agrupar usuários. As equipes podem ser usadas no lugar de usuários para compartilhar recursos como Visualizações." + }, + "manageDialog": { + "title": "Administrar Equipe", + "description": "Gerencie a equipe renomeando-a, alterando sua descrição e adicionando ou removendo membros da equipe." + }, + "deleteDialog": { + "title": "Apagar Equipe", + "description": "Tem certeza que deseja apagar a equipe \"{name}\"? Esta ação não pode ser desfeita. Os membros não serão excluídos.", + "confirm": "Apagar" + }, + "fields": { + "name": "Nome da Equipe", + "namePlaceholder": "Adicionar o nome da equipe aqui, por exemplo, Equipe de Marketing", + "description": "Descrição da equipe", + "descriptionPlaceholder": "Adicione a descrição da equipe aqui, por exemplo, Gerencie contatos de marketing.", + "members": "Membros da equipe", + "membersPlaceholder": "Adicione um usuário aqui, por exemplo, John." + }, + "membersCount": "{count, plural, =0 {Nenhum membro} =1 {1 membro} other {# membros}}", + "save": "Guardar" + }, + "webhooks": { + "title": "Webhooks" + }, + "reflinks": { + "title": "Links de Entrada", + "description": "Links com palavras-chave que iniciam fluxos e capturam payloads de ref dos canais." + }, + "magicLinks": { + "title": "Magic Links", + "description": "Crie deep links que redirecionam para sua URL com parâmetros de query." + }, + "QRCode": { + "title": "QR Code", + "description": "Compartilhe este código para que as pessoas abram seu link de rastreamento." + }, + "trigger": { + "when": "Quando isso acontecer", + "shortcutMenu": { + "button": "Atalhos", + "title": "Disparar fluxo", + "loading": "Carregando atalhos...", + "empty": "Nenhum atalho configurado. Crie um fluxo com gatilho de atalho em Fluxos.", + "fired": "Atalho disparado", + "fireError": "Falha ao disparar o atalho" + }, + "conditions": { + "tagApplied": "Tag Aplicada", + "tagRemoved": "Tag Removida", + "dateTimeBasedTrigger": "Gatilho Baseado em Data/Hora", + "customFieldValueChanged": "Campo Personalizado Alterado", + "conversationTransferredToHuman": "Conversa transferida para humano", + "conversationTransferredToBot": "Conversa transferida para bot", + "newContact": "Novo Contato", + "contactUnsubscribedFormBroadcast": "Contato cancelou inscrição em Transmissão", + "archived": "Arquivado", + "followUp": "Follow Up", + "conversationAssigned": "Conversa Atribuída", + "conversationUnassigned": "Conversa Desatribuída", + "incomingCall": "Chamada Recebida", + "missedAudioCall": "Chamada de Áudio Perdida", + "callEnded": "Chamada Encerrada", + "ticketCreated": "Ticket Criado", + "ticketMovedToStage": "Ticket Movido para Etapa", + "ticketValueChanged": "Valor do Ticket Alterado", + "ticketStatusChanged": "Status do Ticket Alterado", + "ticketPriorityChanged": "Prioridade do Ticket Alterada", + "subscribedToSequence": "Inscrito em Sequência", + "unsubscribedFromSequence": "Cancelou Inscrição da Sequência", + "WhatsappShoppingCartSent": "Carrinho de Compras do WhatsApp Enviado", + "userAskedAboutProduct": "Usuário Perguntou Sobre Produto", + "cartAbandoned": "Carrinho Abandonado", + "newOrder": "Novo Pedido", + "orderAccepted": "Pedido Aceito", + "orderShipped": "Pedido Enviado", + "orderConcluded": "Pedido Concluído", + "orderCancelled": "Pedido Cancelado", + "categoryAddedToCart": "Categoria Adicionada ao Carrinho", + "productAddedToCart": "Produto Adicionado ao Carrinho", + "productRemovedFromCart": "Produto Removido do Carrinho", + "productOrdered": "Produto Pedido", + "contactReferredANewContact": "Contato Indicou um Novo Contato", + "contactReferredExistingContact": "Contato Indicou Contato Existente", + "lifecycleStageChanged": "Estágio do ciclo de vida alterado" + }, + "actions": { + "addTag": "Adicionar Tag", + "removeTag": "Remover Tag", + "setCustomField": "Definir Campo Personalizado", + "clearCustomField": "Limpar Campo Personalizado", + "startAnotherFlow": "Iniciar Outro Fluxo", + "notifyAdmins": "Notificar Admins", + "transferConversationToHuman": "Transferir Conversa para Humano" + }, + "triggerGoesOff": "Gatilho dispara", + "before": "Antes", + "after": "Depois", + "atTheDayOf": "No dia de", + "day": "Dia", + "hour": "Hora", + "minute": "Minuto", + "at": "Às", + "flowNode": { + "helperText": "Todo fluxo começa com um gatilho. Quando o evento configurado acontece, este fluxo é disparado automaticamente para o contato.", + "typeLabel": "Tipo de gatilho", + "typePlaceholder": "Selecione um gatilho", + "noFilters": "Sem filtros — dispara em qualquer evento deste tipo. Para refinar, mude o tipo ou use um passo de condição depois do gatilho.", + "anyOption": "Qualquer", + "conversationOpened": { + "label": "Conversa aberta", + "hint": "Dispara quando uma conversa é aberta com o contato (nova ou reaberta após arquivamento)." + }, + "conversationClosed": { + "label": "Conversa fechada", + "hint": "Dispara quando uma conversa é encerrada." + }, + "contactFieldUpdated": { + "label": "Campo de contato atualizado", + "hint": "Dispara quando qualquer campo de contato é alterado.", + "fieldsLabel": "Campos que disparam o gatilho", + "empty": "Nenhum campo personalizado configurado neste workspace." + }, + "contactTagUpdated": { + "label": "Etiqueta de contato atualizada", + "hint": "Dispara quando uma etiqueta é adicionada ou removida do contato.", + "actionLabel": "Ação na etiqueta", + "actionAny": "Qualquer (adicionada ou removida)", + "actionAdded": "Quando etiqueta for adicionada", + "actionRemoved": "Quando etiqueta for removida", + "tagsLabel": "Etiquetas que disparam o gatilho", + "empty": "Nenhuma etiqueta cadastrada neste workspace." + }, + "shortcut": { + "label": "Atalho", + "hint": "Dispara quando um atendente clica num atalho dentro do Inbox.", + "iconLabel": "Ícone (emoji)", + "iconHint": "Aparece no botão do atalho dentro do Inbox.", + "labelLabel": "Texto do botão", + "labelPlaceholder": "Ex: Reenviar oferta", + "note": "Este gatilho não dispara automaticamente. Ele aparece como um botão no Inbox para o atendente clicar quando quiser rodar este fluxo manualmente." + }, + "lifecycleStageChanged": { + "label": "Ciclo de vida atualizado", + "hint": "Dispara quando a etapa do ciclo de vida do contato muda.", + "toStagesLabel": "Para etapas (destino)", + "fromStagesLabel": "De etapas (origem)", + "triggerOnCleared": "Disparar também quando a etapa for removida (limpa)", + "loading": "Carregando etapas...", + "emptyStages": "Nenhuma etapa de ciclo de vida configurada neste workspace." + } + } + }, + "errorLogs": { + "title": "Logs de Erro" + }, + "developerAccessToken": { + "title": "Token de Acesso da API ChatbotX", + "description": "Gere um token para permitir que seu workspace acesse as APIs do ChatbotX. Mantenha este token em segredo e não compartilhe com ninguém." + }, + "contacts": { + "title": "Contatos", + "fieldsTitle": "Campos de contatos", + "admin": "Administrar", + "details": "Detalhes do contato", + "channels": "Canais", + "tagsTitle": "Etiquetas", + "ownerLabel": "Administrador", + "noTags": "Sem etiquetas", + "addTag": "Adicionar etiqueta", + "searchTag": "Pesquisar e selecionar tags", + "hiddenFields": "Campos de contato ocultos", + "delete": { + "usageDescription": "Você vai excluir {contactCount, plural, one {# contato} other {# contatos}} com {tagCount, plural, one {# etiqueta} other {# etiquetas}} e {conversationCount, plural, one {# conversa} other {# conversas}} vinculadas. Mensagens, notas e eventos relacionados também serão removidos. Digite o total para confirmar.", + "confirmInputLabel": "Digite {count, number} para confirmar" + }, + "columnsPicker": { + "button": "Colunas", + "title": "Exibir colunas", + "showAll": "Mostrar todas", + "hideAll": "Ocultar todas" + }, + "unmerge": { + "loading": "Carregando contatos fundidos…", + "bannerTitle": "{count, plural, one {# contato fundido aqui} other {# contatos fundidos aqui}}", + "warning": "Desfazer a fusão restaura o contato. Conversas, etiquetas e campos personalizados que foram transferidos continuam neste contato principal — mova de volta manualmente se necessário.", + "button": "Desfazer fusão", + "successToast": "Contato restaurado" + }, + "visibility": { + "customize": "Personalizar visualização", + "title": "Personalizar visualização", + "description": "Personalize a visibilidade dos campos de contato no painel direito do Inbox. A alteração será aplicada a todos os usuários do espaço de trabalho.", + "fieldHeader": "Campo de contato", + "visibilityHeader": "Visibilidade", + "showAlways": "Exibir sempre", + "alwaysHide": "Sempre ocultar", + "saved": "Visualização atualizada" + }, + "column": { + "channels": "Canal(ais)", + "country": "País/região", + "language": "Idioma", + "conversationStatus": "Status da conversa", + "lastMessage": "Última mensagem", + "statusOpen": "Abertas", + "statusClosed": "Fechadas" + }, + "merge": { + "action": "Mesclar contatos", + "title": "Mesclar contatos", + "description": "Escolha qual contato fica como principal. Os outros serão mesclados sob ele (tags, campos personalizados e conversas migram pro principal) e depois excluídos.", + "selectPrimary": "Contato principal", + "unnamed": "sem nome", + "warning": "{count} duplicata(s) será(ão) excluída(s) após a mesclagem. Esta ação não pode ser desfeita.", + "confirm": "Mesclar", + "success": "{count} contato(s) mesclado(s)" + }, + "sidebar": { + "all": "Todos os contatos", + "blocked": "Contatos bloqueados" + }, + "drawer": { + "title": "Detalhes do contato", + "openInInbox": "Abrir no Inbox" + }, + "placeholders": { + "addFirstName": "Adicione um nome", + "addLastName": "Adicione um sobrenome", + "addEmail": "Adicione um e-mail", + "selectAssignee": "Selecione um responsável" + } + }, + "auditLogs": { + "title": "Logs de Atividade", + "subtitle": "Histórico completo de ações realizadas no espaço de trabalho.", + "searchPlaceholder": "Pesquisar por detalhe, ação ou usuário", + "filterUser": "Todos os usuários", + "filterUserAll": "Todos os usuários", + "filterAction": "Todas as ações", + "filterActionAll": "Todas as ações", + "exportCsv": "Exportar CSV", + "exportSuccess": "Logs exportados com sucesso.", + "exportError": "Falha ao exportar logs.", + "empty": "Nenhuma atividade registrada ainda.", + "totalRows": "{count, plural, =0 {Nenhum registro} =1 {1 registro} other {# registros}}", + "columns": { + "user": "Usuário", + "action": "Ação", + "detail": "Detalhe", + "when": "Data/Hora" + } + }, + "customFields": { + "title": "Campos Personalizados", + "subtitle": "Crie e gerencie campos personalizados para enriquecer os perfis dos seus contatos", + "addValue": "Adicione um valor para {field}", + "valuesLabel": "Opções", + "valuesPlaceholder": "Digite e Enter para adicionar", + "emptyOptions": "Sem opções configuradas", + "defaultsTitle": "Campos padrão do contato", + "defaultsTooltip": "Esses 7 campos vêm do sistema e estão disponíveis em todo contato. Não podem ser editados ou removidos — apenas a visibilidade no Inbox pode ser ajustada em \"Personalizar visualização\".", + "defaultsLockTooltip": "Campo padrão — não editável" + }, + "keywords": { + "title": "Palavras-chave" + }, + "triggers": { + "title": "Gatilhos" + }, + "users": { + "title": "Usuários" + }, + "organizationUsers": { + "pageTitle": "Configurações de administrador", + "pageSubtitle": "Gerencie os usuários da sua organização aqui.", + "addButton": "Adicionar Usuário", + "searchPlaceholder": "Pesquisar usuários", + "empty": "Nenhum usuário encontrado.", + "edit": "Alterar", + "delete": "Apagar", + "addTitle": "Adicionar Usuário", + "addDescription": "Defina o acesso desse usuário à organização e aos workspaces.", + "addSubmit": "Adicionar", + "created": "Usuário adicionado.", + "editTitle": "Editar Usuário", + "editDescription": "Atualize o acesso desse usuário à organização e aos workspaces.", + "editSubmit": "Guardar", + "updated": "Usuário atualizado.", + "deleteTitle": "Apagar Usuário", + "deleteConfirm": "Tem certeza que deseja apagar {name}? Esta ação não pode ser desfeita.", + "deleteSubmit": "Apagar", + "deleted": "Usuário apagado.", + "orgAccess": "Acesso da Organização", + "workspaceAccess": "Acesso ao workspace", + "selectWorkspace": "Selecionar workspace", + "selectRole": "Selecionar acesso", + "addWorkspace": "Adicionar workspace", + "noWorkspaces": "Nenhum workspace atribuído.", + "orgRoles": { + "owner": "Proprietário", + "manager": "Administrador", + "agent": "Membro" + }, + "wsRoles": { + "owner": "Proprietário", + "manager": "Administrador", + "agent": "Representante" + } + }, + "plans": { + "title": "Planos de Faturamento" + }, + "subscriptions": { + "title": "Assinaturas" + }, + "sequences": { + "title": "Sequências", + "subscribers": "Inscritos", + "step": "Mensagem", + "addStep": "Adicionar Mensagem", + "addFirstStep": "Adicionar Primeira Mensagem", + "noSteps": "Nenhuma mensagem ainda. Adicione sua primeira mensagem para começar.", + "selectFlow": "Selecionar Fluxo", + "confirmDeleteStep": "Tem certeza que deseja excluir esta mensagem?", + "delayUnits": { + "immediate": "Imediatamente", + "minutes": "Minutos", + "hours": "Horas", + "days": "Dias", + "specificTime": "Horário Específico" + }, + "timeValidation": "O horário da mensagem deve ser depois da mensagem anterior", + "validation": { + "exception": "Exceção de Validação", + "nameExists": "Já existe uma sequência com esse nome" + }, + "sendLabel": "Enviar", + "selectFlowFirst": "Selecione um fluxo antes de ativar a mensagem", + "invalidTimeRange": "O horário de início deve ser antes do horário de término", + "schedule": "Agenda", + "scheduleDescription": "Configure quando as mensagens devem ser enviadas", + "sendTime": "Horário de envio", + "sendTimeDescription": "As mensagens serão enviadas apenas neste intervalo de horário", + "timezone": "Fuso horário", + "timezoneDescription": "Todos os horários estão no fuso horário do seu chatbot", + "enableSchedule": "Ativar agenda", + "startTime": "Horário de início", + "endTime": "Horário de término", + "allDay": "Dia todo", + "customTime": "Horário personalizado", + "enrollmentSettings": "Configurações de Inscrição", + "enrollmentDescription": "Controle como os contatos são inscritos nesta sequência", + "allowReEnrollment": "Permitir reinscrição", + "allowReEnrollmentDescription": "Contatos podem ser inscritos múltiplas vezes", + "skipWeekends": "Pular finais de semana", + "skipWeekendsDescription": "Não enviar mensagens aos sábados e domingos", + "unenrollOnReply": "Cancelar inscrição ao responder", + "unenrollOnReplyDescription": "Remover contato da sequência quando ele responder", + "performance": "Desempenho", + "enrolled": "Inscritos", + "completed": "Concluídos", + "openRate": "Taxa de abertura", + "clickRate": "Taxa de clique", + "replyRate": "Taxa de resposta", + "unsubscribeRate": "Taxa de cancelamento", + "overview": "Visão Geral", + "analytics": "Análises", + "stats": { + "sent": "Enviado", + "delivered": "Entregue", + "seen": "Visualizado", + "clicked": "Clicado", + "failed": "Falhou", + "noContacts": "Nenhum contato encontrado", + "pageInfo": "Página {page} de {pageCount}" + }, + "settings": "Configurações", + "flow": "Fluxo", + "active": "Ativo", + "messageSentAtLeast": "Esta mensagem será enviada pelo menos", + "afterPreviousMessage": "depois da mensagem anterior (dia = 24h)", + "anyTime": "Qualquer horário", + "setTimeRange": "Definir intervalo de horário", + "selectDays": "Selecionar dias", + "allDays": "Todos os Dias", + "deselectAll": "Desmarcar Todos", + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday": "Sábado", + "sunday": "Domingo", + "afterText": "Depois" + }, + "tools": { + "title": "Ferramentas" + }, + "aiProviders": { + "openai": "OpenAI", + "gemini": "Gemini", + "claude": "Claude", + "deepseek": "DeepSeek" + }, + "smtp": { + "setting": { + "description": "Envie e-mails usando seu servidor SMTP.", + "label": "(E-mail) SMTP" + }, + "errors": { + "connectionFailed": "Não foi possível conectar ao servidor SMTP. Verifique suas credenciais e tente novamente." + } + }, + "botSimulator": { + "title": "Simulador de Bot", + "description": "Simule o comportamento do seu bot em ambiente de tempo real." + }, + "pollManager": { + "title": "Gerenciador de Enquetes", + "description": "Crie e gerencie enquetes para seus contatos." + }, + "placesNearMe": { + "title": "Locais Próximos a Mim", + "description": "Encontre locais próximos aos seus contatos." + }, + "questionnaires": { + "title": "Questionários", + "description": "Crie e gerencie questionários para seus contatos." + }, + "facebookCommentAutomation": { + "title": "Automação de Comentários no Facebook", + "description": "Automatize comentários no Facebook para seus contatos." + }, + "facebookLeadAdsAutomation": { + "title": "Automação de Lead Ads do Facebook", + "description": "Automatize lead ads do Facebook para seus contatos." + }, + "ecommerce": { + "title": "E-commerce", + "description": "Crie e gerencie sua loja de e-commerce." + }, + "appointmentScheduling": { + "title": "Agendamento de Compromissos", + "description": "Crie e gerencie seu agendamento de compromissos." + }, + "templates": { + "title": "Templates", + "description": "Crie e gerencie seus templates." + }, + "qrCodeGenerator": { + "title": "Gerador de QR Code", + "description": "Crie e gerencie seus QR codes." + }, + "flowEditor": { + "startBadge": "Início", + "copySuffix": "Cópia", + "addNode": "Adicionar bloco", + "zoomIn": "Aumentar zoom", + "zoomOut": "Diminuir zoom", + "fitView": "Centralizar fluxo" + }, + "conversationFilter": { + "all": "Todos", + "human": "Humano", + "bot": "Bot" + }, + "inboxFilters": { + "all": "Todos", + "mine": "Minhas", + "unassigned": "Não atribuídas", + "teamInbox": "Caixa de entrada de equipe", + "collapseSidebar": "Recolher barra lateral", + "expandSidebar": "Expandir barra lateral", + "newMessageIndicator": "Nova mensagem", + "blockedContacts": "Contatos bloqueados" + }, + "inboxDisplayFilter": { + "showLabel": "Mostrar", + "sortLabel": "Classificar por", + "advanced": "Filtros avançados", + "noReply": "Sem resposta", + "show": { + "all": "Todas", + "open": "Abertas", + "closed": "Encerradas" + }, + "sort": { + "recent": "Recentes", + "oldest": "Antigos" + }, + "searchContact": "Pesquisar contato" + }, + "snippets": { + "title": "Trechos", + "subtitle": "Crie e gerencie respostas reutilizáveis para agilizar suas conversas", + "feature": "Trecho", + "addButton": "Adicionar Snippet", + "createTitle": "Adicionar Snippet", + "createConfirm": "Criar", + "searchPlaceholder": "Pesquisar Trechos", + "nameLabel": "Nome", + "namePlaceholder": "Nomeie seu Snippet (ex: saudação1)", + "shortcutLabel": "Atalho", + "shortcutPlaceholder": "ex: ola", + "topicsLabel": "Tópicos", + "topicsPlaceholder": "Adicionar tópico (ex: vendas, suporte)", + "topicsLimitReached": "Limite de tópicos atingido", + "filesLabel": "Anexos", + "addFile": "Adicionar arquivo", + "filesLimitReached": "Limite de arquivos atingido", + "messageLabel": "Mensagem", + "messagePlaceholder": "Digite a mensagem do snippet aqui", + "formDescription": "Trechos são respostas pré-escritas reutilizáveis. Use \"/\" no composer para inseri-las nas conversas.", + "deleteTitle": "Excluir Trecho", + "deleteDescription": "Tem certeza que deseja excluir o trecho \"{name}\"? Esta ação não pode ser desfeita." + }, + "botFields": { + "title": "Campos do bot", + "subtitle": "Variáveis globais do workspace usadas em fluxos e mensagens automatizadas" + }, + "closingNotes": { + "title": "Notas de Fechamento", + "subtitle": "Configure resumo + categoria que atendente preenche ao encerrar uma conversa no Inbox.", + "modeTitle": "Modo de operação", + "modeSubtitle": "Decide se atendente é obrigado a categorizar e/ou resumir a conversa antes de fechar.", + "modeSaved": "Modo atualizado", + "savingMode": "Salvando modo…", + "categoriesTitle": "Categorias de conversa", + "categoriesSubtitle": "Lista de categorias disponíveis pro atendente escolher ao fechar (ex.: Resolvido, Sem resposta, Spam).", + "addCategory": "Adicionar categoria", + "editCategoryTitle": "Editar categoria", + "categoryFormDescription": "Categoria fica disponível ao atendente no diálogo de fechamento da conversa.", + "namePlaceholder": "Ex.: Resolvido", + "descriptionPlaceholder": "Quando usar esta categoria (opcional)", + "categoryCreated": "Categoria criada", + "categoryUpdated": "Categoria atualizada", + "categoryDeleted": "Categoria removida", + "emptyCategories": "Nenhuma categoria ainda. Adicione a primeira pra atendente categorizar conversas ao fechar.", + "deleteCategoryTitle": "Excluir categoria", + "confirmDelete": "Excluir a categoria \"{name}\"? Conversas que usavam esta categoria ficarão sem ela.", + "mode": { + "disabled": "Desabilitado", + "disabledDesc": "Atendente fecha a conversa direto, sem dialog.", + "optional": "Diálogo opcional", + "optionalDesc": "Abre dialog ao fechar mas atendente pode pular sem preencher.", + "mandatoryDialog": "Categoria obrigatória", + "mandatoryDialogDesc": "Abre dialog, atendente precisa escolher uma categoria. Resumo opcional.", + "mandatoryBoth": "Categoria + Resumo obrigatórios", + "mandatoryBothDesc": "Abre dialog, atendente precisa escolher categoria E escrever resumo da conversa." + }, + "dialog": { + "title": "Fechar conversa", + "description": { + "optional": "Categorize e/ou resuma esta conversa. Ambos os campos são opcionais.", + "mandatoryDialog": "Escolha uma categoria antes de fechar esta conversa.", + "mandatoryBoth": "Escolha uma categoria e escreva um resumo curto antes de fechar." + }, + "categoryLabel": "Categoria", + "categoryPlaceholder": "Selecione uma categoria", + "summaryLabel": "Resumo", + "summaryPlaceholder": "Descreva rapidamente o que aconteceu nesta conversa…", + "noCategories": "Nenhuma categoria cadastrada. Peça pro admin adicionar categorias em Configurações → Notas de Fechamento.", + "skip": "Pular e fechar", + "confirm": "Fechar conversa", + "successToast": "Conversa fechada" + } + } +} diff --git a/apps/builder/messages/vi.json b/apps/builder/messages/vi.json index 65f1c8b61..5ece5eb8a 100644 --- a/apps/builder/messages/vi.json +++ b/apps/builder/messages/vi.json @@ -19,6 +19,8 @@ "archive": "Lưu trữ", "unarchive": "Bỏ lưu trữ", "archiveConversation": "Lưu trữ cuộc trò chuyện", + "closeConversation": "Đóng", + "openConversation": "Mở", "assign": "Gán", "assignConversation": "Gán cuộc trò chuyện", "back": "Quay lại", @@ -109,15 +111,96 @@ "deactivate": "Tắt", "onFailure": "Khi thất bại", "onSkip": "Khi bỏ qua", - "onSuccess": "Khi thành công" + "onSuccess": "Khi thành công", + "trigger": "Trigger", + "close": "Đóng", + "remove": "Xóa" }, "states": { "success": "Thành công", "error": "Lỗi", - "skip": "Bỏ qua" + "skip": "Bỏ qua", + "closed": "Đã đóng", + "blocked": "Đã chặn" }, "admins": { - "title": "Quản trị viên" + "title": "Cài đặt người dùng", + "subtitle": "Quản lý và mời thành viên vào Không gian làm việc của bạn.", + "addUser": "Thêm Người dùng", + "searchPlaceholder": "Tìm trong Cài đặt người dùng", + "revoke": "Thu hồi", + "edit": "Sửa", + "roles": { + "owner": "Chủ sở hữu", + "manager": "Quản trị viên", + "agent": "Đại diện" + }, + "lastSeen": "Hoạt động gần nhất", + "inviteDialog": { + "title": "Thêm Người dùng", + "description": "Nhập địa chỉ email và chọn cấp truy cập để mời người dùng mới vào Không gian làm việc của bạn.", + "emailLabel": "Địa chỉ Email", + "emailPlaceholder": "user@company.com", + "accessLevelLabel": "Cấp truy cập", + "submit": "Thêm" + }, + "editDialog": { + "title": "Chỉnh sửa Người dùng", + "description": "Để chỉnh sửa người dùng này, chọn cấp truy cập mới và bấm \"Sửa\". Cấp truy cập xác định những gì người dùng có thể làm.", + "submit": "Cập nhật" + }, + "revokeDialog": { + "title": "Thu hồi truy cập", + "description": "Nếu bạn thu hồi người dùng này, tất cả các liên hệ được gán cho họ sẽ bị bỏ gán. Bạn có chắc muốn thu hồi quyền truy cập của người dùng này?", + "confirm": "Thu hồi" + }, + "roleDescriptions": { + "owner": "Chủ sở hữu có thể truy cập tất cả các mô-đun và có toàn quyền kiểm soát mà không có hạn chế.", + "manager": "Quản trị viên có quyền truy cập tất cả các mô-đun bao gồm Cài đặt. Tuy nhiên, họ không thể mời/xóa quản trị viên khác hoặc xóa kênh.", + "agent": "Đại diện chỉ có quyền truy cập tin nhắn trong không gian làm việc. Tùy chọn, bạn có thể giới hạn khả năng xem trong Liên hệ." + }, + "advancedRestrictions": "Hạn chế nâng cao", + "managerRestrictions": { + "restrictDataExport": { + "label": "Hạn chế Xuất Dữ liệu", + "description": "Người dùng này sẽ không thể xuất dữ liệu từ không gian làm việc." + }, + "restrictContactDeletion": { + "label": "Hạn chế Xóa Liên hệ", + "description": "Người dùng này sẽ không thể xóa bất kỳ liên hệ nào." + }, + "restrictWorkspaceSettings": { + "label": "Hạn chế truy cập Cài đặt Workspace", + "description": "Người dùng này sẽ không thể truy cập cài đặt không gian làm việc." + }, + "restrictIntegrationSettings": { + "label": "Hạn chế truy cập Cài đặt Tích hợp", + "description": "Người dùng này sẽ không thể truy cập cài đặt tích hợp." + } + }, + "agentRestrictions": { + "contactVisibility": { + "label": "Hạn chế Hiển thị Liên hệ", + "description": "Người dùng này chỉ xem liên hệ được gán cho", + "options": { + "all": "tất cả", + "team": "bất kỳ ai trong nhóm", + "self": "người dùng" + } + }, + "restrictCalling": { + "label": "Hạn chế Khả năng Gọi", + "description": "Người dùng này không thể nhận hoặc thực hiện cuộc gọi." + }, + "restrictWorkflows": { + "label": "Nút Hạn chế Luồng công việc", + "description": "Nút Luồng công việc trong mô-đun Tin nhắn sẽ bị tắt cho người dùng này. Bao gồm Phím tắt và dừng Luồng cho Liên hệ." + }, + "maskPhoneAndEmail": { + "label": "Che Số điện thoại và Địa chỉ Email", + "description": "Người dùng này sẽ không thể xem số điện thoại và email của tất cả Liên hệ." + } + } }, "assignAdmin": { "assignConversation": "Gán cuộc trò chuyện", @@ -215,6 +298,9 @@ "label": "Phản hồi tự động" } }, + "automation": { + "title": "Tự động hóa" + }, "billing": { "title": "Thanh toán" }, @@ -315,9 +401,93 @@ "channels": { "title": "Kênh" }, + "organization": { + "manage": "Quản lý Tổ chức" + }, + "personalSettings": { + "title": "Cài đặt cá nhân", + "account": "Tài khoản", + "profile": "Hồ sơ", + "notifications": "Cài đặt thông báo", + "activityStatus": { + "sectionLabel": "Trạng thái của tôi", + "available": "Sẵn sàng", + "busy": "Bận", + "offline": "Ngoại tuyến", + "updateSuccess": "Đã cập nhật trạng thái thành {status}", + "updateError": "Không thể cập nhật trạng thái" + }, + "profileSection": { + "title": "Hồ sơ", + "subtitle": "Quản lý hồ sơ và chi tiết đăng nhập của bạn.", + "changeAvatar": "Đổi ảnh", + "avatarUpdated": "Đã cập nhật ảnh.", + "firstNameLabel": "Tên", + "lastNameLabel": "Họ", + "languageLabel": "Ngôn ngữ", + "changePassword": "Đổi mật khẩu", + "saveProfile": "Lưu Hồ sơ", + "savedSuccess": "Hồ sơ đã cập nhật.", + "twoFA": { + "title": "Xác thực hai yếu tố (2FA)", + "toggle": "Bật xác thực hai yếu tố (2FA)" + }, + "darkMode": { + "title": "Chế độ tối", + "system": "Dùng cài đặt hệ thống", + "light": "Tắt", + "dark": "Bật" + } + }, + "notificationsSection": { + "title": "Cài đặt thông báo", + "subtitle": "Chọn cách bạn nhận thông báo workspace." + } + }, + "manageWorkspaces": { + "sidebarTitle": "Workspaces", + "title": "Workspaces", + "subtitle": "Quản lý các workspace của tổ chức.", + "addWorkspace": "Thêm Workspace", + "searchPlaceholder": "Tìm workspace", + "open": "Mở", + "rename": "Đổi tên", + "delete": "Xóa", + "empty": "Chưa có workspace nào.", + "columns": { + "name": "Tên", + "members": "Người dùng", + "createdAt": "Ngày tạo", + "actions": "Hành động" + }, + "renameDialog": { + "title": "Đổi tên Workspace", + "description": "Cập nhật tên workspace.", + "nameLabel": "Tên Workspace", + "submit": "Lưu", + "success": "Đã đổi tên workspace." + }, + "deleteDialog": { + "title": "Xóa Workspace", + "description": "Xóa workspace \"{name}\" sẽ xóa vĩnh viễn tất cả liên hệ, kênh, trường và dữ liệu liên quan. Hành động không thể hoàn tác.", + "confirmLabel": "Nhập \"{name}\" để xác nhận", + "submit": "Xóa", + "success": "Đã xóa workspace." + } + }, "workspaces": { "list": { "title": "Danh sách workspace" + }, + "searchPlaceholder": "Tìm kiếm dự án", + "addWorkspace": "Thêm Workspace", + "createDialog": { + "title": "Thêm Workspace", + "description": "Tạo một workspace mới để tách kênh, liên hệ, nhóm và cài đặt.", + "nameLabel": "Tên Workspace", + "namePlaceholder": "Ví dụ: Bán hàng, Thu nợ, Hỗ trợ", + "submit": "Tạo", + "success": "Đã tạo workspace \"{name}\"." } }, "workspaceToken": { @@ -715,11 +885,15 @@ "isDefault": { "label": "Đánh dấu làm mặc định" }, + "country": { + "label": "Quốc gia" + }, "language": { "description": "Liên hệ được gán ngôn ngữ mặc định khi ngôn ngữ liên hệ không xác định.", "english": "Tiếng Anh", "label": "Ngôn ngữ", "placeholder": "Chọn ngôn ngữ", + "portuguese": "Tiếng Bồ Đào Nha (Brazil)", "vietnamese": "Tiếng Việt" }, "lastName": { @@ -973,6 +1147,12 @@ "datetime": { "label": "Ngày giờ" }, + "time": { + "label": "Giờ" + }, + "list": { + "label": "Danh sách (đa chọn)" + }, "boolean": { "label": "Boolean", "placeholder": "Chọn đúng hoặc sai", @@ -1249,6 +1429,21 @@ }, "webhookUrl": { "label": "URL webhook" + }, + "addedAt": { + "label": "Ngày thêm" + }, + "lastEditedAt": { + "label": "Lần chỉnh sửa cuối" + }, + "visibility": { + "label": "Hiển thị", + "alwaysShow": "Luôn hiển thị", + "alwaysHide": "Luôn ẩn", + "hideWhenEmpty": "Ẩn khi rỗng" + }, + "fieldId": { + "label": "ID trường" } }, "flows": { @@ -1327,7 +1522,8 @@ "subscribeSequence": "Đăng ký chăm sóc", "unsubscribeSequence": "Hủy đăng ký chăm sóc", "noTemplatesAvailable": "No templates available", - "splitTraffic": "Chia lưu lượng" + "splitTraffic": "Chia lưu lượng", + "updateContactLifecycleStage": "Cập nhật giai đoạn vòng đời" }, "splitTraffic": { "balanceHint": "Tổng phần trăm phải bằng 100%.", @@ -1439,7 +1635,59 @@ "whatsappSystemUserTokenRequired": "System User Token là bắt buộc để cấu hình WhatsApp." } }, + "conversations": { + "searchInConversation": "Tìm trong cuộc trò chuyện", + "searchInConversationPlaceholder": "Tìm tin nhắn…", + "searchNoResults": "Không có kết quả", + "searchPrevious": "Trước", + "searchNext": "Sau", + "composerHint": "Dùng '/' cho snippets, '$' cho biến, ':' cho emoji", + "composerPlaceholder": "Nhập tin nhắn", + "channelPrefix": "đến:", + "todayLabel": "Hôm nay", + "yesterdayLabel": "Hôm qua", + "unassignedAgent": "Chưa được phân công", + "unnamedContact": "Liên hệ chưa đặt tên" + }, + "inboxSideRail": { + "details": "Chi tiết liên hệ", + "channels": "Kênh", + "activities": "Hoạt động", + "attachments": "Tệp đính kèm" + }, "messages": { + "actions": { + "menu": "Tùy chọn khác", + "copy": "Sao chép", + "copyLink": "Sao chép liên kết", + "reply": "Trả lời", + "copied": "Đã sao chép", + "linkCopied": "Đã sao chép liên kết" + }, + "tooltip": { + "sent": "Đã gửi", + "delivered": "Đã chuyển", + "read": "Đã đọc", + "received": "Đã nhận", + "failed": "Thất bại" + }, + "composer": { + "internalComment": "Ghi chú nội bộ", + "addComment": "Thêm bình luận", + "exitInternal": "Quay lại tin nhắn", + "internalLabel": "Ghi chú nội bộ", + "internalPlaceholder": "Dùng @ để nhắc đồng đội. Ghi chú chỉ nhóm của bạn thấy.", + "internalHint": "Ghi chú chỉ nhóm của bạn thấy", + "channelHint": "Kênh dùng để gửi tin nhắn", + "formLabel": "Biểu mẫu tin nhắn", + "textareaLabel": "Nhập tin nhắn", + "emoji": "Biểu tượng cảm xúc", + "emojiSearch": "Tìm kiếm", + "attach": "Đính kèm tệp", + "snippets": "Trả lời nhanh", + "send": "Gửi tin nhắn" + }, + "runningWorkflows": "Quy trình đang chạy", "moveFeature": "Di chuyển {feature}", "poweredBy": "Được cung cấp bởi {name}", "noGiphyApiKey": "Không tìm thấy khóa API GIPHY. Vui lòng thêm khóa API GIPHY trong cài đặt tổ chức.", @@ -1551,6 +1799,83 @@ "settings": { "title": "Cài đặt" }, + "settingsGroups": { + "general": "Cài đặt chung", + "workspace": "Workspace", + "apps": "Ứng dụng", + "advanced": "Nâng cao", + "inbox": "Hộp thư", + "data": "Dữ liệu" + }, + "lifecycle": { + "title": "Vòng đời", + "subtitle": "Theo dõi và quản lý liên hệ qua các giai đoạn của quy trình bán hàng", + "learnMore": "Tìm hiểu", + "toggleTitle": "Hiện/ẩn giai đoạn vòng đời", + "toggleSubtitle": "Hiển thị giai đoạn vòng đời trong hộp thư và liên hệ.", + "configureTitle": "Cấu hình các giai đoạn vòng đời", + "activeTitle": "Giai đoạn vòng đời", + "activeSubtitle": "Tổ chức và theo dõi liên hệ qua các giai đoạn của phễu.", + "lostTitle": "Giai đoạn thất bại", + "lostSubtitle": "Theo dõi nơi liên hệ rời khỏi phễu.", + "addStage": "Thêm giai đoạn", + "maxStagesReached": "Đã đạt tối đa {max} giai đoạn (đang hoạt động + đã mất).", + "stageNumber": "Giai đoạn {n}", + "stageName": "Tên giai đoạn", + "newStage": "Giai đoạn mới", + "defaultBadge": "Mặc định", + "saved": "Đã lưu", + "saveError": "Lỗi khi lưu", + "saving": "Đang lưu…", + "emptyNameError": "Tất cả các giai đoạn cần có tên", + "emojiSearch": "Tìm", + "emojiCategories": { + "suggested": "Đã dùng", + "smileys": "Mặt cười & cảm xúc", + "animals": "Động vật & thiên nhiên", + "food": "Đồ ăn & thức uống", + "travel": "Du lịch & địa điểm", + "activities": "Hoạt động", + "objects": "Đồ vật", + "symbols": "Ký hiệu", + "flags": "Cờ" + }, + "deleteConfirm": "Xóa giai đoạn \"{name}\"?", + "selectPlaceholder": "Chọn giai đoạn", + "noStage": "Không có giai đoạn", + "nextStageTooltip": "Giai đoạn tiếp theo: {name}", + "lastStageTooltip": "Đã ở giai đoạn cuối", + "contactLifecycle": "Vòng đời liên hệ", + "activeStages": "Giai đoạn hoạt động", + "lostStages": "Giai đoạn đã mất", + "contactUpdated": "Đã cập nhật", + "contactUpdateError": "Lỗi", + "bulkMove": "Di chuyển", + "bulkMoved": "{count} liên hệ đã di chuyển", + "deleteModalTitle": "Xóa \"{name}\"", + "deleteModalDescription": "Giai đoạn này có {count} liên hệ.", + "deleteModalReassignTo": "Di chuyển đến", + "deleteModalNoContacts": "Không có liên hệ.", + "deleteModalNoTarget": "Bỏ trống", + "deleteModalConfirm": "Xóa và di chuyển", + "deleteModalCancel": "Hủy", + "stageBadgeLabel": "Giai đoạn", + "activityLog": "Lịch sử giai đoạn", + "activityLogEmpty": "Không có thay đổi", + "menu": { + "setAsDefault": "Đặt làm mặc định", + "delete": "Xóa" + }, + "anyStage": "Bất kỳ giai đoạn nào", + "triggerFromStage": "Từ (gốc)", + "triggerToStage": "Đến (đích)", + "stepHint": "Cập nhật giai đoạn vòng đời của liên hệ sang giai đoạn mới.", + "stepUpdateAction": "Cập nhật giai đoạn", + "stepUpdateActionHint": "Cập nhật giai đoạn của liên hệ sang giai đoạn khác", + "stepRemoveAction": "Xóa giai đoạn", + "stepRemoveActionHint": "Xóa giai đoạn của liên hệ", + "stepStageLabel": "Giai đoạn vòng đời" + }, "auth": { "orContinueWith": "Hoặc tiếp tục với", "dontHaveAnAccount": "Chưa có tài khoản?", @@ -1580,7 +1905,23 @@ "signUpTitle": "Đăng ký vào {name}" }, "tags": { - "title": "Thẻ" + "title": "Thẻ", + "subtitle": "Tạo và quản lý thẻ để sắp xếp cuộc trò chuyện và liên hệ", + "formDescription": "Cấu hình giao diện thẻ — chọn emoji, màu sắc và mô tả.", + "namePlaceholder": "Ví dụ: Khách tiềm năng", + "emojiLabel": "Biểu tượng", + "clearEmoji": "Xóa biểu tượng", + "colorLabel": "Màu", + "descriptionLabel": "Mô tả", + "descriptionPlaceholder": "Tùy chọn — thẻ này dùng để làm gì?", + "preview": "Xem trước", + "addTag": "Thêm nhãn", + "searchPlaceholder": "Tìm kiếm nhãn", + "emptyState": "Không tìm thấy nhãn", + "delete": { + "usageDescription": "Bạn sắp xóa {tagCount, plural, =1 {1 thẻ} other {# thẻ}} đã được gán cho {contactCount, plural, =1 {1 liên hệ} other {# liên hệ}}. Hành động này sẽ gỡ thẻ khỏi tất cả các liên hệ và không thể hoàn tác.", + "confirmInputLabel": "Để xác nhận, hãy nhập {count} bên dưới:" + } }, "texts": { "or": "hoặc" @@ -1601,6 +1942,10 @@ }, "whatsapp": { "category": { + "authentication": { + "description": "Mã OTP, xác minh 2 bước và tin nhắn xác thực.", + "label": "Xác thực" + }, "marketing": { "description": "Mô tả danh mục Marketing", "label": "Marketing" @@ -1610,6 +1955,31 @@ "label": "Tiện ích" } }, + "connectionType": { + "embedded": "Embedded Signup", + "manual": "Kết nối thủ công" + }, + "list": { + "empty": "Chưa có WhatsApp nào được kết nối." + }, + "templatePicker": { + "empty": "Không có mẫu nào được phê duyệt.", + "preview": "Xem trước", + "sent": "Đã gửi mẫu", + "title": "Gửi mẫu", + "variables": "Biến" + }, + "webhook": { + "pending": "Webhook chờ xử lý", + "verified": "Webhook đã xác minh" + }, + "windowAlert": { + "description": "Meta không cho phép gửi tin nhắn sau 24 giờ kể từ lần phản hồi cuối của liên hệ. Để tiếp tục, hãy gửi một mẫu tin nhắn đã được phê duyệt hoặc chờ phản hồi mới từ liên hệ để tiếp tục gửi tin nhắn bình thường.", + "learnMore": "Tìm hiểu thêm", + "openTemplate": "Mở danh sách mẫu", + "sendTemplateButton": "Gửi mẫu tin nhắn", + "title": "Cửa sổ nhắn tin WhatsApp đã đóng" + }, "commands": { "description": "Đây là những từ khóa đặc biệt cho bot WhatsApp biết phải làm gì", "label": "Lệnh" @@ -1831,7 +2201,35 @@ "organizationInvitationDescription2": "{userName} đã mời bạn tham gia {organizationName}." }, "inboxTeams": { - "title": "Nhóm hộp thư" + "title": "Cài đặt nhóm", + "subtitle": "Tạo và quản lý các nhóm để gom người dùng trong Không gian làm việc của bạn.", + "addTeam": "Thêm Nhóm", + "searchPlaceholder": "Tìm trong Cài đặt nhóm", + "manage": "Quản lý", + "delete": "Xóa", + "createDialog": { + "title": "Thêm Nhóm", + "description": "Tạo một Nhóm để gom người dùng. Nhóm có thể được dùng thay cho người dùng để chia sẻ tài nguyên như Chế độ xem." + }, + "manageDialog": { + "title": "Quản lý Nhóm", + "description": "Quản lý nhóm bằng cách đổi tên, thay đổi mô tả và thêm hoặc xóa thành viên." + }, + "deleteDialog": { + "title": "Xóa Nhóm", + "description": "Bạn có chắc muốn xóa nhóm \"{name}\"? Hành động này không thể hoàn tác. Thành viên sẽ không bị xóa.", + "confirm": "Xóa" + }, + "fields": { + "name": "Tên Nhóm", + "namePlaceholder": "Nhập tên nhóm tại đây, ví dụ Nhóm Marketing", + "description": "Mô tả Nhóm", + "descriptionPlaceholder": "Nhập mô tả nhóm tại đây, ví dụ Quản lý liên hệ marketing.", + "members": "Thành viên Nhóm", + "membersPlaceholder": "Thêm người dùng tại đây, ví dụ John." + }, + "membersCount": "{count, plural, =0 {Không có thành viên} =1 {1 thành viên} other {# thành viên}}", + "save": "Lưu" }, "trigger": { "when": "Khi điều này xảy ra", @@ -1871,7 +2269,8 @@ "productRemovedFromCart": "Sản phẩm xóa khỏi giỏ", "productOrdered": "Sản phẩm đã đặt", "contactReferredANewContact": "Liên hệ giới thiệu liên hệ mới", - "contactReferredExistingContact": "Liên hệ giới thiệu liên hệ hiện có" + "contactReferredExistingContact": "Liên hệ giới thiệu liên hệ hiện có", + "lifecycleStageChanged": "Giai đoạn vòng đời đã thay đổi" }, "actions": { "addTag": "Thêm thẻ", @@ -1889,23 +2288,175 @@ "day": "Ngày", "hour": "Giờ", "minute": "Phút", - "at": "Lúc" + "at": "Lúc", + "shortcutMenu": { + "button": "Phím tắt", + "title": "Kích hoạt luồng", + "loading": "Đang tải...", + "empty": "Chưa có phím tắt nào.", + "fired": "Đã kích hoạt", + "fireError": "Không thể kích hoạt" + }, + "flowNode": { + "helperText": "Mọi luồng đều bắt đầu với một trigger. Khi sự kiện được cấu hình xảy ra, luồng này tự động được kích hoạt cho liên hệ.", + "typeLabel": "Loại trigger", + "typePlaceholder": "Chọn một trigger", + "noFilters": "Không có bộ lọc — kích hoạt trên mọi sự kiện loại này.", + "anyOption": "Bất kỳ", + "conversationOpened": { + "label": "Cuộc trò chuyện mở", + "hint": "Kích hoạt khi mở cuộc trò chuyện (mới hoặc mở lại)." + }, + "conversationClosed": { + "label": "Cuộc trò chuyện đóng", + "hint": "Kích hoạt khi cuộc trò chuyện đóng." + }, + "contactFieldUpdated": { + "label": "Trường liên hệ cập nhật", + "hint": "Kích hoạt khi trường thay đổi.", + "fieldsLabel": "Trường kích hoạt", + "empty": "Không có trường tùy chỉnh." + }, + "contactTagUpdated": { + "label": "Thẻ liên hệ cập nhật", + "hint": "Kích hoạt khi thẻ được thêm/xóa.", + "actionLabel": "Hành động trên thẻ", + "actionAny": "Bất kỳ (thêm hoặc xóa)", + "actionAdded": "Khi thẻ được thêm", + "actionRemoved": "Khi thẻ bị xóa", + "tagsLabel": "Thẻ kích hoạt", + "empty": "Không có thẻ." + }, + "shortcut": { + "label": "Phím tắt", + "hint": "Kích hoạt khi nhân viên nhấn phím tắt.", + "iconLabel": "Biểu tượng (emoji)", + "iconHint": "Hiển thị trên nút phím tắt trong Inbox.", + "labelLabel": "Văn bản nút", + "labelPlaceholder": "VD: Gửi lại ưu đãi", + "note": "Trigger này không kích hoạt tự động. Nó hiển thị như một nút trong Inbox." + }, + "lifecycleStageChanged": { + "label": "Giai đoạn vòng đời thay đổi", + "hint": "Kích hoạt khi giai đoạn thay đổi.", + "toStagesLabel": "Đến giai đoạn", + "fromStagesLabel": "Từ giai đoạn", + "triggerOnCleared": "Cũng kích hoạt khi giai đoạn bị xóa", + "loading": "Đang tải...", + "emptyStages": "Không có giai đoạn." + } + } }, "developerAccessToken": { "title": "Token truy cập API ChatbotX", "description": "Tạo token để cho phép workspace của bạn truy cập API ChatbotX. Giữ token này bí mật và không chia sẻ với ai." }, "contacts": { - "title": "Liên hệ" + "title": "Liên hệ", + "fieldsTitle": "Trường liên hệ", + "admin": "Quản lý", + "details": "Chi tiết liên hệ", + "channels": "Kênh", + "tagsTitle": "Thẻ", + "ownerLabel": "Quản trị viên", + "noTags": "Không có thẻ", + "addTag": "Thêm thẻ", + "searchTag": "Tìm và chọn thẻ", + "hiddenFields": "Trường liên hệ ẩn", + "delete": { + "usageDescription": "Bạn sắp xóa {contactCount, plural, one {# liên hệ} other {# liên hệ}} với {tagCount, plural, one {# thẻ} other {# thẻ}} và {conversationCount, plural, one {# cuộc trò chuyện} other {# cuộc trò chuyện}}. Tin nhắn, ghi chú và sự kiện liên quan cũng sẽ bị xóa. Nhập tổng số để xác nhận.", + "confirmInputLabel": "Nhập {count, number} để xác nhận" + }, + "columnsPicker": { + "button": "Cột", + "title": "Hiển thị cột", + "showAll": "Hiện tất cả", + "hideAll": "Ẩn tất cả" + }, + "unmerge": { + "loading": "Đang tải danh sách hợp nhất…", + "bannerTitle": "{count, plural, one {# liên hệ đã được hợp nhất} other {# liên hệ đã được hợp nhất}}", + "warning": "Khôi phục liên hệ đã hợp nhất chỉ phục hồi thẻ liên hệ. Cuộc trò chuyện, thẻ và trường tùy chỉnh được chuyển sẽ ở lại liên hệ chính — chuyển lại thủ công nếu cần.", + "button": "Hoàn tác hợp nhất", + "successToast": "Liên hệ đã được khôi phục" + }, + "visibility": { + "customize": "Tùy chỉnh hiển thị", + "title": "Tùy chỉnh hiển thị", + "description": "Tùy chỉnh khả năng hiển thị của các trường liên hệ ở khung phải Inbox. Thay đổi áp dụng cho mọi người trong workspace.", + "fieldHeader": "Trường liên hệ", + "visibilityHeader": "Hiển thị", + "showAlways": "Luôn hiển thị", + "alwaysHide": "Luôn ẩn", + "saved": "Đã cập nhật hiển thị" + }, + "column": { + "channels": "Kênh", + "country": "Quốc gia/khu vực", + "language": "Ngôn ngữ", + "conversationStatus": "Trạng thái hội thoại", + "lastMessage": "Tin nhắn cuối", + "statusOpen": "Mở", + "statusClosed": "Đã đóng" + }, + "merge": { + "action": "Hợp nhất liên hệ", + "title": "Hợp nhất liên hệ", + "description": "Chọn liên hệ chính. Các liên hệ khác sẽ được hợp nhất bên dưới (thẻ, trường tùy chỉnh và hội thoại chuyển sang chính) rồi bị xóa.", + "selectPrimary": "Liên hệ chính", + "unnamed": "chưa đặt tên", + "warning": "{count} bản trùng sẽ bị xóa sau khi hợp nhất. Không thể hoàn tác.", + "confirm": "Hợp nhất", + "success": "Đã hợp nhất {count} liên hệ" + }, + "sidebar": { + "all": "Tất cả liên hệ", + "blocked": "Liên hệ bị chặn" + }, + "drawer": { + "title": "Chi tiết liên hệ", + "openInInbox": "Mở trong hộp thư" + }, + "placeholders": { + "addFirstName": "Thêm tên", + "addLastName": "Thêm họ", + "addEmail": "Thêm email", + "selectAssignee": "Chọn người phụ trách" + } }, "errorLogs": { "title": "Nhật ký lỗi" }, "auditLogs": { - "title": "Nhật ký kiểm tra" + "title": "Nhật ký hoạt động", + "subtitle": "Lịch sử đầy đủ các hành động đã thực hiện trong không gian làm việc.", + "searchPlaceholder": "Tìm theo chi tiết, hành động hoặc người dùng", + "filterUser": "Tất cả người dùng", + "filterUserAll": "Tất cả người dùng", + "filterAction": "Tất cả hành động", + "filterActionAll": "Tất cả hành động", + "exportCsv": "Xuất CSV", + "exportSuccess": "Xuất nhật ký thành công.", + "exportError": "Lỗi xuất nhật ký.", + "empty": "Chưa có hoạt động nào.", + "totalRows": "{count, plural, =0 {Không có bản ghi} =1 {1 bản ghi} other {# bản ghi}}", + "columns": { + "user": "Người dùng", + "action": "Hành động", + "detail": "Chi tiết", + "when": "Thời gian" + } }, "customFields": { - "title": "Trường tùy chỉnh" + "title": "Trường tùy chỉnh", + "subtitle": "Tạo và quản lý các trường tùy chỉnh để làm phong phú hồ sơ liên hệ", + "addValue": "Thêm giá trị cho {field}", + "valuesLabel": "Tùy chọn", + "valuesPlaceholder": "Nhập và nhấn Enter để thêm", + "emptyOptions": "Chưa cấu hình tùy chọn", + "defaultsTitle": "Trường liên hệ mặc định", + "defaultsTooltip": "7 trường này thuộc hệ thống và có sẵn cho mọi liên hệ. Không thể sửa hoặc xóa — chỉ có thể điều chỉnh hiển thị trong Inbox qua \"Tùy chỉnh hiển thị\".", + "defaultsLockTooltip": "Trường mặc định — không thể sửa" }, "keywords": { "title": "Từ khóa" @@ -2015,6 +2566,43 @@ "tools": { "title": "Công cụ" }, + "organizationUsers": { + "pageTitle": "Cài đặt quản trị viên", + "pageSubtitle": "Quản lý người dùng tổ chức của bạn tại đây.", + "addButton": "Thêm người dùng", + "searchPlaceholder": "Tìm kiếm người dùng", + "empty": "Không tìm thấy người dùng.", + "edit": "Chỉnh sửa", + "delete": "Xóa", + "addTitle": "Thêm người dùng", + "addDescription": "Đặt quyền truy cập của người dùng vào tổ chức và workspace.", + "addSubmit": "Thêm", + "created": "Đã thêm người dùng.", + "editTitle": "Chỉnh sửa người dùng", + "editDescription": "Cập nhật quyền truy cập của người dùng vào tổ chức và workspace.", + "editSubmit": "Lưu", + "updated": "Đã cập nhật người dùng.", + "deleteTitle": "Xóa người dùng", + "deleteConfirm": "Bạn có chắc muốn xóa {name}? Hành động này không thể hoàn tác.", + "deleteSubmit": "Xóa", + "deleted": "Đã xóa người dùng.", + "orgAccess": "Quyền truy cập tổ chức", + "workspaceAccess": "Quyền truy cập workspace", + "selectWorkspace": "Chọn workspace", + "selectRole": "Chọn quyền", + "addWorkspace": "Thêm workspace", + "noWorkspaces": "Chưa có workspace nào được gán.", + "orgRoles": { + "owner": "Chủ sở hữu", + "manager": "Quản trị viên", + "agent": "Thành viên" + }, + "wsRoles": { + "owner": "Chủ sở hữu", + "manager": "Quản trị viên", + "agent": "Đại diện" + } + }, "plans": { "title": "Billing Plans" }, @@ -2082,5 +2670,106 @@ "QRCode": { "title": "QR Code", "description": "Share this code so people open your tracking link." + }, + "inboxFilters": { + "all": "Tất cả", + "mine": "Của tôi", + "unassigned": "Chưa gán", + "teamInbox": "Hộp thư nhóm", + "collapseSidebar": "Thu gọn thanh bên", + "expandSidebar": "Mở rộng thanh bên", + "newMessageIndicator": "Tin nhắn mới", + "blockedContacts": "Liên hệ bị chặn" + }, + "inboxDisplayFilter": { + "showLabel": "Hiển thị", + "sortLabel": "Sắp xếp", + "advanced": "Bộ lọc nâng cao", + "noReply": "Chưa trả lời", + "show": { + "all": "Tất cả", + "open": "Đang mở", + "closed": "Đã đóng" + }, + "sort": { + "recent": "Mới nhất", + "oldest": "Cũ nhất" + }, + "searchContact": "Tìm liên hệ" + }, + "snippets": { + "title": "Đoạn mẫu", + "subtitle": "Tạo và quản lý các câu trả lời tái sử dụng để tăng tốc cuộc trò chuyện", + "feature": "Đoạn mẫu", + "addButton": "Thêm đoạn mẫu", + "createTitle": "Thêm đoạn mẫu", + "createConfirm": "Tạo", + "searchPlaceholder": "Tìm đoạn mẫu", + "nameLabel": "Tên", + "namePlaceholder": "Đặt tên đoạn mẫu (ví dụ: chao1)", + "shortcutLabel": "Phím tắt", + "shortcutPlaceholder": "ví dụ: chao", + "topicsLabel": "Chủ đề", + "topicsPlaceholder": "Thêm chủ đề (ví dụ: bán hàng, hỗ trợ)", + "topicsLimitReached": "Đạt giới hạn chủ đề", + "filesLabel": "Tệp đính kèm", + "addFile": "Thêm tệp", + "filesLimitReached": "Đạt giới hạn tệp", + "messageLabel": "Tin nhắn", + "messagePlaceholder": "Nhập nội dung đoạn mẫu tại đây", + "formDescription": "Đoạn mẫu là các câu trả lời tái sử dụng. Dùng \"/\" trong khung soạn để chèn vào cuộc trò chuyện.", + "deleteTitle": "Xoá đoạn mẫu", + "deleteDescription": "Bạn có chắc muốn xoá đoạn mẫu \"{name}\"? Thao tác này không thể hoàn tác." + }, + "botFields": { + "title": "Trường bot", + "subtitle": "Biến toàn cục của workspace dùng trong luồng và tin nhắn tự động" + }, + "closingNotes": { + "title": "Ghi chú khi đóng", + "subtitle": "Cấu hình tóm tắt + danh mục mà nhân viên điền khi đóng cuộc trò chuyện trong Inbox.", + "modeTitle": "Chế độ hoạt động", + "modeSubtitle": "Quyết định xem nhân viên có bắt buộc phải phân loại và/hoặc tóm tắt cuộc trò chuyện trước khi đóng hay không.", + "modeSaved": "Đã cập nhật chế độ", + "savingMode": "Đang lưu chế độ…", + "categoriesTitle": "Danh mục cuộc trò chuyện", + "categoriesSubtitle": "Danh sách danh mục nhân viên có thể chọn khi đóng (ví dụ: Đã giải quyết, Không phản hồi, Spam).", + "addCategory": "Thêm danh mục", + "editCategoryTitle": "Sửa danh mục", + "categoryFormDescription": "Danh mục sẽ có sẵn cho nhân viên trong hộp thoại đóng cuộc trò chuyện.", + "namePlaceholder": "Ví dụ: Đã giải quyết", + "descriptionPlaceholder": "Khi nào dùng danh mục này (tùy chọn)", + "categoryCreated": "Đã tạo danh mục", + "categoryUpdated": "Đã cập nhật danh mục", + "categoryDeleted": "Đã xóa danh mục", + "emptyCategories": "Chưa có danh mục. Thêm danh mục đầu tiên để nhân viên có thể phân loại cuộc trò chuyện khi đóng.", + "deleteCategoryTitle": "Xóa danh mục", + "confirmDelete": "Xóa danh mục \"{name}\"? Các cuộc trò chuyện đã dùng danh mục này sẽ không còn nó.", + "mode": { + "disabled": "Tắt", + "disabledDesc": "Nhân viên đóng cuộc trò chuyện trực tiếp, không có hộp thoại.", + "optional": "Hộp thoại tùy chọn", + "optionalDesc": "Hộp thoại mở khi đóng nhưng nhân viên có thể bỏ qua không cần điền gì.", + "mandatoryDialog": "Bắt buộc danh mục", + "mandatoryDialogDesc": "Hộp thoại mở, nhân viên phải chọn một danh mục. Tóm tắt tùy chọn.", + "mandatoryBoth": "Bắt buộc danh mục + tóm tắt", + "mandatoryBothDesc": "Hộp thoại mở, nhân viên phải chọn danh mục VÀ viết tóm tắt cuộc trò chuyện." + }, + "dialog": { + "title": "Đóng cuộc trò chuyện", + "description": { + "optional": "Phân loại và/hoặc tóm tắt cuộc trò chuyện này. Cả hai trường đều tùy chọn.", + "mandatoryDialog": "Chọn một danh mục trước khi đóng cuộc trò chuyện này.", + "mandatoryBoth": "Chọn một danh mục và viết tóm tắt ngắn trước khi đóng." + }, + "categoryLabel": "Danh mục", + "categoryPlaceholder": "Chọn một danh mục", + "summaryLabel": "Tóm tắt", + "summaryPlaceholder": "Mô tả ngắn gọn điều đã xảy ra trong cuộc trò chuyện này…", + "noCategories": "Chưa đăng ký danh mục nào. Yêu cầu quản trị viên thêm danh mục trong Cài đặt → Ghi chú khi đóng.", + "skip": "Bỏ qua và đóng", + "confirm": "Đóng cuộc trò chuyện", + "successToast": "Đã đóng cuộc trò chuyện" + } } } diff --git a/apps/builder/next.config.ts b/apps/builder/next.config.ts index d1128238b..2949633b7 100644 --- a/apps/builder/next.config.ts +++ b/apps/builder/next.config.ts @@ -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: [ @@ -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 [ { diff --git a/apps/builder/package.json b/apps/builder/package.json index 9dd79a089..99392412a 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -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", @@ -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", @@ -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", diff --git a/apps/builder/public/brand/icon-ai-agent.svg b/apps/builder/public/brand/icon-ai-agent.svg new file mode 100644 index 000000000..a296e385d --- /dev/null +++ b/apps/builder/public/brand/icon-ai-agent.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/builder/public/brand/icon-flow.svg b/apps/builder/public/brand/icon-flow.svg new file mode 100644 index 000000000..1b1e4411f --- /dev/null +++ b/apps/builder/public/brand/icon-flow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/builder/public/brand/x1_black.png b/apps/builder/public/brand/x1_black.png new file mode 100644 index 000000000..25af93ea5 Binary files /dev/null and b/apps/builder/public/brand/x1_black.png differ diff --git a/apps/builder/public/brand/x1_white.png b/apps/builder/public/brand/x1_white.png new file mode 100644 index 000000000..231d1bab5 Binary files /dev/null and b/apps/builder/public/brand/x1_white.png differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/1.webp b/apps/builder/public/respond-avatars/amber-lighten-1/1.webp new file mode 100644 index 000000000..78c455f63 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/1.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/10.webp b/apps/builder/public/respond-avatars/amber-lighten-1/10.webp new file mode 100644 index 000000000..fc64baa0f Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/10.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/11.webp b/apps/builder/public/respond-avatars/amber-lighten-1/11.webp new file mode 100644 index 000000000..0eef11cad Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/11.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/12.webp b/apps/builder/public/respond-avatars/amber-lighten-1/12.webp new file mode 100644 index 000000000..2d1af5cec Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/12.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/2.webp b/apps/builder/public/respond-avatars/amber-lighten-1/2.webp new file mode 100644 index 000000000..2f6f8c5a3 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/2.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/3.webp b/apps/builder/public/respond-avatars/amber-lighten-1/3.webp new file mode 100644 index 000000000..7d7ea17a4 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/3.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/4.webp b/apps/builder/public/respond-avatars/amber-lighten-1/4.webp new file mode 100644 index 000000000..056f120b2 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/4.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/5.webp b/apps/builder/public/respond-avatars/amber-lighten-1/5.webp new file mode 100644 index 000000000..048fadc10 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/5.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/6.webp b/apps/builder/public/respond-avatars/amber-lighten-1/6.webp new file mode 100644 index 000000000..89c9a6a46 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/6.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/7.webp b/apps/builder/public/respond-avatars/amber-lighten-1/7.webp new file mode 100644 index 000000000..7189a66a0 Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/7.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/8.webp b/apps/builder/public/respond-avatars/amber-lighten-1/8.webp new file mode 100644 index 000000000..075ccab3e Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/8.webp differ diff --git a/apps/builder/public/respond-avatars/amber-lighten-1/9.webp b/apps/builder/public/respond-avatars/amber-lighten-1/9.webp new file mode 100644 index 000000000..5a985974b Binary files /dev/null and b/apps/builder/public/respond-avatars/amber-lighten-1/9.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/1.webp b/apps/builder/public/respond-avatars/blue-darken-1/1.webp new file mode 100644 index 000000000..8470c1983 Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/1.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/10.webp b/apps/builder/public/respond-avatars/blue-darken-1/10.webp new file mode 100644 index 000000000..fa27c354c Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/10.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/11.webp b/apps/builder/public/respond-avatars/blue-darken-1/11.webp new file mode 100644 index 000000000..8c4022cb0 Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/11.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/12.webp b/apps/builder/public/respond-avatars/blue-darken-1/12.webp new file mode 100644 index 000000000..277a797ef Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/12.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/2.webp b/apps/builder/public/respond-avatars/blue-darken-1/2.webp new file mode 100644 index 000000000..752d23c0b Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/2.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/3.webp b/apps/builder/public/respond-avatars/blue-darken-1/3.webp new file mode 100644 index 000000000..54dc56cdb Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/3.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/4.webp b/apps/builder/public/respond-avatars/blue-darken-1/4.webp new file mode 100644 index 000000000..090a365ab Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/4.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/5.webp b/apps/builder/public/respond-avatars/blue-darken-1/5.webp new file mode 100644 index 000000000..73aff2adc Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/5.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/6.webp b/apps/builder/public/respond-avatars/blue-darken-1/6.webp new file mode 100644 index 000000000..00ee60b69 Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/6.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/7.webp b/apps/builder/public/respond-avatars/blue-darken-1/7.webp new file mode 100644 index 000000000..b3bc5597b Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/7.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/8.webp b/apps/builder/public/respond-avatars/blue-darken-1/8.webp new file mode 100644 index 000000000..fc278821a Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/8.webp differ diff --git a/apps/builder/public/respond-avatars/blue-darken-1/9.webp b/apps/builder/public/respond-avatars/blue-darken-1/9.webp new file mode 100644 index 000000000..e35f19520 Binary files /dev/null and b/apps/builder/public/respond-avatars/blue-darken-1/9.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/1.webp b/apps/builder/public/respond-avatars/brown-lighten-2/1.webp new file mode 100644 index 000000000..9f087e08d Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/10.webp b/apps/builder/public/respond-avatars/brown-lighten-2/10.webp new file mode 100644 index 000000000..82791d6b6 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/11.webp b/apps/builder/public/respond-avatars/brown-lighten-2/11.webp new file mode 100644 index 000000000..bc7588179 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/12.webp b/apps/builder/public/respond-avatars/brown-lighten-2/12.webp new file mode 100644 index 000000000..1a108aaf1 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/2.webp b/apps/builder/public/respond-avatars/brown-lighten-2/2.webp new file mode 100644 index 000000000..79bbb866c Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/3.webp b/apps/builder/public/respond-avatars/brown-lighten-2/3.webp new file mode 100644 index 000000000..60640f311 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/4.webp b/apps/builder/public/respond-avatars/brown-lighten-2/4.webp new file mode 100644 index 000000000..9b569bb20 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/5.webp b/apps/builder/public/respond-avatars/brown-lighten-2/5.webp new file mode 100644 index 000000000..588e6df7e Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/6.webp b/apps/builder/public/respond-avatars/brown-lighten-2/6.webp new file mode 100644 index 000000000..1e383e5fb Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/7.webp b/apps/builder/public/respond-avatars/brown-lighten-2/7.webp new file mode 100644 index 000000000..92049115c Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/8.webp b/apps/builder/public/respond-avatars/brown-lighten-2/8.webp new file mode 100644 index 000000000..9b6c10729 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/brown-lighten-2/9.webp b/apps/builder/public/respond-avatars/brown-lighten-2/9.webp new file mode 100644 index 000000000..61e45d8a2 Binary files /dev/null and b/apps/builder/public/respond-avatars/brown-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/1.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/1.webp new file mode 100644 index 000000000..0584dfc76 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/1.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/10.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/10.webp new file mode 100644 index 000000000..f3b3bc711 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/10.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/11.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/11.webp new file mode 100644 index 000000000..d55095742 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/11.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/12.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/12.webp new file mode 100644 index 000000000..06ecbfd9b Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/12.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/2.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/2.webp new file mode 100644 index 000000000..3d0de415f Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/2.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/3.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/3.webp new file mode 100644 index 000000000..5a6cb0b81 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/3.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/4.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/4.webp new file mode 100644 index 000000000..061c87656 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/4.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/5.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/5.webp new file mode 100644 index 000000000..f27131e3b Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/5.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/6.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/6.webp new file mode 100644 index 000000000..dfc274b04 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/6.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/7.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/7.webp new file mode 100644 index 000000000..dc8471573 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/7.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/8.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/8.webp new file mode 100644 index 000000000..512d0641c Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/8.webp differ diff --git a/apps/builder/public/respond-avatars/deep-orange-lighten-1/9.webp b/apps/builder/public/respond-avatars/deep-orange-lighten-1/9.webp new file mode 100644 index 000000000..aa3a1f9eb Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-orange-lighten-1/9.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/1.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/1.webp new file mode 100644 index 000000000..90dbd144a Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/10.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/10.webp new file mode 100644 index 000000000..14ec4ab1c Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/11.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/11.webp new file mode 100644 index 000000000..367146a32 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/12.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/12.webp new file mode 100644 index 000000000..e0d44783c Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/2.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/2.webp new file mode 100644 index 000000000..5133f181f Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/3.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/3.webp new file mode 100644 index 000000000..3369d1d30 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/4.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/4.webp new file mode 100644 index 000000000..5695fed77 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/5.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/5.webp new file mode 100644 index 000000000..bac3b97f9 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/6.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/6.webp new file mode 100644 index 000000000..5dd29b252 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/7.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/7.webp new file mode 100644 index 000000000..0d5c26144 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/8.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/8.webp new file mode 100644 index 000000000..b4f4a14c8 Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/deep-purple-lighten-2/9.webp b/apps/builder/public/respond-avatars/deep-purple-lighten-2/9.webp new file mode 100644 index 000000000..d5928cb4c Binary files /dev/null and b/apps/builder/public/respond-avatars/deep-purple-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/1.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/1.webp new file mode 100644 index 000000000..4055226ff Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/10.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/10.webp new file mode 100644 index 000000000..5f6a0e803 Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/11.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/11.webp new file mode 100644 index 000000000..967f6513e Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/12.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/12.webp new file mode 100644 index 000000000..ce0085fee Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/2.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/2.webp new file mode 100644 index 000000000..b4d09e747 Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/3.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/3.webp new file mode 100644 index 000000000..c8f9cd8e9 Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/4.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/4.webp new file mode 100644 index 000000000..fad0b2945 Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/5.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/5.webp new file mode 100644 index 000000000..302dcdceb Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/6.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/6.webp new file mode 100644 index 000000000..8eef4498d Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/7.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/7.webp new file mode 100644 index 000000000..ce7025671 Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/8.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/8.webp new file mode 100644 index 000000000..d70aeada9 Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/indigo-lighten-2/9.webp b/apps/builder/public/respond-avatars/indigo-lighten-2/9.webp new file mode 100644 index 000000000..7555dd7ae Binary files /dev/null and b/apps/builder/public/respond-avatars/indigo-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/1.webp b/apps/builder/public/respond-avatars/light-green-darken-2/1.webp new file mode 100644 index 000000000..8e5a07968 Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/10.webp b/apps/builder/public/respond-avatars/light-green-darken-2/10.webp new file mode 100644 index 000000000..57c0d36ce Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/11.webp b/apps/builder/public/respond-avatars/light-green-darken-2/11.webp new file mode 100644 index 000000000..5aee534c5 Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/12.webp b/apps/builder/public/respond-avatars/light-green-darken-2/12.webp new file mode 100644 index 000000000..f694cc6d7 Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/2.webp b/apps/builder/public/respond-avatars/light-green-darken-2/2.webp new file mode 100644 index 000000000..19d3f059f Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/3.webp b/apps/builder/public/respond-avatars/light-green-darken-2/3.webp new file mode 100644 index 000000000..59156034e Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/4.webp b/apps/builder/public/respond-avatars/light-green-darken-2/4.webp new file mode 100644 index 000000000..bc5446702 Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/5.webp b/apps/builder/public/respond-avatars/light-green-darken-2/5.webp new file mode 100644 index 000000000..73131d5c6 Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/6.webp b/apps/builder/public/respond-avatars/light-green-darken-2/6.webp new file mode 100644 index 000000000..a0e3eb813 Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/7.webp b/apps/builder/public/respond-avatars/light-green-darken-2/7.webp new file mode 100644 index 000000000..7d927ff5f Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/8.webp b/apps/builder/public/respond-avatars/light-green-darken-2/8.webp new file mode 100644 index 000000000..7e571176e Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/light-green-darken-2/9.webp b/apps/builder/public/respond-avatars/light-green-darken-2/9.webp new file mode 100644 index 000000000..89ef86cfc Binary files /dev/null and b/apps/builder/public/respond-avatars/light-green-darken-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/1.webp b/apps/builder/public/respond-avatars/lime-darken-2/1.webp new file mode 100644 index 000000000..d67edfbac Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/10.webp b/apps/builder/public/respond-avatars/lime-darken-2/10.webp new file mode 100644 index 000000000..d461652f4 Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/11.webp b/apps/builder/public/respond-avatars/lime-darken-2/11.webp new file mode 100644 index 000000000..a3145f103 Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/12.webp b/apps/builder/public/respond-avatars/lime-darken-2/12.webp new file mode 100644 index 000000000..3ec33fc73 Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/2.webp b/apps/builder/public/respond-avatars/lime-darken-2/2.webp new file mode 100644 index 000000000..624543c33 Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/3.webp b/apps/builder/public/respond-avatars/lime-darken-2/3.webp new file mode 100644 index 000000000..74547e287 Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/4.webp b/apps/builder/public/respond-avatars/lime-darken-2/4.webp new file mode 100644 index 000000000..3ed87fbae Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/5.webp b/apps/builder/public/respond-avatars/lime-darken-2/5.webp new file mode 100644 index 000000000..00d4607ba Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/6.webp b/apps/builder/public/respond-avatars/lime-darken-2/6.webp new file mode 100644 index 000000000..af814b378 Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/7.webp b/apps/builder/public/respond-avatars/lime-darken-2/7.webp new file mode 100644 index 000000000..b2798d5bf Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/8.webp b/apps/builder/public/respond-avatars/lime-darken-2/8.webp new file mode 100644 index 000000000..fe74e7ccf Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/lime-darken-2/9.webp b/apps/builder/public/respond-avatars/lime-darken-2/9.webp new file mode 100644 index 000000000..ffea6417c Binary files /dev/null and b/apps/builder/public/respond-avatars/lime-darken-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/1.webp b/apps/builder/public/respond-avatars/orange-lighten-1/1.webp new file mode 100644 index 000000000..ef28376ef Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/1.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/10.webp b/apps/builder/public/respond-avatars/orange-lighten-1/10.webp new file mode 100644 index 000000000..e60cfc644 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/10.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/11.webp b/apps/builder/public/respond-avatars/orange-lighten-1/11.webp new file mode 100644 index 000000000..a3cbe2e04 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/11.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/12.webp b/apps/builder/public/respond-avatars/orange-lighten-1/12.webp new file mode 100644 index 000000000..f0fb10b65 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/12.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/2.webp b/apps/builder/public/respond-avatars/orange-lighten-1/2.webp new file mode 100644 index 000000000..6253ee639 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/2.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/3.webp b/apps/builder/public/respond-avatars/orange-lighten-1/3.webp new file mode 100644 index 000000000..abce59793 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/3.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/4.webp b/apps/builder/public/respond-avatars/orange-lighten-1/4.webp new file mode 100644 index 000000000..95cf4bd25 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/4.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/5.webp b/apps/builder/public/respond-avatars/orange-lighten-1/5.webp new file mode 100644 index 000000000..4ff8171c4 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/5.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/6.webp b/apps/builder/public/respond-avatars/orange-lighten-1/6.webp new file mode 100644 index 000000000..fbb7d7172 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/6.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/7.webp b/apps/builder/public/respond-avatars/orange-lighten-1/7.webp new file mode 100644 index 000000000..f0233d7c1 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/7.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/8.webp b/apps/builder/public/respond-avatars/orange-lighten-1/8.webp new file mode 100644 index 000000000..25b850173 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/8.webp differ diff --git a/apps/builder/public/respond-avatars/orange-lighten-1/9.webp b/apps/builder/public/respond-avatars/orange-lighten-1/9.webp new file mode 100644 index 000000000..4abba75a4 Binary files /dev/null and b/apps/builder/public/respond-avatars/orange-lighten-1/9.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/1.webp b/apps/builder/public/respond-avatars/pink-lighten-2/1.webp new file mode 100644 index 000000000..059ec6a7a Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/10.webp b/apps/builder/public/respond-avatars/pink-lighten-2/10.webp new file mode 100644 index 000000000..2489b734e Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/11.webp b/apps/builder/public/respond-avatars/pink-lighten-2/11.webp new file mode 100644 index 000000000..0d760f7ae Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/12.webp b/apps/builder/public/respond-avatars/pink-lighten-2/12.webp new file mode 100644 index 000000000..261d581e6 Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/2.webp b/apps/builder/public/respond-avatars/pink-lighten-2/2.webp new file mode 100644 index 000000000..7a636533a Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/3.webp b/apps/builder/public/respond-avatars/pink-lighten-2/3.webp new file mode 100644 index 000000000..239e49f7b Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/4.webp b/apps/builder/public/respond-avatars/pink-lighten-2/4.webp new file mode 100644 index 000000000..98009b07b Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/5.webp b/apps/builder/public/respond-avatars/pink-lighten-2/5.webp new file mode 100644 index 000000000..2f0681b04 Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/6.webp b/apps/builder/public/respond-avatars/pink-lighten-2/6.webp new file mode 100644 index 000000000..251dcf927 Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/7.webp b/apps/builder/public/respond-avatars/pink-lighten-2/7.webp new file mode 100644 index 000000000..3ec636a90 Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/8.webp b/apps/builder/public/respond-avatars/pink-lighten-2/8.webp new file mode 100644 index 000000000..6a2b50f2d Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/pink-lighten-2/9.webp b/apps/builder/public/respond-avatars/pink-lighten-2/9.webp new file mode 100644 index 000000000..e7f1ad728 Binary files /dev/null and b/apps/builder/public/respond-avatars/pink-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/preview.html b/apps/builder/public/respond-avatars/preview.html new file mode 100644 index 000000000..d138d8229 --- /dev/null +++ b/apps/builder/public/respond-avatars/preview.html @@ -0,0 +1,89 @@ + + + + + Respond.io avatars (156) + + + +

Respond.io — Galeria de avatares (156)

+
+ 13 cores × 12 expressões — extraídos de cdn.respond.io +
+
+ + + diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/1.webp b/apps/builder/public/respond-avatars/purple-lighten-2/1.webp new file mode 100644 index 000000000..d0ef16fbc Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/10.webp b/apps/builder/public/respond-avatars/purple-lighten-2/10.webp new file mode 100644 index 000000000..eb7162538 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/11.webp b/apps/builder/public/respond-avatars/purple-lighten-2/11.webp new file mode 100644 index 000000000..e642dd280 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/12.webp b/apps/builder/public/respond-avatars/purple-lighten-2/12.webp new file mode 100644 index 000000000..b05f387b2 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/2.webp b/apps/builder/public/respond-avatars/purple-lighten-2/2.webp new file mode 100644 index 000000000..8a618223c Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/3.webp b/apps/builder/public/respond-avatars/purple-lighten-2/3.webp new file mode 100644 index 000000000..9ca6b69b7 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/4.webp b/apps/builder/public/respond-avatars/purple-lighten-2/4.webp new file mode 100644 index 000000000..76ea5951b Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/5.webp b/apps/builder/public/respond-avatars/purple-lighten-2/5.webp new file mode 100644 index 000000000..03d853f17 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/6.webp b/apps/builder/public/respond-avatars/purple-lighten-2/6.webp new file mode 100644 index 000000000..5088b84a6 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/7.webp b/apps/builder/public/respond-avatars/purple-lighten-2/7.webp new file mode 100644 index 000000000..ed86e1808 Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/8.webp b/apps/builder/public/respond-avatars/purple-lighten-2/8.webp new file mode 100644 index 000000000..a1789f05b Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/purple-lighten-2/9.webp b/apps/builder/public/respond-avatars/purple-lighten-2/9.webp new file mode 100644 index 000000000..7c3f4b65b Binary files /dev/null and b/apps/builder/public/respond-avatars/purple-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/1.webp b/apps/builder/public/respond-avatars/red-lighten-2/1.webp new file mode 100644 index 000000000..ce2f18dd6 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/10.webp b/apps/builder/public/respond-avatars/red-lighten-2/10.webp new file mode 100644 index 000000000..037b4aa86 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/11.webp b/apps/builder/public/respond-avatars/red-lighten-2/11.webp new file mode 100644 index 000000000..7c6a54ef7 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/12.webp b/apps/builder/public/respond-avatars/red-lighten-2/12.webp new file mode 100644 index 000000000..6c02777c3 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/2.webp b/apps/builder/public/respond-avatars/red-lighten-2/2.webp new file mode 100644 index 000000000..f8d2e7bc2 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/3.webp b/apps/builder/public/respond-avatars/red-lighten-2/3.webp new file mode 100644 index 000000000..c72a0e55a Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/4.webp b/apps/builder/public/respond-avatars/red-lighten-2/4.webp new file mode 100644 index 000000000..97afd3953 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/5.webp b/apps/builder/public/respond-avatars/red-lighten-2/5.webp new file mode 100644 index 000000000..8946964ff Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/6.webp b/apps/builder/public/respond-avatars/red-lighten-2/6.webp new file mode 100644 index 000000000..be179d930 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/7.webp b/apps/builder/public/respond-avatars/red-lighten-2/7.webp new file mode 100644 index 000000000..459cda5bf Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/8.webp b/apps/builder/public/respond-avatars/red-lighten-2/8.webp new file mode 100644 index 000000000..aa1a090e0 Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/red-lighten-2/9.webp b/apps/builder/public/respond-avatars/red-lighten-2/9.webp new file mode 100644 index 000000000..05873b64d Binary files /dev/null and b/apps/builder/public/respond-avatars/red-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/1.webp b/apps/builder/public/respond-avatars/teal-lighten-2/1.webp new file mode 100644 index 000000000..a9ebb4239 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/1.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/10.webp b/apps/builder/public/respond-avatars/teal-lighten-2/10.webp new file mode 100644 index 000000000..3c33ff1f3 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/10.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/11.webp b/apps/builder/public/respond-avatars/teal-lighten-2/11.webp new file mode 100644 index 000000000..1201a5504 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/11.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/12.webp b/apps/builder/public/respond-avatars/teal-lighten-2/12.webp new file mode 100644 index 000000000..10956e2ce Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/12.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/2.webp b/apps/builder/public/respond-avatars/teal-lighten-2/2.webp new file mode 100644 index 000000000..45617bae2 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/2.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/3.webp b/apps/builder/public/respond-avatars/teal-lighten-2/3.webp new file mode 100644 index 000000000..0520204be Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/3.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/4.webp b/apps/builder/public/respond-avatars/teal-lighten-2/4.webp new file mode 100644 index 000000000..1a0bf90c1 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/4.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/5.webp b/apps/builder/public/respond-avatars/teal-lighten-2/5.webp new file mode 100644 index 000000000..763e6dc6c Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/5.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/6.webp b/apps/builder/public/respond-avatars/teal-lighten-2/6.webp new file mode 100644 index 000000000..e9399d521 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/6.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/7.webp b/apps/builder/public/respond-avatars/teal-lighten-2/7.webp new file mode 100644 index 000000000..4c1403d06 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/7.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/8.webp b/apps/builder/public/respond-avatars/teal-lighten-2/8.webp new file mode 100644 index 000000000..96375da41 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/8.webp differ diff --git a/apps/builder/public/respond-avatars/teal-lighten-2/9.webp b/apps/builder/public/respond-avatars/teal-lighten-2/9.webp new file mode 100644 index 000000000..0bd853e80 Binary files /dev/null and b/apps/builder/public/respond-avatars/teal-lighten-2/9.webp differ diff --git a/apps/builder/public/respond-icons/iconfont.ttf b/apps/builder/public/respond-icons/iconfont.ttf new file mode 100644 index 000000000..27038f7d5 Binary files /dev/null and b/apps/builder/public/respond-icons/iconfont.ttf differ diff --git a/apps/builder/public/respond-icons/iconfont.woff b/apps/builder/public/respond-icons/iconfont.woff new file mode 100644 index 000000000..0606cc415 Binary files /dev/null and b/apps/builder/public/respond-icons/iconfont.woff differ diff --git a/apps/builder/public/respond-icons/iconfont.woff2 b/apps/builder/public/respond-icons/iconfont.woff2 new file mode 100644 index 000000000..29ea2a3de Binary files /dev/null and b/apps/builder/public/respond-icons/iconfont.woff2 differ diff --git a/apps/builder/src/app/api/audit-logs/export/route.ts b/apps/builder/src/app/api/audit-logs/export/route.ts new file mode 100644 index 000000000..de87b398f --- /dev/null +++ b/apps/builder/src/app/api/audit-logs/export/route.ts @@ -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"`, + }, + }) +} diff --git a/apps/builder/src/app/api/scalar-ui/route.ts b/apps/builder/src/app/api/scalar-ui/route.ts index 49b662a5f..a52fe25e9 100644 --- a/apps/builder/src/app/api/scalar-ui/route.ts +++ b/apps/builder/src/app/api/scalar-ui/route.ts @@ -3,7 +3,7 @@ export async function GET() { - My Client + Meu Cliente diff --git a/apps/builder/src/app/globals.css b/apps/builder/src/app/globals.css index 71bc65b7f..02eece512 100644 --- a/apps/builder/src/app/globals.css +++ b/apps/builder/src/app/globals.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "@chatbotx.io/ui/styles/default.css"; +@import "./respond-icons.css"; /** * Tailwind CSS official document: @@ -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); +} diff --git a/apps/builder/src/app/integrations/[...integration]/callback.ts b/apps/builder/src/app/integrations/[...integration]/callback.ts index 1877977f7..82dfab928 100644 --- a/apps/builder/src/app/integrations/[...integration]/callback.ts +++ b/apps/builder/src/app/integrations/[...integration]/callback.ts @@ -64,7 +64,7 @@ export const handleCallback = async ( : await workspaceService.create({ data: { organizationId: organization.id, - name: "New Workspace", + name: "Novo Workspace", }, organization, createdBy: userId, diff --git a/apps/builder/src/app/integrations/[...integration]/webhook.ts b/apps/builder/src/app/integrations/[...integration]/webhook.ts index 12e64454d..306efc80b 100644 --- a/apps/builder/src/app/integrations/[...integration]/webhook.ts +++ b/apps/builder/src/app/integrations/[...integration]/webhook.ts @@ -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" }, @@ -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" }, @@ -93,7 +93,7 @@ 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" } }, ) } @@ -101,14 +101,14 @@ const handleTelegramWebhook = async (req: NextRequest) => { 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" }, }) diff --git a/apps/builder/src/app/integrations/whatsapp/webhook/[integrationId]/route.ts b/apps/builder/src/app/integrations/whatsapp/webhook/[integrationId]/route.ts index 76f3b332e..b92ac2025 100644 --- a/apps/builder/src/app/integrations/whatsapp/webhook/[integrationId]/route.ts +++ b/apps/builder/src/app/integrations/whatsapp/webhook/[integrationId]/route.ts @@ -36,7 +36,7 @@ const loadManualIntegration = async (integrationId: string) => { const handleGet = async (req: NextRequest, integrationId: string) => { const result = await loadManualIntegration(integrationId) if (!result) { - return json({ message: "Integration not found" }, 404) + return json({ message: "Integração não encontrada" }, 404) } const { auth } = result @@ -46,11 +46,11 @@ const handleGet = async (req: NextRequest, integrationId: string) => { const challenge = params.get("hub.challenge") if (mode !== "subscribe" || !challenge) { - return json({ message: "Invalid handshake" }, 400) + return json({ message: "Handshake inválido" }, 400) } if (challenge.length > MAX_CHALLENGE_LENGTH) { - return json({ message: "Challenge too long" }, 400) + return json({ message: "Challenge muito longo" }, 400) } if (token !== auth.verifyToken) { @@ -58,7 +58,7 @@ const handleGet = async (req: NextRequest, integrationId: string) => { { integrationId }, "Whatsapp manual webhook verify_token mismatch", ) - return json({ message: "Forbidden" }, 403) + return json({ message: "Proibido" }, 403) } await markWhatsappWebhookVerified(integrationId, auth) @@ -72,26 +72,26 @@ const handleGet = async (req: NextRequest, integrationId: string) => { const handlePost = async (req: NextRequest, integrationId: string) => { const result = await loadManualIntegration(integrationId) if (!result) { - return json({ message: "Integration not found" }, 404) + return json({ message: "Integração não encontrada" }, 404) } const { auth } = result const signature = req.headers.get(SIGNATURE_HEADER) if (!signature?.startsWith(SIGNATURE_PREFIX)) { - return json({ message: "Missing signature" }, 401) + return json({ message: "Assinatura ausente" }, 401) } const verified = Boolean(auth.metadata?.webhookVerifiedAt) || Boolean(auth.metadata?.subscribeOverrideOk) if (!verified) { - return json({ message: "Webhook not verified" }, 403) + return json({ message: "Webhook não verificado" }, 403) } const integration = integrations.whatsapp if (!integration?.handleRequest) { - return json({ message: "Method is not implemented" }, 400) + return json({ message: "Método não implementado" }, 400) } try { diff --git a/apps/builder/src/app/layout.tsx b/apps/builder/src/app/layout.tsx index 708eb32c5..bd5dc77a1 100644 --- a/apps/builder/src/app/layout.tsx +++ b/apps/builder/src/app/layout.tsx @@ -5,6 +5,7 @@ import "./themes.css" import { UiProvider } from "@chatbotx.io/ui" import { NextIntlClientProvider } from "next-intl" import { getLocale } from "next-intl/server" +import { DevClickToComponent } from "@/components/dev-click-to-component" import { PlatformSettingsProvider } from "@/features/platform" import { getPlatformSettings } from "@/features/platform/utils" @@ -54,6 +55,9 @@ export default async function RootLayout({ children }: Props) { {children} + {/* Dev only — segurando ⌥ Option e clicando em qualquer elemento abre + o arquivo React correspondente no Cursor (linha exata). NULL em prod. */} + ) diff --git a/apps/builder/src/app/manage/layout.tsx b/apps/builder/src/app/manage/layout.tsx index 02a032aad..62e4efef8 100644 --- a/apps/builder/src/app/manage/layout.tsx +++ b/apps/builder/src/app/manage/layout.tsx @@ -31,7 +31,8 @@ export default async function ManageLayoutPage({ if ( !organizationMember || - organizationMember.role !== organizationMemberRoles.enum.admin + (organizationMember.role !== organizationMemberRoles.enum.owner && + organizationMember.role !== organizationMemberRoles.enum.manager) ) { return notFound() } diff --git a/apps/builder/src/app/manage/page.tsx b/apps/builder/src/app/manage/page.tsx index 67cb5f92a..760c7eb2f 100644 --- a/apps/builder/src/app/manage/page.tsx +++ b/apps/builder/src/app/manage/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation" export default function ManagePage() { - redirect("/manage/integrations") + redirect("/manage/settings") } diff --git a/apps/builder/src/app/manage/users/page.tsx b/apps/builder/src/app/manage/users/page.tsx index a29e1f89e..f15fe78d9 100644 --- a/apps/builder/src/app/manage/users/page.tsx +++ b/apps/builder/src/app/manage/users/page.tsx @@ -1,9 +1,13 @@ +import { organizationService } from "@chatbotx.io/business" +import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" import type { SearchParams } from "nuqs/server" -import { Suspense } from "react" -import OrganizationMembersTable from "@/enterprise/features/organization-members/components/organization-members-table" -import { listOrganizationMembersRSC } from "@/enterprise/features/organization-members/queries" +import { OrganizationMembersList } from "@/enterprise/features/organization-members/components/organization-members-list" +import { listOrganizationMembers } from "@/enterprise/features/organization-members/queries" import { listOrganizationMembersSearchParamsCache } from "@/enterprise/features/organization-members/schema/mutation" +import { listOrgWorkspaces } from "@/features/workspaces/queries/list-org-workspaces" +import { getCurrentUser } from "@/lib/auth/utils" +import { getDomainFromHeader } from "@/lib/domain" type ManageUsersPageProps = { searchParams: Promise @@ -11,18 +15,44 @@ type ManageUsersPageProps = { export default async function ManageUsersPage(props: ManageUsersPageProps) { const t = await getTranslations() + const user = await getCurrentUser() + if (!user) { + return notFound() + } + + const domain = await getDomainFromHeader() + const organization = await organizationService.findByDomain(domain) const searchParams = await props.searchParams const search = listOrganizationMembersSearchParamsCache.parse(searchParams) - const promises = Promise.all([listOrganizationMembersRSC(search)]) + + const [{ data: members }, workspaces] = await Promise.all([ + listOrganizationMembers({ + ...search, + organizationId: organization.id, + }), + listOrgWorkspaces({ organizationId: organization.id }), + ]) + + const workspaceOptions = workspaces.map((w) => ({ id: w.id, name: w.name })) return (
-

{t("users.title")}

+
+

+ {t("organizationUsers.pageTitle")} +

+

+ {t("organizationUsers.pageSubtitle")} +

+
- - - +
) } diff --git a/apps/builder/src/app/manage/workspaces/page.tsx b/apps/builder/src/app/manage/workspaces/page.tsx new file mode 100644 index 000000000..e671fd469 --- /dev/null +++ b/apps/builder/src/app/manage/workspaces/page.tsx @@ -0,0 +1,19 @@ +import { organizationService } from "@chatbotx.io/business" +import { notFound } from "next/navigation" +import { ManageWorkspacesList } from "@/features/workspaces/components/manage-workspaces-list" +import { listOrgWorkspaces } from "@/features/workspaces/queries/list-org-workspaces" +import { getDomainFromHeader } from "@/lib/domain" + +export default async function ManageWorkspacesPage() { + const domain = await getDomainFromHeader() + const organization = await organizationService.findByDomain(domain) + if (!organization) { + return notFound() + } + + const workspaces = await listOrgWorkspaces({ + organizationId: organization.id, + }) + + return +} diff --git a/apps/builder/src/app/page.tsx b/apps/builder/src/app/page.tsx index 7b3b63b26..10759666a 100644 --- a/apps/builder/src/app/page.tsx +++ b/apps/builder/src/app/page.tsx @@ -1,12 +1,5 @@ -import { notFound } from "next/navigation" -import WorkspacesList from "@/features/workspaces/components/workspaces-list" -import { getCurrentUserAndAllLinkedWorkspaces } from "@/lib/auth/utils" +import { redirect } from "next/navigation" -export default async function MainPage() { - const userAndWorkspaces = await getCurrentUserAndAllLinkedWorkspaces() - if (!userAndWorkspaces) { - return notFound() - } - - return +export default function MainPage() { + redirect("/manage") } diff --git a/apps/builder/src/app/r/[workspaceId]/[name]/route.ts b/apps/builder/src/app/r/[workspaceId]/[name]/route.ts index da1c2f1cb..7c34da018 100644 --- a/apps/builder/src/app/r/[workspaceId]/[name]/route.ts +++ b/apps/builder/src/app/r/[workspaceId]/[name]/route.ts @@ -25,7 +25,7 @@ export const GET = async ( }) if (!row) { - return NextResponse.json({ message: "Not found" }, { status: 404 }) + return NextResponse.json({ message: "Não encontrado" }, { status: 404 }) } let destination: string @@ -35,7 +35,7 @@ export const GET = async ( }) } catch { return NextResponse.json( - { message: "Invalid link configuration" }, + { message: "Configuração de link inválida" }, { status: 400 }, ) } @@ -49,11 +49,11 @@ export const GET = async ( // Decode the button payload const decodedButton = decodeButtonPayload(code) if (!decodedButton) { - return NextResponse.json({ message: "Invalid code" }, { status: 400 }) + return NextResponse.json({ message: "Código inválido" }, { status: 400 }) } if (!decodedButton.contactInboxId) { return NextResponse.json( - { message: "Contact inbox ID is missing" }, + { message: "ID da caixa de entrada do contato ausente" }, { status: 400 }, ) } @@ -69,7 +69,7 @@ export const GET = async ( if (!contactInbox) { return NextResponse.json( - { message: "Contact inbox not found" }, + { message: "Caixa de entrada do contato não encontrada" }, { status: 404 }, ) } @@ -89,10 +89,13 @@ export const GET = async ( ) if (!foundedButton) { - return NextResponse.json({ message: "Button not found" }, { status: 404 }) + return NextResponse.json( + { message: "Botão não encontrado" }, + { status: 404 }, + ) } if (!foundedNodeId) { - return NextResponse.json({ message: "Node ID is missing" }, { status: 400 }) + return NextResponse.json({ message: "ID do Nó ausente" }, { status: 400 }) } await emit(flowEventTypeSchema.enum["flow:clicked"], { diff --git a/apps/builder/src/app/respond-icons.css b/apps/builder/src/app/respond-icons.css new file mode 100644 index 000000000..b2cdbcf40 --- /dev/null +++ b/apps/builder/src/app/respond-icons.css @@ -0,0 +1,321 @@ +/* Iconfont do Respond.io — 848 ícones extraídos do CDN oficial + 2026-05-24. Pedro pediu "ícones idênticos ao Respond.io". + Arquivos em /public/respond-icons/ (woff2 + woff + ttf). + + Uso: + Ou via componente: +*/ + +@font-face { + font-family: "respond-iconfont"; + font-display: block; + src: + url("/respond-icons/iconfont.woff2") format("woff2"), + url("/respond-icons/iconfont.woff") format("woff"), + url("/respond-icons/iconfont.ttf") format("truetype"); +} + +.icon { + display: inline-block; + font-family: "respond-iconfont", sans-serif !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1; + vertical-align: middle; + text-transform: none; +} + +/* Tamanhos canônicos do Respond.io */ +.icon-xs { + font-size: 12px; +} +.icon-sm { + font-size: 14px; +} +.icon-md { + font-size: 16px; +} +.icon-lg { + font-size: 18px; +} +.icon-xl { + font-size: 20px; +} +.icon-2xl { + font-size: 24px; +} + +/* Mapa de ícones essenciais (~65 dos 848 totais). Ampliar conforme uso. */ +.icon-tick-circle::before { + content: "\00f156"; +} +.icon-tick-square::before { + content: "\00f154"; +} +.icon-tick-square-bold::before { + content: "\00f155"; +} +.icon-tick::before { + content: "\00f153"; +} +.icon-send::before { + content: "\00f1ad"; +} +.icon-search-normal::before { + content: "\00f1b6"; +} +.icon-more::before { + content: "\00f215"; +} +.icon-message-text::before { + content: "\00f236"; +} +.icon-message-question::before { + content: "\00f23b"; +} +.icon-message-2::before { + content: "\00f243"; +} +.icon-emoji-happy::before { + content: "\00f352"; +} +.icon-close-circle::before { + content: "\00f3ac"; +} +.icon-attach-square::before { + content: "\00f417"; +} +.icon-arrow-down::before { + content: "\00f42b"; +} +.icon-arrow-down-1::before { + content: "\00f42f"; +} +.icon-arrow-up::before { + content: "\00f41b"; +} +.icon-arrow-up-1::before { + content: "\00f41f"; +} +.icon-arrow-left::before { + content: "\00f427"; +} +.icon-arrow-right::before { + content: "\00f421"; +} +.icon-ai-assist::before { + content: "\00f447"; +} +.icon-ai-agent::before { + content: "\00f449"; +} +.icon-add::before { + content: "\00f44b"; +} +.icon-workflow::before { + content: "\00f105"; +} +.icon-user::before { + content: "\00f11d"; +} +.icon-user-add::before { + content: "\00f128"; +} +.icon-flash::before { + content: "\00f337"; +} +.icon-sticker::before { + content: "\00f17c"; +} +.icon-document::before { + content: "\00f364"; +} +.icon-star::before { + content: "\00f180"; +} +.icon-star-bold::before { + content: "\00f181"; +} +.icon-volume-high::before { + content: "\00f112"; +} +.icon-eye::before { + content: "\00f343"; +} +.icon-eye-bold::before { + content: "\00f344"; +} +.icon-edit::before { + content: "\00f35d"; +} +.icon-edit-bold::before { + content: "\00f35e"; +} +.icon-trash::before { + content: "\00f137"; +} +.icon-trash-bold::before { + content: "\00f138"; +} +.icon-forbidden::before { + content: "\00f333"; +} +.icon-forbidden-bold::before { + content: "\00f334"; +} +.icon-reply::before { + content: "\00f1c9"; +} +.icon-copy::before { + content: "\00f38f"; +} +.icon-link-1::before { + content: "\00f2f0"; +} +.icon-message-delivered::before { + content: "\00f241"; +} +.icon-magicpen-bold::before { + content: "\00f2de"; +} +.icon-emoji-normal::before { + content: "\00f350"; +} +.icon-i-microphone::before { + content: "\00f317"; +} +.icon-receipt::before { + content: "\00f1da"; +} +.icon-message-programming::before { + content: "\00f23d"; +} +.icon-send-bold::before { + content: "\00f1b1"; +} +.icon-comments-ai::before { + content: "\00f399"; +} +.icon-ai-assist-bold::before { + content: "\00f448"; +} +.icon-channels::before { + content: "\00f3c0"; +} +.icon-at::before { + content: "\00f419"; +} +.icon-close-square::before { + content: "\00f3aa"; +} +.icon-custom-user-square-bold::before { + content: "\00f382"; +} +.icon-translate::before { + content: "\00f139"; +} +.icon-code::before { + content: "\00f3a0"; +} +.icon-clock::before { + content: "\00f3ae"; +} +.icon-clock-bold::before { + content: "\00f3af"; +} +.icon-sms::before { + content: "\00f18e"; +} +.icon-sms-bold::before { + content: "\00f18f"; +} +.icon-phone::before { + content: "\00f1f3"; +} +.icon-call-calling::before { + content: "\00f3de"; +} +.icon-home::before { + content: "\00f31d"; +} +.icon-global::before { + content: "\00f327"; +} +.icon-profile::before { + content: "\00f1e5"; +} +.icon-profile-2user::before { + content: "\00f1ea"; +} +.icon-note::before { + content: "\00f20a"; +} +.icon-notification::before { + content: "\00f206"; +} +.icon-notification-bing::before { + content: "\00f208"; +} +.icon-tag::before { + content: "\00f169"; +} +.icon-tag-bold::before { + content: "\00f16a"; +} +.icon-filter::before { + content: "\00f33b"; +} +.icon-filter-bold::before { + content: "\00f33e"; +} +.icon-sort::before { + content: "\00f18c"; +} +.icon-verify-bold::before { + content: "\00f11b"; +} +.icon-received::before { + content: "\00f1d8"; +} +.icon-whatsapp-bold::before { + content: "\00f10a"; +} +.icon-instagram::before { + content: "\00f306"; +} +.icon-facebook::before { + content: "\00f341"; +} +.icon-email::before { + content: "\00f354"; +} +.icon-menu::before { + content: "\00f245"; +} +.icon-sidebar-left::before { + content: "\00f19b"; +} +.icon-sidebar-right::before { + content: "\00f199"; +} +.icon-refresh::before { + content: "\00f1cc"; +} +.icon-refresh-circle::before { + content: "\00f1cd"; +} +.icon-archive::before { + content: "\00f434"; +} +.icon-minus::before { + content: "\00f223"; +} +.icon-play::before { + content: "\00f1f1"; +} +.icon-pause::before { + content: "\00f1fa"; +} diff --git a/apps/builder/src/app/space/[workspaceId]/(enterprise)/pricing/page.tsx b/apps/builder/src/app/space/[workspaceId]/(enterprise)/pricing/page.tsx index e0c6dfef7..071430f50 100644 --- a/apps/builder/src/app/space/[workspaceId]/(enterprise)/pricing/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(enterprise)/pricing/page.tsx @@ -19,9 +19,9 @@ export default function PricingTableFourDemo() { plans={samplePlans} showBillingToggle={false} size="medium" - subtitle="Simple Pricing" + subtitle="Preços Simples" theme="classic" - title="Choose Your Perfect Plan" + title="Escolha Seu Plano Ideal" /> ) } diff --git a/apps/builder/src/app/(no-sidebar)/space/[workspaceId]/flows/[id]/page.tsx b/apps/builder/src/app/space/[workspaceId]/(full-canvas)/flows/[id]/page.tsx similarity index 72% rename from apps/builder/src/app/(no-sidebar)/space/[workspaceId]/flows/[id]/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(full-canvas)/flows/[id]/page.tsx index 9020c4b24..cc1cd0ce1 100644 --- a/apps/builder/src/app/(no-sidebar)/space/[workspaceId]/flows/[id]/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(full-canvas)/flows/[id]/page.tsx @@ -9,6 +9,18 @@ type FlowPageProps = { params: Promise<{ workspaceId: string; id: string }> } +/** + * Page do flow editor. + * + * Mudança 2026-05-23 (Pedro): movido de `(no-sidebar)/` pra dentro do route + * group `(full-canvas)/` — assim o AppSidebar global (com ícones: Inbox, + * Flows, Contatos, etc.) fica visível também dentro do flow editor (estilo + * Respond.io: sidebar fixo em todas as páginas). + * + * O wrapper agora usa `h-full w-full` em vez de `h-screen w-screen` porque + * vive dentro do `
` do layout pai — não precisa pegar + * o viewport inteiro, basta preencher o espaço disponível. + */ export default async function FlowPage({ params }: FlowPageProps) { const { data } = await withWorkspaceIdAndIdSchema.safeParse(await params) if (!data) { @@ -50,7 +62,7 @@ export default async function FlowPage({ params }: FlowPageProps) { } return ( -
+
do layout pai. + * + * Necessário porque o layout `space/[workspaceId]/layout.tsx` aplica + * `p-6 gap-4 overflow-auto` no
, o que quebra páginas tipo flow + * editor que precisam de canvas full-bleed sem padding. + */ +body[data-full-canvas="true"] main { + gap: 0 !important; + padding: 0 !important; + overflow: hidden !important; +} diff --git a/apps/builder/src/app/space/[workspaceId]/(full-canvas)/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(full-canvas)/layout.tsx new file mode 100644 index 000000000..2b168d8b5 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/(full-canvas)/layout.tsx @@ -0,0 +1,32 @@ +"use client" + +import { useEffect } from "react" +import "./full-canvas.css" + +/** + * Layout do route group `(full-canvas)` — usado pelo flow editor (e outras + * páginas que precisem ocupar todo o espaço sem padding). + * + * Por que existe: o layout pai `space/[workspaceId]/layout.tsx` aplica + * `p-6 gap-4 overflow-auto` no `
` (correto pra páginas tipo dashboard, + * settings, etc.). Mas o flow editor precisa de canvas full-bleed — + * adicionamos `data-full-canvas="true"` no body via useEffect, e o CSS + * sobrescreve essas regras só pra essa rota (ver `full-canvas.css`). + * + * Vantagem versus a rota antiga `(no-sidebar)`: aqui o AppSidebar global + * fica visível (Pedro pediu "sidebar fixo em todas as opções, estilo + * Respond.io"). Antes o flow editor sumia com o sidebar. + */ +export default function FullCanvasLayout({ + children, +}: { + children: React.ReactNode +}) { + useEffect(() => { + document.body.dataset.fullCanvas = "true" + return () => { + delete document.body.dataset.fullCanvas + } + }, []) + return
{children}
+} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/_folders_disabled/page.tsx similarity index 100% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/@folders/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/_folders_disabled/page.tsx diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/layout.tsx index 7ab2ca8e8..d337b6458 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/layout.tsx @@ -6,11 +6,9 @@ import { FolderStoreProvider } from "@/features/folders/provider/folder-store-co export default async function FolderableLayout({ children, - folders, params, }: { children: ReactNode - folders: ReactNode params: Promise<{ workspaceId: string }> }) { const workspaceId = getIdFromParams(await params, "workspaceId") @@ -24,7 +22,6 @@ export default async function FolderableLayout({ workspaceId={workspaceId} > - {folders} {children} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/page.tsx index 773768b37..8bea69bc6 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/automated-responses/page.tsx @@ -3,6 +3,7 @@ import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" import type { SearchParams } from "nuqs/server" import { Suspense } from "react" +import { AutomationTabs } from "@/components/automation-tabs" import { AutomatedResponsesTable } from "@/features/automated-response/automated-response-table" import { listAutomatedResponses } from "@/features/automated-response/queries" import { listAutomatedResponsesSearchParams } from "@/features/automated-response/schema/query" @@ -29,8 +30,14 @@ export default async function AutomatedResponesPage(props: { ]) return ( - - - +
+ + + + +
) } diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/layout.tsx deleted file mode 100644 index 9d03f34e0..000000000 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/layout.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { getIdFromParams } from "@chatbotx.io/utils" -import { notFound } from "next/navigation" -import { getTranslations } from "next-intl/server" -import type { ReactNode } from "react" -import { AppTab } from "@/components/app-tab" -import { FolderStoreProvider } from "@/features/folders/provider/folder-store-context" - -export default async function FolderableLayout({ - children, - folders, - params, -}: { - children: ReactNode - folders: ReactNode - params: Promise<{ workspaceId: string }> -}) { - const workspaceId = getIdFromParams(await params, "workspaceId") - if (!workspaceId) { - return notFound() - } - - const t = await getTranslations() - - return ( - - - {folders} - {children} - - ) -} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/flows/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/flows/layout.tsx index 0bd488c3a..8d6213fe2 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/flows/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/flows/layout.tsx @@ -1,20 +1,20 @@ "use client" import { notFound } from "next/navigation" -import { useTranslations } from "next-intl" import type { ReactNode } from "react" -import { AppTab } from "@/components/app-tab" import { FolderStoreProvider } from "@/features/folders/provider/folder-store-context" import { useWorkspaceId } from "@/hooks/routing" +/** + * Layout da página /flows — só FolderStoreProvider + children. + * Pedro pediu pra remover o painel "Pastas" do topo em TODAS as páginas + * (2026-05-24) pra deixar UI flat estilo Respond.io. + */ export default function FolderableLayout({ children, - folders, }: { children: ReactNode folders: ReactNode }) { - const t = useTranslations() - const workspaceId = useWorkspaceId() if (!workspaceId) { return notFound() @@ -22,31 +22,6 @@ export default function FolderableLayout({ return ( - - {folders} {children} ) diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/_folders_disabled/page.tsx similarity index 100% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/@folders/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/_folders_disabled/page.tsx diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/layout.tsx index d901a38fd..4e5f0c83a 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/layout.tsx @@ -7,11 +7,9 @@ import { FolderStoreProvider } from "@/features/folders/provider/folder-store-co export default async function FolderableLayout({ children, - folders, params, }: { children: ReactNode - folders: ReactNode params: Promise<{ workspaceId: string }> }) { const workspaceId = getIdFromParams(await params, "workspaceId") @@ -25,7 +23,6 @@ export default async function FolderableLayout({ folderType={folderTypes.enum.sequence} workspaceId={workspaceId} > - {folders} {children} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/page.tsx index b2b00fbe1..25ca7e88e 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/page.tsx @@ -1,17 +1,12 @@ -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@chatbotx.io/ui/components/ui/card" import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" -import { getTranslations } from "next-intl/server" import type { SearchParams } from "nuqs/server" +import { AutomationTabs } from "@/components/automation-tabs" import { listSequences } from "@/features/sequences/queries" import { listSequencesSearchParamsCache } from "@/features/sequences/schema/action" import { SequencesTable } from "@/features/sequences/sequences-table" +// Card wrapper removido — Pedro pediu UI flat estilo Respond.io 2026-05-24. export default async function SequencesPage(props: { params: Promise<{ workspaceId: string }> searchParams: Promise @@ -23,7 +18,6 @@ export default async function SequencesPage(props: { const searchParams = await props.searchParams const search = await listSequencesSearchParamsCache.parse(searchParams) - const t = await getTranslations() const promises = Promise.all([ listSequences({ @@ -33,15 +27,9 @@ export default async function SequencesPage(props: { ]) return ( - - - - {t("sequences.title")} - - - - - - +
+ + +
) } diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/@folders/page.tsx deleted file mode 100644 index 8d3f12de8..000000000 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/@folders/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { getIdFromParams } from "@chatbotx.io/utils" -import { notFound } from "next/navigation" -import type { SearchParams } from "nuqs/server" -import SharedFolderSlot from "@/features/folders/shared-folder-slot" - -export default async function FolderPage(props: { - params: Promise<{ workspaceId: string }> - searchParams: Promise -}) { - const workspaceId = getIdFromParams(await props.params, "workspaceId") - if (!workspaceId) { - return notFound() - } - - return ( - - ) -} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/layout.tsx deleted file mode 100644 index 92ec31b92..000000000 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/layout.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { getIdFromParams } from "@chatbotx.io/utils" -import { notFound } from "next/navigation" -import { getTranslations } from "next-intl/server" -import type { ReactNode } from "react" -import { AppTab } from "@/components/app-tab" -import { FolderStoreProvider } from "@/features/folders/provider/folder-store-context" - -export default async function TagsLayout({ - children, - folders, - params, -}: { - children: ReactNode - folders: ReactNode - params: Promise<{ workspaceId: string }> -}) { - const workspaceId = getIdFromParams(await params, "workspaceId") - if (!workspaceId) { - return notFound() - } - - const t = await getTranslations() - - return ( - - - {folders} - {children} - - ) -} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/@folders/page.tsx deleted file mode 100644 index 8d3f12de8..000000000 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/@folders/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { getIdFromParams } from "@chatbotx.io/utils" -import { notFound } from "next/navigation" -import type { SearchParams } from "nuqs/server" -import SharedFolderSlot from "@/features/folders/shared-folder-slot" - -export default async function FolderPage(props: { - params: Promise<{ workspaceId: string }> - searchParams: Promise -}) { - const workspaceId = getIdFromParams(await props.params, "workspaceId") - if (!workspaceId) { - return notFound() - } - - return ( - - ) -} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/_folders_disabled/page.tsx similarity index 100% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/@folders/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/_folders_disabled/page.tsx diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/layout.tsx index 5d2a3860f..b58f1091d 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/layout.tsx @@ -6,11 +6,9 @@ import { FolderStoreProvider } from "@/features/folders/provider/folder-store-co export default async function TriggersLayout({ children, - folders, params, }: { children: ReactNode - folders: ReactNode params: Promise<{ workspaceId: string }> }) { const workspaceId = getIdFromParams(await params, "workspaceId") @@ -24,7 +22,6 @@ export default async function TriggersLayout({ folderType={folderTypes.enum.trigger} workspaceId={workspaceId} > - {folders} {children} ) diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/page.tsx index cc4bb7188..d6e777c85 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/triggers/page.tsx @@ -1,9 +1,3 @@ -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@chatbotx.io/ui/components/ui/card" import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" @@ -34,21 +28,15 @@ export default async function TriggersPage(props: { ]) return ( - - - - {t("triggers.title")} - - - - - - - - +
+

{t("triggers.title")}

+ + + +
) } diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/@folders/page.tsx deleted file mode 100644 index 8d3f12de8..000000000 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/@folders/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { getIdFromParams } from "@chatbotx.io/utils" -import { notFound } from "next/navigation" -import type { SearchParams } from "nuqs/server" -import SharedFolderSlot from "@/features/folders/shared-folder-slot" - -export default async function FolderPage(props: { - params: Promise<{ workspaceId: string }> - searchParams: Promise -}) { - const workspaceId = getIdFromParams(await props.params, "workspaceId") - if (!workspaceId) { - return notFound() - } - - return ( - - ) -} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/@folders/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/_folders_disabled/page.tsx similarity index 100% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/sequences/@folders/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/_folders_disabled/page.tsx diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/layout.tsx index 2118f7657..703e08b0e 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/layout.tsx @@ -6,11 +6,9 @@ import { FolderStoreProvider } from "@/features/folders/provider/folder-store-co export default async function WebhooksLayout({ children, - folders, params, }: { children: ReactNode - folders: ReactNode params: Promise<{ workspaceId: string }> }) { const workspaceId = getIdFromParams(await params, "workspaceId") @@ -24,7 +22,6 @@ export default async function WebhooksLayout({ folderType={folderTypes.enum.webhook} workspaceId={workspaceId} > - {folders} {children} ) diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/page.tsx b/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/page.tsx index aa77bfcee..b4620c3cb 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(has-folder)/webhooks/page.tsx @@ -1,9 +1,3 @@ -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@chatbotx.io/ui/components/ui/card" import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" @@ -34,21 +28,15 @@ export default async function WebhooksPage(props: { ]) return ( - - - - {t("webhooks.title")} - - - - - - - - +
+

{t("webhooks.title")}

+ + + +
) } diff --git a/apps/builder/src/app/space/[workspaceId]/(integrations)/whatsapps/[id]/automation/page.tsx b/apps/builder/src/app/space/[workspaceId]/(integrations)/whatsapps/[id]/automation/page.tsx index a51bfb3ad..a60ff3139 100644 --- a/apps/builder/src/app/space/[workspaceId]/(integrations)/whatsapps/[id]/automation/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(integrations)/whatsapps/[id]/automation/page.tsx @@ -28,7 +28,7 @@ export default async function WhatsappAutomationPage(props: { error: err instanceof Error ? err.message - : "Failed to load automation settings", + : "Falha ao carregar configurações de automação", })), ]) diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/admins/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/admins/page.tsx index 8a80e6dd0..a4011abb0 100644 --- a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/admins/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/admins/page.tsx @@ -1,9 +1,3 @@ -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@chatbotx.io/ui/components/ui/card" import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" @@ -31,13 +25,9 @@ export default async function SettingsAdminPage({ ]) return ( - - - {t("admins.title")} - - - - - +
+

{t("admins.title")}

+ +
) } diff --git a/apps/builder/src/app/space/[workspaceId]/(enterprise)/audit-logs/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/audit-logs/page.tsx similarity index 55% rename from apps/builder/src/app/space/[workspaceId]/(enterprise)/audit-logs/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(settings)/settings/audit-logs/page.tsx index 6265c8850..9096b7ab6 100644 --- a/apps/builder/src/app/space/[workspaceId]/(enterprise)/audit-logs/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/audit-logs/page.tsx @@ -2,9 +2,14 @@ import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" import type { SearchParams } from "nuqs/server" import { Suspense } from "react" -import { AuditLogsTable } from "@/enterprise/features/audit-logs/audit-logs-table" -import { listAuditLogs } from "@/enterprise/features/audit-logs/queries" -import { listAuditLogsSearchParamsCache } from "@/enterprise/features/audit-logs/schemas/query" +import { AuditLogsTable } from "@/features/audit-logs/audit-logs-table" +import { + listAuditLogActions, + listAuditLogs, +} from "@/features/audit-logs/queries" +import { listAuditLogsSearchParamsCache } from "@/features/audit-logs/schemas/query" +import { listWorkspaceMembers } from "@/features/workspace-members/queries" +import { getWorkspaceMembersSearchParamsCache } from "@/features/workspace-members/schema/query" export default async function AuditLogsPage(props: { params: Promise<{ workspaceId: string }> @@ -19,9 +24,11 @@ export default async function AuditLogsPage(props: { const search = listAuditLogsSearchParamsCache.parse(searchParams) const promises = Promise.all([ - listAuditLogs({ - ...search, + listAuditLogs({ ...search, workspaceId }), + listAuditLogActions(workspaceId), + listWorkspaceMembers({ workspaceId, + ...getWorkspaceMembersSearchParamsCache.parse({}), }), ]) diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/bot-fields/layout.tsx similarity index 85% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/layout.tsx rename to apps/builder/src/app/space/[workspaceId]/(settings)/settings/bot-fields/layout.tsx index 61324c31e..813f6d556 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/bot-fields/layout.tsx @@ -3,13 +3,11 @@ import { notFound } from "next/navigation" import type { ReactNode } from "react" import { FolderStoreProvider } from "@/features/folders/provider/folder-store-context" -export default async function FolderableLayout({ +export default async function BotFieldsSettingsLayout({ children, - folders, params, }: { children: ReactNode - folders: ReactNode params: Promise<{ workspaceId: string }> }) { const workspaceId = getIdFromParams(await params, "workspaceId") @@ -19,7 +17,6 @@ export default async function FolderableLayout({ return ( - {folders} {children} ) diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/bot-fields/page.tsx similarity index 100% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/bot-fields/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(settings)/settings/bot-fields/page.tsx diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/closing-notes/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/closing-notes/page.tsx new file mode 100644 index 000000000..be4c21827 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/closing-notes/page.tsx @@ -0,0 +1,30 @@ +import { db, eq } from "@chatbotx.io/database/client" +import { workspaceModel } from "@chatbotx.io/database/schema" +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import { ClosingNotesSettings } from "@/features/closing-notes/closing-notes-settings" +import { listConversationCategories } from "@/features/closing-notes/queries/list-categories" + +export default async function ClosingNotesPage(props: { + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await props.params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + const [workspace] = await db + .select({ closingNotesMode: workspaceModel.closingNotesMode }) + .from(workspaceModel) + .where(eq(workspaceModel.id, workspaceId)) + + const categories = await listConversationCategories(workspaceId) + + return ( + + ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/contact-fields/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/contact-fields/layout.tsx new file mode 100644 index 000000000..c2788c3a5 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/contact-fields/layout.tsx @@ -0,0 +1,23 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import type { ReactNode } from "react" +import { FolderStoreProvider } from "@/features/folders/provider/folder-store-context" + +export default async function ContactFieldsSettingsLayout({ + children, + params, +}: { + children: ReactNode + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + return ( + + {children} + + ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/contact-fields/page.tsx similarity index 63% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(settings)/settings/contact-fields/page.tsx index b0772fd9f..9d4c2fcca 100644 --- a/apps/builder/src/app/space/[workspaceId]/(has-folder)/custom-fields/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/contact-fields/page.tsx @@ -5,9 +5,10 @@ import type { SearchParams } from "nuqs/server" import { Suspense } from "react" import { CustomFieldsTable } from "@/features/custom-fields/custom-field-table" import { listCustomFieldsRSC } from "@/features/custom-fields/queries" +import { listHiddenContactFieldKeys } from "@/features/custom-fields/queries/visibility" import { listCustomFieldsSearchParams } from "@/features/custom-fields/schemas/query" -export default async function CustomFieldsPage(props: { +export default async function ContactFieldsPage(props: { params: Promise<{ workspaceId: string }> searchParams: Promise }) { @@ -28,10 +29,26 @@ export default async function CustomFieldsPage(props: { }), ]) + // Pro dialog "Personalizar visualização" precisamos da lista completa + // de custom fields (não paginada) pra mostrar todos. Cap em 500 cobre + // qualquer workspace realista. + const [allCustomFields, initialHiddenKeys] = await Promise.all([ + listCustomFieldsRSC({ + ...search, + workspaceId, + folderId: undefined, + perPage: 500, + page: 1, + }), + listHiddenContactFieldKeys(workspaceId), + ]) + return ( diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/layout.tsx index 4b62fbad0..441631180 100644 --- a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/layout.tsx @@ -1,15 +1,9 @@ import type { ReactNode } from "react" -import { SettingsTab } from "./tab" type LayoutSettingProps = { children: ReactNode } export default function SettingLayout({ children }: LayoutSettingProps) { - return ( - <> - -
{children}
- - ) + return <>{children} } diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/lifecycle/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/lifecycle/page.tsx new file mode 100644 index 000000000..74756ae0c --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/lifecycle/page.tsx @@ -0,0 +1,28 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import { LifecycleStagesEditor } from "@/features/lifecycle-stages/lifecycle-stages-editor" +import { listLifecycleStages } from "@/features/lifecycle-stages/queries" + +export default async function LifecyclePage(props: { + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await props.params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + const stages = await listLifecycleStages(workspaceId) + + const initialStages = stages.map((s) => ({ + id: s.id, + key: s.key, + name: s.name, + icon: s.icon, + color: s.color, + position: s.position, + isDefault: s.isDefault, + isLost: s.isLost, + })) + + return +} diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/snippets/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/snippets/page.tsx new file mode 100644 index 000000000..7b7655174 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/snippets/page.tsx @@ -0,0 +1,22 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import { Suspense } from "react" +import { listSnippetsRSC } from "@/features/saved-replies/queries/list-snippets-rsc" +import { SnippetsTable } from "@/features/saved-replies/snippets-table" + +export default async function SnippetsPage(props: { + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await props.params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + const promises = Promise.all([listSnippetsRSC({ workspaceId })]) + + return ( + + + + ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tab.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tab.tsx deleted file mode 100644 index 59919cc4b..000000000 --- a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tab.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" -import { usePathname } from "next/navigation" -import { useTranslations } from "next-intl" -import { useMemo } from "react" -import { AppTab } from "@/components/app-tab" -import { useWorkspaceId } from "@/hooks/routing" - -export function SettingsTab() { - const t = useTranslations() - const pathname = usePathname() - - const workspaceId = useWorkspaceId() - - const tabs = useMemo( - () => [ - { - label: t("general.title"), - value: "general", - }, - { - label: t("channels.title"), - value: "channels", - }, - { - label: t("integrations.title"), - value: "integrations", - }, - { - label: t("admins.title"), - value: "admins", - }, - { - label: t("inboxTeams.title"), - value: "inbox-teams", - }, - // { - // label: t("billing.title"), - // value: "billing", - // }, - ], - [t], - ) - - const activeTab = useMemo(() => { - const segments = pathname.split("/") - return segments.at(-1) - }, [pathname]) - - return ( - ({ - label: tab.label, - href: `/space/${workspaceId}/settings/${tab.value}`, - isActive: activeTab === tab.value, - }))} - /> - ) -} diff --git a/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tags/layout.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tags/layout.tsx new file mode 100644 index 000000000..d9a1fff48 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tags/layout.tsx @@ -0,0 +1,23 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import type { ReactNode } from "react" +import { FolderStoreProvider } from "@/features/folders/provider/folder-store-context" + +export default async function TagsSettingsLayout({ + children, + params, +}: { + children: ReactNode + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + return ( + + {children} + + ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/page.tsx b/apps/builder/src/app/space/[workspaceId]/(settings)/settings/tags/page.tsx similarity index 100% rename from apps/builder/src/app/space/[workspaceId]/(has-folder)/tags/page.tsx rename to apps/builder/src/app/space/[workspaceId]/(settings)/settings/tags/page.tsx diff --git a/apps/builder/src/app/space/[workspaceId]/account/layout.tsx b/apps/builder/src/app/space/[workspaceId]/account/layout.tsx new file mode 100644 index 000000000..6d80771cf --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/account/layout.tsx @@ -0,0 +1,11 @@ +import type { ReactNode } from "react" +import { PersonalSettingsSidebar } from "@/features/account/personal-settings-sidebar" + +export default function AccountLayout({ children }: { children: ReactNode }) { + return ( +
+ +
{children}
+
+ ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/account/notifications/page.tsx b/apps/builder/src/app/space/[workspaceId]/account/notifications/page.tsx new file mode 100644 index 000000000..5157f722a --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/account/notifications/page.tsx @@ -0,0 +1,46 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { BellIcon } from "lucide-react" +import { notFound } from "next/navigation" +import { getTranslations } from "next-intl/server" +import { getMyWorkspaceMember } from "@/features/notifications/queries" +import { UpdateNotificationsForm } from "@/features/notifications/update-notifications-form" +import { getCurrentUserId } from "@/lib/auth/utils" + +export default async function NotificationsPage({ + params, +}: { + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + const userId = await getCurrentUserId() + if (!userId) { + return notFound() + } + + const workspaceMember = await getMyWorkspaceMember(workspaceId, userId) + if (!workspaceMember) { + return notFound() + } + + const t = await getTranslations("personalSettings.notificationsSection") + return ( +
+
+

+ + {t("title")} +

+

{t("subtitle")}

+
+ +
+ ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/account/page.tsx b/apps/builder/src/app/space/[workspaceId]/account/page.tsx new file mode 100644 index 000000000..5ac8e2006 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/account/page.tsx @@ -0,0 +1,19 @@ +import { UserIcon } from "lucide-react" +import { getTranslations } from "next-intl/server" +import { ProfileForm } from "@/features/account/profile-form" + +export default async function ProfilePage() { + const t = await getTranslations("personalSettings.profileSection") + return ( +
+
+

+ + {t("title")} +

+

{t("subtitle")}

+
+ +
+ ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/broadcasts/page.tsx b/apps/builder/src/app/space/[workspaceId]/broadcasts/page.tsx index a5d96d9a2..f9bf3af0b 100644 --- a/apps/builder/src/app/space/[workspaceId]/broadcasts/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/broadcasts/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" import type { SearchParams } from "nuqs/server" import { Suspense } from "react" +import { AutomationTabs } from "@/components/automation-tabs" import { BroadcastsTable } from "@/features/broadcasts/broadcasts-table" import { listBroadcasts } from "@/features/broadcasts/queries" import { getBroadcastsSearchParamsCache } from "@/features/broadcasts/schemas/query" @@ -29,6 +30,7 @@ export default async function BroadcastsPage(props: { return (
+

{t("broadcasts.title")}

diff --git a/apps/builder/src/app/space/[workspaceId]/contacts/layout.tsx b/apps/builder/src/app/space/[workspaceId]/contacts/layout.tsx new file mode 100644 index 000000000..9ab31ae41 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/contacts/layout.tsx @@ -0,0 +1,94 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import type { ReactNode } from "react" +import { listInboxTeams } from "@/enterprise/features/inbox-teams/queries" +import { InboxSidebarExpandButton } from "@/features/chat/components/inbox-sidebar-expand-button" +import { ChatStoreProvider } from "@/features/chat/store/chat-store-provider" +import { ContactsAreaSidebar } from "@/features/contacts/components/contacts-area-sidebar" +import { countBlockedContacts } from "@/features/contacts/queries/count-blocked-contacts" +import { CustomFieldStoreProvider } from "@/features/custom-fields/provider/custom-field-store-context" +import { InboxStoreProvider } from "@/features/inboxes/provider/inbox-store-context" +import { + countOpenConversationsByAssignment, + countOpenConversationsByLifecycle, + countUnreadConversationsByAssignment, + countUnreadConversationsByLifecycle, + listLifecycleStages, +} from "@/features/lifecycle-stages/queries" +import { TagStoreProvider } from "@/features/tags/provider/tag-store-context" +import { UserStoreProvider } from "@/features/users/provider/user-store-context" +import { getCurrentUserId } from "@/lib/auth/utils" + +// Layout da página /contacts (Pedro 2026-05-26): sidebar lateral fixo +// literalmente o mesmo do Inbox (mesmo visual + mesmos items: Caixas, +// Times, Ciclo de vida, Bloqueados footer). Toda a árvore de Providers +// (ChatStore, InboxStore, TagStore, UserStore, CustomFieldStore) é +// idêntica à do /inbox/layout.tsx — pra que o drawer "Detalhes do +// contato" reuse o componente FULL (com inline edit, +// custom fields, accordion hidden, tags reais). +export default async function ContactsLayout({ + children, + params, +}: { + children: ReactNode + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + const userId = await getCurrentUserId() + if (!userId) { + return notFound() + } + + const [{ data: inboxTeams }, lifecycleStages] = await Promise.all([ + listInboxTeams({ workspaceId }), + listLifecycleStages(workspaceId), + ]) + const teamIds = (inboxTeams ?? []).map((t) => t.id) + const [ + lifecycleCounts, + assignmentCounts, + unreadByStage, + unreadByAssignment, + blockedContactsCount, + ] = await Promise.all([ + countOpenConversationsByLifecycle(workspaceId), + countOpenConversationsByAssignment(workspaceId, userId, teamIds), + countUnreadConversationsByLifecycle(workspaceId), + countUnreadConversationsByAssignment(workspaceId, userId, teamIds), + countBlockedContacts(workspaceId), + ]) + + return ( +
+ + + + + + + +
+ {children} +
+
+
+
+
+
+
+ ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/contacts/page.tsx b/apps/builder/src/app/space/[workspaceId]/contacts/page.tsx index 9ad04aab8..26c40610d 100644 --- a/apps/builder/src/app/space/[workspaceId]/contacts/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/contacts/page.tsx @@ -3,11 +3,13 @@ import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" import type { SearchParams } from "nuqs/server" import { Suspense } from "react" +import { PageHeader } from "@/components/page-header" import { ContactsTable } from "@/features/contacts/contacts-table" import { listContacts } from "@/features/contacts/queries/list-contacts.queries" import { listContactsRequest } from "@/features/contacts/schemas/query" import { CustomFieldStoreProvider } from "@/features/custom-fields/provider/custom-field-store-context" import { InboxStoreProvider } from "@/features/inboxes/provider/inbox-store-context" +import { listLifecycleStages } from "@/features/lifecycle-stages/queries" import { TagStoreProvider } from "@/features/tags/provider/tag-store-context" import { UserStoreProvider } from "@/features/users/provider/user-store-context" @@ -22,7 +24,16 @@ export default async function ContactsPage(props: { const t = await getTranslations() const searchParams = await props.searchParams - const { data: search } = listContactsRequest.safeParse(searchParams) + // safeParse precisa de workspaceId (vem do path, não da query). Normaliza + // lifecycleStageId single → array antes de parsear (sidebar manda single). + const normalized: Record = { + ...searchParams, + workspaceId, + } + if (typeof searchParams.lifecycleStageId === "string") { + normalized.lifecycleStageIds = [searchParams.lifecycleStageId] + } + const { data: search } = listContactsRequest.safeParse(normalized) const promises = Promise.all([ listContacts({ @@ -30,22 +41,28 @@ export default async function ContactsPage(props: { workspaceId, }), ]) + const lifecycleStages = await listLifecycleStages(workspaceId) return ( -
-

{t("contacts.title")}

- - - - - - - - - - - - +
+ +
+ + + + + + + + + + + +
) } diff --git a/apps/builder/src/app/space/[workspaceId]/error-logs/page.tsx b/apps/builder/src/app/space/[workspaceId]/error-logs/page.tsx index e2bb2fddf..942600073 100644 --- a/apps/builder/src/app/space/[workspaceId]/error-logs/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/error-logs/page.tsx @@ -1,9 +1,7 @@ import { getIdFromParams } from "@chatbotx.io/utils" import { notFound } from "next/navigation" -import { getTranslations } from "next-intl/server" import type { SearchParams } from "nuqs/server" import { Suspense } from "react" -import { AppTab } from "@/components/app-tab" import { ErrorLogsTable } from "@/features/error-logs/error-logs-table" import { listErrorLogs } from "@/features/error-logs/queries" import { listErrorLogsSearchParamsCache } from "@/features/error-logs/schemas/query" @@ -12,8 +10,6 @@ export default async function ErrorLogsPage(props: { params: Promise<{ workspaceId: string }> searchParams: Promise }) { - const t = await getTranslations() - const workspaceId = getIdFromParams(await props.params, "workspaceId") if (!workspaceId) { return notFound() @@ -29,35 +25,11 @@ export default async function ErrorLogsPage(props: { }), ]) + // AppTab removido — Logs de Erro agora aparece no sub-sidebar de + // Configurações em vez de tabs na aba Fluxos. return ( -
- - - - -
+ + + ) } diff --git a/apps/builder/src/app/space/[workspaceId]/inbox/layout.tsx b/apps/builder/src/app/space/[workspaceId]/inbox/layout.tsx new file mode 100644 index 000000000..4de52c495 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/inbox/layout.tsx @@ -0,0 +1,99 @@ +import { getIdFromParams } from "@chatbotx.io/utils" +import { notFound } from "next/navigation" +import type { ReactNode } from "react" +import { InboxAreaSidebar } from "@/components/inbox-area-sidebar" +import { listInboxTeams } from "@/enterprise/features/inbox-teams/queries" +import { InboxSidebarExpandButton } from "@/features/chat/components/inbox-sidebar-expand-button" +import { ChatStoreProvider } from "@/features/chat/store/chat-store-provider" +import { countBlockedContacts } from "@/features/contacts/queries/count-blocked-contacts" +import { CustomFieldStoreProvider } from "@/features/custom-fields/provider/custom-field-store-context" +import { FlowStoreProvider } from "@/features/flows/provider/flow-store-context" +import { InboxStoreProvider } from "@/features/inboxes/provider/inbox-store-context" +import { + countOpenConversationsByAssignment, + countOpenConversationsByLifecycle, + countUnreadConversationsByAssignment, + countUnreadConversationsByLifecycle, + listLifecycleStages, +} from "@/features/lifecycle-stages/queries" +import { SavedReplyStoreProvider } from "@/features/saved-replies/provider/saved-reply-store-context" +import { TagStoreProvider } from "@/features/tags/provider/tag-store-context" +import { UserStoreProvider } from "@/features/users/provider/user-store-context" +import { getCurrentUserId } from "@/lib/auth/utils" + +export default async function InboxLayout({ + children, + params, +}: { + children: ReactNode + params: Promise<{ workspaceId: string }> +}) { + const workspaceId = getIdFromParams(await params, "workspaceId") + if (!workspaceId) { + return notFound() + } + + const userId = await getCurrentUserId() + if (!userId) { + return notFound() + } + + // Carrega tudo paralelo. Os badges do sidebar mostram CONVERSAS ABERTAS + // (não contatos) pra alinhar com Respond.io (docs: + // markdown-raw/inbox/getting-started-with-inbox.md). + const [{ data: inboxTeams }, lifecycleStages] = await Promise.all([ + listInboxTeams({ workspaceId }), + listLifecycleStages(workspaceId), + ]) + const teamIds = (inboxTeams ?? []).map((t) => t.id) + const [ + lifecycleCounts, + assignmentCounts, + unreadByStage, + unreadByAssignment, + blockedContactsCount, + ] = await Promise.all([ + countOpenConversationsByLifecycle(workspaceId), + countOpenConversationsByAssignment(workspaceId, userId, teamIds), + countUnreadConversationsByLifecycle(workspaceId), + countUnreadConversationsByAssignment(workspaceId, userId, teamIds), + countBlockedContacts(workspaceId), + ]) + + return ( +
+ + + + + + + + + +
+ {children} +
+
+
+
+
+
+
+
+
+ ) +} diff --git a/apps/builder/src/app/space/[workspaceId]/inbox/page.tsx b/apps/builder/src/app/space/[workspaceId]/inbox/page.tsx index d6e8417ee..b56c14815 100644 --- a/apps/builder/src/app/space/[workspaceId]/inbox/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/inbox/page.tsx @@ -2,13 +2,9 @@ import { getIdFromParams } from "@chatbotx.io/utils" import { cookies } from "next/headers" import { notFound } from "next/navigation" import { ChatLayout } from "@/features/chat/chat-layout" -import { ChatStoreProvider } from "@/features/chat/store/chat-store-provider" -import { CustomFieldStoreProvider } from "@/features/custom-fields/provider/custom-field-store-context" -import { FlowStoreProvider } from "@/features/flows/provider/flow-store-context" -import { InboxStoreProvider } from "@/features/inboxes/provider/inbox-store-context" -import { SavedReplyStoreProvider } from "@/features/saved-replies/provider/saved-reply-store-context" -import { TagStoreProvider } from "@/features/tags/provider/tag-store-context" -import { UserStoreProvider } from "@/features/users/provider/user-store-context" +import { getClosingNotesConfig } from "@/features/closing-notes/queries/get-config" +import { listHiddenContactFieldKeys } from "@/features/custom-fields/queries/visibility" +import { listLifecycleStages } from "@/features/lifecycle-stages/queries" type InboxPageProps = { params: Promise<{ workspaceId: string }> @@ -21,31 +17,30 @@ export default async function InboxPage({ params }: InboxPageProps) { } const layout = (await cookies()).get("csm:layout:inbox") - const savedLayout = layout ? JSON.parse(layout.value) : [25, 50, 25] + // Default Respond.io (Pedro iter 36, medido via Chrome MCP, viewport 2560 + // sidebar Inbox colapsada): + // - Lista chats: 477px (19.34% da área redimensionável) + // - Mensagens: 1640px (66.48%) + // - Drawer: 350px (14.18%) + // Cookie persistido tem prioridade — usuário mantém ajustes próprios. + const savedLayout = layout ? JSON.parse(layout.value) : [19, 67, 14] + const [lifecycleStages, hiddenFieldKeys, closingNotesConfig] = + await Promise.all([ + listLifecycleStages(workspaceId), + listHiddenContactFieldKeys(workspaceId), + getClosingNotesConfig(workspaceId), + ]) return ( -
- - - - - - - - - - - - - - - +
+
) } diff --git a/apps/builder/src/app/space/[workspaceId]/layout.tsx b/apps/builder/src/app/space/[workspaceId]/layout.tsx index 1ba8427db..37657955b 100644 --- a/apps/builder/src/app/space/[workspaceId]/layout.tsx +++ b/apps/builder/src/app/space/[workspaceId]/layout.tsx @@ -1,14 +1,17 @@ -import { workspaceMemberService } from "@chatbotx.io/business" +import { + organizationService, + workspaceMemberService, +} from "@chatbotx.io/business" import { SidebarInset, SidebarProvider, - SidebarTrigger, } from "@chatbotx.io/ui/components/ui/sidebar" import { getIdFromParams } from "@chatbotx.io/utils" -import { cookies } from "next/headers" import { notFound } from "next/navigation" import { AppSidebar } from "@/components/app-sidebar" +import { SettingsAreaSidebar } from "@/components/settings-area-sidebar" import { getCurrentUserId } from "@/lib/auth/utils" +import { getDomainFromHeader } from "@/lib/domain" export default async function WorkspaceLayout({ children, @@ -43,16 +46,32 @@ export default async function WorkspaceLayout({ (workspaceMember) => workspaceMember.workspace, ) - const cookieStore = await cookies() - const defaultOpen = cookieStore.get("sidebar_state")?.value === "true" + const domain = await getDomainFromHeader() + let organization: Awaited< + ReturnType + > | null = null + try { + organization = await organizationService.findByDomain(domain) + } catch { + organization = null + } return ( - - + + - - -
{children}
+
+ +
+ {children} +
+
) diff --git a/apps/builder/src/app/space/[workspaceId]/notifications/page.tsx b/apps/builder/src/app/space/[workspaceId]/notifications/page.tsx new file mode 100644 index 000000000..a472156e4 --- /dev/null +++ b/apps/builder/src/app/space/[workspaceId]/notifications/page.tsx @@ -0,0 +1,10 @@ +import { redirect } from "next/navigation" + +export default async function NotificationsRedirect({ + params, +}: { + params: Promise<{ workspaceId: string }> +}) { + const { workspaceId } = await params + redirect(`/space/${workspaceId}/account/notifications`) +} diff --git a/apps/builder/src/app/space/[workspaceId]/webchats/[id]/edit/page.tsx b/apps/builder/src/app/space/[workspaceId]/webchats/[id]/edit/page.tsx index 346ef0a62..2360f0ad2 100644 --- a/apps/builder/src/app/space/[workspaceId]/webchats/[id]/edit/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/webchats/[id]/edit/page.tsx @@ -22,7 +22,7 @@ export default async function WebchatEditPage({ return ( - Loading...
}> + Carregando...
}>
diff --git a/apps/builder/src/app/space/[workspaceId]/webchats/create/page.tsx b/apps/builder/src/app/space/[workspaceId]/webchats/create/page.tsx index 851801d4f..a6f829694 100644 --- a/apps/builder/src/app/space/[workspaceId]/webchats/create/page.tsx +++ b/apps/builder/src/app/space/[workspaceId]/webchats/create/page.tsx @@ -15,7 +15,7 @@ export default async function CreateWebchatPage({ } return ( - Loading...
}> + Carregando...
}> diff --git a/apps/builder/src/components/app-sidebar.tsx b/apps/builder/src/components/app-sidebar.tsx index c06d617d3..bf793a5cc 100644 --- a/apps/builder/src/components/app-sidebar.tsx +++ b/apps/builder/src/components/app-sidebar.tsx @@ -7,26 +7,21 @@ import { SidebarHeader, } from "@chatbotx.io/ui/components/ui/sidebar" import { - AtomIcon, - BrainIcon, - ChartPieIcon, - ChevronsRight, - LightbulbIcon, - MessageCircleMoreIcon, - RadioIcon, - SlidersHorizontalIcon, - UsersIcon, - WebhookIcon, - WorkflowIcon, - WrenchIcon, -} from "lucide-react" + Brodcast, + DirectInbox, + Element2, + Setting2, + UserSquare, +} from "iconsax-reactjs" import Link from "next/link" import { useTranslations } from "next-intl" import type { ComponentProps } from "react" import { BrandIcon } from "@/components/brand-icon" +import { type IconsaxComponent, wrapIconsax } from "@/components/iconsax-icon" import { NavMain } from "@/components/nav-main" import { NavUser } from "@/components/nav-user" +import { RespondAIAgentIcon, RespondFlowIcon } from "@/components/respond-icons" import { WorkspaceSwitcher } from "@/components/workspace-switcher" import type { WorkspaceResource } from "@/features/workspaces/schema/resource" import { authClient } from "@/lib/auth/auth-client" @@ -34,10 +29,12 @@ import { authClient } from "@/lib/auth/auth-client" export function AppSidebar({ workspaceId, allWorkspaces, + organization, ...props }: ComponentProps & { workspaceId: string allWorkspaces: WorkspaceResource[] + organization?: { id: string; name: string } | null }) { const t = useTranslations() const { data: session } = authClient.useSession() @@ -48,88 +45,85 @@ export function AppSidebar({ email: session?.user.email ?? "", avatar: session?.user.image ?? "", }, + // Ordem definida pelo Pedro 2026-05-26: + // Análises → Inbox → Contatos → Agentes de IA → Transmissão → Fluxos → + // Configurações. navMain: [ { title: t("fields.analytics.label"), url: `/space/${workspaceId}/dashboard`, - icon: ChartPieIcon, + // Equivale ao `icon-element-2` (Dashboard no Respond.io) + icon: wrapIconsax(Element2 as IconsaxComponent), }, { title: t("fields.inbox.label"), url: `/space/${workspaceId}/inbox`, - icon: MessageCircleMoreIcon, - }, - { - title: t("fields.flows.label"), - url: `/space/${workspaceId}/flows`, - icon: WorkflowIcon, + // Equivale ao `icon-direct-inbox` (Inbox no Respond.io) + icon: wrapIconsax(DirectInbox as IconsaxComponent), }, { title: t("fields.contacts.label"), url: `/space/${workspaceId}/contacts`, - icon: UsersIcon, + icon: wrapIconsax(UserSquare as IconsaxComponent), }, { title: t("aiAgent.title"), url: `/space/${workspaceId}/ai-agents`, - icon: BrainIcon, + // AI Agent — robô do Respond.io extraído da fonte deles (pixel-perfect). + // Substitui o CpuCharge da Iconsax. + icon: RespondAIAgentIcon, }, { - title: t("keywords.title"), + title: t("automation.title"), url: `/space/${workspaceId}/automated-responses`, - icon: AtomIcon, - }, - { - title: t("broadcasts.title"), - url: `/space/${workspaceId}/broadcasts`, - icon: RadioIcon, - }, - { - title: t("sequences.title"), - url: `/space/${workspaceId}/sequences`, - icon: ChevronsRight, - }, - { - title: t("triggers.title"), - url: `/space/${workspaceId}/triggers`, - icon: LightbulbIcon, - }, - { - title: t("webhooks.title"), - url: `/space/${workspaceId}/webhooks`, - icon: WebhookIcon, + // Equivale ao `icon-custom-broadcast` (Broadcast/Automação). + // Usamos `Brodcast` (sic — nome real na lib). + icon: wrapIconsax(Brodcast as IconsaxComponent), }, { - title: t("tools.title"), - url: `/space/${workspaceId}/tools`, - icon: WrenchIcon, + title: t("fields.flows.label"), + url: `/space/${workspaceId}/flows`, + // Flow — ícone do Respond.io extraído da fonte deles (pixel-perfect). + // Substitui o Hierarchy3 da Iconsax que era só aproximação visual. + icon: RespondFlowIcon, }, { title: t("settings.title"), url: `/space/${workspaceId}/settings/general`, - icon: SlidersHorizontalIcon, + // Equivale ao `icon-settings`. Usamos `Setting2` da Iconsax. + icon: wrapIconsax(Setting2 as IconsaxComponent), }, ], } return ( - - - - -
- + {/* Header — só o WorkspaceSwitcher. pt-2 (8px) pra subir o AP e + alinhar o centro dele com o título do sub-sidebar (Caixa de Entrada). + Antes era pt-4 (16px) e o AP ficava 12px abaixo do título do sub. */} + +
+
+ {/* Footer — NavUser + BrandIcon (embaixo, com link pra "/"). + Container do logo um pouco maior (h-12) pra acomodar o ícone de 32px + com respiro. */} + + + ) diff --git a/apps/builder/src/components/arrow-handle.tsx b/apps/builder/src/components/arrow-handle.tsx new file mode 100644 index 000000000..0c2a4dd2f --- /dev/null +++ b/apps/builder/src/components/arrow-handle.tsx @@ -0,0 +1,60 @@ +import { cn } from "@chatbotx.io/ui/lib/utils" +import { Handle, type HandleProps, useNodeConnections } from "@xyflow/react" +import { ArrowRight2 } from "iconsax-reactjs" + +/** + * Handle estilo BotConversa: bolinha maior (20px) com setinha `>` dentro, + * posicionada na borda externa do node (metade dentro, metade fora). Usada + * pros source handles do fluxo principal — feedback visual claro de "saída" + * pra próxima ação. + * + * Difere do `BaseHandle` (handles menores 11px sem ícone, usado em handles + * internos como botões de quick reply). + * + * Cor do círculo personalizável via `handleColor` / `handleBorderColor` + * (classes Tailwind tipo `!bg-emerald-500 !border-emerald-600`). Pedro pediu + * cor distinta por tipo de node. + */ +export type ArrowHandleProps = HandleProps & { + ref?: React.RefObject + /** Classes Tailwind extras pra cor da bolinha (override do default). */ + colorClassName?: string +} + +export const ArrowHandle = ( + props: ArrowHandleProps & { type?: "source" | "target" }, +) => { + const { ref, className, colorClassName, children, ...rest } = props + + const connections = useNodeConnections({ + handleType: rest.type, + handleId: rest.id ?? "", + }) + + return ( + 0 && "!bg-blue-600 !border-blue-700", + colorClassName, + className, + )} + {...rest} + ref={ref} + > + {/* Iconsax `ArrowRight2` — setinha estilo BotConversa */} + + + + {children} + + ) +} + +ArrowHandle.displayName = "ArrowHandle" diff --git a/apps/builder/src/components/automation-tabs.tsx b/apps/builder/src/components/automation-tabs.tsx new file mode 100644 index 000000000..900ebcd5b --- /dev/null +++ b/apps/builder/src/components/automation-tabs.tsx @@ -0,0 +1,41 @@ +"use client" + +import { Tabs, TabsList, TabsTrigger } from "@chatbotx.io/ui/components/ui/tabs" +import Link from "next/link" +import { useParams, usePathname } from "next/navigation" +import { useTranslations } from "next-intl" + +type AutomationTab = { + value: "automated-responses" | "broadcasts" | "sequences" + labelKey: "keywords.title" | "broadcasts.title" | "sequences.title" +} + +const TABS: AutomationTab[] = [ + { value: "automated-responses", labelKey: "keywords.title" }, + { value: "broadcasts", labelKey: "broadcasts.title" }, + { value: "sequences", labelKey: "sequences.title" }, +] + +export function AutomationTabs() { + const t = useTranslations() + const pathname = usePathname() + const params = useParams() + const workspaceId = params.workspaceId as string + + const base = `/space/${workspaceId}` + const activeTab = + TABS.find((tab) => pathname.startsWith(`${base}/${tab.value}`))?.value ?? + TABS[0].value + + return ( + + + {TABS.map((tab) => ( + + {t(tab.labelKey)} + + ))} + + + ) +} diff --git a/apps/builder/src/components/brand-icon.tsx b/apps/builder/src/components/brand-icon.tsx index ea165f323..09ac4cb6a 100644 --- a/apps/builder/src/components/brand-icon.tsx +++ b/apps/builder/src/components/brand-icon.tsx @@ -12,7 +12,7 @@ type BrandIconProps = { } export const BrandIcon = ({ - alt = "Brand Icon", + alt = "Ícone da marca", className, }: BrandIconProps) => { const currentTheme = useCurrentTheme() @@ -31,8 +31,11 @@ export const BrandIcon = ({ currentTheme === "dark" ? "/brand/logo_white.svg" : "/brand/logo_black.svg" const logoSrc = logo || baseLogoSrc + // Icon collapsed — usa os novos PNGs do logo X1 do Pedro (2026-05-24). + // PNGs originais: x1_white.png pro dark, x1_black.png pro light. + // Caso o arquivo não exista, cai pros SVGs antigos. const baseIconSrc = - currentTheme === "dark" ? "/brand/icon_white.svg" : "/brand/icon_black.svg" + currentTheme === "dark" ? "/brand/x1_white.png" : "/brand/x1_black.png" const iconSrc = logo || baseIconSrc return ( @@ -48,17 +51,20 @@ export const BrandIcon = ({ src={logoSrc} width={10} /> - {/* Icon - shown when collapsed */} + {/* Icon - shown when collapsed (default state do sidebar global do app). + h-8/w-8 (32px display) com width/height=128 pra Next/Image gerar uma + variant maior e nítida — antes ficava 32×32 borrado parecendo um X + de close button. */} {alt} ) diff --git a/apps/builder/src/components/dev-click-to-component.tsx b/apps/builder/src/components/dev-click-to-component.tsx new file mode 100644 index 000000000..9c3f64f43 --- /dev/null +++ b/apps/builder/src/components/dev-click-to-component.tsx @@ -0,0 +1,31 @@ +"use client" + +/** + * Wrapper do `click-to-react-component` que SÓ carrega em desenvolvimento. + * + * Permite que o Pedro segure **⌥ Option** (Mac) e clique em qualquer elemento + * da UI pra abrir o arquivo React correspondente direto no Cursor, com o + * cursor posicionado na linha exata. Pra ajustes finos de espaçamento, cor, + * etc, basta clicar e pedir "mova 4px pra cima" no chat do Cursor. + * + * Em produção, retorna `null` e o `dynamic({ ssr: false })` impede que o pkg + * seja incluído no bundle. + */ + +import dynamic from "next/dynamic" + +const ClickToComponent = dynamic( + () => + import("click-to-react-component").then((mod) => ({ + default: mod.ClickToComponent, + })), + { ssr: false }, +) + +export function DevClickToComponent() { + if (process.env.NODE_ENV !== "development") { + return null + } + // editor="cursor" → abre via cursor:// protocol; configurado por padrão no Cursor. + return +} diff --git a/apps/builder/src/components/file-dropzone.tsx b/apps/builder/src/components/file-dropzone.tsx index baebf5aeb..6d1191674 100644 --- a/apps/builder/src/components/file-dropzone.tsx +++ b/apps/builder/src/components/file-dropzone.tsx @@ -195,7 +195,7 @@ export default function FileDropzone({ return ( <> Thumbnail @@ -257,7 +257,7 @@ export default function FileDropzone({ -

Upload File

+

Enviar Arquivo

diff --git a/apps/builder/src/components/iconsax-icon.tsx b/apps/builder/src/components/iconsax-icon.tsx new file mode 100644 index 000000000..c6ca31797 --- /dev/null +++ b/apps/builder/src/components/iconsax-icon.tsx @@ -0,0 +1,51 @@ +"use client" + +import type { ComponentType, ReactElement } from "react" + +/** + * Helper compartilhado pra usar ícones da biblioteca pública `iconsax-reactjs` + * (MIT) com a API "className" que o resto do app usa (estilo Lucide). + * + * Iconsax usa `size`/`color`/`variant` em vez de className. O wrapper aqui + * renderiza um `` com a className recebida + o ícone Iconsax dentro + * com `size="100%"` e `color="currentColor"` (herda cor do parent). + * + * Padrão recomendado de uso em qualquer arquivo: + * import { wrapIconsax } from "@/components/iconsax-icon" + * import { Add, ArrowLeft2, Edit2 } from "iconsax-reactjs" + * + * const AddIcon = wrapIconsax(Add) + * + */ +export type IconsaxComponent = ComponentType<{ + size?: string | number + color?: string + variant?: "Linear" | "Outline" | "Broken" | "Bold" | "Bulk" | "TwoTone" +}> + +export type WrappedIcon = (props: { className?: string }) => ReactElement + +/** + * @param Component componente Iconsax (ex.: `Add`, `ArrowLeft2`, etc.) + * @param variant variant default ("Outline" combina com o tema do app) + */ +export function wrapIconsax( + Component: IconsaxComponent, + variant: + | "Linear" + | "Outline" + | "Broken" + | "Bold" + | "Bulk" + | "TwoTone" = "Outline", +): WrappedIcon { + const Wrapped: WrappedIcon = ({ className }) => ( + + + + ) + // biome-ignore lint/suspicious/noExplicitAny: precisamos atribuir displayName + ;(Wrapped as any).displayName = + Component.displayName || Component.name || "IconsaxIcon" + return Wrapped +} diff --git a/apps/builder/src/components/image-dropzone.tsx b/apps/builder/src/components/image-dropzone.tsx index d4fe3f6d2..b826f101a 100644 --- a/apps/builder/src/components/image-dropzone.tsx +++ b/apps/builder/src/components/image-dropzone.tsx @@ -20,7 +20,7 @@ function AttachedImage({ return (
Uploaded Image 9999 ? "9,999+" : count.toLocaleString("pt-BR") +} + +function renderCountBadge( + count: number, + hasUnread: boolean, + t: ReturnType, +) { + if (hasUnread) { + // O número virou indicador "nova mensagem" — pill azul brilhante. + // Quando count for 0 (raro mas possível), mostra o pill mesmo assim + // pra sinalizar que TEM unread. + return ( + + {count > 0 ? formatCount(count) : ""} + + ) + } + if (count > 0) { + // Só contagem de conversas abertas, sem unread — número simples negrito. + return ( + + {formatCount(count)} + + ) + } + return null +} + +type InboxFilterItem = { + key: string + label: string + icon: LucideIcon + assignedId: string + isActive: (currentAssignedId: string | undefined) => boolean + onSelect: () => void +} + +type AssignmentCounts = { + all: number + mine: number + unassigned: number + teams: Record +} + +type InboxAreaSidebarProps = { + workspaceId: string + currentUserId: string + inboxTeams: InboxTeamModel[] + lifecycleStages: LifecycleStageModel[] + // Contagens de CONVERSAS ABERTAS por stage (badge ao lado de cada + // "Novo Lead", "Lead Quente" etc) — não é número de contatos. Alinhado + // com docs Respond.io ("Contagem de conversas abertas"). + lifecycleCounts: Record + // Contagens dos filtros top (Todos / Minhas / Não atribuídas + teams). + assignmentCounts: AssignmentCounts + // Recortes COM conversas NÃO LIDAS (contactLastReadAt > agentLastReadAt). + // Usados pro indicador "nova mensagem" (bolinha azul que segue prioridade + // Minhas > Equipe > Não atribuídas > Todas — doc Respond.io). Some quando + // filtro é ativo. + unreadByAssignment: AssignmentCounts + unreadByStage: Record + // Quantidade de contatos com `blockedAt IS NOT NULL`. Usado no item + // "Contatos bloqueados" do rodapé do sidebar (pixel-perfect Respond.io). + blockedContactsCount: number +} + +export function InboxAreaSidebar({ + workspaceId, + currentUserId, + inboxTeams, + lifecycleStages, + lifecycleCounts, + assignmentCounts, + unreadByAssignment, + unreadByStage, + blockedContactsCount, +}: InboxAreaSidebarProps) { + const t = useTranslations() + const [collapsed, toggleCollapsed] = useInboxSidebarCollapsed() + const filters = useChatStore((state) => state.filters) + const setFilters = useChatStore((state) => state.setFilters) + const resetState = useChatStore((state) => state.resetState) + const loadMoreConversations = useChatStore( + (state) => state.loadMoreConversations, + ) + + // Quando filter "Contatos bloqueados" tá ativo, ele é EXCLUSIVO: os + // outros filtros do topo (Todos/Minhas/Não atribuídas), teams e lifecycle + // stages perdem o highlight de "ativo" mesmo que o assignedId/lifecycleId + // ainda esteja setado no store. Pedro pediu 2026-05-25 — comportamento + // de "caixa especial" igual Respond.io. + const isBlockedFilterActive = filters.tags?.includes("blocked") ?? false + const currentAssignedId = isBlockedFilterActive + ? "__blocked__" + : (filters.assignedId ?? "all") + const currentLifecycleId = isBlockedFilterActive + ? null + : (filters.lifecycleStageId ?? null) + // Lost stages ficam OCULTOS no Inbox (igual Respond.io) — só aparecem nas configurações + const activeLifecycleStages = lifecycleStages.filter((s) => !s.isLost) + + // Lê o toggle global do localStorage (controla se a seção Ciclo de vida aparece) + const [lifecycleVisible, setLifecycleVisible] = useState(true) + useEffect(() => { + const stored = localStorage.getItem(LIFECYCLE_VISIBILITY_KEY) + if (stored !== null) { + setLifecycleVisible(stored === "true") + } + const handler = (e: StorageEvent) => { + if (e.key === LIFECYCLE_VISIBILITY_KEY && e.newValue !== null) { + setLifecycleVisible(e.newValue === "true") + } + } + window.addEventListener("storage", handler) + return () => window.removeEventListener("storage", handler) + }, []) + + // Polling 30s das contagens (mantém sidebar fresca quando uma conversa + // muda de stage / é arquivada / é atribuída / leitura). Re-busca todas + // as 4 séries (counts + unread por stage e assignment) numa só chamada. + const [polledLifecycle, setPolledLifecycle] = + useState>(lifecycleCounts) + const [polledAssignment, setPolledAssignment] = + useState(assignmentCounts) + const [polledUnreadStage, setPolledUnreadStage] = + useState>(unreadByStage) + const [polledUnreadAssignment, setPolledUnreadAssignment] = + useState(unreadByAssignment) + useEffect(() => { + let mounted = true + const teamIds = inboxTeams.map((t) => t.id) + const boundAction = getLifecycleCountsAction.bind(null, workspaceId) + const tick = async () => { + try { + const result = await boundAction({ teamIds }) + if (mounted && result?.data) { + setPolledLifecycle(result.data.byStage) + setPolledAssignment(result.data.byAssignment) + setPolledUnreadStage(result.data.unreadByStage) + setPolledUnreadAssignment(result.data.unreadByAssignment) + } + } catch { + // silent — manter contagem anterior + } + } + const interval = setInterval(tick, LIFECYCLE_COUNTS_POLL_MS) + return () => { + mounted = false + clearInterval(interval) + } + }, [workspaceId, inboxTeams.map]) + const counts = polledLifecycle + const assignCounts = polledAssignment + const unreadStage = polledUnreadStage + const unreadAssign = polledUnreadAssignment + + // Optimistic update da bolinha "nova mensagem": zera o recorte + // localmente assim que o user clica. UPDATE no DB roda em background; + // polling 30s reconfirma. Doc: "Ao abrir a caixa de entrada, o ponto + // azul desaparece." + const markAssignmentReadOptimistic = (assignedId: string) => { + // Cada filtro zera APENAS seu próprio recorte. "Todos" não toca em + // lifecycle stages (decisão Pedro 2026-05-24 — visão "Todos" é só + // mudar de visão, não confirma leitura geral). + setPolledUnreadAssignment((prev) => { + if (assignedId === "all") { + return { all: 0, mine: 0, unassigned: 0, teams: {} } + } + if (assignedId === `u_${currentUserId}`) { + return { ...prev, mine: 0, all: Math.max(0, prev.all - prev.mine) } + } + if (assignedId === "unassigned") { + return { + ...prev, + unassigned: 0, + all: Math.max(0, prev.all - prev.unassigned), + } + } + if (assignedId.startsWith("t_")) { + const teamId = assignedId.slice(2) + const teamCount = prev.teams[teamId] ?? 0 + return { + ...prev, + teams: { ...prev.teams, [teamId]: 0 }, + all: Math.max(0, prev.all - teamCount), + } + } + return prev + }) + } + + const markLifecycleReadOptimistic = (stageId: string) => { + setPolledUnreadStage((prev) => ({ ...prev, [stageId]: 0 })) + } + + // Hook do next-safe-action (chamada direta sem useAction não dispara + // o handler). EXCEÇÃO: "Todos" não confirma leitura — só muda visão. + const { execute: executeMarkFilterRead } = useAction( + markFilterReadAction.bind(null, workspaceId), + ) + + // Helper: remove a tag "blocked" do array (preservando outras tags). + // Top items / lifecycle / teams SEMPRE chamam isto pra desativar a + // "caixa de bloqueados" automaticamente quando o user volta pra um + // filtro normal — comportamento exclusivo igual sidebar do Respond.io + // (Pedro reforçou 2026-05-25: "quando volto pra Todos, sai do blocked"). + const stripBlocked = () => + (filters.tags ?? []).filter((tag) => tag !== "blocked") + + const applyFilter = (assignedId: string) => { + setFilters({ + ...filters, + assignedId, + lifecycleStageId: null, + tags: stripBlocked(), + }) + resetState() + loadMoreConversations(workspaceId) + markAssignmentReadOptimistic(assignedId) + if (assignedId === "all") { + // no-op + } else if (assignedId === `u_${currentUserId}`) { + executeMarkFilterRead({ scope: "mine", userId: currentUserId }) + } else if (assignedId === "unassigned") { + executeMarkFilterRead({ scope: "unassigned" }) + } else if (assignedId.startsWith("t_")) { + executeMarkFilterRead({ scope: "team", teamId: assignedId.slice(2) }) + } + } + + const applyLifecycleFilter = (stageId: string | null) => { + setFilters({ + ...filters, + lifecycleStageId: stageId, + tags: stripBlocked(), + }) + resetState() + loadMoreConversations(workspaceId) + if (stageId) { + markLifecycleReadOptimistic(stageId) + executeMarkFilterRead({ scope: "lifecycle", stageId }) + } + } + + // Filtro "Contatos bloqueados" (rodapé) — exclusivo dos filtros + // assignment/lifecycle. Clicar nele LIMPA assignedId/lifecycleStageId + // e ativa `tags: ["blocked"]`. Clicar de novo (toggle off) volta pra + // visão "Todos". Quando o user clica em qualquer outro filtro + // (Todos / Minhas / Lifecycle / Team), `stripBlocked()` desativa este + // automaticamente. + const applyBlockedFilter = () => { + if (isBlockedFilterActive) { + // Toggle off — volta pra visão padrão "Todos". + setFilters({ + ...filters, + tags: stripBlocked(), + assignedId: "all", + lifecycleStageId: null, + }) + } else { + // Toggle on — limpa outros filtros e ativa apenas blocked. + setFilters({ + ...filters, + tags: [...stripBlocked(), "blocked" as const], + assignedId: "all", + lifecycleStageId: null, + }) + } + resetState() + loadMoreConversations(workspaceId) + } + + // Prioridade da bolinha "nova mensagem" (doc Respond.io): + // Minhas > Equipe > Não atribuídas > Todas. + // Se um nível superior tem unread, esconde dos níveis inferiores. + const anyTeamUnread = Object.values(unreadAssign.teams).some((c) => c > 0) + const showAllDot = + unreadAssign.all > 0 && + unreadAssign.mine === 0 && + unreadAssign.unassigned === 0 && + !anyTeamUnread + const showMineDot = unreadAssign.mine > 0 + const showUnassignedDot = + unreadAssign.unassigned > 0 && unreadAssign.mine === 0 && !anyTeamUnread + const showTeamDot = (teamId: string): boolean => + (unreadAssign.teams[teamId] ?? 0) > 0 && unreadAssign.mine === 0 + + // Cada filtro top vem com a count real de conversas abertas naquele + // recorte + flag `showDot` que indica se DEVE renderizar a bolinha azul + // "nova mensagem" (segue prioridade Minhas > Equipe > Não atribuídas > + // Todas — doc Respond.io). Esconde quando filtro está ativo. + const topItems: Array = + [ + { + key: "all", + label: t("inboxFilters.all"), + icon: InboxIcon, + assignedId: "all", + count: assignCounts.all, + showDot: showAllDot, + isActive: (current) => + !current || + current === "all" || + (current !== "unassigned" && + !current.startsWith("u_") && + !current.startsWith("t_")), + onSelect: () => applyFilter("all"), + }, + { + key: "mine", + label: t("inboxFilters.mine"), + icon: UserIcon, + assignedId: `u_${currentUserId}`, + count: assignCounts.mine, + showDot: showMineDot, + isActive: (current) => current === `u_${currentUserId}`, + onSelect: () => applyFilter(`u_${currentUserId}`), + }, + { + key: "unassigned", + label: t("inboxFilters.unassigned"), + icon: UserSearchIcon, + assignedId: "unassigned", + count: assignCounts.unassigned, + showDot: showUnassignedDot, + isActive: (current) => current === "unassigned", + onSelect: () => applyFilter("unassigned"), + }, + ] + + // Collapsed: sidebar 215px some inteira. Botão "expandir" mora no + // chat-layout (renderiza só quando collapsed lendo do mesmo hook). + if (collapsed) { + return null + } + + return ( + // Pixel-perfect Respond.io 2026-05-24: + // bg `--app-surface` (#222225), w 215, border-right padrão. + // Header 43px com font 14/700 + search + collapse à direita. + // Filtros 32px h (medido no Respond ao vivo), lifecycle 32px h. + // Section labels uppercase letter-spacing pra parecer respond.io. + // Badge contagem com pill bg-primary (azul) igual mapping do Diego. + // Layout em 3 partes (Pedro pediu 2026-05-25 — pixel-perfect Respond.io): + // 1. Header 43px (título + collapse) + // 2. Conteúdo principal scrollável (filtros + teams + lifecycle) + // 3. Rodapé fixo com "Contatos bloqueados" (igual Respond.io) + + ) +} diff --git a/apps/builder/src/components/lang-selector.tsx b/apps/builder/src/components/lang-selector.tsx index 8edf97f8a..168832e43 100644 --- a/apps/builder/src/components/lang-selector.tsx +++ b/apps/builder/src/components/lang-selector.tsx @@ -33,6 +33,7 @@ export const LangSelector: React.FC = () => { {t("fields.language.english")} {t("fields.language.vietnamese")} + {t("fields.language.portuguese")} ) diff --git a/apps/builder/src/components/nav-main.tsx b/apps/builder/src/components/nav-main.tsx index a83ed6acf..50bb77581 100644 --- a/apps/builder/src/components/nav-main.tsx +++ b/apps/builder/src/components/nav-main.tsx @@ -6,9 +6,16 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@chatbotx.io/ui/components/ui/sidebar" -import type { LucideIcon } from "lucide-react" import Link from "next/link" import { usePathname } from "next/navigation" +import type { ComponentType } from "react" + +/** + * Componente genérico de ícone — aceita qualquer componente que recebe + * `className`. Compatível com Lucide (`LucideIcon`) e com wrappers de + * outras libs (ex.: Iconsax via wrapIconsax no app-sidebar). + */ +type NavIcon = ComponentType<{ className?: string }> export function NavMain({ items, @@ -16,7 +23,7 @@ export function NavMain({ items: { title: string url: string - icon?: LucideIcon + icon?: NavIcon isActive?: boolean items?: { title: string diff --git a/apps/builder/src/components/nav-user.tsx b/apps/builder/src/components/nav-user.tsx index bae18a984..77d525e0a 100644 --- a/apps/builder/src/components/nav-user.tsx +++ b/apps/builder/src/components/nav-user.tsx @@ -20,11 +20,31 @@ import { SidebarMenuItem, useSidebar, } from "@chatbotx.io/ui/components/ui/sidebar" -import { BadgeCheck, Bell, ChevronsUpDown, Crown } from "lucide-react" +import { + ArrowSwapVertical, + Buildings2, + Crown, + ShieldTick, +} from "iconsax-reactjs" +import { type IconsaxComponent, wrapIconsax } from "@/components/iconsax-icon" + +const BadgeCheck = wrapIconsax(ShieldTick as IconsaxComponent) +const ChevronsUpDown = wrapIconsax(ArrowSwapVertical as IconsaxComponent) +const CrownIcon = wrapIconsax(Crown as IconsaxComponent) +const OrgIcon = wrapIconsax(Buildings2 as IconsaxComponent) + +import Link from "next/link" +import { useParams } from "next/navigation" import { useTranslations } from "next-intl" +import { toast } from "sonner" +import { + ACTIVITY_STATUSES, + type ActivityStatus, + ActivityStatusDot, + isActivityStatus, +} from "@/features/account/activity-status" import { SignOut } from "@/features/auth/sign-out" -import { LangSelector } from "./lang-selector" -import { ThemeSwitcher } from "./theme-switcher" +import { authClient } from "@/lib/auth/auth-client" export function NavUser({ user, @@ -37,6 +57,31 @@ export function NavUser({ }) { const { isMobile } = useSidebar() const t = useTranslations() + const tStatus = useTranslations("personalSettings.activityStatus") + const params = useParams() + const workspaceId = params.workspaceId as string + + // Status real do user (session). Default "available" se ainda não setado. + const { data: session } = authClient.useSession() + const rawStatus = (session?.user as unknown as { activityStatus?: unknown }) + ?.activityStatus + const currentStatus: ActivityStatus = isActivityStatus(rawStatus) + ? rawStatus + : "available" + + const handleStatusChange = async (next: ActivityStatus) => { + if (next === currentStatus) { + return + } + const { error } = await authClient.updateUser({ + activityStatus: next, + } as unknown as Parameters[0]) + if (error) { + toast.error(error.message ?? tStatus("updateError")) + return + } + toast.success(tStatus("updateSuccess", { status: tStatus(next) })) + } return ( @@ -44,15 +89,21 @@ export function NavUser({ - - - - {user.name.slice(0, 2) || " "} - - +
+ + + + {user.name.slice(0, 2) || " "} + + + +
{user.name} @@ -70,12 +121,18 @@ export function NavUser({ >
- - - - {user.name.slice(0, 2) || " "} - - +
+ + + + {user.name.slice(0, 2) || " "} + + + +
{user.name} @@ -85,35 +142,46 @@ export function NavUser({
+ + {tStatus("sectionLabel")} + - - - {t("actions.upgradeToPro")} - - - - - - Language - - + {ACTIVITY_STATUSES.map((s) => ( + handleStatusChange(s)} + > + + {tStatus(s)} + {s === currentStatus && ( + + • + + )} + + ))} - Theme - + + {t("actions.upgradeToPro")} - - - Account + + + + {t("personalSettings.account")} + - - - Notifications + + + + {t("organization.manage")} + diff --git a/apps/builder/src/components/node-header.tsx b/apps/builder/src/components/node-header.tsx index 564976f4c..f679d847c 100644 --- a/apps/builder/src/components/node-header.tsx +++ b/apps/builder/src/components/node-header.tsx @@ -197,7 +197,7 @@ export const NodeHeaderDeleteAction = () => { }, [id, setNodes]) return ( - + ) diff --git a/apps/builder/src/components/page-header.tsx b/apps/builder/src/components/page-header.tsx new file mode 100644 index 000000000..5f1ae96b3 --- /dev/null +++ b/apps/builder/src/components/page-header.tsx @@ -0,0 +1,100 @@ +"use client" + +import { Button } from "@chatbotx.io/ui/components/ui/button" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@chatbotx.io/ui/components/ui/tooltip" +import { cn } from "@chatbotx.io/ui/lib/utils" +import { ArrowLeft2 } from "iconsax-reactjs" +import Link from "next/link" +import type { ReactNode } from "react" +import { type IconsaxComponent, wrapIconsax } from "@/components/iconsax-icon" + +const ArrowLeftIcon = wrapIconsax(ArrowLeft2 as IconsaxComponent) + +/** + * PageHeader — componente padronizado de header de página. + * + * Altura fixa **52px** (`h-[52px]`) — bate com o padrão do Respond.io + * (medido via Chrome MCP: Dashboard/Workflows/Settings = 53px). Pedro + * pediu padronização: "no Respond.io toda aba tem essa parte na horizontal + * do mesmo tamanho". + * + * Estrutura: + * - Botão de voltar opcional (se `backHref` for passado) + * - Título + subtítulo opcional + ações inline opcionais (esquerda) + * - Slot `actions` no extremo direito + * + * Cada página interna deve usar este componente em vez de header próprio + * pra manter o "linha imaginária" consistente entre abas. + */ +export type PageHeaderProps = { + /** Texto principal mostrado em destaque. */ + title: ReactNode + /** Subtítulo opcional ao lado do título (cor mutada). */ + subtitle?: ReactNode + /** Se passado, renderiza botão "voltar" no canto esquerdo. */ + backHref?: string + /** Aria-label do botão voltar (default: "Voltar"). */ + backLabel?: string + /** Conteúdo extra entre o título e o slot de ações (ex.: pencil). */ + titleAdornment?: ReactNode + /** Botões/ações no extremo direito. */ + actions?: ReactNode + /** Classes extras pra customizar (geralmente desnecessário). */ + className?: string +} + +export function PageHeader({ + title, + subtitle, + backHref, + backLabel = "Voltar", + titleAdornment, + actions, + className, +}: PageHeaderProps) { + return ( +
+ {backHref && ( + + + + + {backLabel} + + )} + +
+

{title}

+ {subtitle && ( + + {subtitle} + + )} + {titleAdornment} +
+ + {actions && ( +
{actions}
+ )} +
+ ) +} diff --git a/apps/builder/src/components/respond-icon.tsx b/apps/builder/src/components/respond-icon.tsx new file mode 100644 index 000000000..c9e8dd922 --- /dev/null +++ b/apps/builder/src/components/respond-icon.tsx @@ -0,0 +1,120 @@ +import { cn } from "@chatbotx.io/ui/lib/utils" +import type { HTMLAttributes } from "react" + +// Nomes de ícones disponíveis (mapeados em /src/app/respond-icons.css). Se +// precisar de outro, adicione no CSS + aqui. Lista completa de 848 está em +// _Projetos/respond-io-avatars/preview.html → consultar pra novos. +// 2026-05-24 — Pedro pediu "ícones idênticos ao Respond.io". +export type RespondIconName = + | "tick-circle" + | "tick-square" + | "tick-square-bold" + | "tick" + | "send" + | "search-normal" + | "more" + | "message-text" + | "message-question" + | "message-2" + | "emoji-happy" + | "close-circle" + | "attach-square" + | "arrow-down" + | "arrow-down-1" + | "arrow-up" + | "arrow-up-1" + | "arrow-left" + | "arrow-right" + | "ai-assist" + | "ai-agent" + | "add" + | "workflow" + | "user" + | "user-add" + | "flash" + | "sticker" + | "document" + | "star" + | "star-bold" + | "volume-high" + | "eye" + | "eye-bold" + | "edit" + | "edit-bold" + | "trash" + | "trash-bold" + | "forbidden" + | "forbidden-bold" + | "reply" + | "copy" + | "link-1" + | "message-delivered" + | "magicpen-bold" + | "emoji-normal" + | "i-microphone" + | "receipt" + | "message-programming" + | "send-bold" + | "comments-ai" + | "ai-assist-bold" + | "channels" + | "at" + | "close-square" + | "custom-user-square-bold" + | "translate" + | "code" + | "clock" + | "clock-bold" + | "sms" + | "sms-bold" + | "phone" + | "call-calling" + | "home" + | "global" + | "profile" + | "profile-2user" + | "note" + | "notification" + | "notification-bing" + | "tag" + | "tag-bold" + | "filter" + | "filter-bold" + | "sort" + | "verify-bold" + | "received" + | "whatsapp-bold" + | "instagram" + | "facebook" + | "email" + | "menu" + | "sidebar-left" + | "sidebar-right" + | "refresh" + | "refresh-circle" + | "archive" + | "minus" + | "play" + | "pause" + +type RespondIconProps = HTMLAttributes & { + name: RespondIconName + size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" +} + +// Wrapper pro iconfont Respond.io. Renderiza um com a classe correta. +// Sizes mapeados pro CSS: xs=12, sm=14, md=16, lg=18, xl=20, 2xl=24. +export function RespondIcon({ + name, + size = "lg", + className, + ...rest +}: RespondIconProps) { + return ( +