initial commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainConfig extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/config', { accountScoped: true });
|
||||
}
|
||||
|
||||
get() {
|
||||
return axios.get(this.url);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainConfig();
|
||||
@@ -577,6 +577,12 @@ const menuItems = computed(() => {
|
||||
icon: 'i-lucide-credit-card',
|
||||
to: accountScopedRoute('billing_settings_index'),
|
||||
},
|
||||
{
|
||||
name: 'Settings Captain',
|
||||
label: t('SIDEBAR.CAPTAIN_AI'),
|
||||
icon: 'i-lucide-sparkles',
|
||||
to: accountScopedRoute('captain_settings_index'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -377,7 +377,52 @@
|
||||
"INFO_SHORT": "Automatically mark offline when you aren't using the app."
|
||||
},
|
||||
"DOCS": "Read docs",
|
||||
"SECURITY": "Security"
|
||||
"SECURITY": "Security",
|
||||
"CAPTAIN_AI": "Captain"
|
||||
},
|
||||
"CAPTAIN_SETTINGS": {
|
||||
"TITLE": "Captain Settings",
|
||||
"DESCRIPTION": "Configure your AI models and features for Captain in Chatwoot.",
|
||||
"LOADING": "Loading Captain configuration...",
|
||||
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
|
||||
"MODEL_CONFIG": {
|
||||
"TITLE": "Model Configuration",
|
||||
"DESCRIPTION": "Select AI models for different features.",
|
||||
"SELECT_MODEL": "Select model",
|
||||
"CREDITS_PER_MESSAGE": "{credits} credit/message",
|
||||
"EDITOR": {
|
||||
"TITLE": "Editor Features",
|
||||
"DESCRIPTION": "Powers smart compose, grammar corrections, tone adjustments, and content enhancement in your message editor."
|
||||
},
|
||||
"ASSISTANT": {
|
||||
"TITLE": "Assistant",
|
||||
"DESCRIPTION": "Handles automated responses, conversation summaries, and intelligent reply suggestions for customer interactions."
|
||||
},
|
||||
"COPILOT": {
|
||||
"TITLE": "Co-pilot",
|
||||
"DESCRIPTION": "Provides real-time contextual suggestions, knowledge base recommendations, and proactive insights during conversations."
|
||||
}
|
||||
},
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"DESCRIPTION": "Enable or disable AI-powered features.",
|
||||
"AUDIO_TRANSCRIPTION": {
|
||||
"TITLE": "Audio Transcription",
|
||||
"DESCRIPTION": "Automatically convert voice messages and call recordings into searchable text transcripts."
|
||||
},
|
||||
"HELP_CENTER_SEARCH": {
|
||||
"TITLE": "Help Center Search Indexing",
|
||||
"DESCRIPTION": "Enable AI to search and reference your help center articles for accurate, context-aware customer responses."
|
||||
},
|
||||
"LABEL_SUGGESTION": {
|
||||
"TITLE": "Label Suggestion",
|
||||
"DESCRIPTION": "Automatically suggest relevant labels and tags for conversations based on content analysis and context."
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS": "Captain settings updated successfully.",
|
||||
"ERROR": "Failed to update Captain settings. Please try again."
|
||||
}
|
||||
},
|
||||
"BILLING_SETTINGS": {
|
||||
"TITLE": "Billing",
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SectionLayout from '../account/components/SectionLayout.vue';
|
||||
import ModelSelector from './components/ModelSelector.vue';
|
||||
import FeatureToggle from './components/FeatureToggle.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { captainEnabled } = useCaptain();
|
||||
|
||||
const uiFlags = useMapGetter('captainConfig/getUIFlags');
|
||||
const features = useMapGetter('captainConfig/getFeatures');
|
||||
|
||||
const isLoading = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
const modelFeatures = computed(() => [
|
||||
{
|
||||
key: 'editor',
|
||||
title: t('CAPTAIN_SETTINGS.MODEL_CONFIG.EDITOR.TITLE'),
|
||||
description: t('CAPTAIN_SETTINGS.MODEL_CONFIG.EDITOR.DESCRIPTION'),
|
||||
},
|
||||
{
|
||||
key: 'assistant',
|
||||
title: t('CAPTAIN_SETTINGS.MODEL_CONFIG.ASSISTANT.TITLE'),
|
||||
description: t('CAPTAIN_SETTINGS.MODEL_CONFIG.ASSISTANT.DESCRIPTION'),
|
||||
},
|
||||
{
|
||||
key: 'copilot',
|
||||
title: t('CAPTAIN_SETTINGS.MODEL_CONFIG.COPILOT.TITLE'),
|
||||
description: t('CAPTAIN_SETTINGS.MODEL_CONFIG.COPILOT.DESCRIPTION'),
|
||||
},
|
||||
]);
|
||||
|
||||
const featureToggles = computed(() => [
|
||||
{
|
||||
key: 'audio_transcription',
|
||||
title: t('CAPTAIN_SETTINGS.FEATURES.AUDIO_TRANSCRIPTION.TITLE'),
|
||||
description: t('CAPTAIN_SETTINGS.FEATURES.AUDIO_TRANSCRIPTION.DESCRIPTION'),
|
||||
},
|
||||
{
|
||||
key: 'help_center_search',
|
||||
title: t('CAPTAIN_SETTINGS.FEATURES.HELP_CENTER_SEARCH.TITLE'),
|
||||
description: t('CAPTAIN_SETTINGS.FEATURES.HELP_CENTER_SEARCH.DESCRIPTION'),
|
||||
},
|
||||
{
|
||||
key: 'label_suggestion',
|
||||
title: t('CAPTAIN_SETTINGS.FEATURES.LABEL_SUGGESTION.TITLE'),
|
||||
description: t('CAPTAIN_SETTINGS.FEATURES.LABEL_SUGGESTION.DESCRIPTION'),
|
||||
},
|
||||
]);
|
||||
|
||||
const hasFeatureToggle = key => {
|
||||
return features.value[key] !== undefined;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainConfig/fetch');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="isLoading"
|
||||
:loading-message="t('CAPTAIN_SETTINGS.LOADING')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="t('CAPTAIN_SETTINGS.TITLE')"
|
||||
:description="t('CAPTAIN_SETTINGS.DESCRIPTION')"
|
||||
icon-name="bot"
|
||||
feature-name="captain"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="captainEnabled" class="flex flex-col gap-1">
|
||||
<!-- Model Configuration Section -->
|
||||
<SectionLayout
|
||||
:title="t('CAPTAIN_SETTINGS.MODEL_CONFIG.TITLE')"
|
||||
:description="t('CAPTAIN_SETTINGS.MODEL_CONFIG.DESCRIPTION')"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<ModelSelector
|
||||
v-for="feature in modelFeatures"
|
||||
:key="feature.key"
|
||||
:feature-key="feature.key"
|
||||
:title="feature.title"
|
||||
:description="feature.description"
|
||||
/>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
|
||||
<!-- Features Section -->
|
||||
<SectionLayout
|
||||
:title="t('CAPTAIN_SETTINGS.FEATURES.TITLE')"
|
||||
:description="t('CAPTAIN_SETTINGS.FEATURES.DESCRIPTION')"
|
||||
with-border
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<FeatureToggle
|
||||
v-for="feature in featureToggles"
|
||||
v-show="hasFeatureToggle(feature.key)"
|
||||
:key="feature.key"
|
||||
:feature-key="feature.key"
|
||||
:title="feature.title"
|
||||
:description="feature.description"
|
||||
/>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
</div>
|
||||
<div v-else class="text-n-slate-11 py-8 text-center">
|
||||
{{ t('CAPTAIN_SETTINGS.NOT_ENABLED') }}
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/captain'),
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: 'CAPTAIN_SETTINGS.TITLE',
|
||||
icon: 'i-lucide-bot',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'captain_settings_index',
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||
|
||||
const props = defineProps({
|
||||
featureKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const features = useMapGetter('captainConfig/getFeatures');
|
||||
|
||||
const isEnabled = ref(false);
|
||||
|
||||
const featureConfig = computed(() => features.value[props.featureKey]);
|
||||
|
||||
watch(
|
||||
featureConfig,
|
||||
newConfig => {
|
||||
if (newConfig !== undefined) {
|
||||
isEnabled.value = !!newConfig.enabled;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const toggleFeature = () => {
|
||||
emit('change', { feature: props.featureKey, enabled: isEnabled.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 p-4 rounded-xl border border-n-weak bg-n-solid-1"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-n-slate-12">{{ title }}</h4>
|
||||
<p class="text-sm text-n-slate-11 mt-0.5">{{ description }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<Switch v-model="isEnabled" @change="toggleFeature" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
featureKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const isOpen = ref(false);
|
||||
|
||||
const models = useMapGetter('captainConfig/getModels');
|
||||
const getModelsForFeature = useMapGetter('captainConfig/getModelsForFeature');
|
||||
const getDefaultModelForFeature = useMapGetter(
|
||||
'captainConfig/getDefaultModelForFeature'
|
||||
);
|
||||
|
||||
const availableModels = computed(() =>
|
||||
getModelsForFeature.value(props.featureKey)
|
||||
);
|
||||
const defaultModel = computed(() =>
|
||||
getDefaultModelForFeature.value(props.featureKey)
|
||||
);
|
||||
const selectedModel = ref(null);
|
||||
|
||||
watch(
|
||||
defaultModel,
|
||||
newDefault => {
|
||||
if (newDefault && !selectedModel.value) {
|
||||
selectedModel.value = newDefault;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const selectedModelDetails = computed(() => {
|
||||
if (!selectedModel.value || !models.value[selectedModel.value]) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: selectedModel.value,
|
||||
...models.value[selectedModel.value],
|
||||
};
|
||||
});
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
|
||||
const selectModel = model => {
|
||||
selectedModel.value = model.key;
|
||||
emit('change', { feature: props.featureKey, model: model.key });
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const getCreditLabel = model => {
|
||||
const multiplier = model.credit_multiplier || 1;
|
||||
return t('CAPTAIN_SETTINGS.MODEL_CONFIG.CREDITS_PER_MESSAGE', {
|
||||
credits: multiplier,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 p-4 rounded-xl border border-n-weak bg-n-solid-1"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-n-slate-12">{{ title }}</h4>
|
||||
<p class="text-sm text-n-slate-11 mt-0.5">{{ description }}</p>
|
||||
</div>
|
||||
<div v-on-clickaway="closeDropdown" class="relative flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-3 py-2 text-sm border rounded-lg border-n-weak bg-n-solid-2 hover:bg-n-solid-3 min-w-[180px] justify-between"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<span v-if="selectedModelDetails" class="text-n-slate-12">
|
||||
{{ selectedModelDetails.display_name }}
|
||||
</span>
|
||||
<span v-else class="text-n-slate-10">
|
||||
{{ t('CAPTAIN_SETTINGS.MODEL_CONFIG.SELECT_MODEL') }}
|
||||
</span>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-down"
|
||||
class="size-4 text-n-slate-11 transition-transform"
|
||||
:class="{ 'rotate-180': isOpen }"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="absolute right-0 z-50 w-56 mt-1 overflow-hidden border rounded-xl border-n-weak bg-n-alpha-3 backdrop-blur-[100px] shadow-lg"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
v-for="model in availableModels"
|
||||
:key="model.key"
|
||||
type="button"
|
||||
class="flex flex-col w-full px-3 py-2 text-left hover:bg-n-alpha-1"
|
||||
:class="{
|
||||
'bg-n-alpha-2': selectedModel === model.key,
|
||||
}"
|
||||
@click="selectModel(model)"
|
||||
>
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ model.display_name }}
|
||||
</span>
|
||||
<span class="text-xs text-n-slate-11">
|
||||
{{ getCreditLabel(model) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -24,6 +24,7 @@ import teams from './teams/teams.routes';
|
||||
import customRoles from './customRoles/customRole.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import security from './security/security.routes';
|
||||
import captain from './captain/captain.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -63,5 +64,6 @@ export default {
|
||||
...customRoles.routes,
|
||||
...profile.routes,
|
||||
...security.routes,
|
||||
...captain.routes,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import CaptainConfigAPI from 'dashboard/api/captain/config';
|
||||
|
||||
const state = {
|
||||
providers: {},
|
||||
models: {},
|
||||
features: {},
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getProviders: $state => $state.providers,
|
||||
getModels: $state => $state.models,
|
||||
getFeatures: $state => $state.features,
|
||||
getUIFlags: $state => $state.uiFlags,
|
||||
getModelsForFeature: $state => featureKey => {
|
||||
const feature = $state.features[featureKey];
|
||||
if (!feature?.models) return [];
|
||||
return feature.models.map(modelKey => ({
|
||||
key: modelKey,
|
||||
...$state.models[modelKey],
|
||||
}));
|
||||
},
|
||||
getDefaultModelForFeature: $state => featureKey => {
|
||||
const feature = $state.features[featureKey];
|
||||
return feature?.default || null;
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
SET_UI_FLAG($state, data) {
|
||||
$state.uiFlags = { ...$state.uiFlags, ...data };
|
||||
},
|
||||
SET_CONFIG($state, { providers, models, features }) {
|
||||
$state.providers = providers || {};
|
||||
$state.models = models || {};
|
||||
$state.features = features || {};
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async fetch({ commit }) {
|
||||
commit('SET_UI_FLAG', { isFetching: true });
|
||||
try {
|
||||
const response = await CaptainConfigAPI.get();
|
||||
commit('SET_CONFIG', response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
commit('SET_UI_FLAG', { isFetching: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
||||
@@ -58,6 +58,7 @@ import copilotMessages from './captain/copilotMessages';
|
||||
import captainScenarios from './captain/scenarios';
|
||||
import captainTools from './captain/tools';
|
||||
import captainCustomTools from './captain/customTools';
|
||||
import captainConfig from './captain/config';
|
||||
|
||||
const plugins = [];
|
||||
|
||||
@@ -121,6 +122,7 @@ export default createStore({
|
||||
captainScenarios,
|
||||
captainTools,
|
||||
captainCustomTools,
|
||||
captainConfig,
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user