mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: add custom tool dialog
This commit is contained in:
		| @@ -0,0 +1,73 @@ | |||||||
|  | <script setup> | ||||||
|  | import { defineModel, watch } from 'vue'; | ||||||
|  | import { useI18n } from 'vue-i18n'; | ||||||
|  | import Input from 'dashboard/components-next/input/Input.vue'; | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   authType: { | ||||||
|  |     type: String, | ||||||
|  |     required: true, | ||||||
|  |     validator: value => ['none', 'bearer', 'basic', 'api_key'].includes(value), | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const { t } = useI18n(); | ||||||
|  |  | ||||||
|  | const authConfig = defineModel('authConfig', { | ||||||
|  |   type: Object, | ||||||
|  |   default: () => ({}), | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | watch( | ||||||
|  |   () => props.authType, | ||||||
|  |   () => { | ||||||
|  |     authConfig.value = {}; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-2"> | ||||||
|  |     <Input | ||||||
|  |       v-if="authType === 'bearer'" | ||||||
|  |       v-model="authConfig.token" | ||||||
|  |       :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.BEARER_TOKEN')" | ||||||
|  |       :placeholder=" | ||||||
|  |         t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.BEARER_TOKEN_PLACEHOLDER') | ||||||
|  |       " | ||||||
|  |     /> | ||||||
|  |     <template v-else-if="authType === 'basic'"> | ||||||
|  |       <Input | ||||||
|  |         v-model="authConfig.username" | ||||||
|  |         :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.USERNAME')" | ||||||
|  |         :placeholder=" | ||||||
|  |           t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.USERNAME_PLACEHOLDER') | ||||||
|  |         " | ||||||
|  |       /> | ||||||
|  |       <Input | ||||||
|  |         v-model="authConfig.password" | ||||||
|  |         type="password" | ||||||
|  |         :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.PASSWORD')" | ||||||
|  |         :placeholder=" | ||||||
|  |           t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.PASSWORD_PLACEHOLDER') | ||||||
|  |         " | ||||||
|  |       /> | ||||||
|  |     </template> | ||||||
|  |     <template v-else-if="authType === 'api_key'"> | ||||||
|  |       <Input | ||||||
|  |         v-model="authConfig.key" | ||||||
|  |         :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_KEY')" | ||||||
|  |         :placeholder=" | ||||||
|  |           t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_KEY_PLACEHOLDER') | ||||||
|  |         " | ||||||
|  |       /> | ||||||
|  |       <Input | ||||||
|  |         v-model="authConfig.value" | ||||||
|  |         :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_VALUE')" | ||||||
|  |         :placeholder=" | ||||||
|  |           t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_VALUE_PLACEHOLDER') | ||||||
|  |         " | ||||||
|  |       /> | ||||||
|  |     </template> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | <script setup> | ||||||
|  | import { ref } from 'vue'; | ||||||
|  | import { useStore } from 'dashboard/composables/store'; | ||||||
|  | import { useAlert } from 'dashboard/composables'; | ||||||
|  | import { useI18n } from 'vue-i18n'; | ||||||
|  | import { parseAPIErrorResponse } from 'dashboard/store/utils/api'; | ||||||
|  |  | ||||||
|  | import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; | ||||||
|  | import CustomToolForm from './CustomToolForm.vue'; | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['close']); | ||||||
|  | const { t } = useI18n(); | ||||||
|  | const store = useStore(); | ||||||
|  |  | ||||||
|  | const dialogRef = ref(null); | ||||||
|  |  | ||||||
|  | const i18nKey = 'CAPTAIN.CUSTOM_TOOLS.CREATE'; | ||||||
|  |  | ||||||
|  | const handleSubmit = async newTool => { | ||||||
|  |   try { | ||||||
|  |     await store.dispatch('captainCustomTools/create', newTool); | ||||||
|  |     useAlert(t(`${i18nKey}.SUCCESS_MESSAGE`)); | ||||||
|  |     dialogRef.value.close(); | ||||||
|  |   } catch (error) { | ||||||
|  |     const errorMessage = | ||||||
|  |       parseAPIErrorResponse(error) || t(`${i18nKey}.ERROR_MESSAGE`); | ||||||
|  |     useAlert(errorMessage); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleClose = () => { | ||||||
|  |   emit('close'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleCancel = () => { | ||||||
|  |   dialogRef.value.close(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | defineExpose({ dialogRef }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <Dialog | ||||||
|  |     ref="dialogRef" | ||||||
|  |     width="2xl" | ||||||
|  |     :title="$t(`${i18nKey}.TITLE`)" | ||||||
|  |     :description="$t('CAPTAIN.CUSTOM_TOOLS.FORM_DESCRIPTION')" | ||||||
|  |     :show-cancel-button="false" | ||||||
|  |     :show-confirm-button="false" | ||||||
|  |     @close="handleClose" | ||||||
|  |   > | ||||||
|  |     <CustomToolForm @submit="handleSubmit" @cancel="handleCancel" /> | ||||||
|  |     <template #footer /> | ||||||
|  |   </Dialog> | ||||||
|  | </template> | ||||||
| @@ -0,0 +1,228 @@ | |||||||
|  | <script setup> | ||||||
|  | import { reactive, computed, useTemplateRef } from 'vue'; | ||||||
|  | import { useI18n } from 'vue-i18n'; | ||||||
|  | import { useVuelidate } from '@vuelidate/core'; | ||||||
|  | import { required, url } from '@vuelidate/validators'; | ||||||
|  | import { useMapGetter } from 'dashboard/composables/store'; | ||||||
|  |  | ||||||
|  | import Input from 'dashboard/components-next/input/Input.vue'; | ||||||
|  | import Textarea from 'dashboard/components-next/textarea/Textarea.vue'; | ||||||
|  | import Button from 'dashboard/components-next/button/Button.vue'; | ||||||
|  | import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; | ||||||
|  | import ParamRow from './ParamRow.vue'; | ||||||
|  | import AuthConfig from './AuthConfig.vue'; | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['submit', 'cancel']); | ||||||
|  |  | ||||||
|  | const { t } = useI18n(); | ||||||
|  |  | ||||||
|  | const formState = { | ||||||
|  |   uiFlags: useMapGetter('captainCustomTools/getUIFlags'), | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const initialState = { | ||||||
|  |   title: '', | ||||||
|  |   description: '', | ||||||
|  |   endpoint_url: '', | ||||||
|  |   http_method: 'GET', | ||||||
|  |   request_template: '', | ||||||
|  |   response_template: '', | ||||||
|  |   auth_type: 'none', | ||||||
|  |   auth_config: {}, | ||||||
|  |   param_schema: [], | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const state = reactive({ ...initialState }); | ||||||
|  |  | ||||||
|  | const DEFAULT_PARAM = { | ||||||
|  |   name: '', | ||||||
|  |   type: 'string', | ||||||
|  |   description: '', | ||||||
|  |   required: false, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const validationRules = { | ||||||
|  |   title: { required }, | ||||||
|  |   endpoint_url: { required, url }, | ||||||
|  |   http_method: { required }, | ||||||
|  |   auth_type: { required }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const httpMethodOptions = computed(() => [ | ||||||
|  |   { value: 'GET', label: 'GET' }, | ||||||
|  |   { value: 'POST', label: 'POST' }, | ||||||
|  | ]); | ||||||
|  |  | ||||||
|  | const authTypeOptions = computed(() => [ | ||||||
|  |   { value: 'none', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.NONE') }, | ||||||
|  |   { value: 'bearer', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.BEARER') }, | ||||||
|  |   { value: 'basic', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.BASIC') }, | ||||||
|  |   { | ||||||
|  |     value: 'api_key', | ||||||
|  |     label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.API_KEY'), | ||||||
|  |   }, | ||||||
|  | ]); | ||||||
|  |  | ||||||
|  | const v$ = useVuelidate(validationRules, state); | ||||||
|  |  | ||||||
|  | const isLoading = computed(() => formState.uiFlags.value.creatingItem); | ||||||
|  |  | ||||||
|  | const getErrorMessage = (field, errorKey) => { | ||||||
|  |   return v$.value[field].$error | ||||||
|  |     ? t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`) | ||||||
|  |     : ''; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const formErrors = computed(() => ({ | ||||||
|  |   title: getErrorMessage('title', 'TITLE'), | ||||||
|  |   endpoint_url: getErrorMessage('endpoint_url', 'ENDPOINT_URL'), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const paramsRef = useTemplateRef('paramsRef'); | ||||||
|  |  | ||||||
|  | const isParamsValid = () => { | ||||||
|  |   if (!paramsRef.value || paramsRef.value.length === 0) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |   return paramsRef.value.every(param => param.validate()); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const removeParam = index => { | ||||||
|  |   state.param_schema.splice(index, 1); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addParam = () => { | ||||||
|  |   state.param_schema.push({ ...DEFAULT_PARAM }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleCancel = () => emit('cancel'); | ||||||
|  |  | ||||||
|  | const handleSubmit = async () => { | ||||||
|  |   const isFormValid = await v$.value.$validate(); | ||||||
|  |   if (!isFormValid || !isParamsValid()) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   emit('submit', state); | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <form class="flex flex-col gap-4" @submit.prevent="handleSubmit"> | ||||||
|  |     <Input | ||||||
|  |       v-model="state.title" | ||||||
|  |       :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.TITLE.LABEL')" | ||||||
|  |       :placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.TITLE.PLACEHOLDER')" | ||||||
|  |       :message="formErrors.title" | ||||||
|  |       :message-type="formErrors.title ? 'error' : 'info'" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Textarea | ||||||
|  |       v-model="state.description" | ||||||
|  |       :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.DESCRIPTION.LABEL')" | ||||||
|  |       :placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.DESCRIPTION.PLACEHOLDER')" | ||||||
|  |       :rows="2" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div class="flex gap-2"> | ||||||
|  |       <div class="flex flex-col gap-1 w-28"> | ||||||
|  |         <label class="mb-0.5 text-sm font-medium text-n-slate-12"> | ||||||
|  |           {{ t('CAPTAIN.CUSTOM_TOOLS.FORM.HTTP_METHOD.LABEL') }} | ||||||
|  |         </label> | ||||||
|  |         <ComboBox | ||||||
|  |           v-model="state.http_method" | ||||||
|  |           :options="httpMethodOptions" | ||||||
|  |           class="[&>div>button]:bg-n-alpha-black2" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |       <Input | ||||||
|  |         v-model="state.endpoint_url" | ||||||
|  |         :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.ENDPOINT_URL.LABEL')" | ||||||
|  |         :placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.ENDPOINT_URL.PLACEHOLDER')" | ||||||
|  |         :message="formErrors.endpoint_url" | ||||||
|  |         :message-type="formErrors.endpoint_url ? 'error' : 'info'" | ||||||
|  |         class="flex-1" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="flex flex-col gap-1"> | ||||||
|  |       <label class="mb-0.5 text-sm font-medium text-n-slate-12"> | ||||||
|  |         {{ t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPE.LABEL') }} | ||||||
|  |       </label> | ||||||
|  |       <ComboBox | ||||||
|  |         v-model="state.auth_type" | ||||||
|  |         :options="authTypeOptions" | ||||||
|  |         class="[&>div>button]:bg-n-alpha-black2" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <AuthConfig | ||||||
|  |       v-model:auth-config="state.auth_config" | ||||||
|  |       :auth-type="state.auth_type" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div class="flex flex-col gap-2"> | ||||||
|  |       <label class="text-sm font-medium text-n-slate-12"> | ||||||
|  |         {{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAMETERS.LABEL') }} | ||||||
|  |       </label> | ||||||
|  |       <p class="text-xs text-n-slate-11 -mt-1"> | ||||||
|  |         {{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAMETERS.HELP_TEXT') }} | ||||||
|  |       </p> | ||||||
|  |       <ul v-if="state.param_schema.length > 0" class="grid gap-2 list-none"> | ||||||
|  |         <ParamRow | ||||||
|  |           v-for="(param, index) in state.param_schema" | ||||||
|  |           :key="index" | ||||||
|  |           ref="paramsRef" | ||||||
|  |           v-model:name="param.name" | ||||||
|  |           v-model:type="param.type" | ||||||
|  |           v-model:description="param.description" | ||||||
|  |           v-model:required="param.required" | ||||||
|  |           @remove="removeParam(index)" | ||||||
|  |         /> | ||||||
|  |       </ul> | ||||||
|  |       <Button | ||||||
|  |         type="button" | ||||||
|  |         sm | ||||||
|  |         ghost | ||||||
|  |         blue | ||||||
|  |         icon="i-lucide-plus" | ||||||
|  |         :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.ADD_PARAMETER')" | ||||||
|  |         @click="addParam" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <Textarea | ||||||
|  |       v-model="state.request_template" | ||||||
|  |       :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.REQUEST_TEMPLATE.LABEL')" | ||||||
|  |       :placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.REQUEST_TEMPLATE.PLACEHOLDER')" | ||||||
|  |       :rows="4" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Textarea | ||||||
|  |       v-model="state.response_template" | ||||||
|  |       :label="t('CAPTAIN.CUSTOM_TOOLS.FORM.RESPONSE_TEMPLATE.LABEL')" | ||||||
|  |       :placeholder=" | ||||||
|  |         t('CAPTAIN.CUSTOM_TOOLS.FORM.RESPONSE_TEMPLATE.PLACEHOLDER') | ||||||
|  |       " | ||||||
|  |       :rows="4" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div class="flex gap-3 justify-between items-center w-full"> | ||||||
|  |       <Button | ||||||
|  |         type="button" | ||||||
|  |         variant="faded" | ||||||
|  |         color="slate" | ||||||
|  |         :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.FORM.CREATE')" | ||||||
|  |         class="w-full" | ||||||
|  |         :is-loading="isLoading" | ||||||
|  |         :disabled="isLoading" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | </template> | ||||||
| @@ -0,0 +1,115 @@ | |||||||
|  | <script setup> | ||||||
|  | import { computed, defineModel, ref, watch } from 'vue'; | ||||||
|  | import { useI18n } from 'vue-i18n'; | ||||||
|  | import Button from 'dashboard/components-next/button/Button.vue'; | ||||||
|  | import Input from 'dashboard/components-next/input/Input.vue'; | ||||||
|  | import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; | ||||||
|  | import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue'; | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['remove']); | ||||||
|  | const { t } = useI18n(); | ||||||
|  | const showErrors = ref(false); | ||||||
|  |  | ||||||
|  | const name = defineModel('name', { | ||||||
|  |   type: String, | ||||||
|  |   required: true, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const type = defineModel('type', { | ||||||
|  |   type: String, | ||||||
|  |   required: true, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const description = defineModel('description', { | ||||||
|  |   type: String, | ||||||
|  |   default: '', | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const required = defineModel('required', { | ||||||
|  |   type: Boolean, | ||||||
|  |   default: false, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const paramTypeOptions = computed(() => [ | ||||||
|  |   { value: 'string', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.STRING') }, | ||||||
|  |   { value: 'number', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.NUMBER') }, | ||||||
|  |   { | ||||||
|  |     value: 'boolean', | ||||||
|  |     label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.BOOLEAN'), | ||||||
|  |   }, | ||||||
|  |   { value: 'array', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.ARRAY') }, | ||||||
|  |   { value: 'object', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.OBJECT') }, | ||||||
|  | ]); | ||||||
|  |  | ||||||
|  | const validationError = computed(() => { | ||||||
|  |   if (!name.value || name.value.trim() === '') { | ||||||
|  |     return 'PARAM_NAME_REQUIRED'; | ||||||
|  |   } | ||||||
|  |   return null; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | watch([name, type, description, required], () => { | ||||||
|  |   showErrors.value = false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const validate = () => { | ||||||
|  |   showErrors.value = true; | ||||||
|  |   return !validationError.value; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | defineExpose({ validate }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <li class="list-none"> | ||||||
|  |     <div | ||||||
|  |       class="flex items-start gap-2 p-3 rounded-lg border border-n-weak bg-n-alpha-2" | ||||||
|  |       :class="{ | ||||||
|  |         'animate-wiggle border-n-ruby-9': showErrors && validationError, | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <div class="flex flex-col flex-1 gap-3"> | ||||||
|  |         <div class="flex gap-2"> | ||||||
|  |           <Input | ||||||
|  |             v-model="name" | ||||||
|  |             :placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_NAME.PLACEHOLDER')" | ||||||
|  |             class="flex-1 [&>input]:h-8 [&>input]:py-1.5 [&>input]:outline-offset-0" | ||||||
|  |           /> | ||||||
|  |           <ComboBox | ||||||
|  |             v-model="type" | ||||||
|  |             :options="paramTypeOptions" | ||||||
|  |             :placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPE.PLACEHOLDER')" | ||||||
|  |             class="w-36 [&>div>button]:h-8 [&>div>button]:py-1.5" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <Input | ||||||
|  |           v-model="description" | ||||||
|  |           :placeholder=" | ||||||
|  |             t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_DESCRIPTION.PLACEHOLDER') | ||||||
|  |           " | ||||||
|  |           class="[&>input]:h-8 [&>input]:py-1.5 [&>input]:outline-offset-0" | ||||||
|  |         /> | ||||||
|  |         <label class="flex items-center gap-2 cursor-pointer"> | ||||||
|  |           <Checkbox v-model="required" /> | ||||||
|  |           <span class="text-sm text-n-slate-11"> | ||||||
|  |             {{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_REQUIRED.LABEL') }} | ||||||
|  |           </span> | ||||||
|  |         </label> | ||||||
|  |       </div> | ||||||
|  |       <Button | ||||||
|  |         sm | ||||||
|  |         solid | ||||||
|  |         slate | ||||||
|  |         icon="i-lucide-trash" | ||||||
|  |         class="flex-shrink-0 mt-0.5" | ||||||
|  |         @click.stop="emit('remove')" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |     <span | ||||||
|  |       v-if="showErrors && validationError" | ||||||
|  |       class="block mt-1 text-sm text-n-ruby-11" | ||||||
|  |     > | ||||||
|  |       {{ t(`CAPTAIN.CUSTOM_TOOLS.FORM.ERRORS.${validationError}`) }} | ||||||
|  |     </span> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
| @@ -760,6 +760,87 @@ | |||||||
|           "TITLE": "Custom Tools", |           "TITLE": "Custom Tools", | ||||||
|           "NOTE": "Custom tools allow your assistant to interact with external APIs and services. Create tools to fetch data, perform actions, or integrate with your existing systems to enhance your assistant's capabilities." |           "NOTE": "Custom tools allow your assistant to interact with external APIs and services. Create tools to fetch data, perform actions, or integrate with your existing systems to enhance your assistant's capabilities." | ||||||
|         } |         } | ||||||
|  |       }, | ||||||
|  |       "FORM_DESCRIPTION": "Configure your custom tool to connect with external APIs", | ||||||
|  |       "CREATE": { | ||||||
|  |         "TITLE": "Create Custom Tool", | ||||||
|  |         "SUCCESS_MESSAGE": "Custom tool created successfully", | ||||||
|  |         "ERROR_MESSAGE": "Failed to create custom tool" | ||||||
|  |       }, | ||||||
|  |       "FORM": { | ||||||
|  |         "TITLE": { | ||||||
|  |           "LABEL": "Tool Name", | ||||||
|  |           "PLACEHOLDER": "Order Lookup", | ||||||
|  |           "ERROR": "Tool name is required" | ||||||
|  |         }, | ||||||
|  |         "DESCRIPTION": { | ||||||
|  |           "LABEL": "Description", | ||||||
|  |           "PLACEHOLDER": "Looks up order details by order ID" | ||||||
|  |         }, | ||||||
|  |         "HTTP_METHOD": { | ||||||
|  |           "LABEL": "Method" | ||||||
|  |         }, | ||||||
|  |         "ENDPOINT_URL": { | ||||||
|  |           "LABEL": "Endpoint URL", | ||||||
|  |           "PLACEHOLDER": "https://api.example.com/orders/{{ order_id }}", | ||||||
|  |           "ERROR": "Valid URL is required" | ||||||
|  |         }, | ||||||
|  |         "AUTH_TYPE": { | ||||||
|  |           "LABEL": "Authentication Type" | ||||||
|  |         }, | ||||||
|  |         "AUTH_TYPES": { | ||||||
|  |           "NONE": "None", | ||||||
|  |           "BEARER": "Bearer Token", | ||||||
|  |           "BASIC": "Basic Auth", | ||||||
|  |           "API_KEY": "API Key" | ||||||
|  |         }, | ||||||
|  |         "AUTH_CONFIG": { | ||||||
|  |           "BEARER_TOKEN": "Bearer Token", | ||||||
|  |           "BEARER_TOKEN_PLACEHOLDER": "Enter your bearer token", | ||||||
|  |           "USERNAME": "Username", | ||||||
|  |           "USERNAME_PLACEHOLDER": "Enter username", | ||||||
|  |           "PASSWORD": "Password", | ||||||
|  |           "PASSWORD_PLACEHOLDER": "Enter password", | ||||||
|  |           "API_KEY": "Header Name", | ||||||
|  |           "API_KEY_PLACEHOLDER": "X-API-Key", | ||||||
|  |           "API_VALUE": "Header Value", | ||||||
|  |           "API_VALUE_PLACEHOLDER": "Enter API key value" | ||||||
|  |         }, | ||||||
|  |         "PARAMETERS": { | ||||||
|  |           "LABEL": "Parameters", | ||||||
|  |           "HELP_TEXT": "Define the parameters that will be extracted from user queries" | ||||||
|  |         }, | ||||||
|  |         "ADD_PARAMETER": "Add Parameter", | ||||||
|  |         "PARAM_NAME": { | ||||||
|  |           "PLACEHOLDER": "Parameter name (e.g., order_id)" | ||||||
|  |         }, | ||||||
|  |         "PARAM_TYPE": { | ||||||
|  |           "PLACEHOLDER": "Type" | ||||||
|  |         }, | ||||||
|  |         "PARAM_TYPES": { | ||||||
|  |           "STRING": "String", | ||||||
|  |           "NUMBER": "Number", | ||||||
|  |           "BOOLEAN": "Boolean", | ||||||
|  |           "ARRAY": "Array", | ||||||
|  |           "OBJECT": "Object" | ||||||
|  |         }, | ||||||
|  |         "PARAM_DESCRIPTION": { | ||||||
|  |           "PLACEHOLDER": "Description of the parameter" | ||||||
|  |         }, | ||||||
|  |         "PARAM_REQUIRED": { | ||||||
|  |           "LABEL": "Required" | ||||||
|  |         }, | ||||||
|  |         "REQUEST_TEMPLATE": { | ||||||
|  |           "LABEL": "Request Body Template (Optional)", | ||||||
|  |           "PLACEHOLDER": "{\n  \"order_id\": \"{{ order_id }}\"\n}" | ||||||
|  |         }, | ||||||
|  |         "RESPONSE_TEMPLATE": { | ||||||
|  |           "LABEL": "Response Template (Optional)", | ||||||
|  |           "PLACEHOLDER": "Order {{ order_id }} status: {{ status }}" | ||||||
|  |         }, | ||||||
|  |         "ERRORS": { | ||||||
|  |           "PARAM_NAME_REQUIRED": "Parameter name is required" | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "RESPONSES": { |     "RESPONSES": { | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| <script setup> | <script setup> | ||||||
| import { computed, onMounted } from 'vue'; | import { computed, onMounted, ref } from 'vue'; | ||||||
| import { useMapGetter, useStore } from 'dashboard/composables/store'; | import { useMapGetter, useStore } from 'dashboard/composables/store'; | ||||||
| import { FEATURE_FLAGS } from 'dashboard/featureFlags'; | import { FEATURE_FLAGS } from 'dashboard/featureFlags'; | ||||||
|  |  | ||||||
| import PageLayout from 'dashboard/components-next/captain/PageLayout.vue'; | import PageLayout from 'dashboard/components-next/captain/PageLayout.vue'; | ||||||
| import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue'; | import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue'; | ||||||
| import CustomToolsPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/CustomToolsPageEmptyState.vue'; | import CustomToolsPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/CustomToolsPageEmptyState.vue'; | ||||||
|  | import CreateCustomToolDialog from 'dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue'; | ||||||
|  |  | ||||||
| const store = useStore(); | const store = useStore(); | ||||||
|  |  | ||||||
| @@ -14,12 +15,18 @@ const customTools = useMapGetter('captainCustomTools/getRecords'); | |||||||
| const isFetching = computed(() => uiFlags.value.fetchingList); | const isFetching = computed(() => uiFlags.value.fetchingList); | ||||||
| const customToolsMeta = useMapGetter('captainCustomTools/getMeta'); | const customToolsMeta = useMapGetter('captainCustomTools/getMeta'); | ||||||
|  |  | ||||||
|  | const createDialogRef = ref(null); | ||||||
|  |  | ||||||
| const fetchCustomTools = (page = 1) => { | const fetchCustomTools = (page = 1) => { | ||||||
|   store.dispatch('captainCustomTools/get', { page }); |   store.dispatch('captainCustomTools/get', { page }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const onPageChange = page => fetchCustomTools(page); | const onPageChange = page => fetchCustomTools(page); | ||||||
|  |  | ||||||
|  | const openCreateDialog = () => { | ||||||
|  |   createDialogRef.value.dialogRef.open(); | ||||||
|  | }; | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   fetchCustomTools(); |   fetchCustomTools(); | ||||||
| }); | }); | ||||||
| @@ -37,13 +44,14 @@ onMounted(() => { | |||||||
|     :is-empty="!customTools.length" |     :is-empty="!customTools.length" | ||||||
|     :feature-flag="FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS" |     :feature-flag="FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS" | ||||||
|     @update:current-page="onPageChange" |     @update:current-page="onPageChange" | ||||||
|  |     @button-click="openCreateDialog" | ||||||
|   > |   > | ||||||
|     <template #paywall> |     <template #paywall> | ||||||
|       <CaptainPaywall /> |       <CaptainPaywall /> | ||||||
|     </template> |     </template> | ||||||
|  |  | ||||||
|     <template #emptyState> |     <template #emptyState> | ||||||
|       <CustomToolsPageEmptyState /> |       <CustomToolsPageEmptyState @click="openCreateDialog" /> | ||||||
|     </template> |     </template> | ||||||
|  |  | ||||||
|     <template #body> |     <template #body> | ||||||
| @@ -76,4 +84,6 @@ onMounted(() => { | |||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
|   </PageLayout> |   </PageLayout> | ||||||
|  |  | ||||||
|  |   <CreateCustomToolDialog ref="createDialogRef" /> | ||||||
| </template> | </template> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra