diff --git a/.circleci/config.yml b/.circleci/config.yml index b8a628677..db7f87d5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,6 +26,12 @@ jobs: override-ci-command: pnpm i - run: node --version - run: pnpm --version + - run: + name: Add PostgreSQL repository and update + command: | + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update -y - run: name: Install System Dependencies @@ -34,7 +40,9 @@ jobs: DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ libpq-dev \ redis-server \ - postgresql \ + postgresql-common \ + postgresql-16 \ + postgresql-16-pgvector \ build-essential \ git \ curl \ diff --git a/.codeclimate.yml b/.codeclimate.yml index 213f7d172..c68251f32 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -51,6 +51,7 @@ exclude_patterns: - 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js' - 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js' - 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js' + - 'app/javascript/dashboard/store/captain/storeFactory.js' - 'app/javascript/dashboard/i18n/index.js' - 'app/javascript/widget/i18n/index.js' - 'app/javascript/survey/i18n/index.js' diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index a804bb15c..8b0704bfa 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -40,7 +40,7 @@ services: network_mode: service:db db: - image: postgres:latest + image: pgvector/pgvector:pg16 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 6a018b314..0af172849 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -1,9 +1,3 @@ -# # -# # This action will strip the enterprise folder -# # and run the spec. -# # This is set to run against every PR. -# # - name: Run Chatwoot CE spec on: push: @@ -18,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 services: postgres: - image: postgres:15.3 + image: pgvector/pgvector:pg15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: '' diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml deleted file mode 100644 index c788a0400..000000000 --- a/.github/workflows/run_response_bot_spec.yml +++ /dev/null @@ -1,89 +0,0 @@ -# # -# # This workflow will run specs related to response bot -# # This can only be activated in installations Where vector extension is available. -# # - -name: Run Response Bot spec -on: - push: - branches: - - develop - - master - pull_request: - workflow_dispatch: - -jobs: - test: - runs-on: ubuntu-20.04 - services: - postgres: - image: ankane/pgvector - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: "" - POSTGRES_DB: postgres - POSTGRES_HOST_AUTH_METHOD: trust - ports: - - 5432:5432 - # needed because the postgres container does not provide a healthcheck - # tmpfs makes DB faster by using RAM - options: >- - --mount type=tmpfs,destination=/var/lib/postgresql/data - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - redis: - image: redis - ports: - - 6379:6379 - options: --entrypoint redis-server - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - - uses: pnpm/action-setup@v2 - with: - version: 9.3.0 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: pnpm - run: pnpm install - - - name: Create database - run: bundle exec rake db:create - - - name: Seed database - run: bundle exec rake db:schema:load - - - name: Enable ResponseBotService in installation - run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation" - - # Run Response Bot specs - - name: Run backend tests - run: | - bundle exec rspec \ - spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \ - spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \ - spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \ - spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \ - --profile=10 \ - --format documentation - - - name: Upload rails log folder - uses: actions/upload-artifact@v4 - if: always() - with: - name: rails-log-folder - path: log diff --git a/Gemfile b/Gemfile index 439b0f63e..45e194e13 100644 --- a/Gemfile +++ b/Gemfile @@ -175,6 +175,8 @@ gem 'pgvector' # Convert Website HTML to Markdown gem 'reverse_markdown' +gem 'ruby-openai' + ### Gems required only in specific deployment environments ### ############################################################## diff --git a/Gemfile.lock b/Gemfile.lock index 74cdbeea8..45c54bd46 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,6 +231,7 @@ GEM erubi (1.13.0) et-orbi (1.2.11) tzinfo + event_stream_parser (1.0.0) execjs (2.8.1) facebook-messenger (2.0.1) httparty (~> 0.13, >= 0.13.7) @@ -684,6 +685,10 @@ GEM rubocop-rspec (2.21.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) + ruby-openai (7.3.1) + event_stream_parser (>= 0.3.0, < 2.0.0) + faraday (>= 1) + faraday-multipart (>= 1) ruby-progressbar (1.13.0) ruby-vips (2.1.4) ffi (~> 1.12) @@ -941,6 +946,7 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec + ruby-openai scout_apm scss_lint seed_dump diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 4a36342fc..fd2a01863 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -3.0.0 +3.1.0 diff --git a/app/actions/contact_merge_action.rb b/app/actions/contact_merge_action.rb index 650dbe89f..2633c907d 100644 --- a/app/actions/contact_merge_action.rb +++ b/app/actions/contact_merge_action.rb @@ -54,7 +54,7 @@ class ContactMergeAction # attributes in base contact are given preference merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes) - @mergee_contact.destroy! + @mergee_contact.reload.destroy! Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact, tokens: [@base_contact.contact_inboxes.filter_map(&:pubsub_token)]) @base_contact.update!(merged_attributes) diff --git a/app/builders/notification_builder.rb b/app/builders/notification_builder.rb index 3d8ce9674..d5461eb4e 100644 --- a/app/builders/notification_builder.rb +++ b/app/builders/notification_builder.rb @@ -25,6 +25,8 @@ class NotificationBuilder def build_notification # Create conversation_creation notification only if user is subscribed to it return if notification_type == 'conversation_creation' && !user_subscribed_to_notification? + # skip notifications for blocked conversations except for user mentions + return if primary_actor.contact.blocked? && notification_type != 'conversation_mention' user.notifications.create!( notification_type: notification_type, diff --git a/app/builders/v2/reports/agent_summary_builder.rb b/app/builders/v2/reports/agent_summary_builder.rb index 382c67bf5..9b4541aaa 100644 --- a/app/builders/v2/reports/agent_summary_builder.rb +++ b/app/builders/v2/reports/agent_summary_builder.rb @@ -2,52 +2,38 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder pattr_initialize [:account!, :params!] def build - set_grouped_conversations_count - set_grouped_avg_reply_time - set_grouped_avg_first_response_time - set_grouped_avg_resolution_time + load_data prepare_report end private - def set_grouped_conversations_count - @grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count + attr_reader :conversations_count, :resolved_count, + :avg_resolution_time, :avg_first_response_time, :avg_reply_time + + def fetch_conversations_count + account.conversations.where(created_at: range).group('assignee_id').count end - def set_grouped_avg_resolution_time - @grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved')) + def prepare_report + account.account_users.map do |account_user| + build_agent_stats(account_user) + end end - def set_grouped_avg_first_response_time - @grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response')) - end - - def set_grouped_avg_reply_time - @grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time')) + def build_agent_stats(account_user) + user_id = account_user.user_id + { + id: user_id, + conversations_count: conversations_count[user_id] || 0, + resolved_conversations_count: resolved_count[user_id] || 0, + avg_resolution_time: avg_resolution_time[user_id], + avg_first_response_time: avg_first_response_time[user_id], + avg_reply_time: avg_reply_time[user_id] + } end def group_by_key :user_id end - - def reporting_events - @reporting_events ||= Current.account.reporting_events.where(created_at: range) - end - - def prepare_report - account.account_users.each_with_object([]) do |account_user, arr| - arr << { - id: account_user.user_id, - conversations_count: @grouped_conversations_count[account_user.user_id], - avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id], - avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id], - avg_reply_time: @grouped_avg_reply_time[account_user.user_id] - } - end - end - - def average_value_key - ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value - end end diff --git a/app/builders/v2/reports/base_summary_builder.rb b/app/builders/v2/reports/base_summary_builder.rb index 33fe5e400..4de65926d 100644 --- a/app/builders/v2/reports/base_summary_builder.rb +++ b/app/builders/v2/reports/base_summary_builder.rb @@ -1,17 +1,50 @@ class V2::Reports::BaseSummaryBuilder include DateRangeHelper + def build + load_data + prepare_report + end + private + def load_data + @conversations_count = fetch_conversations_count + @resolved_count = fetch_resolved_count + @avg_resolution_time = fetch_average_time('conversation_resolved') + @avg_first_response_time = fetch_average_time('first_response') + @avg_reply_time = fetch_average_time('reply_time') + end + + def reporting_events + @reporting_events ||= account.reporting_events.where(created_at: range) + end + + def fetch_conversations_count + # Override this method + end + + def fetch_average_time(event_name) + get_grouped_average(reporting_events.where(name: event_name)) + end + + def fetch_resolved_count + reporting_events.where(name: 'conversation_resolved').group(group_by_key).count + end + def group_by_key # Override this method end + def prepare_report + # Override this method + end + def get_grouped_average(events) events.group(group_by_key).average(average_value_key) end def average_value_key - params[:business_hours].present? ? :value_in_business_hours : :value + ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value end end diff --git a/app/builders/v2/reports/inbox_summary_builder.rb b/app/builders/v2/reports/inbox_summary_builder.rb new file mode 100644 index 000000000..e27385856 --- /dev/null +++ b/app/builders/v2/reports/inbox_summary_builder.rb @@ -0,0 +1,50 @@ +class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder + pattr_initialize [:account!, :params!] + + def build + load_data + prepare_report + end + + private + + attr_reader :conversations_count, :resolved_count, + :avg_resolution_time, :avg_first_response_time, :avg_reply_time + + def load_data + @conversations_count = fetch_conversations_count + @resolved_count = fetch_resolved_count + @avg_resolution_time = fetch_average_time('conversation_resolved') + @avg_first_response_time = fetch_average_time('first_response') + @avg_reply_time = fetch_average_time('reply_time') + end + + def fetch_conversations_count + account.conversations.where(created_at: range).group(group_by_key).count + end + + def prepare_report + account.inboxes.map do |inbox| + build_inbox_stats(inbox) + end + end + + def build_inbox_stats(inbox) + { + id: inbox.id, + conversations_count: conversations_count[inbox.id] || 0, + resolved_conversations_count: resolved_count[inbox.id] || 0, + avg_resolution_time: avg_resolution_time[inbox.id], + avg_first_response_time: avg_first_response_time[inbox.id], + avg_reply_time: avg_reply_time[inbox.id] + } + end + + def group_by_key + :inbox_id + end + + def average_value_key + ActiveModel::Type::Boolean.new.cast(params[:business_hours]) ? :value_in_business_hours : :value + end +end diff --git a/app/builders/v2/reports/team_summary_builder.rb b/app/builders/v2/reports/team_summary_builder.rb index 3d7e765b3..b98151bc6 100644 --- a/app/builders/v2/reports/team_summary_builder.rb +++ b/app/builders/v2/reports/team_summary_builder.rb @@ -1,49 +1,37 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder pattr_initialize [:account!, :params!] - def build - set_grouped_conversations_count - set_grouped_avg_reply_time - set_grouped_avg_first_response_time - set_grouped_avg_resolution_time - prepare_report - end - private - def set_grouped_conversations_count - @grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count - end + attr_reader :conversations_count, :resolved_count, + :avg_resolution_time, :avg_first_response_time, :avg_reply_time - def set_grouped_avg_resolution_time - @grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved')) - end - - def set_grouped_avg_first_response_time - @grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response')) - end - - def set_grouped_avg_reply_time - @grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time')) + def fetch_conversations_count + account.conversations.where(created_at: range).group(:team_id).count end def reporting_events - @reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation) + @reporting_events ||= account.reporting_events.where(created_at: range).joins(:conversation) + end + + def prepare_report + account.teams.map do |team| + build_team_stats(team) + end + end + + def build_team_stats(team) + { + id: team.id, + conversations_count: conversations_count[team.id] || 0, + resolved_conversations_count: resolved_count[team.id] || 0, + avg_resolution_time: avg_resolution_time[team.id], + avg_first_response_time: avg_first_response_time[team.id], + avg_reply_time: avg_reply_time[team.id] + } end def group_by_key 'conversations.team_id' end - - def prepare_report - account.teams.each_with_object([]) do |team, arr| - arr << { - id: team.id, - conversations_count: @grouped_conversations_count[team.id], - avg_resolution_time: @grouped_avg_resolution_time[team.id], - avg_first_response_time: @grouped_avg_first_response_time[team.id], - avg_reply_time: @grouped_avg_reply_time[team.id] - } - end - end end diff --git a/app/controllers/api/v1/accounts/integrations/captain_controller.rb b/app/controllers/api/v1/accounts/integrations/captain_controller.rb deleted file mode 100644 index 6813d14b5..000000000 --- a/app/controllers/api/v1/accounts/integrations/captain_controller.rb +++ /dev/null @@ -1,78 +0,0 @@ -class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::BaseController - before_action :hook - - def proxy - request_url = build_request_url(request_path) - response = HTTParty.send(request_method, request_url, body: permitted_params[:body].to_json, headers: headers) - render plain: response.body, status: response.code - end - - def copilot - request_url = build_request_url(build_request_path("/assistants/#{hook.settings['assistant_id']}/copilot")) - params = { - previous_messages: copilot_params[:previous_messages], - conversation_history: conversation_history, - message: copilot_params[:message] - } - response = HTTParty.send(:post, request_url, body: params.to_json, headers: headers) - render plain: response.body, status: response.code - end - - private - - def headers - { - 'X-User-Email' => hook.settings['account_email'], - 'X-User-Token' => hook.settings['access_token'], - 'Content-Type' => 'application/json', - 'Accept' => '*/*' - } - end - - def build_request_path(route) - "api/accounts/#{hook.settings['account_id']}#{route}" - end - - def request_path - request_route = with_leading_hash_on_route(params[:route]) - - return 'api/sessions/profile' if request_route == '/sessions/profile' - - build_request_path(request_route) - end - - def build_request_url(request_path) - base_url = InstallationConfig.find_by(name: 'CAPTAIN_API_URL').value - URI.join(base_url, request_path).to_s - end - - def hook - @hook ||= Current.account.hooks.find_by!(app_id: 'captain') - end - - def request_method - method = permitted_params[:method].downcase - raise 'Invalid or missing HTTP method' unless %w[get post put patch delete options head].include?(method) - - method - end - - def with_leading_hash_on_route(request_route) - return '' if request_route.blank? - - request_route.start_with?('/') ? request_route : "/#{request_route}" - end - - def conversation_history - conversation = Current.account.conversations.find_by!(display_id: copilot_params[:conversation_id]) - conversation.to_llm_text - end - - def copilot_params - params.permit(:previous_messages, :conversation_id, :message) - end - - def permitted_params - params.permit(:method, :route, body: {}) - end -end diff --git a/app/controllers/api/v2/accounts/summary_reports_controller.rb b/app/controllers/api/v2/accounts/summary_reports_controller.rb index 0cbd6dd8e..989952cfd 100644 --- a/app/controllers/api/v2/accounts/summary_reports_controller.rb +++ b/app/controllers/api/v2/accounts/summary_reports_controller.rb @@ -1,6 +1,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController before_action :check_authorization - before_action :prepare_builder_params, only: [:agent, :team] + before_action :prepare_builder_params, only: [:agent, :team, :inbox] def agent render_report_with(V2::Reports::AgentSummaryBuilder) @@ -10,6 +10,10 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr render_report_with(V2::Reports::TeamSummaryBuilder) end + def inbox + render_report_with(V2::Reports::InboxSummaryBuilder) + end + private def check_authorization @@ -26,8 +30,7 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr def render_report_with(builder_class) builder = builder_class.new(account: Current.account, params: @builder_params) - data = builder.build - render json: data + render json: builder.build end def permitted_params diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 08c62bada..f0fcf403d 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -7,9 +7,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def index @articles = @portal.articles.published + @articles_count = @articles.count search_articles order_by_sort_param - @articles.page(list_params[:page]) if list_params[:page].present? + @articles = @articles.page(list_params[:page]) if list_params[:page].present? end def show; end @@ -44,7 +45,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B end def list_params - params.permit(:query, :locale, :sort, :status) + params.permit(:query, :locale, :sort, :status, :page) end def permitted_params diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index b582bc5dd..7416b7861 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -22,3 +22,5 @@ class AsyncDispatcher < BaseDispatcher ] end end + +AsyncDispatcher.prepend_mod_with('AsyncDispatcher') diff --git a/app/javascript/dashboard/api/captain/assistant.js b/app/javascript/dashboard/api/captain/assistant.js new file mode 100644 index 000000000..ce636e526 --- /dev/null +++ b/app/javascript/dashboard/api/captain/assistant.js @@ -0,0 +1,19 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainAssistant extends ApiClient { + constructor() { + super('captain/assistants', { accountScoped: true }); + } + + get({ page = 1, searchKey } = {}) { + return axios.get(this.url, { + params: { + page, + searchKey, + }, + }); + } +} + +export default new CaptainAssistant(); diff --git a/app/javascript/dashboard/api/captain/document.js b/app/javascript/dashboard/api/captain/document.js new file mode 100644 index 000000000..dc22b0c32 --- /dev/null +++ b/app/javascript/dashboard/api/captain/document.js @@ -0,0 +1,20 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainDocument extends ApiClient { + constructor() { + super('captain/documents', { accountScoped: true }); + } + + get({ page = 1, searchKey, assistantId } = {}) { + return axios.get(this.url, { + params: { + page, + searchKey, + assistant_id: assistantId, + }, + }); + } +} + +export default new CaptainDocument(); diff --git a/app/javascript/dashboard/api/captain/inboxes.js b/app/javascript/dashboard/api/captain/inboxes.js new file mode 100644 index 000000000..e0a1efdfe --- /dev/null +++ b/app/javascript/dashboard/api/captain/inboxes.js @@ -0,0 +1,26 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainInboxes extends ApiClient { + constructor() { + super('captain/assistants', { accountScoped: true }); + } + + get({ assistantId } = {}) { + return axios.get(`${this.url}/${assistantId}/inboxes`); + } + + create(params = {}) { + const { assistantId, inboxId } = params; + return axios.post(`${this.url}/${assistantId}/inboxes`, { + inbox: { inbox_id: inboxId }, + }); + } + + delete(params = {}) { + const { assistantId, inboxId } = params; + return axios.delete(`${this.url}/${assistantId}/inboxes/${inboxId}`); + } +} + +export default new CaptainInboxes(); diff --git a/app/javascript/dashboard/api/captain/response.js b/app/javascript/dashboard/api/captain/response.js new file mode 100644 index 000000000..e3c42757a --- /dev/null +++ b/app/javascript/dashboard/api/captain/response.js @@ -0,0 +1,22 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainResponses extends ApiClient { + constructor() { + super('captain/assistant_responses', { accountScoped: true }); + } + + get({ page = 1, searchKey, assistantId, documentId, status } = {}) { + return axios.get(this.url, { + params: { + page, + searchKey, + assistant_id: assistantId, + document_id: documentId, + status, + }, + }); + } +} + +export default new CaptainResponses(); diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index ebc6176e7..8b9eacf3f 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -133,6 +133,10 @@ class ConversationApi extends ApiClient { getAllAttachments(conversationId) { return axios.get(`${this.url}/${conversationId}/attachments`); } + + requestCopilot(conversationId, body) { + return axios.post(`${this.url}/${conversationId}/copilot`, body); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js index b78a2ea7e..2b816e603 100644 --- a/app/javascript/dashboard/api/integrations.js +++ b/app/javascript/dashboard/api/integrations.js @@ -32,14 +32,6 @@ class IntegrationsAPI extends ApiClient { deleteHook(hookId) { return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`); } - - requestCaptain(body) { - return axios.post(`${this.baseUrl()}/integrations/captain/proxy`, body); - } - - requestCaptainCopilot(body) { - return axios.post(`${this.baseUrl()}/integrations/captain/copilot`, body); - } } export default new IntegrationsAPI(); diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index c6304cf19..d736d7b5a 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -124,6 +124,19 @@ --teal-11: 0 133 115; --teal-12: 13 61 56; + --gray-1: 252 252 252; + --gray-2: 249 249 249; + --gray-3: 240 240 240; + --gray-4: 232 232 232; + --gray-5: 224 224 224; + --gray-6: 217 217 217; + --gray-7: 206 206 206; + --gray-8: 187 187 187; + --gray-9: 141 141 141; + --gray-10: 131 131 131; + --gray-11: 100 100 100; + --gray-12: 32 32 32; + --background-color: 253 253 253; --text-blue: 8 109 224; --border-container: 236 236 236; @@ -213,6 +226,19 @@ --teal-11: 11 216 182; --teal-12: 173 240 221; + --gray-1: 17 17 17; + --gray-2: 25 25 25; + --gray-3: 34 34 34; + --gray-4: 42 42 42; + --gray-5: 49 49 49; + --gray-6: 58 58 58; + --gray-7: 72 72 72; + --gray-8: 96 96 96; + --gray-9: 110 110 110; + --gray-10: 123 123 123; + --gray-11: 180 180 180; + --gray-12: 238 238 238; + --background-color: 18 18 19; --border-strong: 52 52 52; --border-weak: 38 38 42; @@ -232,7 +258,7 @@ --black-alpha-2: 0, 0, 0, 0.2; --border-blue: 39, 129, 246, 0.5; --border-container: 236, 236, 236, 0; - --white-alpha: 255, 255, 255, 0.8; + --white-alpha: 255, 255, 255, 0.1; } /* NEXT COLORS END */ diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss index e08099ae6..9ab51d12a 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss @@ -1,7 +1,7 @@ .dropdown-pane { - @apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999]; + @apply border rounded-lg hidden relative invisible shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-[9999]; &.dropdown-pane--open { - @apply bg-white absolute dark:bg-slate-800 block visible; + @apply bg-n-alpha-3 backdrop-blur-[100px] absolute block visible; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 640e1395f..1213e7c26 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -31,7 +31,7 @@ button { } .button { - @apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium; + @apply items-center bg-n-brand px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium; .button__content { @apply w-full whitespace-nowrap overflow-hidden text-ellipsis; @@ -42,8 +42,10 @@ button { } } - &:hover { - @apply bg-woot-600 dark:bg-woot-600; + &:hover:not(.secondary):not(.success):not(.alert):not(.warning):not( + .clear + ):not(.smooth):not(.hollow) { + @apply bg-n-brand/80 dark:bg-n-brand/80; } &:disabled, @@ -52,23 +54,23 @@ button { } &.success { - @apply bg-[#44ce4b] dark:bg-[#44ce4b] text-white dark:text-white; + @apply bg-n-teal-9 text-white dark:text-white; } &.secondary { - @apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white; + @apply bg-n-solid-3 text-white dark:text-white; } &.primary { - @apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white; + @apply bg-n-brand text-white dark:text-white; } &.clear { - @apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent; + @apply text-n-blue-text dark:text-n-blue-text bg-transparent dark:bg-transparent; } &.alert { - @apply bg-red-500 dark:bg-red-500 text-white dark:text-white; + @apply bg-n-ruby-9 text-white dark:text-white; &.clear { @apply bg-transparent dark:bg-transparent; @@ -76,7 +78,7 @@ button { } &.warning { - @apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white; + @apply bg-n-amber-9 text-white dark:text-white; &.clear { @apply bg-transparent dark:bg-transparent; @@ -115,114 +117,74 @@ button { } &.hollow { - @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; + @apply border border-n-brand/40 bg-transparent text-n-blue-text hover:bg-n-brand/20; &.secondary { - @apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; + @apply text-n-slate-12 border-n-slate-5 hover:bg-n-slate-5; } &.success { - @apply text-green-700 dark:text-green-400 border-green-100 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-800; + @apply text-n-teal-9 border-n-teal-8 hover:bg-n-teal-5; } &.alert { - @apply text-red-700 dark:text-red-400 border-red-100 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-800; + @apply text-n-ruby-9 border-n-ruby-8 hover:bg-n-ruby-5; } &.warning { - @apply text-yellow-600 dark:text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-800; - } - - &:hover { - @apply bg-woot-75 dark:bg-woot-800 border-slate-100 dark:border-woot-600 dark:text-woot-400; - - &.secondary { - @apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100; - } - - &.success { - @apply border-slate-100 dark:border-slate-700 text-green-800 dark:text-green-100; - } - - &.alert { - @apply border-slate-100 dark:border-slate-700 text-red-700 dark:text-red-100; - } - - &.warning { - @apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-500; - } + @apply text-n-amber-9 border-n-amber-8 hover:bg-n-amber-5; } } // Smooth style &.smooth { - @apply bg-woot-50 dark:bg-woot-800 text-woot-700 dark:text-woot-100 hover:text-woot-700 dark:hover:text-woot-700 hover:bg-woot-100 dark:hover:bg-woot-900; + @apply bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text hover:bg-n-brand/20 dark:hover:bg-n-brand/40; &.secondary { - @apply bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800; + @apply bg-n-slate-4 text-n-slate-11 hover:text-n-slate-11 hover:bg-n-slate-5; } &.success { - @apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100; + @apply bg-n-teal-4 text-n-teal-11 hover:text-n-teal-11 hover:bg-n-teal-5; } &.alert { - @apply bg-red-50 dark:bg-red-700 dark:bg-opacity-50 text-red-700 dark:text-red-100 hover:bg-red-100 dark:hover:bg-red-800 dark:hover:bg-opacity-30; + @apply bg-n-ruby-4 text-n-ruby-11 hover:text-n-ruby-11 hover:bg-n-ruby-5; } &.warning { - @apply bg-yellow-100 dark:bg-yellow-100 text-yellow-700 dark:text-yellow-700 hover:bg-yellow-200 dark:hover:bg-yellow-200; + @apply bg-n-amber-4 text-n-amber-11 hover:text-n-amber-11 hover:bg-n-amber-5; } } &.clear { - @apply text-woot-500 dark:text-woot-500; + @apply text-n-blue-text hover:bg-n-brand/10 dark:hover:bg-n-brand/30; &.secondary { - @apply text-slate-700 dark:text-slate-100; + @apply text-n-slate-12 hover:bg-n-slate-4; } &.success { - @apply text-green-700 dark:text-green-100; + @apply text-n-teal-10 hover:bg-n-teal-4; } &.alert { - @apply text-red-700 dark:text-red-100; + @apply text-n-ruby-11 hover:bg-n-ruby-4; } &.warning { - @apply text-yellow-700 dark:text-yellow-600; - } - - &:hover { - @apply hover:bg-woot-50 dark:hover:bg-woot-900/50 hover:text-woot-500 dark:hover:text-woot-100; - - &.secondary { - @apply hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-800 dark:hover:text-slate-100; - } - - &.success { - @apply hover:bg-green-50 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100; - } - - &.alert { - @apply hover:bg-red-50 dark:hover:bg-red-800 hover:text-red-700 dark:hover:text-red-100; - } - - &.warning { - @apply hover:bg-yellow-100 dark:hover:bg-yellow-800 hover:text-yellow-700 dark:hover:text-yellow-600; - } + @apply text-n-amber-11 hover:bg-n-amber-4; } &:active { &.secondary { - @apply active:bg-slate-100 dark:active:bg-slate-900; + @apply active:bg-n-slate-3 dark:active:bg-n-slate-7; } } &:focus { &.secondary { - @apply focus:bg-slate-50 dark:focus:bg-slate-700; + @apply focus:bg-n-slate-4 dark:focus:bg-n-slate-6; } } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index 01ccaf671..72a2e6be8 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -3,7 +3,7 @@ } .tabs--container--with-border { - @apply border-b border-slate-50 dark:border-slate-800/50; + @apply border-b border-n-weak; } .tabs--container--compact.tab--chat-type { @@ -42,7 +42,7 @@ @apply flex-shrink-0 my-0 mx-2; .badge { - @apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0; + @apply bg-n-alpha-black2 dark:bg-n-solid-3 rounded-md text-n-slate-11 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0; } &:first-child { @@ -56,22 +56,22 @@ &:hover, &:focus { a { - @apply text-slate-800 dark:text-slate-100; + @apply text-n-slate-12; } } a { - @apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative; + @apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-n-slate-11 text-sm top-[1px] relative; transition: border-color 0.15s $swift-ease-out-function; } &.is-active { a { - @apply border-b border-woot-500 text-woot-500 dark:text-woot-500; + @apply border-b border-n-brand text-n-blue-text; } .badge { - @apply bg-woot-50 dark:bg-woot-500 text-woot-500 dark:text-woot-50 dark:bg-opacity-40; + @apply bg-n-brand/10 dark:bg-n-brand/20 text-n-blue-text; } } } diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue index dfe3f437c..d5586cb12 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue @@ -29,7 +29,7 @@ const getWrittenBy = note => { const isCurrentUser = note?.user?.id === currentUser.value.id; return isCurrentUser ? t('CONTACTS_LAYOUT.SIDEBAR.NOTES.YOU') - : note.user.name; + : note?.user?.name || 'Bot'; }; const onAdd = content => { diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue index 504be1547..17367c930 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue @@ -33,8 +33,8 @@ const handleDelete = () => {
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue index a81c80b87..a2b3ad7fb 100644 --- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue +++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue @@ -21,7 +21,7 @@ const lastNonActivityMessageContent = computed(() => { props.conversation; const { email: { subject } = {} } = customAttributes; return getPlainText( - subject || lastNonActivityMessage.content || t('CHAT_LIST.NO_CONTENT') + subject || lastNonActivityMessage?.content || t('CHAT_LIST.NO_CONTENT') ); }); diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue index 19dc7008a..f1ad9d09b 100644 --- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue +++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue @@ -1,5 +1,5 @@