Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602c56d1c5 | ||
|
|
647a65d481 | ||
|
|
99ec18c95b | ||
|
|
64ba23688b | ||
|
|
2fec2e5993 | ||
|
|
090316a078 | ||
|
|
fcd604dcde | ||
|
|
13b4fdb34c | ||
|
|
45d4d3660c | ||
|
|
0ea616a6ea | ||
|
|
6b8dd3c86a | ||
|
|
61d10044a0 | ||
|
|
4378506a35 | ||
|
|
1132556512 | ||
|
|
93f18315cc | ||
|
|
d89fa09359 | ||
|
|
07dd6448f4 | ||
|
|
661326ae51 | ||
|
|
18eb53acf4 | ||
|
|
1b02ebec68 | ||
|
|
5b9f997fa0 | ||
|
|
802f0694ed |
@@ -1,62 +0,0 @@
|
||||
version: '2'
|
||||
plugins:
|
||||
rubocop:
|
||||
enabled: false
|
||||
channel: rubocop-0-73
|
||||
eslint:
|
||||
enabled: false
|
||||
csslint:
|
||||
enabled: true
|
||||
scss-lint:
|
||||
enabled: true
|
||||
brakeman:
|
||||
enabled: false
|
||||
checks:
|
||||
similar-code:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 32
|
||||
file-lines:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 300
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 50
|
||||
exclude_patterns:
|
||||
- 'spec/'
|
||||
- '**/specs/**/**'
|
||||
- '**/spec/**/**'
|
||||
- 'db/*'
|
||||
- 'bin/**/*'
|
||||
- 'db/**/*'
|
||||
- 'config/**/*'
|
||||
- 'public/**/*'
|
||||
- 'vendor/**/*'
|
||||
- 'node_modules/**/*'
|
||||
- 'lib/tasks/auto_annotate_models.rake'
|
||||
- 'app/test-matchers.js'
|
||||
- 'docs/*'
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
- 'app/javascript/dashboard/i18n/locale'
|
||||
- '**/*.stories.js'
|
||||
- 'stories/'
|
||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js'
|
||||
- 'app/javascript/shared/constants/countries.js'
|
||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
|
||||
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
|
||||
- 'app/javascript/dashboard/store/captain/storeFactory.js'
|
||||
- 'app/javascript/dashboard/i18n/index.js'
|
||||
- 'app/javascript/widget/i18n/index.js'
|
||||
- 'app/javascript/survey/i18n/index.js'
|
||||
- 'app/javascript/shared/constants/locales.js'
|
||||
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
|
||||
- '**/fixtures/**'
|
||||
- '**/*/fixtures.js'
|
||||
@@ -0,0 +1,7 @@
|
||||
*
|
||||
!configs
|
||||
!configs/**
|
||||
!hooks
|
||||
!hooks/**
|
||||
!qlty.toml
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
ignored:
|
||||
- DL3008
|
||||
@@ -0,0 +1 @@
|
||||
source-path=SCRIPTDIR
|
||||
@@ -0,0 +1,8 @@
|
||||
rules:
|
||||
document-start: disable
|
||||
quoted-strings:
|
||||
required: only-when-needed
|
||||
extra-allowed: ["{|}"]
|
||||
key-duplicates: {}
|
||||
octal-values:
|
||||
forbid-implicit-octal: true
|
||||
@@ -0,0 +1,84 @@
|
||||
# This file was automatically generated by `qlty init`.
|
||||
# You can modify it to suit your needs.
|
||||
# We recommend you to commit this file to your repository.
|
||||
#
|
||||
# This configuration is used by both Qlty CLI and Qlty Cloud.
|
||||
#
|
||||
# Qlty CLI -- Code quality toolkit for developers
|
||||
# Qlty Cloud -- Fully automated Code Health Platform
|
||||
#
|
||||
# Try Qlty Cloud: https://qlty.sh
|
||||
#
|
||||
# For a guide to configuration, visit https://qlty.sh/d/config
|
||||
# Or for a full reference, visit https://qlty.sh/d/qlty-toml
|
||||
config_version = "0"
|
||||
|
||||
exclude_patterns = [
|
||||
"*_min.*",
|
||||
"*-min.*",
|
||||
"*.min.*",
|
||||
"**/.yarn/**",
|
||||
"**/*.d.ts",
|
||||
"**/assets/**",
|
||||
"**/bower_components/**",
|
||||
"**/build/**",
|
||||
"**/cache/**",
|
||||
"**/config/**",
|
||||
"**/db/**",
|
||||
"**/deps/**",
|
||||
"**/dist/**",
|
||||
"**/extern/**",
|
||||
"**/external/**",
|
||||
"**/generated/**",
|
||||
"**/Godeps/**",
|
||||
"**/gradlew/**",
|
||||
"**/mvnw/**",
|
||||
"**/node_modules/**",
|
||||
"**/protos/**",
|
||||
"**/seed/**",
|
||||
"**/target/**",
|
||||
"**/templates/**",
|
||||
"**/testdata/**",
|
||||
"**/vendor/**", "spec/", "**/specs/**/**", "**/spec/**/**", "db/*", "bin/**/*", "db/**/*", "config/**/*", "public/**/*", "vendor/**/*", "node_modules/**/*", "lib/tasks/auto_annotate_models.rake", "app/test-matchers.js", "docs/*", "**/*.md", "**/*.yml", "app/javascript/dashboard/i18n/locale", "**/*.stories.js", "stories/", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js", "app/javascript/shared/constants/countries.js", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js", "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js", "app/javascript/dashboard/routes/dashboard/settings/automation/constants.js", "app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js", "app/javascript/dashboard/routes/dashboard/settings/reports/constants.js", "app/javascript/dashboard/store/captain/storeFactory.js", "app/javascript/dashboard/i18n/index.js", "app/javascript/widget/i18n/index.js", "app/javascript/survey/i18n/index.js", "app/javascript/shared/constants/locales.js", "app/javascript/dashboard/helper/specs/macrosFixtures.js", "app/javascript/dashboard/routes/dashboard/settings/macros/constants.js", "**/fixtures/**", "**/*/fixtures.js",
|
||||
]
|
||||
|
||||
test_patterns = [
|
||||
"**/test/**",
|
||||
"**/spec/**",
|
||||
"**/*.test.*",
|
||||
"**/*.spec.*",
|
||||
"**/*_test.*",
|
||||
"**/*_spec.*",
|
||||
"**/test_*.*",
|
||||
"**/spec_*.*",
|
||||
]
|
||||
|
||||
[smells]
|
||||
mode = "comment"
|
||||
|
||||
[smells.boolean_logic]
|
||||
threshold = 4
|
||||
|
||||
[smells.file_complexity]
|
||||
threshold = 66
|
||||
enabled = true
|
||||
|
||||
[smells.return_statements]
|
||||
threshold = 4
|
||||
|
||||
[smells.nested_control_flow]
|
||||
threshold = 4
|
||||
|
||||
[smells.function_parameters]
|
||||
threshold = 4
|
||||
|
||||
[smells.function_complexity]
|
||||
threshold = 5
|
||||
|
||||
[smells.duplication]
|
||||
enabled = true
|
||||
threshold = 20
|
||||
|
||||
[[source]]
|
||||
name = "default"
|
||||
default = true
|
||||
+1
-1
@@ -283,7 +283,7 @@ Rails/RedundantActiveRecordAllMethod:
|
||||
Enabled: false
|
||||
|
||||
Layout/TrailingEmptyLines:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
|
||||
@@ -30,7 +30,14 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def facebook_pages
|
||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
pages = []
|
||||
fb_pages = fb_object.get_connections('me', 'accounts')
|
||||
pages.concat(fb_pages)
|
||||
while fb_pages.respond_to?(:next_page) && (next_page = fb_pages.next_page)
|
||||
fb_pages = next_page
|
||||
pages.concat(fb_pages)
|
||||
end
|
||||
@page_details = mark_already_existing_facebook_pages(pages)
|
||||
end
|
||||
|
||||
def set_instagram_id(page_access_token, facebook_channel)
|
||||
|
||||
@@ -29,6 +29,6 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def campaign_params
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {}, template_params: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,4 +11,4 @@ class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::Bas
|
||||
def fetch_hook
|
||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'notion')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,4 +18,4 @@ class Api::V1::Accounts::Notion::AuthorizationsController < Api::V1::Accounts::O
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
before_action :validate_feature_enabled!
|
||||
|
||||
# POST /api/v1/accounts/:account_id/whatsapp/authorization
|
||||
# Handles the embedded signup callback data from the Facebook SDK
|
||||
def create
|
||||
validate_embedded_signup_params!
|
||||
channel = process_embedded_signup
|
||||
render_success_response(channel.inbox)
|
||||
rescue StandardError => e
|
||||
render_error_response(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_embedded_signup
|
||||
service = Whatsapp::EmbeddedSignupService.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
business_id: params[:business_id],
|
||||
waba_id: params[:waba_id],
|
||||
phone_number_id: params[:phone_number_id]
|
||||
)
|
||||
service.perform
|
||||
end
|
||||
|
||||
def render_success_response(inbox)
|
||||
render json: {
|
||||
success: true,
|
||||
id: inbox.id,
|
||||
name: inbox.name,
|
||||
channel_type: 'whatsapp'
|
||||
}
|
||||
end
|
||||
|
||||
def render_error_response(error)
|
||||
Rails.logger.error "[WHATSAPP AUTHORIZATION] Embedded signup error: #{error.message}"
|
||||
Rails.logger.error error.backtrace.join("\n")
|
||||
render json: {
|
||||
success: false,
|
||||
error: error.message
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def validate_feature_enabled!
|
||||
return if Current.account.feature_whatsapp_embedded_signup?
|
||||
|
||||
render json: {
|
||||
success: false,
|
||||
error: 'WhatsApp embedded signup is not enabled for this account'
|
||||
}, status: :forbidden
|
||||
end
|
||||
|
||||
def validate_embedded_signup_params!
|
||||
missing_params = []
|
||||
missing_params << 'code' if params[:code].blank?
|
||||
missing_params << 'business_id' if params[:business_id].blank?
|
||||
missing_params << 'waba_id' if params[:waba_id].blank?
|
||||
|
||||
return if missing_params.empty?
|
||||
|
||||
raise ArgumentError, "Required parameters are missing: #{missing_params.join(', ')}"
|
||||
end
|
||||
end
|
||||
@@ -47,6 +47,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.settings.merge!(settings_params)
|
||||
@account.sso_config.merge!(sso_config_params) if sso_config_params.present? && sso_feature_enabled?
|
||||
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
|
||||
@account.save!
|
||||
end
|
||||
@@ -95,6 +96,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
|
||||
end
|
||||
|
||||
def sso_config_params
|
||||
params.permit(sso_config: [:enabled, :provider_name, :login_url, :logout_url, :secret_key, :token_expiry])[:sso_config]
|
||||
end
|
||||
|
||||
def sso_feature_enabled?
|
||||
@account.feature_enabled?('sso')
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
@@ -67,6 +67,8 @@ class DashboardController < ActionController::Base
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||
INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''),
|
||||
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
|
||||
WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''),
|
||||
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||
AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''),
|
||||
GIT_SHA: GIT_HASH
|
||||
|
||||
@@ -33,4 +33,4 @@ class Notion::CallbacksController < OauthCallbackController
|
||||
def notion_redirect_uri
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/notion"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,8 +39,10 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'],
|
||||
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
|
||||
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
|
||||
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
|
||||
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION],
|
||||
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET],
|
||||
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT]
|
||||
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI]
|
||||
}
|
||||
|
||||
@allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS])
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
class SuperAdmin::ApplicationController < Administrate::ApplicationController
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Context
|
||||
include SuperAdmin::NavigationHelper
|
||||
|
||||
helper_method :render_vue_component
|
||||
helper_method :render_vue_component, :settings_open?, :settings_pages
|
||||
# authenticiation done via devise : SuperAdmin Model
|
||||
before_action :authenticate_super_admin!
|
||||
|
||||
|
||||
+32
-9
@@ -1,5 +1,7 @@
|
||||
# TODO: Move this values to features.yml itself
|
||||
# No need to replicate the same values in two places
|
||||
|
||||
# ------- Premium Features ------- #
|
||||
captain:
|
||||
name: 'Captain'
|
||||
description: 'Enable AI-powered conversations with your customers.'
|
||||
@@ -32,6 +34,15 @@ disable_branding:
|
||||
enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
|
||||
icon: 'icon-sailbot-fill'
|
||||
enterprise: true
|
||||
|
||||
# ------- Product Features ------- #
|
||||
help_center:
|
||||
name: 'Help Center'
|
||||
description: 'Allow agents to create help center articles and publish them in a portal.'
|
||||
enabled: true
|
||||
icon: 'icon-book-2-line'
|
||||
|
||||
# ------- Communication Channels ------- #
|
||||
live_chat:
|
||||
name: 'Live Chat'
|
||||
description: 'Improve your customer experience using a live chat on your website.'
|
||||
@@ -42,6 +53,12 @@ email:
|
||||
description: 'Manage your email customer interactions from Chatwoot.'
|
||||
enabled: true
|
||||
icon: 'icon-mail-send-fill'
|
||||
config_key: 'email'
|
||||
sms:
|
||||
name: 'SMS'
|
||||
description: 'Manage your SMS customer interactions from Chatwoot.'
|
||||
enabled: true
|
||||
icon: 'icon-message-line'
|
||||
messenger:
|
||||
name: 'Messenger'
|
||||
description: 'Stay connected with your customers on Facebook & Instagram.'
|
||||
@@ -69,22 +86,22 @@ line:
|
||||
description: 'Manage your Line customer interactions from Chatwoot.'
|
||||
enabled: true
|
||||
icon: 'icon-line-line'
|
||||
sms:
|
||||
name: 'SMS'
|
||||
description: 'Manage your SMS customer interactions from Chatwoot.'
|
||||
|
||||
# ------- OAuth & Authentication ------- #
|
||||
google:
|
||||
name: 'Google'
|
||||
description: 'Configuration for setting up Google OAuth Integration'
|
||||
enabled: true
|
||||
icon: 'icon-message-line'
|
||||
help_center:
|
||||
name: 'Help Center'
|
||||
description: 'Allow agents to create help center articles and publish them in a portal.'
|
||||
enabled: true
|
||||
icon: 'icon-book-2-line'
|
||||
icon: 'icon-google'
|
||||
config_key: 'google'
|
||||
microsoft:
|
||||
name: 'Microsoft'
|
||||
description: 'Configuration for setting up Microsoft Email'
|
||||
enabled: true
|
||||
icon: 'icon-microsoft'
|
||||
config_key: 'microsoft'
|
||||
|
||||
# ------- Third-party Integrations ------- #
|
||||
linear:
|
||||
name: 'Linear'
|
||||
description: 'Configuration for setting up Linear Integration'
|
||||
@@ -103,6 +120,12 @@ slack:
|
||||
enabled: true
|
||||
icon: 'icon-slack'
|
||||
config_key: 'slack'
|
||||
whatsapp_embedded:
|
||||
name: 'WhatsApp Embedded'
|
||||
description: 'Configuration for setting up WhatsApp Embedded Integration'
|
||||
enabled: true
|
||||
icon: 'icon-whatsapp-line'
|
||||
config_key: 'whatsapp_embedded'
|
||||
shopify:
|
||||
name: 'Shopify'
|
||||
description: 'Configuration for setting up Shopify Integration'
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module SuperAdmin::FeaturesHelper
|
||||
def self.available_features
|
||||
YAML.load(ERB.new(Rails.root.join('enterprise/app/helpers/super_admin/features.yml').read).result).with_indifferent_access
|
||||
YAML.load(ERB.new(Rails.root.join('app/helpers/super_admin/features.yml').read).result).with_indifferent_access
|
||||
end
|
||||
|
||||
def self.plan_details
|
||||
@@ -0,0 +1,16 @@
|
||||
module SuperAdmin::NavigationHelper
|
||||
def settings_open?
|
||||
params[:controller].in? %w[super_admin/settings super_admin/app_configs]
|
||||
end
|
||||
|
||||
def settings_pages
|
||||
features = SuperAdmin::FeaturesHelper.available_features.select do |_feature, attrs|
|
||||
attrs['config_key'].present? && attrs['enabled']
|
||||
end
|
||||
|
||||
# Add general at the beginning
|
||||
general_feature = [['general', { 'config_key' => 'general', 'name' => 'General' }]]
|
||||
|
||||
general_feature + features.to_a
|
||||
end
|
||||
end
|
||||
@@ -15,14 +15,6 @@ class CaptainDocument extends ApiClient {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
uploadPdf(formData) {
|
||||
return axios.post(`${this.url}/upload_pdf`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainDocument();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class WhatsappChannel extends ApiClient {
|
||||
constructor() {
|
||||
super('whatsapp', { accountScoped: true });
|
||||
}
|
||||
|
||||
createEmbeddedSignup(params) {
|
||||
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WhatsappChannel();
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT } from './CampaignEmptyStateContent';
|
||||
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import CampaignCard from 'dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
||||
<template #empty-state-item>
|
||||
<div class="flex flex-col gap-4 p-px">
|
||||
<CampaignCard
|
||||
v-for="campaign in ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT"
|
||||
:key="campaign.id"
|
||||
:title="campaign.title"
|
||||
:message="campaign.message"
|
||||
:is-enabled="campaign.enabled"
|
||||
:status="campaign.campaign_status"
|
||||
:sender="campaign.sender"
|
||||
:inbox="campaign.inbox"
|
||||
:scheduled-at="campaign.scheduled_at"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</EmptyStateLayout>
|
||||
</template>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { CAMPAIGN_TYPES } from 'shared/constants/campaign.js';
|
||||
import { CAMPAIGNS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events.js';
|
||||
|
||||
import WhatsAppCampaignForm from 'dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const addCampaign = async campaignDetails => {
|
||||
try {
|
||||
await store.dispatch('campaigns/create', campaignDetails);
|
||||
|
||||
useTrack(CAMPAIGNS_EVENTS.CREATE_CAMPAIGN, {
|
||||
type: CAMPAIGN_TYPES.ONE_OFF,
|
||||
});
|
||||
|
||||
useAlert(t('CAMPAIGN.WHATSAPP.CREATE.FORM.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
t('CAMPAIGN.WHATSAPP.CREATE.FORM.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = campaignDetails => {
|
||||
addCampaign(campaignDetails);
|
||||
};
|
||||
|
||||
const handleClose = () => emit('close');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6"
|
||||
>
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
|
||||
</h3>
|
||||
<WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" />
|
||||
</div>
|
||||
</template>
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
<script setup>
|
||||
import { reactive, computed, watch, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue';
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formState = {
|
||||
uiFlags: useMapGetter('campaigns/getUIFlags'),
|
||||
labels: useMapGetter('labels/getLabels'),
|
||||
inboxes: useMapGetter('inboxes/getWhatsAppInboxes'),
|
||||
getWhatsAppTemplates: useMapGetter('inboxes/getWhatsAppTemplates'),
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
title: '',
|
||||
inboxId: null,
|
||||
templateId: null,
|
||||
scheduledAt: null,
|
||||
selectedAudience: [],
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
const processedParams = ref({});
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
inboxId: { required },
|
||||
templateId: { required },
|
||||
scheduledAt: { required },
|
||||
selectedAudience: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const isCreating = computed(() => formState.uiFlags.value.isCreating);
|
||||
|
||||
const currentDateTime = computed(() => {
|
||||
// Added to disable the scheduled at field from being set to the current time
|
||||
const now = new Date();
|
||||
const localTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
|
||||
return localTime.toISOString().slice(0, 16);
|
||||
});
|
||||
|
||||
const mapToOptions = (items, valueKey, labelKey) =>
|
||||
items?.map(item => ({
|
||||
value: item[valueKey],
|
||||
label: item[labelKey],
|
||||
})) ?? [];
|
||||
|
||||
const audienceList = computed(() =>
|
||||
mapToOptions(formState.labels.value, 'id', 'title')
|
||||
);
|
||||
|
||||
const inboxOptions = computed(() =>
|
||||
mapToOptions(formState.inboxes.value, 'id', 'name')
|
||||
);
|
||||
|
||||
const templateOptions = computed(() => {
|
||||
if (!state.inboxId) return [];
|
||||
const templates = formState.getWhatsAppTemplates.value(state.inboxId);
|
||||
return templates.map(template => {
|
||||
// Create a more user-friendly label from template name
|
||||
const friendlyName = template.name
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
return {
|
||||
value: template.id,
|
||||
label: `${friendlyName} (${template.language || 'en'})`,
|
||||
template: template,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const selectedTemplate = computed(() => {
|
||||
if (!state.templateId) return null;
|
||||
return templateOptions.value.find(option => option.value === state.templateId)
|
||||
?.template;
|
||||
});
|
||||
|
||||
const templateString = computed(() => {
|
||||
if (!selectedTemplate.value) return '';
|
||||
try {
|
||||
return (
|
||||
selectedTemplate.value.components?.find(
|
||||
component => component.type === 'BODY'
|
||||
)?.text || ''
|
||||
);
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const processedString = computed(() => {
|
||||
if (!templateString.value) return '';
|
||||
return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||
return processedParams.value[variable] || `{{${variable}}}`;
|
||||
});
|
||||
});
|
||||
|
||||
const getErrorMessage = (field, errorKey) => {
|
||||
const baseKey = 'CAMPAIGN.WHATSAPP.CREATE.FORM';
|
||||
return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : '';
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
title: getErrorMessage('title', 'TITLE'),
|
||||
inbox: getErrorMessage('inboxId', 'INBOX'),
|
||||
template: getErrorMessage('templateId', 'TEMPLATE'),
|
||||
scheduledAt: getErrorMessage('scheduledAt', 'SCHEDULED_AT'),
|
||||
audience: getErrorMessage('selectedAudience', 'AUDIENCE'),
|
||||
}));
|
||||
|
||||
const hasRequiredTemplateParams = computed(() => {
|
||||
const params = Object.values(processedParams.value);
|
||||
return params.length === 0 || params.every(param => param.trim() !== '');
|
||||
});
|
||||
|
||||
const isSubmitDisabled = computed(
|
||||
() => v$.value.$invalid || !hasRequiredTemplateParams.value
|
||||
);
|
||||
|
||||
const formatToUTCString = localDateTime =>
|
||||
localDateTime ? new Date(localDateTime).toISOString() : null;
|
||||
|
||||
const resetState = () => {
|
||||
Object.assign(state, initialState);
|
||||
processedParams.value = {};
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
|
||||
const generateVariables = () => {
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (!matchedVariables) {
|
||||
processedParams.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const finalVars = matchedVariables.map(match => match.replace(/{{|}}/g, ''));
|
||||
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||
acc[variable] = processedParams.value[variable] || '';
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const prepareCampaignDetails = () => {
|
||||
// Find the selected template to get its content
|
||||
const currentTemplate = selectedTemplate.value;
|
||||
|
||||
// Extract template content - this should be the template message body
|
||||
const templateContent = templateString.value;
|
||||
|
||||
// Prepare template_params object with the same structure as used in contacts
|
||||
const templateParams = {
|
||||
name: currentTemplate?.name || '',
|
||||
namespace: currentTemplate?.namespace || '',
|
||||
category: currentTemplate?.category || 'UTILITY',
|
||||
language: currentTemplate?.language || 'en_US',
|
||||
processed_params: processedParams.value,
|
||||
};
|
||||
|
||||
return {
|
||||
title: state.title,
|
||||
message: templateContent,
|
||||
template_params: templateParams,
|
||||
inbox_id: state.inboxId,
|
||||
scheduled_at: formatToUTCString(state.scheduledAt),
|
||||
audience: state.selectedAudience?.map(id => ({
|
||||
id,
|
||||
type: 'Label',
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const isFormValid = await v$.value.$validate();
|
||||
if (!isFormValid) return;
|
||||
|
||||
emit('submit', prepareCampaignDetails());
|
||||
resetState();
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
// Reset template selection when inbox changes
|
||||
watch(
|
||||
() => state.inboxId,
|
||||
() => {
|
||||
state.templateId = null;
|
||||
processedParams.value = {};
|
||||
}
|
||||
);
|
||||
|
||||
// Generate variables when template changes
|
||||
watch(
|
||||
() => state.templateId,
|
||||
() => {
|
||||
generateVariables();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.TITLE.LABEL')"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.TITLE.PLACEHOLDER')"
|
||||
:message="formErrors.title"
|
||||
:message-type="formErrors.title ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="inbox" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.INBOX.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
id="inbox"
|
||||
v-model="state.inboxId"
|
||||
:options="inboxOptions"
|
||||
:has-error="!!formErrors.inbox"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.INBOX.PLACEHOLDER')"
|
||||
:message="formErrors.inbox"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="template" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
id="template"
|
||||
v-model="state.templateId"
|
||||
:options="templateOptions"
|
||||
:has-error="!!formErrors.template"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.PLACEHOLDER')"
|
||||
:message="formErrors.template"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-n-slate-11">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Template Preview -->
|
||||
<div
|
||||
v-if="selectedTemplate"
|
||||
class="flex flex-col gap-4 p-4 rounded-lg bg-n-alpha-black2"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-sm font-medium text-n-slate-12">
|
||||
{{ selectedTemplate.name }}
|
||||
</h3>
|
||||
<span class="text-xs text-n-slate-11">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.LANGUAGE') }}:
|
||||
{{ selectedTemplate.language || 'en' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="rounded-md bg-n-alpha-black3">
|
||||
<div class="text-sm whitespace-pre-wrap text-n-slate-12">
|
||||
{{ processedString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-n-slate-11">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.CATEGORY') }}:
|
||||
{{ selectedTemplate.category || 'UTILITY' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Variables -->
|
||||
<div
|
||||
v-if="Object.keys(processedParams).length > 0"
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.VARIABLES_LABEL') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="(value, key) in processedParams"
|
||||
:key="key"
|
||||
class="flex gap-2 items-center"
|
||||
>
|
||||
<Input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1"
|
||||
:placeholder="
|
||||
t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.VARIABLE_PLACEHOLDER', {
|
||||
variable: key,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="audience" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.AUDIENCE.LABEL') }}
|
||||
</label>
|
||||
<TagMultiSelectComboBox
|
||||
v-model="state.selectedAudience"
|
||||
:options="audienceList"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.AUDIENCE.LABEL')"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.AUDIENCE.PLACEHOLDER')"
|
||||
:has-error="!!formErrors.audience"
|
||||
:message="formErrors.audience"
|
||||
class="[&>div>button]:bg-n-alpha-black2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="state.scheduledAt"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.SCHEDULED_AT.LABEL')"
|
||||
type="datetime-local"
|
||||
:min="currentDateTime"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.SCHEDULED_AT.PLACEHOLDER')"
|
||||
:message="formErrors.scheduledAt"
|
||||
:message-type="formErrors.scheduledAt ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex gap-3 justify-between items-center w-full">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
type="button"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.BUTTONS.CREATE')"
|
||||
class="w-full"
|
||||
type="submit"
|
||||
:is-loading="isCreating"
|
||||
:disabled="isCreating || isSubmitDisabled"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -218,10 +218,13 @@ const resetForm = () => {
|
||||
Object.assign(state, defaultState);
|
||||
};
|
||||
|
||||
watch(() => props.contactData, prepareStateBasedOnProps, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
});
|
||||
watch(
|
||||
() => props.contactData?.id,
|
||||
id => {
|
||||
if (id) prepareStateBasedOnProps();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Expose state to parent component for avatar upload
|
||||
defineExpose({
|
||||
|
||||
@@ -15,8 +15,8 @@ const emit = defineEmits(['click']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const onClick = event => {
|
||||
emit('click', event);
|
||||
const onClick = (item, index) => {
|
||||
emit('click', item, index);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -24,22 +24,25 @@ const onClick = event => {
|
||||
<nav :aria-label="t('BREADCRUMB.ARIA_LABEL')" class="flex items-center h-8">
|
||||
<ol class="flex items-center mb-0">
|
||||
<li v-for="(item, index) in items" :key="index" class="flex items-center">
|
||||
<Icon
|
||||
v-if="index > 0"
|
||||
icon="i-lucide-chevron-right"
|
||||
class="flex-shrink-0 mx-2 size-4 text-n-slate-11 dark:text-n-slate-11"
|
||||
/>
|
||||
|
||||
<!-- Render as button for all except the last item -->
|
||||
<button
|
||||
v-if="index === 0"
|
||||
v-if="index !== items.length - 1"
|
||||
class="inline-flex items-center justify-center min-w-0 gap-2 p-0 text-sm font-medium transition-all duration-200 ease-in-out border-0 rounded-lg text-n-slate-11 hover:text-n-slate-12 outline-transparent max-w-56"
|
||||
@click="onClick"
|
||||
@click="onClick(item, index)"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ item.label }}</span>
|
||||
</button>
|
||||
<template v-else>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-right"
|
||||
class="flex-shrink-0 mx-2 size-4 text-n-slate-11 dark:text-n-slate-11"
|
||||
/>
|
||||
<span class="text-sm truncate text-n-slate-12 max-w-56">
|
||||
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- The last breadcrumb item is plain text -->
|
||||
<span v-else class="text-sm truncate text-n-slate-12 max-w-56">
|
||||
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
|
||||
defineProps({
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
showPaginationFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
breadcrumbItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:currentPage']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handlePageChange = event => {
|
||||
emit('update:currentPage', event);
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="mt-4 px-10 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
||||
>
|
||||
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full mb-4">
|
||||
<header class="mb-7 sticky top-0 z-10 bg-n-background">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</header>
|
||||
<main class="flex gap-16 w-full flex-1 pb-16">
|
||||
<section
|
||||
v-if="$slots.body || $slots.emptyState || isFetching"
|
||||
class="flex flex-col w-full"
|
||||
>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="isEmpty">
|
||||
<slot name="emptyState" />
|
||||
</div>
|
||||
<slot v-else name="body" />
|
||||
</section>
|
||||
<section v-if="$slots.controls" class="flex w-full">
|
||||
<slot name="controls" />
|
||||
</section>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 pb-4">
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalCount"
|
||||
:items-per-page="itemsPerPage"
|
||||
@update:current-page="handlePageChange"
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import AddNewRulesDialog from './AddNewRulesDialog.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/AddNewRulesDialog"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="px-4 py-4 bg-n-background h-[200px]">
|
||||
<AddNewRulesDialog
|
||||
button-label="Add a guardrail"
|
||||
placeholder="Type in another guardrail..."
|
||||
confirm-label="Create"
|
||||
cancel-label="Cancel"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const [showPopover, togglePopover] = useToggle();
|
||||
const onClickAdd = () => {
|
||||
if (!modelValue.value?.trim()) return;
|
||||
emit('add', modelValue.value.trim());
|
||||
modelValue.value = '';
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const onClickCancel = () => {
|
||||
togglePopover(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex relative">
|
||||
<Button
|
||||
:label="buttonLabel"
|
||||
sm
|
||||
slate
|
||||
class="flex-shrink-0"
|
||||
@click="togglePopover(!showPopover)"
|
||||
/>
|
||||
<div
|
||||
v-if="showPopover"
|
||||
class="absolute w-[26.5rem] top-9 z-50 ltr:left-0 rtl:right-0 flex flex-col gap-5 bg-n-alpha-3 backdrop-blur-[100px] p-4 rounded-xl border border-n-weak shadow-md"
|
||||
>
|
||||
<InlineInput
|
||||
v-model="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="onClickAdd"
|
||||
/>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<Button
|
||||
:label="cancelLabel"
|
||||
sm
|
||||
link
|
||||
slate
|
||||
class="h-10 hover:!no-underline"
|
||||
@click="onClickCancel"
|
||||
/>
|
||||
<Button :label="confirmLabel" sm @click="onClickAdd" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import AddNewRulesInput from './AddNewRulesInput.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/AddNewRulesInput"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="px-6 py-4 bg-n-background">
|
||||
<AddNewRulesInput
|
||||
placeholder="Type in another response guideline..."
|
||||
label="Add and save (↵)"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const onClickAdd = () => {
|
||||
if (!modelValue.value?.trim()) return;
|
||||
emit('add', modelValue.value.trim());
|
||||
modelValue.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex py-3 ltr:pl-3 h-16 rtl:pr-3 ltr:pr-4 rtl:pl-4 items-center gap-3 rounded-xl bg-n-solid-2 outline-1 outline outline-n-container"
|
||||
>
|
||||
<Icon icon="i-lucide-plus" class="text-n-slate-10 size-5 flex-shrink-0" />
|
||||
|
||||
<InlineInput
|
||||
v-model="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="onClickAdd"
|
||||
/>
|
||||
<Button
|
||||
:label="label"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="onClickAdd"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
allItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectAllLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedCountLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
deleteLabel: {
|
||||
type: String,
|
||||
default: 'Delete',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['bulkDelete']);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: Set,
|
||||
default: () => new Set(),
|
||||
});
|
||||
|
||||
const selectedCount = computed(() => modelValue.value.size);
|
||||
const totalCount = computed(() => props.allItems.length);
|
||||
|
||||
const hasSelected = computed(() => selectedCount.value > 0);
|
||||
const isIndeterminate = computed(
|
||||
() => hasSelected.value && selectedCount.value < totalCount.value
|
||||
);
|
||||
const allSelected = computed(
|
||||
() => totalCount.value > 0 && selectedCount.value === totalCount.value
|
||||
);
|
||||
|
||||
const bulkCheckboxState = computed({
|
||||
get: () => allSelected.value,
|
||||
set: shouldSelectAll => {
|
||||
const newSelectedIds = shouldSelectAll
|
||||
? new Set(props.allItems.map(item => item.id))
|
||||
: new Set();
|
||||
modelValue.value = newSelectedIds;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
name="slide-fade"
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4"
|
||||
enter-to-class="opacity-100 transform translate-x-0"
|
||||
leave-active-class="hidden opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="hasSelected"
|
||||
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Checkbox
|
||||
v-model="bulkCheckboxState"
|
||||
:indeterminate="isIndeterminate"
|
||||
/>
|
||||
<span class="text-sm font-medium text-n-slate-12 tabular-nums">
|
||||
{{ selectAllLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-10 tabular-nums">
|
||||
{{ selectedCountLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-4 w-px bg-n-strong" />
|
||||
<div class="flex items-center gap-3">
|
||||
<slot name="actions" :selected-count="selectedCount">
|
||||
<Button
|
||||
:label="deleteLabel"
|
||||
sm
|
||||
ruby
|
||||
ghost
|
||||
class="!px-1.5"
|
||||
icon="i-lucide-trash"
|
||||
@click="emit('bulkDelete')"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-3">
|
||||
<slot name="default-actions" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -8,7 +8,6 @@ import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -27,10 +26,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
file: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
createdAt: {
|
||||
type: Number,
|
||||
required: true,
|
||||
@@ -68,22 +63,6 @@ const menuItems = computed(() => {
|
||||
|
||||
const createdAt = computed(() => dynamicTime(props.createdAt));
|
||||
|
||||
const displayLink = computed(() => {
|
||||
// For PDF uploads, show the filename instead of the generated external_link
|
||||
if (props.file?.filename) {
|
||||
return props.file.filename;
|
||||
}
|
||||
return props.externalLink;
|
||||
});
|
||||
|
||||
const actualLink = computed(() => {
|
||||
// For PDF uploads, return the actual file URL; for URLs, return the external_link
|
||||
if (props.file?.url) {
|
||||
return props.file.url;
|
||||
}
|
||||
return props.externalLink;
|
||||
});
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
emit('action', { action, value, id: props.id });
|
||||
@@ -124,15 +103,12 @@ const handleAction = ({ action, value }) => {
|
||||
<i class="i-woot-captain" />
|
||||
{{ assistant?.name || '' }}
|
||||
</span>
|
||||
<a
|
||||
:href="actualLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-n-slate-11 text-sm truncate flex justify-start flex-1 items-center gap-1 hover:text-n-slate-12 transition-colors"
|
||||
<span
|
||||
class="text-n-slate-11 text-sm truncate flex justify-start flex-1 items-center gap-1"
|
||||
>
|
||||
<Icon icon="i-lucide-link" class="shrink-0 size-4" />
|
||||
<span class="truncate">{{ displayLink }}</span>
|
||||
</a>
|
||||
<i class="i-ph-link-simple shrink-0" />
|
||||
<span class="truncate">{{ externalLink }}</span>
|
||||
</span>
|
||||
<div class="shrink-0 text-sm text-n-slate-11 line-clamp-1">
|
||||
{{ createdAt }}
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
|
||||
@@ -130,7 +134,7 @@ const handleDocumentableClick = () => {
|
||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
||||
{{ question }}
|
||||
</span>
|
||||
<div v-if="!compact" class="flex items-center gap-2">
|
||||
<div v-if="!compact && showMenu" class="flex items-center gap-2">
|
||||
<Policy
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
:permissions="['administrator']"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import RuleCard from './RuleCard.vue';
|
||||
|
||||
const sampleRules = [
|
||||
{ id: 1, content: 'Block sensitive personal information', selectable: true },
|
||||
{ id: 2, content: 'Reject offensive language', selectable: true },
|
||||
{ id: 3, content: 'Deflect legal or medical advice', selectable: true },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/RuleCard"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Selectable List">
|
||||
<div class="flex flex-col gap-4 px-20 py-4 bg-n-background">
|
||||
<RuleCard
|
||||
v-for="rule in sampleRules"
|
||||
:id="rule.id"
|
||||
:key="rule.id"
|
||||
:content="rule.content"
|
||||
:selectable="rule.selectable"
|
||||
@select="id => console.log('Selected rule', id)"
|
||||
@edit="id => console.log('Edit', id)"
|
||||
@delete="id => console.log('Delete', id)"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Non-Selectable">
|
||||
<div class="flex flex-col gap-4 px-20 py-4 bg-n-background">
|
||||
<RuleCard id="4" content="Replies should be friendly and clear." />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'hover', 'edit', 'delete']);
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
set: () => emit('select', props.id),
|
||||
});
|
||||
|
||||
const isEditing = ref(false);
|
||||
const editedContent = ref(props.content);
|
||||
|
||||
// Local content to display to avoid flicker until parent prop updates on inline edit
|
||||
const localContent = ref(props.content);
|
||||
|
||||
// Keeps localContent in sync when parent updates content prop
|
||||
watch(
|
||||
() => props.content,
|
||||
newVal => {
|
||||
localContent.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
const startEdit = () => {
|
||||
isEditing.value = true;
|
||||
editedContent.value = props.content;
|
||||
};
|
||||
|
||||
const saveEdit = () => {
|
||||
isEditing.value = false;
|
||||
// Update local content
|
||||
localContent.value = editedContent.value;
|
||||
emit('edit', { id: props.id, content: editedContent.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout
|
||||
selectable
|
||||
class="relative [&>div]:!py-5 [&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4"
|
||||
layout="row"
|
||||
@mouseenter="emit('hover', true)"
|
||||
@mouseleave="emit('hover', false)"
|
||||
>
|
||||
<div v-show="selectable" class="absolute top-6 ltr:left-3 rtl:right-3">
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
<InlineInput
|
||||
v-if="isEditing"
|
||||
v-model="editedContent"
|
||||
focus-on-mount
|
||||
custom-input-class="flex items-center gap-2 text-sm text-n-slate-12"
|
||||
@keyup.enter="saveEdit"
|
||||
/>
|
||||
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
||||
{{ localContent }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button icon="i-lucide-pen" slate xs ghost @click="startEdit" />
|
||||
<span class="w-px h-4 bg-n-weak" />
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
slate
|
||||
xs
|
||||
ghost
|
||||
@click="emit('delete', id)"
|
||||
/>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import SuggestedRules from './SuggestedRules.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const guidelinesExample = [
|
||||
{
|
||||
content:
|
||||
'Block queries that share or request sensitive personal information (e.g. phone numbers, passwords).',
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Reject queries that include offensive, discriminatory, or threatening language.',
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Deflect when the assistant is asked for legal or medical diagnosis or treatment.',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/SuggestedRules"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Suggested Rules List">
|
||||
<div class="px-20 py-4 bg-n-background">
|
||||
<SuggestedRules
|
||||
title="Example response guidelines"
|
||||
:items="guidelinesExample"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span class="text-sm text-n-slate-12">{{ item.content }}</span>
|
||||
<Button
|
||||
label="Add this"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
/>
|
||||
</template>
|
||||
</SuggestedRules>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const onAddClick = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start self-stretch rounded-xl w-full overflow-hidden border border-dashed border-n-strong"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full gap-3 px-4 pb-1 pt-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h5 class="text-sm font-medium text-n-slate-11">{{ title }}</h5>
|
||||
<span class="h-3 w-px bg-n-weak" />
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.GUARDRAILS.ADD.SUGGESTED.ADD')"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="onAddClick"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="onClickClose"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-start divide-y divide-n-strong divide-dashed w-full"
|
||||
>
|
||||
<div v-for="item in items" :key="item.content" class="w-full px-4 py-4">
|
||||
<slot :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<script setup>
|
||||
import { reactive, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
assistant: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialState = {
|
||||
name: '',
|
||||
description: '',
|
||||
productName: '',
|
||||
features: {
|
||||
conversationFaqs: false,
|
||||
memories: false,
|
||||
},
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
|
||||
const validationRules = {
|
||||
name: { required, minLength: minLength(1) },
|
||||
description: { required, minLength: minLength(1) },
|
||||
productName: { required, minLength: minLength(1) },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const getErrorMessage = field => {
|
||||
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
name: getErrorMessage('name'),
|
||||
description: getErrorMessage('description'),
|
||||
productName: getErrorMessage('productName'),
|
||||
}));
|
||||
|
||||
const updateStateFromAssistant = assistant => {
|
||||
const { config = {} } = assistant;
|
||||
state.name = assistant.name;
|
||||
state.description = assistant.description;
|
||||
state.productName = config.product_name;
|
||||
state.features = {
|
||||
conversationFaqs: config.feature_faq || false,
|
||||
memories: config.feature_memory || false,
|
||||
};
|
||||
};
|
||||
|
||||
const handleBasicInfoUpdate = async () => {
|
||||
const result = await Promise.all([
|
||||
v$.value.name.$validate(),
|
||||
v$.value.description.$validate(),
|
||||
v$.value.productName.$validate(),
|
||||
]).then(results => results.every(Boolean));
|
||||
if (!result) return;
|
||||
|
||||
const payload = {
|
||||
name: state.name,
|
||||
description: state.description,
|
||||
config: {
|
||||
...props.assistant.config,
|
||||
product_name: state.productName,
|
||||
feature_faq: state.features.conversationFaqs,
|
||||
feature_memory: state.features.memories,
|
||||
},
|
||||
};
|
||||
|
||||
emit('submit', payload);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.assistant,
|
||||
newAssistant => {
|
||||
if (newAssistant) updateStateFromAssistant(newAssistant);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Input
|
||||
v-model="state.name"
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
|
||||
:message="formErrors.name"
|
||||
:message-type="formErrors.name ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<Input
|
||||
v-model="state.productName"
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
|
||||
:message="formErrors.productName"
|
||||
:message-type="formErrors.productName ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<Editor
|
||||
v-model="state.description"
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
:message="formErrors.description"
|
||||
:message-type="formErrors.description ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="state.features.conversationFaqs"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="state.features.memories"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||
@click="handleBasicInfoUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
controlItem: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const onClick = name => {
|
||||
router.push({ name });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:key="controlItem.name"
|
||||
class="pt-3 ltr:pl-4 rtl:pr-4 ltr:pr-2 rtl:pl-2 pb-5 gap-2 flex flex-col w-full shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 cursor-pointer"
|
||||
@click="onClick(controlItem.routeName)"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full gap-1 h-8">
|
||||
<span class="text-sm font-medium text-n-slate-12 line-clamp-1">
|
||||
{{ controlItem.name }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
icon="i-lucide-chevron-right"
|
||||
slate
|
||||
ghost
|
||||
xs
|
||||
@click="onClick(controlItem.routeName)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-n-slate-11 text-sm leading-[21px] line-clamp-5">
|
||||
{{ controlItem.description }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { reactive, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { minLength } from '@vuelidate/validators';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
assistant: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialState = {
|
||||
handoffMessage: '',
|
||||
resolutionMessage: '',
|
||||
temperature: 1,
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
|
||||
const validationRules = {
|
||||
handoffMessage: { minLength: minLength(1) },
|
||||
resolutionMessage: { minLength: minLength(1) },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const getErrorMessage = field => {
|
||||
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
handoffMessage: getErrorMessage('handoffMessage'),
|
||||
resolutionMessage: getErrorMessage('resolutionMessage'),
|
||||
}));
|
||||
|
||||
const updateStateFromAssistant = assistant => {
|
||||
const { config = {} } = assistant;
|
||||
state.handoffMessage = config.handoff_message;
|
||||
state.resolutionMessage = config.resolution_message;
|
||||
state.temperature = config.temperature || 1;
|
||||
};
|
||||
|
||||
const handleSystemMessagesUpdate = async () => {
|
||||
const result = await Promise.all([
|
||||
v$.value.handoffMessage.$validate(),
|
||||
v$.value.resolutionMessage.$validate(),
|
||||
]).then(results => results.every(Boolean));
|
||||
if (!result) return;
|
||||
|
||||
const payload = {
|
||||
config: {
|
||||
...props.assistant.config,
|
||||
handoff_message: state.handoffMessage,
|
||||
resolution_message: state.resolutionMessage,
|
||||
temperature: state.temperature || 1,
|
||||
},
|
||||
};
|
||||
|
||||
emit('submit', payload);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.assistant,
|
||||
newAssistant => {
|
||||
if (newAssistant) updateStateFromAssistant(newAssistant);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Editor
|
||||
v-model="state.handoffMessage"
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')"
|
||||
:message="formErrors.handoffMessage"
|
||||
:message-type="formErrors.handoffMessage ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<Editor
|
||||
v-model="state.resolutionMessage"
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')"
|
||||
:message="formErrors.resolutionMessage"
|
||||
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<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>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||
@click="handleSystemMessagesUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+10
-23
@@ -3,40 +3,27 @@ import { ref } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import DocumentForm from './DocumentForm.vue';
|
||||
|
||||
const emit = defineEmits(['close', 'success']);
|
||||
|
||||
// Constants
|
||||
const DOCUMENT_TYPE = {
|
||||
PDF: 'pdf',
|
||||
URL: 'url',
|
||||
};
|
||||
|
||||
const store = useStore();
|
||||
const emit = defineEmits(['close']);
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const documentForm = ref(null);
|
||||
|
||||
const i18nKey = 'CAPTAIN.DOCUMENTS.CREATE';
|
||||
|
||||
const handleSubmit = async newDocument => {
|
||||
try {
|
||||
await store.dispatch('captainDocuments/createDocument', newDocument);
|
||||
|
||||
// Show appropriate success message based on document type
|
||||
const successMessage =
|
||||
newDocument.type === DOCUMENT_TYPE.PDF
|
||||
? t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.SUCCESS_MESSAGE')
|
||||
: t('CAPTAIN.DOCUMENTS.CREATE.SUCCESS_MESSAGE');
|
||||
|
||||
useAlert(successMessage);
|
||||
emit('success');
|
||||
await store.dispatch('captainDocuments/create', newDocument);
|
||||
useAlert(t(`${i18nKey}.SUCCESS_MESSAGE`));
|
||||
dialogRef.value.close();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.message ||
|
||||
t('CAPTAIN.DOCUMENTS.CREATE.ERROR_MESSAGE');
|
||||
error?.response?.message || t(`${i18nKey}.ERROR_MESSAGE`);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
@@ -55,8 +42,8 @@ defineExpose({ dialogRef });
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="$t('CAPTAIN.DOCUMENTS.CREATE.TITLE')"
|
||||
:description="$t('CAPTAIN.DOCUMENTS.CREATE.DESCRIPTION')"
|
||||
:title="$t(`${i18nKey}.TITLE`)"
|
||||
:description="$t('CAPTAIN.DOCUMENTS.FORM_DESCRIPTION')"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
@close="handleClose"
|
||||
|
||||
+30
-223
@@ -1,70 +1,34 @@
|
||||
<script setup>
|
||||
import { reactive, computed, ref } from 'vue';
|
||||
import { reactive, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, url } from '@vuelidate/validators';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatBytes } from 'shared/helpers/FileHelper';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import FileUpload from 'dashboard/components-next/fileUpload/FileUpload.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Constants
|
||||
const SOURCE_TYPE = {
|
||||
URL: 'url',
|
||||
PDF: 'pdf',
|
||||
};
|
||||
|
||||
const MESSAGE_TYPE = {
|
||||
ERROR: 'error',
|
||||
INFO: 'info',
|
||||
};
|
||||
|
||||
const DOCUMENT_TYPE = {
|
||||
URL: 'url',
|
||||
PDF: 'pdf',
|
||||
};
|
||||
|
||||
const FIELD_NAME = {
|
||||
URL: 'url',
|
||||
ASSISTANT_ID: 'assistantId',
|
||||
};
|
||||
|
||||
const FILE_ACCEPT_TYPE = 'application/pdf';
|
||||
const MAX_FILE_SIZE_MB = 25;
|
||||
|
||||
const formState = {
|
||||
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
|
||||
assistants: useMapGetter('captainAssistants/getRecords'),
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
url: '',
|
||||
name: '',
|
||||
assistantId: null,
|
||||
selectedFile: null,
|
||||
sourceType: SOURCE_TYPE.URL,
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
const uploadError = ref('');
|
||||
|
||||
const validationRules = computed(() => {
|
||||
const rules = {
|
||||
assistantId: { required },
|
||||
};
|
||||
|
||||
if (state.sourceType === SOURCE_TYPE.URL) {
|
||||
rules.url = { required, url, minLength: minLength(1) };
|
||||
}
|
||||
|
||||
return rules;
|
||||
});
|
||||
const validationRules = {
|
||||
url: { required, url, minLength: minLength(1) },
|
||||
assistantId: { required },
|
||||
};
|
||||
|
||||
const assistantList = computed(() =>
|
||||
formState.assistants.value.map(assistant => ({
|
||||
@@ -77,227 +41,70 @@ const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
||||
|
||||
const formattedFileSize = computed(() => {
|
||||
if (!state.selectedFile) return '';
|
||||
return formatBytes(state.selectedFile.size);
|
||||
});
|
||||
|
||||
const getErrorMessage = (field, errorMessage) => {
|
||||
return v$.value[field]?.$error ? errorMessage : '';
|
||||
const getErrorMessage = (field, errorKey) => {
|
||||
return v$.value[field].$error
|
||||
? t(`CAPTAIN.DOCUMENTS.FORM.${errorKey}.ERROR`)
|
||||
: '';
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
url: getErrorMessage(
|
||||
FIELD_NAME.URL,
|
||||
t('CAPTAIN.DOCUMENTS.CREATE.FORM.URL.ERROR')
|
||||
),
|
||||
assistantId: getErrorMessage(
|
||||
FIELD_NAME.ASSISTANT_ID,
|
||||
t('CAPTAIN.DOCUMENTS.CREATE.FORM.ASSISTANT.ERROR')
|
||||
),
|
||||
url: getErrorMessage('url', 'URL'),
|
||||
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
|
||||
}));
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
|
||||
const prepareDocumentDetails = () => {
|
||||
if (state.sourceType === SOURCE_TYPE.PDF) {
|
||||
return {
|
||||
type: DOCUMENT_TYPE.PDF,
|
||||
pdf_document: state.selectedFile,
|
||||
assistant_id: state.assistantId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: DOCUMENT_TYPE.URL,
|
||||
external_link: state.url,
|
||||
assistant_id: state.assistantId,
|
||||
};
|
||||
};
|
||||
const prepareDocumentDetails = () => ({
|
||||
external_link: state.url,
|
||||
assistant_id: state.assistantId,
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
uploadError.value = '';
|
||||
|
||||
// Validate form
|
||||
const isFormValid = await v$.value.$validate();
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For PDF uploads, check if file is selected
|
||||
if (state.sourceType === SOURCE_TYPE.PDF && !state.selectedFile) {
|
||||
uploadError.value = t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
emit('submit', prepareDocumentDetails());
|
||||
};
|
||||
|
||||
// PDF Upload handlers
|
||||
const handleFileSelected = file => {
|
||||
state.selectedFile = file;
|
||||
uploadError.value = '';
|
||||
};
|
||||
|
||||
const handleFileError = error => {
|
||||
uploadError.value = error;
|
||||
state.selectedFile = null;
|
||||
};
|
||||
|
||||
const clearSelectedFile = () => {
|
||||
state.selectedFile = null;
|
||||
uploadError.value = '';
|
||||
};
|
||||
|
||||
const handleSourceTypeChange = type => {
|
||||
state.sourceType = type;
|
||||
uploadError.value = '';
|
||||
state.selectedFile = null;
|
||||
state.url = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<!-- Source Type Selection -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.LABEL') }}
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 p-3 rounded-lg bg-n-alpha-2 hover:border-n-slate-7 transition-colors focus:outline-none"
|
||||
:class="{
|
||||
'ring-2 ring-n-brand text-n-brand border-0':
|
||||
state.sourceType === SOURCE_TYPE.URL,
|
||||
'border-2 border-n-slate-7 text-n-slate-11':
|
||||
state.sourceType !== SOURCE_TYPE.URL,
|
||||
}"
|
||||
@click="handleSourceTypeChange(SOURCE_TYPE.URL)"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Icon icon="i-lucide-link" class="size-5" />
|
||||
<span class="text-sm font-medium">{{
|
||||
$t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.WEBSITE_URL')
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 p-3 rounded-lg bg-n-alpha-2 hover:border-n-slate-7 transition-colors focus:outline-none"
|
||||
:class="{
|
||||
'ring-2 ring-n-brand text-n-brand border-0':
|
||||
state.sourceType === SOURCE_TYPE.PDF,
|
||||
'border-2 border-n-slate-7 text-n-slate-11':
|
||||
state.sourceType !== SOURCE_TYPE.PDF,
|
||||
}"
|
||||
@click="handleSourceTypeChange(SOURCE_TYPE.PDF)"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Icon icon="i-lucide-file" class="size-5" />
|
||||
<span class="text-sm font-medium">{{
|
||||
$t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.PDF_UPLOAD')
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL Input (when URL is selected) -->
|
||||
<div v-if="state.sourceType === SOURCE_TYPE.URL">
|
||||
<Input
|
||||
v-model="state.url"
|
||||
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.URL.LABEL')"
|
||||
:placeholder="t('CAPTAIN.DOCUMENTS.CREATE.FORM.URL.PLACEHOLDER')"
|
||||
:message="formErrors.url"
|
||||
:message-type="formErrors.url ? MESSAGE_TYPE.ERROR : MESSAGE_TYPE.INFO"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- PDF Upload (when PDF is selected) -->
|
||||
<div
|
||||
v-if="state.sourceType === SOURCE_TYPE.PDF"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.LABEL') }}
|
||||
</label>
|
||||
|
||||
<!-- File upload area -->
|
||||
<div v-if="!state.selectedFile">
|
||||
<FileUpload
|
||||
:accept="FILE_ACCEPT_TYPE"
|
||||
:max-size-m-b="MAX_FILE_SIZE_MB"
|
||||
:placeholder="
|
||||
t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.PLACEHOLDER')
|
||||
"
|
||||
:upload-text="
|
||||
t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.UPLOAD_TEXT')
|
||||
"
|
||||
:drag-text="t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.DROP_TEXT')"
|
||||
@file-selected="handleFileSelected"
|
||||
@file-error="handleFileError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Selected file display -->
|
||||
<div v-else class="p-3 bg-n-alpha-2 rounded-lg border border-n-slate-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Icon icon="i-lucide-file" class="size-5 text-n-ruby-9" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-n-slate-12">
|
||||
{{ state.selectedFile.name }}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-8">
|
||||
{{ formattedFileSize }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 rounded hover:bg-n-alpha-3 text-n-slate-8 hover:text-n-slate-10"
|
||||
@click="clearSelectedFile"
|
||||
>
|
||||
<Icon icon="i-lucide-x" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload error -->
|
||||
<p v-if="uploadError" class="text-sm text-n-ruby-9">{{ uploadError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Assistant Selection -->
|
||||
<Input
|
||||
v-model="state.url"
|
||||
:label="t('CAPTAIN.DOCUMENTS.FORM.URL.LABEL')"
|
||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.URL.PLACEHOLDER')"
|
||||
:message="formErrors.url"
|
||||
:message-type="formErrors.url ? 'error' : 'info'"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.ASSISTANT.LABEL') }}
|
||||
{{ t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
id="assistant"
|
||||
v-model="state.assistantId"
|
||||
:options="assistantList"
|
||||
:has-error="!!formErrors.assistantId"
|
||||
:placeholder="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ASSISTANT.PLACEHOLDER')"
|
||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.PLACEHOLDER')"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||
:message="formErrors.assistantId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ACTIONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
|
||||
:label="t('CAPTAIN.FORM.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ACTIONS.CREATE')"
|
||||
:label="t('CAPTAIN.FORM.CREATE')"
|
||||
class="w-full"
|
||||
:is-loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxSizeMB: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
uploadText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dragText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['fileSelected', 'fileError']);
|
||||
const { t } = useI18n();
|
||||
const fileInput = ref(null);
|
||||
const isDragOver = ref(false);
|
||||
|
||||
const acceptTypes = computed(() => props.accept);
|
||||
const maxSize = computed(() => props.maxSizeMB * 1024 * 1024);
|
||||
|
||||
const handleFileClick = () => {
|
||||
if (!props.disabled) {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const validateFile = file => {
|
||||
// Check file size
|
||||
if (file.size > maxSize.value) {
|
||||
const errorMsg = t(
|
||||
'CAPTAIN.DOCUMENTS.CREATE.FORM.FILE_UPLOAD.ERROR_TOO_LARGE',
|
||||
{
|
||||
fileName: file.name,
|
||||
maxSize: props.maxSizeMB,
|
||||
}
|
||||
);
|
||||
emit('fileError', errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file type if specified
|
||||
if (
|
||||
props.accept !== '*' &&
|
||||
!file.type.match(new RegExp(props.accept.replace(/\*/g, '.*')))
|
||||
) {
|
||||
const errorMsg = t(
|
||||
'CAPTAIN.DOCUMENTS.CREATE.FORM.FILE_UPLOAD.ERROR_TYPE_NOT_SUPPORTED',
|
||||
{
|
||||
acceptedTypes: props.accept,
|
||||
}
|
||||
);
|
||||
emit('fileError', errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleFileChange = event => {
|
||||
const files = event.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (validateFile(file)) {
|
||||
emit('fileSelected', file);
|
||||
}
|
||||
}
|
||||
// Reset input value to allow selecting the same file again
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = event => {
|
||||
event.preventDefault();
|
||||
if (!props.disabled) {
|
||||
isDragOver.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = event => {
|
||||
event.preventDefault();
|
||||
// Only set to false if we're leaving the drop zone completely
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
isDragOver.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = event => {
|
||||
event.preventDefault();
|
||||
isDragOver.value = false;
|
||||
|
||||
if (props.disabled) return;
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (validateFile(file)) {
|
||||
emit('fileSelected', file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (isDragOver.value && props.dragText) {
|
||||
return props.dragText;
|
||||
}
|
||||
return props.uploadText || props.placeholder;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border-2 border-dashed border-n-slate-6 rounded-lg p-6 bg-n-alpha-2 hover:bg-n-alpha-3 cursor-pointer transition-all duration-200 ease-in-out flex flex-col items-center justify-center text-center min-h-[120px] hover:border-n-slate-7"
|
||||
:class="{
|
||||
'border-n-blue-9 bg-n-blue-2/50': isDragOver,
|
||||
'opacity-50 cursor-not-allowed hover:bg-n-alpha-2 hover:border-n-slate-6':
|
||||
disabled,
|
||||
}"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@click="handleFileClick"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
:accept="acceptTypes"
|
||||
:multiple="multiple"
|
||||
:disabled="disabled"
|
||||
class="hidden"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="p-2 rounded-full bg-n-alpha-3">
|
||||
<Icon icon="i-lucide-file-up" class="size-6 text-n-slate-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-n-slate-11">
|
||||
{{ displayText }}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-8 mt-1">
|
||||
{{
|
||||
$t('CAPTAIN.DOCUMENTS.CREATE.FORM.FILE_UPLOAD.MAX_SIZE', {
|
||||
size: maxSizeMB,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -331,6 +331,11 @@ const menuItems = computed(() => {
|
||||
label: t('SIDEBAR.SMS'),
|
||||
to: accountScopedRoute('campaigns_sms_index'),
|
||||
},
|
||||
{
|
||||
name: 'WhatsApp',
|
||||
label: t('SIDEBAR.WHATSAPP'),
|
||||
to: accountScopedRoute('campaigns_whatsapp_index'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ref, watch, computed, nextTick } from 'vue';
|
||||
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -19,19 +19,16 @@ const mentionsListContainerRef = ref(null);
|
||||
const selectedIndex = ref(0);
|
||||
|
||||
const adjustScroll = () => {
|
||||
const container = mentionsListContainerRef.value;
|
||||
const item = container.querySelector(`#mention-item-${selectedIndex.value}`);
|
||||
if (item) {
|
||||
const itemTop = item.offsetTop;
|
||||
const itemBottom = itemTop + item.offsetHeight;
|
||||
const containerTop = container.scrollTop;
|
||||
const containerBottom = containerTop + container.offsetHeight;
|
||||
if (itemTop < containerTop) {
|
||||
container.scrollTop = itemTop;
|
||||
} else if (itemBottom + 34 > containerBottom) {
|
||||
container.scrollTop = itemBottom - container.offsetHeight + 34;
|
||||
nextTick(() => {
|
||||
const container = mentionsListContainerRef.value;
|
||||
if (!container) return;
|
||||
const selectedElement = container.querySelector(
|
||||
`#mention-item-${selectedIndex.value}`
|
||||
);
|
||||
if (selectedElement) {
|
||||
selectedElement.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSelect = () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ export const FEATURE_FLAGS = {
|
||||
AUTO_RESOLVE_CONVERSATIONS: 'auto_resolve_conversations',
|
||||
AUTOMATIONS: 'automations',
|
||||
CAMPAIGNS: 'campaigns',
|
||||
WHATSAPP_CAMPAIGNS: 'whatsapp_campaign',
|
||||
CANNED_RESPONSES: 'canned_responses',
|
||||
CRM: 'crm',
|
||||
CUSTOM_ATTRIBUTES: 'custom_attributes',
|
||||
@@ -36,6 +37,9 @@ export const FEATURE_FLAGS = {
|
||||
REPORT_V4: 'report_v4',
|
||||
CHANNEL_INSTAGRAM: 'channel_instagram',
|
||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||
WHATSAPP_EMBEDDED_SIGNUP: 'whatsapp_embedded_signup',
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
SSO: 'sso',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURES = [
|
||||
@@ -44,4 +48,6 @@ export const PREMIUM_FEATURES = [
|
||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||
FEATURE_FLAGS.AUDIT_LOGS,
|
||||
FEATURE_FLAGS.HELP_CENTER,
|
||||
FEATURE_FLAGS.CAPTAIN_V2,
|
||||
FEATURE_FLAGS.SSO,
|
||||
];
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Fetching custom attributes",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Conversation",
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Text",
|
||||
"NUMBER": "Number",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "List",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Custom Attribute",
|
||||
"SUBMIT": "Create",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Press enter to select",
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Select one",
|
||||
"SELECT": "Select"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "جلب الصفات المخصصة",
|
||||
"DESCRIPTION": "سمة مخصصة تتتبع تفاصيل إضافية حول جهات الاتصال أو المحادثات الخاصة بك - مثل خطة الاشتراك أو تاريخ الشراء الأول. يمكنك إضافة أنواع مختلفة من السمات المخصصة، مثل النص أو القوائم أو الأرقام، لالتقاط المعلومات المحددة التي تحتاجها.",
|
||||
"LEARN_MORE": "تعرف على المزيد حول السمات المخصصة",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "المحادثات",
|
||||
"CONTACT": "جهات الاتصال"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "النص",
|
||||
"NUMBER": "العدد",
|
||||
"LINK": "الرابط",
|
||||
"DATE": "Date",
|
||||
"LIST": "القائمة",
|
||||
"CHECKBOX": "خانة الاختيار"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "إضافة صفة خاصة",
|
||||
"SUBMIT": "إنشاء",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "فتح المحادثة"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "لا شيء",
|
||||
"LOW": "منخفضة",
|
||||
"MEDIUM": "متوسطة",
|
||||
"HIGH": "عالية",
|
||||
"URGENT": "عاجل"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
|
||||
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "اختر واحدا",
|
||||
"SELECT": "اختر"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "تغيير الأولوية",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "لا شيء",
|
||||
"LOW": "منخفضة",
|
||||
"MEDIUM": "متوسطة",
|
||||
"HIGH": "عالية",
|
||||
"URGENT": "عاجل"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Fetching custom attributes",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Conversation",
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Text",
|
||||
"NUMBER": "Number",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "List",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Custom Attribute",
|
||||
"SUBMIT": "Create",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Press enter to select",
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Select one",
|
||||
"SELECT": "Select"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Fetching custom attributes",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Разговор",
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Text",
|
||||
"NUMBER": "Number",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "List",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Custom Attribute",
|
||||
"SUBMIT": "Създаване",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Press enter to select",
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Select one",
|
||||
"SELECT": "Select"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "S'estan recollint atributs personalitzats",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Conversa",
|
||||
"CONTACT": "Contacte"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Llista",
|
||||
"NUMBER": "Número",
|
||||
"LINK": "Enllaç",
|
||||
"DATE": "Date",
|
||||
"LIST": "Llista",
|
||||
"CHECKBOX": "Casella de selecció"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Afegir atribut personalitzat",
|
||||
"SUBMIT": "Crear",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Afegeix SLA",
|
||||
"OPEN_CONVERSATION": "Obrir conversa"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Ningú",
|
||||
"LOW": "Baixa",
|
||||
"MEDIUM": "Mitjana",
|
||||
"HIGH": "Alta",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Presiona retorn (tecla enter) per seleccionar",
|
||||
"ENTER_TO_REMOVE": "Presiona retorn (tecla enter) per eliminar",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Selecciona un",
|
||||
"SELECT": "Selecciona"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Canvia la prioritat",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Ningú",
|
||||
"LOW": "Baixa",
|
||||
"MEDIUM": "Mitjana",
|
||||
"HIGH": "Alta",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Fetching custom attributes",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Conversation",
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Text",
|
||||
"NUMBER": "Number",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "List",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Custom Attribute",
|
||||
"SUBMIT": "Create",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Nic",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Typ zprávy",
|
||||
"MESSAGE_CONTAINS": "Zpráva obsahuje",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Stiskněte Enter pro vybrání",
|
||||
"ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Vyberte jeden",
|
||||
"SELECT": "Select"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_PRIVATE_NOTE": "Přidat soukromou poznámku",
|
||||
"SEND_WEBHOOK_EVENT": "Poslat událost webhook"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Nic",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Henter brugerdefinerede attributter",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Samtale",
|
||||
"CONTACT": "Kontakt"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Tekst",
|
||||
"NUMBER": "Nummer",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "Liste",
|
||||
"CHECKBOX": "Afkrydsningsfelt"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Tilføj Tilpasset Attribut",
|
||||
"SUBMIT": "Opret",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Åbn samtale"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Ingen",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Tryk enter for at vælge",
|
||||
"ENTER_TO_REMOVE": "Tryk enter for at fjerne",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Vælg en",
|
||||
"SELECT": "Vælg"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Ingen",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Benutzerdefinierte Attribute abrufen",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Konversation",
|
||||
"CONTACT": "Kontakt"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Text",
|
||||
"NUMBER": "Nummer",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "Liste",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Benutzerdefiniertes Attribut hinzufügen",
|
||||
"SUBMIT": "Erstellen",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Unterhaltung öffnen"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Keine",
|
||||
"LOW": "Niedrig",
|
||||
"MEDIUM": "Mittel",
|
||||
"HIGH": "Hoch",
|
||||
"URGENT": "Dringend"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Drücken Sie zur Auswahl die Eingabetaste",
|
||||
"ENTER_TO_REMOVE": "Drücken Sie zum Entfernen die Eingabetaste",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Eines wählen",
|
||||
"SELECT": "Auswählen"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Priorität ändern",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Keine",
|
||||
"LOW": "Niedrig",
|
||||
"MEDIUM": "Mittel",
|
||||
"HIGH": "Hoch",
|
||||
"URGENT": "Dringend"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"LOADING": "Λήψη προσαρμοσμένων ιδιοτήτων",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Συνομιλία",
|
||||
"CONTACT": "Επαφές"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Κείμενο",
|
||||
"NUMBER": "Αριθμός",
|
||||
"LINK": "Σύνδεσμος",
|
||||
"DATE": "Date",
|
||||
"LIST": "Λίστα",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Προσθήκη προσαρμοσμένης ιδιότητας",
|
||||
"SUBMIT": "Δημιουργία",
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Άνοιγμα συνομιλίας"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Κανένα",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Πάτησε enter για επιλογή",
|
||||
"ENTER_TO_REMOVE": "Πάτησε enter για αφαίρεση",
|
||||
"NO_OPTIONS": "List is empty",
|
||||
"SELECT_ONE": "Επιλέξτε ένα",
|
||||
"SELECT": "Επιλογή"
|
||||
}
|
||||
|
||||
@@ -481,6 +481,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "Κανένα",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||
"SEND_ATTACHMENT": "Send Attachment",
|
||||
"SEND_MESSAGE": "Send a Message",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
|
||||
@@ -137,6 +137,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"HEADER_TITLE": "WhatsApp campaigns",
|
||||
"NEW_CAMPAIGN": "Create campaign",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No WhatsApp campaigns are available",
|
||||
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||
},
|
||||
"CARD": {
|
||||
"STATUS": {
|
||||
"COMPLETED": "Completed",
|
||||
"SCHEDULED": "Scheduled"
|
||||
},
|
||||
"CAMPAIGN_DETAILS": {
|
||||
"SENT_FROM": "Sent from",
|
||||
"ON": "on"
|
||||
}
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create WhatsApp campaign",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CREATE_BUTTON_TEXT": "Create",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox",
|
||||
"ERROR": "Inbox is required"
|
||||
},
|
||||
"TEMPLATE": {
|
||||
"LABEL": "WhatsApp Template",
|
||||
"PLACEHOLDER": "Select a template",
|
||||
"INFO": "Select a template to use for this campaign.",
|
||||
"ERROR": "Template is required",
|
||||
"PREVIEW_TITLE": "Process {templateName}",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONFIRM_DELETE": {
|
||||
"TITLE": "Are you sure to delete?",
|
||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
"PLACEHOLDER": "Search",
|
||||
"EMPTY_STATE": "No results found"
|
||||
},
|
||||
"CLOSE": "Close",
|
||||
"FILE_SIZE": {
|
||||
"MB": "MB"
|
||||
}
|
||||
"CLOSE": "Close"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,50 @@
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
|
||||
}
|
||||
},
|
||||
"SSO": {
|
||||
"TITLE": "Single Sign-On",
|
||||
"DESCRIPTION": "Configure SSO authentication for your organization",
|
||||
"FORM": {
|
||||
"ENABLE_SSO": {
|
||||
"LABEL": "Enable SSO",
|
||||
"HELP": "Allow users to log in using Single Sign-On"
|
||||
},
|
||||
"PROVIDER_NAME": {
|
||||
"LABEL": "Provider Name",
|
||||
"PLACEHOLDER": "e.g., 'Okta', 'Azure AD', 'Google Workspace'",
|
||||
"ERROR": "Please enter a provider name"
|
||||
},
|
||||
"LOGIN_URL": {
|
||||
"LABEL": "SSO Login URL",
|
||||
"PLACEHOLDER": "https://your-sso-provider.com/login",
|
||||
"ERROR": "Please enter a valid SSO login URL",
|
||||
"HELP": "Users will be redirected to this URL for authentication"
|
||||
},
|
||||
"LOGOUT_URL": {
|
||||
"LABEL": "SSO Logout URL (Optional)",
|
||||
"PLACEHOLDER": "https://your-sso-provider.com/logout",
|
||||
"HELP": "Users will be redirected to this URL after logout"
|
||||
},
|
||||
"SECRET_KEY": {
|
||||
"LABEL": "Secret Key",
|
||||
"PLACEHOLDER": "Enter your SSO secret key",
|
||||
"ERROR": "Please enter a secret key",
|
||||
"HELP": "Secret key used to verify SSO tokens"
|
||||
},
|
||||
"TOKEN_EXPIRY": {
|
||||
"LABEL": "Token Expiry (minutes)",
|
||||
"PLACEHOLDER": "5",
|
||||
"ERROR": "Please enter a valid expiry time",
|
||||
"HELP": "How long SSO tokens remain valid"
|
||||
},
|
||||
"ERROR": "Please fix the form errors"
|
||||
},
|
||||
"SUBMIT": "Update SSO Settings",
|
||||
"UPDATE": {
|
||||
"SUCCESS": "SSO settings updated successfully",
|
||||
"ERROR": "Failed to update SSO settings"
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"LEARN_MORE": "Learn more",
|
||||
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
|
||||
|
||||
@@ -222,10 +222,17 @@
|
||||
"DESC": "Start supporting your customers via WhatsApp.",
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||
"360_DIALOG": "360Dialog"
|
||||
},
|
||||
"SELECT_PROVIDER": {
|
||||
"TITLE": "Select your API provider",
|
||||
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Inbox Name",
|
||||
"PLACEHOLDER": "Please enter an inbox name",
|
||||
@@ -264,6 +271,28 @@
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"EMBEDDED_SIGNUP": {
|
||||
"TITLE": "Quick Setup with Meta",
|
||||
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||
"BENEFITS": {
|
||||
"TITLE": "Benefits of Embedded Signup:",
|
||||
"EASY_SETUP": "No manual configuration required",
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||
"LOADING_SDK": "Loading Facebook SDK...",
|
||||
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||
"SIGNUP_ERROR": "Signup error occurred",
|
||||
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||
},
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
}
|
||||
|
||||
@@ -478,6 +478,37 @@
|
||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"BREADCRUMB": {
|
||||
"ASSISTANT": "Assistant"
|
||||
},
|
||||
"BASIC_SETTINGS": {
|
||||
"TITLE": "Basic settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"SYSTEM_SETTINGS": {
|
||||
"TITLE": "System settings",
|
||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||
},
|
||||
"CONTROL_ITEMS": {
|
||||
"TITLE": "The Fun Stuff",
|
||||
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||
"OPTIONS": {
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"EDIT_ASSISTANT": "Edit Assistant",
|
||||
"DELETE_ASSISTANT": "Delete Assistant",
|
||||
@@ -490,6 +521,100 @@
|
||||
"TITLE": "Captain Assistant",
|
||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||
}
|
||||
},
|
||||
"GUARDRAILS": {
|
||||
"TITLE": "Guardrails",
|
||||
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Guardrails"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Delete"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example guardrails",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"SAVE": "Add and save (↵)",
|
||||
"PLACEHOLDER": "Type in another guardrail..."
|
||||
},
|
||||
"NEW": {
|
||||
"TITLE": "Add a guardrail",
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel",
|
||||
"PLACEHOLDER": "Type in another guardrail...",
|
||||
"TEST_ALL": "Test all"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
"ERROR": "There was an error adding guardrails, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Guardrails updated successfully",
|
||||
"ERROR": "There was an error updating guardrails, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Guardrails deleted successfully",
|
||||
"ERROR": "There was an error deleting guardrails, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"RESPONSE_GUIDELINES": {
|
||||
"TITLE": "Response Guidelines",
|
||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Response Guidelines"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Delete"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example response guidelines",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"SAVE": "Add and save (↵)",
|
||||
"PLACEHOLDER": "Type in another response guideline..."
|
||||
},
|
||||
"NEW": {
|
||||
"TITLE": "Add a response guideline",
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel",
|
||||
"PLACEHOLDER": "Type in another response guideline...",
|
||||
"TEST_ALL": "Test all"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
"ERROR": "There was an error adding response guidelines, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Response Guidelines updated successfully",
|
||||
"ERROR": "There was an error updating response guidelines, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Response Guidelines deleted successfully",
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
@@ -501,44 +626,9 @@
|
||||
},
|
||||
"FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.",
|
||||
"CREATE": {
|
||||
"TITLE": "Create New Document",
|
||||
"DESCRIPTION": "Add a new document from a website URL or by uploading a PDF file",
|
||||
"TITLE": "Add a document",
|
||||
"SUCCESS_MESSAGE": "The document has been successfully created",
|
||||
"ERROR_MESSAGE": "There was an error creating the document, please try again.",
|
||||
"FORM": {
|
||||
"SOURCE_TYPE": {
|
||||
"LABEL": "Choose Source Type",
|
||||
"WEBSITE_URL": "Website URL",
|
||||
"PDF_UPLOAD": "PDF Upload"
|
||||
},
|
||||
"PDF_UPLOAD": {
|
||||
"LABEL": "Upload PDF Document",
|
||||
"SUCCESS_MESSAGE": "PDF uploaded successfully! Processing will begin shortly.",
|
||||
"ERROR": "Please select a PDF file to upload.",
|
||||
"PLACEHOLDER": "Select a PDF file",
|
||||
"UPLOAD_TEXT": "Click to select PDF or drag and drop",
|
||||
"DROP_TEXT": "Drop PDF file here"
|
||||
},
|
||||
"ASSISTANT": {
|
||||
"LABEL": "Select Assistant",
|
||||
"PLACEHOLDER": "Choose an assistant",
|
||||
"ERROR": "Please select an assistant"
|
||||
},
|
||||
"URL": {
|
||||
"LABEL": "Website URL",
|
||||
"PLACEHOLDER": "Enter the website URL to crawl",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"FILE_UPLOAD": {
|
||||
"MAX_SIZE": "Maximum file size: {size}MB",
|
||||
"ERROR_TOO_LARGE": "File \"{fileName}\" is too large. Maximum size is {maxSize}MB.",
|
||||
"ERROR_TYPE_NOT_SUPPORTED": "File type not supported. Only {acceptedTypes} files are allowed."
|
||||
},
|
||||
"ACTIONS": {
|
||||
"CANCEL": "Cancel",
|
||||
"CREATE": "Create Document"
|
||||
}
|
||||
}
|
||||
"ERROR_MESSAGE": "There was an error creating the document, please try again."
|
||||
},
|
||||
"FORM": {
|
||||
"URL": {
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"BUSINESS_ACCOUNTS_ONLY": "Please use your company email address to login",
|
||||
"NO_ACCOUNT_FOUND": "We couldn't find an account for your email address."
|
||||
},
|
||||
"SSO": {
|
||||
"BUTTON_TEXT": "Login with {provider}"
|
||||
},
|
||||
"FORGOT_PASSWORD": "Forgot your password?",
|
||||
"CREATE_NEW_ACCOUNT": "Create a new account",
|
||||
"SUBMIT": "Login"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user