diff --git a/app/javascript/dashboard/api/captain/customTools.js b/app/javascript/dashboard/api/captain/customTools.js
index d0818d941..471c2846b 100644
--- a/app/javascript/dashboard/api/captain/customTools.js
+++ b/app/javascript/dashboard/api/captain/customTools.js
@@ -31,6 +31,12 @@ class CaptainCustomTools extends ApiClient {
delete(id) {
return axios.delete(`${this.url}/${id}`);
}
+
+ test(data = {}) {
+ return axios.post(`${this.url}/test`, {
+ custom_tool: data,
+ });
+ }
}
export default new CaptainCustomTools();
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue
index d1d1dd011..d5f1e3e52 100644
--- a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue
@@ -101,12 +101,9 @@ const authTypeLabel = computed(() => {
-
-
-
+
+
+
{{ description }}
-import { reactive, computed, useTemplateRef, watch } from 'vue';
+import { reactive, computed, ref, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
-import { required } from '@vuelidate/validators';
+import { required, maxLength } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store';
+import CustomToolsAPI from 'dashboard/api/captain/customTools';
import Input from 'dashboard/components-next/input/Input.vue';
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
@@ -72,8 +73,12 @@ const DEFAULT_PARAM = {
required: false,
};
+// OpenAI enforces a 64-char limit on function names. The backend slug is
+// "custom_" (7 chars) + parameterized title, so cap the title conservatively.
+const MAX_TOOL_NAME_LENGTH = 55;
+
const validationRules = {
- title: { required },
+ title: { required, maxLength: maxLength(MAX_TOOL_NAME_LENGTH) },
endpoint_url: { required },
http_method: { required },
auth_type: { required },
@@ -103,9 +108,15 @@ const isLoading = computed(() =>
);
const getErrorMessage = (field, errorKey) => {
- return v$.value[field].$error
- ? t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`)
- : '';
+ if (!v$.value[field].$error) return '';
+
+ const failedRule = v$.value[field].$errors[0]?.$validator;
+ if (failedRule === 'maxLength') {
+ return t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.MAX_LENGTH_ERROR`, {
+ max: MAX_TOOL_NAME_LENGTH,
+ });
+ }
+ return t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`);
};
const formErrors = computed(() => ({
@@ -140,6 +151,30 @@ const handleSubmit = async () => {
emit('submit', state);
};
+
+const isTesting = ref(false);
+const testResult = ref(null);
+const isTestDisabled = computed(
+ () => state.endpoint_url.includes('{{') || !!state.request_template
+);
+
+const handleTest = async () => {
+ if (!state.endpoint_url) return;
+
+ isTesting.value = true;
+ testResult.value = null;
+ try {
+ const { data } = await CustomToolsAPI.test(state);
+ const isOk = data.status >= 200 && data.status < 300;
+ testResult.value = { success: isOk, status: data.status };
+ } catch (e) {
+ const message =
+ e.response?.data?.error || t('CAPTAIN.CUSTOM_TOOLS.TEST.ERROR');
+ testResult.value = { success: false, message };
+ } finally {
+ isTesting.value = false;
+ }
+};
@@ -248,6 +283,45 @@ const handleSubmit = async () => {
class="[&_textarea]:font-mono"
/>
+
+
+
+ {{ t('CAPTAIN.CUSTOM_TOOLS.TEST.DISABLED_HINT') }}
+
+
+
+ {{
+ testResult.status
+ ? t('CAPTAIN.CUSTOM_TOOLS.TEST.SUCCESS', {
+ status: testResult.status,
+ })
+ : testResult.message
+ }}
+
+
+