Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf39a52d1 | ||
|
|
5e570d08f3 | ||
|
|
8070b0897c | ||
|
|
45a1b44b61 | ||
|
|
72c5671e09 | ||
|
|
8c0885e1d2 | ||
|
|
e92f72b318 | ||
|
|
ea04a292ed | ||
|
|
deeb6d4e3f | ||
|
|
a3bf753f4c | ||
|
|
459f225559 | ||
|
|
99de5f4257 | ||
|
|
4b417ce9e7 | ||
|
|
8697a30dc5 | ||
|
|
8565341682 | ||
|
|
9bd7daeaf8 | ||
|
|
076059cd3c | ||
|
|
67311c2c9a |
@@ -7,6 +7,8 @@ gem 'rack-cors', '2.0.0', require: 'rack/cors'
|
||||
gem 'rails', '~> 7.1'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
# Turbo for SPA-like page acceleration
|
||||
gem 'turbo-rails'
|
||||
|
||||
##-- rails application helper gems --##
|
||||
gem 'acts-as-taggable-on'
|
||||
@@ -33,6 +35,8 @@ gem 'liquid'
|
||||
gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
gem 'json_schemer'
|
||||
# used in swagger build
|
||||
gem 'json_refs'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack', '>= 6.7.0'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
@@ -196,9 +200,6 @@ group :development do
|
||||
gem 'scss_lint', require: false
|
||||
gem 'web-console', '>= 4.2.1'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs'
|
||||
|
||||
# When we want to squash migrations
|
||||
gem 'squasher'
|
||||
|
||||
|
||||
@@ -815,6 +815,9 @@ GEM
|
||||
i18n
|
||||
timeout (0.4.3)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.13)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
twilio-ruby (5.77.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
@@ -1004,6 +1007,7 @@ DEPENDENCIES
|
||||
telephone_number
|
||||
test-prof
|
||||
time_diff
|
||||
turbo-rails
|
||||
twilio-ruby (~> 5.66)
|
||||
twitty (~> 0.1.5)
|
||||
tzinfo-data
|
||||
|
||||
@@ -152,11 +152,13 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
||||
end
|
||||
|
||||
def message_already_exists?
|
||||
cw_message = conversation.messages.where(
|
||||
source_id: @messaging[:message][:mid]
|
||||
).first
|
||||
find_message_by_source_id(@messaging[:message][:mid]).present?
|
||||
end
|
||||
|
||||
cw_message.present?
|
||||
def find_message_by_source_id(source_id)
|
||||
return unless source_id
|
||||
|
||||
@message = Message.find_by(source_id: source_id)
|
||||
end
|
||||
|
||||
def all_unsupported_files?
|
||||
|
||||
@@ -68,7 +68,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
|
||||
def article_params
|
||||
params.require(:article).permit(
|
||||
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status,
|
||||
:title, :slug, :position, :content, :description, :category_id, :author_id, :associated_article_id, :status,
|
||||
:locale, meta: [:title,
|
||||
:description,
|
||||
{ tags: [] }]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
class Platform::Api::V1::UsersController < PlatformController
|
||||
# ref: https://stackoverflow.com/a/45190318/939299
|
||||
# set resource is called for other actions already in platform controller
|
||||
# we want to add login to that chain as well
|
||||
before_action(only: [:login]) { set_resource }
|
||||
before_action(only: [:login]) { validate_platform_app_permissible }
|
||||
# we want to add login and token to that chain as well
|
||||
before_action(only: [:login, :token]) { set_resource }
|
||||
before_action(only: [:login, :token]) { validate_platform_app_permissible }
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -18,6 +18,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
render json: { url: @resource.generate_sso_link }
|
||||
end
|
||||
|
||||
def token; end
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(user_update_params)
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
class Public::Api::V1::Portals::SearchController < Public::Api::V1::Portals::BaseController
|
||||
before_action :portal
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@query = params[:query]
|
||||
@articles = @portal.articles.published
|
||||
|
||||
@articles = @articles.search(search_params) if @query.present?
|
||||
|
||||
@articles = @articles.page(params[:page]).per(10) # Add pagination
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_params
|
||||
params.permit(:query, :locale, :sort, :status, :page)
|
||||
end
|
||||
end
|
||||
+23
-1
@@ -42,6 +42,7 @@ const initialState = {
|
||||
conversationFaqs: false,
|
||||
memories: false,
|
||||
},
|
||||
temperature: 1,
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
@@ -87,6 +88,7 @@ const updateStateFromAssistant = assistant => {
|
||||
conversationFaqs: config.feature_faq || false,
|
||||
memories: config.feature_memory || false,
|
||||
};
|
||||
state.temperature = config.temperature || 1;
|
||||
};
|
||||
|
||||
const handleBasicInfoUpdate = async () => {
|
||||
@@ -136,6 +138,7 @@ const handleInstructionsUpdate = async () => {
|
||||
const payload = {
|
||||
config: {
|
||||
...props.assistant.config,
|
||||
temperature: state.temperature || 1,
|
||||
instructions: state.instructions,
|
||||
},
|
||||
};
|
||||
@@ -212,7 +215,7 @@ watch(
|
||||
|
||||
<!-- Instructions Section -->
|
||||
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Editor
|
||||
v-model="state.instructions"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
|
||||
@@ -221,6 +224,25 @@ watch(
|
||||
:message-type="formErrors.instructions ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-4">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
|
||||
</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<input
|
||||
v-model="state.temperature"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-n-slate-11 italic">
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -3,6 +3,9 @@ import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnec
|
||||
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||
|
||||
const { isImpersonating } = useImpersonation();
|
||||
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
@@ -52,6 +55,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
};
|
||||
|
||||
onPresenceUpdate = data => {
|
||||
if (isImpersonating.value) return;
|
||||
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
|
||||
this.app.$store.dispatch('agents/updatePresence', data.users);
|
||||
this.app.$store.dispatch('setCurrentUserAvailability', data.users);
|
||||
|
||||
@@ -367,7 +367,6 @@
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"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.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
@@ -410,6 +409,10 @@
|
||||
"PLACEHOLDER": "Enter assistant name",
|
||||
"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": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Enter assistant description",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Rails from '@rails/ujs';
|
||||
import Turbolinks from 'turbolinks';
|
||||
import { Turbo } from '@hotwired/turbo-rails';
|
||||
import '../portal/application.scss';
|
||||
import { InitializationHelpers } from '../portal/portalHelpers';
|
||||
|
||||
Rails.start();
|
||||
Turbolinks.start();
|
||||
Turbo.start();
|
||||
|
||||
document.addEventListener('turbolinks:load', InitializationHelpers.onLoad);
|
||||
document.addEventListener('turbo:load', InitializationHelpers.onLoad);
|
||||
|
||||
@@ -90,18 +90,29 @@ export default {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.searchTerm.trim()) {
|
||||
const url = `/hc/${this.portalSlug}/${this.localeCode}/search?query=${encodeURIComponent(this.searchTerm)}`;
|
||||
window.location.href = url;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-on-clickaway="closeSearch" class="relative w-full max-w-5xl my-4">
|
||||
<PublicSearchInput
|
||||
:search-term="searchTerm"
|
||||
:search-placeholder="searchTranslations.searchPlaceholder"
|
||||
@update:search-term="onUpdateSearchTerm"
|
||||
@focus="openSearch"
|
||||
/>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<PublicSearchInput
|
||||
:search-term="searchTerm"
|
||||
:search-placeholder="searchTranslations.searchPlaceholder"
|
||||
@update:search-term="onUpdateSearchTerm"
|
||||
@focus="openSearch"
|
||||
/>
|
||||
<button type="submit" class="sr-only">
|
||||
{{ searchTranslations.submit || 'Search' }}
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
v-if="shouldShowSearchBox"
|
||||
class="absolute w-full top-14"
|
||||
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
<p class="py-1 px-3" :class="getClassName(element)">
|
||||
<a
|
||||
:href="`#${element.slug}`"
|
||||
data-turbolinks="false"
|
||||
data-turbo="false"
|
||||
class="font-medium text-sm tracking-[0.28px] cursor-pointer"
|
||||
:class="elementTextStyles(element)"
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const getHeadingsfromTheArticle = () => {
|
||||
const slug = slugifyWithCounter(element.innerText);
|
||||
element.id = slug;
|
||||
element.className = 'scroll-mt-24 heading';
|
||||
element.innerHTML += `<a class="permalink text-slate-600 ml-3" href="#${slug}" title="${element.innerText}" data-turbolinks="false">#</a>`;
|
||||
element.innerHTML += `<a class="permalink text-slate-600 ml-3" href="#${slug}" title="${element.innerText}" data-turbo="false">#</a>`;
|
||||
rows.push({
|
||||
slug,
|
||||
title: element.innerText,
|
||||
@@ -144,7 +144,7 @@ export const InitializationHelpers = {
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = window.location.hash;
|
||||
a['data-turbolinks'] = false;
|
||||
a['data-turbo'] = false;
|
||||
a.click();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -204,3 +204,5 @@ class ActionCableListener < BaseListener
|
||||
::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, payload)
|
||||
end
|
||||
end
|
||||
|
||||
ActionCableListener.prepend_mod_with('ActionCableListener')
|
||||
|
||||
@@ -31,7 +31,11 @@ class Crm::Leadsquared::LeadFinderService
|
||||
def find_by_phone_number(contact)
|
||||
return if contact.phone_number.blank?
|
||||
|
||||
search_by_field(contact.phone_number)
|
||||
lead_data = Crm::Leadsquared::Mappers::ContactMapper.map(contact)
|
||||
|
||||
return if lead_data.blank? || lead_data['Mobile'].nil?
|
||||
|
||||
search_by_field(lead_data['Mobile'])
|
||||
end
|
||||
|
||||
def search_by_field(value)
|
||||
|
||||
@@ -20,11 +20,29 @@ class Crm::Leadsquared::Mappers::ContactMapper
|
||||
'FirstName' => contact.name.presence,
|
||||
'LastName' => contact.last_name.presence,
|
||||
'EmailAddress' => contact.email.presence,
|
||||
'Mobile' => contact.phone_number.presence,
|
||||
'Mobile' => formatted_phone_number,
|
||||
'Source' => brand_name
|
||||
}.compact
|
||||
end
|
||||
|
||||
def formatted_phone_number
|
||||
# it seems like leadsquared needs a different phone number format
|
||||
# it's not documented anywhere, so don't bother trying to look up online
|
||||
# After some trial and error, I figured out the format, its +<country_code>-<national_number>
|
||||
return nil if contact.phone_number.blank?
|
||||
|
||||
parsed = TelephoneNumber.parse(contact.phone_number)
|
||||
return contact.phone_number unless parsed.valid?
|
||||
|
||||
country_code = parsed.country.country_code
|
||||
e164 = parsed.e164_number
|
||||
e164 = e164.sub(/^\+/, '')
|
||||
|
||||
national_number = e164.sub(/^#{Regexp.escape(country_code)}/, '')
|
||||
|
||||
"+#{country_code}-#{national_number}"
|
||||
end
|
||||
|
||||
def brand_name
|
||||
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ class MessageTemplates::HookExecutionService
|
||||
return false unless message.incoming?
|
||||
# prevents sending out-of-office message if an agent has sent a message in last 5 minutes
|
||||
# ensures better UX by not interrupting active conversations at the end of business hours
|
||||
return false if conversation.messages.outgoing.exists?(['created_at > ?', 5.minutes.ago])
|
||||
return false if conversation.messages.outgoing.where(private: false).exists?(['created_at > ?', 5.minutes.ago])
|
||||
|
||||
inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ By default, it renders:
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<meta name= "turbolinks-cache-control" content= "no-cache">
|
||||
<meta name= "turbo-cache-control" content= "no-cache">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<%= vite_client_tag %>
|
||||
@@ -39,7 +39,7 @@ By default, it renders:
|
||||
<% else %>
|
||||
<title><%= @portal.page_title%></title>
|
||||
<% end %>
|
||||
|
||||
|
||||
<% if @portal.logo.present? %>
|
||||
<link rel="icon" href="<%= url_for(@portal.logo) %>">
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
json.access_token @resource.access_token.token
|
||||
json.expiry nil
|
||||
json.user do
|
||||
json.id @resource.id
|
||||
json.name @resource.name
|
||||
json.display_name @resource.display_name
|
||||
json.email @resource.email
|
||||
json.pubsub_token @resource.pubsub_token
|
||||
end
|
||||
@@ -0,0 +1,184 @@
|
||||
<% content_for :head do %>
|
||||
<title><%= I18n.t('public_portal.search.results_for', query: @query) %> | <%= @portal.name %></title>
|
||||
<% end %>
|
||||
|
||||
<% if !@is_plain_layout_enabled %>
|
||||
<div id="portal-bg" class="bg-white dark:bg-slate-900 shadow-inner">
|
||||
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6">
|
||||
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col">
|
||||
<div class="flex flex-row items-center gap-px mb-6">
|
||||
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer hover:underline leading-8 font-semibold"
|
||||
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
|
||||
<%= I18n.t('public_portal.common.home') %>
|
||||
</a>
|
||||
<span class="w-4 h-4 [&>svg]:w-3 [&>svg]:h-3 flex items-center justify-center text-xs text-slate-500 dark:text-slate-300">
|
||||
<%= render partial: 'icons/chevron-right' %>
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100">
|
||||
<%= I18n.t('public_portal.search.results') %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
|
||||
<%= I18n.t('public_portal.search.results_for', query: @query) %>
|
||||
</h1>
|
||||
|
||||
<!-- Search form for the results page -->
|
||||
<form class="w-full my-4" action="<%= request.path %>" method="GET" data-search-form>
|
||||
<input type="hidden" name="locale" value="<%= params[:locale] %>">
|
||||
<input type="text"
|
||||
name="query"
|
||||
value="<%= @query %>"
|
||||
placeholder="<%= I18n.t('public_portal.search.search_placeholder') %>"
|
||||
data-search-input
|
||||
autofocus
|
||||
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
<button type="submit" class="sr-only">
|
||||
<%= I18n.t('public_portal.search.submit') %>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="max-w-5xl mx-auto space-y-4 w-full px-4 md:px-8 py-4">
|
||||
<div class="flex items-center flex-row">
|
||||
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:underline hover:cursor-pointer leading-8 font-semibold"
|
||||
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
|
||||
<%= I18n.t('public_portal.common.home') %>
|
||||
</a>
|
||||
<span class="w-4 h-4 [&>svg]:w-3 [&>svg]:h-3 flex items-center justify-center text-xs text-slate-500 dark:text-slate-300">
|
||||
<%= render partial: 'icons/chevron-right' %>
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100">
|
||||
<%= I18n.t('public_portal.search.results') %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
|
||||
<%= I18n.t('public_portal.search.results_for', query: @query) %>
|
||||
</h1>
|
||||
|
||||
<!-- Search form for the results page -->
|
||||
<form class="w-full my-4" action="<%= request.path %>" method="GET" data-search-form>
|
||||
<input type="hidden" name="locale" value="<%= params[:locale] %>">
|
||||
<input type="text"
|
||||
name="query"
|
||||
value="<%= @query %>"
|
||||
placeholder="<%= I18n.t('public_portal.search.search_placeholder') %>"
|
||||
data-search-input
|
||||
autofocus
|
||||
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
<button type="submit" class="sr-only">
|
||||
<%= I18n.t('public_portal.search.submit') %>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
document.addEventListener('turbo:load', function() {
|
||||
const searchInputs = document.querySelectorAll('[data-search-input]');
|
||||
|
||||
searchInputs.forEach(function(input) {
|
||||
let debounceTimer;
|
||||
|
||||
// Move cursor to end of input text after autofocus
|
||||
if (input.value.length > 0) {
|
||||
setTimeout(function() {
|
||||
input.setSelectionRange(input.value.length, input.value.length);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
const form = input.closest('[data-search-form]');
|
||||
const query = input.value.trim();
|
||||
|
||||
// Clear existing timer
|
||||
clearTimeout(debounceTimer);
|
||||
|
||||
// Only search if there's a query and it's different from current
|
||||
if (query.length > 0) {
|
||||
debounceTimer = setTimeout(function() {
|
||||
// Check if the query has actually changed
|
||||
const currentQuery = new URLSearchParams(window.location.search).get('query') || '';
|
||||
if (query !== currentQuery) {
|
||||
// Use Turbo.visit for SPA-like navigation
|
||||
const formData = new FormData(form);
|
||||
const searchParams = new URLSearchParams(formData);
|
||||
const url = form.action + '?' + searchParams.toString();
|
||||
Turbo.visit(url);
|
||||
}
|
||||
}, 500); // 500ms debounce
|
||||
}
|
||||
});
|
||||
|
||||
// Handle manual form submission
|
||||
input.closest('[data-search-form]').addEventListener('submit', function(e) {
|
||||
clearTimeout(debounceTimer);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="max-w-5xl w-full mx-auto px-4 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
|
||||
<div class="w-full flex flex-col gap-6 flex-grow">
|
||||
<% if @articles.empty? %>
|
||||
<div class="h-full flex items-center justify-center bg-slate-50 dark:bg-slate-800 rounded-xl py-6">
|
||||
<p class="text-sm text-slate-500"><%= I18n.t('public_portal.search.no_results', query: @query) %></p>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
<%= I18n.t('public_portal.search.found_results', count: @articles.size) %>
|
||||
</p>
|
||||
|
||||
<% @articles.each do |article| %>
|
||||
<div class="border border-solid border-slate-100 dark:border-slate-800 rounded-lg">
|
||||
<a class="p-4 text-slate-800 dark:text-slate-50 flex justify-between content-center hover:cursor-pointer"
|
||||
href="<%= generate_article_link(@portal.slug, article.slug, @theme_from_params, @is_plain_layout_enabled) %>">
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-lg text-slate-900 tracking-[0.28px] dark:text-slate-50 font-semibold">
|
||||
<%= article.title %>
|
||||
</h3>
|
||||
<p class="text-base font-normal text-slate-600 dark:text-slate-200 line-clamp-2 break-all">
|
||||
<%= render_category_content(article.content) %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<% if article.category.present? %>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">
|
||||
<%= article.category.name %>
|
||||
</span>
|
||||
<span class="text-slate-600 dark:text-slate-400">•</span>
|
||||
<% end %>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">
|
||||
<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if @articles.respond_to?(:total_pages) && @articles.total_pages > 1 %>
|
||||
<div class="flex justify-center mt-6">
|
||||
<nav class="inline-flex">
|
||||
<% if @articles.prev_page %>
|
||||
<a href="<%= url_for(page: @articles.prev_page) %>" class="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-l-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<%= I18n.t('public_portal.common.previous') %>
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
<% if @articles.next_page %>
|
||||
<a href="<%= url_for(page: @articles.next_page) %>" class="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-r-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<%= I18n.t('public_portal.common.next') %>
|
||||
</a>
|
||||
<% end %>
|
||||
</nav>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,7 +15,6 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
||||
resource '*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
|
||||
end
|
||||
|
||||
# This is temporary fix for the issue the loading APIs from the frontend, we will remove this once we have a proper solution
|
||||
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_API_CORS', false))
|
||||
resource '/api/*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
|
||||
end
|
||||
|
||||
@@ -254,6 +254,13 @@ en:
|
||||
empty_placeholder: No results found.
|
||||
loading_placeholder: Searching...
|
||||
results_title: Search results
|
||||
results: Search Results
|
||||
results_for: "Search Results for '%{query}'"
|
||||
no_results: "No results found for '%{query}'"
|
||||
found_results:
|
||||
one: Found 1 result
|
||||
other: "Found %{count} results"
|
||||
submit: Search
|
||||
toc_header: 'On this page'
|
||||
hero:
|
||||
sub_title: Search for the articles here or browse the categories below.
|
||||
|
||||
@@ -240,6 +240,13 @@ es:
|
||||
empty_placeholder: No se encontraron resultados.
|
||||
loading_placeholder: Buscando...
|
||||
results_title: Buscar resultados
|
||||
results: Resultados de búsqueda
|
||||
results_for: "Resultados de búsqueda para '%{query}'"
|
||||
no_results: "No se encontraron resultados para '%{query}'"
|
||||
found_results:
|
||||
one: Se encontró 1 resultado
|
||||
other: "Se encontraron %{count} resultados"
|
||||
submit: Buscar
|
||||
toc_header: 'En esta página'
|
||||
hero:
|
||||
sub_title: Busque aquí los artículos o busque las categorías de abajo.
|
||||
|
||||
+4
-2
@@ -60,8 +60,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :assistant_responses
|
||||
resources :bulk_actions, only: [:create]
|
||||
resources :copilot_threads, only: [:index] do
|
||||
resources :copilot_messages, only: [:index]
|
||||
resources :copilot_threads, only: [:index, :create] do
|
||||
resources :copilot_messages, only: [:index, :create]
|
||||
end
|
||||
resources :documents, only: [:index, :show, :create, :destroy]
|
||||
end
|
||||
@@ -396,6 +396,7 @@ Rails.application.routes.draw do
|
||||
resources :users, only: [:create, :show, :update, :destroy] do
|
||||
member do
|
||||
get :login
|
||||
post :token
|
||||
end
|
||||
end
|
||||
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
|
||||
@@ -441,6 +442,7 @@ Rails.application.routes.draw do
|
||||
get 'hc/:slug', to: 'public/api/v1/portals#show'
|
||||
get 'hc/:slug/sitemap.xml', to: 'public/api/v1/portals#sitemap'
|
||||
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show'
|
||||
get 'hc/:slug/:locale/search', to: 'public/api/v1/portals/search#index', as: :portal_search
|
||||
get 'hc/:slug/:locale/articles', to: 'public/api/v1/portals/articles#index'
|
||||
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
|
||||
get 'hc/:slug/:locale/categories/:category_slug', to: 'public/api/v1/portals/categories#show'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
class RemoveUuidFromCopilotThreads < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
remove_column :copilot_threads, :uuid, :string
|
||||
|
||||
add_column :copilot_threads, :assistant_id, :integer
|
||||
add_index :copilot_threads, :assistant_id
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemoveUserIdFromCopilotMessages < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
remove_reference :copilot_messages, :user, index: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class ChangeMessageTypeToIntegerInCopilotMessages < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
remove_column :copilot_messages, :message_type
|
||||
add_column :copilot_messages, :message_type, :integer, default: 0
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :copilot_messages, :message_type
|
||||
add_column :copilot_messages, :message_type, :string, default: 'user'
|
||||
end
|
||||
end
|
||||
+4
-6
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2025_05_23_031839) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -577,27 +577,25 @@ ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
|
||||
|
||||
create_table "copilot_messages", force: :cascade do |t|
|
||||
t.bigint "copilot_thread_id", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.string "message_type", null: false
|
||||
t.jsonb "message", default: {}, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "message_type", default: 0
|
||||
t.index ["account_id"], name: "index_copilot_messages_on_account_id"
|
||||
t.index ["copilot_thread_id"], name: "index_copilot_messages_on_copilot_thread_id"
|
||||
t.index ["user_id"], name: "index_copilot_messages_on_user_id"
|
||||
end
|
||||
|
||||
create_table "copilot_threads", force: :cascade do |t|
|
||||
t.string "title", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "assistant_id"
|
||||
t.index ["account_id"], name: "index_copilot_threads_on_account_id"
|
||||
t.index ["assistant_id"], name: "index_copilot_threads_on_assistant_id"
|
||||
t.index ["user_id"], name: "index_copilot_threads_on_user_id"
|
||||
t.index ["uuid"], name: "index_copilot_threads_on_uuid", unique: true
|
||||
end
|
||||
|
||||
create_table "csat_survey_responses", force: :cascade do |t|
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
## Chatwoot Developer Documentation
|
||||
|
||||
Welcome to the official Chatwoot developer documentation. This guide contains everything you need to know about Chatwoot APIs and build custom flows on top of Chatwoot APIs.
|
||||
|
||||
### 👩💻 Development
|
||||
|
||||
Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
|
||||
|
||||
```
|
||||
npm i -g mintlify
|
||||
```
|
||||
|
||||
Run the following command at the root of your documentation (where mint.json is)
|
||||
|
||||
```
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
### 😎 Publishing Changes
|
||||
|
||||
Changes will be deployed to production automatically after pushing to the default branch.
|
||||
|
||||
You can also preview changes using PRs, which generates a preview link of the docs.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
|
||||
- Page loads as a 404 - Make sure you are running in a folder with `mint.json`
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"name": "Chatwoot Developer Docs",
|
||||
"description": "Official developer documentation for Chatwoot - the open-source customer support platform. Learn about our APIs, integrations, and development guidelines.",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.png",
|
||||
"light": "/logo/light.png"
|
||||
},
|
||||
"favicon": "/favicon.png",
|
||||
"colors": {
|
||||
"primary": "#0069ED",
|
||||
"light": "#4D9CFF",
|
||||
"dark": "#0050B4"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": {
|
||||
"family": "Haskoy",
|
||||
"weight": 500,
|
||||
"source": "https://d1j5ayohogpcd2.cloudfront.net/fonts/haskoy/Haskoy-Medium.woff2",
|
||||
"format": "woff2"
|
||||
},
|
||||
"body": {
|
||||
"family": "Haskoy",
|
||||
"weight": 400,
|
||||
"source": "https://d1j5ayohogpcd2.cloudfront.net/fonts/haskoy/Haskoy-Regular.woff2",
|
||||
"format": "woff2"
|
||||
}
|
||||
},
|
||||
"theme": "maple",
|
||||
"navigation": {
|
||||
"groups": [
|
||||
{
|
||||
"group": "API Reference",
|
||||
"pages": [
|
||||
"introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Application API",
|
||||
"description": "APIs for managing application aspects of Chatwoot",
|
||||
"includeTags": [
|
||||
"Agents",
|
||||
"Automation Rule",
|
||||
"Account AgentBots",
|
||||
"Canned Responses",
|
||||
"Contact Labels",
|
||||
"Contacts",
|
||||
"Conversation Assignments",
|
||||
"Conversation Labels",
|
||||
"Conversations",
|
||||
"Custom Attributes",
|
||||
"Custom Filters",
|
||||
"Help Center",
|
||||
"Inboxes",
|
||||
"Integrations",
|
||||
"Messages",
|
||||
"Profile",
|
||||
"Reports",
|
||||
"Teams",
|
||||
"Webhooks"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/application_swagger.json"
|
||||
},
|
||||
{
|
||||
"group": "Platform API",
|
||||
"description": "APIs for managing platform aspects of Chatwoot",
|
||||
"includeTags": [
|
||||
"Accounts",
|
||||
"Account Users",
|
||||
"AgentBots",
|
||||
"Users"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/platform_swagger.json"
|
||||
},
|
||||
{
|
||||
"group": "Client API",
|
||||
"description": "APIs for client applications",
|
||||
"includeTags": [
|
||||
"Contacts API",
|
||||
"Conversations API",
|
||||
"Messages API"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/client_swagger.json"
|
||||
},
|
||||
{
|
||||
"group": "Other APIs",
|
||||
"description": "Other Chatwoot APIs",
|
||||
"includeTags": [
|
||||
"CSAT Survey Page"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/other_swagger.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"footerSocials": {
|
||||
"twitter": "https://twitter.com/chatwootapp",
|
||||
"github": "https://github.com/chatwoot",
|
||||
"linkedin": "https://www.linkedin.com/company/chatwoot"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Introduction to Chatwoot APIs
|
||||
description: Learn how to use Chatwoot APIs to build integrations, customize chat experiences, and manage your installation.
|
||||
sidebarTitle: Introduction
|
||||
---
|
||||
|
||||
Welcome to the Chatwoot API documentation. Whether you're building custom workflows for your support team, integrating Chatwoot into your product, or managing users across installations, our APIs provide the flexibility and power to help you do more with Chatwoot.
|
||||
|
||||
Chatwoot provides three categories of APIs, each designed with a specific use case in mind:
|
||||
|
||||
- **Application APIs** – For account-level automation and agent-facing integrations.
|
||||
- **Client APIs** – For building custom chat interfaces for end-users
|
||||
- **Platform APIs** – For managing and administering installations at scale
|
||||
|
||||
---
|
||||
|
||||
## Application APIs
|
||||
|
||||
Application APIs are designed for interacting with a Chatwoot account from an agent/admin perspective. Use them to build internal tools, automate workflows, or perform bulk operations like data import/export.
|
||||
|
||||
- **Authentication**: Requires a user `access_token`, which can be generated from **Profile Settings** after logging into your Chatwoot account.
|
||||
- **Availability**: Supported on both **Cloud** and **Self-hosted** Chatwoot installations.
|
||||
- **Example**: [Google Cloud Functions Demo](https://github.com/chatwoot/google-cloud-functions-demo)
|
||||
|
||||
---
|
||||
|
||||
## Client APIs
|
||||
|
||||
Client APIs are intended for building custom messaging experiences over Chatwoot. If you're not using the native website widget or want to embed chat in your mobile app, these APIs are the way to go.
|
||||
|
||||
- **Authentication**: Uses `inbox_identifier` (from **Settings → Configuration** in API inboxes) and `contact_identifier` (returned when creating a contact).
|
||||
- **Availability**: Supported on both **Cloud** and **Self-hosted** Chatwoot installations.
|
||||
- **Examples**:
|
||||
|
||||
- [Client API Demo](https://github.com/chatwoot/client-api-demo)
|
||||
- [Flutter SDK](https://github.com/chatwoot/chatwoot-flutter-sdk)
|
||||
|
||||
---
|
||||
|
||||
## Platform APIs
|
||||
|
||||
Platform APIs are used to manage Chatwoot installations at the admin level. These APIs allow you to control users, roles, and accounts, or sync data from external authentication systems.
|
||||
|
||||
- **Authentication**: Requires an `access_token` generated by a **Platform App**, which can be created in the **Super Admin Console**.
|
||||
- **Availability**: Available on **Self-hosted** / **Managed Hosting** Chatwoot installations only.
|
||||
|
||||
---
|
||||
|
||||
Use the right API for your use case, and you'll be able to extend, customize, and integrate Chatwoot into your stack with ease.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -47,7 +47,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
||||
config: [
|
||||
:product_name, :feature_faq, :feature_memory,
|
||||
:welcome_message, :handoff_message, :resolution_message,
|
||||
:instructions
|
||||
:instructions, :temperature
|
||||
])
|
||||
end
|
||||
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
class Api::V1::Accounts::Captain::CopilotMessagesController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
before_action :set_copilot_thread
|
||||
|
||||
def index
|
||||
@copilot_messages = @copilot_thread
|
||||
.copilot_messages
|
||||
.includes(:copilot_thread)
|
||||
.order(created_at: :asc)
|
||||
.page(permitted_params[:page] || 1)
|
||||
.per(1000)
|
||||
end
|
||||
|
||||
def create
|
||||
@copilot_message = @copilot_thread.copilot_messages.create!(
|
||||
message: params[:message],
|
||||
message_type: :user
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_copilot_thread
|
||||
@copilot_thread = Current.account.copilot_threads.find_by!(
|
||||
uuid: params[:copilot_thread_id], user_id: Current.user.id
|
||||
id: params[:copilot_thread_id],
|
||||
user: Current.user
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
before_action :ensure_message, only: :create
|
||||
|
||||
def index
|
||||
@copilot_threads = Current.account.copilot_threads
|
||||
.where(user_id: Current.user.id)
|
||||
.includes(:user)
|
||||
.includes(:user, :assistant)
|
||||
.order(created_at: :desc)
|
||||
.page(permitted_params[:page] || 1)
|
||||
.per(5)
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@copilot_thread = Current.account.copilot_threads.create!(
|
||||
title: copilot_thread_params[:message],
|
||||
user: Current.user,
|
||||
assistant: assistant
|
||||
)
|
||||
|
||||
@copilot_thread.copilot_messages.create!(message_type: :user, message: copilot_thread_params[:message])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_message
|
||||
return render_could_not_create_error('Message is required') if copilot_thread_params[:message].blank?
|
||||
end
|
||||
|
||||
def assistant
|
||||
Current.account.captain_assistants.find(copilot_thread_params[:assistant_id])
|
||||
end
|
||||
|
||||
def copilot_thread_params
|
||||
params.permit(:message, :assistant_id)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:page)
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ module Captain::ChatHelper
|
||||
model: @model,
|
||||
messages: @messages,
|
||||
tools: @tool_registry&.registered_tools || [],
|
||||
response_format: { type: 'json_object' }
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: @assistant&.config&.[]('temperature').to_f || 1
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+3
@@ -1,7 +1,10 @@
|
||||
class CaptainListener < BaseListener
|
||||
include ::Events::Types
|
||||
|
||||
def conversation_resolved(event)
|
||||
conversation = extract_conversation_and_account(event)[0]
|
||||
assistant = conversation.inbox.captain_assistant
|
||||
|
||||
return unless conversation.inbox.captain_active?
|
||||
|
||||
Captain::Llm::ContactNotesService.new(assistant, conversation).generate_and_update_notes if assistant.config['feature_memory'].present?
|
||||
@@ -0,0 +1,11 @@
|
||||
module Enterprise::ActionCableListener
|
||||
include Events::Types
|
||||
def copilot_message_created(event)
|
||||
copilot_message = event.data[:copilot_message]
|
||||
copilot_thread = copilot_message.copilot_thread
|
||||
account = copilot_thread.account
|
||||
user = copilot_thread.user
|
||||
|
||||
broadcast(account, [user.pubsub_token], COPILOT_MESSAGE_CREATED, copilot_message.push_event_data)
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,7 @@ class Captain::Assistant < ApplicationRecord
|
||||
has_many :inboxes,
|
||||
through: :captain_inboxes
|
||||
has_many :messages, as: :sender, dependent: :nullify
|
||||
has_many :copilot_threads, dependent: :destroy_async
|
||||
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
|
||||
@@ -4,24 +4,47 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# message :jsonb not null
|
||||
# message_type :string not null
|
||||
# message_type :integer default("user")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# copilot_thread_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_copilot_messages_on_account_id (account_id)
|
||||
# index_copilot_messages_on_copilot_thread_id (copilot_thread_id)
|
||||
# index_copilot_messages_on_user_id (user_id)
|
||||
#
|
||||
class CopilotMessage < ApplicationRecord
|
||||
belongs_to :copilot_thread
|
||||
belongs_to :user
|
||||
belongs_to :account
|
||||
|
||||
validates :message_type, presence: true, inclusion: { in: %w[user assistant assistant_thinking] }
|
||||
before_validation :ensure_account
|
||||
|
||||
enum message_type: { user: 0, assistant: 1, assistant_thinking: 2 }
|
||||
|
||||
validates :message_type, presence: true, inclusion: { in: message_types.keys }
|
||||
validates :message, presence: true
|
||||
|
||||
after_create_commit :broadcast_message
|
||||
|
||||
def push_event_data
|
||||
{
|
||||
id: id,
|
||||
message: message,
|
||||
message_type: message_type,
|
||||
created_at: created_at.to_i,
|
||||
copilot_thread: copilot_thread.push_event_data
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_account
|
||||
self.account = copilot_thread.account
|
||||
end
|
||||
|
||||
def broadcast_message
|
||||
Rails.configuration.dispatcher.dispatch(COPILOT_MESSAGE_CREATED, Time.zone.now, copilot_message: self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,25 +2,47 @@
|
||||
#
|
||||
# Table name: copilot_threads
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# title :string not null
|
||||
# uuid :uuid not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# title :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# assistant_id :integer
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_copilot_threads_on_account_id (account_id)
|
||||
# index_copilot_threads_on_user_id (user_id)
|
||||
# index_copilot_threads_on_uuid (uuid) UNIQUE
|
||||
# index_copilot_threads_on_account_id (account_id)
|
||||
# index_copilot_threads_on_assistant_id (assistant_id)
|
||||
# index_copilot_threads_on_user_id (user_id)
|
||||
#
|
||||
class CopilotThread < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :account
|
||||
has_many :copilot_messages, dependent: :destroy
|
||||
belongs_to :assistant, class_name: 'Captain::Assistant'
|
||||
has_many :copilot_messages, dependent: :destroy_async
|
||||
|
||||
validates :title, presence: true
|
||||
validates :uuid, presence: true, uniqueness: true
|
||||
|
||||
def push_event_data
|
||||
{
|
||||
id: id,
|
||||
title: title,
|
||||
created_at: created_at.to_i,
|
||||
user: user.push_event_data,
|
||||
account_id: account_id
|
||||
}
|
||||
end
|
||||
|
||||
def previous_history
|
||||
copilot_messages
|
||||
.where(message_type: %w[user assistant])
|
||||
.order(created_at: :asc)
|
||||
.map do |copilot_message|
|
||||
{
|
||||
content: copilot_message.message,
|
||||
role: copilot_message.message_type
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/models/captain/copilot_message', formats: [:json], resource: @copilot_message
|
||||
@@ -1,8 +1,5 @@
|
||||
json.payload do
|
||||
json.array! @copilot_messages do |message|
|
||||
json.id message.id
|
||||
json.message message.message
|
||||
json.message_type message.message_type
|
||||
json.created_at message.created_at.to_i
|
||||
json.partial! 'api/v1/models/captain/copilot_message', formats: [:json], resource: message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/models/captain/copilot_thread', formats: [:json], resource: @copilot_thread
|
||||
@@ -1,12 +1,5 @@
|
||||
json.payload do
|
||||
json.array! @copilot_threads do |thread|
|
||||
json.id thread.id
|
||||
json.title thread.title
|
||||
json.uuid thread.uuid
|
||||
json.created_at thread.created_at.to_i
|
||||
json.user do
|
||||
json.id thread.user.id
|
||||
json.name thread.user.name
|
||||
end
|
||||
json.partial! 'api/v1/models/captain/copilot_thread', resource: thread
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
json.id resource.id
|
||||
json.message resource.message
|
||||
json.message_type resource.message_type
|
||||
json.created_at resource.created_at.to_i
|
||||
json.copilot_thread resource.copilot_thread.push_event_data
|
||||
json.account_id resource.account_id
|
||||
@@ -0,0 +1,6 @@
|
||||
json.id resource.id
|
||||
json.title resource.title
|
||||
json.created_at resource.created_at.to_i
|
||||
json.user resource.user.push_event_data
|
||||
json.assistant resource.assistant.push_event_data
|
||||
json.account_id resource.account_id
|
||||
@@ -6,6 +6,7 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
|
||||
VIMEO_REGEX = %r{https?://(?:www\.)?vimeo\.com/(\d+)}
|
||||
MP4_REGEX = %r{https?://(?:www\.)?.+\.(mp4)}
|
||||
ARCADE_REGEX = %r{https?://(?:www\.)?app\.arcade\.software/share/([^&/]+)}
|
||||
WISTIA_REGEX = %r{https?://(?:www\.)?([^/]+)\.wistia\.com/medias/([^&/]+)}
|
||||
|
||||
def text(node)
|
||||
content = node.string_content
|
||||
@@ -50,7 +51,8 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
|
||||
VIMEO_REGEX => :make_vimeo_embed,
|
||||
MP4_REGEX => :make_video_embed,
|
||||
LOOM_REGEX => :make_loom_embed,
|
||||
ARCADE_REGEX => :make_arcade_embed
|
||||
ARCADE_REGEX => :make_arcade_embed,
|
||||
WISTIA_REGEX => :make_wistia_embed
|
||||
}
|
||||
|
||||
embedding_methods.each do |regex, method|
|
||||
@@ -76,67 +78,30 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
|
||||
|
||||
def make_youtube_embed(youtube_match)
|
||||
video_id = youtube_match[1]
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/#{video_id}"
|
||||
frameborder="0"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
)
|
||||
EmbedRenderer.youtube(video_id)
|
||||
end
|
||||
|
||||
def make_loom_embed(loom_match)
|
||||
video_id = loom_match[1]
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://www.loom.com/embed/#{video_id}"
|
||||
frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
|
||||
</div>
|
||||
)
|
||||
EmbedRenderer.loom(video_id)
|
||||
end
|
||||
|
||||
def make_vimeo_embed(vimeo_match)
|
||||
video_id = vimeo_match[1]
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/#{video_id}?dnt=true"
|
||||
frameborder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
allowfullscreen
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
|
||||
</div>
|
||||
)
|
||||
EmbedRenderer.vimeo(video_id)
|
||||
end
|
||||
|
||||
def make_video_embed(link_url)
|
||||
%(
|
||||
<video width="640" height="360" controls>
|
||||
<source src="#{link_url}" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)
|
||||
EmbedRenderer.video(link_url)
|
||||
end
|
||||
|
||||
def make_wistia_embed(wistia_match)
|
||||
video_id = wistia_match[2]
|
||||
EmbedRenderer.wistia(video_id)
|
||||
end
|
||||
|
||||
def make_arcade_embed(arcade_match)
|
||||
video_id = arcade_match[1]
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://app.arcade.software/embed/#{video_id}"
|
||||
frameborder="0"
|
||||
webkitallowfullscreen
|
||||
mozallowfullscreen
|
||||
allowfullscreen
|
||||
allow="fullscreen"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
</div>
|
||||
)
|
||||
EmbedRenderer.arcade(video_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
module EmbedRenderer
|
||||
def self.youtube(video_id)
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/#{video_id}"
|
||||
frameborder="0"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
)
|
||||
end
|
||||
|
||||
def self.loom(video_id)
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://www.loom.com/embed/#{video_id}"
|
||||
frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
|
||||
</div>
|
||||
)
|
||||
end
|
||||
|
||||
def self.vimeo(video_id)
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/#{video_id}?dnt=true"
|
||||
frameborder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
allowfullscreen
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
|
||||
</div>
|
||||
)
|
||||
end
|
||||
|
||||
def self.video(link_url)
|
||||
%(
|
||||
<video width="640" height="360" controls>
|
||||
<source src="#{link_url}" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)
|
||||
end
|
||||
|
||||
# Generates an HTML embed for a Wistia video.
|
||||
# @param wistia_match [MatchData] A match object from the WISTIA_REGEX regex, where wistia_match[2] contains the video ID.
|
||||
def self.wistia(video_id)
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
|
||||
<script src="https://fast.wistia.com/player.js" async></script>
|
||||
<script src="https://fast.wistia.com/embed/#{video_id}.js" async type="module"></script>
|
||||
<style>
|
||||
wistia-player[media-id='#{video_id}']:not(:defined) {
|
||||
background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/#{video_id}/swatch');
|
||||
display: block;
|
||||
filter: blur(5px);
|
||||
padding-top:56.25%;
|
||||
}
|
||||
</style>
|
||||
<wistia-player
|
||||
media-id="#{video_id}"
|
||||
aspect="1.7777777777777777"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
|
||||
</wistia-player>
|
||||
</div>
|
||||
)
|
||||
end
|
||||
|
||||
def self.arcade(video_id)
|
||||
%(
|
||||
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
|
||||
<iframe
|
||||
src="https://app.arcade.software/embed/#{video_id}"
|
||||
frameborder="0"
|
||||
webkitallowfullscreen
|
||||
mozallowfullscreen
|
||||
allowfullscreen
|
||||
allow="fullscreen"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
</div>
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -54,4 +54,7 @@ module Events::Types
|
||||
# agent events
|
||||
AGENT_ADDED = 'agent.added'
|
||||
AGENT_REMOVED = 'agent.removed'
|
||||
|
||||
# copilot events
|
||||
COPILOT_MESSAGE_CREATED = 'copilot.message.created'
|
||||
end
|
||||
|
||||
+144
-10
@@ -1,22 +1,156 @@
|
||||
namespace :swagger do
|
||||
desc 'build combined swagger.json file from all the fragmented definitions and paths inside swagger folder'
|
||||
task build: :environment do
|
||||
require 'json_refs'
|
||||
require 'json_refs'
|
||||
require 'fileutils'
|
||||
require 'pathname'
|
||||
require 'yaml'
|
||||
require 'json'
|
||||
|
||||
base_path = Rails.root.join('swagger')
|
||||
Dir.chdir(base_path) do
|
||||
swagger_index = YAML.safe_load(File.open('index.yml'))
|
||||
module SwaggerTaskActions
|
||||
def self.execute_build
|
||||
swagger_dir = Rails.root.join('swagger')
|
||||
# Paths relative to swagger_dir for use within Dir.chdir
|
||||
index_yml_relative_path = 'index.yml'
|
||||
swagger_json_relative_path = 'swagger.json'
|
||||
|
||||
Dir.chdir(swagger_dir) do
|
||||
# Operations within this block are relative to swagger_dir
|
||||
swagger_index_content = File.read(index_yml_relative_path)
|
||||
swagger_index = YAML.safe_load(swagger_index_content)
|
||||
|
||||
final_build = JsonRefs.call(
|
||||
swagger_index,
|
||||
resolve_local_ref: false,
|
||||
resolve_file_ref: true,
|
||||
resolve_file_ref: true, # Uses CWD (swagger_dir) for resolving file refs
|
||||
logging: true
|
||||
)
|
||||
File.write('swagger.json', JSON.pretty_generate(final_build))
|
||||
File.write(swagger_json_relative_path, JSON.pretty_generate(final_build))
|
||||
|
||||
# For user messages, provide the absolute path
|
||||
absolute_swagger_json_path = swagger_dir.join(swagger_json_relative_path)
|
||||
puts 'Swagger build was successful.'
|
||||
puts "Generated #{base_path}/swagger.json"
|
||||
puts "Generated #{absolute_swagger_json_path}"
|
||||
puts 'Go to http://localhost:3000/swagger see the changes.'
|
||||
|
||||
# Trigger dependent task
|
||||
Rake::Task['swagger:build_tag_groups'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
def self.execute_build_tag_groups
|
||||
base_swagger_path = Rails.root.join('swagger')
|
||||
tag_groups_output_dir = base_swagger_path.join('tag_groups')
|
||||
full_spec_path = base_swagger_path.join('swagger.json')
|
||||
index_yml_path = base_swagger_path.join('index.yml')
|
||||
|
||||
full_spec = JSON.parse(File.read(full_spec_path))
|
||||
swagger_index = YAML.safe_load(File.read(index_yml_path))
|
||||
tag_groups = swagger_index['x-tagGroups']
|
||||
|
||||
FileUtils.mkdir_p(tag_groups_output_dir)
|
||||
|
||||
tag_groups.each do |tag_group|
|
||||
_process_tag_group(tag_group, full_spec, tag_groups_output_dir)
|
||||
end
|
||||
|
||||
puts 'Tag-specific swagger files generated successfully.'
|
||||
end
|
||||
|
||||
def self.execute_build_for_docs
|
||||
Rake::Task['swagger:build'].invoke # Ensure all swagger files are built first
|
||||
|
||||
developer_docs_public_path = Rails.root.join('developer-docs/public')
|
||||
tag_groups_in_dev_docs_path = developer_docs_public_path.join('swagger/tag_groups')
|
||||
source_tag_groups_path = Rails.root.join('swagger/tag_groups')
|
||||
|
||||
FileUtils.mkdir_p(tag_groups_in_dev_docs_path)
|
||||
puts 'Creating symlinks for developer-docs...'
|
||||
|
||||
symlink_files = %w[platform_swagger.json application_swagger.json client_swagger.json other_swagger.json]
|
||||
symlink_files.each do |file|
|
||||
_create_symlink(source_tag_groups_path.join(file), tag_groups_in_dev_docs_path.join(file))
|
||||
end
|
||||
|
||||
puts 'Symlinks created successfully.'
|
||||
puts 'You can now run the Mintlify dev server to preview the documentation.'
|
||||
end
|
||||
|
||||
# Private helper methods
|
||||
class << self
|
||||
private
|
||||
|
||||
def _process_tag_group(tag_group, full_spec, output_dir)
|
||||
group_name = tag_group['name']
|
||||
tags_in_current_group = tag_group['tags']
|
||||
|
||||
tag_spec = JSON.parse(JSON.generate(full_spec)) # Deep clone
|
||||
|
||||
tag_spec['paths'] = _filter_paths_for_tag_group(tag_spec['paths'], tags_in_current_group)
|
||||
tag_spec['tags'] = _filter_tags_for_tag_group(tag_spec['tags'], tags_in_current_group)
|
||||
|
||||
output_filename = _determine_output_filename(group_name)
|
||||
File.write(output_dir.join(output_filename), JSON.pretty_generate(tag_spec))
|
||||
end
|
||||
|
||||
def _operation_has_matching_tags?(operation, tags_in_group)
|
||||
return false unless operation.is_a?(Hash)
|
||||
|
||||
operation_tags = operation['tags']
|
||||
return false unless operation_tags.is_a?(Array)
|
||||
|
||||
operation_tags.intersect?(tags_in_group)
|
||||
end
|
||||
|
||||
def _filter_paths_for_tag_group(paths_spec, tags_in_group)
|
||||
(paths_spec || {}).filter_map do |path, path_item|
|
||||
next unless path_item.is_a?(Hash)
|
||||
|
||||
operations_with_group_tags = path_item.any? do |_method, operation|
|
||||
_operation_has_matching_tags?(operation, tags_in_group)
|
||||
end
|
||||
[path, path_item] if operations_with_group_tags
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def _filter_tags_for_tag_group(tags_spec, tags_in_group)
|
||||
if tags_spec.is_a?(Array)
|
||||
tags_spec.select { |tag_definition| tags_in_group.include?(tag_definition['name']) }
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def _determine_output_filename(group_name)
|
||||
return 'other_swagger.json' if group_name.casecmp('others').zero?
|
||||
|
||||
sanitized_group_name = group_name.downcase.tr(' ', '_').gsub(/[^a-z0-9_]+/, '')
|
||||
"#{sanitized_group_name}_swagger.json"
|
||||
end
|
||||
|
||||
def _create_symlink(source_file_path, target_file_path)
|
||||
FileUtils.rm_f(target_file_path) # Remove existing to avoid errors
|
||||
|
||||
if File.exist?(source_file_path)
|
||||
relative_source_path = Pathname.new(source_file_path).relative_path_from(target_file_path.dirname)
|
||||
FileUtils.ln_sf(relative_source_path, target_file_path)
|
||||
else
|
||||
puts "Warning: Source file #{source_file_path} not found. Skipping symlink for #{File.basename(target_file_path)}."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :swagger do
|
||||
desc 'build combined swagger.json file from all the fragmented definitions and paths inside swagger folder'
|
||||
task build: :environment do
|
||||
SwaggerTaskActions.execute_build
|
||||
end
|
||||
|
||||
desc 'build separate swagger files for each tag group'
|
||||
task build_tag_groups: :environment do
|
||||
SwaggerTaskActions.execute_build_tag_groups
|
||||
end
|
||||
|
||||
desc 'build swagger files and create symlinks in developer-docs'
|
||||
task build_for_docs: :environment do
|
||||
SwaggerTaskActions.execute_build_for_docs
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -39,6 +39,7 @@
|
||||
"@formkit/vue": "^1.6.7",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@highlightjs/vue-plugin": "^2.1.0",
|
||||
"@hotwired/turbo-rails": "^8.0.13",
|
||||
"@iconify-json/material-symbols": "^1.2.10",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
"@lk77/vue3-color": "^3.0.6",
|
||||
@@ -84,7 +85,6 @@
|
||||
"snakecase-keys": "^8.0.1",
|
||||
"timezone-phone-codes": "^0.0.2",
|
||||
"tinykeys": "^3.0.0",
|
||||
"turbolinks": "^5.2.0",
|
||||
"urlpattern-polyfill": "^10.0.0",
|
||||
"video.js": "7.18.1",
|
||||
"videojs-record": "4.5.0",
|
||||
|
||||
Generated
+22
-8
@@ -37,6 +37,9 @@ importers:
|
||||
'@highlightjs/vue-plugin':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0(highlight.js@11.10.0)(vue@3.5.12(typescript@5.6.2))
|
||||
'@hotwired/turbo-rails':
|
||||
specifier: ^8.0.13
|
||||
version: 8.0.13
|
||||
'@iconify-json/material-symbols':
|
||||
specifier: ^1.2.10
|
||||
version: 1.2.10
|
||||
@@ -172,9 +175,6 @@ importers:
|
||||
tinykeys:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
turbolinks:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
urlpattern-polyfill:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
@@ -865,6 +865,13 @@ packages:
|
||||
'@histoire/vendors@0.17.17':
|
||||
resolution: {integrity: sha512-QZvmffdoJlLuYftPIkOU5Q2FPAdG2JjMuQ5jF7NmEl0n1XnmbMqtRkdYTZ4eF6CO1KLZ0Zyf6gBQvoT1uWNcjA==}
|
||||
|
||||
'@hotwired/turbo-rails@8.0.13':
|
||||
resolution: {integrity: sha512-6SCnnOSzhtaJ0pNkAjncZxjtKsK3sP/vPEkCnTXBXSHkr+vF7DTZkOlwjhms1DbbQNTsjCsBoKvzSMbh/omSCQ==}
|
||||
|
||||
'@hotwired/turbo@8.0.13':
|
||||
resolution: {integrity: sha512-M7qXUqcGab6G5PKOiwhgbByTtrPgKPFCTMNQ52QhzUEXEqmp0/ApEguUesh/FPiUjrmFec+3lq98KsWnYY2C7g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -1036,6 +1043,9 @@ packages:
|
||||
'@rails/actioncable@6.1.3':
|
||||
resolution: {integrity: sha512-m02524MR9cTnUNfGz39Lkx9jVvuL0tle4O7YgvouJ7H83FILxzG1nQ5jw8pAjLAr9XQGu+P1sY4SKE3zyhCNjw==}
|
||||
|
||||
'@rails/actioncable@7.2.201':
|
||||
resolution: {integrity: sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw==}
|
||||
|
||||
'@rails/ujs@7.1.400':
|
||||
resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==}
|
||||
|
||||
@@ -4710,9 +4720,6 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
turbolinks@5.2.0:
|
||||
resolution: {integrity: sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5723,6 +5730,13 @@ snapshots:
|
||||
|
||||
'@histoire/vendors@0.17.17': {}
|
||||
|
||||
'@hotwired/turbo-rails@8.0.13':
|
||||
dependencies:
|
||||
'@hotwired/turbo': 8.0.13
|
||||
'@rails/actioncable': 7.2.201
|
||||
|
||||
'@hotwired/turbo@8.0.13': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
@@ -5942,6 +5956,8 @@ snapshots:
|
||||
|
||||
'@rails/actioncable@6.1.3': {}
|
||||
|
||||
'@rails/actioncable@7.2.201': {}
|
||||
|
||||
'@rails/ujs@7.1.400': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.40.2':
|
||||
@@ -10200,8 +10216,6 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
turbolinks@5.2.0: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
@@ -79,6 +79,18 @@ describe Messages::Instagram::MessageBuilder do
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
end
|
||||
|
||||
it 'discards duplicate messages from webhook events with the same message_id' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
initial_message_count = instagram_inbox.messages.count
|
||||
expect(initial_message_count).to be 1
|
||||
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
expect(instagram_inbox.messages.count).to eq initial_message_count
|
||||
end
|
||||
|
||||
it 'creates message for shared reel' do
|
||||
messaging = shared_reel_params[:entry][0]['messaging'][0]
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
@@ -151,11 +163,15 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'does not create message for unsupported file type' do
|
||||
conversation
|
||||
|
||||
# try to create a message with unsupported file type
|
||||
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
|
||||
described_class.new(messaging, instagram_inbox, outgoing_echo: false).perform
|
||||
|
||||
# Conversation should exist but no new message should be created
|
||||
expect(instagram_inbox.conversations.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 0
|
||||
end
|
||||
|
||||
@@ -189,19 +189,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
|
||||
conversation
|
||||
|
||||
# create a message with unsupported file type
|
||||
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
contact_inbox
|
||||
|
||||
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
|
||||
# we would have contact created but message and attachments won't be created
|
||||
# Conversation should exist but no new message should be created
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 0
|
||||
|
||||
contact = instagram_messenger_channel.inbox.contacts.first
|
||||
|
||||
expect(contact.name).to eq('Jane Dae')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,6 +76,54 @@ RSpec.describe 'Platform Users API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /platform/api/v1/users/{user_id}/token' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
post "/platform/api/v1/users/#{user.id}/token"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
post "/platform/api/v1/users/#{user.id}/token", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
post "/platform/api/v1/users/#{user.id}/token", headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns access token for the user with expiry and user info' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: user)
|
||||
|
||||
post "/platform/api/v1/users/#{user.id}/token",
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
|
||||
# Check access token and expiry
|
||||
expect(data['access_token']).to eq(user.access_token.token)
|
||||
expect(data['expiry']).to be_nil
|
||||
|
||||
# Check user info
|
||||
expect(data['user']).to include(
|
||||
'id' => user.id,
|
||||
'name' => user.name,
|
||||
'display_name' => user.display_name,
|
||||
'email' => user.email,
|
||||
'pubsub_token' => user.pubsub_token
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /platform/api/v1/users/' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
|
||||
+50
-5
@@ -4,12 +4,12 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :r
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account, role: :administrator) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user) }
|
||||
let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, user: user, account: account) }
|
||||
let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, account: account) }
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.uuid}/copilot_messages' do
|
||||
describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.id}/copilot_messages' do
|
||||
context 'when it is an authenticated user' do
|
||||
it 'returns all messages' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.uuid}/copilot_messages",
|
||||
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.id}/copilot_messages",
|
||||
headers: user.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
@@ -20,9 +20,9 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :r
|
||||
end
|
||||
end
|
||||
|
||||
context 'when thread uuid is invalid' do
|
||||
context 'when thread id is invalid' do
|
||||
it 'returns not found error' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/invalid-uuid/copilot_messages",
|
||||
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/999999999/copilot_messages",
|
||||
headers: user.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
@@ -30,4 +30,49 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :r
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.id}/copilot_messages' do
|
||||
context 'when it is an authenticated user' do
|
||||
it 'creates a new message' do
|
||||
message_content = { 'content' => 'This is a test message' }
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.id}/copilot_messages",
|
||||
params: { message: message_content },
|
||||
headers: user.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change(CopilotMessage, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(CopilotMessage.last.message).to eq(message_content)
|
||||
expect(CopilotMessage.last.message_type).to eq('user')
|
||||
expect(CopilotMessage.last.copilot_thread_id).to eq(copilot_thread.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when thread does not exist' do
|
||||
it 'returns not found error' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/999999999/copilot_messages",
|
||||
params: { message: { text: 'Test message' } },
|
||||
headers: user.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when thread belongs to another user' do
|
||||
let(:another_user) { create(:user, account: account) }
|
||||
let(:another_thread) { create(:captain_copilot_thread, account: account, user: another_user) }
|
||||
|
||||
it 'returns not found error' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{another_thread.id}/copilot_messages",
|
||||
params: { message: { text: 'Test message' } },
|
||||
headers: user.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+62
-1
@@ -18,7 +18,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
context 'when it is an authenticated user' do
|
||||
it 'fetches copilot threads for the current user' do
|
||||
# Create threads for the current agent
|
||||
create_list(:captain_copilot_thread, 3, account: account, user: agent)
|
||||
@@ -47,4 +47,65 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads' do
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:valid_params) { { message: 'Hello, how can you help me?', assistant_id: assistant.id } }
|
||||
|
||||
context 'when it is an un-authenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
||||
params: valid_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'with invalid params' do
|
||||
it 'returns error when message is blank' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
||||
params: { message: '', assistant_id: assistant.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json_response[:error]).to eq('Message is required')
|
||||
end
|
||||
|
||||
it 'returns error when assistant_id is invalid' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
||||
params: { message: 'Hello', assistant_id: 0 },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid params' do
|
||||
it 'creates a new copilot thread with initial message' do
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
||||
params: valid_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change(CopilotThread, :count).by(1)
|
||||
.and change(CopilotMessage, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
thread = CopilotThread.last
|
||||
expect(thread.title).to eq(valid_params[:message])
|
||||
expect(thread.user_id).to eq(agent.id)
|
||||
expect(thread.assistant_id).to eq(assistant.id)
|
||||
|
||||
message = thread.copilot_messages.last
|
||||
expect(message.message_type).to eq('user')
|
||||
expect(message.message).to eq(valid_params[:message])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ActionCableListener do
|
||||
describe '#copilot_message_created' do
|
||||
let(:event_name) { :copilot_message_created }
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
|
||||
let(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread) }
|
||||
let(:event) { Events::Base.new(event_name, Time.zone.now, copilot_message: copilot_message) }
|
||||
let(:listener) { described_class.instance }
|
||||
|
||||
it 'broadcasts message to the user' do
|
||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||
[user.pubsub_token],
|
||||
'copilot.message.created',
|
||||
copilot_message.push_event_data.merge(account_id: account.id)
|
||||
)
|
||||
|
||||
listener.copilot_message_created(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe CaptainListener do
|
||||
let(:listener) { described_class.instance }
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account, config: { feature_memory: true, feature_faq: true }) }
|
||||
|
||||
describe '#conversation_resolved' do
|
||||
let(:agent) { create(:user, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: agent) }
|
||||
|
||||
let(:event_name) { :conversation_resolved }
|
||||
let(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
|
||||
|
||||
before do
|
||||
create(:captain_inbox, captain_assistant: assistant, inbox: inbox)
|
||||
end
|
||||
|
||||
context 'when feature_memory is enabled' do
|
||||
before do
|
||||
assistant.config['feature_memory'] = true
|
||||
assistant.config['feature_faq'] = false
|
||||
assistant.save!
|
||||
end
|
||||
|
||||
it 'generates and updates notes' do
|
||||
expect(Captain::Llm::ContactNotesService)
|
||||
.to receive(:new)
|
||||
.with(assistant, conversation)
|
||||
.and_return(instance_double(Captain::Llm::ContactNotesService, generate_and_update_notes: nil))
|
||||
expect(Captain::Llm::ConversationFaqService).not_to receive(:new)
|
||||
|
||||
listener.conversation_resolved(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature_faq is enabled' do
|
||||
before do
|
||||
assistant.config['feature_faq'] = true
|
||||
assistant.config['feature_memory'] = false
|
||||
assistant.save!
|
||||
end
|
||||
|
||||
it 'generates and deduplicates FAQs' do
|
||||
expect(Captain::Llm::ConversationFaqService)
|
||||
.to receive(:new)
|
||||
.with(assistant, conversation)
|
||||
.and_return(instance_double(Captain::Llm::ConversationFaqService, generate_and_deduplicate: false))
|
||||
expect(Captain::Llm::ContactNotesService).not_to receive(:new)
|
||||
|
||||
listener.conversation_resolved(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CopilotMessage, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:copilot_thread) }
|
||||
it { is_expected.to belong_to(:account) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:message_type) }
|
||||
it { is_expected.to validate_presence_of(:message) }
|
||||
it { is_expected.to validate_inclusion_of(:message_type).in_array(described_class.message_types.keys) }
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
|
||||
|
||||
describe '#ensure_account' do
|
||||
it 'sets the account from the copilot thread before validation' do
|
||||
message = build(:captain_copilot_message, copilot_thread: copilot_thread, account: nil)
|
||||
message.valid?
|
||||
expect(message.account).to eq(copilot_thread.account)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#broadcast_message' do
|
||||
it 'dispatches COPILOT_MESSAGE_CREATED event after create' do
|
||||
message = build(:captain_copilot_message, copilot_thread: copilot_thread)
|
||||
|
||||
expect(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
.with(COPILOT_MESSAGE_CREATED, anything, copilot_message: message)
|
||||
|
||||
message.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#push_event_data' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
|
||||
let(:message_content) { { 'content' => 'Test message' } }
|
||||
let(:copilot_message) do
|
||||
create(:captain_copilot_message,
|
||||
copilot_thread: copilot_thread,
|
||||
message_type: 'user',
|
||||
message: message_content)
|
||||
end
|
||||
|
||||
it 'returns the correct event data' do
|
||||
event_data = copilot_message.push_event_data
|
||||
|
||||
expect(event_data[:id]).to eq(copilot_message.id)
|
||||
expect(event_data[:message]).to eq(message_content)
|
||||
expect(event_data[:message_type]).to eq('user')
|
||||
expect(event_data[:created_at]).to eq(copilot_message.created_at.to_i)
|
||||
expect(event_data[:copilot_thread]).to eq(copilot_thread.push_event_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CopilotThread, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:account) }
|
||||
it { is_expected.to belong_to(:assistant).class_name('Captain::Assistant') }
|
||||
it { is_expected.to have_many(:copilot_messages).dependent(:destroy_async) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:title) }
|
||||
end
|
||||
|
||||
describe '#push_event_data' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant, title: 'Test Thread') }
|
||||
|
||||
it 'returns the correct event data' do
|
||||
event_data = copilot_thread.push_event_data
|
||||
|
||||
expect(event_data[:id]).to eq(copilot_thread.id)
|
||||
expect(event_data[:title]).to eq('Test Thread')
|
||||
expect(event_data[:created_at]).to eq(copilot_thread.created_at.to_i)
|
||||
expect(event_data[:user]).to eq(user.push_event_data)
|
||||
expect(event_data[:account_id]).to eq(account.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#previous_history' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
|
||||
|
||||
context 'when there are messages in the thread' do
|
||||
before do
|
||||
create(:captain_copilot_message, copilot_thread: copilot_thread, message_type: 'user', message: { 'content' => 'User message' })
|
||||
create(:captain_copilot_message, copilot_thread: copilot_thread, message_type: 'assistant_thinking', message: { 'content' => 'Thinking...' })
|
||||
create(:captain_copilot_message, copilot_thread: copilot_thread, message_type: 'assistant', message: { 'content' => 'Assistant message' })
|
||||
end
|
||||
|
||||
it 'returns only user and assistant messages in chronological order' do
|
||||
history = copilot_thread.previous_history
|
||||
|
||||
expect(history.length).to eq(2)
|
||||
expect(history[0][:role]).to eq('user')
|
||||
expect(history[0][:content]).to eq({ 'content' => 'User message' })
|
||||
expect(history[1][:role]).to eq('assistant')
|
||||
expect(history[1][:content]).to eq({ 'content' => 'Assistant message' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no messages in the thread' do
|
||||
it 'returns an empty array' do
|
||||
expect(copilot_thread.previous_history).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,8 @@
|
||||
FactoryBot.define do
|
||||
factory :captain_copilot_message, class: 'CopilotMessage' do
|
||||
account
|
||||
user
|
||||
copilot_thread { association :captain_copilot_thread }
|
||||
message { { content: 'This is a test message' } }
|
||||
message_type { 'user' }
|
||||
message_type { 0 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,6 @@ FactoryBot.define do
|
||||
account
|
||||
user
|
||||
title { Faker::Lorem.sentence }
|
||||
uuid { SecureRandom.uuid }
|
||||
assistant { create(:captain_assistant, account: account) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -143,6 +143,17 @@ describe CustomMarkdownRenderer do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when link is a wistia URL' do
|
||||
let(:wistia_url) { 'https://chatwoot.wistia.com/medias/kjwjeq6f9i' }
|
||||
|
||||
it 'renders a custom element with Wistia embed code' do
|
||||
output = render_markdown_link(wistia_url)
|
||||
expect(output).to include('<script src="https://fast.wistia.com/player.js" async></script>')
|
||||
expect(output).to include('<wistia-player')
|
||||
expect(output).to include('media-id="kjwjeq6f9i"')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multiple links including Arcade are present' do
|
||||
it 'renders Arcade embed along with other content types' do
|
||||
markdown = "\n[arcade](https://app.arcade.software/share/ARCADE_ID)\n\n[youtube](https://www.youtube.com/watch?v=VIDEO_ID)\n"
|
||||
|
||||
@@ -203,4 +203,24 @@ describe ActionCableListener do
|
||||
listener.conversation_updated(event)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#copilot_message_created' do
|
||||
let(:event_name) { :copilot_message_created }
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
|
||||
let(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread) }
|
||||
let(:event) { Events::Base.new(event_name, Time.zone.now, copilot_message: copilot_message) }
|
||||
|
||||
it 'broadcasts message to the user' do
|
||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||
[user.pubsub_token],
|
||||
'copilot.message.created',
|
||||
copilot_message.push_event_data
|
||||
)
|
||||
|
||||
listener.copilot_message_created(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,6 +16,7 @@ RSpec.describe Crm::Leadsquared::Mappers::ContactMapper do
|
||||
name: 'John',
|
||||
last_name: 'Doe',
|
||||
email: 'john@example.com',
|
||||
# the phone number is intentionally wrong
|
||||
phone_number: '+1234567890'
|
||||
)
|
||||
|
||||
@@ -29,6 +30,19 @@ RSpec.describe Crm::Leadsquared::Mappers::ContactMapper do
|
||||
'Source' => 'Test Brand'
|
||||
)
|
||||
end
|
||||
|
||||
it 'represents the phone number correctly' do
|
||||
contact.update!(
|
||||
name: 'John',
|
||||
last_name: 'Doe',
|
||||
email: 'john@example.com',
|
||||
phone_number: '+917507684392'
|
||||
)
|
||||
|
||||
mapped_data = described_class.map(contact)
|
||||
|
||||
expect(mapped_data).to include('Mobile' => '+91-7507684392')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -194,23 +194,44 @@ describe MessageTemplates::HookExecutionService do
|
||||
expect(out_of_office_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'does not call ::MessageTemplates::Template::OutOfOffice when there are recent outgoing messages' do
|
||||
contact = create(:contact)
|
||||
conversation = create(:conversation, contact: contact)
|
||||
context 'with recent outgoing messages' do
|
||||
it 'does not call ::MessageTemplates::Template::OutOfOffice when there are recent outgoing messages' do
|
||||
contact = create(:contact)
|
||||
conversation = create(:conversation, contact: contact)
|
||||
|
||||
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
|
||||
conversation.inbox.working_hours.today.update!(closed_all_day: true)
|
||||
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
|
||||
conversation.inbox.working_hours.today.update!(closed_all_day: true)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :outgoing, created_at: 2.minutes.ago)
|
||||
create(:message, conversation: conversation, message_type: :outgoing, created_at: 2.minutes.ago)
|
||||
|
||||
out_of_office_service = double
|
||||
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
out_of_office_service = double
|
||||
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: conversation)
|
||||
create(:message, conversation: conversation)
|
||||
|
||||
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
|
||||
expect(out_of_office_service).not_to have_received(:perform)
|
||||
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
|
||||
expect(out_of_office_service).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'ignores private note and calls ::MessageTemplates::Template::OutOfOffice' do
|
||||
contact = create(:contact)
|
||||
conversation = create(:conversation, contact: contact)
|
||||
|
||||
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
|
||||
conversation.inbox.working_hours.today.update!(closed_all_day: true)
|
||||
|
||||
create(:message, conversation: conversation, private: true, message_type: :outgoing, created_at: 2.minutes.ago)
|
||||
|
||||
out_of_office_service = double
|
||||
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: conversation)
|
||||
|
||||
expect(MessageTemplates::Template::OutOfOffice).to have_received(:new).with(conversation: conversation)
|
||||
expect(out_of_office_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
it 'will not calls ::MessageTemplates::Template::OutOfOffice when outgoing message' do
|
||||
|
||||
@@ -6,4 +6,4 @@ properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/request_error'
|
||||
$ref: '#/components/schemas/request_error'
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
## ---------- ERRORS ------------- ##
|
||||
## ---------- ERRORS -------------- ##
|
||||
## -------------------------------- ##
|
||||
bad_request_error:
|
||||
$ref: ./error/bad_request.yml
|
||||
request_error:
|
||||
$ref: ./error/request.yml
|
||||
|
||||
|
||||
|
||||
## ---------- RESOURCE ------------- ##
|
||||
## ---------- RESOURCE ------------ ##
|
||||
## -------------------------------- ##
|
||||
|
||||
generic_id:
|
||||
@@ -18,12 +16,20 @@ custom_attribute:
|
||||
$ref: ./resource/custom_attribute.yml
|
||||
automation_rule:
|
||||
$ref: ./resource/automation_rule.yml
|
||||
automation_rule_item:
|
||||
$ref: ./resource/automation_rule_item.yml
|
||||
portal:
|
||||
$ref: ./resource/portal.yml
|
||||
category:
|
||||
$ref: ./resource/category.yml
|
||||
article:
|
||||
$ref: ./resource/article.yml
|
||||
portal_single:
|
||||
$ref: ./resource/portal_single.yml
|
||||
portal_config:
|
||||
$ref: ./resource/portal_config.yml
|
||||
portal_logo:
|
||||
$ref: ./resource/portal_logo.yml
|
||||
portal_meta:
|
||||
$ref: ./resource/portal_meta.yml
|
||||
portal_item:
|
||||
$ref: ./resource/portal_item.yml
|
||||
category:
|
||||
$ref: ./resource/category.yml
|
||||
article:
|
||||
@@ -40,6 +46,8 @@ agent:
|
||||
$ref: ./resource/agent.yml
|
||||
inbox:
|
||||
$ref: ./resource/inbox.yml
|
||||
inbox_contact:
|
||||
$ref: ./resource/inbox_contact.yml
|
||||
agent_bot:
|
||||
$ref: ./resource/agent_bot.yml
|
||||
contact_inboxes:
|
||||
@@ -52,6 +60,8 @@ webhook:
|
||||
$ref: ./resource/webhook.yml
|
||||
account:
|
||||
$ref: ./resource/account.yml
|
||||
account_user:
|
||||
$ref: ./resource/account_user.yml
|
||||
platform_account:
|
||||
$ref: ./resource/platform_account.yml
|
||||
team:
|
||||
@@ -71,13 +81,18 @@ public_message:
|
||||
public_inbox:
|
||||
$ref: ./resource/public/inbox.yml
|
||||
|
||||
|
||||
## ---------- REQUEST------------- ##
|
||||
## ---------- REQUEST ------------- ##
|
||||
## -------------------------------- ##
|
||||
|
||||
account_create_update_payload:
|
||||
$ref: ./request/account/create_update_payload.yml
|
||||
|
||||
account_user_create_update_payload:
|
||||
$ref: ./request/account_user/create_update_payload.yml
|
||||
|
||||
platform_agent_bot_create_update_payload:
|
||||
$ref: ./request/platform/agent_bot/create_update_payload.yml
|
||||
|
||||
agent_bot_create_update_payload:
|
||||
$ref: ./request/agent_bot/create_update_payload.yml
|
||||
|
||||
@@ -89,21 +104,36 @@ canned_response_create_update_payload:
|
||||
|
||||
custom_attribute_create_update_payload:
|
||||
$ref: ./request/custom_attribute/create_update_payload.yml
|
||||
## contact
|
||||
contact_create:
|
||||
$ref: ./request/contact/create.yml
|
||||
contact_update:
|
||||
$ref: ./request/contact/update.yml
|
||||
|
||||
## conversation
|
||||
conversation_message_create:
|
||||
$ref: ./request/conversation/create_message.yml
|
||||
## Agent
|
||||
agent_create_payload:
|
||||
$ref: ./request/agent/create_payload.yml
|
||||
agent_update_payload:
|
||||
$ref: ./request/agent/update_payload.yml
|
||||
|
||||
# Team request Payload
|
||||
## Contact
|
||||
contact_create_payload:
|
||||
$ref: ./request/contact/create_payload.yml
|
||||
contact_update_payload:
|
||||
$ref: ./request/contact/update_payload.yml
|
||||
|
||||
## Conversation
|
||||
conversation_create_payload:
|
||||
$ref: ./request/conversation/create_payload.yml
|
||||
conversation_message_create_payload:
|
||||
$ref: ./request/conversation/create_message_payload.yml
|
||||
|
||||
# Inbox
|
||||
inbox_create_payload:
|
||||
$ref: ./request/inbox/create_payload.yml
|
||||
inbox_update_payload:
|
||||
$ref: ./request/inbox/update_payload.yml
|
||||
|
||||
# Team
|
||||
team_create_update_payload:
|
||||
$ref: ./request/team/create_update_payload.yml
|
||||
|
||||
# Custom Filter request Payload
|
||||
# Custom Filter
|
||||
custom_filter_create_update_payload:
|
||||
$ref: ./request/custom_filter/create_update_payload.yml
|
||||
|
||||
@@ -128,7 +158,6 @@ category_create_update_payload:
|
||||
article_create_update_payload:
|
||||
$ref: ./request/portal/article_create_update_payload.yml
|
||||
|
||||
|
||||
## public requests
|
||||
public_contact_create_update_payload:
|
||||
$ref: ./request/public/contact/create_update_payload.yml
|
||||
@@ -141,37 +170,36 @@ public_message_update_payload:
|
||||
public_conversation_create_payload:
|
||||
$ref: ./request/public/conversation/create_payload.yml
|
||||
|
||||
## ---------- RESPONSE ------------- ##
|
||||
## ---------- RESPONSE ------------ ##
|
||||
## -------------------------------- ##
|
||||
|
||||
## contact
|
||||
## Contact
|
||||
extended_contact:
|
||||
allOf:
|
||||
- $ref: '#/definitions/contact'
|
||||
- $ref: '#/components/schemas/contact'
|
||||
- $ref: ./resource/extension/contact/show.yml
|
||||
contact_base:
|
||||
allOf:
|
||||
- $ref: '#/definitions/generic_id'
|
||||
- $ref: '#/definitions/contact'
|
||||
- $ref: '#/components/schemas/generic_id'
|
||||
- $ref: '#/components/schemas/contact'
|
||||
contact_list:
|
||||
type: array
|
||||
description: 'array of contacts'
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/definitions/generic_id'
|
||||
- $ref: '#/definitions/contact'
|
||||
- $ref: '#/components/schemas/contact'
|
||||
contact_conversations:
|
||||
type: array
|
||||
description: 'array of conversations'
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/definitions/conversation'
|
||||
- $ref: '#/components/schemas/conversation'
|
||||
- $ref: ./resource/extension/contact/conversation.yml
|
||||
- $ref: ./resource/extension/conversation/with_display_id.yml
|
||||
contact_labels:
|
||||
$ref: ./resource/extension/contact/labels.yml
|
||||
|
||||
## conversation
|
||||
## Conversation
|
||||
conversation_list:
|
||||
$ref: ./resource/extension/conversation/list.yml
|
||||
conversation_show:
|
||||
@@ -181,9 +209,33 @@ conversation_status_toggle:
|
||||
conversation_labels:
|
||||
$ref: ./resource/extension/conversation/labels.yml
|
||||
|
||||
|
||||
## report
|
||||
## Report
|
||||
account_summary:
|
||||
$ref: './resource/reports/summary.yml'
|
||||
agent_conversation_metrics:
|
||||
$ref: './resource/reports/conversation/agent.yml'
|
||||
|
||||
contact_detail:
|
||||
$ref: ./resource/contact_detail.yml
|
||||
message_detailed:
|
||||
$ref: ./resource/message_detailed.yml
|
||||
conversation_meta:
|
||||
$ref: ./resource/conversation_meta.yml
|
||||
conversation_messages:
|
||||
$ref: ./resource/conversation_messages.yml
|
||||
contact_meta:
|
||||
$ref: ./resource/contact_meta.yml
|
||||
contact_inbox:
|
||||
$ref: ./resource/contact_inbox.yml
|
||||
contact_list_item:
|
||||
$ref: ./resource/contact_list_item.yml
|
||||
contacts_list_response:
|
||||
$ref: ./resource/contacts_list_response.yml
|
||||
contact_show_response:
|
||||
$ref: ./resource/contact_show_response.yml
|
||||
contact_conversation_message:
|
||||
$ref: ./resource/contact_conversation_message.yml
|
||||
contact_conversations_response:
|
||||
$ref: ./resource/contact_conversations_response.yml
|
||||
contactable_inboxes_response:
|
||||
$ref: ./resource/contactable_inboxes_response.yml
|
||||
|
||||
@@ -3,3 +3,29 @@ properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name of the account
|
||||
example: 'My Account'
|
||||
locale:
|
||||
type: string
|
||||
description: The locale of the account
|
||||
example: 'en'
|
||||
domain:
|
||||
type: string
|
||||
description: The domain of the account
|
||||
example: 'example.com'
|
||||
support_email:
|
||||
type: string
|
||||
description: The support email of the account
|
||||
example: 'support@example.com'
|
||||
status:
|
||||
type: string
|
||||
enum: ['active', 'suspended']
|
||||
description: The status of the account
|
||||
example: 'active'
|
||||
limits:
|
||||
type: object
|
||||
description: The limits of the account
|
||||
example: {}
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: The custom attributes of the account
|
||||
example: {}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
type: object
|
||||
required:
|
||||
- user_id
|
||||
- role
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
description: The ID of the user
|
||||
example: 1
|
||||
role:
|
||||
type: string
|
||||
description: whether user is an administrator or agent
|
||||
example: administrator
|
||||
@@ -0,0 +1,28 @@
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- email
|
||||
- role
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Full Name of the agent
|
||||
example: 'John Doe'
|
||||
email:
|
||||
type: string
|
||||
description: Email of the Agent
|
||||
example: 'john.doe@acme.inc'
|
||||
role:
|
||||
type: string
|
||||
enum: ['agent', 'administrator']
|
||||
description: Whether its administrator or agent
|
||||
example: 'agent'
|
||||
availability_status:
|
||||
type: string
|
||||
enum: ['available', 'busy', 'offline']
|
||||
description: The availability setting of the agent.
|
||||
example: 'available'
|
||||
auto_offline:
|
||||
type: boolean
|
||||
description: Whether the availability status of agent is configured to go offline automatically when away.
|
||||
example: true
|
||||
@@ -0,0 +1,18 @@
|
||||
type: object
|
||||
required:
|
||||
- role
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
enum: ['agent', 'administrator']
|
||||
description: Whether its administrator or agent
|
||||
example: 'agent'
|
||||
availability_status:
|
||||
type: string
|
||||
enum: ['available', 'busy', 'offline']
|
||||
description: The availability status of the agent.
|
||||
example: 'available'
|
||||
auto_offline:
|
||||
type: boolean
|
||||
description: Whether the availability status of agent is configured to go offline automatically when away.
|
||||
example: true
|
||||
@@ -3,9 +3,28 @@ properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the agent bot
|
||||
example: 'My Agent Bot'
|
||||
description:
|
||||
type: string
|
||||
description: The description about the agent bot
|
||||
description: The description of the agent bot
|
||||
example: 'This is a sample agent bot'
|
||||
outgoing_url:
|
||||
type: string
|
||||
description: The webhook URL for the bot
|
||||
example: 'https://example.com/webhook'
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
description: Send the form data with the avatar image binary or use the avatar_url
|
||||
avatar_url:
|
||||
type: string
|
||||
description: The url to a jpeg, png file for the agent bot avatar
|
||||
example: https://example.com/avatar.png
|
||||
bot_type:
|
||||
type: integer
|
||||
description: The type of the bot (0 for webhook)
|
||||
example: 0
|
||||
bot_config:
|
||||
type: object
|
||||
description: The configuration for the bot
|
||||
example: {}
|
||||
|
||||
@@ -36,6 +36,6 @@ properties:
|
||||
example:
|
||||
attribute_key: content
|
||||
filter_operator: contains
|
||||
query_operator: nil
|
||||
query_operator: OR
|
||||
values:
|
||||
- help
|
||||
|
||||
@@ -3,6 +3,8 @@ properties:
|
||||
content:
|
||||
type: string
|
||||
description: Message content for canned response
|
||||
example: 'Hello, {{contact.name}}! Welcome to our service.'
|
||||
short_code:
|
||||
type: string
|
||||
description: Short Code for quick access of the canned response
|
||||
example: 'welcome'
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
type: object
|
||||
required:
|
||||
- inbox_id
|
||||
properties:
|
||||
inbox_id:
|
||||
type: number
|
||||
description: ID of the inbox to which the contact belongs
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
description: name of the contact
|
||||
example: Alice
|
||||
email:
|
||||
type: string
|
||||
description: email of the contact
|
||||
example: alice@acme.inc
|
||||
blocked:
|
||||
type: boolean
|
||||
description: whether the contact is blocked or not
|
||||
example: false
|
||||
phone_number:
|
||||
type: string
|
||||
description: phone number of the contact
|
||||
example: '+123456789'
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
description: Send the form data with the avatar image binary or use the avatar_url
|
||||
avatar_url:
|
||||
type: string
|
||||
description: The url to a jpeg, png file for the contact avatar
|
||||
example: https://example.com/avatar.png
|
||||
identifier:
|
||||
type: string
|
||||
description: A unique identifier for the contact in external system
|
||||
example: '1234567890'
|
||||
additional_attributes:
|
||||
type: object
|
||||
description: An object where you can store additional attributes for contact. example {"type":"customer", "age":30}
|
||||
example: { 'type': 'customer', 'age': 30 }
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}, this should have a valid custom attribute definition.
|
||||
example: {}
|
||||
@@ -1,25 +0,0 @@
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: name of the contact
|
||||
email:
|
||||
type: string
|
||||
description: email of the contact
|
||||
phone_number:
|
||||
type: string
|
||||
description: phone number of the contact
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
description: Send the form data with the avatar image binary or use the avatar_url
|
||||
avatar_url:
|
||||
type: string
|
||||
description: The url to a jpeg, png file for the contact avatar
|
||||
identifier:
|
||||
type: string
|
||||
description: A unique identifier for the contact in external system
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}
|
||||
|
||||
+15
-5
@@ -1,18 +1,21 @@
|
||||
type: object
|
||||
required:
|
||||
- inbox_id
|
||||
properties:
|
||||
inbox_id:
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
description: name of the contact
|
||||
example: Alice
|
||||
email:
|
||||
type: string
|
||||
description: email of the contact
|
||||
example: alice@acme.inc
|
||||
blocked:
|
||||
type: boolean
|
||||
description: whether the contact is blocked or not
|
||||
example: false
|
||||
phone_number:
|
||||
type: string
|
||||
description: phone number of the contact
|
||||
example: '+123456789'
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
@@ -20,9 +23,16 @@ properties:
|
||||
avatar_url:
|
||||
type: string
|
||||
description: The url to a jpeg, png file for the contact avatar
|
||||
example: https://example.com/avatar.png
|
||||
identifier:
|
||||
type: string
|
||||
description: A unique identifier for the contact in external system
|
||||
example: '1234567890'
|
||||
additional_attributes:
|
||||
type: object
|
||||
description: An object where you can store additional attributes for contact. example {"type":"customer", "age":30}
|
||||
example: { 'type': 'customer', 'age': 30 }
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}
|
||||
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}, this should have a valid custom attribute definition.
|
||||
example: {}
|
||||
+16
-7
@@ -5,20 +5,29 @@ properties:
|
||||
content:
|
||||
type: string
|
||||
description: The content of the message
|
||||
example: 'Hello, how can I help you?'
|
||||
message_type:
|
||||
type: string
|
||||
enum: ['outgoing', 'incoming']
|
||||
description: The type of the message
|
||||
example: 'outgoing'
|
||||
private:
|
||||
type: boolean
|
||||
description: Flag to identify if it is a private note
|
||||
example: false
|
||||
content_type:
|
||||
type: string
|
||||
enum: ['text', 'input_email', 'cards', 'input_select', 'form' , 'article']
|
||||
example: 'cards'
|
||||
description: 'if you want to create custom message types'
|
||||
enum: ['text', 'input_email', 'cards', 'input_select', 'form', 'article']
|
||||
description: Content type of the message
|
||||
example: 'text'
|
||||
content_attributes:
|
||||
type: object
|
||||
description: attributes based on your content type
|
||||
description: Attributes based on the content type
|
||||
example: {}
|
||||
campaign_id:
|
||||
type: integer
|
||||
description: The campaign id to which the message belongs
|
||||
example: 1
|
||||
template_params:
|
||||
type: object
|
||||
description: The template params for the message in case of whatsapp Channel
|
||||
@@ -35,8 +44,8 @@ properties:
|
||||
type: string
|
||||
description: Language of the template
|
||||
example: en_US
|
||||
processed_params:
|
||||
processed_params:
|
||||
type: object
|
||||
description: The processed param values for template variables in template
|
||||
example:
|
||||
1: "Chatwoot"
|
||||
example:
|
||||
1: 'Chatwoot'
|
||||
@@ -0,0 +1,79 @@
|
||||
type: object
|
||||
required:
|
||||
- source_id
|
||||
- inbox_id
|
||||
properties:
|
||||
source_id:
|
||||
type: string
|
||||
description: Conversation source id
|
||||
example: '1234567890'
|
||||
inbox_id:
|
||||
type: integer
|
||||
description: 'Id of inbox in which the conversation is created <br/> Allowed Inbox Types: Website, Phone, Api, Email'
|
||||
example: 1
|
||||
contact_id:
|
||||
type: integer
|
||||
description: Contact Id for which conversation is created
|
||||
example: 1
|
||||
additional_attributes:
|
||||
type: object
|
||||
description: Lets you specify attributes like browser information
|
||||
example:
|
||||
{
|
||||
browser: 'Chrome',
|
||||
browser_version: '89.0.4389.82',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
}
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: The object to save custom attributes for conversation, accepts custom attributes key and value
|
||||
example: { attribute_key: attribute_value, priority_conversation_number: 3 }
|
||||
status:
|
||||
type: string
|
||||
enum: ['open', 'resolved', 'pending']
|
||||
description: Specify the conversation whether it's pending, open, closed
|
||||
example: open
|
||||
assignee_id:
|
||||
type: integer
|
||||
description: Agent Id for assigning a conversation to an agent
|
||||
example: 1
|
||||
team_id:
|
||||
type: integer
|
||||
description: Team Id for assigning a conversation to a team\
|
||||
example: 1
|
||||
snoozed_until:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Snoozed until date time
|
||||
example: '2030-07-21T17:32:28Z'
|
||||
message:
|
||||
type: object
|
||||
description: The initial message to be sent to the conversation
|
||||
required: ['content']
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description: The content of the message
|
||||
example: 'Hello, how can I help you?'
|
||||
template_params:
|
||||
type: object
|
||||
description: The template params for the message in case of whatsapp Channel
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name of the template
|
||||
example: 'sample_issue_resolution'
|
||||
category:
|
||||
type: string
|
||||
description: Category of the template
|
||||
example: UTILITY
|
||||
language:
|
||||
type: string
|
||||
description: Language of the template
|
||||
example: en_US
|
||||
processed_params:
|
||||
type: object
|
||||
description: The processed param values for template variables in template
|
||||
example:
|
||||
1: 'Chatwoot'
|
||||
@@ -3,20 +3,34 @@ properties:
|
||||
attribute_display_name:
|
||||
type: string
|
||||
description: Attribute display name
|
||||
example: 'Custom Attribute'
|
||||
attribute_display_type:
|
||||
type: integer
|
||||
description: Attribute display type (text- 0, number- 1, currency- 2, percent- 3, link- 4, date- 5, list- 6, checkbox- 7)
|
||||
example: 0
|
||||
attribute_description:
|
||||
type: string
|
||||
description: Attribute description
|
||||
example: 'This is a custom attribute'
|
||||
attribute_key:
|
||||
type: string
|
||||
description: Attribute unique key value
|
||||
example: 'custom_attribute'
|
||||
attribute_values:
|
||||
type: array
|
||||
description: Attribute values
|
||||
items:
|
||||
type: string
|
||||
example: ['value1', 'value2']
|
||||
attribute_model:
|
||||
type: integer
|
||||
description: Attribute type(conversation_attribute- 0, contact_attribute- 1)
|
||||
example: 0
|
||||
regex_pattern:
|
||||
type: string
|
||||
description: Regex pattern (Only applicable for type- text). The regex pattern is used to validate the attribute value(s).
|
||||
example: '^[a-zA-Z0-9]+$'
|
||||
regex_cue:
|
||||
type: string
|
||||
description: Regex cue message (Only applicable for type- text). The cue message is shown when the regex pattern is not matched.
|
||||
example: 'Please enter a valid value'
|
||||
|
||||
@@ -3,10 +3,13 @@ properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the custom filter
|
||||
example: 'My Custom Filter'
|
||||
type:
|
||||
type: string
|
||||
enum: ["conversation", "contact", "report"]
|
||||
enum: ['conversation', 'contact', 'report']
|
||||
description: The description about the custom filter
|
||||
example: 'conversation'
|
||||
query:
|
||||
type: object
|
||||
description: A query that needs to be saved as a custom filter
|
||||
example: {}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the inbox
|
||||
example: 'Support'
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
description: Image file for avatar
|
||||
greeting_enabled:
|
||||
type: boolean
|
||||
description: Enable greeting message
|
||||
example: true
|
||||
greeting_message:
|
||||
type: string
|
||||
description: Greeting message to be displayed on the widget
|
||||
example: Hello, how can I help you?
|
||||
enable_email_collect:
|
||||
type: boolean
|
||||
description: Enable email collection
|
||||
example: true
|
||||
csat_survey_enabled:
|
||||
type: boolean
|
||||
description: Enable CSAT survey
|
||||
example: true
|
||||
enable_auto_assignment:
|
||||
type: boolean
|
||||
description: Enable Auto Assignment
|
||||
example: true
|
||||
working_hours_enabled:
|
||||
type: boolean
|
||||
description: Enable working hours
|
||||
example: true
|
||||
out_of_office_message:
|
||||
type: string
|
||||
description: Out of office message to be displayed on the widget
|
||||
example: We are currently out of office. Please leave a message and we will get back to you.
|
||||
timezone:
|
||||
type: string
|
||||
description: Timezone of the inbox
|
||||
example: 'America/New_York'
|
||||
allow_messages_after_resolved:
|
||||
type: boolean
|
||||
description: Allow messages after conversation is resolved
|
||||
example: true
|
||||
lock_to_single_conversation:
|
||||
type: boolean
|
||||
description: Lock to single conversation
|
||||
example: true
|
||||
portal_id:
|
||||
type: integer
|
||||
description: Id of the help center portal to attach to the inbox
|
||||
example: 1
|
||||
sender_name_type:
|
||||
type: string
|
||||
description: Sender name type for the inbox
|
||||
enum: ['friendly', 'professional']
|
||||
example: 'friendly'
|
||||
business_name:
|
||||
type: string
|
||||
description: Business name for the inbox
|
||||
example: 'My Business'
|
||||
channel:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: Type of the channel
|
||||
enum:
|
||||
['web_widget', 'api', 'email', 'line', 'telegram', 'whatsapp', 'sms']
|
||||
example: web_widget
|
||||
website_url:
|
||||
type: string
|
||||
description: URL at which the widget will be loaded
|
||||
example: 'https://example.com'
|
||||
welcome_title:
|
||||
type: string
|
||||
description: Welcome title to be displayed on the widget
|
||||
example: 'Welcome to our support'
|
||||
welcome_tagline:
|
||||
type: string
|
||||
description: Welcome tagline to be displayed on the widget
|
||||
example: 'We are here to help you'
|
||||
widget_color:
|
||||
type: string
|
||||
description: A Hex-color string used to customize the widget
|
||||
example: '#FF5733'
|
||||
@@ -0,0 +1,82 @@
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the inbox
|
||||
example: 'Support'
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
description: Image file for avatar
|
||||
greeting_enabled:
|
||||
type: boolean
|
||||
description: Enable greeting message
|
||||
example: true
|
||||
greeting_message:
|
||||
type: string
|
||||
description: Greeting message to be displayed on the widget
|
||||
example: Hello, how can I help you?
|
||||
enable_email_collect:
|
||||
type: boolean
|
||||
description: Enable email collection
|
||||
example: true
|
||||
csat_survey_enabled:
|
||||
type: boolean
|
||||
description: Enable CSAT survey
|
||||
example: true
|
||||
enable_auto_assignment:
|
||||
type: boolean
|
||||
description: Enable Auto Assignment
|
||||
example: true
|
||||
working_hours_enabled:
|
||||
type: boolean
|
||||
description: Enable working hours
|
||||
example: true
|
||||
out_of_office_message:
|
||||
type: string
|
||||
description: Out of office message to be displayed on the widget
|
||||
example: We are currently out of office. Please leave a message and we will get back to you.
|
||||
timezone:
|
||||
type: string
|
||||
description: Timezone of the inbox
|
||||
example: 'America/New_York'
|
||||
allow_messages_after_resolved:
|
||||
type: boolean
|
||||
description: Allow messages after conversation is resolved
|
||||
example: true
|
||||
lock_to_single_conversation:
|
||||
type: boolean
|
||||
description: Lock to single conversation
|
||||
example: true
|
||||
portal_id:
|
||||
type: integer
|
||||
description: Id of the help center portal to attach to the inbox
|
||||
example: 1
|
||||
sender_name_type:
|
||||
type: string
|
||||
description: Sender name type for the inbox
|
||||
enum: ['friendly', 'professional']
|
||||
example: 'friendly'
|
||||
business_name:
|
||||
type: string
|
||||
description: Business name for the inbox
|
||||
example: 'My Business'
|
||||
channel:
|
||||
type: object
|
||||
properties:
|
||||
website_url:
|
||||
type: string
|
||||
description: URL at which the widget will be loaded
|
||||
example: 'https://example.com'
|
||||
welcome_title:
|
||||
type: string
|
||||
description: Welcome title to be displayed on the widget
|
||||
example: 'Welcome to our support'
|
||||
welcome_tagline:
|
||||
type: string
|
||||
description: Welcome tagline to be displayed on the widget
|
||||
example: 'We are here to help you'
|
||||
widget_color:
|
||||
type: string
|
||||
description: A Hex-color string used to customize the widget
|
||||
example: '#FF5733'
|
||||
@@ -1,11 +1,18 @@
|
||||
type: object
|
||||
properties:
|
||||
app_id:
|
||||
type: string
|
||||
type: integer
|
||||
description: The ID of app for which integration hook is being created
|
||||
example: 1
|
||||
inbox_id:
|
||||
type: string
|
||||
type: integer
|
||||
description: The inbox ID, if the hook is an inbox hook
|
||||
example: 1
|
||||
status:
|
||||
type: integer
|
||||
description: The status of the integration (0 for inactive, 1 for active)
|
||||
example: 1
|
||||
settings:
|
||||
type: object
|
||||
description: The settings required by the integration
|
||||
example: {}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
description: The status of the integration (0 for inactive, 1 for active)
|
||||
example: 1
|
||||
settings:
|
||||
type: object
|
||||
description: The settings required by the integration
|
||||
example: {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the agent bot
|
||||
example: 'My Agent Bot'
|
||||
description:
|
||||
type: string
|
||||
description: The description of the agent bot
|
||||
example: 'This is a sample agent bot'
|
||||
outgoing_url:
|
||||
type: string
|
||||
description: The webhook URL for the bot
|
||||
example: 'https://example.com/webhook'
|
||||
account_id:
|
||||
type: integer
|
||||
description: The account ID to associate the agent bot with
|
||||
example: 1
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
description: Send the form data with the avatar image binary or use the avatar_url
|
||||
avatar_url:
|
||||
type: string
|
||||
description: The url to a jpeg, png file for the agent bot avatar
|
||||
example: https://example.com/avatar.png
|
||||
@@ -1,34 +1,51 @@
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
title:
|
||||
type: string
|
||||
description: The text content.
|
||||
meta:
|
||||
type: object
|
||||
description: Use for search
|
||||
example: { tags: ['article_name'], title: 'article title', description: 'article description' }
|
||||
description: The title of the article
|
||||
example: 'Article Title'
|
||||
slug:
|
||||
type: string
|
||||
description: The slug of the article
|
||||
example: 'article-title'
|
||||
position:
|
||||
type: integer
|
||||
description: article position in category
|
||||
status:
|
||||
type: integer
|
||||
example: ['draft', 'published', 'archived']
|
||||
title:
|
||||
example: 1
|
||||
content:
|
||||
type: string
|
||||
slug:
|
||||
description: The text content.
|
||||
example: 'This is the content of the article'
|
||||
description:
|
||||
type: string
|
||||
views:
|
||||
type: integer
|
||||
portal_id:
|
||||
type: integer
|
||||
account_id:
|
||||
type: integer
|
||||
author_id:
|
||||
type: integer
|
||||
description: The description of the article
|
||||
example: 'This is the description of the article'
|
||||
category_id:
|
||||
type: integer
|
||||
folder_id:
|
||||
description: The category id of the article
|
||||
example: 1
|
||||
author_id:
|
||||
type: integer
|
||||
description: The author agent id of the article
|
||||
example: 1
|
||||
associated_article_id:
|
||||
type: integer
|
||||
description: To associate similar articles to each other, e.g to provide the link for the reference.
|
||||
example: 2
|
||||
status:
|
||||
type: integer
|
||||
description: The status of the article. 0 for draft, 1 for published, 2 for archived
|
||||
example: 1
|
||||
locale:
|
||||
type: string
|
||||
description: The locale of the article
|
||||
example: 'en'
|
||||
meta:
|
||||
type: object
|
||||
description: Use for search
|
||||
example:
|
||||
{
|
||||
tags: ['article_name'],
|
||||
title: 'article title',
|
||||
description: 'article description',
|
||||
}
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
description: Category description
|
||||
locale:
|
||||
type: string
|
||||
description: Category locale
|
||||
example: en/es
|
||||
name:
|
||||
type: string
|
||||
description: Category name
|
||||
slug:
|
||||
description: The name of the category
|
||||
example: 'Category Name'
|
||||
description:
|
||||
type: string
|
||||
description: Category slug
|
||||
description: A description for the category
|
||||
example: 'Category description'
|
||||
position:
|
||||
type: integer
|
||||
description: Category position in the portal list to sort
|
||||
portal_id:
|
||||
type: integer
|
||||
account_id:
|
||||
type: integer
|
||||
associated_category_id:
|
||||
type: integer
|
||||
description: To associate similar categories to each other, e.g same category of product documentation in different languages
|
||||
example: 1
|
||||
slug:
|
||||
type: string
|
||||
description: The category slug used in the URL
|
||||
example: 'category-name'
|
||||
locale:
|
||||
type: string
|
||||
description: The locale of the category
|
||||
example: en
|
||||
icon:
|
||||
type: string
|
||||
description: The icon of the category as a string (emoji)
|
||||
example: '📚'
|
||||
parent_category_id:
|
||||
type: integer
|
||||
description: To define parent category, e.g product documentation has multiple level features in sales category or in engineering category.
|
||||
example: 1
|
||||
associated_category_id:
|
||||
type: integer
|
||||
description: To associate similar categories to each other, e.g same category of product documentation in different languages
|
||||
example: 2
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
type: object
|
||||
properties:
|
||||
archived:
|
||||
type: boolean
|
||||
description: Status to check if portal is live
|
||||
color:
|
||||
type: string
|
||||
description: Header color for help-center
|
||||
example: add color HEX string, "#fffff"
|
||||
config:
|
||||
type: object
|
||||
description: Configuration about supporting locales
|
||||
example: { allowed_locales: ['en', 'es'], default_locale: 'en' }
|
||||
description: Header color for help-center in hex format
|
||||
example: '#FFFFFF'
|
||||
custom_domain:
|
||||
type: string
|
||||
description: Custom domain to display help center.
|
||||
example: https://chatwoot.help/.
|
||||
description: Custom domain to display help center.
|
||||
example: chatwoot.help
|
||||
header_text:
|
||||
type: string
|
||||
description: Help center header
|
||||
@@ -26,11 +19,20 @@ properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name for the portal
|
||||
slug:
|
||||
type: string
|
||||
description: Slug for the portal to display in link
|
||||
example: Handbook
|
||||
page_title:
|
||||
type: string
|
||||
description: Page title for the portal
|
||||
account_id:
|
||||
type: integer
|
||||
example: Handbook
|
||||
slug:
|
||||
type: string
|
||||
description: Slug for the portal to display in link
|
||||
example: handbook
|
||||
archived:
|
||||
type: boolean
|
||||
description: Status to check if portal is live
|
||||
example: false
|
||||
config:
|
||||
type: object
|
||||
description: Configuration about supporting locales
|
||||
example: { allowed_locales: ['en', 'es'], default_locale: 'en' }
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
|
||||
type: object
|
||||
properties:
|
||||
identifier:
|
||||
type: string
|
||||
description: External identifier of the contact
|
||||
example: '1234567890'
|
||||
identifier_hash:
|
||||
type: string
|
||||
description: Identifier hash prepared for HMAC authentication
|
||||
example: 'e93275d4eba0e5679ad55f5360af00444e2a888df9b0afa3e8b691c3173725f9'
|
||||
email:
|
||||
type: string
|
||||
description: Email of the contact
|
||||
example: alice@acme.inc
|
||||
name:
|
||||
type: string
|
||||
description: Name of the contact
|
||||
example: Alice
|
||||
phone_number:
|
||||
type: string
|
||||
description: Phone number of the contact
|
||||
avatar_url:
|
||||
example: '+123456789'
|
||||
avatar:
|
||||
type: string
|
||||
description: The url to a jpeg, png file for the user avatar
|
||||
format: binary
|
||||
description: Send the form data with the avatar image binary or use the avatar_url
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: Custom attributes of the customer
|
||||
description: Custom attributes of the customer
|
||||
example: {}
|
||||
|
||||
@@ -3,3 +3,4 @@ properties:
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: Custom attributes of the conversation
|
||||
example: {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description: Content for the message
|
||||
example: 'Hello, how can I help you?'
|
||||
echo_id:
|
||||
type: string
|
||||
description: Temporary identifier which will be passed back via websockets
|
||||
|
||||
example: '1234567890'
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
|
||||
type: object
|
||||
properties:
|
||||
submitted_values:
|
||||
type: object
|
||||
description: Replies to the Bot Message Types
|
||||
description: Replies to the Bot Message Types
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the submiitted value
|
||||
example: 'My Name'
|
||||
title:
|
||||
type: string
|
||||
description: The title of the submitted value
|
||||
example: 'My Title'
|
||||
value:
|
||||
type: string
|
||||
description: The value of the submitted value
|
||||
example: 'value'
|
||||
csat_survey_response:
|
||||
type: object
|
||||
description: The CSAT survey response
|
||||
properties:
|
||||
feedback_message:
|
||||
type: string
|
||||
description: The feedback message of the CSAT survey response
|
||||
example: 'Great service!'
|
||||
rating:
|
||||
type: integer
|
||||
description: The rating of the CSAT survey response
|
||||
example: 5
|
||||
|
||||
@@ -3,9 +3,12 @@ properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the team
|
||||
example: Support Team
|
||||
description:
|
||||
type: string
|
||||
description: The description of the team
|
||||
example: This is a team of support agents
|
||||
allow_auto_assign:
|
||||
type: boolean
|
||||
description: If this setting is turned on, the system would automatically assign the conversation to an agent in the team while assigning the conversation to a team
|
||||
example: true
|
||||
|
||||
@@ -3,13 +3,20 @@ properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name of the user
|
||||
example: 'Daniel'
|
||||
display_name:
|
||||
type: string
|
||||
description: Display name of the user
|
||||
example: 'Dan'
|
||||
email:
|
||||
type: string
|
||||
description: Email of the user
|
||||
example: 'daniel@acme.inc'
|
||||
password:
|
||||
type: string
|
||||
description: Password must contain uppercase, lowercase letters, number and a special character
|
||||
example: 'Password2!'
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: Custom attributes you want to associate with the user
|
||||
|
||||
example: {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user