When a new account finishes onboarding we want to land them on a
dashboard with a working web widget already configured, branded, named,
and assigned to them, instead of an empty inbox list. This PR adds the
services that produce that widget. **No user-visible change yet:** the
services are dormant until the trigger and background job are wired up
in the follow-up PR.
## Context
Milestone 1 added `Account::BrandingEnrichmentJob`, which calls
context.dev during signup and stores brand data on
`account.custom_attributes['brand_info']`, plus the new onboarding form
that captures `domain`, `name`, `industry`, etc. Milestone 2 starts
using that data, and the first thing we want is a web widget
materialized automatically. Splitting the service layer from the
orchestration plumbing (Redis key, `onboarding_step` extension,
controller wiring, ActionCable) keeps this diff focused and lets the
LLM/widget logic merge independently.
## How to test
Run against an existing account that already has `brand_info` populated.
```ruby
account = Account.find(<account_id>)
user = account.administrators.first
inbox = WidgetCreationService.new(account, user).perform
inbox.channel.widget_color # color from brand_info, or '#1f93ff'
inbox.channel.welcome_title # brand_info[:title], or account.name
inbox.channel.welcome_tagline # LLM tagline (Enterprise + system key set),
# else brand_info[:slogan]/[:description]/nil
inbox.inbox_members.pluck(:user_id)
```
Toggle `InstallationConfig['CAPTAIN_OPEN_AI_API_KEY']` to flip between
LLM and brand-text tagline paths. To verify failure isolation, raise
inside `Captain::Llm::WidgetTaglineService#perform` and confirm widget
creation still succeeds with the fallback tagline.
76 lines
2.1 KiB
Ruby
76 lines
2.1 KiB
Ruby
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).
|
|
WELCOME_TAGLINE_MAX_LENGTH = 255
|
|
|
|
def initialize(account, user)
|
|
@account = account
|
|
@user = user
|
|
end
|
|
|
|
def perform
|
|
existing = existing_web_widget_inbox
|
|
if existing
|
|
Rails.logger.info "[WidgetCreation] Reusing existing web widget inbox #{existing.id} for account #{@account.id}"
|
|
return existing
|
|
end
|
|
|
|
if website_url.blank?
|
|
Rails.logger.info "[WidgetCreation] Skipping for account #{@account.id}: no website_url available"
|
|
return nil
|
|
end
|
|
|
|
attrs = channel_attributes
|
|
|
|
ActiveRecord::Base.transaction do
|
|
channel = @account.web_widgets.create!(attrs)
|
|
inbox = @account.inboxes.create!(name: @account.name, channel: channel)
|
|
InboxMember.find_or_create_by!(inbox: inbox, user: @user)
|
|
inbox
|
|
end
|
|
rescue StandardError => e
|
|
Rails.logger.error "[WidgetCreation] #{e.message}"
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
def existing_web_widget_inbox
|
|
@account.inboxes.find_by(channel_type: 'Channel::WebWidget')
|
|
end
|
|
|
|
def channel_attributes
|
|
{
|
|
website_url: website_url,
|
|
widget_color: widget_color,
|
|
welcome_title: welcome_title,
|
|
welcome_tagline: welcome_tagline_text&.truncate(WELCOME_TAGLINE_MAX_LENGTH)
|
|
}
|
|
end
|
|
|
|
def brand_info
|
|
@brand_info ||= (@account.custom_attributes['brand_info'] || {}).deep_symbolize_keys
|
|
end
|
|
|
|
def website_url
|
|
@account.domain.presence || brand_info[:domain].presence
|
|
end
|
|
|
|
def widget_color
|
|
hex = brand_info[:colors]&.first&.dig(:hex)
|
|
hex.to_s.match?(/\A#\h{6}\z/) ? hex : DEFAULT_WIDGET_COLOR
|
|
end
|
|
|
|
def welcome_title
|
|
brand_info[:title].presence || @account.name
|
|
end
|
|
|
|
def welcome_tagline_text
|
|
brand_info[:slogan].presence || brand_info[:description].presence
|
|
end
|
|
end
|
|
|
|
Onboarding::WebWidgetCreationService.prepend_mod_with('Onboarding::WebWidgetCreationService')
|