diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml
new file mode 100644
index 000000000..98df89707
--- /dev/null
+++ b/.github/workflows/auto-assign-pr.yml
@@ -0,0 +1,28 @@
+name: Auto-assign PR to Author
+
+on:
+ pull_request:
+ types: [opened]
+
+jobs:
+ auto-assign:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Auto-assign PR to author
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const pull_number = context.payload.pull_request.number;
+ const author = context.payload.pull_request.user.login;
+
+ await github.rest.issues.addAssignees({
+ owner,
+ repo,
+ issue_number: pull_number,
+ assignees: [author]
+ });
+
+ console.log(`Assigned PR #${pull_number} to ${author}`);
\ No newline at end of file
diff --git a/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue b/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue
index 8ca325607..8a1b26e90 100644
--- a/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue
+++ b/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue
@@ -15,8 +15,8 @@ const emit = defineEmits(['click']);
const { t } = useI18n();
-const onClick = event => {
- emit('click', event);
+const onClick = (item, index) => {
+ emit('click', item, index);
};
@@ -24,22 +24,25 @@ const onClick = event => {
diff --git a/app/javascript/dashboard/components-next/captain/SettingsPageLayout.vue b/app/javascript/dashboard/components-next/captain/SettingsPageLayout.vue
new file mode 100644
index 000000000..4a48794b3
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/SettingsPageLayout.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
index 7879411c8..a924ca228 100644
--- a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
@@ -55,6 +55,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ showMenu: {
+ type: Boolean,
+ default: true,
+ },
});
const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
@@ -130,7 +134,7 @@ const handleDocumentableClick = () => {
{{ question }}
-
+
+import { reactive, computed, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useVuelidate } from '@vuelidate/core';
+import { required, minLength } from '@vuelidate/validators';
+
+import Input from 'dashboard/components-next/input/Input.vue';
+import Button from 'dashboard/components-next/button/Button.vue';
+import Editor from 'dashboard/components-next/Editor/Editor.vue';
+
+const props = defineProps({
+ assistant: {
+ type: Object,
+ default: () => ({}),
+ },
+});
+
+const emit = defineEmits(['submit']);
+
+const { t } = useI18n();
+
+const initialState = {
+ name: '',
+ description: '',
+ productName: '',
+ features: {
+ conversationFaqs: false,
+ memories: false,
+ },
+};
+
+const state = reactive({ ...initialState });
+
+const validationRules = {
+ name: { required, minLength: minLength(1) },
+ description: { required, minLength: minLength(1) },
+ productName: { required, minLength: minLength(1) },
+};
+
+const v$ = useVuelidate(validationRules, state);
+
+const getErrorMessage = field => {
+ return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
+};
+
+const formErrors = computed(() => ({
+ name: getErrorMessage('name'),
+ description: getErrorMessage('description'),
+ productName: getErrorMessage('productName'),
+}));
+
+const updateStateFromAssistant = assistant => {
+ const { config = {} } = assistant;
+ state.name = assistant.name;
+ state.description = assistant.description;
+ state.productName = config.product_name;
+ state.features = {
+ conversationFaqs: config.feature_faq || false,
+ memories: config.feature_memory || false,
+ };
+};
+
+const handleBasicInfoUpdate = async () => {
+ const result = await Promise.all([
+ v$.value.name.$validate(),
+ v$.value.description.$validate(),
+ v$.value.productName.$validate(),
+ ]).then(results => results.every(Boolean));
+ if (!result) return;
+
+ const payload = {
+ name: state.name,
+ description: state.description,
+ config: {
+ ...props.assistant.config,
+ product_name: state.productName,
+ feature_faq: state.features.conversationFaqs,
+ feature_memory: state.features.memories,
+ },
+ };
+
+ emit('submit', payload);
+};
+
+watch(
+ () => props.assistant,
+ newAssistant => {
+ if (newAssistant) updateStateFromAssistant(newAssistant);
+ },
+ { immediate: true }
+);
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue
new file mode 100644
index 000000000..2bbede899
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ {{ controlItem.name }}
+
+
+
+
+
+
+ {{ controlItem.description }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue
new file mode 100644
index 000000000..b86bdacb4
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ state.temperature }}
+
+
+ {{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js
index 1a6ada9fa..98234539b 100644
--- a/app/javascript/dashboard/featureFlags.js
+++ b/app/javascript/dashboard/featureFlags.js
@@ -36,6 +36,7 @@ export const FEATURE_FLAGS = {
REPORT_V4: 'report_v4',
CHANNEL_INSTAGRAM: 'channel_instagram',
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
+ CAPTAIN_V2: 'captain_integration_v2',
};
export const PREMIUM_FEATURES = [
@@ -44,4 +45,5 @@ export const PREMIUM_FEATURES = [
FEATURE_FLAGS.CUSTOM_ROLES,
FEATURE_FLAGS.AUDIT_LOGS,
FEATURE_FLAGS.HELP_CENTER,
+ FEATURE_FLAGS.CAPTAIN_V2,
];
diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json
index b3e091722..fc8cff644 100644
--- a/app/javascript/dashboard/i18n/locale/en/integrations.json
+++ b/app/javascript/dashboard/i18n/locale/en/integrations.json
@@ -478,6 +478,37 @@
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
+ "SETTINGS": {
+ "BREADCRUMB": {
+ "ASSISTANT": "Assistant"
+ },
+ "BASIC_SETTINGS": {
+ "TITLE": "Basic settings",
+ "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
+ },
+ "SYSTEM_SETTINGS": {
+ "TITLE": "System settings",
+ "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
+ },
+ "CONTROL_ITEMS": {
+ "TITLE": "The Fun Stuff",
+ "DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
+ "OPTIONS": {
+ "GUARDRAILS": {
+ "TITLE": "Guardrails",
+ "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
+ },
+ "SCENARIOS": {
+ "TITLE": "Scenarios",
+ "DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
+ },
+ "RESPONSE_GUIDELINES": {
+ "TITLE": "Response guidelines",
+ "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
+ }
+ }
+ }
+ },
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
"DELETE_ASSISTANT": "Delete Assistant",
diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue
index 8b64e2663..2b2506934 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue
+++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue
@@ -5,10 +5,13 @@ import { useStore } from 'dashboard/composables/store';
import { useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
+import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.vue';
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
+import AssistantSettings from 'dashboard/routes/dashboard/captain/assistants/settings/Settings.vue';
+
const route = useRoute();
const store = useStore();
const { t } = useI18n();
@@ -19,6 +22,16 @@ const assistant = computed(() =>
store.getters['captainAssistants/getRecord'](Number(assistantId))
);
+const isFeatureEnabledonAccount = useMapGetter(
+ 'accounts/isFeatureEnabledonAccount'
+);
+const currentAccountId = useMapGetter('getCurrentAccountId');
+
+const isCaptainV2Enabled = isFeatureEnabledonAccount.value(
+ currentAccountId.value,
+ FEATURE_FLAGS.CAPTAIN_V2
+);
+
const isAssistantAvailable = computed(() => !!assistant.value?.id);
const handleSubmit = async updatedAssistant => {
@@ -36,14 +49,16 @@ const handleSubmit = async updatedAssistant => {
};
onMounted(() => {
- if (!isAssistantAvailable.value) {
+ if (!isAssistantAvailable.value || !isCaptainV2Enabled) {
store.dispatch('captainAssistants/show', assistantId);
}
});
+
+import { computed, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import { useAlert } from 'dashboard/composables';
+import { useStore } from 'dashboard/composables/store';
+import { useMapGetter } from 'dashboard/composables/store';
+import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
+import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
+import AssistantBasicSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue';
+import AssistantSystemSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue';
+import AssistantControlItems from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue';
+
+const { t } = useI18n();
+const route = useRoute();
+const store = useStore();
+const assistantId = route.params.assistantId;
+const uiFlags = useMapGetter('captainAssistants/getUIFlags');
+const isFetching = computed(() => uiFlags.value.fetchingItem);
+const assistant = computed(() =>
+ store.getters['captainAssistants/getRecord'](Number(assistantId))
+);
+
+const isAssistantAvailable = computed(() => !!assistant.value?.id);
+
+const controlItems = computed(() => {
+ return [
+ {
+ name: t(
+ 'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.GUARDRAILS.TITLE'
+ ),
+ description: t(
+ 'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.GUARDRAILS.DESCRIPTION'
+ ),
+ // routeName: 'captain_assistants_guardrails_index',
+ },
+ {
+ name: t(
+ 'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.TITLE'
+ ),
+ description: t(
+ 'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
+ ),
+ // routeName: 'captain_assistants_scenarios_index',
+ },
+ {
+ name: t(
+ 'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.TITLE'
+ ),
+ description: t(
+ 'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.DESCRIPTION'
+ ),
+ // routeName: 'captain_assistants_guidelines_index',
+ },
+ ];
+});
+
+const breadcrumbItems = computed(() => {
+ const activeControlItem = controlItems.value?.find(
+ item => item.routeName === route.name
+ );
+
+ return [
+ {
+ label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
+ routeName: 'captain_assistants_index',
+ },
+ { label: assistant.value?.name, routeName: 'captain_assistants_edit' },
+ ...(activeControlItem
+ ? [
+ {
+ label: activeControlItem.name,
+ routeName: activeControlItem.routeName,
+ },
+ ]
+ : []),
+ ];
+});
+
+const handleSubmit = async updatedAssistant => {
+ try {
+ await store.dispatch('captainAssistants/update', {
+ id: assistantId,
+ ...updatedAssistant,
+ });
+ useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
+ } catch (error) {
+ const errorMessage =
+ error?.message || t('CAPTAIN.ASSISTANTS.EDIT.ERROR_MESSAGE');
+ useAlert(errorMessage);
+ }
+};
+
+onMounted(() => {
+ if (!isAssistantAvailable.value) {
+ store.dispatch('captainAssistants/show', assistantId);
+ }
+});
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js b/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js
index 17afeca14..7294a7433 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js
@@ -3,6 +3,7 @@ import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from '../../../helper/URLHelper';
import AssistantIndex from './assistants/Index.vue';
import AssistantEdit from './assistants/Edit.vue';
+// import AssistantSettings from './assistants/settings/Settings.vue';
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
import DocumentsIndex from './documents/Index.vue';
import ResponsesIndex from './responses/Index.vue';
diff --git a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue
index e771e72ed..85e7f1ed0 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue
@@ -55,6 +55,12 @@ const statusOptions = computed(() =>
}))
);
+const filteredResponses = computed(() => {
+ return selectedStatus.value === 'pending'
+ ? responses.value.filter(r => r.status === 'pending')
+ : responses.value;
+});
+
const selectedStatusLabel = computed(() => {
const status = statusOptions.value.find(
option => option.value === selectedStatus.value
@@ -94,7 +100,9 @@ const handleEdit = () => {
};
const handleAction = ({ action, id }) => {
- selectedResponse.value = responses.value.find(response => id === response.id);
+ selectedResponse.value = filteredResponses.value.find(
+ response => id === response.id
+ );
nextTick(() => {
if (action === 'delete') {
handleDelete();
@@ -139,7 +147,7 @@ const hoveredCard = ref(null);
const bulkSelectionState = computed(() => {
const selectedCount = bulkSelectedIds.value.size;
- const totalCount = responses.value?.length || 0;
+ const totalCount = filteredResponses.value?.length || 0;
return {
hasSelected: selectedCount > 0,
@@ -152,13 +160,13 @@ const bulkCheckbox = computed({
get: () => bulkSelectionState.value.allSelected,
set: value => {
bulkSelectedIds.value = value
- ? new Set(responses.value.map(r => r.id))
+ ? new Set(filteredResponses.value.map(r => r.id))
: new Set();
},
});
const buildSelectedCountLabel = computed(() => {
- const count = responses.value?.length || 0;
+ const count = filteredResponses.value?.length || 0;
return bulkSelectionState.value.allSelected
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
@@ -174,6 +182,24 @@ const handleCardSelect = id => {
bulkSelectedIds.value = selected;
};
+const fetchResponseAfterBulkAction = () => {
+ const hasNoResponsesLeft = filteredResponses.value?.length === 0;
+ const currentPage = responseMeta.value?.page;
+
+ if (hasNoResponsesLeft) {
+ // Page is now empty after bulk action.
+ // Fetch the previous page if not already on the first page.
+ const pageToFetch = currentPage > 1 ? currentPage - 1 : currentPage;
+ fetchResponses(pageToFetch);
+ } else {
+ // Page still has responses left, re-fetch the same page.
+ fetchResponses(currentPage);
+ }
+
+ // Clear selection
+ bulkSelectedIds.value = new Set();
+};
+
const handleBulkApprove = async () => {
try {
await store.dispatch(
@@ -181,8 +207,7 @@ const handleBulkApprove = async () => {
Array.from(bulkSelectedIds.value)
);
- // Clear selection
- bulkSelectedIds.value = new Set();
+ fetchResponseAfterBulkAction();
useAlert(t('CAPTAIN.RESPONSES.BULK_APPROVE.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(
@@ -205,23 +230,13 @@ const onPageChange = page => {
};
const onDeleteSuccess = () => {
- if (responses.value?.length === 0 && responseMeta.value?.page > 1) {
+ if (filteredResponses.value?.length === 0 && responseMeta.value?.page > 1) {
onPageChange(responseMeta.value.page - 1);
}
};
const onBulkDeleteSuccess = () => {
- // Only fetch if no records left
- if (responses.value?.length === 0) {
- const page =
- responseMeta.value?.page > 1
- ? responseMeta.value.page - 1
- : responseMeta.value.page;
- fetchResponses(page);
- }
-
- // Clear selection
- bulkSelectedIds.value = new Set();
+ fetchResponseAfterBulkAction();
};
const handleStatusFilterChange = ({ value }) => {
@@ -249,8 +264,8 @@ onMounted(() => {
:header-title="$t('CAPTAIN.RESPONSES.HEADER')"
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
:is-fetching="isFetching"
- :is-empty="!responses.length"
- :show-pagination-footer="!isFetching && !!responses.length"
+ :is-empty="!filteredResponses.length"
+ :show-pagination-footer="!isFetching && !!filteredResponses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@update:current-page="onPageChange"
@click="handleCreate"
@@ -368,7 +383,7 @@ onMounted(() => {
{
:updated-at="response.updated_at"
:is-selected="bulkSelectedIds.has(response.id)"
:selectable="hoveredCard === response.id || bulkSelectedIds.size > 0"
+ :show-menu="!bulkSelectedIds.has(response.id)"
@action="handleAction"
@navigate="handleNavigationAction"
@select="handleCardSelect"
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue
index aae2263a7..c4cf5f5ad 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue
@@ -1,12 +1,16 @@