mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: New Scenarios page (#11975)
This commit is contained in:
		
							
								
								
									
										36
									
								
								app/javascript/dashboard/api/captain/scenarios.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/javascript/dashboard/api/captain/scenarios.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
/* global axios */
 | 
			
		||||
import ApiClient from '../ApiClient';
 | 
			
		||||
 | 
			
		||||
class CaptainScenarios extends ApiClient {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super('captain/assistants', { accountScoped: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get({ assistantId, page = 1, searchKey } = {}) {
 | 
			
		||||
    return axios.get(`${this.url}/${assistantId}/scenarios`, {
 | 
			
		||||
      params: { page, searchKey },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  show({ assistantId, id }) {
 | 
			
		||||
    return axios.get(`${this.url}/${assistantId}/scenarios/${id}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  create({ assistantId, ...data } = {}) {
 | 
			
		||||
    return axios.post(`${this.url}/${assistantId}/scenarios`, {
 | 
			
		||||
      scenario: data,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update({ assistantId, id }, data = {}) {
 | 
			
		||||
    return axios.put(`${this.url}/${assistantId}/scenarios/${id}`, {
 | 
			
		||||
      scenario: data,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete({ assistantId, id }) {
 | 
			
		||||
    return axios.delete(`${this.url}/${assistantId}/scenarios/${id}`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new CaptainScenarios();
 | 
			
		||||
							
								
								
									
										16
									
								
								app/javascript/dashboard/api/captain/tools.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/javascript/dashboard/api/captain/tools.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
/* global axios */
 | 
			
		||||
import ApiClient from '../ApiClient';
 | 
			
		||||
 | 
			
		||||
class CaptainTools extends ApiClient {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super('captain/assistants/tools', { accountScoped: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get(params = {}) {
 | 
			
		||||
    return axios.get(this.url, {
 | 
			
		||||
      params,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new CaptainTools();
 | 
			
		||||
@@ -20,6 +20,7 @@ const props = defineProps({
 | 
			
		||||
  enableVariables: { type: Boolean, default: false },
 | 
			
		||||
  enableCannedResponses: { type: Boolean, default: true },
 | 
			
		||||
  enabledMenuOptions: { type: Array, default: () => [] },
 | 
			
		||||
  enableCaptainTools: { type: Boolean, default: false },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:modelValue']);
 | 
			
		||||
@@ -98,6 +99,7 @@ watch(
 | 
			
		||||
        :enable-variables="enableVariables"
 | 
			
		||||
        :enable-canned-responses="enableCannedResponses"
 | 
			
		||||
        :enabled-menu-options="enabledMenuOptions"
 | 
			
		||||
        :enable-captain-tools="enableCaptainTools"
 | 
			
		||||
        @input="handleInput"
 | 
			
		||||
        @focus="handleFocus"
 | 
			
		||||
        @blur="handleBlur"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { useToggle } from '@vueuse/core';
 | 
			
		||||
import { vOnClickOutside } from '@vueuse/components';
 | 
			
		||||
 | 
			
		||||
import Button from 'dashboard/components-next/button/Button.vue';
 | 
			
		||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
 | 
			
		||||
@@ -44,7 +45,10 @@ const onClickCancel = () => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="inline-flex relative">
 | 
			
		||||
  <div
 | 
			
		||||
    v-on-click-outside="() => togglePopover(false)"
 | 
			
		||||
    class="inline-flex relative"
 | 
			
		||||
  >
 | 
			
		||||
    <Button
 | 
			
		||||
      :label="buttonLabel"
 | 
			
		||||
      sm
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,155 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed, reactive } from 'vue';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { useToggle } from '@vueuse/core';
 | 
			
		||||
import { useVuelidate } from '@vuelidate/core';
 | 
			
		||||
import { vOnClickOutside } from '@vueuse/components';
 | 
			
		||||
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 TextArea from 'dashboard/components-next/textarea/TextArea.vue';
 | 
			
		||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['add']);
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
const [showPopover, togglePopover] = useToggle();
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
  id: '',
 | 
			
		||||
  title: '',
 | 
			
		||||
  description: '',
 | 
			
		||||
  instruction: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
  title: { required, minLength: minLength(1) },
 | 
			
		||||
  description: { required },
 | 
			
		||||
  instruction: { required },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const v$ = useVuelidate(rules, state);
 | 
			
		||||
 | 
			
		||||
const titleError = computed(() =>
 | 
			
		||||
  v$.value.title.$error
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
 | 
			
		||||
    : ''
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const descriptionError = computed(() =>
 | 
			
		||||
  v$.value.description.$error
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
 | 
			
		||||
    : ''
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const instructionError = computed(() =>
 | 
			
		||||
  v$.value.instruction.$error
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
 | 
			
		||||
    : ''
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const resetState = () => {
 | 
			
		||||
  Object.assign(state, {
 | 
			
		||||
    id: '',
 | 
			
		||||
    title: '',
 | 
			
		||||
    description: '',
 | 
			
		||||
    instruction: '',
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onClickAdd = async () => {
 | 
			
		||||
  v$.value.$touch();
 | 
			
		||||
  if (v$.value.$invalid) return;
 | 
			
		||||
 | 
			
		||||
  await emit('add', state);
 | 
			
		||||
  resetState();
 | 
			
		||||
  togglePopover(false);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onClickCancel = () => {
 | 
			
		||||
  togglePopover(false);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    v-on-click-outside="() => togglePopover(false)"
 | 
			
		||||
    class="inline-flex relative"
 | 
			
		||||
  >
 | 
			
		||||
    <Button
 | 
			
		||||
      :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.CREATE')"
 | 
			
		||||
      sm
 | 
			
		||||
      slate
 | 
			
		||||
      class="flex-shrink-0"
 | 
			
		||||
      @click="togglePopover(!showPopover)"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
      v-if="showPopover"
 | 
			
		||||
      class="w-[31.25rem] absolute top-10 ltr:left-0 rtl:right-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6 z-50"
 | 
			
		||||
    >
 | 
			
		||||
      <h3 class="text-base font-medium text-n-slate-12">
 | 
			
		||||
        {{ t(`CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.TITLE`) }}
 | 
			
		||||
      </h3>
 | 
			
		||||
 | 
			
		||||
      <div class="flex flex-col gap-4">
 | 
			
		||||
        <Input
 | 
			
		||||
          v-model="state.title"
 | 
			
		||||
          :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
 | 
			
		||||
          :placeholder="
 | 
			
		||||
            t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
 | 
			
		||||
          "
 | 
			
		||||
          :message="titleError"
 | 
			
		||||
          :message-type="titleError ? 'error' : 'info'"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <TextArea
 | 
			
		||||
          v-model="state.description"
 | 
			
		||||
          :label="
 | 
			
		||||
            t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
 | 
			
		||||
          "
 | 
			
		||||
          :placeholder="
 | 
			
		||||
            t(
 | 
			
		||||
              'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER'
 | 
			
		||||
            )
 | 
			
		||||
          "
 | 
			
		||||
          :message="descriptionError"
 | 
			
		||||
          :message-type="descriptionError ? 'error' : 'info'"
 | 
			
		||||
          show-character-count
 | 
			
		||||
        />
 | 
			
		||||
        <Editor
 | 
			
		||||
          v-model="state.instruction"
 | 
			
		||||
          :label="
 | 
			
		||||
            t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
 | 
			
		||||
          "
 | 
			
		||||
          :placeholder="
 | 
			
		||||
            t(
 | 
			
		||||
              'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER'
 | 
			
		||||
            )
 | 
			
		||||
          "
 | 
			
		||||
          :message="instructionError"
 | 
			
		||||
          :message-type="instructionError ? 'error' : 'info'"
 | 
			
		||||
          :show-character-count="false"
 | 
			
		||||
          enable-captain-tools
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center justify-between w-full gap-3">
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="faded"
 | 
			
		||||
          color="slate"
 | 
			
		||||
          :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CANCEL')"
 | 
			
		||||
          class="w-full bg-n-alpha-2 !text-n-blue-text hover:bg-n-alpha-3"
 | 
			
		||||
          @click="onClickCancel"
 | 
			
		||||
        />
 | 
			
		||||
        <Button
 | 
			
		||||
          :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CREATE')"
 | 
			
		||||
          class="w-full"
 | 
			
		||||
          @click="onClickAdd"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -73,7 +73,6 @@ const saveEdit = () => {
 | 
			
		||||
      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">
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import ScenariosCard from './ScenariosCard.vue';
 | 
			
		||||
 | 
			
		||||
const sampleScenarios = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    title: 'Refund Order',
 | 
			
		||||
    description: 'User requests a refund for a recent purchase.',
 | 
			
		||||
    instruction:
 | 
			
		||||
      'Gather order details and reason for refund. Use [Order Search](tool://order_search) then submit with [Refund Payment](tool://refund_payment).',
 | 
			
		||||
    tools: ['order_search', 'refund_payment'],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 2,
 | 
			
		||||
    title: 'Bug Report',
 | 
			
		||||
    description: 'Customer reports a bug in the mobile app.',
 | 
			
		||||
    instruction:
 | 
			
		||||
      'Ask for reproduction steps and environment. Check [Known Issues](tool://known_issues) then create ticket with [Create Bug Report](tool://bug_report_create).',
 | 
			
		||||
    tools: ['known_issues', 'bug_report_create'],
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Story
 | 
			
		||||
    title="Captain/Assistant/ScenariosCard"
 | 
			
		||||
    :layout="{ type: 'grid', width: '800px' }"
 | 
			
		||||
  >
 | 
			
		||||
    <Variant title="Default">
 | 
			
		||||
      <div
 | 
			
		||||
        v-for="scenario in sampleScenarios"
 | 
			
		||||
        :key="scenario.id"
 | 
			
		||||
        class="px-4 py-4 bg-n-background"
 | 
			
		||||
      >
 | 
			
		||||
        <ScenariosCard
 | 
			
		||||
          :id="scenario.id"
 | 
			
		||||
          :title="scenario.title"
 | 
			
		||||
          :description="scenario.description"
 | 
			
		||||
          :instruction="scenario.instruction"
 | 
			
		||||
          :tools="scenario.tools"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Variant>
 | 
			
		||||
  </Story>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,218 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed, h, reactive } from 'vue';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { useToggle } from '@vueuse/core';
 | 
			
		||||
import { useVuelidate } from '@vuelidate/core';
 | 
			
		||||
import { required, minLength } from '@vuelidate/validators';
 | 
			
		||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
 | 
			
		||||
import Input from 'dashboard/components-next/input/Input.vue';
 | 
			
		||||
import Button from 'dashboard/components-next/button/Button.vue';
 | 
			
		||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
 | 
			
		||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
 | 
			
		||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
 | 
			
		||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  title: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  description: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  instruction: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  tools: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  selectable: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
  isSelected: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['select', 'hover', 'delete', 'update']);
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
const { formatMessage } = useMessageFormatter();
 | 
			
		||||
 | 
			
		||||
const modelValue = computed({
 | 
			
		||||
  get: () => props.isSelected,
 | 
			
		||||
  set: () => emit('select', props.id),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
  id: '',
 | 
			
		||||
  title: '',
 | 
			
		||||
  description: '',
 | 
			
		||||
  instruction: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [isEditing, toggleEditing] = useToggle();
 | 
			
		||||
 | 
			
		||||
const startEdit = () => {
 | 
			
		||||
  Object.assign(state, {
 | 
			
		||||
    id: props.id,
 | 
			
		||||
    title: props.title,
 | 
			
		||||
    description: props.description,
 | 
			
		||||
    instruction: props.instruction,
 | 
			
		||||
    tools: props.tools,
 | 
			
		||||
  });
 | 
			
		||||
  toggleEditing(true);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
  title: { required, minLength: minLength(1) },
 | 
			
		||||
  description: { required },
 | 
			
		||||
  instruction: { required },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const v$ = useVuelidate(rules, state);
 | 
			
		||||
 | 
			
		||||
const titleError = computed(() =>
 | 
			
		||||
  v$.value.title.$error
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
 | 
			
		||||
    : ''
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const descriptionError = computed(() =>
 | 
			
		||||
  v$.value.description.$error
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
 | 
			
		||||
    : ''
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const onClickUpdate = () => {
 | 
			
		||||
  v$.value.$touch();
 | 
			
		||||
  if (v$.value.$invalid) return;
 | 
			
		||||
  emit('update', { ...state });
 | 
			
		||||
  toggleEditing(false);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const instructionError = computed(() =>
 | 
			
		||||
  v$.value.instruction.$error
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
 | 
			
		||||
    : ''
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const LINK_INSTRUCTION_CLASS =
 | 
			
		||||
  '[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
 | 
			
		||||
 | 
			
		||||
const renderInstruction = instruction => () =>
 | 
			
		||||
  h('p', {
 | 
			
		||||
    class: `text-sm text-n-slate-12 py-4 mb-0 [&_ol]:list-decimal ${LINK_INSTRUCTION_CLASS}`,
 | 
			
		||||
    innerHTML: instruction,
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <CardLayout
 | 
			
		||||
    selectable
 | 
			
		||||
    class="relative [&>div]:!py-4"
 | 
			
		||||
    :class="{
 | 
			
		||||
      '[&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4': !isEditing,
 | 
			
		||||
      '[&>div]:ltr:!pr-10 [&>div]:rtl:!pl-10': isEditing,
 | 
			
		||||
    }"
 | 
			
		||||
    layout="row"
 | 
			
		||||
    @mouseenter="emit('hover', true)"
 | 
			
		||||
    @mouseleave="emit('hover', false)"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      v-show="selectable && !isEditing"
 | 
			
		||||
      class="absolute top-[1.125rem] ltr:left-3 rtl:right-3"
 | 
			
		||||
    >
 | 
			
		||||
      <Checkbox v-model="modelValue" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-if="!isEditing" class="flex flex-col w-full">
 | 
			
		||||
      <div class="flex items-start justify-between w-full gap-2">
 | 
			
		||||
        <div class="flex flex-col items-start">
 | 
			
		||||
          <span class="text-sm text-n-slate-12 font-medium">{{ title }}</span>
 | 
			
		||||
          <span class="text-sm text-n-slate-11 mt-2">
 | 
			
		||||
            {{ description }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex items-center gap-2">
 | 
			
		||||
          <!-- <Button label="Test" slate xs ghost class="!text-sm" />
 | 
			
		||||
          <span class="w-px h-4 bg-n-weak" /> -->
 | 
			
		||||
          <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>
 | 
			
		||||
      </div>
 | 
			
		||||
      <component :is="renderInstruction(formatMessage(instruction, false))" />
 | 
			
		||||
      <span class="text-sm text-n-slate-11 font-medium mb-1">
 | 
			
		||||
        {{ t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TOOLS_USED') }}
 | 
			
		||||
        {{ tools?.map(tool => `@${tool}`).join(', ') }}
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-else class="overflow-hidden flex flex-col gap-4 w-full">
 | 
			
		||||
      <Input
 | 
			
		||||
        v-model="state.title"
 | 
			
		||||
        :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
 | 
			
		||||
        :placeholder="
 | 
			
		||||
          t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
 | 
			
		||||
        "
 | 
			
		||||
        :message="titleError"
 | 
			
		||||
        :message-type="titleError ? 'error' : 'info'"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <TextArea
 | 
			
		||||
        v-model="state.description"
 | 
			
		||||
        :label="
 | 
			
		||||
          t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
 | 
			
		||||
        "
 | 
			
		||||
        :placeholder="
 | 
			
		||||
          t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER')
 | 
			
		||||
        "
 | 
			
		||||
        :message="descriptionError"
 | 
			
		||||
        :message-type="descriptionError ? 'error' : 'info'"
 | 
			
		||||
        show-character-count
 | 
			
		||||
      />
 | 
			
		||||
      <Editor
 | 
			
		||||
        v-model="state.instruction"
 | 
			
		||||
        :label="
 | 
			
		||||
          t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
 | 
			
		||||
        "
 | 
			
		||||
        :placeholder="
 | 
			
		||||
          t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER')
 | 
			
		||||
        "
 | 
			
		||||
        :message="instructionError"
 | 
			
		||||
        :message-type="instructionError ? 'error' : 'info'"
 | 
			
		||||
        :show-character-count="false"
 | 
			
		||||
        enable-captain-tools
 | 
			
		||||
      />
 | 
			
		||||
      <div class="flex items-center gap-3">
 | 
			
		||||
        <Button
 | 
			
		||||
          faded
 | 
			
		||||
          slate
 | 
			
		||||
          sm
 | 
			
		||||
          :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.CANCEL')"
 | 
			
		||||
          @click="toggleEditing(false)"
 | 
			
		||||
        />
 | 
			
		||||
        <Button
 | 
			
		||||
          sm
 | 
			
		||||
          :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.UPDATE')"
 | 
			
		||||
          @click="onClickUpdate"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </CardLayout>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import ToolsDropdown from './ToolsDropdown.vue';
 | 
			
		||||
 | 
			
		||||
const items = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 'order_search',
 | 
			
		||||
    title: 'Order Search',
 | 
			
		||||
    description: 'Lookup orders by customer ID, email, or order number',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 'refund_payment',
 | 
			
		||||
    title: 'Refund Payment',
 | 
			
		||||
    description: 'Initiates a refund on a specific payment',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 'fetch_customer',
 | 
			
		||||
    title: 'Fetch Customer',
 | 
			
		||||
    description: 'Pulls customer details (email, tags, last seen, etc.)',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const selectedIndex = ref(0);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Story
 | 
			
		||||
    title="Captain/Assistant/ToolsDropdown"
 | 
			
		||||
    :layout="{ type: 'grid', width: '600px' }"
 | 
			
		||||
  >
 | 
			
		||||
    <Variant title="Default">
 | 
			
		||||
      <div class="relative h-80 bg-n-background p-4">
 | 
			
		||||
        <ToolsDropdown :items="items" :selected-index="selectedIndex" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Variant>
 | 
			
		||||
  </Story>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, watch, nextTick } from 'vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  items: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  selectedIndex: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    default: 0,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['select']);
 | 
			
		||||
 | 
			
		||||
const toolsDropdownRef = ref(null);
 | 
			
		||||
 | 
			
		||||
const onItemClick = idx => emit('select', idx);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.selectedIndex,
 | 
			
		||||
  () => {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      const el = toolsDropdownRef.value?.querySelector(
 | 
			
		||||
        `#tool-item-${props.selectedIndex}`
 | 
			
		||||
      );
 | 
			
		||||
      if (el) {
 | 
			
		||||
        el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    ref="toolsDropdownRef"
 | 
			
		||||
    class="w-[22.5rem] p-2 flex flex-col gap-1 z-50 absolute rounded-xl bg-n-alpha-3 shadow outline outline-1 outline-n-weak backdrop-blur-[50px] max-h-[20rem] overflow-y-auto"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="(tool, idx) in items"
 | 
			
		||||
      :id="`tool-item-${idx}`"
 | 
			
		||||
      :key="tool.id || idx"
 | 
			
		||||
      :class="{ 'bg-n-alpha-black2': idx === selectedIndex }"
 | 
			
		||||
      class="flex flex-col gap-1 rounded-md py-2 px-2 cursor-pointer hover:bg-n-alpha-black2"
 | 
			
		||||
      @click="onItemClick(idx)"
 | 
			
		||||
    >
 | 
			
		||||
      <span class="text-n-slate-12 font-medium text-sm">{{ tool.title }}</span>
 | 
			
		||||
      <span class="text-n-slate-11 text-sm">{{ tool.description }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -15,6 +15,7 @@ import CannedResponse from '../conversation/CannedResponse.vue';
 | 
			
		||||
import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
 | 
			
		||||
import TagAgents from '../conversation/TagAgents.vue';
 | 
			
		||||
import VariableList from '../conversation/VariableList.vue';
 | 
			
		||||
import TagTools from '../conversation/TagTools.vue';
 | 
			
		||||
 | 
			
		||||
import { useEmitter } from 'dashboard/composables/emitter';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
@@ -72,6 +73,7 @@ const props = defineProps({
 | 
			
		||||
  updateSelectionWith: { type: String, default: '' },
 | 
			
		||||
  enableVariables: { type: Boolean, default: false },
 | 
			
		||||
  enableCannedResponses: { type: Boolean, default: true },
 | 
			
		||||
  enableCaptainTools: { type: Boolean, default: false },
 | 
			
		||||
  variables: { type: Object, default: () => ({}) },
 | 
			
		||||
  enabledMenuOptions: { type: Array, default: () => [] },
 | 
			
		||||
  signature: { type: String, default: '' },
 | 
			
		||||
@@ -89,6 +91,7 @@ const emit = defineEmits([
 | 
			
		||||
  'toggleUserMention',
 | 
			
		||||
  'toggleCannedMenu',
 | 
			
		||||
  'toggleVariablesMenu',
 | 
			
		||||
  'toggleToolsMenu',
 | 
			
		||||
  'clearSelection',
 | 
			
		||||
  'blur',
 | 
			
		||||
  'focus',
 | 
			
		||||
@@ -140,7 +143,9 @@ const showUserMentions = ref(false);
 | 
			
		||||
const showCannedMenu = ref(false);
 | 
			
		||||
const showVariables = ref(false);
 | 
			
		||||
const showEmojiMenu = ref(false);
 | 
			
		||||
const showToolsMenu = ref(false);
 | 
			
		||||
const mentionSearchKey = ref('');
 | 
			
		||||
const toolSearchKey = ref('');
 | 
			
		||||
const cannedSearchTerm = ref('');
 | 
			
		||||
const variableSearchTerm = ref('');
 | 
			
		||||
const emojiSearchTerm = ref('');
 | 
			
		||||
@@ -216,11 +221,17 @@ const plugins = computed(() => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    createSuggestionPlugin({
 | 
			
		||||
      trigger: '@',
 | 
			
		||||
      showMenu: showToolsMenu,
 | 
			
		||||
      searchTerm: toolSearchKey,
 | 
			
		||||
      isAllowed: () => props.enableCaptainTools,
 | 
			
		||||
    }),
 | 
			
		||||
    createSuggestionPlugin({
 | 
			
		||||
      trigger: '@',
 | 
			
		||||
      showMenu: showUserMentions,
 | 
			
		||||
      searchTerm: mentionSearchKey,
 | 
			
		||||
      isAllowed: () => props.isPrivate,
 | 
			
		||||
      isAllowed: () => props.isPrivate || !props.enableCaptainTools,
 | 
			
		||||
    }),
 | 
			
		||||
    createSuggestionPlugin({
 | 
			
		||||
      trigger: '/',
 | 
			
		||||
@@ -262,6 +273,9 @@ watch(showCannedMenu, updatedValue => {
 | 
			
		||||
watch(showVariables, updatedValue => {
 | 
			
		||||
  emit('toggleVariablesMenu', !props.isPrivate && updatedValue);
 | 
			
		||||
});
 | 
			
		||||
watch(showToolsMenu, updatedValue => {
 | 
			
		||||
  emit('toggleToolsMenu', props.enableCaptainTools && updatedValue);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function focusEditorInputField(pos = 'end') {
 | 
			
		||||
  const { tr } = editorView.state;
 | 
			
		||||
@@ -538,6 +552,7 @@ function insertSpecialContent(type, content) {
 | 
			
		||||
    cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
 | 
			
		||||
    variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
 | 
			
		||||
    emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
 | 
			
		||||
    tool: CONVERSATION_EVENTS.INSERTED_A_TOOL,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useTrack(event_map[type]);
 | 
			
		||||
@@ -699,6 +714,11 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
 | 
			
		||||
      :search-key="emojiSearchTerm"
 | 
			
		||||
      @select-emoji="emoji => insertSpecialContent('emoji', emoji)"
 | 
			
		||||
    />
 | 
			
		||||
    <TagTools
 | 
			
		||||
      v-if="showToolsMenu"
 | 
			
		||||
      :search-key="toolSearchKey"
 | 
			
		||||
      @select-tool="content => insertSpecialContent('tool', content)"
 | 
			
		||||
    />
 | 
			
		||||
    <input
 | 
			
		||||
      ref="imageUpload"
 | 
			
		||||
      type="file"
 | 
			
		||||
@@ -812,6 +832,10 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prosemirror-tools-node {
 | 
			
		||||
  @apply font-medium text-n-slate-12 py-0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.editor-wrap {
 | 
			
		||||
  @apply mb-4;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, computed, watch } from 'vue';
 | 
			
		||||
import ToolsDropdown from 'dashboard/components-next/captain/assistant/ToolsDropdown.vue';
 | 
			
		||||
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
 | 
			
		||||
import { useMapGetter } from 'dashboard/composables/store.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  searchKey: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: '',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['selectTool']);
 | 
			
		||||
 | 
			
		||||
const tools = useMapGetter('captainTools/getRecords');
 | 
			
		||||
 | 
			
		||||
const selectedIndex = ref(0);
 | 
			
		||||
 | 
			
		||||
const filteredTools = computed(() => {
 | 
			
		||||
  const search = props.searchKey?.trim().toLowerCase() || '';
 | 
			
		||||
 | 
			
		||||
  return tools.value.filter(tool => tool.title.toLowerCase().includes(search));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const adjustScroll = () => {};
 | 
			
		||||
 | 
			
		||||
const onSelect = idx => {
 | 
			
		||||
  if (idx) selectedIndex.value = idx;
 | 
			
		||||
  emit('selectTool', filteredTools.value[selectedIndex.value]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
useKeyboardNavigableList({
 | 
			
		||||
  items: filteredTools,
 | 
			
		||||
  onSelect,
 | 
			
		||||
  adjustScroll,
 | 
			
		||||
  selectedIndex,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(filteredTools, newListOfTools => {
 | 
			
		||||
  if (newListOfTools.length < selectedIndex.value + 1) {
 | 
			
		||||
    selectedIndex.value = 0;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <ToolsDropdown
 | 
			
		||||
    v-if="filteredTools.length"
 | 
			
		||||
    :items="filteredTools"
 | 
			
		||||
    :selected-index="selectedIndex"
 | 
			
		||||
    class="bottom-20"
 | 
			
		||||
    @select="onSelect"
 | 
			
		||||
  />
 | 
			
		||||
  <template v-else />
 | 
			
		||||
</template>
 | 
			
		||||
@@ -6,6 +6,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
 | 
			
		||||
  TRANSLATE_A_MESSAGE: 'Translated a message',
 | 
			
		||||
  INSERTED_A_VARIABLE: 'Inserted a variable',
 | 
			
		||||
  INSERTED_AN_EMOJI: 'Inserted an emoji',
 | 
			
		||||
  INSERTED_A_TOOL: 'Inserted a tool',
 | 
			
		||||
  USED_MENTIONS: 'Used mentions',
 | 
			
		||||
  SEARCH_CONVERSATION: 'Searched conversations',
 | 
			
		||||
  APPLY_FILTER: 'Applied filters in the conversation list',
 | 
			
		||||
 
 | 
			
		||||
@@ -319,6 +319,12 @@ const createNode = (editorView, nodeType, content) => {
 | 
			
		||||
      return state.schema.text(`{{${content}}}`);
 | 
			
		||||
    case 'emoji':
 | 
			
		||||
      return state.schema.text(content);
 | 
			
		||||
    case 'tool': {
 | 
			
		||||
      return state.schema.nodes.tools.create({
 | 
			
		||||
        id: content.id,
 | 
			
		||||
        name: content.title,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return null;
 | 
			
		||||
  }
 | 
			
		||||
@@ -355,6 +361,11 @@ const nodeCreators = {
 | 
			
		||||
    from,
 | 
			
		||||
    to,
 | 
			
		||||
  }),
 | 
			
		||||
  tool: (editorView, content, from, to) => ({
 | 
			
		||||
    node: createNode(editorView, 'tool', content),
 | 
			
		||||
    from,
 | 
			
		||||
    to,
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -554,6 +554,7 @@
 | 
			
		||||
          "SEARCH_PLACEHOLDER": "Search..."
 | 
			
		||||
        },
 | 
			
		||||
        "EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
 | 
			
		||||
        "SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
 | 
			
		||||
        "API": {
 | 
			
		||||
          "ADD": {
 | 
			
		||||
            "SUCCESS": "Guardrails added successfully",
 | 
			
		||||
@@ -601,6 +602,7 @@
 | 
			
		||||
          "SEARCH_PLACEHOLDER": "Search..."
 | 
			
		||||
        },
 | 
			
		||||
        "EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
 | 
			
		||||
        "SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
 | 
			
		||||
        "API": {
 | 
			
		||||
          "ADD": {
 | 
			
		||||
            "SUCCESS": "Response Guidelines added successfully",
 | 
			
		||||
@@ -615,6 +617,73 @@
 | 
			
		||||
            "ERROR": "There was an error deleting response guidelines, please try again."
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "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.”",
 | 
			
		||||
        "BREADCRUMB": {
 | 
			
		||||
          "TITLE": "Scenarios"
 | 
			
		||||
        },
 | 
			
		||||
        "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 scenarios",
 | 
			
		||||
            "ADD": "Add all",
 | 
			
		||||
            "ADD_SINGLE": "Add this",
 | 
			
		||||
            "TOOLS_USED": "Tools used :"
 | 
			
		||||
          },
 | 
			
		||||
          "NEW": {
 | 
			
		||||
            "CREATE": "Add a scenario",
 | 
			
		||||
            "TITLE": "Create a scenario",
 | 
			
		||||
            "FORM": {
 | 
			
		||||
              "TITLE": {
 | 
			
		||||
                "LABEL": "Title",
 | 
			
		||||
                "PLACEHOLDER": "Enter a name for the scenario",
 | 
			
		||||
                "ERROR": "Scenario name is required"
 | 
			
		||||
              },
 | 
			
		||||
              "DESCRIPTION": {
 | 
			
		||||
                "LABEL": "Description",
 | 
			
		||||
                "PLACEHOLDER": "Describe how and where this scenario will be used",
 | 
			
		||||
                "ERROR": "Scenario description is required"
 | 
			
		||||
              },
 | 
			
		||||
              "INSTRUCTION": {
 | 
			
		||||
                "LABEL": "How to handle",
 | 
			
		||||
                "PLACEHOLDER": "Describe how and where this scenario will be handled",
 | 
			
		||||
                "ERROR": "Scenario content is required"
 | 
			
		||||
              },
 | 
			
		||||
              "CREATE": "Create",
 | 
			
		||||
              "CANCEL": "Cancel"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "UPDATE": {
 | 
			
		||||
          "CANCEL": "Cancel",
 | 
			
		||||
          "UPDATE": "Update changes"
 | 
			
		||||
        },
 | 
			
		||||
        "LIST": {
 | 
			
		||||
          "SEARCH_PLACEHOLDER": "Search..."
 | 
			
		||||
        },
 | 
			
		||||
        "EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
 | 
			
		||||
        "SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
 | 
			
		||||
        "API": {
 | 
			
		||||
          "ADD": {
 | 
			
		||||
            "SUCCESS": "Scenarios added successfully",
 | 
			
		||||
            "ERROR": "There was an error adding scenarios, please try again."
 | 
			
		||||
          },
 | 
			
		||||
          "UPDATE": {
 | 
			
		||||
            "SUCCESS": "Scenarios updated successfully",
 | 
			
		||||
            "ERROR": "There was an error updating scenarios, please try again."
 | 
			
		||||
          },
 | 
			
		||||
          "DELETE": {
 | 
			
		||||
            "SUCCESS": "Scenarios deleted successfully",
 | 
			
		||||
            "ERROR": "There was an error deleting scenarios, please try again."
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "DOCUMENTS": {
 | 
			
		||||
 
 | 
			
		||||
@@ -266,6 +266,11 @@ const addAllExample = () => {
 | 
			
		||||
            {{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.EMPTY_MESSAGE') }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else-if="filteredGuardrails.length === 0" class="mt-1 mb-2">
 | 
			
		||||
          <span class="text-n-slate-11 text-sm">
 | 
			
		||||
            {{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.SEARCH_EMPTY_MESSAGE') }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else class="flex flex-col gap-2">
 | 
			
		||||
          <RuleCard
 | 
			
		||||
            v-for="guardrail in filteredGuardrails"
 | 
			
		||||
 
 | 
			
		||||
@@ -284,6 +284,13 @@ const addAllExample = async () => {
 | 
			
		||||
            {{ t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.EMPTY_MESSAGE') }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else-if="filteredGuidelines.length === 0" class="mt-1 mb-2">
 | 
			
		||||
          <span class="text-n-slate-11 text-sm">
 | 
			
		||||
            {{
 | 
			
		||||
              t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.SEARCH_EMPTY_MESSAGE')
 | 
			
		||||
            }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else class="flex flex-col gap-2">
 | 
			
		||||
          <RuleCard
 | 
			
		||||
            v-for="guideline in filteredGuidelines"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,320 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed, h, ref, onMounted } from 'vue';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { picoSearch } from '@scmmishra/pico-search';
 | 
			
		||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
 | 
			
		||||
import { useAlert } from 'dashboard/composables';
 | 
			
		||||
import { useUISettings } from 'dashboard/composables/useUISettings';
 | 
			
		||||
import Button from 'dashboard/components-next/button/Button.vue';
 | 
			
		||||
import Input from 'dashboard/components-next/input/Input.vue';
 | 
			
		||||
 | 
			
		||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
 | 
			
		||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
 | 
			
		||||
import SuggestedScenarios from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
 | 
			
		||||
import ScenariosCard from 'dashboard/components-next/captain/assistant/ScenariosCard.vue';
 | 
			
		||||
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
 | 
			
		||||
import AddNewScenariosDialog from 'dashboard/components-next/captain/assistant/AddNewScenariosDialog.vue';
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const store = useStore();
 | 
			
		||||
const { uiSettings, updateUISettings } = useUISettings();
 | 
			
		||||
const assistantId = route.params.assistantId;
 | 
			
		||||
 | 
			
		||||
const uiFlags = useMapGetter('captainScenarios/getUIFlags');
 | 
			
		||||
const isFetching = computed(() => uiFlags.value.fetchingList);
 | 
			
		||||
const assistant = computed(() =>
 | 
			
		||||
  store.getters['captainAssistants/getRecord'](Number(assistantId))
 | 
			
		||||
);
 | 
			
		||||
const scenarios = useMapGetter('captainScenarios/getRecords');
 | 
			
		||||
 | 
			
		||||
const searchQuery = ref('');
 | 
			
		||||
 | 
			
		||||
const breadcrumbItems = computed(() => {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
 | 
			
		||||
      routeName: 'captain_assistants_index',
 | 
			
		||||
    },
 | 
			
		||||
    { label: assistant.value?.name, routeName: 'captain_assistants_edit' },
 | 
			
		||||
    { label: t('CAPTAIN.ASSISTANTS.SCENARIOS.BREADCRUMB.TITLE') },
 | 
			
		||||
  ];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const TOOL_LINK_REGEX = /\[([^\]]+)]\(tool:\/\/.+?\)/g;
 | 
			
		||||
 | 
			
		||||
const renderInstruction = instruction => () =>
 | 
			
		||||
  h('span', {
 | 
			
		||||
    class: 'text-sm text-n-slate-12 py-4',
 | 
			
		||||
    innerHTML: instruction.replace(
 | 
			
		||||
      TOOL_LINK_REGEX,
 | 
			
		||||
      (_, title) =>
 | 
			
		||||
        `<span class="text-n-iris-11 font-medium">@${title.replace(/^@/, '')}</span>`
 | 
			
		||||
    ),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
// Suggested example scenarios for quick add
 | 
			
		||||
const scenariosExample = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    title: 'Refund Order',
 | 
			
		||||
    description: 'User encountered a technical issue or error message.',
 | 
			
		||||
    instruction:
 | 
			
		||||
      'Ask for steps to reproduce + browser/app version. Use [Known Issues](tool://known_issues) to check if it’s a known bug. File with [Create Bug Report](tool://bug_report_create) if new.',
 | 
			
		||||
    tools: ['create_bug_report', 'known_issues'],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 2,
 | 
			
		||||
    title: 'Product Recommendation',
 | 
			
		||||
    description: 'User is unsure which product or service to choose.',
 | 
			
		||||
    instruction:
 | 
			
		||||
      'Ask 2–3 clarifying questions. Use [Product Match](tool://product_match[user_needs]) and suggest 2–3 options with pros/cons. Link to compare page if available.',
 | 
			
		||||
    tools: ['product_match[user_needs]'],
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const filteredScenarios = computed(() => {
 | 
			
		||||
  const query = searchQuery.value.trim();
 | 
			
		||||
  const source = scenarios.value;
 | 
			
		||||
  if (!query) return source;
 | 
			
		||||
  return picoSearch(source, query, ['title', 'description', 'instruction']);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const shouldShowSuggestedRules = computed(() => {
 | 
			
		||||
  return uiSettings.value?.show_scenarios_suggestions !== false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const closeSuggestedRules = () => {
 | 
			
		||||
  updateUISettings({ show_scenarios_suggestions: false });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Bulk selection & hover state
 | 
			
		||||
const bulkSelectedIds = ref(new Set());
 | 
			
		||||
const hoveredCard = ref(null);
 | 
			
		||||
 | 
			
		||||
const handleRuleSelect = id => {
 | 
			
		||||
  const selected = new Set(bulkSelectedIds.value);
 | 
			
		||||
  selected[selected.has(id) ? 'delete' : 'add'](id);
 | 
			
		||||
  bulkSelectedIds.value = selected;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const buildSelectedCountLabel = computed(() => {
 | 
			
		||||
  const count = scenarios.value.length || 0;
 | 
			
		||||
  const isAllSelected = bulkSelectedIds.value.size === count && count > 0;
 | 
			
		||||
  return isAllSelected
 | 
			
		||||
    ? t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.UNSELECT_ALL', { count })
 | 
			
		||||
    : t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.SELECT_ALL', { count });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const selectedCountLabel = computed(() => {
 | 
			
		||||
  return t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.SELECTED', {
 | 
			
		||||
    count: bulkSelectedIds.value.size,
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const handleRuleHover = (isHovered, id) => {
 | 
			
		||||
  hoveredCard.value = isHovered ? id : null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getToolsFromInstruction = instruction => [
 | 
			
		||||
  ...new Set(
 | 
			
		||||
    [...(instruction?.matchAll(/\(tool:\/\/([^)]+)\)/g) ?? [])].map(m => m[1])
 | 
			
		||||
  ),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const updateScenario = async scenario => {
 | 
			
		||||
  try {
 | 
			
		||||
    await store.dispatch('captainScenarios/update', {
 | 
			
		||||
      id: scenario.id,
 | 
			
		||||
      assistantId: route.params.assistantId,
 | 
			
		||||
      ...scenario,
 | 
			
		||||
      tools: getToolsFromInstruction(scenario.instruction),
 | 
			
		||||
    });
 | 
			
		||||
    useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.UPDATE.SUCCESS'));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    const errorMessage =
 | 
			
		||||
      error?.response?.message ||
 | 
			
		||||
      t('CAPTAIN.ASSISTANTS.SCENARIOS.API.UPDATE.ERROR');
 | 
			
		||||
    useAlert(errorMessage);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteScenario = async id => {
 | 
			
		||||
  try {
 | 
			
		||||
    await store.dispatch('captainScenarios/delete', {
 | 
			
		||||
      id,
 | 
			
		||||
      assistantId: route.params.assistantId,
 | 
			
		||||
    });
 | 
			
		||||
    useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    const errorMessage =
 | 
			
		||||
      error?.response?.message ||
 | 
			
		||||
      t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.ERROR');
 | 
			
		||||
    useAlert(errorMessage);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// TODO: Add bulk delete endpoint
 | 
			
		||||
const bulkDeleteScenarios = async ids => {
 | 
			
		||||
  const idsArray = ids || Array.from(bulkSelectedIds.value);
 | 
			
		||||
  await Promise.all(
 | 
			
		||||
    idsArray.map(id =>
 | 
			
		||||
      store.dispatch('captainScenarios/delete', {
 | 
			
		||||
        id,
 | 
			
		||||
        assistantId: route.params.assistantId,
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
  bulkSelectedIds.value = new Set();
 | 
			
		||||
  useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addScenario = async scenario => {
 | 
			
		||||
  try {
 | 
			
		||||
    await store.dispatch('captainScenarios/create', {
 | 
			
		||||
      assistantId: route.params.assistantId,
 | 
			
		||||
      ...scenario,
 | 
			
		||||
      tools: getToolsFromInstruction(scenario.instruction),
 | 
			
		||||
    });
 | 
			
		||||
    useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.SUCCESS'));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    const errorMessage =
 | 
			
		||||
      error?.response?.message ||
 | 
			
		||||
      t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.ERROR');
 | 
			
		||||
    useAlert(errorMessage);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addAllExampleScenarios = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    scenariosExample.forEach(async scenario => {
 | 
			
		||||
      await store.dispatch('captainScenarios/create', {
 | 
			
		||||
        assistantId: route.params.assistantId,
 | 
			
		||||
        ...scenario,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.SUCCESS'));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    const errorMessage =
 | 
			
		||||
      error?.response?.message ||
 | 
			
		||||
      t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.ERROR');
 | 
			
		||||
    useAlert(errorMessage);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  store.dispatch('captainScenarios/get', {
 | 
			
		||||
    assistantId: assistantId,
 | 
			
		||||
  });
 | 
			
		||||
  store.dispatch('captainTools/getTools');
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <SettingsPageLayout
 | 
			
		||||
    :breadcrumb-items="breadcrumbItems"
 | 
			
		||||
    :is-fetching="isFetching"
 | 
			
		||||
  >
 | 
			
		||||
    <template #body>
 | 
			
		||||
      <SettingsHeader
 | 
			
		||||
        :heading="$t('CAPTAIN.ASSISTANTS.SCENARIOS.TITLE')"
 | 
			
		||||
        :description="$t('CAPTAIN.ASSISTANTS.SCENARIOS.DESCRIPTION')"
 | 
			
		||||
      />
 | 
			
		||||
      <div v-if="shouldShowSuggestedRules" class="flex mt-7 flex-col gap-4">
 | 
			
		||||
        <SuggestedScenarios
 | 
			
		||||
          :title="$t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TITLE')"
 | 
			
		||||
          :items="scenariosExample"
 | 
			
		||||
          @close="closeSuggestedRules"
 | 
			
		||||
          @add="addAllExampleScenarios"
 | 
			
		||||
        >
 | 
			
		||||
          <template #default="{ item }">
 | 
			
		||||
            <div class="flex items-center gap-3 justify-between">
 | 
			
		||||
              <span class="text-sm text-n-slate-12">
 | 
			
		||||
                {{ item.title }}
 | 
			
		||||
              </span>
 | 
			
		||||
              <Button
 | 
			
		||||
                :label="
 | 
			
		||||
                  $t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.ADD_SINGLE')
 | 
			
		||||
                "
 | 
			
		||||
                ghost
 | 
			
		||||
                xs
 | 
			
		||||
                slate
 | 
			
		||||
                class="!text-sm !text-n-slate-11 flex-shrink-0"
 | 
			
		||||
                @click="addScenario(item)"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="flex flex-col">
 | 
			
		||||
              <span class="text-sm text-n-slate-11 mt-2">
 | 
			
		||||
                {{ item.description }}
 | 
			
		||||
              </span>
 | 
			
		||||
              <component :is="renderInstruction(item.instruction)" />
 | 
			
		||||
              <span class="text-sm text-n-slate-11 font-medium mb-1">
 | 
			
		||||
                {{ t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TOOLS_USED') }}
 | 
			
		||||
                {{ item.tools?.map(tool => `@${tool}`).join(', ') }}
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </SuggestedScenarios>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="flex mt-7 flex-col gap-4">
 | 
			
		||||
        <div class="flex justify-between items-center">
 | 
			
		||||
          <BulkSelectBar
 | 
			
		||||
            v-model="bulkSelectedIds"
 | 
			
		||||
            :all-items="scenarios"
 | 
			
		||||
            :select-all-label="buildSelectedCountLabel"
 | 
			
		||||
            :selected-count-label="selectedCountLabel"
 | 
			
		||||
            :delete-label="
 | 
			
		||||
              $t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.BULK_DELETE_BUTTON')
 | 
			
		||||
            "
 | 
			
		||||
            @bulk-delete="bulkDeleteScenarios"
 | 
			
		||||
          >
 | 
			
		||||
            <template #default-actions>
 | 
			
		||||
              <AddNewScenariosDialog @add="addScenario" />
 | 
			
		||||
            </template>
 | 
			
		||||
          </BulkSelectBar>
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="scenarios.length && bulkSelectedIds.size === 0"
 | 
			
		||||
            class="max-w-[22.5rem] w-full min-w-0"
 | 
			
		||||
          >
 | 
			
		||||
            <Input
 | 
			
		||||
              v-model="searchQuery"
 | 
			
		||||
              :placeholder="
 | 
			
		||||
                t('CAPTAIN.ASSISTANTS.SCENARIOS.LIST.SEARCH_PLACEHOLDER')
 | 
			
		||||
              "
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="scenarios.length === 0" class="mt-1 mb-2">
 | 
			
		||||
          <span class="text-n-slate-11 text-sm">
 | 
			
		||||
            {{ t('CAPTAIN.ASSISTANTS.SCENARIOS.EMPTY_MESSAGE') }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else-if="filteredScenarios.length === 0" class="mt-1 mb-2">
 | 
			
		||||
          <span class="text-n-slate-11 text-sm">
 | 
			
		||||
            {{ t('CAPTAIN.ASSISTANTS.SCENARIOS.SEARCH_EMPTY_MESSAGE') }}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else class="flex flex-col gap-2">
 | 
			
		||||
          <ScenariosCard
 | 
			
		||||
            v-for="scenario in filteredScenarios"
 | 
			
		||||
            :id="scenario.id"
 | 
			
		||||
            :key="scenario.id"
 | 
			
		||||
            :title="scenario.title"
 | 
			
		||||
            :description="scenario.description"
 | 
			
		||||
            :instruction="scenario.instruction"
 | 
			
		||||
            :tools="scenario.tools"
 | 
			
		||||
            :is-selected="bulkSelectedIds.has(scenario.id)"
 | 
			
		||||
            :selectable="
 | 
			
		||||
              hoveredCard === scenario.id || bulkSelectedIds.size > 0
 | 
			
		||||
            "
 | 
			
		||||
            @select="handleRuleSelect"
 | 
			
		||||
            @delete="deleteScenario(scenario.id)"
 | 
			
		||||
            @update="updateScenario"
 | 
			
		||||
            @hover="isHovered => handleRuleHover(isHovered, scenario.id)"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </SettingsPageLayout>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -41,7 +41,7 @@ const controlItems = computed(() => {
 | 
			
		||||
      description: t(
 | 
			
		||||
        'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
 | 
			
		||||
      ),
 | 
			
		||||
      // routeName: 'captain_assistants_scenarios_index',
 | 
			
		||||
      routeName: 'captain_assistants_scenarios_index',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: t(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import AssistantEdit from './assistants/Edit.vue';
 | 
			
		||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
 | 
			
		||||
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
 | 
			
		||||
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
 | 
			
		||||
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
 | 
			
		||||
import DocumentsIndex from './documents/Index.vue';
 | 
			
		||||
import ResponsesIndex from './responses/Index.vue';
 | 
			
		||||
 | 
			
		||||
@@ -67,6 +68,21 @@ export const routes = [
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: frontendURL(
 | 
			
		||||
      'accounts/:accountId/captain/assistants/:assistantId/scenarios'
 | 
			
		||||
    ),
 | 
			
		||||
    component: AssistantScenariosIndex,
 | 
			
		||||
    name: 'captain_assistants_scenarios_index',
 | 
			
		||||
    meta: {
 | 
			
		||||
      permissions: ['administrator', 'agent'],
 | 
			
		||||
      featureFlag: FEATURE_FLAGS.CAPTAIN,
 | 
			
		||||
      installationTypes: [
 | 
			
		||||
        INSTALLATION_TYPES.CLOUD,
 | 
			
		||||
        INSTALLATION_TYPES.ENTERPRISE,
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: frontendURL(
 | 
			
		||||
      'accounts/:accountId/captain/assistants/:assistantId/guidelines'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								app/javascript/dashboard/store/captain/scenarios.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/javascript/dashboard/store/captain/scenarios.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import CaptainScenarios from 'dashboard/api/captain/scenarios';
 | 
			
		||||
import { createStore } from './storeFactory';
 | 
			
		||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
 | 
			
		||||
 | 
			
		||||
export default createStore({
 | 
			
		||||
  name: 'CaptainScenario',
 | 
			
		||||
  API: CaptainScenarios,
 | 
			
		||||
  actions: mutations => ({
 | 
			
		||||
    update: async ({ commit }, { id, assistantId, ...updateObj }) => {
 | 
			
		||||
      commit(mutations.SET_UI_FLAG, { updatingItem: true });
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await CaptainScenarios.update(
 | 
			
		||||
          { id, assistantId },
 | 
			
		||||
          updateObj
 | 
			
		||||
        );
 | 
			
		||||
        commit(mutations.EDIT, response.data);
 | 
			
		||||
        commit(mutations.SET_UI_FLAG, { updatingItem: false });
 | 
			
		||||
        return response.data;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        commit(mutations.SET_UI_FLAG, { updatingItem: false });
 | 
			
		||||
        return throwErrorMessage(error);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    delete: async ({ commit }, { id, assistantId }) => {
 | 
			
		||||
      commit(mutations.SET_UI_FLAG, { deletingItem: true });
 | 
			
		||||
      try {
 | 
			
		||||
        await CaptainScenarios.delete({ id, assistantId });
 | 
			
		||||
        commit(mutations.DELETE, id);
 | 
			
		||||
        commit(mutations.SET_UI_FLAG, { deletingItem: false });
 | 
			
		||||
        return id;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        commit(mutations.SET_UI_FLAG, { deletingItem: false });
 | 
			
		||||
        return throwErrorMessage(error);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										24
									
								
								app/javascript/dashboard/store/captain/tools.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/javascript/dashboard/store/captain/tools.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import { createStore } from './storeFactory';
 | 
			
		||||
import CaptainToolsAPI from '../../api/captain/tools';
 | 
			
		||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
 | 
			
		||||
 | 
			
		||||
const toolsStore = createStore({
 | 
			
		||||
  name: 'captainTool',
 | 
			
		||||
  API: CaptainToolsAPI,
 | 
			
		||||
  actions: mutations => ({
 | 
			
		||||
    getTools: async ({ commit }) => {
 | 
			
		||||
      commit(mutations.SET_UI_FLAG, { fetchingList: true });
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await CaptainToolsAPI.get();
 | 
			
		||||
        commit(mutations.SET, response.data);
 | 
			
		||||
        commit(mutations.SET_UI_FLAG, { fetchingList: false });
 | 
			
		||||
        return response.data;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        commit(mutations.SET_UI_FLAG, { fetchingList: false });
 | 
			
		||||
        return throwErrorMessage(error);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default toolsStore;
 | 
			
		||||
@@ -53,6 +53,8 @@ import captainInboxes from './captain/inboxes';
 | 
			
		||||
import captainBulkActions from './captain/bulkActions';
 | 
			
		||||
import copilotThreads from './captain/copilotThreads';
 | 
			
		||||
import copilotMessages from './captain/copilotMessages';
 | 
			
		||||
import captainScenarios from './captain/scenarios';
 | 
			
		||||
import captainTools from './captain/tools';
 | 
			
		||||
 | 
			
		||||
const plugins = [];
 | 
			
		||||
 | 
			
		||||
@@ -111,6 +113,8 @@ export default createStore({
 | 
			
		||||
    captainBulkActions,
 | 
			
		||||
    copilotThreads,
 | 
			
		||||
    copilotMessages,
 | 
			
		||||
    captainScenarios,
 | 
			
		||||
    captainTools,
 | 
			
		||||
  },
 | 
			
		||||
  plugins,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,10 @@
 | 
			
		||||
json.data do
 | 
			
		||||
json.payload do
 | 
			
		||||
  json.array! @scenarios do |scenario|
 | 
			
		||||
    json.partial! 'api/v1/models/captain/scenario', scenario: scenario
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
json.meta do
 | 
			
		||||
  json.total_count @scenarios.count
 | 
			
		||||
  json.page 1
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@breezystack/lamejs": "^1.2.7",
 | 
			
		||||
    "@chatwoot/ninja-keys": "1.2.3",
 | 
			
		||||
    "@chatwoot/prosemirror-schema": "1.1.6-next",
 | 
			
		||||
    "@chatwoot/prosemirror-schema": "1.2.1",
 | 
			
		||||
    "@chatwoot/utils": "^0.0.48",
 | 
			
		||||
    "@formkit/core": "^1.6.7",
 | 
			
		||||
    "@formkit/vue": "^1.6.7",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -20,8 +20,8 @@ importers:
 | 
			
		||||
        specifier: 1.2.3
 | 
			
		||||
        version: 1.2.3
 | 
			
		||||
      '@chatwoot/prosemirror-schema':
 | 
			
		||||
        specifier: 1.1.6-next
 | 
			
		||||
        version: 1.1.6-next
 | 
			
		||||
        specifier: 1.2.1
 | 
			
		||||
        version: 1.2.1
 | 
			
		||||
      '@chatwoot/utils':
 | 
			
		||||
        specifier: ^0.0.48
 | 
			
		||||
        version: 0.0.48
 | 
			
		||||
@@ -403,8 +403,8 @@ packages:
 | 
			
		||||
  '@chatwoot/ninja-keys@1.2.3':
 | 
			
		||||
    resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
 | 
			
		||||
 | 
			
		||||
  '@chatwoot/prosemirror-schema@1.1.6-next':
 | 
			
		||||
    resolution: {integrity: sha512-9lf7FrcED/B5oyGrMmIkbegkhlC/P0NrtXoX8k94YWRosZcx0hGVGhpTud+0Mhm7saAfGerKIwTRVDmmnxPuCA==}
 | 
			
		||||
  '@chatwoot/prosemirror-schema@1.2.1':
 | 
			
		||||
    resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==}
 | 
			
		||||
 | 
			
		||||
  '@chatwoot/utils@0.0.48':
 | 
			
		||||
    resolution: {integrity: sha512-67M2lvpBp0Ciczv1uRzabOXSCGiEeJE3wYVoPAxkqI35CJSkotu4tSX2TFOwagUQoRyU6F8YV3xXGfCpDN9WAA==}
 | 
			
		||||
@@ -5237,7 +5237,7 @@ snapshots:
 | 
			
		||||
      hotkeys-js: 3.8.7
 | 
			
		||||
      lit: 2.2.6
 | 
			
		||||
 | 
			
		||||
  '@chatwoot/prosemirror-schema@1.1.6-next':
 | 
			
		||||
  '@chatwoot/prosemirror-schema@1.2.1':
 | 
			
		||||
    dependencies:
 | 
			
		||||
      markdown-it-sup: 2.0.0
 | 
			
		||||
      prosemirror-commands: 1.6.0
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
 | 
			
		||||
            as: :json
 | 
			
		||||
 | 
			
		||||
        expect(response).to have_http_status(:success)
 | 
			
		||||
        expect(json_response[:data].length).to eq(3)
 | 
			
		||||
        expect(json_response[:payload].length).to eq(3)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
 | 
			
		||||
            as: :json
 | 
			
		||||
 | 
			
		||||
        expect(response).to have_http_status(:success)
 | 
			
		||||
        expect(json_response[:data].length).to eq(5)
 | 
			
		||||
        expect(json_response[:payload].length).to eq(5)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns only enabled scenarios' do
 | 
			
		||||
@@ -49,8 +49,8 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
 | 
			
		||||
            as: :json
 | 
			
		||||
 | 
			
		||||
        expect(response).to have_http_status(:success)
 | 
			
		||||
        expect(json_response[:data].length).to eq(1)
 | 
			
		||||
        expect(json_response[:data].first[:enabled]).to be(true)
 | 
			
		||||
        expect(json_response[:payload].length).to eq(1)
 | 
			
		||||
        expect(json_response[:payload].first[:enabled]).to be(true)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user