feat: Workflows v1

This commit is contained in:
Pranav
2026-02-27 21:05:26 -08:00
parent c08fa631a9
commit c0490794d1
52 changed files with 1903 additions and 33 deletions
@@ -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'),