feat: Workflows v1
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainWorkflows extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/assistants', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ assistantId, page = 1 } = {}) {
|
||||
return axios.get(`${this.url}/${assistantId}/workflows`, {
|
||||
params: { page },
|
||||
});
|
||||
}
|
||||
|
||||
show({ assistantId, id }) {
|
||||
return axios.get(`${this.url}/${assistantId}/workflows/${id}`);
|
||||
}
|
||||
|
||||
create({ assistantId, ...data } = {}) {
|
||||
return axios.post(`${this.url}/${assistantId}/workflows`, {
|
||||
workflow: data,
|
||||
});
|
||||
}
|
||||
|
||||
update({ assistantId, id }, data = {}) {
|
||||
return axios.put(`${this.url}/${assistantId}/workflows/${id}`, {
|
||||
workflow: data,
|
||||
});
|
||||
}
|
||||
|
||||
delete({ assistantId, id }) {
|
||||
return axios.delete(`${this.url}/${assistantId}/workflows/${id}`);
|
||||
}
|
||||
|
||||
executions({ assistantId, id }) {
|
||||
return axios.get(`${this.url}/${assistantId}/workflows/${id}/executions`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainWorkflows();
|
||||
@@ -0,0 +1,159 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Textarea from 'dashboard/components-next/textarea/Textarea.vue';
|
||||
|
||||
const props = defineProps({
|
||||
node: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update', 'delete', 'close']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const nodeData = ref({ ...props.node.data });
|
||||
|
||||
watch(
|
||||
() => props.node,
|
||||
val => {
|
||||
nodeData.value = { ...val.data };
|
||||
}
|
||||
);
|
||||
|
||||
const FIELD_CONFIGS = {
|
||||
send_message: [
|
||||
{
|
||||
key: 'message',
|
||||
component: 'textarea',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.MESSAGE',
|
||||
},
|
||||
],
|
||||
add_label: [
|
||||
{
|
||||
key: 'label',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.LABEL',
|
||||
},
|
||||
],
|
||||
assign_agent: [
|
||||
{
|
||||
key: 'agent_id',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.AGENT_ID',
|
||||
},
|
||||
],
|
||||
assign_team: [
|
||||
{
|
||||
key: 'team_id',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.TEAM_ID',
|
||||
},
|
||||
],
|
||||
add_private_note: [
|
||||
{
|
||||
key: 'message',
|
||||
component: 'textarea',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.MESSAGE',
|
||||
},
|
||||
],
|
||||
update_priority: [
|
||||
{
|
||||
key: 'priority',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.PRIORITY',
|
||||
},
|
||||
],
|
||||
condition: [
|
||||
{
|
||||
key: 'attribute',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.ATTRIBUTE',
|
||||
},
|
||||
{
|
||||
key: 'operator',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.OPERATOR',
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.VALUE',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fields = ref(FIELD_CONFIGS[props.node.type] || []);
|
||||
|
||||
watch(
|
||||
() => props.node.type,
|
||||
type => {
|
||||
fields.value = FIELD_CONFIGS[type] || [];
|
||||
}
|
||||
);
|
||||
|
||||
const applyChanges = () => {
|
||||
emit('update', props.node.id, nodeData.value);
|
||||
};
|
||||
|
||||
const deleteNode = () => {
|
||||
emit('delete', props.node.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-72 shrink-0 border-l border-n-weak bg-n-solid-2 overflow-y-auto flex flex-col"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center px-4 py-3 border-b border-n-weak"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.TITLE') }}
|
||||
</h3>
|
||||
<Button icon="i-lucide-x" xs ghost slate @click="emit('close')" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 p-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-n-slate-11">
|
||||
{{ t('CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.LABEL_FIELD') }}
|
||||
</label>
|
||||
<Input v-model="nodeData.label" @change="applyChanges" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
class="flex flex-col gap-1.5"
|
||||
>
|
||||
<label class="text-xs font-medium text-n-slate-11">
|
||||
{{ t(field.label) }}
|
||||
</label>
|
||||
<Textarea
|
||||
v-if="field.component === 'textarea'"
|
||||
v-model="nodeData[field.key]"
|
||||
rows="3"
|
||||
@change="applyChanges"
|
||||
/>
|
||||
<Input v-else v-model="nodeData[field.key]" @change="applyChanges" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex items-center gap-2 p-4 border-t border-n-weak">
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.APPLY')"
|
||||
sm
|
||||
@click="applyChanges"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.DELETE')"
|
||||
icon="i-lucide-trash-2"
|
||||
sm
|
||||
ghost
|
||||
slate
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const NODE_CATEGORIES = [
|
||||
{
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.PALETTE.TRIGGERS',
|
||||
color: 'n-teal',
|
||||
icon: 'i-lucide-zap',
|
||||
nodes: [
|
||||
{
|
||||
type: 'trigger_conversation_created',
|
||||
label:
|
||||
'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.TRIGGER_CONVERSATION_CREATED',
|
||||
},
|
||||
{
|
||||
type: 'trigger_message_created',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.TRIGGER_MESSAGE_CREATED',
|
||||
},
|
||||
{
|
||||
type: 'trigger_conversation_resolved',
|
||||
label:
|
||||
'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.TRIGGER_CONVERSATION_RESOLVED',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.PALETTE.LOGIC',
|
||||
color: 'n-amber',
|
||||
icon: 'i-lucide-git-branch',
|
||||
nodes: [
|
||||
{
|
||||
type: 'condition',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.CONDITION',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.PALETTE.ACTIONS',
|
||||
color: 'n-blue',
|
||||
icon: 'i-lucide-play',
|
||||
nodes: [
|
||||
{
|
||||
type: 'send_message',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.SEND_MESSAGE',
|
||||
},
|
||||
{
|
||||
type: 'add_label',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.ADD_LABEL',
|
||||
},
|
||||
{
|
||||
type: 'assign_agent',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.ASSIGN_AGENT',
|
||||
},
|
||||
{
|
||||
type: 'assign_team',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.ASSIGN_TEAM',
|
||||
},
|
||||
{
|
||||
type: 'resolve_conversation',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.RESOLVE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
type: 'add_private_note',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.ADD_PRIVATE_NOTE',
|
||||
},
|
||||
{
|
||||
type: 'update_priority',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.UPDATE_PRIORITY',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.PALETTE.SHOPIFY',
|
||||
color: 'n-teal',
|
||||
icon: 'i-lucide-shopping-bag',
|
||||
nodes: [
|
||||
{
|
||||
type: 'shopify_search_customer',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.SHOPIFY_SEARCH_CUSTOMER',
|
||||
},
|
||||
{
|
||||
type: 'shopify_get_customer_orders',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.SHOPIFY_GET_CUSTOMER_ORDERS',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const onDragStart = (event, type) => {
|
||||
event.dataTransfer.setData('application/captainnode', type);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-60 shrink-0 border-r border-n-weak bg-n-solid-2 overflow-y-auto"
|
||||
>
|
||||
<div class="p-3 flex flex-col gap-4">
|
||||
<div v-for="category in NODE_CATEGORIES" :key="category.label">
|
||||
<div class="flex items-center gap-1.5 mb-2 px-1">
|
||||
<span class="size-3 text-n-slate-10" :class="[category.icon]" />
|
||||
<h4 class="text-[11px] font-semibold text-n-slate-10 uppercase">
|
||||
{{ t(category.label) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div
|
||||
v-for="node in category.nodes"
|
||||
:key="node.type"
|
||||
class="flex items-center gap-2 px-2.5 py-2 text-xs text-n-slate-12 rounded-lg cursor-grab bg-n-solid-2 hover:bg-n-alpha-2 outline outline-1 -outline-offset-1 outline-n-container transition-colors"
|
||||
draggable="true"
|
||||
@dragstart="e => onDragStart(e, node.type)"
|
||||
>
|
||||
{{ t(node.label) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||
import SelectMenu from 'dashboard/components-next/selectmenu/SelectMenu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
workflow: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['save', 'back']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
const triggerEvent = ref('conversation_created');
|
||||
const enabled = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.workflow,
|
||||
val => {
|
||||
if (val && val.id) {
|
||||
name.value = val.name || '';
|
||||
description.value = val.description || '';
|
||||
triggerEvent.value = val.trigger_event || 'conversation_created';
|
||||
enabled.value = val.enabled || false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const TRIGGER_OPTIONS = [
|
||||
{
|
||||
value: 'conversation_created',
|
||||
label: t('CAPTAIN.ASSISTANTS.WORKFLOWS.TRIGGERS.CONVERSATION_CREATED'),
|
||||
},
|
||||
{
|
||||
value: 'message_created',
|
||||
label: t('CAPTAIN.ASSISTANTS.WORKFLOWS.TRIGGERS.MESSAGE_CREATED'),
|
||||
},
|
||||
{
|
||||
value: 'conversation_resolved',
|
||||
label: t('CAPTAIN.ASSISTANTS.WORKFLOWS.TRIGGERS.CONVERSATION_RESOLVED'),
|
||||
},
|
||||
];
|
||||
|
||||
const triggerLabel = () => {
|
||||
const option = TRIGGER_OPTIONS.find(o => o.value === triggerEvent.value);
|
||||
return option ? option.label : triggerEvent.value;
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
emit('save', {
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
trigger_event: triggerEvent.value,
|
||||
enabled: enabled.value,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-3 px-4 py-2.5 border-b border-n-weak bg-n-solid-2"
|
||||
>
|
||||
<Button icon="i-lucide-arrow-left" xs ghost slate @click="emit('back')" />
|
||||
<span class="w-px h-4 bg-n-weak" />
|
||||
<Input
|
||||
v-model="name"
|
||||
class="max-w-xs"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.WORKFLOWS.TOOLBAR.NAME_PLACEHOLDER')"
|
||||
/>
|
||||
<SelectMenu
|
||||
v-model="triggerEvent"
|
||||
:options="TRIGGER_OPTIONS"
|
||||
:label="triggerLabel()"
|
||||
/>
|
||||
<div class="flex items-center gap-2.5 ml-auto">
|
||||
<span class="text-xs text-n-slate-11">
|
||||
{{ t('CAPTAIN.ASSISTANTS.WORKFLOWS.TOOLBAR.ENABLED') }}
|
||||
</span>
|
||||
<Switch v-model="enabled" />
|
||||
<span class="w-px h-4 bg-n-weak" />
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.WORKFLOWS.TOOLBAR.SAVE')"
|
||||
sm
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
defineProps({
|
||||
data: { type: Object, default: () => ({}) },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-52 rounded-xl bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm overflow-hidden"
|
||||
>
|
||||
<Handle type="target" :position="Position.Top" class="!bg-n-blue-9" />
|
||||
<div class="w-1 shrink-0 bg-n-blue-9" />
|
||||
<div class="flex flex-col gap-1 px-3 py-2.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-lucide-play text-n-blue-11 size-3.5 shrink-0" />
|
||||
<span class="text-xs font-medium text-n-slate-12 truncate">
|
||||
{{ data.label || 'Action' }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="data.description"
|
||||
class="text-[11px] text-n-slate-10 leading-tight truncate"
|
||||
>
|
||||
{{ data.description }}
|
||||
</span>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Bottom" class="!bg-n-blue-9" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
defineProps({
|
||||
data: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-52 rounded-xl bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm overflow-hidden"
|
||||
>
|
||||
<Handle type="target" :position="Position.Top" class="!bg-n-amber-9" />
|
||||
<div class="w-1 shrink-0 bg-n-amber-9" />
|
||||
<div class="flex flex-col gap-1 px-3 py-2.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-lucide-git-branch text-n-amber-11 size-3.5 shrink-0" />
|
||||
<span class="text-xs font-medium text-n-slate-12 truncate">
|
||||
{{ data.label || 'Condition' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<span class="text-[11px] font-medium text-n-teal-11">
|
||||
{{ t('CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.CONDITION_TRUE') }}
|
||||
</span>
|
||||
<span class="text-[11px] font-medium text-n-ruby-11">
|
||||
{{ t('CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.CONDITION_FALSE') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
id="true"
|
||||
type="source"
|
||||
:position="Position.Bottom"
|
||||
class="!left-[25%] !bg-n-teal-9"
|
||||
/>
|
||||
<Handle
|
||||
id="false"
|
||||
type="source"
|
||||
:position="Position.Bottom"
|
||||
class="!left-[75%] !bg-n-ruby-9"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
defineProps({
|
||||
data: { type: Object, default: () => ({}) },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-52 rounded-xl bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm overflow-hidden"
|
||||
>
|
||||
<Handle type="target" :position="Position.Top" class="!bg-n-teal-9" />
|
||||
<div class="w-1 shrink-0 bg-n-teal-9" />
|
||||
<div class="flex flex-col gap-1 px-3 py-2.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-lucide-shopping-bag text-n-teal-11 size-3.5 shrink-0" />
|
||||
<span class="text-xs font-medium text-n-slate-12 truncate">
|
||||
{{ data.label || 'Shopify' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-n-slate-10 leading-tight">
|
||||
{{ data.description || 'Shopify integration' }}
|
||||
</span>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Bottom" class="!bg-n-teal-9" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
defineProps({
|
||||
data: { type: Object, default: () => ({}) },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-52 rounded-xl bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm overflow-hidden"
|
||||
>
|
||||
<div class="w-1 shrink-0 bg-n-teal-9" />
|
||||
<div class="flex flex-col gap-1 px-3 py-2.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-lucide-zap text-n-teal-11 size-3.5 shrink-0" />
|
||||
<span class="text-xs font-medium text-n-slate-12 truncate">
|
||||
{{ data.label || 'Trigger' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-n-slate-10 leading-tight">
|
||||
{{ data.event || 'Starts the workflow' }}
|
||||
</span>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Bottom" class="!bg-n-teal-9" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -348,6 +348,17 @@ const menuItems = computed(() => {
|
||||
navigationPath: 'captain_assistants_scenarios_index',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Workflows',
|
||||
label: t('SIDEBAR.CAPTAIN_WORKFLOWS'),
|
||||
activeOn: [
|
||||
'captain_assistants_workflows_index',
|
||||
'captain_assistants_workflow_editor',
|
||||
],
|
||||
to: accountScopedRoute('captain_assistants_index', {
|
||||
navigationPath: 'captain_assistants_workflows_index',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Playground',
|
||||
label: t('SIDEBAR.CAPTAIN_PLAYGROUND'),
|
||||
|
||||
@@ -732,6 +732,74 @@
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"WORKFLOWS": {
|
||||
"TITLE": "Workflows",
|
||||
"DESCRIPTION": "Automate actions triggered by conversation events with a visual workflow builder.",
|
||||
"ADD_BUTTON": "New Workflow",
|
||||
"DEFAULT_NAME": "New Workflow",
|
||||
"EMPTY_MESSAGE": "No workflows yet. Create one to automate conversation actions.",
|
||||
"TRIGGERS": {
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved"
|
||||
},
|
||||
"PALETTE": {
|
||||
"TRIGGERS": "Triggers",
|
||||
"LOGIC": "Logic",
|
||||
"ACTIONS": "Actions",
|
||||
"SHOPIFY": "Shopify"
|
||||
},
|
||||
"NODES": {
|
||||
"TRIGGER_CONVERSATION_CREATED": "Conversation Created",
|
||||
"TRIGGER_MESSAGE_CREATED": "Message Created",
|
||||
"TRIGGER_CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONDITION": "Condition",
|
||||
"SEND_MESSAGE": "Send Message",
|
||||
"ADD_LABEL": "Add Label",
|
||||
"ASSIGN_AGENT": "Assign Agent",
|
||||
"ASSIGN_TEAM": "Assign Team",
|
||||
"RESOLVE_CONVERSATION": "Resolve Conversation",
|
||||
"ADD_PRIVATE_NOTE": "Add Private Note",
|
||||
"UPDATE_PRIORITY": "Update Priority",
|
||||
"SHOPIFY_SEARCH_CUSTOMER": "Search Customer",
|
||||
"SHOPIFY_GET_CUSTOMER_ORDERS": "Get Customer Orders"
|
||||
},
|
||||
"CONFIG": {
|
||||
"TITLE": "Node Configuration",
|
||||
"LABEL_FIELD": "Label",
|
||||
"MESSAGE": "Message",
|
||||
"LABEL": "Label",
|
||||
"AGENT_ID": "Agent ID",
|
||||
"TEAM_ID": "Team ID",
|
||||
"PRIORITY": "Priority",
|
||||
"ATTRIBUTE": "Attribute",
|
||||
"OPERATOR": "Operator",
|
||||
"VALUE": "Value",
|
||||
"APPLY": "Apply",
|
||||
"DELETE": "Delete Node",
|
||||
"CONDITION_TRUE": "True",
|
||||
"CONDITION_FALSE": "False"
|
||||
},
|
||||
"TOOLBAR": {
|
||||
"NAME_PLACEHOLDER": "Workflow name",
|
||||
"ENABLED": "Enabled",
|
||||
"SAVE": "Save"
|
||||
},
|
||||
"API": {
|
||||
"CREATE": {
|
||||
"SUCCESS": "Workflow created successfully",
|
||||
"ERROR": "There was an error creating the workflow, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Workflow updated successfully",
|
||||
"ERROR": "There was an error updating the workflow, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Workflow deleted successfully",
|
||||
"ERROR": "There was an error deleting the workflow, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -316,6 +316,7 @@
|
||||
"CAPTAIN_RESPONSES": "FAQs",
|
||||
"CAPTAIN_TOOLS": "Tools",
|
||||
"CAPTAIN_SCENARIOS": "Scenarios",
|
||||
"CAPTAIN_WORKFLOWS": "Workflows",
|
||||
"CAPTAIN_PLAYGROUND": "Playground",
|
||||
"CAPTAIN_INBOXES": "Inboxes",
|
||||
"CAPTAIN_SETTINGS": "Settings",
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { VueFlow, useVueFlow } from '@vue-flow/core';
|
||||
|
||||
import WorkflowToolbar from 'dashboard/components-next/captain/workflows/WorkflowToolbar.vue';
|
||||
import NodePalette from 'dashboard/components-next/captain/workflows/NodePalette.vue';
|
||||
import NodeConfigPanel from 'dashboard/components-next/captain/workflows/NodeConfigPanel.vue';
|
||||
import TriggerNode from 'dashboard/components-next/captain/workflows/nodes/TriggerNode.vue';
|
||||
import ActionNode from 'dashboard/components-next/captain/workflows/nodes/ActionNode.vue';
|
||||
import ConditionNode from 'dashboard/components-next/captain/workflows/nodes/ConditionNode.vue';
|
||||
import ShopifyNode from 'dashboard/components-next/captain/workflows/nodes/ShopifyNode.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const assistantId = computed(() => Number(route.params.assistantId));
|
||||
const workflowId = computed(() => Number(route.params.workflowId));
|
||||
const workflow = computed(() =>
|
||||
store.getters['captainWorkflows/getRecord'](workflowId.value)
|
||||
);
|
||||
|
||||
const selectedNode = ref(null);
|
||||
|
||||
const { onConnect, addEdges, project } = useVueFlow();
|
||||
const nodes = ref([]);
|
||||
const edges = ref([]);
|
||||
|
||||
const nodeTypes = {
|
||||
trigger_conversation_created: TriggerNode,
|
||||
trigger_message_created: TriggerNode,
|
||||
trigger_conversation_resolved: TriggerNode,
|
||||
condition: ConditionNode,
|
||||
send_message: ActionNode,
|
||||
add_label: ActionNode,
|
||||
assign_agent: ActionNode,
|
||||
assign_team: ActionNode,
|
||||
resolve_conversation: ActionNode,
|
||||
add_private_note: ActionNode,
|
||||
update_priority: ActionNode,
|
||||
shopify_search_customer: ShopifyNode,
|
||||
shopify_get_customer_orders: ShopifyNode,
|
||||
};
|
||||
|
||||
watch(workflow, val => {
|
||||
if (val && val.id) {
|
||||
nodes.value = (val.nodes || []).map(n => ({
|
||||
...n,
|
||||
type: n.type,
|
||||
}));
|
||||
edges.value = val.edges || [];
|
||||
}
|
||||
});
|
||||
|
||||
onConnect(params => {
|
||||
addEdges([params]);
|
||||
});
|
||||
|
||||
const onDragOver = event => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
const onDrop = event => {
|
||||
const type = event.dataTransfer.getData('application/captainnode');
|
||||
if (!type) return;
|
||||
|
||||
const { left, top } = event.currentTarget.getBoundingClientRect();
|
||||
const position = project({
|
||||
x: event.clientX - left,
|
||||
y: event.clientY - top,
|
||||
});
|
||||
|
||||
const newNode = {
|
||||
id: `node_${Date.now()}`,
|
||||
type,
|
||||
position,
|
||||
data: { label: type.replace(/_/g, ' ') },
|
||||
};
|
||||
nodes.value = [...nodes.value, newNode];
|
||||
};
|
||||
|
||||
const onNodeClick = (_event, node) => {
|
||||
selectedNode.value = node;
|
||||
};
|
||||
|
||||
const onPaneClick = () => {
|
||||
selectedNode.value = null;
|
||||
};
|
||||
|
||||
const updateNodeData = (nodeId, data) => {
|
||||
nodes.value = nodes.value.map(n =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
|
||||
);
|
||||
if (selectedNode.value?.id === nodeId) {
|
||||
selectedNode.value = {
|
||||
...selectedNode.value,
|
||||
data: { ...selectedNode.value.data, ...data },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const deleteNode = nodeId => {
|
||||
nodes.value = nodes.value.filter(n => n.id !== nodeId);
|
||||
edges.value = edges.value.filter(
|
||||
e => e.source !== nodeId && e.target !== nodeId
|
||||
);
|
||||
if (selectedNode.value?.id === nodeId) {
|
||||
selectedNode.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const saveWorkflow = async ({ name, description, trigger_event, enabled }) => {
|
||||
try {
|
||||
await store.dispatch('captainWorkflows/update', {
|
||||
id: workflowId.value,
|
||||
assistantId: assistantId.value,
|
||||
name,
|
||||
description,
|
||||
trigger_event,
|
||||
enabled,
|
||||
nodes: nodes.value.map(n => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
position: n.position,
|
||||
data: n.data,
|
||||
})),
|
||||
edges: edges.value.map(e => ({
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
sourceHandle: e.sourceHandle,
|
||||
target: e.target,
|
||||
targetHandle: e.targetHandle,
|
||||
})),
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.UPDATE.ERROR')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
router.push({
|
||||
name: 'captain_assistants_workflows_index',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
assistantId: assistantId.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await store.dispatch('captainWorkflows/show', {
|
||||
assistantId: assistantId.value,
|
||||
id: workflowId.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full w-full bg-n-surface-1">
|
||||
<WorkflowToolbar :workflow="workflow" @save="saveWorkflow" @back="goBack" />
|
||||
<div class="flex flex-1 min-h-0">
|
||||
<NodePalette />
|
||||
<div class="flex-1 relative" @dragover="onDragOver" @drop="onDrop">
|
||||
<VueFlow
|
||||
v-model:nodes="nodes"
|
||||
v-model:edges="edges"
|
||||
:node-types="nodeTypes"
|
||||
:default-viewport="{ zoom: 1 }"
|
||||
fit-view-on-init
|
||||
class="w-full h-full"
|
||||
@node-click="onNodeClick"
|
||||
@pane-click="onPaneClick"
|
||||
/>
|
||||
</div>
|
||||
<NodeConfigPanel
|
||||
v-if="selectedNode"
|
||||
:node="selectedNode"
|
||||
@update="updateNodeData"
|
||||
@delete="deleteNode"
|
||||
@close="selectedNode = null"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const assistantId = computed(() => Number(route.params.assistantId));
|
||||
const uiFlags = useMapGetter('captainWorkflows/getUIFlags');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const workflows = useMapGetter('captainWorkflows/getRecords');
|
||||
|
||||
const TRIGGER_LABELS = {
|
||||
conversation_created:
|
||||
'CAPTAIN.ASSISTANTS.WORKFLOWS.TRIGGERS.CONVERSATION_CREATED',
|
||||
message_created: 'CAPTAIN.ASSISTANTS.WORKFLOWS.TRIGGERS.MESSAGE_CREATED',
|
||||
conversation_resolved:
|
||||
'CAPTAIN.ASSISTANTS.WORKFLOWS.TRIGGERS.CONVERSATION_RESOLVED',
|
||||
};
|
||||
|
||||
const createWorkflow = async () => {
|
||||
try {
|
||||
const workflow = await store.dispatch('captainWorkflows/create', {
|
||||
assistantId: assistantId.value,
|
||||
name: t('CAPTAIN.ASSISTANTS.WORKFLOWS.DEFAULT_NAME'),
|
||||
trigger_event: 'conversation_created',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
router.push({
|
||||
name: 'captain_assistants_workflow_editor',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
assistantId: assistantId.value,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.CREATE.ERROR')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const editWorkflow = workflowId => {
|
||||
router.push({
|
||||
name: 'captain_assistants_workflow_editor',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
assistantId: assistantId.value,
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleWorkflow = async workflow => {
|
||||
try {
|
||||
await store.dispatch('captainWorkflows/update', {
|
||||
id: workflow.id,
|
||||
assistantId: assistantId.value,
|
||||
enabled: !workflow.enabled,
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.UPDATE.ERROR')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteWorkflow = async id => {
|
||||
try {
|
||||
await store.dispatch('captainWorkflows/delete', {
|
||||
id,
|
||||
assistantId: assistantId.value,
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.WORKFLOWS.API.DELETE.ERROR')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainWorkflows/get', {
|
||||
assistantId: assistantId.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.DOCUMENTS.HEADER')"
|
||||
:is-fetching="isFetching"
|
||||
:show-know-more="false"
|
||||
:show-pagination-footer="false"
|
||||
>
|
||||
<template #body>
|
||||
<SettingsHeader
|
||||
:heading="$t('CAPTAIN.ASSISTANTS.WORKFLOWS.TITLE')"
|
||||
:description="$t('CAPTAIN.ASSISTANTS.WORKFLOWS.DESCRIPTION')"
|
||||
/>
|
||||
<div class="flex mt-7 flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<Button
|
||||
:label="$t('CAPTAIN.ASSISTANTS.WORKFLOWS.ADD_BUTTON')"
|
||||
icon="i-lucide-plus"
|
||||
@click="createWorkflow"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="workflows.length === 0" class="mt-1 mb-2">
|
||||
<span class="text-n-slate-11 text-sm">
|
||||
{{ $t('CAPTAIN.ASSISTANTS.WORKFLOWS.EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="workflow in workflows"
|
||||
:key="workflow.id"
|
||||
class="flex items-center justify-between p-4 border border-n-weak rounded-lg bg-n-solid-2 hover:bg-n-solid-3 cursor-pointer"
|
||||
@click="editWorkflow(workflow.id)"
|
||||
>
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class="text-sm font-medium text-n-slate-12 truncate">
|
||||
{{ workflow.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="workflow.description"
|
||||
class="text-xs text-n-slate-11 truncate"
|
||||
>
|
||||
{{ workflow.description }}
|
||||
</span>
|
||||
<span class="text-xs text-n-slate-10">
|
||||
{{ $t(TRIGGER_LABELS[workflow.trigger_event]) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<Switch
|
||||
:model-value="workflow.enabled"
|
||||
@click.stop
|
||||
@update:model-value="toggleWorkflow(workflow)"
|
||||
/>
|
||||
<Button
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ghost
|
||||
slate
|
||||
@click.stop="deleteWorkflow(workflow.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -12,6 +12,8 @@ import AssistantPlaygroundIndex from './assistants/playground/Index.vue';
|
||||
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
||||
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
|
||||
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
|
||||
import AssistantWorkflowsIndex from './assistants/workflows/Index.vue';
|
||||
import AssistantWorkflowEditor from './assistants/workflows/Editor.vue';
|
||||
import DocumentsIndex from './documents/Index.vue';
|
||||
import ResponsesIndex from './responses/Index.vue';
|
||||
import ResponsesPendingIndex from './responses/Pending.vue';
|
||||
@@ -54,6 +56,20 @@ const assistantRoutes = [
|
||||
name: 'captain_assistants_scenarios_index',
|
||||
meta: metaV2,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/workflows'),
|
||||
component: AssistantWorkflowsIndex,
|
||||
name: 'captain_assistants_workflows_index',
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/captain/:assistantId/workflows/:workflowId'
|
||||
),
|
||||
component: AssistantWorkflowEditor,
|
||||
name: 'captain_assistants_workflow_editor',
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/playground'),
|
||||
component: AssistantPlaygroundIndex,
|
||||
|
||||
@@ -56,6 +56,7 @@ const routeToLastActiveAssistant = () => {
|
||||
'captain_assistants_responses_index', // Faq page
|
||||
'captain_assistants_documents_index', // Document page
|
||||
'captain_assistants_scenarios_index', // Scenario page
|
||||
'captain_assistants_workflows_index', // Workflows page
|
||||
'captain_assistants_playground_index', // Playground page
|
||||
'captain_assistants_inboxes_index', // Inboxes page
|
||||
'captain_tools_index', // Tools page
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import CaptainWorkflows from 'dashboard/api/captain/workflows';
|
||||
import { createStore } from '../storeFactory';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
|
||||
export default createStore({
|
||||
name: 'CaptainWorkflow',
|
||||
API: CaptainWorkflows,
|
||||
actions: mutations => ({
|
||||
get: async ({ commit }, { assistantId, page = 1 } = {}) => {
|
||||
commit(mutations.SET_UI_FLAG, { fetchingList: true });
|
||||
try {
|
||||
const response = await CaptainWorkflows.get({ assistantId, page });
|
||||
commit(mutations.SET, response.data.payload);
|
||||
commit(mutations.SET_META, response.data.meta);
|
||||
commit(mutations.SET_UI_FLAG, { fetchingList: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(mutations.SET_UI_FLAG, { fetchingList: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
show: async ({ commit }, { assistantId, id }) => {
|
||||
commit(mutations.SET_UI_FLAG, { fetchingItem: true });
|
||||
try {
|
||||
const response = await CaptainWorkflows.show({ assistantId, id });
|
||||
commit(mutations.ADD, response.data);
|
||||
commit(mutations.SET_UI_FLAG, { fetchingItem: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(mutations.SET_UI_FLAG, { fetchingItem: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
create: async ({ commit }, { assistantId, ...data }) => {
|
||||
commit(mutations.SET_UI_FLAG, { creatingItem: true });
|
||||
try {
|
||||
const response = await CaptainWorkflows.create({
|
||||
assistantId,
|
||||
...data,
|
||||
});
|
||||
commit(mutations.ADD, response.data);
|
||||
commit(mutations.SET_UI_FLAG, { creatingItem: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(mutations.SET_UI_FLAG, { creatingItem: false });
|
||||
return throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
update: async ({ commit }, { id, assistantId, ...updateObj }) => {
|
||||
commit(mutations.SET_UI_FLAG, { updatingItem: true });
|
||||
try {
|
||||
const response = await CaptainWorkflows.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 CaptainWorkflows.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);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -58,6 +58,7 @@ import copilotMessages from './captain/copilotMessages';
|
||||
import captainScenarios from './captain/scenarios';
|
||||
import captainTools from './captain/tools';
|
||||
import captainCustomTools from './captain/customTools';
|
||||
import captainWorkflows from './captain/workflows';
|
||||
|
||||
const plugins = [];
|
||||
|
||||
@@ -121,6 +122,7 @@ export default createStore({
|
||||
captainScenarios,
|
||||
captainTools,
|
||||
captainCustomTools,
|
||||
captainWorkflows,
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user