Compare commits

..
Author SHA1 Message Date
Vishnu Narayanan ef19fcd8c0 fix: test switchlocale method directly instead of via http routing 2025-11-21 16:31:14 +05:30
Vishnu Narayanan d261dfd394 fix: shopify and locale spec 2025-11-21 16:28:03 +05:30
Vishnu Narayanan 1b9673b272 fix: test switchlocale method directly instead of via http routing 2025-11-21 16:21:05 +05:30
Sivin VargheseandGitHub 45fa697885 chore: Hide pagination for empty company list (#12920) 2025-11-21 12:23:01 +05:30
Sivin VargheseandGitHub 51fbf583b6 fix: Companies card pluralization issue (#12919) 2025-11-21 12:22:02 +05:30
Sivin VargheseandGitHub 38d6ee6dd2 chore: Save company list sorting preferences in UI settings (#12916)
# Pull Request Template

## Description

This PR saves the company list sorting preferences (field and order) in
the user’s UI settings. The sort state is initialized from the stored
preferences when the component mounts, defaulting to `-created_at` if
none exist.

Fixes
https://linear.app/chatwoot/issue/CW-5992/save-sort-filter-to-ui-settings

## Type of change
- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-11-20 16:51:27 -08:00
Vishnu NarayananandGitHub a547c28c8d fix: reduce sidekiq memorymax to 1.2gb (#12915)
- reduce sidekiq memorymax to 1.2GB to ensure OOM restarts work on a 2GB
machine
2025-11-20 21:20:42 +05:30
5608f5a1a2 fix: Widget shows 'away' on initial load despite agents being online (#12869)
## Description

Fixes #12868

This PR fixes a Vue 3 reactivity bug that causes the widget to display
"We are away at the moment" on initial page load, even when agents are
online and the API correctly returns their availability.

## Problem

The widget welcome screen shows "We are away" on first render, only
updating to show correct agent status after navigating to the
conversation view and back. This misleads visitors into thinking no
agents are available.

**Reproduction:** Open website with widget in fresh incognito window,
click bubble immediately → shows "away" despite agents being online.

## Root Cause

Vue 3 reactivity chain breaks in the `useAvailability` composable:

**Before (broken):**
```javascript
// AvailabilityContainer.vue
const { isOnline } = useAvailability(props.agents); // Passes VALUE

// useAvailability.js  
const availableAgents = toRef(agents); // Creates ref from VALUE, doesn't track changes
```

When the API responds and updates the Vuex store, the parent component's
computed `props.agents` updates correctly, but the composable's
`toRef()` doesn't know about the change because it was created from a
static value, not a reactive source.

## Solution

**After (fixed):**
```javascript
// AvailabilityContainer.vue
const { isOnline } = useAvailability(toRef(props, 'agents')); // Passes REACTIVE REF

// useAvailability.js
const availableAgents = computed(() => unref(agents)); // Unwraps ref and tracks changes
```

Now when `props.agents` updates after the API response, the `computed()`
re-evaluates and all downstream reactive properties (`hasOnlineAgents`,
`isOnline`) update correctly.

## Testing

-  Initial page load shows correct agent status immediately
-  Status changes via WebSocket update correctly  
-  No configuration changes or workarounds needed
-  Tested with network monitoring (Puppeteer) confirming API returns
correct data

## Files Changed

1.
`app/javascript/widget/components/Availability/AvailabilityContainer.vue`
   - Pass `toRef(props, 'agents')` instead of `props.agents`

2. `app/javascript/widget/composables/useAvailability.js`
   - Use `computed(() => unref(agents))` instead of `toRef(agents)`
   - Added explanatory comments

## Related Issues

- #5918 - Similar symptoms, closed with workaround (business hours
toggle) rather than fixing root cause
- #5763 - Different issue (mobile app presence)

This is a genuine Vue 3 reactivity bug affecting all widgets,
independent of business hours configuration.

Co-authored-by: rcoenen <rcoenen@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-11-20 20:27:44 +05:30
Shivam MishraandGitHub da4110a495 feat: add retry loadWithRetry composable (#12873) 2025-11-20 19:40:20 +05:30
Vinay KeerthiandGitHub 9a2136caf1 fix: Change messages.source_id to text column (#12908)
## Summary
Changes `messages.source_id` from `string` (255 char limit) to `text`
(20,000 char limit) to support long email Message-ID headers.

## Changes
- Migration to change column type from string to text
- Added spec tests for source_id length validation

## Related
Fixes
https://linear.app/chatwoot/issue/CW-5961/activerecordrecordinvalid-validation-failed-source-is-too-long-maximum
2025-11-20 11:41:41 +05:30
Shivam MishraandGitHub a44192bbe7 feat: remove captain v2 from chatwoot internal list (#12903) 2025-11-19 17:49:47 +05:30
Vishnu NarayananandGitHub 5a5b30fe1e chore: increment CW version (#12902)
fixes https://github.com/chatwoot/chatwoot/issues/12889
2025-11-19 17:10:01 +05:30
Vishnu NarayananandGitHub 08b9134486 feat: speed up circleci and github actions (#12849)
# 🚀 Speed up CI/CD test execution with parallelization

## TL;DR

- **Problem**: CI tests took 36-42 minutes per commit, blocking
developer workflow
- **Solution**: Implemented 16-way parallelization + optimized slow
tests + fixed Docker builds
- **Impact**: **1,358 hours/month saved** (7.7 FTE) across GitHub
Actions + CircleCI
  - GitHub Actions tests: 36m → 7m (82% faster)
  - Backend tests: 28m → 4m (87% faster with 16-way parallelization)
  - CircleCI tests: 42m → 7m (83% faster)
  - Docker builds: 34m → 5m (85% faster)
- **Result**: 5-6x faster feedback loops, 100% success rate on recent
runs

---

## Problem
CI test runs were taking **36-42 minutes per commit push** (GitHub
Actions: 36m avg, CircleCI: 42m P95), creating a significant bottleneck
in the development workflow.

## Solution
This PR comprehensively restructures both CI test pipelines to leverage
16-way parallelization and optimize test execution, reducing test
runtime from **36-42 minutes to ~7 minutes** - an **82% improvement**.

---

## 📊 Real Performance Data (Both CI Systems)

### GitHub Actions

#### Before (develop branch - 5 recent runs)
```
Individual runs: 35m 29s | 36m 1s | 40m 0s | 36m 4s | 34m 18s
Average: 36m 22s
```

#### After (feat/speed_up_ci branch - 9 successful runs)
```
Individual runs: 6m 39s | 7m 2s | 6m 53s | 6m 26s | 6m 52s | 6m 42s | 6m 45s | 6m 40s | 6m 37s
Average: 6m 44s
Range: 6m 26s - 7m 2s
```

**Improvement**:  **81.5% faster** (29m 38s saved per run)


#### Backend Tests Specific Impact
With 16-way parallelization, backend tests show dramatic improvement:
- **Before**: 27m 52s (sequential execution)
- **After**: 3m 44s (longest of 16 parallel runners)
  - Average across runners: 2m 30s
  - Range: 1m 52s - 3m 44s
- **Improvement**:  **86.6% faster** (24m 8s saved)
---

### CircleCI

#### Before (develop branch - CircleCI Insights)
```
Duration (P95): 41m 44s
Runs: 70 (last 30 days)
Success Rate: 84%
```

#### After (feat/speed_up_ci branch - Last 2 pipeline runs)
```
Run 1 (1h ago): 7m 7s
  ├─ lint: 4m 12s
  ├─ frontend-tests: 5m 36s
  ├─ backend-tests: 6m 23s
  ├─ coverage: 20s
  └─ build: 1s

Run 2 (2h ago): 7m 21s
  ├─ lint: 3m 47s
  ├─ frontend-tests: 5m 4s
  ├─ backend-tests: 6m 33s
  ├─ coverage: 19s
  └─ build: 1s

Average: 7m 14s
Success Rate: 100% 
```

**Improvement**:  **82.7% faster** (34m 30s saved per run)

---

## 🐳 Related Work: Docker Build Optimization

As part of the broader CI/CD optimization effort, Docker build
performance was improved separately in **PR #12859**.

### Docker Build Fix (Merged Separately)
**Problem**: Multi-architecture Docker builds (amd64/arm64) were taking
~34 minutes due to cache thrashing

**Solution**: Added separate cache scopes per platform in
`.github/workflows/test_docker_build.yml`:
```yaml
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
```

**Results** (measured from November 2025 data):
- **Before**: 34.2 minutes/run average (15,547 minutes across 454 runs)
- **After**: 5 minutes/run
- **Improvement**: 85% faster, 29.2 minutes saved per run
- **Frequency**: 25.2 runs/day
- **Monthly savings**: **369 hours** (46 developer-days)

This prevents different architectures from invalidating each other's
caches and contributes 27% of total CI/CD time savings.

---

## 🎯 Key Findings

### Both CI Systems Now Perform Similarly
- **CircleCI**: 7m 14s average
- **GitHub Actions**: 6m 44s average
- **Difference**: Only 30 seconds apart (remarkably consistent!)

### Combined Performance
- **Average improvement across both systems**: **82.1% faster**
- **Time saved per commit**: ~32 minutes
- **Developer feedback loop**: 36-42 minutes → ~7 minutes

### Success Rate Improvement
- **CircleCI**: 84% → 100% (on feat/speed_up_ci branch)
- **GitHub Actions**: 100% (all 9 recent runs successful)
- Fixed all test isolation issues that caused intermittent failures

### Impact at Scale (Based on Real November 2025 Data)
- **CI runs per day**: **30.8 average** for tests, **25.2** for Docker
builds
  - Measured from GitHub Actions Usage Metrics (18 days)
  - Weekdays: 38-54 runs/day
  - Peak: up to 68 runs in a single day
- **This PR (test suite only)**:
  - **Daily time saved**: **15.3 hours** (GitHub Actions + CircleCI)
- **Monthly time saved**: **458 hours** (57 developer-days) on GitHub
Actions
  - Additional **531 hours** (66 developer-days) on CircleCI
- **Combined with Docker optimization** (PR #12859): **1,358
hours/month** (see Summary)
- **Developer experience**: 5-6x faster iteration cycles

---

## Code Changes

### 1. **Backend Test Parallelization (16x)**
Both CI systems now use 16-way parallelization with identical
round-robin test distribution:

```bash
# Distribute tests evenly across 16 runners
SPEC_FILES=($(find spec -name '*_spec.rb' | sort))
for i in "${!SPEC_FILES[@]}"; do
  if [ $(( i % 16 )) -eq $RUNNER_INDEX ]; then
    TESTS="$TESTS ${SPEC_FILES[$i]}"
  fi
done
```

**Why round-robin over timing-based?**
- CircleCI's timing-based splitting grouped similar tests together
- This caused race conditions with OAuth callback tests (Linear,
Shopify, Notion)
- Round-robin ensures even distribution and better test isolation
- Both CI systems now behave identically

### 2. **Frontend Test Optimization**
Enabled Vitest thread parallelization in `vite.config.ts`:

```typescript
pool: 'threads',
poolOptions: {
  threads: {
    singleThread: false,
  },
},
```

### 3. **CI Architecture Restructuring**
Split monolithic CI jobs into parallel stages:
- **Lint** (backend + frontend) - runs independently for fast feedback
- **Frontend tests** - runs in parallel with backend
- **Backend tests** - 16-way parallelized across runners
- **Coverage** - aggregates results from test jobs
- **Build** (CircleCI only) - final job for GitHub status check
compatibility

### 4. **Critical Test Optimization**
**report_builder_spec.rb**: Changed `before` to `before_all`
- Reduced execution time from **19 minutes to 1.2 minutes** (16x
speedup)
- Setup now runs once instead of 21 times
- Single biggest performance improvement after parallelization

---

## Test Stability Fixes (10 spec files)

Parallelization exposed latent test isolation issues that were fixed:

### Object Identity Comparisons (6 files)
Tests were comparing Ruby object instances instead of IDs:
- `spec/models/integrations/hook_spec.rb` - Use `.pluck(:id)` for
comparisons
- `spec/enterprise/models/captain/scenario_spec.rb` - Compare IDs
instead of objects
- `spec/models/notification_spec.rb` - Compare IDs for sort order
validation
- `spec/models/account_spec.rb` - Compare IDs in scope queries
- `spec/services/widget/token_service_spec.rb` - Compare class names
instead of class objects
- `spec/models/concerns/avatarable_shared.rb` - Use `respond_to` checks
for ActiveStorage

### Database Query Caching
- `spec/jobs/delete_object_job_spec.rb` - Added `.reload` to force fresh
database queries

### Test Expectations Timing
- `spec/jobs/mutex_application_job_spec.rb` - Removed flaky unlock
expectation after error block
  - Related to original PR #8770
- Expectation after error block never executes in parallel environments

### Timezone Handling
- `spec/mailers/account_notification_mailer_spec.rb` - Fixed date
parsing at timezone boundaries
  - Changed test time from 23:59:59Z to 12:00:00Z

### Test Setup
- `spec/builders/v2/report_builder_spec.rb` - Optimized with
`before_all`

---

##  CircleCI GitHub Integration Fix

### Problem
GitHub PR checks were stuck on "Waiting for status" even when all
CircleCI jobs passed. GitHub was expecting a job named `build` but the
workflow only had a workflow named "build".

### Solution
Added an explicit `build` job that runs after all other jobs:

```yaml
build:
  steps:
    - run:
        name: Legacy build aggregator
        command: echo "All main jobs passed"
  requires:
    - lint
    - coverage
```

This ensures GitHub's required status checks work correctly.


---

##  Testing & Validation

-  **GitHub Actions**: 9 successful runs, consistent 6m 26s - 7m 2s
runtime
-  **CircleCI**: 2 successful runs, consistent 7m 7s - 7m 21s runtime
-  Both CI systems produce identical, consistent results
-  GitHub PR status checks complete correctly
-  Success rate improved from 84% to 100% (recent runs)
-  No test regressions introduced
-  All flaky tests fixed (callback controllers, mutex jobs, etc.)

---

## 🎉 Summary

This PR delivers an **82% improvement** in test execution time across
both CI systems:

- **GitHub Actions tests**: 36m → 7m (81.5% faster)
- Backend tests specifically: 28m → 4m (86.6% faster)
- **CircleCI tests**: 42m → 7m (82.7% faster)
- **Developer feedback loop**: 5-6x faster
- **Test stability**: 84% → 100% success rate

### 📊 Total CI/CD Impact (All Optimizations)

Based on real November 2025 data, combining this PR with Docker build
optimization (PR #12859):

**Monthly Time Savings**: **1,358 hours/month** = **170
developer-days/month** = **7.7 FTE**

| System | Runs/Day | Before | After | Savings | Monthly Impact |
|--------|----------|---------|--------|---------|----------------|
| **GitHub Actions Tests** | 30.8 | 36.5m | 6.7m | 29.8m/run | 458 hrs
(34%) |
| **GitHub Actions Docker** | 25.2 | 34.2m | 5.0m | 29.2m/run | 369 hrs
(27%) |
| **CircleCI Tests** | 30.8 | 41.7m | 7.2m | 34.5m/run | 531 hrs (39%) |

*Data source: GitHub Actions Usage Metrics (November 2025, 18 days),
CircleCI Insights (30 days)*

The combined optimizations save the equivalent of **nearly 8 full-time
developers** worth of CI waiting time every month, significantly
improving developer velocity and reducing CI costs. All test isolation
issues exposed by parallelization have been fixed, ensuring reliable and
consistent results across both CI platforms.

woot woot !!!

---------
2025-11-19 15:32:48 +05:30
Sojan Jose 7874a6a3dd Merge branch 'release/4.8.0' into develop 2025-11-18 18:41:59 -08:00
Sojan Jose e9edfcafbb Bump version to 4.8.0 2025-11-18 18:39:51 -08:00
5f2b2f4221 feat: APIs to assign agents_bots as assignee in conversations (#12836)
## Summary
- add an assignee_agent_bot_id column as an initital step to prototype
this before fully switching to polymorphic assignee
- update assignment APIs and conversation list / show endpoints to
reflect assignee as agent bot
- ensure webhook payloads contains agent bot assignee


[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912833377e48326b6641b9eee32d50f)

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2025-11-18 18:20:58 -08:00
70c183ea6e feat: hide email forwarding address if INBOUND_EMAIL_DOMAIN is not configured (#12768)
#### Summary

- Improved email inbox setup flow to handle cases where inbound email
forwarding is not configured on the installation
- Added conditional display of email forwarding address based on
MAILER_INBOUND_EMAIL_DOMAIN environment variable availability
- Enhanced user messaging to guide users toward configuring SMTP/IMAP
settings when forwarding is unavailable

#### Changes

**Backend (app/views/api/v1/models/_inbox.json.jbuilder)**
- Added forwarding_enabled boolean flag to inbox API response based on
MAILER_INBOUND_EMAIL_DOMAIN presence
- Made forward_to_email conditional - only included when forwarding is
enabled

  **Frontend - Inbox Creation Flow**
- Created new EmailInboxFinish.vue component to handle email inbox setup
completion
  - Shows different messages based on whether forwarding is enabled:
- With forwarding: displays forwarding address and encourages SMTP/IMAP
configuration
- Without forwarding: warns that SMTP/IMAP configuration is required for
emails to be processed
- Added link to configuration page for easy access to SMTP/IMAP settings

<img width="988" height="312" alt="Screenshot 2025-11-18 at 3 27 27 PM"
src="https://github.com/user-attachments/assets/928aff78-df73-49fa-9a26-dbbd1297b26a"
/>

<img width="765" height="489" alt="Screenshot 2025-11-18 at 3 24 46 PM"
src="https://github.com/user-attachments/assets/6a182c7d-087f-4e88-92a5-30f147a567a7"
/>


Fixes
https://linear.app/chatwoot/issue/CW-5881/hide-forwaring-email-section-if-inbound-email-domain-is-not-configured


## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Tested locally

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2025-11-18 18:05:12 -08:00
6c07f62cfc feat: Add Amazon SES inbound email support (#12893)
## Summary
- add AWS ActionMailbox SES gems
- document SES as incoming email provider
- note SES option in configuration

## Testing
- `bundle exec rubocop config/initializers/mailer.rb
config/environments/production.rb Gemfile`


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_68bbb7d482288326b8f04bb795af0322)

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
2025-11-18 15:33:08 +05:30
e33f28dc33 feat: Companies page (#12842)
# Pull Request Template

## Description

This PR introduces a new Companies section in the Chatwoot dashboard. It
lists all companies associated with the account and includes features
such as **search**, **sorting**, and **pagination** to enable easier
navigation and efficient management.

Fixes
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshot
<img width="1619" height="1200" alt="image"
src="https://github.com/user-attachments/assets/21f0a666-c3d6-4dec-bd02-1e38e0cd9542"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-11-18 15:29:15 +05:30
58ca82c720 feat: Backend - Companies API endpoint with pagination and search (#12840)
## Description

Adds API endpoint to list companies with pagination, search, and
sorting.

Fixes
https://linear.app/chatwoot/issue/CW-5930/add-backend-routes-to-get-companies-result
Parent issue:
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Added comprehensive specs to
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`:
- Pagination (25 per page, multiple pages)
- Search by name and domain (case-insensitive)
- Counter cache for contacts_count
- Account scoping
- Authorization

To reproduce:
```bash
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb
bundle exec rubocop enterprise/app/controllers/api/v1/accounts/companies_controller.rb
```

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2025-11-18 14:28:56 +05:30
Chatwoot BotandGitHub 7acf3c8817 chore: Update translations (#12876) 2025-11-17 22:02:54 -08:00
77f492590e feat: Control the allowed login methods via Super Admin (#12892)
- Control the allowed authentication methods for a chatwoot installation
via super admin configs. [SAML, Google Auth etc]
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6917d503b6e48326a261672c1de91462)

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-11-17 21:55:12 -08:00
Sojan JoseandGitHub 5b56f64838 feat: Customizable webhook timeout configuration (#12777)
## Summary
- Ability to configure the webhook timeout for Chatwoot self hosted
installations

fixes: https://github.com/chatwoot/chatwoot/issues/12754
2025-11-17 14:43:23 -08:00
bf806f0c28 feat: allow configuring attachment upload limit (#12835)
## Summary
- add a configurable MAXIMUM_FILE_UPLOAD_SIZE installation setting and
surface it through super admin and global config payloads
- apply the configurable limit to attachment validations and shared
upload helpers on dashboard and widget
- introduce a reusable helper with unit tests for parsing the limit and
extend attachment specs for configurability


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912644786b08326bc8dee9401af6d0a)

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2025-11-17 14:03:08 -08:00
Vinay KeerthiandGitHub 93374f4327 fix: Change contact_inboxes.source_id to text column (#12882)
## Description

Fixes CW-5961 where IMAP email processing failed with
`ActiveRecord::RecordInvalid: Validation failed: Source is too long
(maximum is 255 characters)` error.

This changes the `contact_inboxes.source_id` column from `string` (255
character limit) to `text` (unlimited) to accommodate long email message
IDs that were causing validation failures.

Fixes CW-5961

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Added spec test validating `source_id` values longer than 255
characters (300 chars)
- All existing `contact_inbox_spec.rb` tests pass (7 examples, 0
failures)
- Migration applied successfully with reversible up/down methods
- Verified `source_id` column type changed to `text` with `null: false`
constraint preserved

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
2025-11-17 16:09:36 +05:30
c9823d9409 feat: Assignment service (v2) (#12320)
## Linear Link

 
## Description

This PR introduces a new robust auto-assignment system for conversations
in Chatwoot. The system replaces the existing round-robin assignment
with a more sophisticated service-based architecture that supports
multiple assignment strategies, rate limiting, and Enterprise features
like capacity-based assignment and balanced distribution.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Unit test cases
- Test conversations getting assigned on status change to open
- Test the job directly via rails console

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a new service-based auto-assignment system with scheduled jobs,
rate limiting, enterprise capacity/balanced selection, and wiring via
inbox/handler; includes Redis helpers and comprehensive tests.
> 
> - **Auto-assignment v2 (core services)**:
> - Add `AutoAssignment::AssignmentService` with bulk assignment,
configurable conversation priority, RR selection, and per-agent rate
limiting via `AutoAssignment::RateLimiter`.
>   - Add `AutoAssignment::RoundRobinSelector` for agent selection.
> - **Jobs & scheduling**:
> - Add `AutoAssignment::AssignmentJob` (per-inbox bulk assign;
env-based limit) and `AutoAssignment::PeriodicAssignmentJob` (batch over
accounts/inboxes).
> - Schedule periodic run in `config/schedule.yml`
(`periodic_assignment_job`).
> - **Model/concerns wiring**:
> - Include `InboxAgentAvailability` in `Inbox`; add
`Inbox#auto_assignment_v2_enabled?`.
> - Update `AutoAssignmentHandler` to trigger v2 job when
`auto_assignment_v2_enabled?`, else fallback to legacy.
> - **Enterprise extensions**:
> - Add `Enterprise::InboxAgentAvailability` (capacity-aware filtering)
and `Enterprise::Concerns::Inbox` association `inbox_capacity_limits`.
> - Extend service via `Enterprise::AutoAssignment::AssignmentService`
(policy-driven config, capacity filtering, exclusion rules) and add
selectors/services: `BalancedSelector`, `CapacityService`.
> - **Infrastructure**:
> - Enhance `Redis::Alfred` with `expire`, key scan/count, and extended
ZSET helpers (`zadd`, `zcount`, `zcard`, `zrangebyscore`).
> - **Tests**:
> - Add specs for jobs, core service, rate limiter, RR selector, and
enterprise features (capacity, balanced selection, exclusions).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0ebe187c8a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-11-17 10:08:25 +05:30
Sivin VargheseandGitHub 2c4e65d68e chore: Hide assistant switcher on paywall screen (#12875) 2025-11-17 09:58:59 +05:30
Vinay KeerthiandGitHub 6ae5e67b52 fix: revert annotaterb migration due to persistent annotation errors (#12881)
## Description

This PR reverts the migration from the `annotate` gem to `annotaterb`
introduced in PR #12845. The annotation errors reported in #11673
persist with both gems, and the old `annotate` gem handles the errors
more gracefully by continuing to process other models instead of
crashing.

**Testing reveals both gems fail with the same underlying issue:**

**Old annotate gem (3.2.0):**
```
Unable to annotate app/models/installation_config.rb: no implicit conversion of Hash into String
Unable to annotate app/models/installation_config.rb: no implicit conversion of nil into Array
Model files unchanged.
```
(Logs error but continues processing)

**New annotaterb gem (4.20.0):**
```
❯ bundle exec annotaterb models
ruby/3.4.4/lib/ruby/gems/3.4.0/gems/reline-0.3.6/lib/reline/terminfo.rb:2: warning: ruby/3.4.4/lib/ruby/3.4.0/fiddle.rb was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.5.0.
You can add fiddle to your Gemfile or gemspec to silence this warning.
Also please contact the author of reline-0.3.6 to request adding fiddle into its gemspec.
Annotating models
bundler: failed to load command: annotaterb (ruby/3.4.4/bin/annotaterb)
ruby/3.4.4/lib/ruby/3.4.0/psych/parser.rb:62:in 'Psych::Parser#_native_parse': no implicit conversion of Hash into String (TypeError)

      _native_parse @handler, yaml, path
                    ^^^^^^^^^^^^^^^^^^^^
        from ruby/3.4.4/lib/ruby/3.4.0/psych/parser.rb:62:in 'Psych::Parser#parse'
        from ruby/3.4.4/lib/ruby/3.4.0/psych.rb:457:in 'Psych.parse_stream'
        from ruby/3.4.4/lib/ruby/3.4.0/psych.rb:401:in 'Psych.parse'
        from ruby/3.4.4/lib/ruby/3.4.0/psych.rb:325:in 'Psych.safe_load'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/coders/yaml_column.rb:37:in 'ActiveRecord::Coders::YAMLColumn::SafeCoder#load'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/coders/column_serializer.rb:37:in 'ActiveRecord::Coders::ColumnSerializer#load'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/type/serialized.rb:22:in 'ActiveRecord::Type::Serialized#deserialize'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute.rb:175:in 'ActiveModel::Attribute::FromDatabase#type_cast'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute.rb:43:in 'ActiveModel::Attribute#value'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute_set.rb:37:in 'block in ActiveModel::AttributeSet#to_hash'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activesupport-7.1.5.2/lib/active_support/core_ext/enumerable.rb:78:in 'block in Enumerable#index_with'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activesupport-7.1.5.2/lib/active_support/core_ext/enumerable.rb:78:in 'Array#each'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activesupport-7.1.5.2/lib/active_support/core_ext/enumerable.rb:78:in 'Enumerable#index_with'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute_set.rb:37:in 'ActiveModel::AttributeSet#to_hash'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/model_schema.rb:499:in 'ActiveRecord::ModelSchema::ClassMethods#column_defaults'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:68:in 'AnnotateRb::ModelAnnotator::ModelWrapper#column_defaults'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:139:in 'block in AnnotateRb::ModelAnnotator::ModelWrapper#built_attributes'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:136:in 'Array#map'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:136:in 'AnnotateRb::ModelAnnotator::ModelWrapper#built_attributes'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/column_annotation/annotation_builder.rb:15:in 'AnnotateRb::ModelAnnotator::ColumnAnnotation::AnnotationBuilder#build'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:52:in 'block in AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#columns'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:51:in 'Array#map'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:51:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#columns'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:26:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#body'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:35:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#build'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:71:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder#build'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:43:in 'AnnotateRb::ModelAnnotator::ProjectAnnotator#build_instructions_for_file'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:17:in 'block in AnnotateRb::ModelAnnotator::ProjectAnnotator#annotate'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:13:in 'Array#map'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:13:in 'AnnotateRb::ModelAnnotator::ProjectAnnotator#annotate'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotator.rb:21:in 'AnnotateRb::ModelAnnotator::Annotator#do_annotations'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotator.rb:8:in 'AnnotateRb::ModelAnnotator::Annotator.do_annotations'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/commands/annotate_models.rb:17:in 'AnnotateRb::Commands::AnnotateModels#call'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/runner.rb:38:in 'AnnotateRb::Runner#run'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/runner.rb:11:in 'AnnotateRb::Runner.run'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/exe/annotaterb:18:in '<top (required)>'
        from ruby/3.4.4/bin/annotaterb:25:in 'Kernel#load'
        from ruby/3.4.4/bin/annotaterb:25:in '<top (required)>'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli/exec.rb:58:in 'Kernel.load'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli/exec.rb:58:in 'Bundler::CLI::Exec#kernel_load'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli/exec.rb:23:in 'Bundler::CLI::Exec#run'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli.rb:455:in 'Bundler::CLI#exec'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor/command.rb:28:in 'Bundler::Thor::Command#run'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in 'Bundler::Thor::Invocation#invoke_command'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor.rb:527:in 'Bundler::Thor.dispatch'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli.rb:35:in 'Bundler::CLI.dispatch'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor/base.rb:584:in 'Bundler::Thor::Base::ClassMethods#start'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli.rb:29:in 'Bundler::CLI.start'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/exe/bundle:28:in 'block in <top (required)>'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/friendly_errors.rb:117:in 'Bundler.with_friendly_errors'
        from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/exe/bundle:20:in '<top (required)>'
        from ruby/3.4.4/bin/bundle:25:in 'Kernel#load'
        from ruby/3.4.4/bin/bundle:25:in '<main>'


```
(Crashes immediately, stops all processing)

**Root cause:** The `InstallationConfig` model uses YAML serialization
(`serialize :serialized_value, coder: YAML`) on a JSONB database column.
When annotation tools read column defaults, PostgreSQL returns JSONB as
a Hash, but YAML expects a String, causing the type error.

The migration to annotaterb doesn't solve the problem - both gems
encounter the same error. The old gem is preferable as it continues
working despite the error.

Reverts #12845
Related to #11673

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

1. Reverted commit 559d1b657
2. Ran `bundle install` to reinstall annotate gem v3.2.0
3. Ran `RAILS_ENV=development bundle exec annotate` 
- Result: Logs errors for InstallationConfig but completes successfully
4. Re-applied the annotaterb changes and tested `bundle exec annotaterb
models`
   - Result: Crashes with full stack trace and stops processing

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes


---
*Edited to truncate environment-specific info from error dump*
2025-11-14 22:42:56 +05:30
Chatwoot BotandGitHub 32da4eec27 chore: Update translations (#12818) 2025-11-14 15:24:38 +05:30
Sojan JoseandGitHub 945a08eadb chore: disable worker MemoryHigh throttling in systemd unit (#12871)
- set MemoryHigh to infinity in deployment/chatwoot-worker.1.service so
the worker is throttled only by the existing
    MemoryMax hard limit
- prevents cgroup reclaim from slowing Sidekiq under transient spikes
while still keeping the hard stop at 1.5 GB
2025-11-13 23:49:06 -08:00
Vinay KeerthiandGitHub 559d1b6576 fix: migrate from deprecated annotate gem to annotaterb (#12845)
## Description

The `annotate` gem has been deprecated and users are experiencing
annotation errors with the new Rails 7 `serialize` syntax. This PR
migrates to `annotaterb`, the actively maintained fork.

Users reported errors when running `make db`:
```
Unable to annotate app/models/installation_config.rb: no implicit conversion of Hash into String  
Unable to annotate app/models/installation_config.rb: no implicit conversion of nil into Array
```

This PR updates the Gemfile and rake configuration to use `annotaterb`
instead.

Fixes #11673

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Tested locally with the following steps:
1. Run `bundle install` - successfully installed annotaterb 4.20.0
2. Run `RAILS_ENV=development bundle exec rails db:chatwoot_prepare` -
completed without annotation errors
3. Run `RAILS_ENV=development bundle exec rails annotate_rb:models` -
successfully annotated all models including InstallationConfig
4. Verified InstallationConfig model annotations are present and correct

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
2025-11-14 12:35:38 +05:30
Sivin VargheseandGitHub eed1825d5a fix: Brand installation name not showing (#12861)
# Pull Request Template

## Description

Fixes
https://linear.app/chatwoot/issue/CW-5946/fix-brand-installation-name-issue-in-dyte

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-11-13 22:16:25 +05:30
Sivin VargheseandGitHub 651645fbd2 chore: Update missing places with new colors (#12862)
# Pull Request Template

## Description

This PR updates the colors in places that were missed during the color
update migration.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-11-13 22:16:13 +05:30
Vishnu NarayananandGitHub b4a252a459 perf: speed up docker builds (#12859)
- Use separate keys to avoid cache overwrites across different
architecture builds


https://linear.app/chatwoot/issue/CW-5945/perf-speed-up-docker-builds

### 25 mins  ---> 5mins


## before

<img width="971" height="452" alt="image"
src="https://github.com/user-attachments/assets/535cebd6-6c16-48d1-a62d-ffb6f2fc9b08"
/>


## after
<img width="940" height="428" alt="image"
src="https://github.com/user-attachments/assets/359eb313-4bb5-4e0e-9492-a8ad48645159"
/>
2025-11-13 20:17:59 +05:30
387 changed files with 5870 additions and 1982 deletions
+196 -49
View File
@@ -3,6 +3,7 @@ orbs:
node: circleci/node@6.1.0
qlty-orb: qltysh/qlty-orb@0.0
# Shared defaults for setup steps
defaults: &defaults
working_directory: ~/build
machine:
@@ -12,10 +13,106 @@ defaults: &defaults
RAILS_LOG_TO_STDOUT: false
COVERAGE: true
LOG_LEVEL: warn
parallelism: 4
jobs:
build:
# Separate job for linting (no parallelism needed)
lint:
<<: *defaults
steps:
- checkout
# Install minimal system dependencies for linting
- run:
name: Install System Dependencies
command: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
libpq-dev \
build-essential \
git \
curl \
libssl-dev \
zlib1g-dev \
libreadline-dev \
libyaml-dev \
openjdk-11-jdk \
jq \
software-properties-common \
ca-certificates \
imagemagick \
libxml2-dev \
libxslt1-dev \
file \
g++ \
gcc \
autoconf \
gnupg2 \
patch \
ruby-dev \
liblzma-dev \
libgmp-dev \
libncurses5-dev \
libffi-dev \
libgdbm6 \
libgdbm-dev \
libvips
- run:
name: Install RVM and Ruby 3.4.4
command: |
sudo apt-get install -y gpg
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | bash -s stable
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
source ~/.rvm/scripts/rvm
rvm install "3.4.4"
rvm use 3.4.4 --default
gem install bundler -v 2.5.16
- run:
name: Install Application Dependencies
command: |
source ~/.rvm/scripts/rvm
bundle install
- node/install:
node-version: '23.7'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
override-ci-command: pnpm i
# Swagger verification
- run:
name: Verify swagger API specification
command: |
bundle exec rake swagger:build
if [[ `git status swagger/swagger.json --porcelain` ]]
then
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
exit 1
fi
mkdir -p ~/tmp
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Bundle audit
- run:
name: Bundle audit
command: bundle exec bundle audit update && bundle exec bundle audit check -v
# Rubocop linting
- run:
name: Rubocop
command: bundle exec rubocop --parallel
# ESLint linting
- run:
name: eslint
command: pnpm run eslint
# Separate job for frontend tests
frontend-tests:
<<: *defaults
steps:
- checkout
@@ -25,8 +122,38 @@ jobs:
- node/install-packages:
pkg-manager: pnpm
override-ci-command: pnpm i
- run: node --version
- run: pnpm --version
- run:
name: Run frontend tests (with coverage)
command: pnpm run test:coverage
- run:
name: Move coverage files if they exist
command: |
if [ -d "coverage" ]; then
mkdir -p ~/build/coverage
cp -r coverage ~/build/coverage/frontend || true
fi
when: always
- persist_to_workspace:
root: ~/build
paths:
- coverage
# Backend tests with parallelization
backend-tests:
<<: *defaults
parallelism: 16
steps:
- checkout
- node/install:
node-version: '23.7'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
override-ci-command: pnpm i
- run:
name: Add PostgreSQL repository and update
command: |
@@ -91,20 +218,6 @@ jobs:
source ~/.rvm/scripts/rvm
bundle install
# Swagger verification
- run:
name: Verify swagger API specification
command: |
bundle exec rake swagger:build
if [[ `git status swagger/swagger.json --porcelain` ]]
then
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
exit 1
fi
mkdir -p ~/tmp
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Configure environment and database
- run:
name: Database Setup and Configure Environment Variables
@@ -127,57 +240,91 @@ jobs:
name: Run DB migrations
command: bundle exec rails db:chatwoot_prepare
# Bundle audit
- run:
name: Bundle audit
command: bundle exec bundle audit update && bundle exec bundle audit check -v
# Rubocop linting
- run:
name: Rubocop
command: bundle exec rubocop
# ESLint linting
- run:
name: eslint
command: pnpm run eslint
- run:
name: Run frontend tests (with coverage)
command: |
mkdir -p ~/build/coverage/frontend
pnpm run test:coverage
# Run backend tests
# Run backend tests (parallelized)
- run:
name: Run backend tests
command: |
mkdir -p ~/tmp/test-results/rspec
mkdir -p ~/tmp/test-artifacts
mkdir -p ~/build/coverage/backend
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
# Use round-robin distribution (same as GitHub Actions) for better test isolation
# This prevents tests with similar timing from being grouped on the same runner
SPEC_FILES=($(find spec -name '*_spec.rb' | sort))
TESTS=""
for i in "${!SPEC_FILES[@]}"; do
if [ $(( i % $CIRCLE_NODE_TOTAL )) -eq $CIRCLE_NODE_INDEX ]; then
TESTS="$TESTS ${SPEC_FILES[$i]}"
fi
done
bundle exec rspec -I ./spec --require coverage_helper --require spec_helper --format progress \
--format RspecJunitFormatter \
--out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES}
-- $TESTS
no_output_timeout: 30m
# Qlty coverage publish
- qlty-orb/coverage_publish:
files: |
coverage/coverage.json
coverage/lcov.info
# Store test results for better splitting in future runs
- store_test_results:
path: ~/tmp/test-results
- run:
name: List coverage directory contents
name: Move coverage files if they exist
command: |
ls -R ~/build/coverage
if [ -d "coverage" ]; then
mkdir -p ~/build/coverage
cp -r coverage ~/build/coverage/backend || true
fi
when: always
- persist_to_workspace:
root: ~/build
paths:
- coverage
# Collect coverage from all jobs
coverage:
<<: *defaults
steps:
- checkout
- attach_workspace:
at: ~/build
# Qlty coverage publish
- qlty-orb/coverage_publish:
files: |
coverage/frontend/lcov.info
- run:
name: List coverage directory contents
command: |
ls -R ~/build/coverage || echo "No coverage directory"
- store_artifacts:
path: coverage
destination: coverage
build:
<<: *defaults
steps:
- run:
name: Legacy build aggregator
command: |
echo "All main jobs passed; build job kept only for GitHub required check compatibility."
workflows:
version: 2
build:
jobs:
- lint
- frontend-tests
- backend-tests
- coverage:
requires:
- frontend-tests
- backend-tests
- build:
requires:
- lint
- coverage
+5
View File
@@ -105,6 +105,7 @@ MAILER_INBOUND_EMAIL_DOMAIN=
# mandrill for Mandrill
# postmark for Postmark
# sendgrid for Sendgrid
# ses for Amazon SES
RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
@@ -114,6 +115,10 @@ RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY=
# SNS topic ARN for ActionMailbox (format: arn:aws:sns:region:account-id:topic-name)
# Configure only if the rails_inbound_email_service = ses
ACTION_MAILBOX_SES_SNS_TOPIC=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
+79 -15
View File
@@ -1,4 +1,6 @@
name: Run Chatwoot CE spec
permissions:
contents: read
on:
push:
branches:
@@ -8,11 +10,58 @@ on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-22.04
# Separate linting jobs for faster feedback
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run Rubocop
run: bundle exec rubocop --parallel
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 23
cache: 'pnpm'
- name: Install pnpm dependencies
run: pnpm i
- name: Run ESLint
run: pnpm run eslint
# Frontend tests run in parallel with backend
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 23
cache: 'pnpm'
- name: Install pnpm dependencies
run: pnpm i
- name: Run frontend tests
run: pnpm run test:coverage
# Backend tests with parallelization
backend-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ci_node_total: [16]
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
services:
postgres:
image: pgvector/pgvector:pg15
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ''
@@ -20,8 +69,6 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
# needed because the postgres container does not provide a healthcheck
# tmpfs makes DB faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
@@ -29,7 +76,7 @@ jobs:
--health-timeout 5s
--health-retries 5
redis:
image: redis
image: redis:alpine
ports:
- 6379:6379
options: --entrypoint redis-server
@@ -43,7 +90,7 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
bundler-cache: true
- uses: actions/setup-node@v4
with:
@@ -64,19 +111,36 @@ jobs:
- name: Seed database
run: bundle exec rake db:schema:load
- name: Run frontend tests
run: pnpm run test:coverage
# Run rails tests
- name: Run backend tests
- name: Run backend tests (parallelized)
run: |
bundle exec rspec --profile=10 --format documentation
# Get all spec files and split them using round-robin distribution
# This ensures slow tests are distributed evenly across all nodes
SPEC_FILES=($(find spec -name '*_spec.rb' | sort))
TESTS=""
for i in "${!SPEC_FILES[@]}"; do
# Assign spec to this node if: index % total == node_index
if [ $(( i % ${{ matrix.ci_node_total }} )) -eq ${{ matrix.ci_node_index }} ]; then
TESTS="$TESTS ${SPEC_FILES[$i]}"
fi
done
if [ -n "$TESTS" ]; then
bundle exec rspec --profile=10 --format progress --format json --out tmp/rspec_results.json $TESTS
fi
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: rails-log-folder
name: rspec-results-${{ matrix.ci_node_index }}
path: tmp/rspec_results.json
- name: Upload rails log folder
uses: actions/upload-artifact@v4
if: failure()
with:
name: rails-log-folder-${{ matrix.ci_node_index }}
path: log
+2 -2
View File
@@ -36,5 +36,5 @@ jobs:
platforms: ${{ matrix.platform }}
push: false
load: false
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
+3
View File
@@ -55,6 +55,9 @@ gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby',
gem 'google-cloud-storage', '>= 1.48.0', require: false
gem 'image_processing'
##-- for actionmailbox --##
gem 'aws-actionmailbox-ses', '~> 0'
##-- gems for database --#
gem 'groupdate'
gem 'pg'
+11 -3
View File
@@ -136,9 +136,13 @@ GEM
audited (5.4.1)
activerecord (>= 5.0, < 7.7)
activesupport (>= 5.0, < 7.7)
aws-actionmailbox-ses (0.1.0)
actionmailbox (>= 7.1.0)
aws-sdk-s3 (~> 1, >= 1.123.0)
aws-sdk-sns (~> 1, >= 1.61.0)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
aws-sdk-core (3.171.1)
aws-sdk-core (3.188.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@@ -146,10 +150,13 @@ GEM
aws-sdk-kms (1.64.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.122.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-s3 (1.126.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sdk-sns (1.70.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
barnes (0.0.9)
@@ -995,6 +1002,7 @@ DEPENDENCIES
annotate
attr_extras
audited (~> 5.4, >= 5.4.1)
aws-actionmailbox-ses (~> 0)
aws-sdk-s3
azure-storage-blob!
barnes
+1 -1
View File
@@ -1 +1 @@
4.4.0
4.8.0
@@ -4,7 +4,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
before_action :agent_bot, except: [:index, :create]
def index
@agent_bots = AgentBot.where(account_id: [nil, Current.account.id])
@agent_bots = AgentBot.accessible_to(Current.account)
end
def show; end
@@ -37,7 +37,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
private
def agent_bot
@agent_bot = AgentBot.where(account_id: [nil, Current.account.id]).find(params[:id]) if params[:action] == 'show'
@agent_bot = AgentBot.accessible_to(Current.account).find(params[:id]) if params[:action] == 'show'
@agent_bot ||= Current.account.agent_bots.find(params[:id])
end
@@ -1,7 +1,7 @@
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
# assigns agent/team to a conversation
def create
if params.key?(:assignee_id)
if params.key?(:assignee_id) || agent_bot_assignment?
set_agent
elsif params.key?(:team_id)
set_team
@@ -13,17 +13,23 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
private
def set_agent
@agent = Current.account.users.find_by(id: params[:assignee_id])
@conversation.assignee = @agent
@conversation.save!
render_agent
resource = Conversations::AssignmentService.new(
conversation: @conversation,
assignee_id: params[:assignee_id],
assignee_type: params[:assignee_type]
).perform
render_agent(resource)
end
def render_agent
if @agent.nil?
render json: nil
def render_agent(resource)
case resource
when User
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: resource }
when AgentBot
render partial: 'api/v1/models/agent_bot_slim', formats: [:json], locals: { resource: resource }
else
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent }
render json: nil
end
end
@@ -32,4 +38,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
@conversation.update!(team: @team)
render json: @team
end
def agent_bot_assignment?
params[:assignee_type].to_s == 'AgentBot'
end
end
@@ -9,7 +9,13 @@ class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
private
def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME')
@global_config = GlobalConfig.get(
'LOGO_THUMBNAIL',
'BRAND_NAME',
'WIDGET_BRAND_URL',
'MAXIMUM_FILE_UPLOAD_SIZE',
'INSTALLATION_NAME'
)
end
def set_contact
@@ -25,6 +25,9 @@ module EnsureCurrentAccountHelper
end
def account_accessible_for_bot?(account)
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
return if @resource.account_id == account.id
return if @resource.agent_bot_inboxes.find_by(account_id: account.id)
render_unauthorized('Bot is not authorized to access this account')
end
end
+35 -20
View File
@@ -1,6 +1,31 @@
class DashboardController < ActionController::Base
include SwitchLocale
GLOBAL_CONFIG_KEYS = %w[
LOGO
LOGO_DARK
LOGO_THUMBNAIL
INSTALLATION_NAME
WIDGET_BRAND_URL
TERMS_URL
BRAND_URL
BRAND_NAME
PRIVACY_URL
DISPLAY_MANIFEST
CREATE_NEW_ACCOUNT_FROM_DASHBOARD
CHATWOOT_INBOX_TOKEN
API_CHANNEL_NAME
API_CHANNEL_THUMBNAIL
ANALYTICS_TOKEN
DIRECT_UPLOADS_ENABLED
MAXIMUM_FILE_UPLOAD_SIZE
HCAPTCHA_SITE_KEY
LOGOUT_REDIRECT_LINK
DISABLE_USER_PROFILE_UPDATE
DEPLOYMENT_ENV
INSTALLATION_PRICING_PLAN
].freeze
before_action :set_application_pack
before_action :set_global_config
before_action :set_dashboard_scripts
@@ -19,25 +44,7 @@ class DashboardController < ActionController::Base
end
def set_global_config
@global_config = GlobalConfig.get(
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
'INSTALLATION_NAME',
'WIDGET_BRAND_URL', 'TERMS_URL',
'BRAND_URL', 'BRAND_NAME',
'PRIVACY_URL',
'DISPLAY_MANIFEST',
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
'CHATWOOT_INBOX_TOKEN',
'API_CHANNEL_NAME',
'API_CHANNEL_THUMBNAIL',
'ANALYTICS_TOKEN',
'DIRECT_UPLOADS_ENABLED',
'HCAPTCHA_SITE_KEY',
'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV',
'INSTALLATION_PRICING_PLAN'
).merge(app_config)
@global_config = GlobalConfig.get(*GLOBAL_CONFIG_KEYS).merge(app_config)
end
def set_dashboard_scripts
@@ -71,10 +78,18 @@ class DashboardController < ActionController::Base
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH
GIT_SHA: GIT_HASH,
ALLOWED_LOGIN_METHODS: allowed_login_methods
}
end
def allowed_login_methods
methods = ['email']
methods << 'google_oauth' if GlobalConfigService.load('ENABLE_GOOGLE_OAUTH_LOGIN', 'true').to_s != 'false'
methods << 'saml' if ChatwootHub.pricing_plan != 'community' && GlobalConfigService.load('ENABLE_SAML_SSO_LOGIN', 'true').to_s != 'false'
methods
end
def set_application_pack
@application_pack = if request.path.include?('/auth') || request.path.include?('/login')
'v3app'
@@ -15,14 +15,20 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
end
def create
errors = []
params['app_config'].each do |key, value|
next unless @allowed_configs.include?(key)
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
i.value = value
i.save!
errors.concat(i.errors.full_messages) unless i.save
end
if errors.any?
redirect_to super_admin_app_config_path(config: @config), alert: errors.join(', ')
else
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
end
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
end
private
@@ -42,10 +48,13 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION],
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET],
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI]
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN]
}
@allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS])
@allowed_configs = mapping.fetch(
@config,
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS WEBHOOK_TIMEOUT MAXIMUM_FILE_UPLOAD_SIZE]
)
end
end
+8 -1
View File
@@ -14,7 +14,14 @@ class WidgetsController < ActionController::Base
private
def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED', 'INSTALLATION_NAME')
@global_config = GlobalConfig.get(
'LOGO_THUMBNAIL',
'BRAND_NAME',
'WIDGET_BRAND_URL',
'DIRECT_UPLOADS_ENABLED',
'MAXIMUM_FILE_UPLOAD_SIZE',
'INSTALLATION_NAME'
)
end
def set_web_widget
+7
View File
@@ -9,6 +9,13 @@ captain:
icon: 'icon-captain'
config_key: 'captain'
enterprise: true
saml:
name: 'SAML SSO'
description: 'Configuration for controlling SAML Single Sign-On availability'
enabled: <%= ChatwootApp.enterprise? %>
icon: 'icon-lock-line'
config_key: 'saml'
enterprise: true
custom_branding:
name: 'Custom Branding'
description: 'Apply your own branding to this installation.'
+37
View File
@@ -0,0 +1,37 @@
/* global axios */
import ApiClient from './ApiClient';
export const buildCompanyParams = (page, sort) => {
let params = `page=${page}`;
if (sort) {
params = `${params}&sort=${sort}`;
}
return params;
};
export const buildSearchParams = (query, page, sort) => {
let params = `q=${encodeURIComponent(query)}&page=${page}`;
if (sort) {
params = `${params}&sort=${sort}`;
}
return params;
};
class CompanyAPI extends ApiClient {
constructor() {
super('companies', { accountScoped: true });
}
get(params = {}) {
const { page = 1, sort = 'name' } = params;
const requestURL = `${this.url}?${buildCompanyParams(page, sort)}`;
return axios.get(requestURL);
}
search(query = '', page = 1, sort = 'name') {
const requestURL = `${this.url}/search?${buildSearchParams(query, page, sort)}`;
return axios.get(requestURL);
}
}
export default new CompanyAPI();
@@ -63,10 +63,9 @@ class ConversationApi extends ApiClient {
}
assignAgent({ conversationId, agentId }) {
return axios.post(
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
{}
);
return axios.post(`${this.url}/${conversationId}/assignments`, {
assignee_id: agentId,
});
}
assignTeam({ conversationId, teamId }) {
@@ -0,0 +1,142 @@
import companyAPI, {
buildCompanyParams,
buildSearchParams,
} from '../companies';
import ApiClient from '../ApiClient';
describe('#CompanyAPI', () => {
it('creates correct instance', () => {
expect(companyAPI).toBeInstanceOf(ApiClient);
expect(companyAPI).toHaveProperty('get');
expect(companyAPI).toHaveProperty('show');
expect(companyAPI).toHaveProperty('create');
expect(companyAPI).toHaveProperty('update');
expect(companyAPI).toHaveProperty('delete');
expect(companyAPI).toHaveProperty('search');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get with default params', () => {
companyAPI.get({});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies?page=1&sort=name'
);
});
it('#get with page and sort params', () => {
companyAPI.get({ page: 2, sort: 'domain' });
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies?page=2&sort=domain'
);
});
it('#get with descending sort', () => {
companyAPI.get({ page: 1, sort: '-created_at' });
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies?page=1&sort=-created_at'
);
});
it('#search with query', () => {
companyAPI.search('acme', 1, 'name');
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies/search?q=acme&page=1&sort=name'
);
});
it('#search with special characters in query', () => {
companyAPI.search('acme & co', 2, 'domain');
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies/search?q=acme%20%26%20co&page=2&sort=domain'
);
});
it('#search with descending sort', () => {
companyAPI.search('test', 1, '-created_at');
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies/search?q=test&page=1&sort=-created_at'
);
});
it('#search with empty query', () => {
companyAPI.search('', 1, 'name');
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies/search?q=&page=1&sort=name'
);
});
});
});
describe('#buildCompanyParams', () => {
it('returns correct string with page only', () => {
expect(buildCompanyParams(1)).toBe('page=1');
});
it('returns correct string with page and sort', () => {
expect(buildCompanyParams(1, 'name')).toBe('page=1&sort=name');
});
it('returns correct string with different page', () => {
expect(buildCompanyParams(3, 'domain')).toBe('page=3&sort=domain');
});
it('returns correct string with descending sort', () => {
expect(buildCompanyParams(1, '-created_at')).toBe(
'page=1&sort=-created_at'
);
});
it('returns correct string without sort parameter', () => {
expect(buildCompanyParams(2, '')).toBe('page=2');
});
});
describe('#buildSearchParams', () => {
it('returns correct string with all parameters', () => {
expect(buildSearchParams('acme', 1, 'name')).toBe(
'q=acme&page=1&sort=name'
);
});
it('returns correct string with special characters', () => {
expect(buildSearchParams('acme & co', 2, 'domain')).toBe(
'q=acme%20%26%20co&page=2&sort=domain'
);
});
it('returns correct string with empty query', () => {
expect(buildSearchParams('', 1, 'name')).toBe('q=&page=1&sort=name');
});
it('returns correct string without sort parameter', () => {
expect(buildSearchParams('test', 1, '')).toBe('q=test&page=1');
});
it('returns correct string with descending sort', () => {
expect(buildSearchParams('company', 3, '-created_at')).toBe(
'q=company&page=3&sort=-created_at'
);
});
it('encodes special characters correctly', () => {
expect(buildSearchParams('test@example.com', 1, 'name')).toBe(
'q=test%40example.com&page=1&sort=name'
);
});
});
@@ -92,8 +92,10 @@ describe('#ConversationAPI', () => {
it('#assignAgent', () => {
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments?assignee_id=34`,
{}
`/api/v1/conversations/12/assignments`,
{
assignee_id: 34,
}
);
});
@@ -0,0 +1,95 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { formatDistanceToNow } from 'date-fns';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
id: { type: Number, required: true },
name: { type: String, default: '' },
domain: { type: String, default: '' },
contactsCount: { type: Number, default: 0 },
description: { type: String, default: '' },
avatarUrl: { type: String, default: '' },
updatedAt: { type: [String, Number], default: null },
});
const emit = defineEmits(['showCompany']);
const { t } = useI18n();
const onClickViewDetails = () => emit('showCompany', props.id);
const displayName = computed(() => props.name || t('COMPANIES.UNNAMED'));
const avatarSource = computed(() => props.avatarUrl || null);
const formattedUpdatedAt = computed(() => {
if (!props.updatedAt) return '';
return formatDistanceToNow(new Date(props.updatedAt), { addSuffix: true });
});
</script>
<template>
<CardLayout layout="row" @click="onClickViewDetails">
<div class="flex items-center justify-start flex-1 gap-4">
<Avatar
:username="displayName"
:src="avatarSource"
class="shrink-0"
:name="name"
:size="48"
hide-offline-status
rounded-full
/>
<div class="flex flex-col gap-0.5 flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 min-w-0">
<span class="text-base font-medium truncate text-n-slate-12">
{{ displayName }}
</span>
<span
v-if="domain && description"
class="inline-flex items-center gap-1.5 text-sm text-n-slate-11 truncate"
>
<Icon icon="i-lucide-globe" size="size-3.5 text-n-slate-11" />
<span class="truncate">{{ domain }}</span>
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 min-w-0">
<span
v-if="domain && !description"
class="inline-flex items-center gap-1.5 text-sm text-n-slate-11 truncate"
>
<Icon icon="i-lucide-globe" size="size-3.5 text-n-slate-11" />
<span class="truncate">{{ domain }}</span>
</span>
<span v-if="description" class="text-sm text-n-slate-11 truncate">
{{ description }}
</span>
<div
v-if="(description || domain) && contactsCount"
class="w-px h-3 bg-n-slate-6"
/>
<span
v-if="contactsCount"
class="inline-flex items-center gap-1.5 text-sm text-n-slate-11 truncate"
>
<Icon icon="i-lucide-contact" size="size-3.5 text-n-slate-11" />
{{ t('COMPANIES.CONTACTS_COUNT', { n: contactsCount }) }}
</span>
</div>
<span
v-if="updatedAt"
class="inline-flex items-center gap-1.5 text-sm text-n-slate-11 flex-shrink-0"
>
{{ formattedUpdatedAt }}
</span>
</div>
</div>
</div>
</CardLayout>
</template>
@@ -0,0 +1,55 @@
<script setup>
import Input from 'dashboard/components-next/input/Input.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import CompanySortMenu from './components/CompanySortMenu.vue';
defineProps({
showSearch: { type: Boolean, default: true },
searchValue: { type: String, default: '' },
headerTitle: { type: String, required: true },
activeSort: { type: String, default: 'last_activity_at' },
activeOrdering: { type: String, default: '' },
});
const emit = defineEmits(['search', 'update:sort']);
</script>
<template>
<header class="sticky top-0 z-10">
<div
class="flex items-start sm:items-center justify-between w-full py-6 px-6 gap-2 mx-auto max-w-[60rem]"
>
<span class="text-xl font-medium truncate text-n-slate-12">
{{ headerTitle }}
</span>
<div class="flex items-center flex-row flex-shrink-0 gap-2">
<div class="flex items-center">
<CompanySortMenu
:active-sort="activeSort"
:active-ordering="activeOrdering"
@update:sort="emit('update:sort', $event)"
/>
</div>
<div v-if="showSearch" class="flex items-center gap-2 w-full">
<Input
:model-value="searchValue"
type="search"
:placeholder="$t('CONTACTS_LAYOUT.HEADER.SEARCH_PLACEHOLDER')"
:custom-input-class="[
'h-8 [&:not(.focus)]:!border-transparent bg-n-alpha-2 dark:bg-n-solid-1 ltr:!pl-8 !py-1 rtl:!pr-8',
]"
class="w-full"
@input="emit('search', $event.target.value)"
>
<template #prefix>
<Icon
icon="i-lucide-search"
class="absolute -translate-y-1/2 text-n-slate-11 size-4 top-1/2 ltr:left-2 rtl:right-2"
/>
</template>
</Input>
</div>
</div>
</div>
</header>
</template>
@@ -0,0 +1,116 @@
<script setup>
import { ref, computed, toRef } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
import SelectMenu from 'dashboard/components-next/selectmenu/SelectMenu.vue';
const props = defineProps({
activeSort: {
type: String,
default: 'name',
},
activeOrdering: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:sort']);
const { t } = useI18n();
const isMenuOpen = ref(false);
const sortMenus = [
{
label: t('COMPANIES.SORT_BY.OPTIONS.NAME'),
value: 'name',
},
{
label: t('COMPANIES.SORT_BY.OPTIONS.DOMAIN'),
value: 'domain',
},
{
label: t('COMPANIES.SORT_BY.OPTIONS.CREATED_AT'),
value: 'created_at',
},
];
const orderingMenus = [
{
label: t('COMPANIES.ORDER.OPTIONS.ASCENDING'),
value: '',
},
{
label: t('COMPANIES.ORDER.OPTIONS.DESCENDING'),
value: '-',
},
];
// Converted the props to refs for better reactivity
const activeSort = toRef(props, 'activeSort');
const activeOrdering = toRef(props, 'activeOrdering');
const activeSortLabel = computed(() => {
const selectedMenu = sortMenus.find(menu => menu.value === activeSort.value);
return selectedMenu?.label || t('COMPANIES.SORT_BY.LABEL');
});
const activeOrderingLabel = computed(() => {
const selectedMenu = orderingMenus.find(
menu => menu.value === activeOrdering.value
);
return selectedMenu?.label || t('COMPANIES.ORDER.LABEL');
});
const handleSortChange = value => {
emit('update:sort', { sort: value, order: props.activeOrdering });
};
const handleOrderChange = value => {
emit('update:sort', { sort: props.activeSort, order: value });
};
</script>
<template>
<div class="relative">
<Button
icon="i-lucide-arrow-down-up"
color="slate"
size="sm"
variant="ghost"
:class="isMenuOpen ? 'bg-n-alpha-2' : ''"
@click="isMenuOpen = !isMenuOpen"
/>
<div
v-if="isMenuOpen"
v-on-clickaway="() => (isMenuOpen = false)"
class="absolute top-full mt-1 ltr:-right-32 rtl:-left-32 sm:ltr:right-0 sm:rtl:left-0 flex flex-col gap-4 bg-n-alpha-3 backdrop-blur-[100px] border border-n-weak w-72 rounded-xl p-4"
>
<div class="flex items-center justify-between gap-2">
<span class="text-sm text-n-slate-12">
{{ t('COMPANIES.SORT_BY.LABEL') }}
</span>
<SelectMenu
:model-value="activeSort"
:options="sortMenus"
:label="activeSortLabel"
@update:model-value="handleSortChange"
/>
</div>
<div class="flex items-center justify-between gap-2">
<span class="text-sm text-n-slate-12">
{{ t('COMPANIES.ORDER.LABEL') }}
</span>
<SelectMenu
:model-value="activeOrdering"
:options="orderingMenus"
:label="activeOrderingLabel"
@update:model-value="handleOrderChange"
/>
</div>
</div>
</div>
</template>
@@ -0,0 +1,51 @@
<script setup>
import CompanyHeader from './CompaniesHeader/CompanyHeader.vue';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
defineProps({
searchValue: { type: String, default: '' },
headerTitle: { type: String, default: '' },
currentPage: { type: Number, default: 1 },
totalItems: { type: Number, default: 100 },
activeSort: { type: String, default: 'name' },
activeOrdering: { type: String, default: '' },
showPaginationFooter: { type: Boolean, default: true },
});
const emit = defineEmits(['update:currentPage', 'update:sort', 'search']);
const updateCurrentPage = page => {
emit('update:currentPage', page);
};
</script>
<template>
<section
class="flex w-full h-full gap-4 overflow-hidden justify-evenly bg-n-background"
>
<div class="flex flex-col w-full h-full transition-all duration-300">
<CompanyHeader
:search-value="searchValue"
:header-title="headerTitle"
:active-sort="activeSort"
:active-ordering="activeOrdering"
@search="emit('search', $event)"
@update:sort="emit('update:sort', $event)"
/>
<main class="flex-1 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem]">
<slot name="default" />
</div>
</main>
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0 px-4 pb-4">
<PaginationFooter
current-page-info="COMPANIES_LAYOUT.PAGINATION_FOOTER.SHOWING"
:current-page="currentPage"
:total-items="totalItems"
:items-per-page="25"
@update:current-page="updateCurrentPage"
/>
</footer>
</div>
</section>
</template>
@@ -2,6 +2,7 @@
import { ref, computed } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store.js';
import { usePolicy } from 'dashboard/composables/usePolicy';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -69,6 +70,8 @@ const props = defineProps({
const emit = defineEmits(['click', 'close', 'update:currentPage']);
const { t } = useI18n();
const route = useRoute();
const { shouldShowPaywall } = usePolicy();
@@ -76,14 +79,16 @@ const showAssistantSwitcherDropdown = ref(false);
const createAssistantDialogRef = ref(null);
const assistants = useMapGetter('captainAssistants/getRecords');
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
const currentAssistantId = computed(() => route.params.assistantId);
const isFetchingAssistants = computed(() => uiFlags.value?.fetchingList);
const activeAssistantName = computed(() => {
return (
assistants.value?.find(
assistant => assistant.id === Number(currentAssistantId.value)
)?.name || ''
)?.name || t('CAPTAIN.ASSISTANT_SWITCHER.NEW_ASSISTANT')
);
});
@@ -118,15 +123,18 @@ const handleCreateAssistant = () => {
>
<div class="flex gap-3 items-center">
<BackButton v-if="backUrl" :back-url="backUrl" />
<div v-if="showAssistantSwitcher" class="flex items-center gap-2">
<div
v-if="showAssistantSwitcher && !showPaywall"
class="flex items-center gap-2"
>
<div class="flex items-center gap-2">
<span
v-if="activeAssistantName"
v-if="!isFetchingAssistants"
class="text-xl font-medium truncate text-n-slate-12"
>
{{ activeAssistantName }}
</span>
<div v-if="activeAssistantName" class="relative group">
<div class="relative group">
<OnClickOutside
@trigger="showAssistantSwitcherDropdown = false"
>
@@ -135,6 +143,8 @@ const handleCreateAssistant = () => {
variant="ghost"
color="slate"
size="xs"
:disabled="isFetchingAssistants"
:is-loading="isFetchingAssistants"
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3 [&>span]:size-4"
@click="toggleAssistantSwitcher"
/>
@@ -151,7 +161,7 @@ const handleCreateAssistant = () => {
</div>
<div class="flex items-center gap-4">
<div
v-if="showAssistantSwitcher && headerTitle"
v-if="showAssistantSwitcher && !showPaywall && headerTitle"
class="w-0.5 h-4 rounded-2xl bg-n-weak"
/>
<span
@@ -139,5 +139,10 @@ const openCreateAssistantDialog = () => {
/>
</Button>
</div>
<div v-else class="flex flex-col items-center gap-2 px-4 py-3">
<p class="text-sm text-n-slate-11">
{{ t('CAPTAIN.ASSISTANT_SWITCHER.EMPTY_LIST') }}
</p>
</div>
</div>
</template>
@@ -1,9 +1,10 @@
<script setup>
import { defineProps, computed, ref } from 'vue';
import { defineProps, computed, reactive } from 'vue';
import Message from './Message.vue';
import { MESSAGE_TYPES } from './constants.js';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import { useMapGetter } from 'dashboard/composables/store.js';
import MessageApi from 'dashboard/api/inbox/message.js';
/**
* Props definition for the component
@@ -39,15 +40,53 @@ const props = defineProps({
});
const emit = defineEmits(['retry']);
const store = useStore();
const fetchingConversations = ref(new Set());
const allMessages = computed(() => {
return useCamelCase(props.messages, { deep: true });
});
const currentChat = useMapGetter('getSelectedChat');
// Cache for fetched reply messages to avoid duplicate API calls
const fetchedReplyMessages = reactive(new Map());
/**
* Fetches a specific message from the API by trying to get messages around it
* @param {number} messageId - The ID of the message to fetch
* @param {number} conversationId - The ID of the conversation
* @returns {Promise<Object|null>} - The fetched message or null if not found/error
*/
const fetchReplyMessage = async (messageId, conversationId) => {
// Return cached result if already fetched
if (fetchedReplyMessages.has(messageId)) {
return fetchedReplyMessages.get(messageId);
}
try {
const response = await MessageApi.getPreviousMessages({
conversationId,
before: messageId + 100,
after: messageId - 100,
});
const messages = response.data?.payload || [];
const targetMessage = messages.find(msg => msg.id === messageId);
if (targetMessage) {
const camelCaseMessage = useCamelCase(targetMessage);
fetchedReplyMessages.set(messageId, camelCaseMessage);
return camelCaseMessage;
}
// Cache null result to avoid repeated API calls
fetchedReplyMessages.set(messageId, null);
return null;
} catch (error) {
fetchedReplyMessages.set(messageId, null);
return null;
}
};
/**
* Determines if a message should be grouped with the next message
* @param {Number} index - Index of the current message
@@ -87,43 +126,36 @@ const shouldGroupWithNext = (index, searchList) => {
* @returns {Object|null} - The message being replied to, or null if not found
*/
const getInReplyToMessage = parentMessage => {
if (!parentMessage) return null;
const inReplyToMessageId =
parentMessage?.contentAttributes?.inReplyTo ??
parentMessage?.content_attributes?.in_reply_to;
parentMessage.contentAttributes?.inReplyTo ??
parentMessage.content_attributes?.in_reply_to;
if (!inReplyToMessageId) return null;
// 1. Check props messages (already camelCased via allMessages)
const foundInProps = allMessages.value?.find(
m => m.id === inReplyToMessageId
);
if (foundInProps) return foundInProps;
// Try to find in current messages first
let replyMessage = props.messages?.find(msg => msg.id === inReplyToMessageId);
// 2. Check store messages
const foundInStore = currentChat.value?.messages?.find(
m => m.id === inReplyToMessageId
);
if (foundInStore) return useCamelCase(foundInStore);
// 3. Fetch if not currently fetching for this conversation
const conversationId = currentChat.value?.id;
if (
conversationId &&
!currentChat.value.allMessagesLoaded &&
!fetchingConversations.value.has(conversationId)
) {
fetchingConversations.value.add(conversationId);
store
.dispatch('fetchPreviousMessages', {
conversationId,
before: currentChat.value.messages?.[0]?.id ?? null,
})
.finally(() => {
fetchingConversations.value.delete(conversationId);
});
// Then try store messages
if (!replyMessage && currentChat.value?.messages) {
replyMessage = currentChat.value.messages.find(
msg => msg.id === inReplyToMessageId
);
}
return null;
// Then check fetch cache
if (!replyMessage && fetchedReplyMessages.has(inReplyToMessageId)) {
replyMessage = fetchedReplyMessages.get(inReplyToMessageId);
}
// If still not found and we have conversation context, fetch it
if (!replyMessage && currentChat.value?.id) {
fetchReplyMessage(inReplyToMessageId, currentChat.value.id);
return null; // Let UI handle loading state
}
return replyMessage ? useCamelCase(replyMessage) : null;
};
</script>
@@ -1,7 +1,8 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useLoadWithRetry } from 'dashboard/composables/loadWithRetry';
import BaseBubble from './Base.vue';
import Button from 'next/button/Button.vue';
import Icon from 'next/icon/Icon.vue';
@@ -11,7 +12,6 @@ import { downloadFile } from '@chatwoot/utils';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
const emit = defineEmits(['error']);
const { t } = useI18n();
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
@@ -20,14 +20,16 @@ const attachment = computed(() => {
return attachments.value[0];
});
const hasError = ref(false);
const { isLoaded, hasError, loadWithRetry } = useLoadWithRetry();
const showGallery = ref(false);
const isDownloading = ref(false);
const handleError = () => {
hasError.value = true;
emit('error');
};
onMounted(() => {
if (attachment.value?.dataUrl) {
loadWithRetry(attachment.value.dataUrl);
}
});
const downloadAttachment = async () => {
const { fileType, dataUrl, extension } = attachment.value;
@@ -40,6 +42,10 @@ const downloadAttachment = async () => {
isDownloading.value = false;
}
};
const handleImageError = () => {
hasError.value = true;
};
</script>
<template>
@@ -54,14 +60,12 @@ const downloadAttachment = async () => {
{{ $t('COMPONENTS.MEDIA.IMAGE_UNAVAILABLE') }}
</p>
</div>
<div v-else class="relative group rounded-lg overflow-hidden">
<div v-else-if="isLoaded" class="relative group rounded-lg overflow-hidden">
<img
class="skip-context-menu"
:src="attachment.dataUrl"
:width="attachment.width"
:height="attachment.height"
@click="onClick"
@error="handleError"
/>
<div
class="inset-0 p-2 pointer-events-none absolute bg-gradient-to-tl from-n-slate-12/30 dark:from-n-slate-1/50 via-transparent to-transparent hidden group-hover:flex"
@@ -86,7 +90,7 @@ const downloadAttachment = async () => {
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@error="handleError"
@error="handleImageError"
@close="() => (showGallery = false)"
/>
</template>
@@ -351,6 +351,23 @@ const menuItems = computed(() => {
},
],
},
{
name: 'Companies',
label: t('SIDEBAR.COMPANIES'),
icon: 'i-lucide-building-2',
children: [
{
name: 'All Companies',
label: t('SIDEBAR.ALL_COMPANIES'),
to: accountScopedRoute(
'companies_dashboard_index',
{},
{ page: 1, search: undefined }
),
activeOn: ['companies_dashboard_index'],
},
],
},
{
name: 'Reports',
label: t('SIDEBAR.REPORTS'),
@@ -23,6 +23,7 @@ defineProps({
slate
sm
class="relative"
no-animation
:icon="icon"
:trailing-icon="trailingIcon"
>
@@ -30,7 +30,7 @@ const assignedAgent = computed({
return currentChat.value?.meta?.assignee;
},
set(agent) {
const agentId = agent ? agent.id : 0;
const agentId = agent ? agent.id : null;
store.dispatch('setCurrentChatAssignee', agent);
store.dispatch('assignAgent', {
conversationId: currentChat.value?.id,
@@ -0,0 +1,57 @@
import { ref } from 'vue';
export const useLoadWithRetry = (config = {}) => {
const maxRetry = config.max_retry || 3;
const backoff = config.backoff || 1000;
const isLoaded = ref(false);
const hasError = ref(false);
const loadWithRetry = async url => {
const attemptLoad = () => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
isLoaded.value = true;
hasError.value = false;
resolve();
};
img.onerror = () => {
reject(new Error('Failed to load image'));
};
img.src = url;
});
};
const sleep = ms => {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
};
const retry = async (attempt = 0) => {
try {
await attemptLoad();
} catch (error) {
if (attempt + 1 >= maxRetry) {
hasError.value = true;
isLoaded.value = false;
return;
}
await sleep(backoff * (attempt + 1));
await retry(attempt + 1);
}
};
await retry();
};
return {
isLoaded,
hasError,
loadWithRetry,
};
};
@@ -12,7 +12,11 @@ vi.mock('dashboard/composables', () => ({
}));
vi.mock('vue-i18n');
vi.mock('activestorage');
vi.mock('shared/helpers/FileHelper');
vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(),
resolveMaximumFileUploadSize: vi.fn(value => Number(value) || 40),
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE: 40,
}));
vi.mock('@chatwoot/utils');
describe('useFileUpload', () => {
@@ -36,7 +40,9 @@ describe('useFileUpload', () => {
getCurrentAccountId: { value: '123' },
getCurrentUser: { value: { access_token: 'test-token' } },
getSelectedChat: { value: { id: '456' } },
'globalConfig/get': { value: { directUploadsEnabled: true } },
'globalConfig/get': {
value: { directUploadsEnabled: true, maximumFileUploadSize: 40 },
},
};
return getterMap[getter];
});
@@ -86,7 +92,9 @@ describe('useFileUpload', () => {
getCurrentAccountId: { value: '123' },
getCurrentUser: { value: { access_token: 'test-token' } },
getSelectedChat: { value: { id: '456' } },
'globalConfig/get': { value: { directUploadsEnabled: false } },
'globalConfig/get': {
value: { directUploadsEnabled: false, maximumFileUploadSize: 40 },
},
};
return getterMap[getter];
});
@@ -4,7 +4,11 @@ import { useI18n } from 'vue-i18n';
import { DirectUpload } from 'activestorage';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import {
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
resolveMaximumFileUploadSize,
} from 'shared/helpers/FileHelper';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
/**
* Composable for handling file uploads in conversations
@@ -21,18 +25,34 @@ export const useFileUpload = ({ inbox, attachFile, isPrivateNote = false }) => {
const currentChat = useMapGetter('getSelectedChat');
const globalConfig = useMapGetter('globalConfig/get');
const installationLimit = resolveMaximumFileUploadSize(
globalConfig.value?.maximumFileUploadSize
);
// helper: compute max upload size for a given file's mime
const maxSizeFor = mime => {
// Use default file size limit for private notes
// Use default/installation limit for private notes
if (isPrivateNote) {
return MAXIMUM_FILE_UPLOAD_SIZE;
return installationLimit;
}
return getMaxUploadSizeByChannel({
channelType: inbox?.channel_type,
const channelType = inbox?.channel_type;
if (!channelType || channelType === INBOX_TYPES.WEB) {
return installationLimit;
}
const channelLimit = getMaxUploadSizeByChannel({
channelType,
medium: inbox?.medium, // e.g. 'sms' | 'whatsapp' | etc.
mime, // e.g. 'image/png'
});
if (channelLimit === DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE) {
return installationLimit;
}
return Math.min(channelLimit, installationLimit);
};
const alertOverLimit = maxSizeMB =>
+1
View File
@@ -41,6 +41,7 @@ export const FEATURE_FLAGS = {
CAPTAIN_V2: 'captain_integration_v2',
SAML: 'saml',
QUOTED_EMAIL_REPLY: 'quoted_email_reply',
COMPANIES: 'companies',
};
export const PREMIUM_FEATURES = [
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "رابط Webhook",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "قائد",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "الإعدادات",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "آخر 7 أيام",
"LAST_14_DAYS": "آخر 14 يوماً",
"LAST_30_DAYS": "آخر 30 يوماً",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "آخر 3 أشهر",
"LAST_6_MONTHS": "آخر 6 أشهر",
"LAST_YEAR": "العام الماضي",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "تم تعليق الحساب",
"MESSAGE": "تم تعليق حسابك. يرجى الاتصال بفريق الدعم للمزيد من المعلومات."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "قنوات التواصل",
"CAPTAIN_SETTINGS": "الإعدادات",
"HOME": "الرئيسية",
"AGENTS": "وكيل الدعم",
"AGENT_BOTS": "الروبوتات",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Агенти",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "URL del webhook",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Configuracions",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Últims 7 dies",
"LAST_14_DAYS": "Últims 14 dies",
"LAST_30_DAYS": "Últims 30 dies",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Últims tres mesos",
"LAST_6_MONTHS": "Últims sis mesos",
"LAST_YEAR": "Darrer any",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Compte suspès",
"MESSAGE": "El teu compte està suspès. Posa't en contacte amb l'equip d'assistència per obtenir més informació."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Safates d'entrada",
"CAPTAIN_SETTINGS": "Configuracions",
"HOME": "Inici",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "URL webového háčku",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Nastavení",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Posledních 7 dní",
"LAST_14_DAYS": "Posledních 14 dní",
"LAST_30_DAYS": "Posledních 30 dní",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Poslední 3 měsíce",
"LAST_6_MONTHS": "Posledních 6 měsíců",
"LAST_YEAR": "Poslední rok",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Schránky",
"CAPTAIN_SETTINGS": "Nastavení",
"HOME": "Domů",
"AGENTS": "Agenti",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Indstillinger",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Seneste 7 dage",
"LAST_14_DAYS": "Seneste 14 dage",
"LAST_30_DAYS": "Seneste 30 dage",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Seneste 3 måneder",
"LAST_6_MONTHS": "Seneste 6 måneder",
"LAST_YEAR": "Sidste år",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Konto Suspenderet",
"MESSAGE": "Din konto er suspenderet. Gå ud til supportteamet for mere information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Indbakker",
"CAPTAIN_SETTINGS": "Indstillinger",
"HOME": "Hjem",
"AGENTS": "Agenter",
"AGENT_BOTS": "Bots",
@@ -18,9 +18,9 @@
"CREATED_AT_LABEL": "Erstellt",
"NEW_MESSAGE": "Neue Nachricht",
"CALL": "Anruf",
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
"CALL_UNDER_DEVELOPMENT": "Anrufe sind in der Entwicklung",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
"TITLE": "Spracheingang wählen"
},
"CONVERSATIONS": {
"NO_RECORDS_FOUND": "Es sind keine vorherigen Gespräche mit diesem Kontakt verbunden.",
@@ -554,12 +554,12 @@
"WROTE": "schrieb",
"YOU": "Sie",
"SAVE": "Notiz speichern",
"ADD_NOTE": "Add contact note",
"ADD_NOTE": "Füge eine Kontakt-Notiz hinzu",
"EXPAND": "Erweitern",
"COLLAPSE": "Einklappen",
"NO_NOTES": "Keine Notizen, Sie können Notizen auf der Kontakt-Detailseite hinzufügen.",
"EMPTY_STATE": "Es gibt keine Notizen zu diesem Kontakt. Sie können eine Notiz hinzufügen, indem Sie diese in das obige Feld eingeben.",
"CONVERSATION_EMPTY_STATE": "There are no notes yet. Use the Add note button to create one."
"CONVERSATION_EMPTY_STATE": "Es sind noch keine Notizen vorhanden. Benutzen Sie die Schaltfläche \"Notiz hinzufügen\" um eine zu erstellen."
}
},
"EMPTY_STATE": {
@@ -574,21 +574,21 @@
"CONTACTS_BULK_ACTIONS": {
"ASSIGN_LABELS": "Labels zuweisen",
"ASSIGN_LABELS_SUCCESS": "Labels erfolgreich zugewiesen.",
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
"NO_LABELS_FOUND": "No labels available yet.",
"ASSIGN_LABELS_FAILED": "Fehler beim Zuweisen der Labels",
"DESCRIPTION": "Wählen Sie die Labels aus, die zu den ausgewählten Kontakten hinzugefügt werden sollen.",
"NO_LABELS_FOUND": "Noch keine Labels vorhanden.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"CLEAR_SELECTION": "Auswahl löschen",
"SELECT_ALL": "Alle auswählen ({count})",
"DELETE_CONTACTS": "Löschen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_SUCCESS": "Kontakte erfolgreich gelöscht.",
"DELETE_FAILED": "Kontakte konnten nicht gelöscht werden.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"TITLE": "Ausgewählte Kontakte löschen",
"SINGULAR_TITLE": "Ausgewählten Kontakt löschen",
"DESCRIPTION": "Dies wird {count} ausgewählte Kontakte dauerhaft löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
"SINGULAR_DESCRIPTION": "Der ausgewählte Kontakt wird dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"CONFIRM_MULTIPLE": "Kontakte löschen",
"CONFIRM_SINGLE": "Kontakt löschen"
}
},
@@ -36,10 +36,10 @@
"NOT_ASSIGNED_TO_YOU": "Diese Konversation ist Ihnen nicht zugeordnet. Möchten Sie dieses Gespräch sich selbst zuordnen?",
"ASSIGN_TO_ME": "Mir zuweisen",
"BOT_HANDOFF_MESSAGE": "Sie antworten auf eine Unterhaltung, die derzeit von einem Assistenten oder einem Bot bearbeitet wird.",
"BOT_HANDOFF_ACTION": "Mark open and assign to you",
"BOT_HANDOFF_REOPEN_ACTION": "Mark conversation open",
"BOT_HANDOFF_SUCCESS": "Conversation has been handed over to you",
"BOT_HANDOFF_ERROR": "Failed to take over the conversation. Please try again.",
"BOT_HANDOFF_ACTION": "Als geöffnet markieren und Ihnen zuweisen",
"BOT_HANDOFF_REOPEN_ACTION": "Gespräch als geöffnet markieren",
"BOT_HANDOFF_SUCCESS": "Die Unterhaltung wurde dir übergeben",
"BOT_HANDOFF_ERROR": "Die Unterhaltung konnte nicht übernommen werden. Bitte versuche es erneut.",
"TWILIO_WHATSAPP_CAN_REPLY": "Sie können auf diese Konversation nur mit einer Nachrichtenvorlage antworten wegen",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24-Stunden-Nachrichtenfenster-Beschränkung",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Dieser Instagram-Account wurde in den neuen Instagram-Kanal übertragen. Alle neuen Nachrichten werden dort erscheinen. Sie werden keine Nachrichten mehr von dieser Unterhaltung senden können.",
@@ -72,15 +72,15 @@
"HIDE_LABELS": "Labels ausblenden"
},
"VOICE_CALL": {
"INCOMING_CALL": "Incoming call",
"OUTGOING_CALL": "Outgoing call",
"CALL_IN_PROGRESS": "Call in progress",
"NO_ANSWER": "No answer",
"MISSED_CALL": "Missed call",
"CALL_ENDED": "Call ended",
"NOT_ANSWERED_YET": "Not answered yet",
"THEY_ANSWERED": "They answered",
"YOU_ANSWERED": "You answered"
"INCOMING_CALL": "Eingehender Anruf",
"OUTGOING_CALL": "Ausgehender Anruf",
"CALL_IN_PROGRESS": "Anruf läuft",
"NO_ANSWER": "Keine Antwort",
"MISSED_CALL": "Entgangener Anruf",
"CALL_ENDED": "Anruf beendet",
"NOT_ANSWERED_YET": "Noch nicht beantwortet",
"THEY_ANSWERED": "Sie antworteten",
"YOU_ANSWERED": "Sie haben beantwortet"
},
"HEADER": {
"RESOLVE_ACTION": "Fall schließen",
@@ -160,9 +160,9 @@
"AGENTS_LOADING": "Agenten werden geladen...",
"ASSIGN_TEAM": "Team zuweisen",
"DELETE": "Unterhaltung löschen",
"OPEN_IN_NEW_TAB": "Open in new tab",
"COPY_LINK": "Copy conversation link",
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
"OPEN_IN_NEW_TAB": "In neuem Tab öffnen",
"COPY_LINK": "Konversationslink kopieren",
"COPY_LINK_SUCCESS": "Konversationslink in Zwischenablage kopiert",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "Konversations-ID {conversationId} zugewiesen zu \"{agentName}\"",
@@ -229,11 +229,11 @@
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
"ENABLE_TOOLTIP": "Zitierten E-Mail-Thread einbeziehen",
"DISABLE_TOOLTIP": "Zitierten E-Mail-Thread nicht einbeziehen",
"REMOVE_PREVIEW": "Zitierte E-Mail-Themen entfernen",
"COLLAPSE": "Vorschau ausblenden",
"EXPAND": "Vorschau erweitern"
}
},
"VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar",
@@ -338,7 +338,7 @@
"CONTACT_ATTRIBUTES": "Kontakt-Attribute",
"PREVIOUS_CONVERSATION": "Vorherige Konversationen",
"MACROS": "Makros",
"LINEAR_ISSUES": "Linked Linear Issues",
"LINEAR_ISSUES": "Verknüpfte lineare Probleme",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
@@ -3,7 +3,7 @@
"LIMIT_MESSAGES": {
"CONVERSATION": "Sie haben das Konversationslimit überschritten. Der Hacker Plan erlaubt nur 500 Unterhaltungen.",
"INBOXES": "Sie haben das Posteingang-Limit überschritten. Der Hacker-Plan unterstützt nur Live-Chat der Webseite. Zusätzliche Posteingänge wie E-Mail, WhatsApp usw. erfordern einen kostenpflichtigen Tarif.",
"AGENTS": "You have exceeded the agent limit. Your plan only allows {allowedAgents} agents.",
"AGENTS": "Sie haben das Agenten-Limit überschritten. Ihr Plan erlaubt nur {allowedAgents} Agenten.",
"NON_ADMIN": "Bitte kontaktieren Sie Ihren Administrator, um den Plan zu upgraden und alle Funktionen weiterzunutzen."
},
"TITLE": "Kontoeinstellungen",
@@ -25,13 +25,13 @@
"DISMISS": "Stornieren",
"PLACE_HOLDER": "Bitte {accountName} zur Bestätigung eingeben"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SUCCESS": "Konto zur Löschung markiert",
"FAILURE": "Konto konnte nicht gelöscht werden, versuche es erneut!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
"TITLE": "Konto für Löschung geplant",
"MESSAGE_MANUAL": "Dieses Konto ist für das Löschen auf {deletionDate} geplant. Dies wurde von einem Administrator angefordert. Sie können das Löschen vor diesem Datum abbrechen.",
"MESSAGE_INACTIVITY": "Dieses Konto ist für die Löschung auf {deletionDate} aufgrund von Kontoinaktivität vorgesehen. Sie können die Löschung vor diesem Datum abbrechen.",
"CLEAR_BUTTON": "Geplante Löschung abbrechen"
}
},
"FORM": {
@@ -45,13 +45,13 @@
"NOTE": "Diese ID ist erforderlich, wenn Sie eine API-basierte Integration erstellen"
},
"AUTO_RESOLVE": {
"TITLE": "Auto-resolve conversations",
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period of inactivity.",
"TITLE": "Unterhaltungen automatisch auflösen",
"NOTE": "Diese Konfiguration würde es Ihnen ermöglichen, die Unterhaltung nach einer gewissen Zeit der Inaktivität automatisch zu beenden.",
"DURATION": {
"LABEL": "Inactivity duration",
"HELP": "Time period of inactivity after which conversation is auto-resolved",
"LABEL": "Dauer der Inaktivität",
"HELP": "Zeit der Inaktivität, nach der die Unterhaltung automatisch gelöst wird",
"PLACEHOLDER": "30",
"ERROR": "Auto resolve duration should be between 10 minutes and 999 days",
"ERROR": "Automatische Auflösungsdauer sollte zwischen 10 Minuten und 999 Tagen liegen",
"API": {
"SUCCESS": "Auto resolve settings updated successfully",
"ERROR": "Failed to update auto resolve settings"
@@ -108,7 +108,7 @@
"LABEL": "Inactivity duration for resolution",
"HELP": "Duration after a conversation should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Auto resolve duration should be between 10 minutes and 999 days",
"ERROR": "Automatische Auflösungsdauer sollte zwischen 10 Minuten und 999 Tagen liegen",
"API": {
"SUCCESS": "Auto resolve settings updated successfully",
"ERROR": "Failed to update auto resolve settings"
@@ -296,17 +296,17 @@
"WAITING_FOR_AUTH": "Waiting for authentication...",
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
"SIGNUP_ERROR": "Signup error occurred",
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured",
"MANUAL_FALLBACK": "If your number is already connected to the WhatsApp Business Platform (API), or if youre a tech provider onboarding your own number, please use the {link} flow",
"MANUAL_LINK_TEXT": "manual setup flow"
"AUTH_NOT_COMPLETED": "Authentifizierung nicht abgeschlossen. Bitte starte den Prozess neu.",
"SUCCESS_FALLBACK": "WhatsApp Business Account wurde erfolgreich konfiguriert",
"MANUAL_FALLBACK": "Wenn Ihre Nummer bereits mit der WhatsApp Business Plattform (API) verbunden ist, oder wenn Sie als Technologieanbieter Ihre eigene Nummer an Bord haben, verwenden Sie bitte den {link} Flow",
"MANUAL_LINK_TEXT": "Manueller Einrichtungsablauf"
},
"API": {
"ERROR_MESSAGE": "Wir konnten den WhatsApp-Kanal nicht speichern"
}
},
"VOICE": {
"TITLE": "Voice Channel",
"TITLE": "Sprachkanal",
"DESC": "Integrate Twilio Voice and start supporting your customers via phone calls.",
"PHONE_NUMBER": {
"LABEL": "Telefonnummer",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook-URL",
"PLACEHOLDER": "Beispiel: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Kapitän",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Probiere diese Prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Einstellungen",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Letzten 7 Tage",
"LAST_14_DAYS": "Letzten 14 Tage",
"LAST_30_DAYS": "Letzte 30 Tage",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Die letzten 3 Monate",
"LAST_6_MONTHS": "Die letzten 6 Monate",
"LAST_YEAR": "Letztes Jahr",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Konto gesperrt",
"MESSAGE": "Ihr Account wurde gesperrt. Wenden Sie sich für weitere Informationen bitte an das Support-Team."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Posteingänge",
"CAPTAIN_SETTINGS": "Einstellungen",
"HOME": "Hauptseite",
"AGENTS": "Agenten",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Σύνδεσμος Webhook",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Ρυθμίσεις",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Τελευταίες 7 ημέρες",
"LAST_14_DAYS": "Τελευταίες 14 ημέρες",
"LAST_30_DAYS": "Τελευταίες 30 ημέρες",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Τελευταίοι 3 μήνες",
"LAST_6_MONTHS": "Τελευταίοι 6 μήνες",
"LAST_YEAR": "Τελευταίο έτος",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Αναστολή Λογαριασμού",
"MESSAGE": "Ο λογαριασμός σας έχει ανασταλεί. Επικοινωνήστε με την ομάδα υποστήριξης για περισσότερες πληροφορίες."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Κιβώτια Εισερχομένων",
"CAPTAIN_SETTINGS": "Ρυθμίσεις",
"HOME": "Αρχική",
"AGENTS": "Πράκτορες",
"AGENT_BOTS": "Bots",
@@ -0,0 +1,32 @@
{
"COMPANIES": {
"HEADER": "Companies",
"SORT_BY": {
"LABEL": "Sort by",
"OPTIONS": {
"NAME": "Name",
"DOMAIN": "Domain",
"CREATED_AT": "Created at"
}
},
"ORDER": {
"LABEL": "Order",
"OPTIONS": {
"ASCENDING": "Ascending",
"DESCENDING": "Descending"
}
},
"SEARCH_PLACEHOLDER": "Search companies...",
"LOADING": "Loading companies...",
"UNNAMED": "Unnamed Company",
"CONTACTS_COUNT": "{n} contact | {n} contacts",
"EMPTY_STATE": {
"TITLE": "No companies found"
}
},
"COMPANIES_LAYOUT": {
"PAGINATION_FOOTER": {
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} companies"
}
}
}
@@ -381,7 +381,11 @@
"API": {
"ERROR_MESSAGE": "We were not able to save the email channel"
},
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
"FINISH_MESSAGE": "Your email inbox has been created successfully! You can start forwarding your emails to the address below, or configure SMTP and IMAP credentials to send and receive emails directly.",
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
"CONFIGURE_SMTP_IMAP_LINK": "Click here",
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
},
"LINE_CHANNEL": {
"TITLE": "LINE Channel",
@@ -707,6 +711,7 @@
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email",
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
@@ -8,6 +8,7 @@ import bulkActions from './bulkActions.json';
import campaign from './campaign.json';
import cannedMgmt from './cannedMgmt.json';
import chatlist from './chatlist.json';
import companies from './companies.json';
import components from './components.json';
import contact from './contact.json';
import contactFilters from './contactFilters.json';
@@ -49,6 +50,7 @@ export default {
...campaign,
...cannedMgmt,
...chatlist,
...companies,
...components,
...contact,
...contactFilters,
@@ -348,7 +348,8 @@
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant"
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
@@ -306,6 +306,8 @@
"SETTINGS": "Settings",
"CONTACTS": "Contacts",
"ACTIVE": "Active",
"COMPANIES": "Companies",
"ALL_COMPANIES": "All Companies",
"CAPTAIN": "Captain",
"CAPTAIN_ASSISTANTS": "Assistants",
"CAPTAIN_DOCUMENTS": "Documents",
@@ -411,6 +413,7 @@
"TITLE": "Security",
"DESCRIPTION": "Manage your account security settings.",
"LINK_TEXT": "Learn more about SAML SSO",
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
"SAML": {
"TITLE": "SAML SSO",
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "URL de Webhook",
"PLACEHOLDER": "Ejemplo: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Capitán",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Prueba estas sugerencias",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Ajustes",
"BASIC_SETTINGS": {
"TITLE": "Configuraciones básicas",
"DESCRIPTION": ""
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Últimos 7 días",
"LAST_14_DAYS": "Últimos 14 días",
"LAST_30_DAYS": "Últimos 30 días",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Últimos 3 meses",
"LAST_6_MONTHS": "Últimos 6 meses",
"LAST_YEAR": "Último año",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Cuenta suspendida",
"MESSAGE": "Tu cuenta está suspendida. Comuníquese con el equipo de soporte para obtener más información."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Cerrar sesión"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documentos",
"CAPTAIN_RESPONSES": "Preguntas frecuentes",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Entradas",
"CAPTAIN_SETTINGS": "Ajustes",
"HOME": "Inicio",
"AGENTS": "Agentes",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "آدرس URL وب هوک",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "تنظیمات",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "در ۷ روز گذشته",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "در ۳۰ روز گذشته",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "۳ ماه گذشته",
"LAST_6_MONTHS": "۶ ماه گذشته",
"LAST_YEAR": "پارسال",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "حساب‌کاربری معلق شده است",
"MESSAGE": "حساب‌کاربری شما به حالت تعلیق درآمده. لطفا برای اطلاعات بیشتر با تیم پشتیبانی تماس بگیرید."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "صندوق‌های ورودی",
"CAPTAIN_SETTINGS": "تنظیمات",
"HOME": "صفحه اصلی",
"AGENTS": "ایجنت ها",
"AGENT_BOTS": "ربات‌ها",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhookin URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Asetukset",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Viimeiset 7 päivää",
"LAST_14_DAYS": "Viimeiset 14 päivää",
"LAST_30_DAYS": "Viimeiset 30 päivää",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Saapuneet-kansiot",
"CAPTAIN_SETTINGS": "Asetukset",
"HOME": "Koti",
"AGENTS": "Edustajat",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Saisie de conversation désactivée"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "URL du Webhook",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Impossible de trouver l'assistant. Veuillez réessayer."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Paramètres",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "7 derniers jours",
"LAST_14_DAYS": "14 derniers jours",
"LAST_30_DAYS": "30 derniers jours",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "3 derniers mois",
"LAST_6_MONTHS": "6 derniers mois",
"LAST_YEAR": "Année dernière",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Compte Suspendu",
"MESSAGE": "Votre compte est suspendu. Veuillez contacter le support pour plus d'informations."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Outils",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Terrain de jeu",
"CAPTAIN_INBOXES": "Boîtes de réception",
"CAPTAIN_SETTINGS": "Paramètres",
"HOME": "Accueil",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -17,10 +17,10 @@
"IP_ADDRESS": "כתובת IP",
"CREATED_AT_LABEL": "נוצר",
"NEW_MESSAGE": "הודעה חדשה",
"CALL": "Call",
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
"CALL": "שיחה",
"CALL_UNDER_DEVELOPMENT": "שיחות עדיין בפיתוח",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
"TITLE": "בחר תא דואר עבור שיחות"
},
"CONVERSATIONS": {
"NO_RECORDS_FOUND": "לא קיימות שיחות קודמות המשויכות לאיש קשר זה.",
@@ -290,7 +290,7 @@
"HEADER": {
"TITLE": "איש קשר",
"SEARCH_TITLE": "Search contacts",
"ACTIVE_TITLE": "Active contacts",
"ACTIVE_TITLE": "אנשי קשר פעילים",
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "הודעה",
"SEND_MESSAGE": "שלח הודעה",
@@ -465,8 +465,8 @@
}
},
"DELETE_CONTACT": {
"MESSAGE": "This action is permanent and irreversible.",
"BUTTON": "Delete now"
"MESSAGE": "הפעולה הזו קבועה ובלתי ניתנת לחזרה.",
"BUTTON": "מחק עכשיו"
}
},
"DETAILS": {
@@ -476,7 +476,7 @@
"DELETE_CONTACT": "מחק איש קשר",
"DELETE_DIALOG": {
"TITLE": "אשר מחיקה",
"DESCRIPTION": "Are you sure you want to delete this contact?",
"DESCRIPTION": "אתה בטוח שאתה רוצה למחוק את איש הקשר הזה?",
"CONFIRM": "כן, מחק",
"API": {
"SUCCESS_MESSAGE": "איש הקשר נמחק בהצלחה",
@@ -554,12 +554,12 @@
"WROTE": "נכתב",
"YOU": "You",
"SAVE": "Save note",
"ADD_NOTE": "Add contact note",
"ADD_NOTE": "הוסף הערה לאיש הקשר",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"COLLAPSE": "צמצום",
"NO_NOTES": "אין הערות, אתה יכול להוסיף הערה מהעמוד של איש הקשר.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above.",
"CONVERSATION_EMPTY_STATE": "There are no notes yet. Use the Add note button to create one."
"CONVERSATION_EMPTY_STATE": "אין הערות כרגע. השתמש בכפתור להוספת הערה."
}
},
"EMPTY_STATE": {
@@ -568,27 +568,27 @@
"BUTTON_LABEL": "Add contact",
"SEARCH_EMPTY_STATE_TITLE": "אין אנשי קשר שתואמים לחיפוש שלך 🔍",
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
"ACTIVE_EMPTY_STATE_TITLE": "אין אנשי קשר פעילים 🌙"
}
},
"CONTACTS_BULK_ACTIONS": {
"ASSIGN_LABELS": "הקצה תוויות",
"ASSIGN_LABELS_SUCCESS": "תוויות שוייכו בהצלחה.",
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
"NO_LABELS_FOUND": "No labels available yet.",
"ASSIGN_LABELS_FAILED": "שיוך תווית נכשל",
"DESCRIPTION": "בחר תווית שאתה רוצה והוסף אותה לאיש קשר.",
"NO_LABELS_FOUND": "אין תוויות כרגע.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"CLEAR_SELECTION": "נקה בחירה",
"SELECT_ALL": "בחר הכל ({count})",
"DELETE_CONTACTS": "מחק",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_SUCCESS": "איש הקשר נמחק בהצלחה.",
"DELETE_FAILED": "מחיקת איש קשר נכשלה.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"TITLE": "מחק אנשי קשר שנבחרו",
"SINGULAR_TITLE": "מחק איש קשר שנבחר",
"DESCRIPTION": "פעולה זו תמחק לצמיתות {count} אנשי קשר שנבחרו. לא ניתן לשחזר פעולה זו.",
"SINGULAR_DESCRIPTION": "פעולה זו תמחק לצמיתות אנשי קשר שנבחרו. לא ניתן לשחזר פעולה זו.",
"CONFIRM_MULTIPLE": "מחק אנשי קשר",
"CONFIRM_SINGLE": "מחק איש קשר"
}
},
@@ -32,17 +32,17 @@
"LOADING_CONVERSATIONS": "טוען שיחות",
"CANNOT_REPLY": "לא ניתן להשיב עקב",
"24_HOURS_WINDOW": "הגבלת חלון הודעות של 24 שעות",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"API_HOURS_WINDOW": "ניתן להשיב לשיחה זו רק תוך {hours} שעות",
"NOT_ASSIGNED_TO_YOU": "השיחה לא שייכת לך, האם תרצה לשייך אותה אליך?",
"ASSIGN_TO_ME": "שייך לעצמך",
"BOT_HANDOFF_MESSAGE": "You are responding to a conversation which is currently handled by an assistant or a bot.",
"BOT_HANDOFF_ACTION": "Mark open and assign to you",
"BOT_HANDOFF_REOPEN_ACTION": "Mark conversation open",
"BOT_HANDOFF_SUCCESS": "Conversation has been handed over to you",
"BOT_HANDOFF_ERROR": "Failed to take over the conversation. Please try again.",
"BOT_HANDOFF_MESSAGE": "אתה מגיב לשיחה שמטופלת כרגע על ידי עוזר או בוט.",
"BOT_HANDOFF_ACTION": "סמן כפתוח והקצה לך",
"BOT_HANDOFF_REOPEN_ACTION": "סמן שיחה כפתוחה",
"BOT_HANDOFF_SUCCESS": "השיחה הועברה אליך",
"BOT_HANDOFF_ERROR": "נכשל בניסיון להשתלט על השיחה. אנא נסה שוב.",
"TWILIO_WHATSAPP_CAN_REPLY": "אתה יכול להשיב לשיחה זו רק באמצעות הודעת תבנית בשל",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "הגבלת חלון הודעות של 24 שעות",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "חשבון אינסטגרם זה הועבר לתיבת הדואר החדשה של ערוץ אינסטגרם. כל ההודעות החדשות יופיעו שם. לא תוכל לשלוח הודעות משיחה זו יותר.",
"REPLYING_TO": "אתה משיב ל:",
"REMOVE_SELECTION": "הסר בחירה",
"DOWNLOAD": "הורד",
@@ -72,21 +72,21 @@
"HIDE_LABELS": "הסתר תוויות"
},
"VOICE_CALL": {
"INCOMING_CALL": "Incoming call",
"OUTGOING_CALL": "Outgoing call",
"CALL_IN_PROGRESS": "Call in progress",
"NO_ANSWER": "No answer",
"MISSED_CALL": "Missed call",
"CALL_ENDED": "Call ended",
"NOT_ANSWERED_YET": "Not answered yet",
"THEY_ANSWERED": "They answered",
"YOU_ANSWERED": "You answered"
"INCOMING_CALL": "שיחה נכנסת",
"OUTGOING_CALL": "שיחה יוצאת",
"CALL_IN_PROGRESS": "שיחה מתקיימת",
"NO_ANSWER": "אין מענה",
"MISSED_CALL": "שיחה שלא נענתה",
"CALL_ENDED": "השיחה הסתיימה",
"NOT_ANSWERED_YET": "לא ענה כרגע",
"THEY_ANSWERED": "ענה",
"YOU_ANSWERED": "אתה ענית"
},
"HEADER": {
"RESOLVE_ACTION": "פתרון",
"REOPEN_ACTION": "פתח מחדש",
"OPEN_ACTION": "פתח",
"MORE_ACTIONS": "More actions",
"MORE_ACTIONS": "עוד פעולות",
"OPEN": "עוד",
"CLOSE": "סגור",
"DETAILS": "פרטים",
@@ -139,8 +139,8 @@
}
},
"DELETE_CONVERSATION": {
"TITLE": "Delete conversation #{conversationId}",
"DESCRIPTION": "Are you sure you want to delete this conversation?",
"TITLE": "מחק שיחה #{conversationId}",
"DESCRIPTION": "אתה בטוח שאתה רוצה למחוק את השיחה הזו?",
"CONFIRM": "מחק"
},
"CARD_CONTEXT_MENU": {
@@ -159,10 +159,10 @@
"ASSIGN_LABEL": "ההקצה תווית",
"AGENTS_LOADING": "טוען סוכנים...",
"ASSIGN_TEAM": "שייך צוות",
"DELETE": "Delete conversation",
"OPEN_IN_NEW_TAB": "Open in new tab",
"COPY_LINK": "Copy conversation link",
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
"DELETE": "מחק שיחה",
"OPEN_IN_NEW_TAB": "פתח בכרטיסיה חדשה",
"COPY_LINK": "העתק קישור לשיחה",
"COPY_LINK_SUCCESS": "קישור לשיחה הועתק ללוח הגזירים",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "מזהה שיחה {conversationId} קושר ל {agentName}",
@@ -229,11 +229,11 @@
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
"ENABLE_TOOLTIP": "כלול שרשור אימייל מצוטט",
"DISABLE_TOOLTIP": "אל תכלול שרשור אימייל מצוטט",
"REMOVE_PREVIEW": "הסר שרשור אימייל מצוטט",
"COLLAPSE": "כווץ תצוגה מקדימה",
"EXPAND": "הרחב תצוגה מקדימה"
}
},
"VISIBLE_TO_AGENTS": "פתקים פרטיים: רק אתה והצוות שלך יכולים לראות",
@@ -244,8 +244,8 @@
"ASSIGN_LABEL_SUCCESFUL": "סמן משימה כבוצעה בהצלחה",
"ASSIGN_LABEL_FAILED": "סמן משימה כנכשלה",
"CHANGE_TEAM": "שיחת קבוצה השתנתה",
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
"SUCCESS_DELETE_CONVERSATION": "השיחה נמחקה בהצלחה",
"FAIL_DELETE_CONVERSATION": "לא ניתן למחוק את השיחה! נסה שוב",
"FILE_SIZE_LIMIT": "הקובץ גדול מ{MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE}MB מגבלת העלאה",
"MESSAGE_ERROR": "לא ניתן לשלוח הודעה, אנא נסה שוב מאוחר יותר",
"SENT_BY": "נשלח על ידי:",
@@ -334,11 +334,11 @@
"CONVERSATION_ACTIONS": "פעולות בשיחה",
"CONVERSATION_LABELS": "תוויות שיחה",
"CONVERSATION_INFO": "מידע על שיחה",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_NOTES": "הערות איש קשר",
"CONTACT_ATTRIBUTES": "תכונות יצירת קשר",
"PREVIOUS_CONVERSATION": "שיחות קודמות",
"MACROS": "מאקרו",
"LINEAR_ISSUES": "Linked Linear Issues",
"LINEAR_ISSUES": "בעיות ליניאריות מקושרות",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
@@ -1,7 +1,7 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"CONVERSATION": "חרגת מהמגבלה של השיחות. תוכנית Hacker מאפשרת רק 500 שיחות.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Your plan only allows {allowedAgents} agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
@@ -16,22 +16,22 @@
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"NOTE": "לאחר מחיקת החשבון, כל הנתונים שלך יימחקו.",
"BUTTON_TEXT": "מחק את חשבונך",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"TITLE": "מחק חשבון",
"MESSAGE": "מחיקת החשבון איננה הפיכה. הזן את שם החשבון מטה כדי לאשר שאתה רוצה למחוק אותו.",
"BUTTON_TEXT": "מחק",
"DISMISS": "ביטול",
"PLACE_HOLDER": "אנא הקלד {accountName} כדי לאשר"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SUCCESS": "חשבון סומן למחיקה",
"FAILURE": "לא הצלחתי למחוק את החשבון, נסה שוב!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": ".",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
"TITLE": "החשבון תוזמן למחיקה",
"MESSAGE_MANUAL": "חשבון זה מתוכנן למחיקה בתאריך {deletionDate}. הבקשה הוגשה על ידי מנהל. ניתן לבטל את המחיקה לפני תאריך זה.",
"MESSAGE_INACTIVITY": "החשבון הזה תוסמן למחיקה ב{deletionDate} בגלל שהחשבון לא היה בשימוש. אתה יכול לבטל זאת לפני התאריך.",
"CLEAR_BUTTON": "בטל מחיקה מתוכננת"
}
},
"FORM": {
@@ -45,10 +45,10 @@
"NOTE": "מזהה זה נדרש אם אתה בונה אינטגרציה מבוססת API"
},
"AUTO_RESOLVE": {
"TITLE": "Auto-resolve conversations",
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period of inactivity.",
"TITLE": "פתרון אוטומטי של שיחות",
"NOTE": "תצורה זו מאפשרת לך לפתור אוטומטית את השיחה לאחר תקופת אי פעילות מסוימת.",
"DURATION": {
"LABEL": "Inactivity duration",
"LABEL": "תקופת זמן של אי פעילות שלאחריה השיחה נפתרת אוטומטית",
"HELP": "Time period of inactivity after which conversation is auto-resolved",
"PLACEHOLDER": "30",
"ERROR": "Auto resolve duration should be between 10 minutes and 999 days",
@@ -58,11 +58,11 @@
}
},
"MESSAGE": {
"LABEL": "Custom auto-resolution message",
"PLACEHOLDER": "Conversation was marked resolved by system due to 15 days of inactivity",
"HELP": "Message sent to the customer after conversation is auto-resolved"
"LABEL": "הודעת פתרון אוטומטית מותאמת אישית",
"PLACEHOLDER": "השיחה סומנה כפתורה על ידי המערכת עקב 15 ימים של אי פעילות",
"HELP": "הודעה שנשלחת ללקוח לאחר שהשיחה נפתרת אוטומטית"
},
"PREFERENCES": "Preferences",
"PREFERENCES": "העדפות",
"LABEL": {
"LABEL": "Add label after auto-resolution",
"PLACEHOLDER": "Select a label"
@@ -115,7 +115,7 @@
},
"UPDATE_BUTTON": "עדכן",
"MESSAGE_LABEL": "Custom resolution message",
"MESSAGE_PLACEHOLDER": "Conversation was marked resolved by system due to 15 days of inactivity",
"MESSAGE_PLACEHOLDER": "השיחה סומנה כפתורה על ידי המערכת עקב 15 ימים של אי פעילות",
"MESSAGE_HELP": "This message is sent to the customer when a conversation is automatically resolved by the system due to inactivity."
},
"FEATURES": {
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "כתובת אתר של Webhook",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "טייס משנה",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "הגדרות",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "7 הימים האחרונים",
"LAST_14_DAYS": "14 הימים האחרונים",
"LAST_30_DAYS": "30 הימים האחרונים",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "3 החודשים האחרונים",
"LAST_6_MONTHS": "6 החודשים האחרונים",
"LAST_YEAR": "שנה שעברה",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "חשבון מושעה",
"MESSAGE": "החשבון שלך מושעה. אנא פנה לצוות התמיכה לקבלת מידע נוסף."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "תיבות דואר נכנס",
"CAPTAIN_SETTINGS": "הגדרות",
"HOME": "בית",
"AGENTS": "סוכנים",
"AGENT_BOTS": "בוטים",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -344,6 +348,12 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant",
"EMPTY_LIST": "No assistants found, please create one to get started"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -483,9 +493,7 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"HEADER": "Settings",
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,15 +510,16 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -529,9 +538,6 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -577,9 +583,6 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistants replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -625,9 +628,6 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -725,11 +725,6 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -923,11 +918,6 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -251,6 +251,12 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -305,6 +311,10 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agenti",
"AGENT_BOTS": "Botovi",

Some files were not shown because too many files have changed in this diff Show More