@@ -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 >