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 { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import { useHelpCenterGenerationStore } from 'dashboard/stores/helpCenterGeneration';
|
||||
import {
|
||||
applyOutboundAnswer,
|
||||
armOutboundRecorder,
|
||||
@@ -49,6 +50,8 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
this.onConversationUnreadCountChanged,
|
||||
'account.cache_invalidated': this.onCacheInvalidate,
|
||||
'account.enrichment_completed': this.onEnrichmentCompleted,
|
||||
'help_center.article_generated': this.onHelpCenterArticleGenerated,
|
||||
'help_center.generation_completed': this.onHelpCenterGenerationCompleted,
|
||||
'copilot.message.created': this.onCopilotMessageCreated,
|
||||
'voice_call.incoming': this.onVoiceCallIncoming,
|
||||
'voice_call.outbound_connected': this.onVoiceCallOutboundConnected,
|
||||
@@ -268,6 +271,16 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
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 => {
|
||||
const keys = data.cache_keys;
|
||||
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
|
||||
DEFAULT_WIDGET_COLOR = '#1f93ff'.freeze
|
||||
# context.dev descriptions and LLM completions are unbounded; bound the
|
||||
# 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).
|
||||
# context.dev descriptions and LLM completions are unbounded; so we limit in the backend
|
||||
WELCOME_TAGLINE_MAX_LENGTH = 255
|
||||
|
||||
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.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?
|
||||
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?
|
||||
if resource.custom_attributes['marked_for_deletion_reason'].present?
|
||||
json.marked_for_deletion_reason resource.custom_attributes['marked_for_deletion_reason']
|
||||
|
||||
@@ -2,10 +2,10 @@ class Onboarding::HelpCenterArticleGenerationJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
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}"
|
||||
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
|
||||
|
||||
def perform(account_id, portal_id, user_id, generation_id)
|
||||
@@ -19,7 +19,7 @@ class Onboarding::HelpCenterArticleGenerationJob < ApplicationJob
|
||||
)
|
||||
rescue Onboarding::HelpCenterErrors::CurationSkipped => e
|
||||
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
|
||||
|
||||
private
|
||||
@@ -94,10 +94,10 @@ class Onboarding::HelpCenterArticleGenerationJob < ApplicationJob
|
||||
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::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
|
||||
|
||||
@@ -19,33 +19,33 @@ class Onboarding::HelpCenterArticleWriterJob < ApplicationJob
|
||||
article: payload[:article]
|
||||
).perform
|
||||
|
||||
finalize(user: user, generation_id: generation_id, article: article)
|
||||
finalize(account_id: account_id, user: user, generation_id: generation_id, article: article)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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}"
|
||||
finalize(user: user, generation_id: generation_id, article: nil)
|
||||
finalize(account_id: account_id, user: user, generation_id: generation_id, article: nil)
|
||||
end
|
||||
|
||||
def failure_context
|
||||
_account_id, _portal_id, user_id, generation_id = arguments
|
||||
[User.find_by(id: user_id), generation_id]
|
||||
account_id, _portal_id, user_id, generation_id = arguments
|
||||
[account_id, User.find_by(id: user_id), generation_id]
|
||||
end
|
||||
|
||||
def finalize(user:, generation_id:, article:)
|
||||
def finalize(account_id:, user:, generation_id:, article:)
|
||||
result = Onboarding::HelpCenterGenerationState.record_article_finished(generation_id)
|
||||
|
||||
if article
|
||||
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
|
||||
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
|
||||
Rails.logger.warn "[HelpCenterWriterJob] gen=#{generation_id} #{e.message}"
|
||||
end
|
||||
|
||||
@@ -4,16 +4,18 @@ module Onboarding::HelpCenterBroadcaster
|
||||
|
||||
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, {
|
||||
account_id: account_id,
|
||||
generation_id: generation_id,
|
||||
article_id: article.id,
|
||||
articles_finished: articles_finished
|
||||
})
|
||||
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, {
|
||||
account_id: account_id,
|
||||
generation_id: generation_id,
|
||||
status: status,
|
||||
skip_reason: skip_reason
|
||||
|
||||
@@ -76,6 +76,7 @@ class Onboarding::HelpCenterCreationService
|
||||
return if homepage_link.blank?
|
||||
|
||||
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)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[HelpCenterCreation] Failed to enqueue article generation for account #{@account.id}: #{e.class} - #{e.message}"
|
||||
|
||||
@@ -1,14 +1,66 @@
|
||||
namespace :onboarding do
|
||||
desc 'Reset onboarding for an account (triggers the onboarding flow again). Usage: rake onboarding:reset[account_id]'
|
||||
task :reset, [:account_id] => :environment do |_task, args|
|
||||
abort 'Error: Please provide an account ID' if args[:account_id].blank?
|
||||
namespace :onboarding do # rubocop:disable Metrics/BlockLength
|
||||
# Resets onboarding for an account so the onboarding flow runs again.
|
||||
# Interactively prompts for an account ID, then resets the onboarding step
|
||||
# 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: Account with ID '#{args[:account_id]}' not found" unless account
|
||||
abort 'Error: Please provide an account ID' if account_id.blank?
|
||||
|
||||
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'
|
||||
# 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!
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user