Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a27c3fcbf | ||
|
|
3197fafad1 | ||
|
|
c5cc972a74 | ||
|
|
d0a7de0b00 | ||
|
|
a7aae69229 | ||
|
|
12957b7438 | ||
|
|
363169667d | ||
|
|
236b7d4541 | ||
|
|
6d101f5ba3 | ||
|
|
c88fb147f8 | ||
|
|
ebdca08da7 | ||
|
|
8150604a54 |
@@ -44,12 +44,6 @@ force_run:
|
|||||||
rm -f tmp/pids/*.pid
|
rm -f tmp/pids/*.pid
|
||||||
overmind start -f Procfile.dev
|
overmind start -f Procfile.dev
|
||||||
|
|
||||||
force_run_tunnel:
|
|
||||||
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
|
|
||||||
rm -f ./.overmind.sock
|
|
||||||
rm -f tmp/pids/*.pid
|
|
||||||
overmind start -f Procfile.tunnel
|
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
overmind connect backend
|
overmind connect backend
|
||||||
|
|
||||||
@@ -59,4 +53,4 @@ debug_worker:
|
|||||||
docker:
|
docker:
|
||||||
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
|
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
|
||||||
|
|
||||||
.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run force_run_tunnel debug debug_worker
|
.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run debug debug_worker
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
backend: DISABLE_MINI_PROFILER=true bin/rails s -p 3000
|
|
||||||
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
|
||||||
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
|
||||||
vite: bin/vite build --watch
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|
||||||
attr_reader :account, :params
|
|
||||||
|
|
||||||
# rubocop:disable Lint/MissingSuper
|
|
||||||
# the parent class has no initialize
|
|
||||||
def initialize(account:, params:)
|
|
||||||
@account = account
|
|
||||||
@params = params
|
|
||||||
|
|
||||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
|
||||||
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
|
||||||
end
|
|
||||||
# rubocop:enable Lint/MissingSuper
|
|
||||||
|
|
||||||
def build
|
|
||||||
labels = account.labels.to_a
|
|
||||||
return [] if labels.empty?
|
|
||||||
|
|
||||||
report_data = collect_report_data
|
|
||||||
labels.map { |label| build_label_report(label, report_data) }
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def collect_report_data
|
|
||||||
conversation_filter = build_conversation_filter
|
|
||||||
use_business_hours = use_business_hours?
|
|
||||||
|
|
||||||
{
|
|
||||||
conversation_counts: fetch_conversation_counts(conversation_filter),
|
|
||||||
resolved_counts: fetch_resolved_counts(conversation_filter),
|
|
||||||
resolution_metrics: fetch_metrics(conversation_filter, 'conversation_resolved', use_business_hours),
|
|
||||||
first_response_metrics: fetch_metrics(conversation_filter, 'first_response', use_business_hours),
|
|
||||||
reply_metrics: fetch_metrics(conversation_filter, 'reply', use_business_hours)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_label_report(label, report_data)
|
|
||||||
{
|
|
||||||
id: label.id,
|
|
||||||
name: label.title,
|
|
||||||
conversations_count: report_data[:conversation_counts][label.title] || 0,
|
|
||||||
avg_resolution_time: report_data[:resolution_metrics][label.title] || 0,
|
|
||||||
avg_first_response_time: report_data[:first_response_metrics][label.title] || 0,
|
|
||||||
avg_reply_time: report_data[:reply_metrics][label.title] || 0,
|
|
||||||
resolved_conversations_count: report_data[:resolved_counts][label.title] || 0
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def use_business_hours?
|
|
||||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_conversation_filter
|
|
||||||
conversation_filter = { account_id: account.id }
|
|
||||||
conversation_filter[:created_at] = range if range.present?
|
|
||||||
|
|
||||||
conversation_filter
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_conversation_counts(conversation_filter)
|
|
||||||
fetch_counts(conversation_filter)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_resolved_counts(conversation_filter)
|
|
||||||
fetch_counts(conversation_filter.merge(status: :resolved))
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_counts(conversation_filter)
|
|
||||||
ActsAsTaggableOn::Tagging
|
|
||||||
.joins('INNER JOIN conversations ON taggings.taggable_id = conversations.id')
|
|
||||||
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
|
|
||||||
.where(
|
|
||||||
taggable_type: 'Conversation',
|
|
||||||
context: 'labels',
|
|
||||||
conversations: conversation_filter
|
|
||||||
)
|
|
||||||
.select('tags.name, COUNT(taggings.*) AS count')
|
|
||||||
.group('tags.name')
|
|
||||||
.each_with_object({}) { |record, hash| hash[record.name] = record.count }
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_metrics(conversation_filter, event_name, use_business_hours)
|
|
||||||
ReportingEvent
|
|
||||||
.joins('INNER JOIN conversations ON reporting_events.conversation_id = conversations.id')
|
|
||||||
.joins('INNER JOIN taggings ON taggings.taggable_id = conversations.id')
|
|
||||||
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
|
|
||||||
.where(
|
|
||||||
conversations: conversation_filter,
|
|
||||||
name: event_name,
|
|
||||||
taggings: { taggable_type: 'Conversation', context: 'labels' }
|
|
||||||
)
|
|
||||||
.group('tags.name')
|
|
||||||
.order('tags.name')
|
|
||||||
.select(
|
|
||||||
'tags.name',
|
|
||||||
use_business_hours ? 'AVG(reporting_events.value_in_business_hours) as avg_value' : 'AVG(reporting_events.value) as avg_value'
|
|
||||||
)
|
|
||||||
.each_with_object({}) { |record, hash| hash[record.name] = record.avg_value.to_f }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -14,7 +14,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index, :active, :search, :filter]
|
before_action :set_current_page, only: [:index, :active, :search, :filter]
|
||||||
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
|
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
|
||||||
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
|
before_action :set_include_contact_inboxes, only: [:index, :search, :filter, :show, :update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@@ -56,7 +56,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||||
.get_available_contact_ids(Current.account.id))
|
.get_available_contact_ids(Current.account.id))
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
@contacts = fetch_contacts(contacts)
|
@contacts = contacts.page(@current_page)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|||||||
@@ -124,12 +124,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
@conversation.save!
|
@conversation.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
|
||||||
authorize @conversation, :destroy?
|
|
||||||
::DeleteObjectJob.perform_later(@conversation, Current.user, request.ip)
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def permitted_update_params
|
def permitted_update_params
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||||
include GoogleConcern
|
include GoogleConcern
|
||||||
before_action :check_authorization
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
email = params[:authorization][:email]
|
|
||||||
redirect_url = google_client.auth_code.authorize_url(
|
redirect_url = google_client.auth_code.authorize_url(
|
||||||
{
|
{
|
||||||
redirect_uri: "#{base_url}/google/callback",
|
redirect_uri: "#{base_url}/google/callback",
|
||||||
scope: 'email profile https://mail.google.com/',
|
scope: scope,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
|
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
|
||||||
access_type: 'offline', # the default is 'online'
|
access_type: 'offline', # the default is 'online'
|
||||||
|
state: state,
|
||||||
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if redirect_url
|
if redirect_url
|
||||||
cache_key = "google::#{email.downcase}"
|
|
||||||
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
|
|
||||||
render json: { success: true, url: redirect_url }
|
render json: { success: true, url: redirect_url }
|
||||||
else
|
else
|
||||||
render json: { success: false }, status: :unprocessable_entity
|
render json: { success: false }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||||
include InstagramConcern
|
include InstagramConcern
|
||||||
include Instagram::IntegrationHelper
|
include Instagram::IntegrationHelper
|
||||||
before_action :check_authorization
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization
|
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization
|
||||||
@@ -21,10 +20,4 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts
|
|||||||
render json: { success: false }, status: :unprocessable_entity
|
render json: { success: false }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
class Api::V1::Accounts::Integrations::GithubController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :check_authorization
|
|
||||||
|
|
||||||
def show
|
|
||||||
@github_integration = Current.account.hooks.find_by(app_id: 'github')
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
authorize ::Webhook, :show?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,28 +1,19 @@
|
|||||||
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||||
include MicrosoftConcern
|
include MicrosoftConcern
|
||||||
before_action :check_authorization
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
email = params[:authorization][:email]
|
|
||||||
redirect_url = microsoft_client.auth_code.authorize_url(
|
redirect_url = microsoft_client.auth_code.authorize_url(
|
||||||
{
|
{
|
||||||
redirect_uri: "#{base_url}/microsoft/callback",
|
redirect_uri: "#{base_url}/microsoft/callback",
|
||||||
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile',
|
scope: scope,
|
||||||
|
state: state,
|
||||||
prompt: 'consent'
|
prompt: 'consent'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if redirect_url
|
if redirect_url
|
||||||
cache_key = "microsoft::#{email.downcase}"
|
|
||||||
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
|
|
||||||
render json: { success: true, url: redirect_url }
|
render json: { success: true, url: redirect_url }
|
||||||
else
|
else
|
||||||
render json: { success: false }, status: :unprocessable_entity
|
render json: { success: false }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :check_authorization
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def scope
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def state
|
||||||
|
Current.account.to_sgid(expires_in: 15.minutes).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def base_url
|
||||||
|
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -92,7 +92,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def settings_params
|
def settings_params
|
||||||
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
|
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_signup_enabled
|
def check_signup_enabled
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label]
|
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
|
||||||
|
|
||||||
def agent
|
def agent
|
||||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||||
@@ -14,10 +14,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
|||||||
render_report_with(V2::Reports::InboxSummaryBuilder)
|
render_report_with(V2::Reports::InboxSummaryBuilder)
|
||||||
end
|
end
|
||||||
|
|
||||||
def label
|
|
||||||
render_report_with(V2::Reports::LabelSummaryBuilder)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module GoogleConcern
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def base_url
|
def scope
|
||||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://mail.google.com/'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module MicrosoftConcern
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def base_url
|
def scope
|
||||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class DashboardController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def ensure_html_format
|
def ensure_html_format
|
||||||
render json: { error: 'Please use API routes instead of dashboard routes for JSON requests' }, status: :not_acceptable if request.format.json?
|
head :not_acceptable unless request.format.html?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_global_config
|
def set_global_config
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
class Github::CallbacksController < ApplicationController
|
|
||||||
include Github::IntegrationHelper
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
|
||||||
|
|
||||||
def show
|
|
||||||
if params[:error].present?
|
|
||||||
redirect_to app_account_integrations_path(account_id: @account.id), alert: params[:error_description]
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:code].present?
|
|
||||||
response = exchange_code_for_token(params[:code])
|
|
||||||
|
|
||||||
if response[:access_token].present?
|
|
||||||
hook = @account.hooks.find_or_initialize_by(app_id: 'github')
|
|
||||||
hook.access_token = response[:access_token]
|
|
||||||
hook.status = 'enabled'
|
|
||||||
|
|
||||||
if hook.save
|
|
||||||
redirect_to app_account_integrations_path(account_id: @account.id), notice: 'GitHub integration connected successfully!'
|
|
||||||
else
|
|
||||||
redirect_to app_account_integrations_path(account_id: @account.id), alert: 'Failed to save GitHub integration'
|
|
||||||
end
|
|
||||||
else
|
|
||||||
redirect_to app_account_integrations_path(account_id: @account.id), alert: 'Failed to get access token from GitHub'
|
|
||||||
end
|
|
||||||
else
|
|
||||||
redirect_to app_account_integrations_path(account_id: @account.id), alert: 'GitHub authorization failed'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_account
|
|
||||||
@account = Current.user.accounts.find(params[:account_id]) if params[:account_id].present?
|
|
||||||
@account ||= Current.user.accounts.first
|
|
||||||
|
|
||||||
return if @account
|
|
||||||
|
|
||||||
redirect_to root_path, alert: 'Account not found'
|
|
||||||
end
|
|
||||||
|
|
||||||
def exchange_code_for_token(code)
|
|
||||||
client_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil)
|
|
||||||
client_secret = GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil)
|
|
||||||
|
|
||||||
return {} unless client_id && client_secret
|
|
||||||
|
|
||||||
response = HTTParty.post('https://github.com/login/oauth/access_token', {
|
|
||||||
body: {
|
|
||||||
client_id: client_id,
|
|
||||||
client_secret: client_secret,
|
|
||||||
code: code
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'Accept' => 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.success?
|
|
||||||
JSON.parse(response.body).with_indifferent_access
|
|
||||||
else
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "GitHub OAuth error: #{e.message}"
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -12,6 +12,16 @@ class Google::CallbacksController < OauthCallbackController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def verify_scopes
|
||||||
|
granted_scopes = parsed_body['scope']&.split || []
|
||||||
|
required_scopes = scope.split
|
||||||
|
|
||||||
|
missing_scopes = required_scopes - granted_scopes
|
||||||
|
return if missing_scopes.empty?
|
||||||
|
|
||||||
|
raise StandardError, I18n.t('errors.oauth.insufficient_scopes', scopes: missing_scopes.join(', '))
|
||||||
|
end
|
||||||
|
|
||||||
def provider_name
|
def provider_name
|
||||||
'google'
|
'google'
|
||||||
end
|
end
|
||||||
@@ -24,4 +34,13 @@ class Google::CallbacksController < OauthCallbackController
|
|||||||
# from GoogleConcern
|
# from GoogleConcern
|
||||||
google_client
|
google_client
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_error(exception)
|
||||||
|
ChatwootExceptionTracker.new(exception).capture_exception
|
||||||
|
|
||||||
|
error_message = exception.message || I18n.t('errors.oauth.authorization_failed')
|
||||||
|
redirect_url = "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/app/accounts/#{account.id}/settings/inboxes/new/email"
|
||||||
|
|
||||||
|
redirect_to "#{redirect_url}?error=#{CGI.escape(error_message)}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ class OauthCallbackController < ApplicationController
|
|||||||
redirect_uri: "#{base_url}/#{provider_name}/callback"
|
redirect_uri: "#{base_url}/#{provider_name}/callback"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
verify_scopes
|
||||||
handle_response
|
handle_response
|
||||||
::Redis::Alfred.delete(cache_key)
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
handle_error(e)
|
||||||
redirect_to '/'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -64,14 +63,22 @@ class OauthCallbackController < ApplicationController
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_key
|
def verify_scopes
|
||||||
"#{provider_name}::#{users_data['email'].downcase}"
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_redirect_url
|
||||||
|
'/'
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_error(exception)
|
||||||
|
ChatwootExceptionTracker.new(exception).capture_exception
|
||||||
|
redirect_to failure_redirect_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_channel_with_inbox
|
def create_channel_with_inbox
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||||
|
|
||||||
account.inboxes.create!(
|
account.inboxes.create!(
|
||||||
account: account,
|
account: account,
|
||||||
channel: channel_email,
|
channel: channel_email,
|
||||||
@@ -86,12 +93,14 @@ class OauthCallbackController < ApplicationController
|
|||||||
decoded_token[0]
|
decoded_token[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_id
|
def account_from_signed_id
|
||||||
::Redis::Alfred.get(cache_key)
|
raise 'Missing state variable' if params[:state].blank?
|
||||||
|
|
||||||
|
GlobalID::Locator.locate_signed(params[:state])
|
||||||
end
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
@account ||= Account.find(account_id)
|
@account ||= account_from_signed_id
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fallback name, for when name field is missing from users_data
|
# Fallback name, for when name field is missing from users_data
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ class SuperAdmin::AccountUsersController < SuperAdmin::ApplicationController
|
|||||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||||
# For example, you may want to send an email after a foo is updated.
|
# For example, you may want to send an email after a foo is updated.
|
||||||
#
|
#
|
||||||
|
|
||||||
# Since account/user page - account user role attribute links to the show page
|
|
||||||
# Handle with a redirect to the user show page
|
|
||||||
def show
|
|
||||||
redirect_to super_admin_user_path(requested_resource.user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
resource = resource_class.new(resource_params)
|
resource = resource_class.new(resource_params)
|
||||||
authorize_resource(resource)
|
authorize_resource(resource)
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
|||||||
'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'],
|
'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'],
|
||||||
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
|
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
|
||||||
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
|
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
|
||||||
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
|
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT]
|
||||||
'github' => %w[GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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])
|
||||||
|
|||||||
@@ -36,13 +36,9 @@ module Api::V2::Accounts::ReportsHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generate_labels_report
|
def generate_labels_report
|
||||||
reports = V2::Reports::LabelSummaryBuilder.new(
|
Current.account.labels.map do |label|
|
||||||
account: Current.account,
|
label_report = report_builder({ type: :label, id: label.id }).short_summary
|
||||||
params: build_params({})
|
[label.title] + generate_readable_report_metrics(label_report)
|
||||||
).build
|
|
||||||
|
|
||||||
reports.map do |report|
|
|
||||||
[report[:name]] + generate_readable_report_metrics(report)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
module Github::IntegrationHelper
|
|
||||||
def github_integration_url(account_id)
|
|
||||||
"#{ENV.fetch('FRONTEND_URL', nil)}/github/callback?account_id=#{account_id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def github_oauth_url(account_id)
|
|
||||||
client_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil)
|
|
||||||
return nil unless client_id
|
|
||||||
|
|
||||||
params = {
|
|
||||||
client_id: client_id,
|
|
||||||
redirect_uri: github_integration_url(account_id),
|
|
||||||
scope: 'repo',
|
|
||||||
state: generate_state_token(account_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
"https://github.com/login/oauth/authorize?#{params.to_query}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def github_configured?
|
|
||||||
GlobalConfigService.load('GITHUB_CLIENT_ID', nil).present? &&
|
|
||||||
GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil).present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def github_integration_enabled?(account)
|
|
||||||
return false unless github_configured?
|
|
||||||
|
|
||||||
account.hooks.exists?(app_id: 'github', status: 'enabled')
|
|
||||||
end
|
|
||||||
|
|
||||||
def github_repositories(access_token)
|
|
||||||
return [] unless access_token
|
|
||||||
|
|
||||||
response = HTTParty.get('https://api.github.com/user/repos', {
|
|
||||||
headers: {
|
|
||||||
'Authorization' => "token #{access_token}",
|
|
||||||
'Accept' => 'application/vnd.github.v3+json'
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
per_page: 100,
|
|
||||||
sort: 'updated'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.success?
|
|
||||||
JSON.parse(response.body)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "GitHub API error: #{e.message}"
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def generate_state_token(account_id)
|
|
||||||
payload = {
|
|
||||||
account_id: account_id,
|
|
||||||
timestamp: Time.current.to_i
|
|
||||||
}
|
|
||||||
|
|
||||||
JWT.encode(payload, Rails.application.secret_key_base, 'HS256')
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_state_token(state)
|
|
||||||
JWT.decode(state, Rails.application.secret_key_base, true, { algorithm: 'HS256' })
|
|
||||||
rescue JWT::DecodeError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -38,7 +38,13 @@ export default {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
profileUpdate({ displayName, avatar, ...profileAttributes }) {
|
profileUpdate({
|
||||||
|
password,
|
||||||
|
password_confirmation,
|
||||||
|
displayName,
|
||||||
|
avatar,
|
||||||
|
...profileAttributes
|
||||||
|
}) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
Object.keys(profileAttributes).forEach(key => {
|
Object.keys(profileAttributes).forEach(key => {
|
||||||
const hasValue = profileAttributes[key] === undefined;
|
const hasValue = profileAttributes[key] === undefined;
|
||||||
@@ -47,22 +53,16 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
formData.append('profile[display_name]', displayName || '');
|
formData.append('profile[display_name]', displayName || '');
|
||||||
|
if (password && password_confirmation) {
|
||||||
|
formData.append('profile[password]', password);
|
||||||
|
formData.append('profile[password_confirmation]', password_confirmation);
|
||||||
|
}
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
formData.append('profile[avatar]', avatar);
|
formData.append('profile[avatar]', avatar);
|
||||||
}
|
}
|
||||||
return axios.put(endPoints('profileUpdate').url, formData);
|
return axios.put(endPoints('profileUpdate').url, formData);
|
||||||
},
|
},
|
||||||
|
|
||||||
profilePasswordUpdate({ currentPassword, password, passwordConfirmation }) {
|
|
||||||
return axios.put(endPoints('profileUpdate').url, {
|
|
||||||
profile: {
|
|
||||||
current_password: currentPassword,
|
|
||||||
password,
|
|
||||||
password_confirmation: passwordConfirmation,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUISettings({ uiSettings }) {
|
updateUISettings({ uiSettings }) {
|
||||||
return axios.put(endPoints('profileUpdate').url, {
|
return axios.put(endPoints('profileUpdate').url, {
|
||||||
profile: { ui_settings: uiSettings },
|
profile: { ui_settings: uiSettings },
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ class ContactAPI extends ApiClient {
|
|||||||
return axios.get(requestURL);
|
return axios.get(requestURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
active(page = 1, sortAttr = 'name') {
|
|
||||||
let requestURL = `${this.url}/active?${buildContactParams(page, sortAttr)}`;
|
|
||||||
return axios.get(requestURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line default-param-last
|
// eslint-disable-next-line default-param-last
|
||||||
filter(page = 1, sortAttr = 'name', queryPayload) {
|
filter(page = 1, sortAttr = 'name', queryPayload) {
|
||||||
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ const endPoints = {
|
|||||||
resendConfirmation: {
|
resendConfirmation: {
|
||||||
url: '/api/v1/profile/resend_confirmation',
|
url: '/api/v1/profile/resend_confirmation',
|
||||||
},
|
},
|
||||||
|
|
||||||
resetAccessToken: {
|
resetAccessToken: {
|
||||||
url: '/api/v1/profile/reset_access_token',
|
url: '/api/v1/profile/reset_access_token',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,12 +134,12 @@ class ConversationApi extends ApiClient {
|
|||||||
return axios.get(`${this.url}/${conversationId}/attachments`);
|
return axios.get(`${this.url}/${conversationId}/attachments`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInboxAssistant(conversationId) {
|
requestCopilot(conversationId, body) {
|
||||||
return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
|
return axios.post(`${this.url}/${conversationId}/copilot`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(conversationId) {
|
getInboxAssistant(conversationId) {
|
||||||
return axios.delete(`${this.url}/${conversationId}`);
|
return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,16 +35,6 @@ class SummaryReportsAPI extends ApiClient {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelReports({ since, until, businessHours } = {}) {
|
|
||||||
return axios.get(`${this.url}/label`, {
|
|
||||||
params: {
|
|
||||||
since,
|
|
||||||
until,
|
|
||||||
business_hours: businessHours,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new SummaryReportsAPI();
|
export default new SummaryReportsAPI();
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const props = defineProps({
|
|||||||
additionalAttributes: { type: Object, default: () => ({}) },
|
additionalAttributes: { type: Object, default: () => ({}) },
|
||||||
phoneNumber: { type: String, default: '' },
|
phoneNumber: { type: String, default: '' },
|
||||||
thumbnail: { type: String, default: '' },
|
thumbnail: { type: String, default: '' },
|
||||||
availabilityStatus: { type: String, default: null },
|
|
||||||
isExpanded: { type: Boolean, default: false },
|
isExpanded: { type: Boolean, default: false },
|
||||||
isUpdating: { type: Boolean, default: false },
|
isUpdating: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
@@ -93,13 +92,7 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
|||||||
<template>
|
<template>
|
||||||
<CardLayout :key="id" layout="row">
|
<CardLayout :key="id" layout="row">
|
||||||
<div class="flex items-center justify-start flex-1 gap-4">
|
<div class="flex items-center justify-start flex-1 gap-4">
|
||||||
<Avatar
|
<Avatar :name="name" :src="thumbnail" :size="48" rounded-full />
|
||||||
:name="name"
|
|
||||||
:src="thumbnail"
|
|
||||||
:size="48"
|
|
||||||
:status="availabilityStatus"
|
|
||||||
rounded-full
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col gap-0.5 flex-1">
|
<div class="flex flex-col gap-0.5 flex-1">
|
||||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||||
<span class="text-base font-medium truncate text-n-slate-12">
|
<span class="text-base font-medium truncate text-n-slate-12">
|
||||||
|
|||||||
+39
-18
@@ -7,16 +7,42 @@ import ContactMoreActions from './components/ContactMoreActions.vue';
|
|||||||
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
showSearch: { type: Boolean, default: true },
|
showSearch: {
|
||||||
searchValue: { type: String, default: '' },
|
type: Boolean,
|
||||||
headerTitle: { type: String, required: true },
|
default: true,
|
||||||
buttonLabel: { type: String, default: '' },
|
},
|
||||||
activeSort: { type: String, default: 'last_activity_at' },
|
searchValue: {
|
||||||
activeOrdering: { type: String, default: '' },
|
type: String,
|
||||||
isSegmentsView: { type: Boolean, default: false },
|
default: '',
|
||||||
hasActiveFilters: { type: Boolean, default: false },
|
},
|
||||||
isLabelView: { type: Boolean, default: false },
|
headerTitle: {
|
||||||
isActiveView: { type: Boolean, default: false },
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
activeSort: {
|
||||||
|
type: String,
|
||||||
|
default: 'last_activity_at',
|
||||||
|
},
|
||||||
|
activeOrdering: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isSegmentsView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hasActiveFilters: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLabelView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -59,7 +85,7 @@ const emit = defineEmits([
|
|||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="!isLabelView && !isActiveView" class="relative">
|
<div v-if="!isLabelView" class="relative">
|
||||||
<Button
|
<Button
|
||||||
id="toggleContactsFilterButton"
|
id="toggleContactsFilterButton"
|
||||||
:icon="
|
:icon="
|
||||||
@@ -79,12 +105,7 @@ const emit = defineEmits([
|
|||||||
<slot name="filter" />
|
<slot name="filter" />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-if="
|
v-if="hasActiveFilters && !isSegmentsView && !isLabelView"
|
||||||
hasActiveFilters &&
|
|
||||||
!isSegmentsView &&
|
|
||||||
!isLabelView &&
|
|
||||||
!isActiveView
|
|
||||||
"
|
|
||||||
icon="i-lucide-save"
|
icon="i-lucide-save"
|
||||||
color="slate"
|
color="slate"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -92,7 +113,7 @@ const emit = defineEmits([
|
|||||||
@click="emit('createSegment')"
|
@click="emit('createSegment')"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="isSegmentsView && !isLabelView && !isActiveView"
|
v-if="isSegmentsView && !isLabelView"
|
||||||
icon="i-lucide-trash"
|
icon="i-lucide-trash"
|
||||||
color="slate"
|
color="slate"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
-2
@@ -36,7 +36,6 @@ const props = defineProps({
|
|||||||
activeSegment: { type: Object, default: null },
|
activeSegment: { type: Object, default: null },
|
||||||
hasAppliedFilters: { type: Boolean, default: false },
|
hasAppliedFilters: { type: Boolean, default: false },
|
||||||
isLabelView: { type: Boolean, default: false },
|
isLabelView: { type: Boolean, default: false },
|
||||||
isActiveView: { type: Boolean, default: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -278,7 +277,6 @@ defineExpose({
|
|||||||
:header-title="headerTitle"
|
:header-title="headerTitle"
|
||||||
:is-segments-view="hasActiveSegments"
|
:is-segments-view="hasActiveSegments"
|
||||||
:is-label-view="isLabelView"
|
:is-label-view="isLabelView"
|
||||||
:is-active-view="isActiveView"
|
|
||||||
:has-active-filters="hasAppliedFilters"
|
:has-active-filters="hasAppliedFilters"
|
||||||
:button-label="t('CONTACTS_LAYOUT.HEADER.MESSAGE_BUTTON')"
|
:button-label="t('CONTACTS_LAYOUT.HEADER.MESSAGE_BUTTON')"
|
||||||
@search="emit('search', $event)"
|
@search="emit('search', $event)"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ContactListHeaderWrapper from 'dashboard/components-next/Contacts/Contact
|
|||||||
import ContactsActiveFiltersPreview from 'dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue';
|
import ContactsActiveFiltersPreview from 'dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue';
|
||||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
searchValue: { type: String, default: '' },
|
searchValue: { type: String, default: '' },
|
||||||
headerTitle: { type: String, default: '' },
|
headerTitle: { type: String, default: '' },
|
||||||
showPaginationFooter: { type: Boolean, default: true },
|
showPaginationFooter: { type: Boolean, default: true },
|
||||||
@@ -37,23 +37,10 @@ const isNotSegmentView = computed(() => {
|
|||||||
return route.name !== 'contacts_dashboard_segments_index';
|
return route.name !== 'contacts_dashboard_segments_index';
|
||||||
});
|
});
|
||||||
|
|
||||||
const isActiveView = computed(() => {
|
|
||||||
return route.name === 'contacts_dashboard_active';
|
|
||||||
});
|
|
||||||
|
|
||||||
const isLabelView = computed(
|
const isLabelView = computed(
|
||||||
() => route.name === 'contacts_dashboard_labels_index'
|
() => route.name === 'contacts_dashboard_labels_index'
|
||||||
);
|
);
|
||||||
|
|
||||||
const showActiveFiltersPreview = computed(() => {
|
|
||||||
return (
|
|
||||||
(props.hasAppliedFilters || !isNotSegmentView.value) &&
|
|
||||||
!props.isFetchingList &&
|
|
||||||
!isLabelView.value &&
|
|
||||||
!isActiveView.value
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateCurrentPage = page => {
|
const updateCurrentPage = page => {
|
||||||
emit('update:currentPage', page);
|
emit('update:currentPage', page);
|
||||||
};
|
};
|
||||||
@@ -70,7 +57,7 @@ const openFilter = () => {
|
|||||||
<div class="flex flex-col w-full h-full transition-all duration-300">
|
<div class="flex flex-col w-full h-full transition-all duration-300">
|
||||||
<ContactListHeaderWrapper
|
<ContactListHeaderWrapper
|
||||||
ref="contactListHeaderWrapper"
|
ref="contactListHeaderWrapper"
|
||||||
:show-search="isNotSegmentView && !isActiveView"
|
:show-search="isNotSegmentView"
|
||||||
:search-value="searchValue"
|
:search-value="searchValue"
|
||||||
:active-sort="activeSort"
|
:active-sort="activeSort"
|
||||||
:active-ordering="activeOrdering"
|
:active-ordering="activeOrdering"
|
||||||
@@ -79,7 +66,6 @@ const openFilter = () => {
|
|||||||
:segments-id="segmentsId"
|
:segments-id="segmentsId"
|
||||||
:has-applied-filters="hasAppliedFilters"
|
:has-applied-filters="hasAppliedFilters"
|
||||||
:is-label-view="isLabelView"
|
:is-label-view="isLabelView"
|
||||||
:is-active-view="isActiveView"
|
|
||||||
@update:sort="emit('update:sort', $event)"
|
@update:sort="emit('update:sort', $event)"
|
||||||
@search="emit('search', $event)"
|
@search="emit('search', $event)"
|
||||||
@apply-filter="emit('applyFilter', $event)"
|
@apply-filter="emit('applyFilter', $event)"
|
||||||
@@ -88,7 +74,11 @@ const openFilter = () => {
|
|||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<div class="w-full mx-auto max-w-[60rem]">
|
<div class="w-full mx-auto max-w-[60rem]">
|
||||||
<ContactsActiveFiltersPreview
|
<ContactsActiveFiltersPreview
|
||||||
v-if="showActiveFiltersPreview"
|
v-if="
|
||||||
|
(hasAppliedFilters || !isNotSegmentView) &&
|
||||||
|
!isFetchingList &&
|
||||||
|
!isLabelView
|
||||||
|
"
|
||||||
:active-segment="activeSegment"
|
:active-segment="activeSegment"
|
||||||
@clear-filters="emit('clearFilters')"
|
@clear-filters="emit('clearFilters')"
|
||||||
@open-filter="openFilter"
|
@open-filter="openFilter"
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ const toggleExpanded = id => {
|
|||||||
:thumbnail="contact.thumbnail"
|
:thumbnail="contact.thumbnail"
|
||||||
:phone-number="contact.phoneNumber"
|
:phone-number="contact.phoneNumber"
|
||||||
:additional-attributes="contact.additionalAttributes"
|
:additional-attributes="contact.additionalAttributes"
|
||||||
:availability-status="contact.availabilityStatus"
|
|
||||||
:is-expanded="expandedCardId === contact.id"
|
:is-expanded="expandedCardId === contact.id"
|
||||||
:is-updating="isUpdating"
|
:is-updating="isUpdating"
|
||||||
@toggle="toggleExpanded(contact.id)"
|
@toggle="toggleExpanded(contact.id)"
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2 border border-n-weak rounded-full gap-2 p-1"
|
class="flex flex-col justify-center items-center absolute top-24 ltr:right-2 rtl:left-2 bg-n-solid-2 border border-n-weak rounded-full gap-2 p-1"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
|
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
|
||||||
|
|||||||
@@ -4,14 +4,38 @@ import { computed, ref, watch, useSlots } from 'vue';
|
|||||||
import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: String, default: '' },
|
modelValue: {
|
||||||
label: { type: String, default: '' },
|
type: String,
|
||||||
placeholder: { type: String, default: '' },
|
default: '',
|
||||||
focusOnMount: { type: Boolean, default: false },
|
},
|
||||||
maxLength: { type: Number, default: 200 },
|
label: {
|
||||||
showCharacterCount: { type: Boolean, default: true },
|
type: String,
|
||||||
disabled: { type: Boolean, default: false },
|
default: '',
|
||||||
message: { type: String, default: '' },
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
focusOnMount: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 200,
|
||||||
|
},
|
||||||
|
showCharacterCount: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
messageType: {
|
messageType: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'info',
|
default: 'info',
|
||||||
@@ -19,7 +43,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
enableVariables: { type: Boolean, default: false },
|
enableVariables: { type: Boolean, default: false },
|
||||||
enableCannedResponses: { type: Boolean, default: true },
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
@@ -97,7 +120,6 @@ watch(
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:enable-variables="enableVariables"
|
:enable-variables="enableVariables"
|
||||||
:enable-canned-responses="enableCannedResponses"
|
:enable-canned-responses="enableCannedResponses"
|
||||||
:enabled-menu-options="enabledMenuOptions"
|
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const handlePageChange = event => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
|
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
|
||||||
<header class="sticky top-0 z-10 px-6">
|
<header class="sticky top-0 z-10 px-6 xl:px-0">
|
||||||
<div class="w-full max-w-[60rem] mx-auto">
|
<div class="w-full max-w-[60rem] mx-auto">
|
||||||
<div
|
<div
|
||||||
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
||||||
@@ -116,7 +116,7 @@ const handlePageChange = event => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 px-6 overflow-y-auto">
|
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
|
||||||
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
|
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
|
||||||
<slot v-if="!showPaywall" name="controls" />
|
<slot v-if="!showPaywall" name="controls" />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, ref, watch, computed } from 'vue';
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useTrack } from 'dashboard/composables';
|
import { useTrack } from 'dashboard/composables';
|
||||||
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
@@ -8,17 +9,23 @@ import CopilotInput from './CopilotInput.vue';
|
|||||||
import CopilotLoader from './CopilotLoader.vue';
|
import CopilotLoader from './CopilotLoader.vue';
|
||||||
import CopilotAgentMessage from './CopilotAgentMessage.vue';
|
import CopilotAgentMessage from './CopilotAgentMessage.vue';
|
||||||
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
|
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
|
||||||
import CopilotThinkingGroup from './CopilotThinkingGroup.vue';
|
|
||||||
import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue';
|
import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue';
|
||||||
import CopilotEmptyState from './CopilotEmptyState.vue';
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue';
|
import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
supportAgent: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
messages: {
|
messages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
isCaptainTyping: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
conversationInboxType: {
|
conversationInboxType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -37,11 +44,18 @@ const emit = defineEmits(['sendMessage', 'reset', 'setAssistant']);
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const COPILOT_USER_ROLES = ['assistant', 'system'];
|
||||||
|
|
||||||
const sendMessage = message => {
|
const sendMessage = message => {
|
||||||
emit('sendMessage', message);
|
emit('sendMessage', message);
|
||||||
useTrack(COPILOT_EVENTS.SEND_MESSAGE);
|
useTrack(COPILOT_EVENTS.SEND_MESSAGE);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useSuggestion = opt => {
|
||||||
|
emit('sendMessage', t(opt.prompt));
|
||||||
|
useTrack(COPILOT_EVENTS.SEND_SUGGESTED);
|
||||||
|
};
|
||||||
|
|
||||||
const chatContainer = ref(null);
|
const chatContainer = ref(null);
|
||||||
|
|
||||||
const scrollToBottom = async () => {
|
const scrollToBottom = async () => {
|
||||||
@@ -51,40 +65,20 @@ const scrollToBottom = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupedMessages = computed(() => {
|
const promptOptions = [
|
||||||
const result = [];
|
{
|
||||||
let thinkingGroup = [];
|
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
|
||||||
props.messages.forEach(message => {
|
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
|
||||||
if (message.message_type === 'assistant_thinking') {
|
},
|
||||||
thinkingGroup.push(message);
|
{
|
||||||
} else {
|
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
|
||||||
if (thinkingGroup.length > 0) {
|
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
|
||||||
result.push({
|
},
|
||||||
id: thinkingGroup[0].id,
|
{
|
||||||
message_type: 'thinking_group',
|
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
|
||||||
messages: thinkingGroup,
|
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
|
||||||
});
|
},
|
||||||
thinkingGroup = [];
|
];
|
||||||
}
|
|
||||||
result.push(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (thinkingGroup.length > 0) {
|
|
||||||
result.push({
|
|
||||||
id: thinkingGroup[0].id,
|
|
||||||
message_type: 'thinking_group',
|
|
||||||
messages: thinkingGroup,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isLastMessageFromAssistant = computed(() => {
|
|
||||||
return (
|
|
||||||
groupedMessages.value[groupedMessages.value.length - 1].message_type ===
|
|
||||||
'assistant'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { updateUISettings } = useUISettings();
|
const { updateUISettings } = useUISettings();
|
||||||
|
|
||||||
@@ -101,22 +95,8 @@ const handleSidebarAction = action => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAssistants = computed(() => props.assistants.length > 0);
|
|
||||||
const hasMessages = computed(() => props.messages.length > 0);
|
|
||||||
const copilotButtons = computed(() => {
|
|
||||||
if (hasMessages.value) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'reset',
|
|
||||||
icon: 'i-lucide-refresh-ccw',
|
|
||||||
tooltip: t('CAPTAIN.COPILOT.RESET'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
watch(
|
watch(
|
||||||
[() => props.messages],
|
[() => props.messages, () => props.isCaptainTyping],
|
||||||
() => {
|
() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
},
|
},
|
||||||
@@ -128,57 +108,64 @@ watch(
|
|||||||
<div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full">
|
<div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full">
|
||||||
<SidebarActionsHeader
|
<SidebarActionsHeader
|
||||||
:title="$t('CAPTAIN.COPILOT.TITLE')"
|
:title="$t('CAPTAIN.COPILOT.TITLE')"
|
||||||
:buttons="copilotButtons"
|
:buttons="[
|
||||||
|
{
|
||||||
|
key: 'reset',
|
||||||
|
icon: 'i-lucide-refresh-ccw',
|
||||||
|
tooltip: $t('CAPTAIN.COPILOT.RESET'),
|
||||||
|
},
|
||||||
|
]"
|
||||||
@click="handleSidebarAction"
|
@click="handleSidebarAction"
|
||||||
@close="closeCopilotPanel"
|
@close="closeCopilotPanel"
|
||||||
/>
|
/>
|
||||||
<div
|
<div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto">
|
||||||
ref="chatContainer"
|
<template v-for="message in messages" :key="message.id">
|
||||||
class="flex-1 flex px-4 py-4 overflow-y-auto items-start"
|
<CopilotAgentMessage
|
||||||
>
|
v-if="message.role === 'user'"
|
||||||
<div v-if="hasMessages" class="space-y-6 flex-1 flex flex-col w-full">
|
:support-agent="supportAgent"
|
||||||
<template v-for="(item, index) in groupedMessages" :key="item.id">
|
:message="message"
|
||||||
<CopilotAgentMessage
|
/>
|
||||||
v-if="item.message_type === 'user'"
|
<CopilotAssistantMessage
|
||||||
:message="item.message"
|
v-else-if="COPILOT_USER_ROLES.includes(message.role)"
|
||||||
/>
|
:message="message"
|
||||||
<CopilotAssistantMessage
|
:conversation-inbox-type="conversationInboxType"
|
||||||
v-else-if="item.message_type === 'assistant'"
|
/>
|
||||||
:message="item.message"
|
</template>
|
||||||
:is-last-message="index === groupedMessages.length - 1"
|
|
||||||
:conversation-inbox-type="conversationInboxType"
|
|
||||||
/>
|
|
||||||
<CopilotThinkingGroup
|
|
||||||
v-else
|
|
||||||
:messages="item.messages"
|
|
||||||
:default-collapsed="isLastMessageFromAssistant"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<CopilotLoader v-if="!isLastMessageFromAssistant" />
|
<CopilotLoader v-if="isCaptainTyping" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!messages.length"
|
||||||
|
class="h-full w-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div class="h-fit px-3 py-3 space-y-1">
|
||||||
|
<span class="text-xs text-n-slate-10">
|
||||||
|
{{ $t('COPILOT.TRY_THESE_PROMPTS') }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-for="prompt in promptOptions"
|
||||||
|
:key="prompt.label"
|
||||||
|
class="px-2 py-1 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center gap-1"
|
||||||
|
@click="() => useSuggestion(prompt)"
|
||||||
|
>
|
||||||
|
<span>{{ t(prompt.label) }}</span>
|
||||||
|
<Icon icon="i-lucide-chevron-right" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CopilotEmptyState
|
|
||||||
v-else
|
|
||||||
:has-assistants="hasAssistants"
|
|
||||||
@use-suggestion="sendMessage"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-3 mt-px mb-2">
|
<div class="mx-3 mt-px mb-2">
|
||||||
<div class="flex items-center gap-2 justify-between w-full mb-1">
|
<div class="flex items-center gap-2 justify-between w-full mb-1">
|
||||||
<ToggleCopilotAssistant
|
<ToggleCopilotAssistant
|
||||||
v-if="assistants.length > 1"
|
v-if="assistants.length"
|
||||||
:assistants="assistants"
|
:assistants="assistants"
|
||||||
:active-assistant="activeAssistant"
|
:active-assistant="activeAssistant"
|
||||||
@set-assistant="$event => emit('setAssistant', $event)"
|
@set-assistant="$event => emit('setAssistant', $event)"
|
||||||
/>
|
/>
|
||||||
<div v-else />
|
<div v-else />
|
||||||
</div>
|
</div>
|
||||||
<CopilotInput
|
<CopilotInput class="mb-1 w-full" @send="sendMessage" />
|
||||||
v-if="hasAssistants"
|
|
||||||
class="mb-1 w-full"
|
|
||||||
@send="sendMessage"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
|||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isLastMessage: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -24,15 +20,6 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const hasEmptyMessageContent = computed(() => !props.message?.content);
|
|
||||||
|
|
||||||
const showUseButton = computed(() => {
|
|
||||||
return (
|
|
||||||
!hasEmptyMessageContent.value &&
|
|
||||||
props.message.reply_suggestion &&
|
|
||||||
props.isLastMessage
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageContent = computed(() => {
|
const messageContent = computed(() => {
|
||||||
const formatter = new MessageFormatter(props.message.content);
|
const formatter = new MessageFormatter(props.message.content);
|
||||||
@@ -45,6 +32,8 @@ const insertIntoRichEditor = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasEmptyMessageContent = computed(() => !props.message?.content);
|
||||||
|
|
||||||
const useCopilotResponse = () => {
|
const useCopilotResponse = () => {
|
||||||
if (insertIntoRichEditor.value) {
|
if (insertIntoRichEditor.value) {
|
||||||
emitter.emit(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, props.message?.content);
|
emitter.emit(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, props.message?.content);
|
||||||
@@ -68,7 +57,7 @@ const useCopilotResponse = () => {
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-row mt-1">
|
<div class="flex flex-row mt-1">
|
||||||
<Button
|
<Button
|
||||||
v-if="showUseButton"
|
v-if="!hasEmptyMessageContent"
|
||||||
:label="$t('CAPTAIN.COPILOT.USE')"
|
:label="$t('CAPTAIN.COPILOT.USE')"
|
||||||
faded
|
faded
|
||||||
sm
|
sm
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import Icon from '../icon/Icon.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
hasAssistants: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['useSuggestion']);
|
|
||||||
const { t } = useI18n();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const routePromptMap = {
|
|
||||||
conversations: [
|
|
||||||
{
|
|
||||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
|
|
||||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
|
|
||||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
|
|
||||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dashboard: [
|
|
||||||
{
|
|
||||||
label: 'CAPTAIN.COPILOT.PROMPTS.HIGH_PRIORITY.LABEL',
|
|
||||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.HIGH_PRIORITY.CONTENT',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'CAPTAIN.COPILOT.PROMPTS.LIST_CONTACTS.LABEL',
|
|
||||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.LIST_CONTACTS.CONTENT',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCurrentRoute = () => {
|
|
||||||
const path = route.path;
|
|
||||||
if (path.includes('/conversations')) return 'conversations';
|
|
||||||
if (path.includes('/dashboard')) return 'dashboard';
|
|
||||||
return 'dashboard';
|
|
||||||
};
|
|
||||||
|
|
||||||
const promptOptions = computed(() => {
|
|
||||||
const currentRoute = getCurrentRoute();
|
|
||||||
return routePromptMap[currentRoute] || routePromptMap.conversations;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSuggestion = opt => {
|
|
||||||
emit('useSuggestion', t(opt.prompt));
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex-1 flex flex-col gap-6 px-2">
|
|
||||||
<div class="flex flex-col space-y-4 py-4">
|
|
||||||
<Icon icon="i-woot-captain" class="text-n-slate-9 text-4xl" />
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h3 class="text-base font-medium text-n-slate-12 leading-8">
|
|
||||||
{{ $t('CAPTAIN.COPILOT.PANEL_TITLE') }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-n-slate-11 leading-6">
|
|
||||||
{{ $t('CAPTAIN.COPILOT.KICK_OFF_MESSAGE') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!hasAssistants" class="w-full space-y-2">
|
|
||||||
<p class="text-sm text-n-slate-11 leading-6">
|
|
||||||
{{ $t('CAPTAIN.ASSISTANTS.NO_ASSISTANTS_AVAILABLE') }}
|
|
||||||
</p>
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'captain_assistants_index',
|
|
||||||
params: { accountId: route.params.accountId },
|
|
||||||
}"
|
|
||||||
class="text-n-slate-11 underline hover:text-n-slate-12"
|
|
||||||
>
|
|
||||||
{{ $t('CAPTAIN.ASSISTANTS.ADD_NEW') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-full space-y-2">
|
|
||||||
<span class="text-xs text-n-slate-10 block">
|
|
||||||
{{ $t('CAPTAIN.COPILOT.TRY_THESE_PROMPTS') }}
|
|
||||||
</span>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<button
|
|
||||||
v-for="prompt in promptOptions"
|
|
||||||
:key="prompt.label"
|
|
||||||
class="w-full px-3 py-2 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center justify-between hover:bg-n-slate-3 transition-colors"
|
|
||||||
@click="handleSuggestion(prompt)"
|
|
||||||
>
|
|
||||||
<span>{{ t(prompt.label) }}</span>
|
|
||||||
<Icon icon="i-lucide-chevron-right" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
|
||||||
|
|
||||||
const isConversationRoute = computed(() => {
|
|
||||||
const CONVERSATION_ROUTES = [
|
|
||||||
'inbox_conversation',
|
|
||||||
'conversation_through_inbox',
|
|
||||||
'conversations_through_label',
|
|
||||||
'team_conversations_through_label',
|
|
||||||
'conversations_through_folders',
|
|
||||||
'conversation_through_mentions',
|
|
||||||
'conversation_through_unattended',
|
|
||||||
'conversation_through_participating',
|
|
||||||
];
|
|
||||||
return CONVERSATION_ROUTES.includes(route.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
|
||||||
const isFeatureEnabledonAccount = useMapGetter(
|
|
||||||
'accounts/isFeatureEnabledonAccount'
|
|
||||||
);
|
|
||||||
|
|
||||||
const showCopilotLauncher = computed(() => {
|
|
||||||
const isCaptainEnabled = isFeatureEnabledonAccount.value(
|
|
||||||
currentAccountId.value,
|
|
||||||
FEATURE_FLAGS.CAPTAIN
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
isCaptainEnabled &&
|
|
||||||
!uiSettings.value.is_copilot_panel_open &&
|
|
||||||
!isConversationRoute.value
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
updateUISettings({
|
|
||||||
is_copilot_panel_open: !uiSettings.value.is_copilot_panel_open,
|
|
||||||
is_contact_sidebar_open: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="showCopilotLauncher" class="fixed bottom-4 right-4 z-50">
|
|
||||||
<div class="rounded-full bg-n-alpha-2 p-1">
|
|
||||||
<Button
|
|
||||||
icon="i-woot-captain"
|
|
||||||
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
|
|
||||||
lg
|
|
||||||
@click="toggleSidebar"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-else />
|
|
||||||
</template>
|
|
||||||
@@ -17,7 +17,7 @@ defineProps({
|
|||||||
icon="i-lucide-sparkles"
|
icon="i-lucide-sparkles"
|
||||||
class="w-4 h-4 mt-0.5 flex-shrink-0 text-n-slate-9"
|
class="w-4 h-4 mt-0.5 flex-shrink-0 text-n-slate-9"
|
||||||
/>
|
/>
|
||||||
<div class="text-sm text-n-slate-12">
|
<div class="text-sm text-n-slate-11">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ watch(
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<CopilotThinkingBlock
|
<CopilotThinkingBlock
|
||||||
v-for="copilotMessage in messages"
|
v-for="message in messages"
|
||||||
:key="copilotMessage.id"
|
:key="message.id"
|
||||||
:content="copilotMessage.message.content"
|
:content="message.content"
|
||||||
:reasoning="copilotMessage.message.reasoning"
|
:reasoning="message.reasoning"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ const updateSelected = newValue => {
|
|||||||
<slot name="trigger" :toggle="toggle">
|
<slot name="trigger" :toggle="toggle">
|
||||||
<Button
|
<Button
|
||||||
ref="triggerRef"
|
ref="triggerRef"
|
||||||
type="button"
|
|
||||||
sm
|
sm
|
||||||
slate
|
slate
|
||||||
:variant
|
:variant
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
|
|||||||
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
||||||
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
||||||
|
|
||||||
const {
|
const { options } = defineProps({
|
||||||
options,
|
|
||||||
disableSearch,
|
|
||||||
placeholderIcon,
|
|
||||||
placeholder,
|
|
||||||
placeholderTrailingIcon,
|
|
||||||
searchPlaceholder,
|
|
||||||
} = defineProps({
|
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -25,22 +18,6 @@ const {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
placeholderIcon: {
|
|
||||||
type: String,
|
|
||||||
default: 'i-lucide-plus',
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
placeholderTrailingIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
searchPlaceholder: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -92,26 +69,15 @@ const toggleSelected = option => {
|
|||||||
sm
|
sm
|
||||||
slate
|
slate
|
||||||
faded
|
faded
|
||||||
type="button"
|
|
||||||
:icon="selectedItem.icon"
|
:icon="selectedItem.icon"
|
||||||
:label="selectedItem.name"
|
:label="selectedItem.name"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button v-else sm slate faded @click="toggle">
|
||||||
v-else
|
|
||||||
sm
|
|
||||||
slate
|
|
||||||
faded
|
|
||||||
type="button"
|
|
||||||
:trailing-icon="placeholderTrailingIcon"
|
|
||||||
@click="toggle"
|
|
||||||
>
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Icon :icon="placeholderIcon" class="text-n-slate-11" />
|
<Icon icon="i-lucide-plus" class="text-n-slate-11" />
|
||||||
</template>
|
</template>
|
||||||
<span class="text-n-slate-11">{{
|
<span class="text-n-slate-11">{{ t('COMBOBOX.PLACEHOLDER') }}</span>
|
||||||
placeholder || t('COMBOBOX.PLACEHOLDER')
|
|
||||||
}}</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<DropdownBody class="top-0 min-w-56 z-50" strong>
|
<DropdownBody class="top-0 min-w-56 z-50" strong>
|
||||||
@@ -121,7 +87,7 @@ const toggleSelected = option => {
|
|||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
autofocus
|
autofocus
|
||||||
class="p-1.5 pl-8 text-n-slate-11 bg-n-alpha-1 rounded-lg w-full"
|
class="p-1.5 pl-8 text-n-slate-11 bg-n-alpha-1 rounded-lg w-full"
|
||||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
:placeholder="t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSection class="max-h-80 overflow-scroll">
|
<DropdownSection class="max-h-80 overflow-scroll">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import Input from './Input.vue';
|
import Input from './Input.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { DURATION_UNITS } from './constants';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
min: { type: Number, default: 0 },
|
min: { type: Number, default: 0 },
|
||||||
@@ -12,50 +11,36 @@ const props = defineProps({
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const duration = defineModel('modelValue', { type: Number, default: null });
|
const duration = defineModel('modelValue', { type: Number, default: null });
|
||||||
const unit = defineModel('unit', {
|
|
||||||
type: String,
|
|
||||||
default: DURATION_UNITS.MINUTES,
|
|
||||||
validate(value) {
|
|
||||||
return Object.values(DURATION_UNITS).includes(value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const convertToMinutes = newValue => {
|
const UNIT_TYPES = {
|
||||||
if (unit.value === DURATION_UNITS.MINUTES) {
|
MINUTES: 'minutes',
|
||||||
return Math.floor(newValue);
|
HOURS: 'hours',
|
||||||
}
|
DAYS: 'days',
|
||||||
if (unit.value === DURATION_UNITS.HOURS) {
|
|
||||||
return Math.floor(newValue) * 60;
|
|
||||||
}
|
|
||||||
return Math.floor(newValue) * 24 * 60;
|
|
||||||
};
|
};
|
||||||
|
const unit = ref(UNIT_TYPES.MINUTES);
|
||||||
|
|
||||||
const transformedValue = computed({
|
const transformedValue = computed({
|
||||||
get() {
|
get() {
|
||||||
if (unit.value === DURATION_UNITS.MINUTES) return duration.value;
|
if (unit.value === UNIT_TYPES.MINUTES) return duration.value;
|
||||||
if (unit.value === DURATION_UNITS.HOURS)
|
if (unit.value === UNIT_TYPES.HOURS) return Math.floor(duration.value / 60);
|
||||||
return Math.floor(duration.value / 60);
|
if (unit.value === UNIT_TYPES.DAYS)
|
||||||
if (unit.value === DURATION_UNITS.DAYS)
|
|
||||||
return Math.floor(duration.value / 24 / 60);
|
return Math.floor(duration.value / 24 / 60);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
set(newValue) {
|
set(newValue) {
|
||||||
let minuteValue = convertToMinutes(newValue);
|
let minuteValue;
|
||||||
|
if (unit.value === UNIT_TYPES.MINUTES) {
|
||||||
|
minuteValue = Math.floor(newValue);
|
||||||
|
} else if (unit.value === UNIT_TYPES.HOURS) {
|
||||||
|
minuteValue = Math.floor(newValue * 60);
|
||||||
|
} else if (unit.value === UNIT_TYPES.DAYS) {
|
||||||
|
minuteValue = Math.floor(newValue * 24 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
|
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// when unit is changed set the nearest value to that unit
|
|
||||||
// so if the minute is set to 900, and the user changes the unit to "days"
|
|
||||||
// the transformed value will show 0, but the real value will still be 900
|
|
||||||
// this might create some confusion, especially when saving
|
|
||||||
// this watcher fixes it by rounding the duration basically, to the nearest unit value
|
|
||||||
watch(unit, () => {
|
|
||||||
let adjustedValue = convertToMinutes(transformedValue.value);
|
|
||||||
duration.value = Math.min(Math.max(adjustedValue, props.min), props.max);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -72,12 +57,10 @@ watch(unit, () => {
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="mb-0 text-sm disabled:outline-n-weak disabled:opacity-40"
|
class="mb-0 text-sm disabled:outline-n-weak disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<option :value="DURATION_UNITS.MINUTES">
|
<option :value="UNIT_TYPES.MINUTES">
|
||||||
{{ t('DURATION_INPUT.MINUTES') }}
|
{{ t('DURATION_INPUT.MINUTES') }}
|
||||||
</option>
|
</option>
|
||||||
<option :value="DURATION_UNITS.HOURS">
|
<option :value="UNIT_TYPES.HOURS">{{ t('DURATION_INPUT.HOURS') }}</option>
|
||||||
{{ t('DURATION_INPUT.HOURS') }}
|
<option :value="UNIT_TYPES.DAYS">{{ t('DURATION_INPUT.DAYS') }}</option>
|
||||||
</option>
|
|
||||||
<option :value="DURATION_UNITS.DAYS">{{ t('DURATION_INPUT.DAYS') }}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export const DURATION_UNITS = {
|
|
||||||
MINUTES: 'minutes',
|
|
||||||
HOURS: 'hours',
|
|
||||||
DAYS: 'days',
|
|
||||||
};
|
|
||||||
@@ -4,11 +4,9 @@ import BaseBubble from './Base.vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { CONTENT_TYPES } from '../constants.js';
|
import { CONTENT_TYPES } from '../constants.js';
|
||||||
import { useMessageContext } from '../provider.js';
|
import { useMessageContext } from '../provider.js';
|
||||||
import { useInbox } from 'dashboard/composables/useInbox';
|
|
||||||
|
|
||||||
const { content, contentAttributes, contentType } = useMessageContext();
|
const { content, contentAttributes, contentType } = useMessageContext();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { isAWebWidgetInbox } = useInbox();
|
|
||||||
|
|
||||||
const formValues = computed(() => {
|
const formValues = computed(() => {
|
||||||
if (contentType.value === CONTENT_TYPES.FORM) {
|
if (contentType.value === CONTENT_TYPES.FORM) {
|
||||||
@@ -58,7 +56,7 @@ const formValues = computed(() => {
|
|||||||
<dd>{{ item.title }}</dd>
|
<dd>{{ item.title }}</dd>
|
||||||
</template>
|
</template>
|
||||||
</dl>
|
</dl>
|
||||||
<div v-else-if="isAWebWidgetInbox" class="my-2 font-medium">
|
<div v-else class="my-2 font-medium">
|
||||||
{{ t('CONVERSATION.NO_RESPONSE') }}
|
{{ t('CONVERSATION.NO_RESPONSE') }}
|
||||||
</div>
|
</div>
|
||||||
</BaseBubble>
|
</BaseBubble>
|
||||||
|
|||||||
@@ -109,58 +109,49 @@ const downloadAudio = async () => {
|
|||||||
</audio>
|
</audio>
|
||||||
<div
|
<div
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
class="rounded-xl w-full gap-2 p-1.5 bg-n-alpha-white flex flex-col items-center border border-n-container shadow-[0px_2px_8px_0px_rgba(94,94,94,0.06)]"
|
class="rounded-xl w-full gap-1 p-1.5 bg-n-alpha-white flex items-center border border-n-container shadow-[0px_2px_8px_0px_rgba(94,94,94,0.06)]"
|
||||||
>
|
>
|
||||||
<div class="flex gap-1 w-full flex-1 items-center justify-start">
|
<button class="p-0 border-0 size-8" @click="playOrPause">
|
||||||
<button class="p-0 border-0 size-8" @click="playOrPause">
|
<Icon
|
||||||
<Icon
|
v-if="isPlaying"
|
||||||
v-if="isPlaying"
|
class="size-8"
|
||||||
class="size-8"
|
icon="i-teenyicons-pause-small-solid"
|
||||||
icon="i-teenyicons-pause-small-solid"
|
/>
|
||||||
/>
|
<Icon v-else class="size-8" icon="i-teenyicons-play-small-solid" />
|
||||||
<Icon v-else class="size-8" icon="i-teenyicons-play-small-solid" />
|
</button>
|
||||||
</button>
|
<div class="tabular-nums text-xs">
|
||||||
<div class="tabular-nums text-xs">
|
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 items-center flex px-2">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
:max="duration"
|
|
||||||
:value="currentTime"
|
|
||||||
class="w-full h-1 bg-n-slate-12/40 rounded-lg appearance-none cursor-pointer accent-current"
|
|
||||||
@input="seek"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="border-0 w-10 h-6 grid place-content-center bg-n-alpha-2 hover:bg-alpha-3 rounded-2xl"
|
|
||||||
@click="changePlaybackSpeed"
|
|
||||||
>
|
|
||||||
<span class="text-xs text-n-slate-11 font-medium">
|
|
||||||
{{ playbackSpeedLabel }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-0 border-0 size-8 grid place-content-center"
|
|
||||||
@click="toggleMute"
|
|
||||||
>
|
|
||||||
<Icon v-if="isMuted" class="size-4" icon="i-lucide-volume-off" />
|
|
||||||
<Icon v-else class="size-4" icon="i-lucide-volume-2" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-0 border-0 size-8 grid place-content-center"
|
|
||||||
@click="downloadAudio"
|
|
||||||
>
|
|
||||||
<Icon class="size-4" icon="i-lucide-download" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1 items-center flex px-2">
|
||||||
<div
|
<input
|
||||||
v-if="attachment.transcribedText"
|
type="range"
|
||||||
class="text-n-slate-12 p-3 text-sm bg-n-alpha-1 rounded-lg w-full break-words"
|
min="0"
|
||||||
|
:max="duration"
|
||||||
|
:value="currentTime"
|
||||||
|
class="w-full h-1 bg-n-slate-12/40 rounded-lg appearance-none cursor-pointer accent-current"
|
||||||
|
@input="seek"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="border-0 w-10 h-6 grid place-content-center bg-n-alpha-2 hover:bg-alpha-3 rounded-2xl"
|
||||||
|
@click="changePlaybackSpeed"
|
||||||
>
|
>
|
||||||
{{ attachment.transcribedText }}
|
<span class="text-xs text-n-slate-11 font-medium">
|
||||||
</div>
|
{{ playbackSpeedLabel }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-0 border-0 size-8 grid place-content-center"
|
||||||
|
@click="toggleMute"
|
||||||
|
>
|
||||||
|
<Icon v-if="isMuted" class="size-4" icon="i-lucide-volume-off" />
|
||||||
|
<Icon v-else class="size-4" icon="i-lucide-volume-2" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-0 border-0 size-8 grid place-content-center"
|
||||||
|
@click="downloadAudio"
|
||||||
|
>
|
||||||
|
<Icon class="size-4" icon="i-lucide-download" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const newReportRoutes = () => [
|
|||||||
{
|
{
|
||||||
name: 'Reports Label',
|
name: 'Reports Label',
|
||||||
label: t('SIDEBAR.REPORTS_LABEL'),
|
label: t('SIDEBAR.REPORTS_LABEL'),
|
||||||
to: accountScopedRoute('label_reports_index'),
|
to: accountScopedRoute('label_reports'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Reports Inbox',
|
name: 'Reports Inbox',
|
||||||
@@ -235,12 +235,6 @@ const menuItems = computed(() => {
|
|||||||
),
|
),
|
||||||
activeOn: ['contacts_dashboard_index', 'contacts_edit'],
|
activeOn: ['contacts_dashboard_index', 'contacts_edit'],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Active',
|
|
||||||
label: t('SIDEBAR.ACTIVE'),
|
|
||||||
to: accountScopedRoute('contacts_dashboard_active'),
|
|
||||||
activeOn: ['contacts_dashboard_active'],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Segments',
|
name: 'Segments',
|
||||||
icon: 'i-lucide-group',
|
icon: 'i-lucide-group',
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
// https://tanstack.com/virtual/latest/docs/framework/vue/examples/variable
|
// https://tanstack.com/virtual/latest/docs/framework/vue/examples/variable
|
||||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
||||||
import ChatListHeader from './ChatListHeader.vue';
|
import ChatListHeader from './ChatListHeader.vue';
|
||||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
|
||||||
import ConversationFilter from 'next/filter/ConversationFilter.vue';
|
import ConversationFilter from 'next/filter/ConversationFilter.vue';
|
||||||
import SaveCustomView from 'next/filter/SaveCustomView.vue';
|
import SaveCustomView from 'next/filter/SaveCustomView.vue';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||||
@@ -83,7 +82,6 @@ const emit = defineEmits(['conversationLoad']);
|
|||||||
const { uiSettings } = useUISettings();
|
const { uiSettings } = useUISettings();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const conversationListRef = ref(null);
|
const conversationListRef = ref(null);
|
||||||
@@ -648,30 +646,6 @@ function openLastItemAfterDeleteInFolder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectToConversationList() {
|
|
||||||
const {
|
|
||||||
params: { accountId, inbox_id: inboxId, label, teamId },
|
|
||||||
name,
|
|
||||||
} = route;
|
|
||||||
|
|
||||||
let conversationType = '';
|
|
||||||
if (isOnMentionsView({ route: { name } })) {
|
|
||||||
conversationType = 'mention';
|
|
||||||
} else if (isOnUnattendedView({ route: { name } })) {
|
|
||||||
conversationType = 'unattended';
|
|
||||||
}
|
|
||||||
router.push(
|
|
||||||
conversationListPageURL({
|
|
||||||
accountId,
|
|
||||||
conversationType: conversationType,
|
|
||||||
customViewId: props.foldersId,
|
|
||||||
inboxId,
|
|
||||||
label,
|
|
||||||
teamId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assignPriority(priority, conversationId = null) {
|
async function assignPriority(priority, conversationId = null) {
|
||||||
store.dispatch('setCurrentChatPriority', {
|
store.dispatch('setCurrentChatPriority', {
|
||||||
priority,
|
priority,
|
||||||
@@ -696,7 +670,26 @@ async function markAsUnread(conversationId) {
|
|||||||
await store.dispatch('markMessagesUnread', {
|
await store.dispatch('markMessagesUnread', {
|
||||||
id: conversationId,
|
id: conversationId,
|
||||||
});
|
});
|
||||||
redirectToConversationList();
|
const {
|
||||||
|
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||||
|
name,
|
||||||
|
} = useRoute();
|
||||||
|
let conversationType = '';
|
||||||
|
if (isOnMentionsView({ route: { name } })) {
|
||||||
|
conversationType = 'mention';
|
||||||
|
} else if (isOnUnattendedView({ route: { name } })) {
|
||||||
|
conversationType = 'unattended';
|
||||||
|
}
|
||||||
|
router.push(
|
||||||
|
conversationListPageURL({
|
||||||
|
accountId,
|
||||||
|
conversationType: conversationType,
|
||||||
|
customViewId: props.foldersId,
|
||||||
|
inboxId,
|
||||||
|
label,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
@@ -710,7 +703,6 @@ async function markAsRead(conversationId) {
|
|||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAssignTeam(team, conversationId = null) {
|
async function onAssignTeam(team, conversationId = null) {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('assignTeam', {
|
await store.dispatch('assignTeam', {
|
||||||
@@ -772,26 +764,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteConversationDialogRef = ref(null);
|
|
||||||
const selectedConversationId = ref(null);
|
|
||||||
|
|
||||||
async function deleteConversation() {
|
|
||||||
try {
|
|
||||||
await store.dispatch('deleteConversation', selectedConversationId.value);
|
|
||||||
redirectToConversationList();
|
|
||||||
selectedConversationId.value = null;
|
|
||||||
deleteConversationDialogRef.value.close();
|
|
||||||
useAlert(t('CONVERSATION.SUCCESS_DELETE_CONVERSATION'));
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(t('CONVERSATION.FAIL_DELETE_CONVERSATION'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = conversationId => {
|
|
||||||
selectedConversationId.value = conversationId;
|
|
||||||
deleteConversationDialogRef.value.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
provide('selectConversation', selectConversation);
|
provide('selectConversation', selectConversation);
|
||||||
provide('deSelectConversation', deSelectConversation);
|
provide('deSelectConversation', deSelectConversation);
|
||||||
provide('assignAgent', onAssignAgent);
|
provide('assignAgent', onAssignAgent);
|
||||||
@@ -803,7 +775,6 @@ provide('markAsUnread', markAsUnread);
|
|||||||
provide('markAsRead', markAsRead);
|
provide('markAsRead', markAsRead);
|
||||||
provide('assignPriority', assignPriority);
|
provide('assignPriority', assignPriority);
|
||||||
provide('isConversationSelected', isConversationSelected);
|
provide('isConversationSelected', isConversationSelected);
|
||||||
provide('deleteConversation', handleDelete);
|
|
||||||
|
|
||||||
watch(activeTeam, () => resetAndFetchData());
|
watch(activeTeam, () => resetAndFetchData());
|
||||||
|
|
||||||
@@ -853,8 +824,6 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
:has-active-folders="hasActiveFolders"
|
:has-active-folders="hasActiveFolders"
|
||||||
:active-status="activeStatus"
|
:active-status="activeStatus"
|
||||||
:is-on-expanded-layout="isOnExpandedLayout"
|
:is-on-expanded-layout="isOnExpandedLayout"
|
||||||
:conversation-stats="conversationStats"
|
|
||||||
:is-list-loading="chatListLoading"
|
|
||||||
@add-folders="onClickOpenAddFoldersModal"
|
@add-folders="onClickOpenAddFoldersModal"
|
||||||
@delete-folders="onClickOpenDeleteFoldersModal"
|
@delete-folders="onClickOpenDeleteFoldersModal"
|
||||||
@filters-modal="onToggleAdvanceFiltersModal"
|
@filters-modal="onToggleAdvanceFiltersModal"
|
||||||
@@ -969,19 +938,6 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
</template>
|
</template>
|
||||||
</DynamicScroller>
|
</DynamicScroller>
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
|
||||||
ref="deleteConversationDialogRef"
|
|
||||||
type="alert"
|
|
||||||
:title="
|
|
||||||
$t('CONVERSATION.DELETE_CONVERSATION.TITLE', {
|
|
||||||
conversationId: selectedConversationId,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
:description="$t('CONVERSATION.DELETE_CONVERSATION.DESCRIPTION')"
|
|
||||||
:confirm-button-label="$t('CONVERSATION.DELETE_CONVERSATION.CONFIRM')"
|
|
||||||
@confirm="deleteConversation"
|
|
||||||
@close="selectedConversationId = null"
|
|
||||||
/>
|
|
||||||
<TeleportWithDirection
|
<TeleportWithDirection
|
||||||
v-if="showAdvancedFilters"
|
v-if="showAdvancedFilters"
|
||||||
to="#conversationFilterTeleportTarget"
|
to="#conversationFilterTeleportTarget"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
import { formatNumber } from '@chatwoot/utils';
|
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
@@ -11,13 +10,26 @@ import SwitchLayout from 'dashboard/routes/dashboard/conversation/search/SwitchL
|
|||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pageTitle: { type: String, required: true },
|
pageTitle: {
|
||||||
hasAppliedFilters: { type: Boolean, required: true },
|
type: String,
|
||||||
hasActiveFolders: { type: Boolean, required: true },
|
required: true,
|
||||||
activeStatus: { type: String, required: true },
|
},
|
||||||
isOnExpandedLayout: { type: Boolean, required: true },
|
hasAppliedFilters: {
|
||||||
conversationStats: { type: Object, required: true },
|
type: Boolean,
|
||||||
isListLoading: { type: Boolean, required: true },
|
required: true,
|
||||||
|
},
|
||||||
|
hasActiveFolders: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
activeStatus: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isOnExpandedLayout: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -50,9 +62,6 @@ const showV4View = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const allCount = computed(() => props.conversationStats?.allCount || 0);
|
|
||||||
const formattedAllCount = computed(() => formatNumber(allCount.value));
|
|
||||||
|
|
||||||
const toggleConversationLayout = () => {
|
const toggleConversationLayout = () => {
|
||||||
const { LAYOUT_TYPES } = wootConstants;
|
const { LAYOUT_TYPES } = wootConstants;
|
||||||
const {
|
const {
|
||||||
@@ -83,15 +92,6 @@ const toggleConversationLayout = () => {
|
|||||||
>
|
>
|
||||||
{{ pageTitle }}
|
{{ pageTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
allCount > 0 && hasAppliedFiltersOrActiveFolders && !isListLoading
|
|
||||||
"
|
|
||||||
class="px-2 py-1 my-0.5 mx-1 rounded-md capitalize bg-n-slate-3 text-xxs text-n-slate-12 shrink-0"
|
|
||||||
:title="allCount"
|
|
||||||
>
|
|
||||||
{{ formattedAllCount }}
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||||
class="px-2 py-1 my-0.5 mx-1 rounded-md capitalize bg-n-slate-3 text-xxs text-n-slate-12 shrink-0"
|
class="px-2 py-1 my-0.5 mx-1 rounded-md capitalize bg-n-slate-3 text-xxs text-n-slate-12 shrink-0"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export default {
|
|||||||
'markAsRead',
|
'markAsRead',
|
||||||
'assignPriority',
|
'assignPriority',
|
||||||
'isConversationSelected',
|
'isConversationSelected',
|
||||||
'deleteConversation',
|
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
source: {
|
source: {
|
||||||
@@ -68,6 +67,5 @@ export default {
|
|||||||
@mark-as-unread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
@mark-as-read="markAsRead"
|
@mark-as-read="markAsRead"
|
||||||
@assign-priority="assignPriority"
|
@assign-priority="assignPriority"
|
||||||
@delete-conversation="deleteConversation"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex items-center justify-end resolve-actions">
|
<div class="relative flex items-center justify-end resolve-actions">
|
||||||
<div
|
<div
|
||||||
class="rounded-lg shadow outline-1 outline flex-shrink-0"
|
class="rounded-lg shadow outline-1 outline"
|
||||||
:class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'"
|
:class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -178,7 +178,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
|||||||
<div
|
<div
|
||||||
v-if="showActionsDropdown"
|
v-if="showActionsDropdown"
|
||||||
v-on-clickaway="closeDropdown"
|
v-on-clickaway="closeDropdown"
|
||||||
class="dropdown-pane dropdown-pane--open left-auto top-full mt-0.5 start-0 xl:start-auto xl:end-0 max-w-[12.5rem] min-w-[9.75rem]"
|
class="dropdown-pane dropdown-pane--open left-auto top-full mt-0.5 ltr:right-0 rtl:left-0 max-w-[12.5rem] min-w-[9.75rem]"
|
||||||
>
|
>
|
||||||
<WootDropdownMenu class="mb-0">
|
<WootDropdownMenu class="mb-0">
|
||||||
<WootDropdownItem v-if="!isPending">
|
<WootDropdownItem v-if="!isPending">
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, watchEffect } from 'vue';
|
||||||
import { useStore } from 'dashboard/composables/store';
|
import { useStore } from 'dashboard/composables/store';
|
||||||
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
||||||
|
import ConversationAPI from 'dashboard/api/inbox/conversation';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
conversationInboxType: {
|
conversationInboxType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -15,25 +20,13 @@ defineProps({
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
|
||||||
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
||||||
const currentChat = useMapGetter('getSelectedChat');
|
|
||||||
|
|
||||||
const selectedCopilotThreadId = ref(null);
|
|
||||||
const messages = computed(() =>
|
|
||||||
store.getters['copilotMessages/getMessagesByThreadId'](
|
|
||||||
selectedCopilotThreadId.value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
|
||||||
const isFeatureEnabledonAccount = useMapGetter(
|
|
||||||
'accounts/isFeatureEnabledonAccount'
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedAssistantId = ref(null);
|
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
const messages = ref([]);
|
||||||
|
const isCaptainTyping = ref(false);
|
||||||
|
const selectedAssistantId = ref(null);
|
||||||
|
|
||||||
const activeAssistant = computed(() => {
|
const activeAssistant = computed(() => {
|
||||||
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
||||||
|
|
||||||
@@ -62,57 +55,68 @@ const setAssistant = async assistant => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowCopilotPanel = computed(() => {
|
|
||||||
const isCaptainEnabled = isFeatureEnabledonAccount.value(
|
|
||||||
currentAccountId.value,
|
|
||||||
FEATURE_FLAGS.CAPTAIN
|
|
||||||
);
|
|
||||||
const { is_copilot_panel_open: isCopilotPanelOpen } = uiSettings.value;
|
|
||||||
return isCaptainEnabled && isCopilotPanelOpen && !uiFlags.value.fetchingList;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
selectedCopilotThreadId.value = null;
|
messages.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessage = async message => {
|
const sendMessage = async message => {
|
||||||
if (selectedCopilotThreadId.value) {
|
// Add user message
|
||||||
await store.dispatch('copilotMessages/create', {
|
messages.value.push({
|
||||||
assistant_id: activeAssistant.value.id,
|
id: messages.value.length + 1,
|
||||||
conversation_id: currentChat.value?.id,
|
role: 'user',
|
||||||
threadId: selectedCopilotThreadId.value,
|
content: message,
|
||||||
message,
|
});
|
||||||
|
isCaptainTyping.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await ConversationAPI.requestCopilot(
|
||||||
|
props.conversationId,
|
||||||
|
{
|
||||||
|
previous_history: messages.value
|
||||||
|
.map(m => ({
|
||||||
|
role: m.role,
|
||||||
|
content: m.content,
|
||||||
|
}))
|
||||||
|
.slice(0, -1),
|
||||||
|
message,
|
||||||
|
assistant_id: selectedAssistantId.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
messages.value.push({
|
||||||
|
id: new Date().getTime(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: data.message,
|
||||||
});
|
});
|
||||||
} else {
|
} catch (error) {
|
||||||
const response = await store.dispatch('copilotThreads/create', {
|
// eslint-disable-next-line
|
||||||
assistant_id: activeAssistant.value.id,
|
console.log(error);
|
||||||
conversation_id: currentChat.value?.id,
|
} finally {
|
||||||
message,
|
isCaptainTyping.value = false;
|
||||||
});
|
|
||||||
selectedCopilotThreadId.value = response.id;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch('captainAssistants/get');
|
store.dispatch('captainAssistants/get');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.conversationId) {
|
||||||
|
store.dispatch('getInboxCaptainAssistantById', props.conversationId);
|
||||||
|
selectedAssistantId.value = activeAssistant.value?.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Copilot
|
||||||
v-if="shouldShowCopilotPanel"
|
:messages="messages"
|
||||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-[320px] min-w-[320px] 2xl:min-w-[360px] 2xl:w-[360px] flex flex-col bg-n-background"
|
:support-agent="currentUser"
|
||||||
>
|
:is-captain-typing="isCaptainTyping"
|
||||||
<Copilot
|
:conversation-inbox-type="conversationInboxType"
|
||||||
:messages="messages"
|
:assistants="assistants"
|
||||||
:support-agent="currentUser"
|
:active-assistant="activeAssistant"
|
||||||
:conversation-inbox-type="conversationInboxType"
|
@set-assistant="setAssistant"
|
||||||
:assistants="assistants"
|
@send-message="sendMessage"
|
||||||
:active-assistant="activeAssistant"
|
@reset="handleReset"
|
||||||
@set-assistant="setAssistant"
|
/>
|
||||||
@send-message="sendMessage"
|
|
||||||
@reset="handleReset"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template v-else />
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const contacts = accountId => ({
|
|||||||
'contacts_edit',
|
'contacts_edit',
|
||||||
'contacts_edit_segment',
|
'contacts_edit_segment',
|
||||||
'contacts_edit_label',
|
'contacts_edit_label',
|
||||||
'contacts_dashboard_active',
|
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
@@ -19,13 +18,6 @@ const contacts = accountId => ({
|
|||||||
toState: frontendURL(`accounts/${accountId}/contacts?page=1`),
|
toState: frontendURL(`accounts/${accountId}/contacts?page=1`),
|
||||||
toStateName: 'contacts_dashboard_index',
|
toStateName: 'contacts_dashboard_index',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'visitor-contacts',
|
|
||||||
label: 'ACTIVE',
|
|
||||||
hasSubMenu: false,
|
|
||||||
toState: frontendURL(`accounts/${accountId}/contacts/active`),
|
|
||||||
toStateName: 'contacts_dashboard_active',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export default {
|
|||||||
'markAsRead',
|
'markAsRead',
|
||||||
'assignPriority',
|
'assignPriority',
|
||||||
'updateConversationStatus',
|
'updateConversationStatus',
|
||||||
'deleteConversation',
|
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -238,10 +237,6 @@ export default {
|
|||||||
this.$emit('assignPriority', priority, this.chat.id);
|
this.$emit('assignPriority', priority, this.chat.id);
|
||||||
this.closeContextMenu();
|
this.closeContextMenu();
|
||||||
},
|
},
|
||||||
async deleteConversation() {
|
|
||||||
this.$emit('deleteConversation', this.chat.id);
|
|
||||||
this.closeContextMenu();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -368,7 +363,6 @@ export default {
|
|||||||
@mark-as-unread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
@mark-as-read="markAsRead"
|
@mark-as-read="markAsRead"
|
||||||
@assign-priority="assignPriority"
|
@assign-priority="assignPriority"
|
||||||
@delete-conversation="deleteConversation"
|
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import SLACardLabel from './components/SLACardLabel.vue';
|
|||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
import Linear from './linear/index.vue';
|
||||||
import { useInbox } from 'dashboard/composables/useInbox';
|
import { useInbox } from 'dashboard/composables/useInbox';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
@@ -34,6 +36,12 @@ const { isAWebWidgetInbox } = useInbox();
|
|||||||
|
|
||||||
const currentChat = computed(() => store.getters.getSelectedChat);
|
const currentChat = computed(() => store.getters.getSelectedChat);
|
||||||
const accountId = computed(() => store.getters.getCurrentAccountId);
|
const accountId = computed(() => store.getters.getCurrentAccountId);
|
||||||
|
const isFeatureEnabledonAccount = computed(
|
||||||
|
() => store.getters['accounts/isFeatureEnabledonAccount']
|
||||||
|
);
|
||||||
|
const appIntegrations = computed(
|
||||||
|
() => store.getters['integrations/getAppIntegrations']
|
||||||
|
);
|
||||||
|
|
||||||
const chatMetadata = computed(() => props.chat.meta);
|
const chatMetadata = computed(() => props.chat.meta);
|
||||||
|
|
||||||
@@ -84,15 +92,25 @@ const hasMultipleInboxes = computed(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
||||||
|
|
||||||
|
const isLinearIntegrationEnabled = computed(() =>
|
||||||
|
appIntegrations.value.find(
|
||||||
|
integration => integration.id === 'linear' && !!integration.hooks.length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLinearFeatureEnabled = computed(() =>
|
||||||
|
isFeatureEnabledonAccount.value(accountId.value, FEATURE_FLAGS.LINEAR)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="conversationHeader"
|
ref="conversationHeader"
|
||||||
class="flex flex-col gap-3 items-center justify-between flex-1 w-full min-w-0 xl:flex-row px-3 py-2 border-b bg-n-background border-n-weak h-24 xl:h-12"
|
class="flex flex-col items-center justify-center flex-1 w-full min-w-0 xl:flex-row px-3 py-2 border-b bg-n-background border-n-weak h-24 xl:h-12"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-start w-full xl:w-auto max-w-full min-w-0 xl:flex-1"
|
class="flex items-center justify-start w-full xl:w-auto max-w-full min-w-0"
|
||||||
>
|
>
|
||||||
<BackButton
|
<BackButton
|
||||||
v-if="showBackButton"
|
v-if="showBackButton"
|
||||||
@@ -104,12 +122,11 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
|||||||
:username="currentContact.name"
|
:username="currentContact.name"
|
||||||
:status="currentContact.availability_status"
|
:status="currentContact.availability_status"
|
||||||
size="32px"
|
size="32px"
|
||||||
class="flex-shrink-0"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2"
|
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center max-w-full gap-1 p-0 m-0">
|
<div class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-medium truncate leading-tight text-n-slate-12"
|
class="text-sm font-medium truncate leading-tight text-n-slate-12"
|
||||||
>
|
>
|
||||||
@@ -119,7 +136,7 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
|||||||
v-if="!isHMACVerified"
|
v-if="!isHMACVerified"
|
||||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||||
size="14"
|
size="14"
|
||||||
class="text-n-amber-10 my-0 mx-0 min-w-[14px] flex-shrink-0"
|
class="text-n-amber-10 my-0 mx-0 min-w-[14px]"
|
||||||
icon="warning"
|
icon="warning"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,14 +152,20 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center justify-start xl:justify-end flex-shrink-0 gap-2 w-full xl:w-auto header-actions-wrap"
|
class="flex flex-row items-center justify-start xl:justify-end flex-grow gap-2 w-full xl:w-auto mt-3 header-actions-wrap xl:mt-0"
|
||||||
>
|
>
|
||||||
<SLACardLabel
|
<SLACardLabel
|
||||||
v-if="hasSlaPolicyId"
|
v-if="hasSlaPolicyId"
|
||||||
:chat="chat"
|
:chat="chat"
|
||||||
show-extended-info
|
show-extended-info
|
||||||
:parent-width="width"
|
:parent-width="width"
|
||||||
class="hidden md:flex"
|
class="hidden lg:flex"
|
||||||
|
/>
|
||||||
|
<Linear
|
||||||
|
v-if="isLinearIntegrationEnabled && isLinearFeatureEnabled"
|
||||||
|
:conversation-id="currentChat.id"
|
||||||
|
:parent-width="width"
|
||||||
|
class="hidden lg:flex"
|
||||||
/>
|
/>
|
||||||
<MoreActions :conversation-id="currentChat.id" />
|
<MoreActions :conversation-id="currentChat.id" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,43 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import CopilotContainer from '../../copilot/CopilotContainer.vue';
|
||||||
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
currentChat: {
|
currentChat: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const channelType = computed(() => props.currentChat?.meta?.channel || '');
|
||||||
|
|
||||||
|
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||||
|
const isFeatureEnabledonAccount = useMapGetter(
|
||||||
|
'accounts/isFeatureEnabledonAccount'
|
||||||
|
);
|
||||||
|
|
||||||
|
const showCopilotTab = computed(() =>
|
||||||
|
isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN)
|
||||||
|
);
|
||||||
|
|
||||||
const { uiSettings } = useUISettings();
|
const { uiSettings } = useUISettings();
|
||||||
|
|
||||||
const activeTab = computed(() => {
|
const activeTab = computed(() => {
|
||||||
const { is_contact_sidebar_open: isContactSidebarOpen } = uiSettings.value;
|
const {
|
||||||
|
is_contact_sidebar_open: isContactSidebarOpen,
|
||||||
|
is_copilot_panel_open: isCopilotPanelOpen,
|
||||||
|
} = uiSettings.value;
|
||||||
|
|
||||||
if (isContactSidebarOpen) {
|
if (isContactSidebarOpen) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (isCopilotPanelOpen) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -32,6 +52,13 @@ const activeTab = computed(() => {
|
|||||||
:conversation-id="currentChat.id"
|
:conversation-id="currentChat.id"
|
||||||
:inbox-id="currentChat.inbox_id"
|
:inbox-id="currentChat.inbox_id"
|
||||||
/>
|
/>
|
||||||
|
<CopilotContainer
|
||||||
|
v-show="activeTab === 1 && showCopilotTab"
|
||||||
|
:key="currentChat.id"
|
||||||
|
:conversation-inbox-type="channelType"
|
||||||
|
:conversation-id="currentChat.id"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+1
-1
@@ -134,7 +134,7 @@ onUnmounted(() => {
|
|||||||
<SLAPopoverCard
|
<SLAPopoverCard
|
||||||
v-if="showSlaPopoverCard"
|
v-if="showSlaPopoverCard"
|
||||||
:sla-missed-events="slaEvents"
|
:sla-missed-events="slaEvents"
|
||||||
class="start-0 xl:start-auto xl:end-0 top-7 hidden group-hover:flex"
|
class="rtl:left-0 ltr:right-0 top-7 hidden group-hover:flex"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import MenuItem from './menuItem.vue';
|
|||||||
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
||||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -46,14 +45,7 @@ export default {
|
|||||||
'assignAgent',
|
'assignAgent',
|
||||||
'assignTeam',
|
'assignTeam',
|
||||||
'assignLabel',
|
'assignLabel',
|
||||||
'deleteConversation',
|
|
||||||
],
|
],
|
||||||
setup() {
|
|
||||||
const { isAdmin } = useAdmin();
|
|
||||||
return {
|
|
||||||
isAdmin,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||||
@@ -129,11 +121,6 @@ export default {
|
|||||||
icon: 'people-team-add',
|
icon: 'people-team-add',
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_TEAM'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_TEAM'),
|
||||||
},
|
},
|
||||||
deleteOption: {
|
|
||||||
key: 'delete',
|
|
||||||
icon: 'delete',
|
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.DELETE'),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -191,9 +178,6 @@ export default {
|
|||||||
assignPriority(priority) {
|
assignPriority(priority) {
|
||||||
this.$emit('assignPriority', priority);
|
this.$emit('assignPriority', priority);
|
||||||
},
|
},
|
||||||
deleteConversation() {
|
|
||||||
this.$emit('deleteConversation', this.chatId);
|
|
||||||
},
|
|
||||||
show(key) {
|
show(key) {
|
||||||
// If the conversation status is same as the action, then don't display the option
|
// If the conversation status is same as the action, then don't display the option
|
||||||
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
||||||
@@ -293,13 +277,5 @@ export default {
|
|||||||
@click.stop="$emit('assignTeam', team)"
|
@click.stop="$emit('assignTeam', team)"
|
||||||
/>
|
/>
|
||||||
</MenuItemWithSubmenu>
|
</MenuItemWithSubmenu>
|
||||||
<template v-if="isAdmin">
|
|
||||||
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
|
||||||
<MenuItem
|
|
||||||
:option="deleteOption"
|
|
||||||
variant="icon"
|
|
||||||
@click.stop="deleteConversation"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||||
|
import IssueHeader from './IssueHeader.vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
issue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
linkId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['unlinkIssue']);
|
||||||
|
|
||||||
|
const priorityMap = {
|
||||||
|
1: 'Urgent',
|
||||||
|
2: 'High',
|
||||||
|
3: 'Medium',
|
||||||
|
4: 'Low',
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
const { createdAt } = props.issue;
|
||||||
|
return format(new Date(createdAt), 'hh:mm a, MMM dd');
|
||||||
|
});
|
||||||
|
|
||||||
|
const assignee = computed(() => {
|
||||||
|
const assigneeDetails = props.issue.assignee;
|
||||||
|
|
||||||
|
if (!assigneeDetails) return null;
|
||||||
|
const { name, avatarUrl } = assigneeDetails;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
thumbnail: avatarUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = computed(() => {
|
||||||
|
return props.issue.labels?.nodes || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const priorityLabel = computed(() => {
|
||||||
|
return priorityMap[props.issue.priority];
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlinkIssue = () => {
|
||||||
|
emit('unlinkIssue', props.linkId);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute flex flex-col items-start bg-n-alpha-3 backdrop-blur-[100px] z-50 px-4 py-3 border border-solid border-n-container w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<IssueHeader
|
||||||
|
:identifier="issue.identifier"
|
||||||
|
:link-id="linkId"
|
||||||
|
:issue-url="issue.url"
|
||||||
|
@unlink-issue="unlinkIssue"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="mt-2 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ issue.title }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="issue.description"
|
||||||
|
class="mt-1 text-sm text-n-slate-11 line-clamp-3"
|
||||||
|
>
|
||||||
|
{{ issue.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center h-6 gap-2">
|
||||||
|
<UserAvatarWithName v-if="assignee" :user="assignee" class="py-1" />
|
||||||
|
<div v-if="assignee" class="w-px h-3 bg-n-slate-4" />
|
||||||
|
<div class="flex items-center gap-1 py-1">
|
||||||
|
<fluent-icon
|
||||||
|
icon="status"
|
||||||
|
size="14"
|
||||||
|
:style="{ color: issue.state.color }"
|
||||||
|
/>
|
||||||
|
<h6 class="text-xs text-n-slate-12">
|
||||||
|
{{ issue.state.name }}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div v-if="priorityLabel" class="w-px h-3 bg-n-slate-4" />
|
||||||
|
<div v-if="priorityLabel" class="flex items-center gap-1 py-1">
|
||||||
|
<fluent-icon
|
||||||
|
:icon="`priority-${priorityLabel.toLowerCase()}`"
|
||||||
|
size="14"
|
||||||
|
view-box="0 0 12 12"
|
||||||
|
/>
|
||||||
|
<h6 class="text-xs text-n-slate-12">{{ priorityLabel }}</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="labels.length" class="flex flex-wrap items-center gap-1">
|
||||||
|
<woot-label
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.id"
|
||||||
|
:title="label.name"
|
||||||
|
:description="label.description"
|
||||||
|
:color="label.color"
|
||||||
|
variant="smooth"
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-xs text-n-slate-11">
|
||||||
|
{{
|
||||||
|
$t('INTEGRATION_SETTINGS.LINEAR.ISSUE.CREATED_AT', {
|
||||||
|
createdAt: formattedDate,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -14,6 +15,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['unlinkIssue']);
|
const emit = defineEmits(['unlinkIssue']);
|
||||||
|
|
||||||
|
const isUnlinking = inject('isUnlinking');
|
||||||
|
|
||||||
const unlinkIssue = () => {
|
const unlinkIssue = () => {
|
||||||
emit('unlinkIssue');
|
emit('unlinkIssue');
|
||||||
};
|
};
|
||||||
@@ -24,33 +27,36 @@ const openIssue = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-2 py-1.5 border rounded-lg border-n-strong"
|
class="flex items-center justify-center gap-1 h-[24px] px-2 py-1 border rounded-lg border-ash-200"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1">
|
<fluent-icon
|
||||||
<fluent-icon
|
icon="linear"
|
||||||
icon="linear"
|
size="19"
|
||||||
size="16"
|
class="text-[#5E6AD2]"
|
||||||
class="text-[#5E6AD2]"
|
view-box="0 0 19 19"
|
||||||
view-box="0 0 19 19"
|
/>
|
||||||
/>
|
<span class="text-xs font-medium text-ash-900">{{ identifier }}</span>
|
||||||
<span class="text-xs font-medium text-n-slate-12">
|
</div>
|
||||||
{{ identifier }}
|
<div class="flex items-center gap-1">
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="w-px h-3 text-n-weak bg-n-weak" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
link
|
ghost
|
||||||
xs
|
xs
|
||||||
slate
|
slate
|
||||||
|
icon="i-lucide-unlink"
|
||||||
|
class="!transition-none"
|
||||||
|
:is-loading="isUnlinking"
|
||||||
|
@click="unlinkIssue"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
slate
|
||||||
|
class="!transition-none"
|
||||||
icon="i-lucide-arrow-up-right"
|
icon="i-lucide-arrow-up-right"
|
||||||
class="!size-4"
|
|
||||||
@click="openIssue"
|
@click="openIssue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button ghost xs slate icon="i-lucide-unlink" @click="unlinkIssue" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, ref, onMounted, watch } from 'vue';
|
|
||||||
import { useAlert, useTrack } from 'dashboard/composables';
|
|
||||||
import { useStoreGetters } from 'dashboard/composables/store';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
|
||||||
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
|
|
||||||
import LinearIssueItem from './LinearIssueItem.vue';
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
|
||||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
|
||||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|
||||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
conversationId: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const getters = useStoreGetters();
|
|
||||||
|
|
||||||
const linkedIssues = ref([]);
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const shouldShowCreateModal = ref(false);
|
|
||||||
|
|
||||||
const currentAccountId = getters.getCurrentAccountId;
|
|
||||||
|
|
||||||
const conversation = computed(
|
|
||||||
() => getters.getConversationById.value(props.conversationId) || {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasIssues = computed(() => linkedIssues.value.length > 0);
|
|
||||||
|
|
||||||
const loadLinkedIssues = async () => {
|
|
||||||
isLoading.value = true;
|
|
||||||
linkedIssues.value = [];
|
|
||||||
try {
|
|
||||||
const response = await LinearAPI.getLinkedIssue(props.conversationId);
|
|
||||||
linkedIssues.value = response.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
// Silent fail - not critical for UX
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const unlinkIssue = async linkId => {
|
|
||||||
try {
|
|
||||||
await LinearAPI.unlinkIssue(linkId);
|
|
||||||
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
|
|
||||||
linkedIssues.value = linkedIssues.value.filter(
|
|
||||||
issue => issue.id !== linkId
|
|
||||||
);
|
|
||||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = parseLinearAPIErrorResponse(
|
|
||||||
error,
|
|
||||||
t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR')
|
|
||||||
);
|
|
||||||
useAlert(errorMessage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
|
||||||
shouldShowCreateModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeCreateModal = () => {
|
|
||||||
shouldShowCreateModal.value = false;
|
|
||||||
loadLinkedIssues();
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.conversationId,
|
|
||||||
() => {
|
|
||||||
loadLinkedIssues();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadLinkedIssues();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="px-4 pt-3 pb-2">
|
|
||||||
<NextButton
|
|
||||||
ghost
|
|
||||||
xs
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')"
|
|
||||||
@click="openCreateModal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isLoading" class="flex justify-center p-8">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="!hasIssues" class="flex justify-center p-4">
|
|
||||||
<p class="text-sm text-n-slate-11">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.NO_LINKED_ISSUES') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="max-h-[300px] overflow-y-auto">
|
|
||||||
<LinearIssueItem
|
|
||||||
v-for="linkedIssue in linkedIssues"
|
|
||||||
:key="linkedIssue.id"
|
|
||||||
class="pt-3 px-4 pb-4 border-b border-n-weak last:border-b-0"
|
|
||||||
:linked-issue="linkedIssue"
|
|
||||||
@unlink-issue="unlinkIssue"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<woot-modal
|
|
||||||
v-model:show="shouldShowCreateModal"
|
|
||||||
:on-close="closeCreateModal"
|
|
||||||
:close-on-backdrop-click="false"
|
|
||||||
class="!items-start [&>div]:!top-12 [&>div]:sticky"
|
|
||||||
>
|
|
||||||
<CreateOrLinkIssue
|
|
||||||
:conversation="conversation"
|
|
||||||
:account-id="currentAccountId"
|
|
||||||
@close="closeCreateModal"
|
|
||||||
/>
|
|
||||||
</woot-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
|
||||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
|
||||||
import CardPriorityIcon from 'dashboard/components-next/Conversation/ConversationCard/CardPriorityIcon.vue';
|
|
||||||
import IssueHeader from './IssueHeader.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
linkedIssue: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['unlinkIssue']);
|
|
||||||
|
|
||||||
const priorityMap = {
|
|
||||||
1: 'Urgent',
|
|
||||||
2: 'High',
|
|
||||||
3: 'Medium',
|
|
||||||
4: 'Low',
|
|
||||||
};
|
|
||||||
|
|
||||||
const issue = computed(() => props.linkedIssue.issue);
|
|
||||||
|
|
||||||
const assignee = computed(() => {
|
|
||||||
const assigneeDetails = issue.value.assignee;
|
|
||||||
if (!assigneeDetails) return null;
|
|
||||||
return {
|
|
||||||
name: assigneeDetails.name,
|
|
||||||
thumbnail: assigneeDetails.avatarUrl,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const labels = computed(() => issue.value.labels?.nodes || []);
|
|
||||||
|
|
||||||
const priorityLabel = computed(() => priorityMap[issue.value.priority]);
|
|
||||||
|
|
||||||
const unlinkIssue = () => {
|
|
||||||
emit('unlinkIssue', props.linkedIssue.id);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<IssueHeader
|
|
||||||
:identifier="issue.identifier"
|
|
||||||
:link-id="linkedIssue.id"
|
|
||||||
:issue-url="issue.url"
|
|
||||||
@unlink-issue="unlinkIssue"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h3 class="mt-2 text-sm font-medium text-n-slate-12">
|
|
||||||
{{ issue.title }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p
|
|
||||||
v-if="issue.description"
|
|
||||||
class="mt-1 text-sm text-n-slate-11 line-clamp-3"
|
|
||||||
>
|
|
||||||
{{ issue.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div v-if="assignee" class="flex items-center gap-1.5">
|
|
||||||
<Avatar :src="assignee.thumbnail" :name="assignee.name" :size="16" />
|
|
||||||
<span class="text-xs capitalize truncate text-n-slate-12">
|
|
||||||
{{ assignee.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="assignee" class="w-px h-3 bg-n-slate-4" />
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Icon
|
|
||||||
icon="i-lucide-activity"
|
|
||||||
class="size-4"
|
|
||||||
:style="{ color: issue.state?.color }"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-n-slate-12">
|
|
||||||
{{ issue.state?.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="priorityLabel" class="w-px h-3 bg-n-slate-4" />
|
|
||||||
|
|
||||||
<div v-if="priorityLabel" class="flex items-center gap-1.5">
|
|
||||||
<CardPriorityIcon :priority="priorityLabel.toLowerCase()" />
|
|
||||||
<span class="text-xs text-n-slate-12">
|
|
||||||
{{ priorityLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="labels.length" class="flex flex-wrap">
|
|
||||||
<woot-label
|
|
||||||
v-for="label in labels"
|
|
||||||
:key="label.id"
|
|
||||||
:title="label.name"
|
|
||||||
:description="label.description"
|
|
||||||
:color="label.color"
|
|
||||||
variant="smooth"
|
|
||||||
small
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useStoreGetters } from 'dashboard/composables/store';
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
|
||||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
|
||||||
|
|
||||||
const { isAdmin } = useAdmin();
|
|
||||||
const getters = useStoreGetters();
|
|
||||||
const accountId = getters.getCurrentAccountId;
|
|
||||||
|
|
||||||
const integrationId = 'linear';
|
|
||||||
|
|
||||||
const actionURL = computed(() =>
|
|
||||||
frontendURL(
|
|
||||||
`accounts/${accountId.value}/settings/integrations/${integrationId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const openLinearAccount = () => {
|
|
||||||
window.open(actionURL.value, '_blank');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col p-3">
|
|
||||||
<div class="w-12 h-12 mb-3">
|
|
||||||
<img
|
|
||||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
|
||||||
class="object-contain w-full h-full border rounded-md shadow-sm border-n-weak dark:hidden dark:bg-n-alpha-2"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
:src="`/dashboard/images/integrations/${integrationId}-dark.png`"
|
|
||||||
class="hidden object-contain w-full h-full border rounded-md shadow-sm border-n-weak dark:block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 mb-4">
|
|
||||||
<h3 class="mb-1.5 text-sm font-medium text-n-slate-12">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.TITLE') }}
|
|
||||||
</h3>
|
|
||||||
<p v-if="isAdmin" class="text-sm text-n-slate-11">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.DESCRIPTION') }}
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-sm text-n-slate-11">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.AGENT_DESCRIPTION') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NextButton v-if="isAdmin" faded slate @click="openLinearAccount">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.BUTTON_TEXT') }}
|
|
||||||
</NextButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, onMounted, watch, defineOptions, provide } from 'vue';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useStoreGetters } from 'dashboard/composables/store';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||||
|
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
|
||||||
|
import Issue from './Issue.vue';
|
||||||
|
import { useTrack } from 'dashboard/composables';
|
||||||
|
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
parentWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Linear',
|
||||||
|
});
|
||||||
|
|
||||||
|
const getters = useStoreGetters();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const linkedIssue = ref(null);
|
||||||
|
const shouldShow = ref(false);
|
||||||
|
const shouldShowPopup = ref(false);
|
||||||
|
const isUnlinking = ref(false);
|
||||||
|
|
||||||
|
provide('isUnlinking', isUnlinking);
|
||||||
|
|
||||||
|
const currentAccountId = getters.getCurrentAccountId;
|
||||||
|
|
||||||
|
const conversation = computed(() =>
|
||||||
|
getters.getConversationById.value(props.conversationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const tooltipText = computed(() => {
|
||||||
|
return linkedIssue.value === null
|
||||||
|
? t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadLinkedIssue = async () => {
|
||||||
|
linkedIssue.value = null;
|
||||||
|
try {
|
||||||
|
const response = await LinearAPI.getLinkedIssue(props.conversationId);
|
||||||
|
const issues = response.data;
|
||||||
|
linkedIssue.value = issues && issues.length ? issues[0] : null;
|
||||||
|
} catch (error) {
|
||||||
|
// We don't want to show an error message here, as it's not critical. When someone clicks on the Linear icon, we can inform them that the integration is disabled.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unlinkIssue = async linkId => {
|
||||||
|
try {
|
||||||
|
isUnlinking.value = true;
|
||||||
|
await LinearAPI.unlinkIssue(linkId);
|
||||||
|
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
|
||||||
|
linkedIssue.value = null;
|
||||||
|
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = parseLinearAPIErrorResponse(
|
||||||
|
error,
|
||||||
|
t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR')
|
||||||
|
);
|
||||||
|
useAlert(errorMessage);
|
||||||
|
} finally {
|
||||||
|
isUnlinking.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldShowIssueIdentifier = computed(() => {
|
||||||
|
if (!linkedIssue.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.parentWidth > 600;
|
||||||
|
});
|
||||||
|
|
||||||
|
const openIssue = () => {
|
||||||
|
if (!linkedIssue.value) shouldShowPopup.value = true;
|
||||||
|
shouldShow.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
shouldShowPopup.value = false;
|
||||||
|
loadLinkedIssue();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeIssue = () => {
|
||||||
|
shouldShow.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.conversationId,
|
||||||
|
() => {
|
||||||
|
loadLinkedIssue();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLinkedIssue();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative after:content-[''] after:h-5 after:bg-transparent after:top-5 after:w-full after:block after:absolute after:z-0"
|
||||||
|
:class="{ group: linkedIssue }"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-on-clickaway="closeIssue"
|
||||||
|
v-tooltip="tooltipText"
|
||||||
|
sm
|
||||||
|
ghost
|
||||||
|
slate
|
||||||
|
class="!gap-1 group-hover:bg-n-alpha-2"
|
||||||
|
@click="openIssue"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="linear"
|
||||||
|
size="19"
|
||||||
|
class="text-[#5E6AD2] flex-shrink-0"
|
||||||
|
view-box="0 0 19 19"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="shouldShowIssueIdentifier"
|
||||||
|
class="text-xs font-medium text-n-slate-11"
|
||||||
|
>
|
||||||
|
{{ linkedIssue.issue.identifier }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<Issue
|
||||||
|
v-if="linkedIssue"
|
||||||
|
:issue="linkedIssue.issue"
|
||||||
|
:link-id="linkedIssue.id"
|
||||||
|
class="absolute rtl:left-0 ltr:right-0 top-9 invisible group-hover:visible"
|
||||||
|
@unlink-issue="unlinkIssue"
|
||||||
|
/>
|
||||||
|
<woot-modal
|
||||||
|
v-model:show="shouldShowPopup"
|
||||||
|
:on-close="closePopup"
|
||||||
|
:close-on-backdrop-click="false"
|
||||||
|
class="!items-start [&>div]:!top-12 [&>div]:sticky"
|
||||||
|
>
|
||||||
|
<CreateOrLinkIssue
|
||||||
|
:conversation="conversation"
|
||||||
|
:account-id="currentAccountId"
|
||||||
|
@close="closePopup"
|
||||||
|
/>
|
||||||
|
</woot-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -45,10 +45,9 @@ export function useAccount() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAccount = async (data, options) => {
|
const updateAccount = async data => {
|
||||||
await store.dispatch('accounts/update', {
|
await store.dispatch('accounts/update', {
|
||||||
...data,
|
...data,
|
||||||
options,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
|||||||
{ name: 'contact_notes' },
|
{ name: 'contact_notes' },
|
||||||
{ name: 'previous_conversation' },
|
{ name: 'previous_conversation' },
|
||||||
{ name: 'conversation_participants' },
|
{ name: 'conversation_participants' },
|
||||||
{ name: 'linear_issues' },
|
|
||||||
{ name: 'shopify_orders' },
|
{ name: 'shopify_orders' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,6 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [
|
|||||||
'code',
|
'code',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const WIDGET_BUILDER_EDITOR_MENU_OPTIONS = [
|
|
||||||
'strong',
|
|
||||||
'em',
|
|
||||||
'link',
|
|
||||||
'undo',
|
|
||||||
'redo',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
||||||
{
|
{
|
||||||
name: 'Small',
|
name: 'Small',
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const translationKeys = {
|
|||||||
'teammember:create': `AUDIT_LOGS.TEAM_MEMBER.ADD`,
|
'teammember:create': `AUDIT_LOGS.TEAM_MEMBER.ADD`,
|
||||||
'teammember:destroy': `AUDIT_LOGS.TEAM_MEMBER.REMOVE`,
|
'teammember:destroy': `AUDIT_LOGS.TEAM_MEMBER.REMOVE`,
|
||||||
'account:update': `AUDIT_LOGS.ACCOUNT.EDIT`,
|
'account:update': `AUDIT_LOGS.ACCOUNT.EDIT`,
|
||||||
'conversation:destroy': `AUDIT_LOGS.CONVERSATION.DELETE`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function extractAttrChange(attrChange) {
|
function extractAttrChange(attrChange) {
|
||||||
@@ -169,11 +168,6 @@ export function generateTranslationPayload(auditLogItem, agentList) {
|
|||||||
const auditableType = auditLogItem.auditable_type.toLowerCase();
|
const auditableType = auditLogItem.auditable_type.toLowerCase();
|
||||||
const action = auditLogItem.action.toLowerCase();
|
const action = auditLogItem.action.toLowerCase();
|
||||||
|
|
||||||
if (auditableType === 'conversation' && action === 'destroy') {
|
|
||||||
translationPayload.id =
|
|
||||||
auditLogItem.audited_changes?.display_id || auditLogItem.auditable_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auditableType === 'accountuser') {
|
if (auditableType === 'accountuser') {
|
||||||
translationPayload = handleAccountUser(
|
translationPayload = handleAccountUser(
|
||||||
auditLogItem,
|
auditLogItem,
|
||||||
|
|||||||
@@ -59,13 +59,6 @@
|
|||||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ACCESS_TOKEN": {
|
|
||||||
"TITLE": "Access Token",
|
|
||||||
"DESCRIPTION": "Copy the access token and save it securely",
|
|
||||||
"COPY_SUCCESSFUL": "Access token copied to clipboard",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"AVATAR": {
|
"AVATAR": {
|
||||||
"LABEL": "Bot avatar"
|
"LABEL": "Bot avatar"
|
||||||
|
|||||||
@@ -69,9 +69,6 @@
|
|||||||
},
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||||
},
|
|
||||||
"CONVERSATION": {
|
|
||||||
"DELETE": "{agentName} deleted conversation #{id}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,6 @@
|
|||||||
"HEADER": {
|
"HEADER": {
|
||||||
"TITLE": "Contacts",
|
"TITLE": "Contacts",
|
||||||
"SEARCH_TITLE": "Search contacts",
|
"SEARCH_TITLE": "Search contacts",
|
||||||
"ACTIVE_TITLE": "Active contacts",
|
|
||||||
"SEARCH_PLACEHOLDER": "Search...",
|
"SEARCH_PLACEHOLDER": "Search...",
|
||||||
"MESSAGE_BUTTON": "Message",
|
"MESSAGE_BUTTON": "Message",
|
||||||
"SEND_MESSAGE": "Send message",
|
"SEND_MESSAGE": "Send message",
|
||||||
@@ -458,10 +457,6 @@
|
|||||||
"PLACEHOLDER": "Add Twitter"
|
"PLACEHOLDER": "Add Twitter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"DELETE_CONTACT": {
|
|
||||||
"MESSAGE": "This action is permanent and irreversible.",
|
|
||||||
"BUTTON": "Delete now"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DETAILS": {
|
"DETAILS": {
|
||||||
@@ -471,7 +466,7 @@
|
|||||||
"DELETE_CONTACT": "Delete contact",
|
"DELETE_CONTACT": "Delete contact",
|
||||||
"DELETE_DIALOG": {
|
"DELETE_DIALOG": {
|
||||||
"TITLE": "Confirm Deletion",
|
"TITLE": "Confirm Deletion",
|
||||||
"DESCRIPTION": "Are you sure you want to delete this contact?",
|
"DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
|
||||||
"CONFIRM": "Yes, Delete",
|
"CONFIRM": "Yes, Delete",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||||
@@ -560,8 +555,7 @@
|
|||||||
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
||||||
"BUTTON_LABEL": "Add contact",
|
"BUTTON_LABEL": "Add contact",
|
||||||
"SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search 🔍",
|
"SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search 🔍",
|
||||||
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
|
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋"
|
||||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"COMPOSE_NEW_CONVERSATION": {
|
"COMPOSE_NEW_CONVERSATION": {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"RESOLVE_ACTION": "Resolve",
|
"RESOLVE_ACTION": "Resolve",
|
||||||
"REOPEN_ACTION": "Reopen",
|
"REOPEN_ACTION": "Reopen",
|
||||||
"OPEN_ACTION": "Open",
|
"OPEN_ACTION": "Open",
|
||||||
"MORE_ACTIONS": "More actions",
|
|
||||||
"OPEN": "More",
|
"OPEN": "More",
|
||||||
"CLOSE": "Close",
|
"CLOSE": "Close",
|
||||||
"DETAILS": "details",
|
"DETAILS": "details",
|
||||||
@@ -118,11 +117,6 @@
|
|||||||
"FAILED": "Couldn't change priority. Please try again."
|
"FAILED": "Couldn't change priority. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE_CONVERSATION": {
|
|
||||||
"TITLE": "Delete conversation #{conversationId}",
|
|
||||||
"DESCRIPTION": "Are you sure you want to delete this conversation?",
|
|
||||||
"CONFIRM": "Delete"
|
|
||||||
},
|
|
||||||
"CARD_CONTEXT_MENU": {
|
"CARD_CONTEXT_MENU": {
|
||||||
"PENDING": "Mark as pending",
|
"PENDING": "Mark as pending",
|
||||||
"RESOLVED": "Mark as resolved",
|
"RESOLVED": "Mark as resolved",
|
||||||
@@ -139,7 +133,6 @@
|
|||||||
"ASSIGN_LABEL": "Assign label",
|
"ASSIGN_LABEL": "Assign label",
|
||||||
"AGENTS_LOADING": "Loading agents...",
|
"AGENTS_LOADING": "Loading agents...",
|
||||||
"ASSIGN_TEAM": "Assign team",
|
"ASSIGN_TEAM": "Assign team",
|
||||||
"DELETE": "Delete conversation",
|
|
||||||
"API": {
|
"API": {
|
||||||
"AGENT_ASSIGNMENT": {
|
"AGENT_ASSIGNMENT": {
|
||||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||||
@@ -214,8 +207,6 @@
|
|||||||
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
||||||
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
||||||
"CHANGE_TEAM": "Conversation team changed",
|
"CHANGE_TEAM": "Conversation team changed",
|
||||||
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
|
|
||||||
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
|
|
||||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
|
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
|
||||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||||
"SENT_BY": "Sent by:",
|
"SENT_BY": "Sent by:",
|
||||||
@@ -308,7 +299,6 @@
|
|||||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||||
"MACROS": "Macros",
|
"MACROS": "Macros",
|
||||||
"LINEAR_ISSUES": "Linked Linear Issues",
|
|
||||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||||
},
|
},
|
||||||
"SHOPIFY": {
|
"SHOPIFY": {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"PHONE_INPUT": {
|
"PHONE_INPUT": {
|
||||||
"PLACEHOLDER": "Search",
|
"PLACEHOLDER": "Search",
|
||||||
"EMPTY_STATE": "No results found"
|
"EMPTY_STATE": "No results found"
|
||||||
},
|
}
|
||||||
"CLOSE": "Close"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,31 +46,7 @@
|
|||||||
},
|
},
|
||||||
"AUTO_RESOLVE": {
|
"AUTO_RESOLVE": {
|
||||||
"TITLE": "Auto-resolve conversations",
|
"TITLE": "Auto-resolve conversations",
|
||||||
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period of inactivity.",
|
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period. Set the duration and customize the message to the user below."
|
||||||
"DURATION": {
|
|
||||||
"LABEL": "Inactivity duration",
|
|
||||||
"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",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS": "Auto resolve settings updated successfully",
|
|
||||||
"ERROR": "Failed to update auto resolve settings"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"PREFERENCES": "Preferences",
|
|
||||||
"LABEL": {
|
|
||||||
"LABEL": "Add label after auto-resolution",
|
|
||||||
"PLACEHOLDER": "Select a label"
|
|
||||||
},
|
|
||||||
"IGNORE_WAITING": {
|
|
||||||
"LABEL": "Skip conversations waiting for agent’s reply"
|
|
||||||
},
|
|
||||||
"UPDATE_BUTTON": "Save Changes"
|
|
||||||
},
|
},
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Account name",
|
"LABEL": "Account name",
|
||||||
@@ -94,15 +70,7 @@
|
|||||||
},
|
},
|
||||||
"AUTO_RESOLVE_IGNORE_WAITING": {
|
"AUTO_RESOLVE_IGNORE_WAITING": {
|
||||||
"LABEL": "Exclude unattended conversations",
|
"LABEL": "Exclude unattended conversations",
|
||||||
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent's reply."
|
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent’s reply."
|
||||||
},
|
|
||||||
"AUDIO_TRANSCRIPTION": {
|
|
||||||
"TITLE": "Transcribe Audio Messages",
|
|
||||||
"NOTE": "Automatically transcribe audio messages in conversations. Generate a text transcript whenever an audio message is sent or received, and display it alongside the message.",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS": "Audio transcription setting updated successfully",
|
|
||||||
"ERROR": "Failed to update audio transcription setting"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"AUTO_RESOLVE_DURATION": {
|
"AUTO_RESOLVE_DURATION": {
|
||||||
"LABEL": "Inactivity duration for resolution",
|
"LABEL": "Inactivity duration for resolution",
|
||||||
|
|||||||
@@ -318,18 +318,11 @@
|
|||||||
"SUCCESS": "Issue unlinked successfully",
|
"SUCCESS": "Issue unlinked successfully",
|
||||||
"ERROR": "There was an error unlinking the issue, please try again"
|
"ERROR": "There was an error unlinking the issue, please try again"
|
||||||
},
|
},
|
||||||
"NO_LINKED_ISSUES": "No linked issues found",
|
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": "Are you sure you want to delete the integration?",
|
"TITLE": "Are you sure you want to delete the integration?",
|
||||||
"MESSAGE": "Are you sure you want to delete the integration?",
|
"MESSAGE": "Are you sure you want to delete the integration?",
|
||||||
"CONFIRM": "Yes, delete",
|
"CONFIRM": "Yes, delete",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
},
|
|
||||||
"CTA": {
|
|
||||||
"TITLE": "Connect to Linear",
|
|
||||||
"AGENT_DESCRIPTION": "Linear workspace is not connected. Request your administrator to connect a workspace to use this integration.",
|
|
||||||
"DESCRIPTION": "Linear workspace is not connected. Click the button below to connect your workspace to use this integration.",
|
|
||||||
"BUTTON_TEXT": "Connect Linear workspace"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -337,17 +330,12 @@
|
|||||||
"NAME": "Captain",
|
"NAME": "Captain",
|
||||||
"HEADER_KNOW_MORE": "Know more",
|
"HEADER_KNOW_MORE": "Know more",
|
||||||
"COPILOT": {
|
"COPILOT": {
|
||||||
"TITLE": "Copilot",
|
|
||||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
|
||||||
"PANEL_TITLE": "Get started with Copilot",
|
|
||||||
"KICK_OFF_MESSAGE": "Need a quick summary, want to check past conversations, or draft a better reply? Copilot’s here to speed things up.",
|
|
||||||
"SEND_MESSAGE": "Send message...",
|
"SEND_MESSAGE": "Send message...",
|
||||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||||
"LOADER": "Captain is thinking",
|
"LOADER": "Captain is thinking",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"USE": "Use this",
|
"USE": "Use this",
|
||||||
"RESET": "Reset",
|
"RESET": "Reset",
|
||||||
"SHOW_STEPS": "Show steps",
|
|
||||||
"SELECT_ASSISTANT": "Select Assistant",
|
"SELECT_ASSISTANT": "Select Assistant",
|
||||||
"PROMPTS": {
|
"PROMPTS": {
|
||||||
"SUMMARIZE": {
|
"SUMMARIZE": {
|
||||||
@@ -361,14 +349,6 @@
|
|||||||
"RATE": {
|
"RATE": {
|
||||||
"LABEL": "Rate this conversation",
|
"LABEL": "Rate this conversation",
|
||||||
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
||||||
},
|
|
||||||
"HIGH_PRIORITY": {
|
|
||||||
"LABEL": "High priority conversations",
|
|
||||||
"CONTENT": "Give me a summary of all high priority open conversations. Include the conversation ID, customer name (if available), last message content, and assigned agent. Group by status if relevant."
|
|
||||||
},
|
|
||||||
"LIST_CONTACTS": {
|
|
||||||
"LABEL": "List contacts",
|
|
||||||
"CONTENT": "Show me the list of top 10 contacts. Include name, email or phone number (if available), last seen time, tags (if any)."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -388,6 +368,7 @@
|
|||||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||||
},
|
},
|
||||||
"ENTERPRISE_PAYWALL": {
|
"ENTERPRISE_PAYWALL": {
|
||||||
|
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
|
||||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
||||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||||
},
|
},
|
||||||
@@ -402,7 +383,6 @@
|
|||||||
},
|
},
|
||||||
"ASSISTANTS": {
|
"ASSISTANTS": {
|
||||||
"HEADER": "Assistants",
|
"HEADER": "Assistants",
|
||||||
"NO_ASSISTANTS_AVAILABLE": "There are no assistants available in your account.",
|
|
||||||
"ADD_NEW": "Create a new assistant",
|
"ADD_NEW": "Create a new assistant",
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": "Are you sure to delete the assistant?",
|
"TITLE": "Are you sure to delete the assistant?",
|
||||||
@@ -431,10 +411,6 @@
|
|||||||
"PLACEHOLDER": "Enter assistant name",
|
"PLACEHOLDER": "Enter assistant name",
|
||||||
"ERROR": "The name is required"
|
"ERROR": "The name is required"
|
||||||
},
|
},
|
||||||
"TEMPERATURE": {
|
|
||||||
"LABEL": "Response Temperature",
|
|
||||||
"DESCRIPTION": "Adjust how creative or restrictive the assistant's responses should be. Lower values produce more focused and deterministic responses, while higher values allow for more creative and varied outputs."
|
|
||||||
},
|
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"LABEL": "Description",
|
"LABEL": "Description",
|
||||||
"PLACEHOLDER": "Enter assistant description",
|
"PLACEHOLDER": "Enter assistant description",
|
||||||
|
|||||||
@@ -193,7 +193,6 @@
|
|||||||
},
|
},
|
||||||
"LABEL_REPORTS": {
|
"LABEL_REPORTS": {
|
||||||
"HEADER": "Labels Overview",
|
"HEADER": "Labels Overview",
|
||||||
"DESCRIPTION": "Track label performance with key metrics including conversations, response times, resolution times, and resolved cases. Click a label name for detailed insights.",
|
|
||||||
"LOADING_CHART": "Loading chart data...",
|
"LOADING_CHART": "Loading chart data...",
|
||||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||||
@@ -560,7 +559,6 @@
|
|||||||
"INBOX": "Inbox",
|
"INBOX": "Inbox",
|
||||||
"AGENT": "Agent",
|
"AGENT": "Agent",
|
||||||
"TEAM": "Team",
|
"TEAM": "Team",
|
||||||
"LABEL": "Label",
|
|
||||||
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
||||||
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
||||||
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"MESSAGES": "Messages",
|
"MESSAGES": "Messages"
|
||||||
"ARTICLES": "Articles"
|
|
||||||
},
|
},
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"MESSAGES": "Messages",
|
"MESSAGES": "Messages"
|
||||||
"ARTICLES": "Articles"
|
|
||||||
},
|
},
|
||||||
"VIEW_MORE": "View more",
|
"VIEW_MORE": "View more",
|
||||||
"LOAD_MORE": "Load more",
|
"LOAD_MORE": "Load more",
|
||||||
|
|||||||
@@ -76,12 +76,7 @@
|
|||||||
"ACCESS_TOKEN": {
|
"ACCESS_TOKEN": {
|
||||||
"TITLE": "Access Token",
|
"TITLE": "Access Token",
|
||||||
"NOTE": "This token can be used if you are building an API based integration",
|
"NOTE": "This token can be used if you are building an API based integration",
|
||||||
"COPY": "Copy",
|
"COPY": "Copy"
|
||||||
"RESET": "Reset",
|
|
||||||
"CONFIRM_RESET": "Are you sure?",
|
|
||||||
"CONFIRM_HINT": "Click again to confirm",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
},
|
||||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Audio Alerts",
|
"TITLE": "Audio Alerts",
|
||||||
@@ -190,8 +185,7 @@
|
|||||||
"OFFLINE": "Offline"
|
"OFFLINE": "Offline"
|
||||||
},
|
},
|
||||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again",
|
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||||
"IMPERSONATING_ERROR": "Cannot change availability while impersonating a user"
|
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Your email address",
|
"LABEL": "Your email address",
|
||||||
@@ -286,7 +280,6 @@
|
|||||||
"REPORTS": "Reports",
|
"REPORTS": "Reports",
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"ACTIVE": "Active",
|
|
||||||
"CAPTAIN": "Captain",
|
"CAPTAIN": "Captain",
|
||||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||||
"CAPTAIN_DOCUMENTS": "Documents",
|
"CAPTAIN_DOCUMENTS": "Documents",
|
||||||
|
|||||||
@@ -59,13 +59,6 @@
|
|||||||
"ERROR_MESSAGE": "تعذر تحديث الروبوت. يرجى المحاولة مرة أخرى."
|
"ERROR_MESSAGE": "تعذر تحديث الروبوت. يرجى المحاولة مرة أخرى."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ACCESS_TOKEN": {
|
|
||||||
"TITLE": "رمز المصادقة",
|
|
||||||
"DESCRIPTION": "Copy the access token and save it securely",
|
|
||||||
"COPY_SUCCESSFUL": "Access token copied to clipboard",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"AVATAR": {
|
"AVATAR": {
|
||||||
"LABEL": "Bot avatar"
|
"LABEL": "Bot avatar"
|
||||||
|
|||||||
@@ -69,9 +69,6 @@
|
|||||||
},
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "{agentName} قام بتحديث إعدادات الحساب (##{id})"
|
"EDIT": "{agentName} قام بتحديث إعدادات الحساب (##{id})"
|
||||||
},
|
|
||||||
"CONVERSATION": {
|
|
||||||
"DELETE": "{agentName} deleted conversation #{id}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,6 @@
|
|||||||
"HEADER": {
|
"HEADER": {
|
||||||
"TITLE": "جهات الاتصال",
|
"TITLE": "جهات الاتصال",
|
||||||
"SEARCH_TITLE": "Search contacts",
|
"SEARCH_TITLE": "Search contacts",
|
||||||
"ACTIVE_TITLE": "Active contacts",
|
|
||||||
"SEARCH_PLACEHOLDER": "Search...",
|
"SEARCH_PLACEHOLDER": "Search...",
|
||||||
"MESSAGE_BUTTON": "رسالة",
|
"MESSAGE_BUTTON": "رسالة",
|
||||||
"SEND_MESSAGE": "إرسال الرسالة",
|
"SEND_MESSAGE": "إرسال الرسالة",
|
||||||
@@ -458,10 +457,6 @@
|
|||||||
"PLACEHOLDER": "Add Twitter"
|
"PLACEHOLDER": "Add Twitter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"DELETE_CONTACT": {
|
|
||||||
"MESSAGE": "This action is permanent and irreversible.",
|
|
||||||
"BUTTON": "Delete now"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DETAILS": {
|
"DETAILS": {
|
||||||
@@ -471,7 +466,7 @@
|
|||||||
"DELETE_CONTACT": "حذف جهة الاتصال",
|
"DELETE_CONTACT": "حذف جهة الاتصال",
|
||||||
"DELETE_DIALOG": {
|
"DELETE_DIALOG": {
|
||||||
"TITLE": "تأكيد الحذف",
|
"TITLE": "تأكيد الحذف",
|
||||||
"DESCRIPTION": "Are you sure you want to delete this contact?",
|
"DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
|
||||||
"CONFIRM": "نعم، احذف",
|
"CONFIRM": "نعم، احذف",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "تم حذف جهة الاتصال بنجاح",
|
"SUCCESS_MESSAGE": "تم حذف جهة الاتصال بنجاح",
|
||||||
@@ -560,8 +555,7 @@
|
|||||||
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
||||||
"BUTTON_LABEL": "Add contact",
|
"BUTTON_LABEL": "Add contact",
|
||||||
"SEARCH_EMPTY_STATE_TITLE": "لا توجد جهات اتصال تطابق بحثك 🔍",
|
"SEARCH_EMPTY_STATE_TITLE": "لا توجد جهات اتصال تطابق بحثك 🔍",
|
||||||
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
|
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋"
|
||||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"COMPOSE_NEW_CONVERSATION": {
|
"COMPOSE_NEW_CONVERSATION": {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"RESOLVE_ACTION": "حل المحادثة",
|
"RESOLVE_ACTION": "حل المحادثة",
|
||||||
"REOPEN_ACTION": "إعادة فتح",
|
"REOPEN_ACTION": "إعادة فتح",
|
||||||
"OPEN_ACTION": "فتح",
|
"OPEN_ACTION": "فتح",
|
||||||
"MORE_ACTIONS": "More actions",
|
|
||||||
"OPEN": "المزيد",
|
"OPEN": "المزيد",
|
||||||
"CLOSE": "أغلق",
|
"CLOSE": "أغلق",
|
||||||
"DETAILS": "التفاصيل",
|
"DETAILS": "التفاصيل",
|
||||||
@@ -118,11 +117,6 @@
|
|||||||
"FAILED": "تعذر تغيير الأولوية، الرجاء المحاولة مرة أخرى."
|
"FAILED": "تعذر تغيير الأولوية، الرجاء المحاولة مرة أخرى."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE_CONVERSATION": {
|
|
||||||
"TITLE": "Delete conversation #{conversationId}",
|
|
||||||
"DESCRIPTION": "Are you sure you want to delete this conversation?",
|
|
||||||
"CONFIRM": "حذف"
|
|
||||||
},
|
|
||||||
"CARD_CONTEXT_MENU": {
|
"CARD_CONTEXT_MENU": {
|
||||||
"PENDING": "تحديد كمعلق",
|
"PENDING": "تحديد كمعلق",
|
||||||
"RESOLVED": "تحديد كمحلولة",
|
"RESOLVED": "تحديد كمحلولة",
|
||||||
@@ -139,7 +133,6 @@
|
|||||||
"ASSIGN_LABEL": "إضافة وسم",
|
"ASSIGN_LABEL": "إضافة وسم",
|
||||||
"AGENTS_LOADING": "جاري جلب الوكلاء...",
|
"AGENTS_LOADING": "جاري جلب الوكلاء...",
|
||||||
"ASSIGN_TEAM": "تعيين فريق",
|
"ASSIGN_TEAM": "تعيين فريق",
|
||||||
"DELETE": "Delete conversation",
|
|
||||||
"API": {
|
"API": {
|
||||||
"AGENT_ASSIGNMENT": {
|
"AGENT_ASSIGNMENT": {
|
||||||
"SUCCESFUL": "معرف المحادثة {conversationId} تم تعيينه لـ \"{agentName}\"",
|
"SUCCESFUL": "معرف المحادثة {conversationId} تم تعيينه لـ \"{agentName}\"",
|
||||||
@@ -214,8 +207,6 @@
|
|||||||
"ASSIGN_LABEL_SUCCESFUL": "تم تعيين الوسم بنجاح",
|
"ASSIGN_LABEL_SUCCESFUL": "تم تعيين الوسم بنجاح",
|
||||||
"ASSIGN_LABEL_FAILED": "فشل تعيين الوسم",
|
"ASSIGN_LABEL_FAILED": "فشل تعيين الوسم",
|
||||||
"CHANGE_TEAM": "تم تغيير فريق المحادثة",
|
"CHANGE_TEAM": "تم تغيير فريق المحادثة",
|
||||||
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
|
|
||||||
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
|
|
||||||
"FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE}",
|
"FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE}",
|
||||||
"MESSAGE_ERROR": "غير قادر على إرسال هذه الرسالة، الرجاء المحاولة مرة أخرى لاحقاً",
|
"MESSAGE_ERROR": "غير قادر على إرسال هذه الرسالة، الرجاء المحاولة مرة أخرى لاحقاً",
|
||||||
"SENT_BY": "أرسلت بواسطة:",
|
"SENT_BY": "أرسلت بواسطة:",
|
||||||
@@ -308,7 +299,6 @@
|
|||||||
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
|
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
|
||||||
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
|
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
|
||||||
"MACROS": "ماكروس",
|
"MACROS": "ماكروس",
|
||||||
"LINEAR_ISSUES": "Linked Linear Issues",
|
|
||||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||||
},
|
},
|
||||||
"SHOPIFY": {
|
"SHOPIFY": {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"PHONE_INPUT": {
|
"PHONE_INPUT": {
|
||||||
"PLACEHOLDER": "بحث",
|
"PLACEHOLDER": "بحث",
|
||||||
"EMPTY_STATE": "لم يتم العثور على النتائج"
|
"EMPTY_STATE": "لم يتم العثور على النتائج"
|
||||||
},
|
}
|
||||||
"CLOSE": "أغلق"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,31 +46,7 @@
|
|||||||
},
|
},
|
||||||
"AUTO_RESOLVE": {
|
"AUTO_RESOLVE": {
|
||||||
"TITLE": "Auto-resolve conversations",
|
"TITLE": "Auto-resolve conversations",
|
||||||
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period of inactivity.",
|
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period. Set the duration and customize the message to the user below."
|
||||||
"DURATION": {
|
|
||||||
"LABEL": "Inactivity duration",
|
|
||||||
"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",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS": "Auto resolve settings updated successfully",
|
|
||||||
"ERROR": "Failed to update auto resolve settings"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"PREFERENCES": "التفضيلات",
|
|
||||||
"LABEL": {
|
|
||||||
"LABEL": "Add label after auto-resolution",
|
|
||||||
"PLACEHOLDER": "Select a label"
|
|
||||||
},
|
|
||||||
"IGNORE_WAITING": {
|
|
||||||
"LABEL": "Skip conversations waiting for agent’s reply"
|
|
||||||
},
|
|
||||||
"UPDATE_BUTTON": "Save Changes"
|
|
||||||
},
|
},
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "اسم الحساب",
|
"LABEL": "اسم الحساب",
|
||||||
@@ -94,15 +70,7 @@
|
|||||||
},
|
},
|
||||||
"AUTO_RESOLVE_IGNORE_WAITING": {
|
"AUTO_RESOLVE_IGNORE_WAITING": {
|
||||||
"LABEL": "Exclude unattended conversations",
|
"LABEL": "Exclude unattended conversations",
|
||||||
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent's reply."
|
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent’s reply."
|
||||||
},
|
|
||||||
"AUDIO_TRANSCRIPTION": {
|
|
||||||
"TITLE": "Transcribe Audio Messages",
|
|
||||||
"NOTE": "Automatically transcribe audio messages in conversations. Generate a text transcript whenever an audio message is sent or received, and display it alongside the message.",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS": "Audio transcription setting updated successfully",
|
|
||||||
"ERROR": "Failed to update audio transcription setting"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"AUTO_RESOLVE_DURATION": {
|
"AUTO_RESOLVE_DURATION": {
|
||||||
"LABEL": "Inactivity duration for resolution",
|
"LABEL": "Inactivity duration for resolution",
|
||||||
|
|||||||
@@ -318,18 +318,11 @@
|
|||||||
"SUCCESS": "تم إلغاء ربط المشكلة بنجاح",
|
"SUCCESS": "تم إلغاء ربط المشكلة بنجاح",
|
||||||
"ERROR": "حدث خطأ أثناء إلغاء ربط المشكلة، الرجاء المحاولة مرة أخرى"
|
"ERROR": "حدث خطأ أثناء إلغاء ربط المشكلة، الرجاء المحاولة مرة أخرى"
|
||||||
},
|
},
|
||||||
"NO_LINKED_ISSUES": "No linked issues found",
|
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": "Are you sure you want to delete the integration?",
|
"TITLE": "Are you sure you want to delete the integration?",
|
||||||
"MESSAGE": "Are you sure you want to delete the integration?",
|
"MESSAGE": "Are you sure you want to delete the integration?",
|
||||||
"CONFIRM": "نعم، احذف",
|
"CONFIRM": "نعم، احذف",
|
||||||
"CANCEL": "إلغاء"
|
"CANCEL": "إلغاء"
|
||||||
},
|
|
||||||
"CTA": {
|
|
||||||
"TITLE": "Connect to Linear",
|
|
||||||
"AGENT_DESCRIPTION": "Linear workspace is not connected. Request your administrator to connect a workspace to use this integration.",
|
|
||||||
"DESCRIPTION": "Linear workspace is not connected. Click the button below to connect your workspace to use this integration.",
|
|
||||||
"BUTTON_TEXT": "Connect Linear workspace"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -337,17 +330,12 @@
|
|||||||
"NAME": "قائد",
|
"NAME": "قائد",
|
||||||
"HEADER_KNOW_MORE": "Know more",
|
"HEADER_KNOW_MORE": "Know more",
|
||||||
"COPILOT": {
|
"COPILOT": {
|
||||||
"TITLE": "Copilot",
|
|
||||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
|
||||||
"PANEL_TITLE": "Get started with Copilot",
|
|
||||||
"KICK_OFF_MESSAGE": "Need a quick summary, want to check past conversations, or draft a better reply? Copilot’s here to speed things up.",
|
|
||||||
"SEND_MESSAGE": "إرسال الرسالة...",
|
"SEND_MESSAGE": "إرسال الرسالة...",
|
||||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||||
"LOADER": "Captain is thinking",
|
"LOADER": "Captain is thinking",
|
||||||
"YOU": "أنت",
|
"YOU": "أنت",
|
||||||
"USE": "Use this",
|
"USE": "Use this",
|
||||||
"RESET": "Reset",
|
"RESET": "Reset",
|
||||||
"SHOW_STEPS": "Show steps",
|
|
||||||
"SELECT_ASSISTANT": "Select Assistant",
|
"SELECT_ASSISTANT": "Select Assistant",
|
||||||
"PROMPTS": {
|
"PROMPTS": {
|
||||||
"SUMMARIZE": {
|
"SUMMARIZE": {
|
||||||
@@ -361,14 +349,6 @@
|
|||||||
"RATE": {
|
"RATE": {
|
||||||
"LABEL": "Rate this conversation",
|
"LABEL": "Rate this conversation",
|
||||||
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
||||||
},
|
|
||||||
"HIGH_PRIORITY": {
|
|
||||||
"LABEL": "High priority conversations",
|
|
||||||
"CONTENT": "Give me a summary of all high priority open conversations. Include the conversation ID, customer name (if available), last message content, and assigned agent. Group by status if relevant."
|
|
||||||
},
|
|
||||||
"LIST_CONTACTS": {
|
|
||||||
"LABEL": "List contacts",
|
|
||||||
"CONTENT": "Show me the list of top 10 contacts. Include name, email or phone number (if available), last seen time, tags (if any)."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -388,6 +368,7 @@
|
|||||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||||
},
|
},
|
||||||
"ENTERPRISE_PAYWALL": {
|
"ENTERPRISE_PAYWALL": {
|
||||||
|
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
|
||||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
||||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||||
},
|
},
|
||||||
@@ -402,7 +383,6 @@
|
|||||||
},
|
},
|
||||||
"ASSISTANTS": {
|
"ASSISTANTS": {
|
||||||
"HEADER": "Assistants",
|
"HEADER": "Assistants",
|
||||||
"NO_ASSISTANTS_AVAILABLE": "There are no assistants available in your account.",
|
|
||||||
"ADD_NEW": "Create a new assistant",
|
"ADD_NEW": "Create a new assistant",
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": "Are you sure to delete the assistant?",
|
"TITLE": "Are you sure to delete the assistant?",
|
||||||
@@ -431,10 +411,6 @@
|
|||||||
"PLACEHOLDER": "Enter assistant name",
|
"PLACEHOLDER": "Enter assistant name",
|
||||||
"ERROR": "The name is required"
|
"ERROR": "The name is required"
|
||||||
},
|
},
|
||||||
"TEMPERATURE": {
|
|
||||||
"LABEL": "Response Temperature",
|
|
||||||
"DESCRIPTION": "Adjust how creative or restrictive the assistant's responses should be. Lower values produce more focused and deterministic responses, while higher values allow for more creative and varied outputs."
|
|
||||||
},
|
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"LABEL": "الوصف",
|
"LABEL": "الوصف",
|
||||||
"PLACEHOLDER": "Enter assistant description",
|
"PLACEHOLDER": "Enter assistant description",
|
||||||
|
|||||||
@@ -193,7 +193,6 @@
|
|||||||
},
|
},
|
||||||
"LABEL_REPORTS": {
|
"LABEL_REPORTS": {
|
||||||
"HEADER": "نظرة عامة على التسميات",
|
"HEADER": "نظرة عامة على التسميات",
|
||||||
"DESCRIPTION": "Track label performance with key metrics including conversations, response times, resolution times, and resolved cases. Click a label name for detailed insights.",
|
|
||||||
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
||||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||||
"DOWNLOAD_LABEL_REPORTS": "تحميل تقارير التسمية",
|
"DOWNLOAD_LABEL_REPORTS": "تحميل تقارير التسمية",
|
||||||
@@ -560,7 +559,6 @@
|
|||||||
"INBOX": "صندوق الوارد",
|
"INBOX": "صندوق الوارد",
|
||||||
"AGENT": "وكيل الدعم",
|
"AGENT": "وكيل الدعم",
|
||||||
"TEAM": "الفريق",
|
"TEAM": "الفريق",
|
||||||
"LABEL": "الوسم",
|
|
||||||
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
||||||
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
||||||
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
"ALL": "الكل",
|
"ALL": "الكل",
|
||||||
"CONTACTS": "جهات الاتصال",
|
"CONTACTS": "جهات الاتصال",
|
||||||
"CONVERSATIONS": "المحادثات",
|
"CONVERSATIONS": "المحادثات",
|
||||||
"MESSAGES": "الرسائل",
|
"MESSAGES": "الرسائل"
|
||||||
"ARTICLES": "Articles"
|
|
||||||
},
|
},
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
"CONTACTS": "جهات الاتصال",
|
"CONTACTS": "جهات الاتصال",
|
||||||
"CONVERSATIONS": "المحادثات",
|
"CONVERSATIONS": "المحادثات",
|
||||||
"MESSAGES": "الرسائل",
|
"MESSAGES": "الرسائل"
|
||||||
"ARTICLES": "Articles"
|
|
||||||
},
|
},
|
||||||
"VIEW_MORE": "View more",
|
"VIEW_MORE": "View more",
|
||||||
"LOAD_MORE": "Load more",
|
"LOAD_MORE": "Load more",
|
||||||
|
|||||||
@@ -76,12 +76,7 @@
|
|||||||
"ACCESS_TOKEN": {
|
"ACCESS_TOKEN": {
|
||||||
"TITLE": "رمز المصادقة",
|
"TITLE": "رمز المصادقة",
|
||||||
"NOTE": "يمكن استخدام هذا رمز المصادقة إذا كنت تبني تطبيقات API للتكامل مع Chatwoot",
|
"NOTE": "يمكن استخدام هذا رمز المصادقة إذا كنت تبني تطبيقات API للتكامل مع Chatwoot",
|
||||||
"COPY": "نسخ",
|
"COPY": "نسخ"
|
||||||
"RESET": "Reset",
|
|
||||||
"CONFIRM_RESET": "Are you sure?",
|
|
||||||
"CONFIRM_HINT": "Click again to confirm",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
},
|
||||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Audio Alerts",
|
"TITLE": "Audio Alerts",
|
||||||
@@ -190,8 +185,7 @@
|
|||||||
"OFFLINE": "غير متصل"
|
"OFFLINE": "غير متصل"
|
||||||
},
|
},
|
||||||
"SET_AVAILABILITY_SUCCESS": "تم تعيين التوافر بنجاح",
|
"SET_AVAILABILITY_SUCCESS": "تم تعيين التوافر بنجاح",
|
||||||
"SET_AVAILABILITY_ERROR": "تعذر تعيين التوافر، الرجاء المحاولة مرة أخرى",
|
"SET_AVAILABILITY_ERROR": "تعذر تعيين التوافر، الرجاء المحاولة مرة أخرى"
|
||||||
"IMPERSONATING_ERROR": "Cannot change availability while impersonating a user"
|
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "عنوان البريد الإلكتروني الخاص بك",
|
"LABEL": "عنوان البريد الإلكتروني الخاص بك",
|
||||||
@@ -286,7 +280,6 @@
|
|||||||
"REPORTS": "التقارير",
|
"REPORTS": "التقارير",
|
||||||
"SETTINGS": "الإعدادات",
|
"SETTINGS": "الإعدادات",
|
||||||
"CONTACTS": "جهات الاتصال",
|
"CONTACTS": "جهات الاتصال",
|
||||||
"ACTIVE": "مفعل",
|
|
||||||
"CAPTAIN": "قائد",
|
"CAPTAIN": "قائد",
|
||||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||||
"CAPTAIN_DOCUMENTS": "Documents",
|
"CAPTAIN_DOCUMENTS": "Documents",
|
||||||
|
|||||||
@@ -59,13 +59,6 @@
|
|||||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ACCESS_TOKEN": {
|
|
||||||
"TITLE": "Access Token",
|
|
||||||
"DESCRIPTION": "Copy the access token and save it securely",
|
|
||||||
"COPY_SUCCESSFUL": "Access token copied to clipboard",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"AVATAR": {
|
"AVATAR": {
|
||||||
"LABEL": "Bot avatar"
|
"LABEL": "Bot avatar"
|
||||||
|
|||||||
@@ -69,9 +69,6 @@
|
|||||||
},
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||||
},
|
|
||||||
"CONVERSATION": {
|
|
||||||
"DELETE": "{agentName} deleted conversation #{id}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,6 @@
|
|||||||
"HEADER": {
|
"HEADER": {
|
||||||
"TITLE": "Contacts",
|
"TITLE": "Contacts",
|
||||||
"SEARCH_TITLE": "Search contacts",
|
"SEARCH_TITLE": "Search contacts",
|
||||||
"ACTIVE_TITLE": "Active contacts",
|
|
||||||
"SEARCH_PLACEHOLDER": "Search...",
|
"SEARCH_PLACEHOLDER": "Search...",
|
||||||
"MESSAGE_BUTTON": "Message",
|
"MESSAGE_BUTTON": "Message",
|
||||||
"SEND_MESSAGE": "Send message",
|
"SEND_MESSAGE": "Send message",
|
||||||
@@ -458,10 +457,6 @@
|
|||||||
"PLACEHOLDER": "Add Twitter"
|
"PLACEHOLDER": "Add Twitter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"DELETE_CONTACT": {
|
|
||||||
"MESSAGE": "This action is permanent and irreversible.",
|
|
||||||
"BUTTON": "Delete now"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DETAILS": {
|
"DETAILS": {
|
||||||
@@ -471,7 +466,7 @@
|
|||||||
"DELETE_CONTACT": "Delete contact",
|
"DELETE_CONTACT": "Delete contact",
|
||||||
"DELETE_DIALOG": {
|
"DELETE_DIALOG": {
|
||||||
"TITLE": "Confirm Deletion",
|
"TITLE": "Confirm Deletion",
|
||||||
"DESCRIPTION": "Are you sure you want to delete this contact?",
|
"DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
|
||||||
"CONFIRM": "Yes, Delete",
|
"CONFIRM": "Yes, Delete",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||||
@@ -560,8 +555,7 @@
|
|||||||
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
||||||
"BUTTON_LABEL": "Add contact",
|
"BUTTON_LABEL": "Add contact",
|
||||||
"SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search 🔍",
|
"SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search 🔍",
|
||||||
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
|
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋"
|
||||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"COMPOSE_NEW_CONVERSATION": {
|
"COMPOSE_NEW_CONVERSATION": {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"RESOLVE_ACTION": "Resolve",
|
"RESOLVE_ACTION": "Resolve",
|
||||||
"REOPEN_ACTION": "Reopen",
|
"REOPEN_ACTION": "Reopen",
|
||||||
"OPEN_ACTION": "Open",
|
"OPEN_ACTION": "Open",
|
||||||
"MORE_ACTIONS": "More actions",
|
|
||||||
"OPEN": "More",
|
"OPEN": "More",
|
||||||
"CLOSE": "Close",
|
"CLOSE": "Close",
|
||||||
"DETAILS": "details",
|
"DETAILS": "details",
|
||||||
@@ -118,11 +117,6 @@
|
|||||||
"FAILED": "Couldn't change priority. Please try again."
|
"FAILED": "Couldn't change priority. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE_CONVERSATION": {
|
|
||||||
"TITLE": "Delete conversation #{conversationId}",
|
|
||||||
"DESCRIPTION": "Are you sure you want to delete this conversation?",
|
|
||||||
"CONFIRM": "Delete"
|
|
||||||
},
|
|
||||||
"CARD_CONTEXT_MENU": {
|
"CARD_CONTEXT_MENU": {
|
||||||
"PENDING": "Mark as pending",
|
"PENDING": "Mark as pending",
|
||||||
"RESOLVED": "Mark as resolved",
|
"RESOLVED": "Mark as resolved",
|
||||||
@@ -139,7 +133,6 @@
|
|||||||
"ASSIGN_LABEL": "Assign label",
|
"ASSIGN_LABEL": "Assign label",
|
||||||
"AGENTS_LOADING": "Loading agents...",
|
"AGENTS_LOADING": "Loading agents...",
|
||||||
"ASSIGN_TEAM": "Assign team",
|
"ASSIGN_TEAM": "Assign team",
|
||||||
"DELETE": "Delete conversation",
|
|
||||||
"API": {
|
"API": {
|
||||||
"AGENT_ASSIGNMENT": {
|
"AGENT_ASSIGNMENT": {
|
||||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||||
@@ -214,8 +207,6 @@
|
|||||||
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
||||||
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
||||||
"CHANGE_TEAM": "Conversation team changed",
|
"CHANGE_TEAM": "Conversation team changed",
|
||||||
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
|
|
||||||
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
|
|
||||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
|
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
|
||||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||||
"SENT_BY": "Sent by:",
|
"SENT_BY": "Sent by:",
|
||||||
@@ -308,7 +299,6 @@
|
|||||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||||
"MACROS": "Macros",
|
"MACROS": "Macros",
|
||||||
"LINEAR_ISSUES": "Linked Linear Issues",
|
|
||||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||||
},
|
},
|
||||||
"SHOPIFY": {
|
"SHOPIFY": {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"PHONE_INPUT": {
|
"PHONE_INPUT": {
|
||||||
"PLACEHOLDER": "Search",
|
"PLACEHOLDER": "Search",
|
||||||
"EMPTY_STATE": "No results found"
|
"EMPTY_STATE": "No results found"
|
||||||
},
|
}
|
||||||
"CLOSE": "Close"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,31 +46,7 @@
|
|||||||
},
|
},
|
||||||
"AUTO_RESOLVE": {
|
"AUTO_RESOLVE": {
|
||||||
"TITLE": "Auto-resolve conversations",
|
"TITLE": "Auto-resolve conversations",
|
||||||
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period of inactivity.",
|
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period. Set the duration and customize the message to the user below."
|
||||||
"DURATION": {
|
|
||||||
"LABEL": "Inactivity duration",
|
|
||||||
"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",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS": "Auto resolve settings updated successfully",
|
|
||||||
"ERROR": "Failed to update auto resolve settings"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"PREFERENCES": "Preferences",
|
|
||||||
"LABEL": {
|
|
||||||
"LABEL": "Add label after auto-resolution",
|
|
||||||
"PLACEHOLDER": "Select a label"
|
|
||||||
},
|
|
||||||
"IGNORE_WAITING": {
|
|
||||||
"LABEL": "Skip conversations waiting for agent’s reply"
|
|
||||||
},
|
|
||||||
"UPDATE_BUTTON": "Save Changes"
|
|
||||||
},
|
},
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Account name",
|
"LABEL": "Account name",
|
||||||
@@ -94,15 +70,7 @@
|
|||||||
},
|
},
|
||||||
"AUTO_RESOLVE_IGNORE_WAITING": {
|
"AUTO_RESOLVE_IGNORE_WAITING": {
|
||||||
"LABEL": "Exclude unattended conversations",
|
"LABEL": "Exclude unattended conversations",
|
||||||
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent's reply."
|
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent’s reply."
|
||||||
},
|
|
||||||
"AUDIO_TRANSCRIPTION": {
|
|
||||||
"TITLE": "Transcribe Audio Messages",
|
|
||||||
"NOTE": "Automatically transcribe audio messages in conversations. Generate a text transcript whenever an audio message is sent or received, and display it alongside the message.",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS": "Audio transcription setting updated successfully",
|
|
||||||
"ERROR": "Failed to update audio transcription setting"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"AUTO_RESOLVE_DURATION": {
|
"AUTO_RESOLVE_DURATION": {
|
||||||
"LABEL": "Inactivity duration for resolution",
|
"LABEL": "Inactivity duration for resolution",
|
||||||
|
|||||||
@@ -318,18 +318,11 @@
|
|||||||
"SUCCESS": "Issue unlinked successfully",
|
"SUCCESS": "Issue unlinked successfully",
|
||||||
"ERROR": "There was an error unlinking the issue, please try again"
|
"ERROR": "There was an error unlinking the issue, please try again"
|
||||||
},
|
},
|
||||||
"NO_LINKED_ISSUES": "No linked issues found",
|
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": "Are you sure you want to delete the integration?",
|
"TITLE": "Are you sure you want to delete the integration?",
|
||||||
"MESSAGE": "Are you sure you want to delete the integration?",
|
"MESSAGE": "Are you sure you want to delete the integration?",
|
||||||
"CONFIRM": "Yes, delete",
|
"CONFIRM": "Yes, delete",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
},
|
|
||||||
"CTA": {
|
|
||||||
"TITLE": "Connect to Linear",
|
|
||||||
"AGENT_DESCRIPTION": "Linear workspace is not connected. Request your administrator to connect a workspace to use this integration.",
|
|
||||||
"DESCRIPTION": "Linear workspace is not connected. Click the button below to connect your workspace to use this integration.",
|
|
||||||
"BUTTON_TEXT": "Connect Linear workspace"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -337,17 +330,12 @@
|
|||||||
"NAME": "Captain",
|
"NAME": "Captain",
|
||||||
"HEADER_KNOW_MORE": "Know more",
|
"HEADER_KNOW_MORE": "Know more",
|
||||||
"COPILOT": {
|
"COPILOT": {
|
||||||
"TITLE": "Copilot",
|
|
||||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
|
||||||
"PANEL_TITLE": "Get started with Copilot",
|
|
||||||
"KICK_OFF_MESSAGE": "Need a quick summary, want to check past conversations, or draft a better reply? Copilot’s here to speed things up.",
|
|
||||||
"SEND_MESSAGE": "Send message...",
|
"SEND_MESSAGE": "Send message...",
|
||||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||||
"LOADER": "Captain is thinking",
|
"LOADER": "Captain is thinking",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"USE": "Use this",
|
"USE": "Use this",
|
||||||
"RESET": "Reset",
|
"RESET": "Reset",
|
||||||
"SHOW_STEPS": "Show steps",
|
|
||||||
"SELECT_ASSISTANT": "Select Assistant",
|
"SELECT_ASSISTANT": "Select Assistant",
|
||||||
"PROMPTS": {
|
"PROMPTS": {
|
||||||
"SUMMARIZE": {
|
"SUMMARIZE": {
|
||||||
@@ -361,14 +349,6 @@
|
|||||||
"RATE": {
|
"RATE": {
|
||||||
"LABEL": "Rate this conversation",
|
"LABEL": "Rate this conversation",
|
||||||
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
||||||
},
|
|
||||||
"HIGH_PRIORITY": {
|
|
||||||
"LABEL": "High priority conversations",
|
|
||||||
"CONTENT": "Give me a summary of all high priority open conversations. Include the conversation ID, customer name (if available), last message content, and assigned agent. Group by status if relevant."
|
|
||||||
},
|
|
||||||
"LIST_CONTACTS": {
|
|
||||||
"LABEL": "List contacts",
|
|
||||||
"CONTENT": "Show me the list of top 10 contacts. Include name, email or phone number (if available), last seen time, tags (if any)."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -388,6 +368,7 @@
|
|||||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||||
},
|
},
|
||||||
"ENTERPRISE_PAYWALL": {
|
"ENTERPRISE_PAYWALL": {
|
||||||
|
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
|
||||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
||||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||||
},
|
},
|
||||||
@@ -402,7 +383,6 @@
|
|||||||
},
|
},
|
||||||
"ASSISTANTS": {
|
"ASSISTANTS": {
|
||||||
"HEADER": "Assistants",
|
"HEADER": "Assistants",
|
||||||
"NO_ASSISTANTS_AVAILABLE": "There are no assistants available in your account.",
|
|
||||||
"ADD_NEW": "Create a new assistant",
|
"ADD_NEW": "Create a new assistant",
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": "Are you sure to delete the assistant?",
|
"TITLE": "Are you sure to delete the assistant?",
|
||||||
@@ -431,10 +411,6 @@
|
|||||||
"PLACEHOLDER": "Enter assistant name",
|
"PLACEHOLDER": "Enter assistant name",
|
||||||
"ERROR": "The name is required"
|
"ERROR": "The name is required"
|
||||||
},
|
},
|
||||||
"TEMPERATURE": {
|
|
||||||
"LABEL": "Response Temperature",
|
|
||||||
"DESCRIPTION": "Adjust how creative or restrictive the assistant's responses should be. Lower values produce more focused and deterministic responses, while higher values allow for more creative and varied outputs."
|
|
||||||
},
|
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"LABEL": "Description",
|
"LABEL": "Description",
|
||||||
"PLACEHOLDER": "Enter assistant description",
|
"PLACEHOLDER": "Enter assistant description",
|
||||||
|
|||||||
@@ -193,7 +193,6 @@
|
|||||||
},
|
},
|
||||||
"LABEL_REPORTS": {
|
"LABEL_REPORTS": {
|
||||||
"HEADER": "Labels Overview",
|
"HEADER": "Labels Overview",
|
||||||
"DESCRIPTION": "Track label performance with key metrics including conversations, response times, resolution times, and resolved cases. Click a label name for detailed insights.",
|
|
||||||
"LOADING_CHART": "Loading chart data...",
|
"LOADING_CHART": "Loading chart data...",
|
||||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||||
@@ -560,7 +559,6 @@
|
|||||||
"INBOX": "Inbox",
|
"INBOX": "Inbox",
|
||||||
"AGENT": "Agent",
|
"AGENT": "Agent",
|
||||||
"TEAM": "Team",
|
"TEAM": "Team",
|
||||||
"LABEL": "Label",
|
|
||||||
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
||||||
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
||||||
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"MESSAGES": "Messages",
|
"MESSAGES": "Messages"
|
||||||
"ARTICLES": "Articles"
|
|
||||||
},
|
},
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"MESSAGES": "Messages",
|
"MESSAGES": "Messages"
|
||||||
"ARTICLES": "Articles"
|
|
||||||
},
|
},
|
||||||
"VIEW_MORE": "View more",
|
"VIEW_MORE": "View more",
|
||||||
"LOAD_MORE": "Load more",
|
"LOAD_MORE": "Load more",
|
||||||
|
|||||||
@@ -76,12 +76,7 @@
|
|||||||
"ACCESS_TOKEN": {
|
"ACCESS_TOKEN": {
|
||||||
"TITLE": "Access Token",
|
"TITLE": "Access Token",
|
||||||
"NOTE": "This token can be used if you are building an API based integration",
|
"NOTE": "This token can be used if you are building an API based integration",
|
||||||
"COPY": "Copy",
|
"COPY": "Copy"
|
||||||
"RESET": "Reset",
|
|
||||||
"CONFIRM_RESET": "Are you sure?",
|
|
||||||
"CONFIRM_HINT": "Click again to confirm",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
},
|
||||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Audio Alerts",
|
"TITLE": "Audio Alerts",
|
||||||
@@ -190,8 +185,7 @@
|
|||||||
"OFFLINE": "Offline"
|
"OFFLINE": "Offline"
|
||||||
},
|
},
|
||||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again",
|
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||||
"IMPERSONATING_ERROR": "Cannot change availability while impersonating a user"
|
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Your email address",
|
"LABEL": "Your email address",
|
||||||
@@ -286,7 +280,6 @@
|
|||||||
"REPORTS": "Reports",
|
"REPORTS": "Reports",
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"ACTIVE": "Active",
|
|
||||||
"CAPTAIN": "Captain",
|
"CAPTAIN": "Captain",
|
||||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||||
"CAPTAIN_DOCUMENTS": "Documents",
|
"CAPTAIN_DOCUMENTS": "Documents",
|
||||||
|
|||||||
@@ -59,13 +59,6 @@
|
|||||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ACCESS_TOKEN": {
|
|
||||||
"TITLE": "Access Token",
|
|
||||||
"DESCRIPTION": "Copy the access token and save it securely",
|
|
||||||
"COPY_SUCCESSFUL": "Access token copied to clipboard",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
|
||||||
},
|
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"AVATAR": {
|
"AVATAR": {
|
||||||
"LABEL": "Bot avatar"
|
"LABEL": "Bot avatar"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user