feat: New Scenarios page (#11975)
This commit is contained in:
parent
1230d1f251
commit
df4de508e7
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 },
|
enableVariables: { type: Boolean, default: false },
|
||||||
enableCannedResponses: { type: Boolean, default: true },
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
|
enableCaptainTools: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
@ -98,6 +99,7 @@ watch(
|
|||||||
:enable-variables="enableVariables"
|
:enable-variables="enableVariables"
|
||||||
:enable-canned-responses="enableCannedResponses"
|
:enable-canned-responses="enableCannedResponses"
|
||||||
:enabled-menu-options="enabledMenuOptions"
|
:enabled-menu-options="enabledMenuOptions"
|
||||||
|
:enable-captain-tools="enableCaptainTools"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useToggle } from '@vueuse/core';
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||||
@ -44,7 +45,10 @@ const onClickCancel = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="inline-flex relative">
|
<div
|
||||||
|
v-on-click-outside="() => togglePopover(false)"
|
||||||
|
class="inline-flex relative"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
:label="buttonLabel"
|
:label="buttonLabel"
|
||||||
sm
|
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-if="isEditing"
|
||||||
v-model="editedContent"
|
v-model="editedContent"
|
||||||
focus-on-mount
|
focus-on-mount
|
||||||
custom-input-class="flex items-center gap-2 text-sm text-n-slate-12"
|
|
||||||
@keyup.enter="saveEdit"
|
@keyup.enter="saveEdit"
|
||||||
/>
|
/>
|
||||||
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
<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 KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
|
||||||
import TagAgents from '../conversation/TagAgents.vue';
|
import TagAgents from '../conversation/TagAgents.vue';
|
||||||
import VariableList from '../conversation/VariableList.vue';
|
import VariableList from '../conversation/VariableList.vue';
|
||||||
|
import TagTools from '../conversation/TagTools.vue';
|
||||||
|
|
||||||
import { useEmitter } from 'dashboard/composables/emitter';
|
import { useEmitter } from 'dashboard/composables/emitter';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@ -72,6 +73,7 @@ const props = defineProps({
|
|||||||
updateSelectionWith: { type: String, default: '' },
|
updateSelectionWith: { type: String, default: '' },
|
||||||
enableVariables: { type: Boolean, default: false },
|
enableVariables: { type: Boolean, default: false },
|
||||||
enableCannedResponses: { type: Boolean, default: true },
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
|
enableCaptainTools: { type: Boolean, default: false },
|
||||||
variables: { type: Object, default: () => ({}) },
|
variables: { type: Object, default: () => ({}) },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
signature: { type: String, default: '' },
|
signature: { type: String, default: '' },
|
||||||
@ -89,6 +91,7 @@ const emit = defineEmits([
|
|||||||
'toggleUserMention',
|
'toggleUserMention',
|
||||||
'toggleCannedMenu',
|
'toggleCannedMenu',
|
||||||
'toggleVariablesMenu',
|
'toggleVariablesMenu',
|
||||||
|
'toggleToolsMenu',
|
||||||
'clearSelection',
|
'clearSelection',
|
||||||
'blur',
|
'blur',
|
||||||
'focus',
|
'focus',
|
||||||
@ -140,7 +143,9 @@ const showUserMentions = ref(false);
|
|||||||
const showCannedMenu = ref(false);
|
const showCannedMenu = ref(false);
|
||||||
const showVariables = ref(false);
|
const showVariables = ref(false);
|
||||||
const showEmojiMenu = ref(false);
|
const showEmojiMenu = ref(false);
|
||||||
|
const showToolsMenu = ref(false);
|
||||||
const mentionSearchKey = ref('');
|
const mentionSearchKey = ref('');
|
||||||
|
const toolSearchKey = ref('');
|
||||||
const cannedSearchTerm = ref('');
|
const cannedSearchTerm = ref('');
|
||||||
const variableSearchTerm = ref('');
|
const variableSearchTerm = ref('');
|
||||||
const emojiSearchTerm = ref('');
|
const emojiSearchTerm = ref('');
|
||||||
@ -216,11 +221,17 @@ const plugins = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
createSuggestionPlugin({
|
||||||
|
trigger: '@',
|
||||||
|
showMenu: showToolsMenu,
|
||||||
|
searchTerm: toolSearchKey,
|
||||||
|
isAllowed: () => props.enableCaptainTools,
|
||||||
|
}),
|
||||||
createSuggestionPlugin({
|
createSuggestionPlugin({
|
||||||
trigger: '@',
|
trigger: '@',
|
||||||
showMenu: showUserMentions,
|
showMenu: showUserMentions,
|
||||||
searchTerm: mentionSearchKey,
|
searchTerm: mentionSearchKey,
|
||||||
isAllowed: () => props.isPrivate,
|
isAllowed: () => props.isPrivate || !props.enableCaptainTools,
|
||||||
}),
|
}),
|
||||||
createSuggestionPlugin({
|
createSuggestionPlugin({
|
||||||
trigger: '/',
|
trigger: '/',
|
||||||
@ -262,6 +273,9 @@ watch(showCannedMenu, updatedValue => {
|
|||||||
watch(showVariables, updatedValue => {
|
watch(showVariables, updatedValue => {
|
||||||
emit('toggleVariablesMenu', !props.isPrivate && updatedValue);
|
emit('toggleVariablesMenu', !props.isPrivate && updatedValue);
|
||||||
});
|
});
|
||||||
|
watch(showToolsMenu, updatedValue => {
|
||||||
|
emit('toggleToolsMenu', props.enableCaptainTools && updatedValue);
|
||||||
|
});
|
||||||
|
|
||||||
function focusEditorInputField(pos = 'end') {
|
function focusEditorInputField(pos = 'end') {
|
||||||
const { tr } = editorView.state;
|
const { tr } = editorView.state;
|
||||||
@ -538,6 +552,7 @@ function insertSpecialContent(type, content) {
|
|||||||
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
|
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
|
||||||
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
|
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
|
||||||
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
|
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
|
||||||
|
tool: CONVERSATION_EVENTS.INSERTED_A_TOOL,
|
||||||
};
|
};
|
||||||
|
|
||||||
useTrack(event_map[type]);
|
useTrack(event_map[type]);
|
||||||
@ -699,6 +714,11 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
|||||||
:search-key="emojiSearchTerm"
|
:search-key="emojiSearchTerm"
|
||||||
@select-emoji="emoji => insertSpecialContent('emoji', emoji)"
|
@select-emoji="emoji => insertSpecialContent('emoji', emoji)"
|
||||||
/>
|
/>
|
||||||
|
<TagTools
|
||||||
|
v-if="showToolsMenu"
|
||||||
|
:search-key="toolSearchKey"
|
||||||
|
@select-tool="content => insertSpecialContent('tool', content)"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
ref="imageUpload"
|
ref="imageUpload"
|
||||||
type="file"
|
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 {
|
.editor-wrap {
|
||||||
@apply mb-4;
|
@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',
|
TRANSLATE_A_MESSAGE: 'Translated a message',
|
||||||
INSERTED_A_VARIABLE: 'Inserted a variable',
|
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||||
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
||||||
|
INSERTED_A_TOOL: 'Inserted a tool',
|
||||||
USED_MENTIONS: 'Used mentions',
|
USED_MENTIONS: 'Used mentions',
|
||||||
SEARCH_CONVERSATION: 'Searched conversations',
|
SEARCH_CONVERSATION: 'Searched conversations',
|
||||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
APPLY_FILTER: 'Applied filters in the conversation list',
|
||||||
|
|||||||
@ -319,6 +319,12 @@ const createNode = (editorView, nodeType, content) => {
|
|||||||
return state.schema.text(`{{${content}}}`);
|
return state.schema.text(`{{${content}}}`);
|
||||||
case 'emoji':
|
case 'emoji':
|
||||||
return state.schema.text(content);
|
return state.schema.text(content);
|
||||||
|
case 'tool': {
|
||||||
|
return state.schema.nodes.tools.create({
|
||||||
|
id: content.id,
|
||||||
|
name: content.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -355,6 +361,11 @@ const nodeCreators = {
|
|||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
}),
|
}),
|
||||||
|
tool: (editorView, content, from, to) => ({
|
||||||
|
node: createNode(editorView, 'tool', content),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -554,6 +554,7 @@
|
|||||||
"SEARCH_PLACEHOLDER": "Search..."
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
},
|
},
|
||||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||||
"API": {
|
"API": {
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"SUCCESS": "Guardrails added successfully",
|
"SUCCESS": "Guardrails added successfully",
|
||||||
@ -601,6 +602,7 @@
|
|||||||
"SEARCH_PLACEHOLDER": "Search..."
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
},
|
},
|
||||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||||
"API": {
|
"API": {
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"SUCCESS": "Response Guidelines added successfully",
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
@ -615,6 +617,73 @@
|
|||||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
"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": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@ -266,6 +266,11 @@ const addAllExample = () => {
|
|||||||
{{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.EMPTY_MESSAGE') }}
|
{{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.EMPTY_MESSAGE') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div v-else class="flex flex-col gap-2">
|
||||||
<RuleCard
|
<RuleCard
|
||||||
v-for="guardrail in filteredGuardrails"
|
v-for="guardrail in filteredGuardrails"
|
||||||
|
|||||||
@ -284,6 +284,13 @@ const addAllExample = async () => {
|
|||||||
{{ t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.EMPTY_MESSAGE') }}
|
{{ t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.EMPTY_MESSAGE') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div v-else class="flex flex-col gap-2">
|
||||||
<RuleCard
|
<RuleCard
|
||||||
v-for="guideline in filteredGuidelines"
|
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(
|
description: t(
|
||||||
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
|
||||||
),
|
),
|
||||||
// routeName: 'captain_assistants_scenarios_index',
|
routeName: 'captain_assistants_scenarios_index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t(
|
name: t(
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import AssistantEdit from './assistants/Edit.vue';
|
|||||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
||||||
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
||||||
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
|
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
|
||||||
|
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
|
||||||
import DocumentsIndex from './documents/Index.vue';
|
import DocumentsIndex from './documents/Index.vue';
|
||||||
import ResponsesIndex from './responses/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(
|
path: frontendURL(
|
||||||
'accounts/:accountId/captain/assistants/:assistantId/guidelines'
|
'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 captainBulkActions from './captain/bulkActions';
|
||||||
import copilotThreads from './captain/copilotThreads';
|
import copilotThreads from './captain/copilotThreads';
|
||||||
import copilotMessages from './captain/copilotMessages';
|
import copilotMessages from './captain/copilotMessages';
|
||||||
|
import captainScenarios from './captain/scenarios';
|
||||||
|
import captainTools from './captain/tools';
|
||||||
|
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
|
|
||||||
@ -111,6 +113,8 @@ export default createStore({
|
|||||||
captainBulkActions,
|
captainBulkActions,
|
||||||
copilotThreads,
|
copilotThreads,
|
||||||
copilotMessages,
|
copilotMessages,
|
||||||
|
captainScenarios,
|
||||||
|
captainTools,
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
json.data do
|
json.payload do
|
||||||
json.array! @scenarios do |scenario|
|
json.array! @scenarios do |scenario|
|
||||||
json.partial! 'api/v1/models/captain/scenario', scenario: scenario
|
json.partial! 'api/v1/models/captain/scenario', scenario: scenario
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json.meta do
|
||||||
|
json.total_count @scenarios.count
|
||||||
|
json.page 1
|
||||||
|
end
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breezystack/lamejs": "^1.2.7",
|
"@breezystack/lamejs": "^1.2.7",
|
||||||
"@chatwoot/ninja-keys": "1.2.3",
|
"@chatwoot/ninja-keys": "1.2.3",
|
||||||
"@chatwoot/prosemirror-schema": "1.1.6-next",
|
"@chatwoot/prosemirror-schema": "1.2.1",
|
||||||
"@chatwoot/utils": "^0.0.48",
|
"@chatwoot/utils": "^0.0.48",
|
||||||
"@formkit/core": "^1.6.7",
|
"@formkit/core": "^1.6.7",
|
||||||
"@formkit/vue": "^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
|
specifier: 1.2.3
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
'@chatwoot/prosemirror-schema':
|
'@chatwoot/prosemirror-schema':
|
||||||
specifier: 1.1.6-next
|
specifier: 1.2.1
|
||||||
version: 1.1.6-next
|
version: 1.2.1
|
||||||
'@chatwoot/utils':
|
'@chatwoot/utils':
|
||||||
specifier: ^0.0.48
|
specifier: ^0.0.48
|
||||||
version: 0.0.48
|
version: 0.0.48
|
||||||
@ -403,8 +403,8 @@ packages:
|
|||||||
'@chatwoot/ninja-keys@1.2.3':
|
'@chatwoot/ninja-keys@1.2.3':
|
||||||
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
|
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
|
||||||
|
|
||||||
'@chatwoot/prosemirror-schema@1.1.6-next':
|
'@chatwoot/prosemirror-schema@1.2.1':
|
||||||
resolution: {integrity: sha512-9lf7FrcED/B5oyGrMmIkbegkhlC/P0NrtXoX8k94YWRosZcx0hGVGhpTud+0Mhm7saAfGerKIwTRVDmmnxPuCA==}
|
resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==}
|
||||||
|
|
||||||
'@chatwoot/utils@0.0.48':
|
'@chatwoot/utils@0.0.48':
|
||||||
resolution: {integrity: sha512-67M2lvpBp0Ciczv1uRzabOXSCGiEeJE3wYVoPAxkqI35CJSkotu4tSX2TFOwagUQoRyU6F8YV3xXGfCpDN9WAA==}
|
resolution: {integrity: sha512-67M2lvpBp0Ciczv1uRzabOXSCGiEeJE3wYVoPAxkqI35CJSkotu4tSX2TFOwagUQoRyU6F8YV3xXGfCpDN9WAA==}
|
||||||
@ -5237,7 +5237,7 @@ snapshots:
|
|||||||
hotkeys-js: 3.8.7
|
hotkeys-js: 3.8.7
|
||||||
lit: 2.2.6
|
lit: 2.2.6
|
||||||
|
|
||||||
'@chatwoot/prosemirror-schema@1.1.6-next':
|
'@chatwoot/prosemirror-schema@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it-sup: 2.0.0
|
markdown-it-sup: 2.0.0
|
||||||
prosemirror-commands: 1.6.0
|
prosemirror-commands: 1.6.0
|
||||||
|
|||||||
@ -26,7 +26,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
|
|||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
|
|||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(json_response[:data].length).to eq(5)
|
expect(json_response[:payload].length).to eq(5)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns only enabled scenarios' do
|
it 'returns only enabled scenarios' do
|
||||||
@ -49,8 +49,8 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
|
|||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(json_response[:data].length).to eq(1)
|
expect(json_response[:payload].length).to eq(1)
|
||||||
expect(json_response[:data].first[:enabled]).to be(true)
|
expect(json_response[:payload].first[:enabled]).to be(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user