Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12831a47d1 |
@@ -5,6 +5,7 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||||
import { useCallsStore } from 'dashboard/stores/calls';
|
import { useCallsStore } from 'dashboard/stores/calls';
|
||||||
|
import { useHelpCenterGenerationStore } from 'dashboard/stores/helpCenterGeneration';
|
||||||
import {
|
import {
|
||||||
applyOutboundAnswer,
|
applyOutboundAnswer,
|
||||||
armOutboundRecorder,
|
armOutboundRecorder,
|
||||||
@@ -49,6 +50,8 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
this.onConversationUnreadCountChanged,
|
this.onConversationUnreadCountChanged,
|
||||||
'account.cache_invalidated': this.onCacheInvalidate,
|
'account.cache_invalidated': this.onCacheInvalidate,
|
||||||
'account.enrichment_completed': this.onEnrichmentCompleted,
|
'account.enrichment_completed': this.onEnrichmentCompleted,
|
||||||
|
'help_center.article_generated': this.onHelpCenterArticleGenerated,
|
||||||
|
'help_center.generation_completed': this.onHelpCenterGenerationCompleted,
|
||||||
'copilot.message.created': this.onCopilotMessageCreated,
|
'copilot.message.created': this.onCopilotMessageCreated,
|
||||||
'voice_call.incoming': this.onVoiceCallIncoming,
|
'voice_call.incoming': this.onVoiceCallIncoming,
|
||||||
'voice_call.outbound_connected': this.onVoiceCallOutboundConnected,
|
'voice_call.outbound_connected': this.onVoiceCallOutboundConnected,
|
||||||
@@ -268,6 +271,16 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
this.app.$store.dispatch('accounts/get', { silent: true });
|
this.app.$store.dispatch('accounts/get', { silent: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
onHelpCenterArticleGenerated = data => {
|
||||||
|
useHelpCenterGenerationStore().handleArticleGenerated(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
onHelpCenterGenerationCompleted = data => {
|
||||||
|
useHelpCenterGenerationStore().handleGenerationCompleted(data);
|
||||||
|
};
|
||||||
|
|
||||||
onCacheInvalidate = data => {
|
onCacheInvalidate = data => {
|
||||||
const keys = data.cache_keys;
|
const keys = data.cache_keys;
|
||||||
this.app.$store.dispatch('labels/revalidate', { newKey: keys.label });
|
this.app.$store.dispatch('labels/revalidate', { newKey: keys.label });
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
// Tracks the onboarding help center article generation, fed live by the
|
||||||
|
// `help_center.*` ActionCable events (see dashboard/helper/actionCable.js).
|
||||||
|
// The connector is created at app init, so events accumulate here before the
|
||||||
|
// onboarding screen mounts. State is in-memory only; a reload restarts the
|
||||||
|
// count from the next event (the generation_id comes from the account).
|
||||||
|
export const useHelpCenterGenerationStore = defineStore(
|
||||||
|
'helpCenterGeneration',
|
||||||
|
{
|
||||||
|
state: () => ({
|
||||||
|
generationId: null,
|
||||||
|
status: null, // 'generating' | 'completed' | 'skipped' | null
|
||||||
|
articlesCount: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isGenerating: state => state.status === 'generating',
|
||||||
|
isCompleted: state => state.status === 'completed',
|
||||||
|
isSkipped: state => state.status === 'skipped',
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// Called by the component on mount with the account's generation_id.
|
||||||
|
// No-op when events have already started populating this generation.
|
||||||
|
hydrate(generationId) {
|
||||||
|
if (!generationId || this.generationId === generationId) return;
|
||||||
|
|
||||||
|
this.generationId = generationId;
|
||||||
|
this.status = 'generating';
|
||||||
|
this.articlesCount = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleArticleGenerated({
|
||||||
|
generation_id: generationId,
|
||||||
|
articles_finished: articlesFinished,
|
||||||
|
}) {
|
||||||
|
if (this.generationId && this.generationId !== generationId) return;
|
||||||
|
|
||||||
|
this.generationId = generationId;
|
||||||
|
this.articlesCount = articlesFinished ?? this.articlesCount + 1;
|
||||||
|
if (!this.isCompleted && !this.isSkipped) this.status = 'generating';
|
||||||
|
},
|
||||||
|
|
||||||
|
handleGenerationCompleted({ generation_id: generationId, status }) {
|
||||||
|
if (this.generationId && this.generationId !== generationId) return;
|
||||||
|
|
||||||
|
this.generationId = generationId;
|
||||||
|
this.status = status === 'skipped' ? 'skipped' : 'completed';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
class Onboarding::WebWidgetCreationService
|
class Onboarding::WebWidgetCreationService
|
||||||
DEFAULT_WIDGET_COLOR = '#1f93ff'.freeze
|
DEFAULT_WIDGET_COLOR = '#1f93ff'.freeze
|
||||||
# context.dev descriptions and LLM completions are unbounded; bound the
|
# context.dev descriptions and LLM completions are unbounded; so we limit in the backend
|
||||||
# stored tagline so a long string doesn't render as a wall of text in the
|
|
||||||
# widget UI (and so backends that enforce varchar limits don't raise).
|
|
||||||
WELCOME_TAGLINE_MAX_LENGTH = 255
|
WELCOME_TAGLINE_MAX_LENGTH = 255
|
||||||
|
|
||||||
def initialize(account, user)
|
def initialize(account, user)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ if resource.custom_attributes.present?
|
|||||||
json.referral_source resource.custom_attributes['referral_source'] if resource.custom_attributes['referral_source'].present?
|
json.referral_source resource.custom_attributes['referral_source'] if resource.custom_attributes['referral_source'].present?
|
||||||
json.brand_info resource.custom_attributes['brand_info'] if resource.custom_attributes['brand_info'].present?
|
json.brand_info resource.custom_attributes['brand_info'] if resource.custom_attributes['brand_info'].present?
|
||||||
json.onboarding_step resource.onboarding_step if resource.onboarding_step.present?
|
json.onboarding_step resource.onboarding_step if resource.onboarding_step.present?
|
||||||
|
if resource.custom_attributes['help_center_generation_id'].present?
|
||||||
|
json.help_center_generation_id resource.custom_attributes['help_center_generation_id']
|
||||||
|
end
|
||||||
json.marked_for_deletion_at resource.custom_attributes['marked_for_deletion_at'] if resource.custom_attributes['marked_for_deletion_at'].present?
|
json.marked_for_deletion_at resource.custom_attributes['marked_for_deletion_at'] if resource.custom_attributes['marked_for_deletion_at'].present?
|
||||||
if resource.custom_attributes['marked_for_deletion_reason'].present?
|
if resource.custom_attributes['marked_for_deletion_reason'].present?
|
||||||
json.marked_for_deletion_reason resource.custom_attributes['marked_for_deletion_reason']
|
json.marked_for_deletion_reason resource.custom_attributes['marked_for_deletion_reason']
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ class Onboarding::HelpCenterArticleGenerationJob < ApplicationJob
|
|||||||
queue_as :low
|
queue_as :low
|
||||||
|
|
||||||
retry_on Firecrawl::FirecrawlError, wait: :polynomially_longer, attempts: 3 do |job, error|
|
retry_on Firecrawl::FirecrawlError, wait: :polynomially_longer, attempts: 3 do |job, error|
|
||||||
_account_id, _portal_id, user_id, generation_id = job.arguments
|
account_id, _portal_id, user_id, generation_id = job.arguments
|
||||||
reason = "firecrawl exhausted: #{error.message}"
|
reason = "firecrawl exhausted: #{error.message}"
|
||||||
Rails.logger.warn "[HelpCenterGenerationJob] gen=#{generation_id} #{reason}"
|
Rails.logger.warn "[HelpCenterGenerationJob] gen=#{generation_id} #{reason}"
|
||||||
job.send(:skip_and_broadcast, user: User.find_by(id: user_id), generation_id: generation_id, reason: reason)
|
job.send(:skip_and_broadcast, account_id: account_id, user: User.find_by(id: user_id), generation_id: generation_id, reason: reason)
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(account_id, portal_id, user_id, generation_id)
|
def perform(account_id, portal_id, user_id, generation_id)
|
||||||
@@ -19,7 +19,7 @@ class Onboarding::HelpCenterArticleGenerationJob < ApplicationJob
|
|||||||
)
|
)
|
||||||
rescue Onboarding::HelpCenterErrors::CurationSkipped => e
|
rescue Onboarding::HelpCenterErrors::CurationSkipped => e
|
||||||
Rails.logger.info "[HelpCenterGenerationJob] gen=#{generation_id} skipped: #{e.message}"
|
Rails.logger.info "[HelpCenterGenerationJob] gen=#{generation_id} skipped: #{e.message}"
|
||||||
skip_and_broadcast(user: User.find_by(id: user_id), generation_id: generation_id, reason: e.message)
|
skip_and_broadcast(account_id: account_id, user: User.find_by(id: user_id), generation_id: generation_id, reason: e.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -94,10 +94,10 @@ class Onboarding::HelpCenterArticleGenerationJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip_and_broadcast(user:, generation_id:, reason:)
|
def skip_and_broadcast(account_id:, user:, generation_id:, reason:)
|
||||||
Onboarding::HelpCenterGenerationState.skip(generation_id, reason: reason)
|
Onboarding::HelpCenterGenerationState.skip(generation_id, reason: reason)
|
||||||
Onboarding::HelpCenterBroadcaster.completed(
|
Onboarding::HelpCenterBroadcaster.completed(
|
||||||
user: user, generation_id: generation_id, status: 'skipped', skip_reason: reason
|
account_id: account_id, user: user, generation_id: generation_id, status: 'skipped', skip_reason: reason
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,33 +19,33 @@ class Onboarding::HelpCenterArticleWriterJob < ApplicationJob
|
|||||||
article: payload[:article]
|
article: payload[:article]
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
finalize(user: user, generation_id: generation_id, article: article)
|
finalize(account_id: account_id, user: user, generation_id: generation_id, article: article)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def on_writer_failure(error)
|
def on_writer_failure(error)
|
||||||
user, generation_id = failure_context
|
account_id, user, generation_id = failure_context
|
||||||
Rails.logger.warn "[HelpCenterWriterJob] gen=#{generation_id} failed: #{error.class} #{error.message}"
|
Rails.logger.warn "[HelpCenterWriterJob] gen=#{generation_id} failed: #{error.class} #{error.message}"
|
||||||
finalize(user: user, generation_id: generation_id, article: nil)
|
finalize(account_id: account_id, user: user, generation_id: generation_id, article: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def failure_context
|
def failure_context
|
||||||
_account_id, _portal_id, user_id, generation_id = arguments
|
account_id, _portal_id, user_id, generation_id = arguments
|
||||||
[User.find_by(id: user_id), generation_id]
|
[account_id, User.find_by(id: user_id), generation_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def finalize(user:, generation_id:, article:)
|
def finalize(account_id:, user:, generation_id:, article:)
|
||||||
result = Onboarding::HelpCenterGenerationState.record_article_finished(generation_id)
|
result = Onboarding::HelpCenterGenerationState.record_article_finished(generation_id)
|
||||||
|
|
||||||
if article
|
if article
|
||||||
Onboarding::HelpCenterBroadcaster.article_generated(
|
Onboarding::HelpCenterBroadcaster.article_generated(
|
||||||
user: user, generation_id: generation_id, article: article, articles_finished: result[:finished]
|
account_id: account_id, user: user, generation_id: generation_id, article: article, articles_finished: result[:finished]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
return unless result[:completed]
|
return unless result[:completed]
|
||||||
|
|
||||||
Onboarding::HelpCenterBroadcaster.completed(user: user, generation_id: generation_id, status: 'completed')
|
Onboarding::HelpCenterBroadcaster.completed(account_id: account_id, user: user, generation_id: generation_id, status: 'completed')
|
||||||
rescue Onboarding::HelpCenterGenerationState::Missing => e
|
rescue Onboarding::HelpCenterGenerationState::Missing => e
|
||||||
Rails.logger.warn "[HelpCenterWriterJob] gen=#{generation_id} #{e.message}"
|
Rails.logger.warn "[HelpCenterWriterJob] gen=#{generation_id} #{e.message}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ module Onboarding::HelpCenterBroadcaster
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def article_generated(user:, generation_id:, article:, articles_finished:)
|
def article_generated(account_id:, user:, generation_id:, article:, articles_finished:)
|
||||||
broadcast(user, ARTICLE_GENERATED, {
|
broadcast(user, ARTICLE_GENERATED, {
|
||||||
|
account_id: account_id,
|
||||||
generation_id: generation_id,
|
generation_id: generation_id,
|
||||||
article_id: article.id,
|
article_id: article.id,
|
||||||
articles_finished: articles_finished
|
articles_finished: articles_finished
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed(user:, generation_id:, status:, skip_reason: nil)
|
def completed(account_id:, user:, generation_id:, status:, skip_reason: nil)
|
||||||
broadcast(user, GENERATION_COMPLETED, {
|
broadcast(user, GENERATION_COMPLETED, {
|
||||||
|
account_id: account_id,
|
||||||
generation_id: generation_id,
|
generation_id: generation_id,
|
||||||
status: status,
|
status: status,
|
||||||
skip_reason: skip_reason
|
skip_reason: skip_reason
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class Onboarding::HelpCenterCreationService
|
|||||||
return if homepage_link.blank?
|
return if homepage_link.blank?
|
||||||
|
|
||||||
generation_id = SecureRandom.uuid
|
generation_id = SecureRandom.uuid
|
||||||
|
@account.update!(custom_attributes: @account.custom_attributes.merge('help_center_generation_id' => generation_id))
|
||||||
Onboarding::HelpCenterArticleGenerationJob.perform_later(@account.id, portal.id, @user.id, generation_id)
|
Onboarding::HelpCenterArticleGenerationJob.perform_later(@account.id, portal.id, @user.id, generation_id)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "[HelpCenterCreation] Failed to enqueue article generation for account #{@account.id}: #{e.class} - #{e.message}"
|
Rails.logger.error "[HelpCenterCreation] Failed to enqueue article generation for account #{@account.id}: #{e.class} - #{e.message}"
|
||||||
|
|||||||
@@ -1,14 +1,66 @@
|
|||||||
namespace :onboarding do
|
namespace :onboarding do # rubocop:disable Metrics/BlockLength
|
||||||
desc 'Reset onboarding for an account (triggers the onboarding flow again). Usage: rake onboarding:reset[account_id]'
|
# Resets onboarding for an account so the onboarding flow runs again.
|
||||||
task :reset, [:account_id] => :environment do |_task, args|
|
# Interactively prompts for an account ID, then resets the onboarding step
|
||||||
abort 'Error: Please provide an account ID' if args[:account_id].blank?
|
# and deletes the account's inboxes and help center (portals, categories, articles).
|
||||||
|
#
|
||||||
|
# How to run:
|
||||||
|
# bundle exec rake onboarding:reset
|
||||||
|
#
|
||||||
|
# You will be prompted for the account ID and a confirmation (y/N).
|
||||||
|
desc 'Reset onboarding for an account (interactive). Resets the onboarding step and deletes inboxes and help center articles.'
|
||||||
|
task reset: :environment do # rubocop:disable Metrics/BlockLength
|
||||||
|
print 'Enter the account ID to reset onboarding for: '
|
||||||
|
account_id = $stdin.gets&.strip
|
||||||
|
|
||||||
account = Account.find_by(id: args[:account_id])
|
abort 'Error: Please provide an account ID' if account_id.blank?
|
||||||
abort "Error: Account with ID '#{args[:account_id]}' not found" unless account
|
|
||||||
|
account = Account.find_by(id: account_id)
|
||||||
|
abort "Error: Account with ID '#{account_id}' not found" unless account
|
||||||
|
|
||||||
|
puts "\nAccount: #{account.name} (ID: #{account.id})"
|
||||||
|
puts "Current onboarding step: #{account.custom_attributes['onboarding_step'] || '(none)'}"
|
||||||
|
|
||||||
|
if account.inboxes.any?
|
||||||
|
puts "\nInboxes (#{account.inboxes.count}):"
|
||||||
|
account.inboxes.each { |inbox| puts " - ##{inbox.id} #{inbox.name} [#{inbox.channel_type}]" }
|
||||||
|
end
|
||||||
|
|
||||||
|
if account.portals.any?
|
||||||
|
puts "\nPortals (#{account.portals.count}):"
|
||||||
|
account.portals.each { |portal| puts " - ##{portal.id} #{portal.name}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
if account.articles.any?
|
||||||
|
puts "\nHelp center articles (#{account.articles.count}):"
|
||||||
|
account.articles.each { |article| puts " - ##{article.id} #{article.title}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "\nTo be deleted: #{account.inboxes.count} inbox(es), #{account.portals.count} portal(s), " \
|
||||||
|
"#{account.categories.count} category(ies), #{account.articles.count} article(s)."
|
||||||
|
|
||||||
|
print "\nReset onboarding for '#{account.name}' (ID: #{account.id}) and delete the above? (y/N): "
|
||||||
|
abort 'Aborted' unless $stdin.gets&.strip&.casecmp?('y')
|
||||||
|
|
||||||
account.custom_attributes['onboarding_step'] = 'account_details'
|
account.custom_attributes['onboarding_step'] = 'account_details'
|
||||||
|
# Seed the socials the website branding service would have detected so the
|
||||||
|
# onboarding inbox setup UI has channels to render. Mirrors the shape of
|
||||||
|
# WebsiteBrandingService#build_socials (keys map to SocialLinkParser).
|
||||||
|
account.custom_attributes['brand_info'] ||= {}
|
||||||
|
account.custom_attributes['brand_info']['socials'] = [
|
||||||
|
{ 'type' => 'whatsapp', 'url' => 'https://wa.me/14155552671' },
|
||||||
|
{ 'type' => 'facebook', 'url' => 'https://facebook.com/chatwoot' },
|
||||||
|
{ 'type' => 'telegram', 'url' => 'https://t.me/chatwoot' },
|
||||||
|
{ 'type' => 'instagram', 'url' => 'https://instagram.com/chatwoot' },
|
||||||
|
{ 'type' => 'line', 'url' => 'https://line.me/chatwoot' },
|
||||||
|
{ 'type' => 'tiktok', 'url' => 'https://tiktok.com/@chatwoot' }
|
||||||
|
]
|
||||||
account.save!
|
account.save!
|
||||||
|
|
||||||
puts "Onboarding has been reset for account '#{account.name}' (ID: #{account.id})"
|
account.inboxes.destroy_all
|
||||||
|
account.articles.destroy_all
|
||||||
|
account.categories.destroy_all
|
||||||
|
account.portals.destroy_all
|
||||||
|
|
||||||
|
puts "\nOnboarding has been reset for account '#{account.name}' (ID: #{account.id}). Inboxes and help center articles deleted."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user