feat: New Scenarios page (#11975)

This commit is contained in:
Sivin Varghese
2025-07-30 19:34:27 +05:30
committed by GitHub
parent 1230d1f251
commit df4de508e7
27 changed files with 1161 additions and 15 deletions

View File

@@ -266,6 +266,11 @@ const addAllExample = () => {
{{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.EMPTY_MESSAGE') }}
</span>
</div>
<div v-else-if="filteredGuardrails.length === 0" class="mt-1 mb-2">
<span class="text-n-slate-11 text-sm">
{{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.SEARCH_EMPTY_MESSAGE') }}
</span>
</div>
<div v-else class="flex flex-col gap-2">
<RuleCard
v-for="guardrail in filteredGuardrails"

View File

@@ -284,6 +284,13 @@ const addAllExample = async () => {
{{ t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.EMPTY_MESSAGE') }}
</span>
</div>
<div v-else-if="filteredGuidelines.length === 0" class="mt-1 mb-2">
<span class="text-n-slate-11 text-sm">
{{
t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.SEARCH_EMPTY_MESSAGE')
}}
</span>
</div>
<div v-else class="flex flex-col gap-2">
<RuleCard
v-for="guideline in filteredGuidelines"

View File

@@ -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 its 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 23 clarifying questions. Use [Product Match](tool://product_match[user_needs]) and suggest 23 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>

View File

@@ -41,7 +41,7 @@ const controlItems = computed(() => {
description: t(
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
),
// routeName: 'captain_assistants_scenarios_index',
routeName: 'captain_assistants_scenarios_index',
},
{
name: t(

View File

@@ -7,6 +7,7 @@ import AssistantEdit from './assistants/Edit.vue';
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
import DocumentsIndex from './documents/Index.vue';
import ResponsesIndex from './responses/Index.vue';
@@ -67,6 +68,21 @@ export const routes = [
],
},
},
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/scenarios'
),
component: AssistantScenariosIndex,
name: 'captain_assistants_scenarios_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/guidelines'