Add workflows inside captain
This commit is contained in:
@@ -64,6 +64,18 @@ const FIELD_CONFIGS = {
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.PRIORITY',
|
||||
},
|
||||
],
|
||||
collect_input: [
|
||||
{
|
||||
key: 'input_key',
|
||||
component: 'input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.INPUT_KEY',
|
||||
},
|
||||
{
|
||||
key: 'prompt',
|
||||
component: 'textarea',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.CONFIG.PROMPT',
|
||||
},
|
||||
],
|
||||
condition: [
|
||||
{
|
||||
key: 'attribute',
|
||||
|
||||
@@ -5,23 +5,13 @@ const { t } = useI18n();
|
||||
|
||||
const NODE_CATEGORIES = [
|
||||
{
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.PALETTE.TRIGGERS',
|
||||
color: 'n-teal',
|
||||
icon: 'i-lucide-zap',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.PALETTE.INTERACTIVE',
|
||||
color: 'n-violet',
|
||||
icon: 'i-lucide-message-circle-question',
|
||||
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',
|
||||
type: 'collect_input',
|
||||
label: 'CAPTAIN.ASSISTANTS.WORKFLOWS.NODES.COLLECT_INPUT',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ 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: () => ({}) },
|
||||
@@ -15,7 +14,6 @@ const { t } = useI18n();
|
||||
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
const triggerEvent = ref('conversation_created');
|
||||
const enabled = ref(false);
|
||||
|
||||
watch(
|
||||
@@ -24,38 +22,16 @@ watch(
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -72,11 +48,6 @@ const save = () => {
|
||||
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') }}
|
||||
|
||||
@@ -8,22 +8,16 @@ defineProps({
|
||||
|
||||
<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"
|
||||
class="flex rounded-lg bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm"
|
||||
>
|
||||
<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>
|
||||
<div class="w-0.5 shrink-0 bg-n-blue-9" />
|
||||
<div class="flex items-center gap-1 px-1.5 py-1 min-w-0">
|
||||
<span class="i-lucide-play text-n-blue-11 size-2.5 shrink-0" />
|
||||
<span
|
||||
v-if="data.description"
|
||||
class="text-[11px] text-n-slate-10 leading-tight truncate"
|
||||
class="text-[11px] leading-none font-medium text-n-slate-12 truncate"
|
||||
>
|
||||
{{ data.description }}
|
||||
{{ data.label || 'Action' }}
|
||||
</span>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Bottom" class="!bg-n-blue-9" />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
defineProps({
|
||||
data: { type: Object, default: () => ({}) },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex rounded-lg bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm"
|
||||
>
|
||||
<Handle type="target" :position="Position.Top" class="!bg-n-violet-9" />
|
||||
<div class="w-0.5 shrink-0 bg-n-violet-9" />
|
||||
<div class="flex items-center gap-1 px-1.5 py-1 min-w-0">
|
||||
<span
|
||||
class="i-lucide-message-circle-question text-n-violet-11 size-2.5 shrink-0"
|
||||
/>
|
||||
<span
|
||||
class="text-[11px] leading-none font-medium text-n-slate-12 truncate"
|
||||
>
|
||||
{{ data.label || 'Collect Input' }}
|
||||
</span>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Bottom" class="!bg-n-violet-9" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,13 +11,13 @@ const { t } = useI18n();
|
||||
|
||||
<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"
|
||||
class="flex w-32 rounded-xl bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm"
|
||||
>
|
||||
<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 flex-col gap-0.5 px-2 py-1.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="i-lucide-git-branch text-n-amber-11 size-3 shrink-0" />
|
||||
<span class="text-xs font-medium text-n-slate-12 truncate">
|
||||
{{ data.label || 'Condition' }}
|
||||
</span>
|
||||
|
||||
@@ -8,13 +8,13 @@ defineProps({
|
||||
|
||||
<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"
|
||||
class="flex w-32 rounded-xl bg-n-solid-2 outline outline-1 -outline-offset-1 outline-n-container shadow-sm"
|
||||
>
|
||||
<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 flex-col gap-0.5 px-2 py-1.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="i-lucide-shopping-bag text-n-teal-11 size-3 shrink-0" />
|
||||
<span class="text-xs font-medium text-n-slate-12 truncate">
|
||||
{{ data.label || 'Shopify' }}
|
||||
</span>
|
||||
|
||||
@@ -735,25 +735,18 @@
|
||||
},
|
||||
"WORKFLOWS": {
|
||||
"TITLE": "Workflows",
|
||||
"DESCRIPTION": "Automate actions triggered by conversation events with a visual workflow builder.",
|
||||
"DESCRIPTION": "Build interactive workflows that Captain can execute during conversations.",
|
||||
"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",
|
||||
"INTERACTIVE": "Interactive",
|
||||
"LOGIC": "Logic",
|
||||
"ACTIONS": "Actions",
|
||||
"SHOPIFY": "Shopify"
|
||||
},
|
||||
"NODES": {
|
||||
"TRIGGER_CONVERSATION_CREATED": "Conversation Created",
|
||||
"TRIGGER_MESSAGE_CREATED": "Message Created",
|
||||
"TRIGGER_CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"COLLECT_INPUT": "Collect Input",
|
||||
"CONDITION": "Condition",
|
||||
"SEND_MESSAGE": "Send Message",
|
||||
"ADD_LABEL": "Add Label",
|
||||
@@ -776,6 +769,8 @@
|
||||
"ATTRIBUTE": "Attribute",
|
||||
"OPERATOR": "Operator",
|
||||
"VALUE": "Value",
|
||||
"INPUT_KEY": "Input Key",
|
||||
"PROMPT": "Prompt",
|
||||
"APPLY": "Apply",
|
||||
"DELETE": "Delete Node",
|
||||
"CONDITION_TRUE": "True",
|
||||
|
||||
@@ -9,10 +9,10 @@ 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';
|
||||
import CollectInputNode from 'dashboard/components-next/captain/workflows/nodes/CollectInputNode.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
@@ -32,9 +32,7 @@ const nodes = ref([]);
|
||||
const edges = ref([]);
|
||||
|
||||
const nodeTypes = {
|
||||
trigger_conversation_created: TriggerNode,
|
||||
trigger_message_created: TriggerNode,
|
||||
trigger_conversation_resolved: TriggerNode,
|
||||
collect_input: CollectInputNode,
|
||||
condition: ConditionNode,
|
||||
send_message: ActionNode,
|
||||
add_label: ActionNode,
|
||||
@@ -115,14 +113,13 @@ const deleteNode = nodeId => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveWorkflow = async ({ name, description, trigger_event, enabled }) => {
|
||||
const saveWorkflow = async ({ name, description, 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,
|
||||
@@ -192,3 +189,40 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vue-flow__node {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
font-size: inherit;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid white;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vue-flow__edge-path {
|
||||
stroke: var(--n-alpha-7);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.vue-flow__edge.selected .vue-flow__edge-path,
|
||||
.vue-flow__edge:hover .vue-flow__edge-path {
|
||||
stroke: var(--n-slate-9);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.vue-flow__connection-line {
|
||||
stroke: var(--n-slate-9);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,20 +20,11 @@ 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: [],
|
||||
});
|
||||
@@ -144,9 +135,6 @@ onMounted(() => {
|
||||
>
|
||||
{{ 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
|
||||
|
||||
@@ -32,6 +32,7 @@ import { vResizeObserver } from '@vueuse/components';
|
||||
import { directive as onClickaway } from 'vue3-click-away';
|
||||
|
||||
import 'floating-vue/dist/style.css';
|
||||
import '@vue-flow/core/dist/style.css';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
|
||||
|
||||
Reference in New Issue
Block a user