feat(chatbot): fully implement chatbot, fix UI issues, add to permissions under user list.

This commit is contained in:
Nicolas Drolet 2026-01-12 14:12:37 -05:00
parent 0fe8c89dd9
commit 7114574ecf
16 changed files with 148 additions and 196 deletions

View File

@ -123,6 +123,7 @@ export default {
personal_profile: "profile",
timesheets: "timesheets",
timesheets_approval: "timesheet approval",
chatbot: "chatbot",
user_access: "module access",
by_role: "by role",
by_module: "by module",

View File

@ -2,7 +2,7 @@ export default {
chatbot: {
chat_header: "Agent IA",
chat_initial_message: "Bienvenue à votre assistant technique.\nVeuillez fournir le ID du Client pour avoir un diagnostique",
chat_placeholder: "Entré un Message",
chat_placeholder: "Entrez un Message",
chat_thinking: "Réflexion en cours...",
error: {
NO_REPLY_RECEIVED: "Une erreur est survenu lors de la réception de la réponse du chatbot",
@ -123,6 +123,7 @@ export default {
personal_profile: "profil personnel",
timesheets: "carte de temps",
timesheets_approval: "validation cartes de temps",
chatbot: "chatbot",
user_access: "module access",
by_role: "par rôle",
by_module: "par module",

View File

@ -3,9 +3,14 @@
setup
>
import { useUiStore } from 'src/stores/ui-store';
import { useAuthStore } from 'src/stores/auth-store';
import { useChatbotStore } from 'src/stores/chatbot-store';
// import HeaderBarNotification from './main-layout-header-bar-notification.vue';
const uiStore = useUiStore();
const chatbot_store = useChatbotStore();
const auth_store = useAuthStore();
</script>
<template>
@ -19,7 +24,11 @@
@click="uiStore.toggleRightDrawer"
class="q-px-none"
>
<q-icon name="menu" size="lg" class="q-mr-lg"/>
<q-icon
name="menu"
size="lg"
class="q-mr-lg"
/>
<q-img
src="src/assets/logo-targo-white.svg"
fit="contain"
@ -28,8 +37,18 @@
/>
</q-btn>
</q-toolbar-title>
<q-item class="q-pa-none">
<!-- <HeaderBarNotification /> -->
<q-btn
v-if="auth_store.user?.user_module_access.includes('chatbot')"
flat
color="transparant"
icon="las la-robot"
size="lg"
@click="chatbot_store.is_showing_chatbot = !chatbot_store.is_showing_chatbot"
style="--q-icon-size: 28px; min-width: auto;"
/>
</q-item>
</q-toolbar>
</q-header>

View File

@ -5,6 +5,7 @@
import HeaderBar from 'src/layouts/components/main-layout-header-bar.vue';
import FooterBar from 'src/layouts/components/main-layout-footer-bar.vue';
import LeftDrawer from 'src/layouts/components/main-layout-left-drawer.vue';
import ChatbotDrawer from 'src/modules/chatbot/components/chatbot-drawer.vue';
import { onMounted, watch, ref } from 'vue';
import { RouterView } from 'vue-router';
@ -35,6 +36,8 @@
<LeftDrawer />
<ChatbotDrawer />
<q-page-container>
<router-view />
</q-page-container>

View File

@ -1,24 +0,0 @@
<script
setup
lang="ts"
>
import { useChatbotStore } from 'src/stores/chatbot-store';
const chatbot_store = useChatbotStore();
</script>
<template>
<div
style="z-index: 3; margin-right: 20px"
class="display: flex"
>
<q-btn
flat
color="transparant"
size="20px"
icon="smart_toy"
@click="chatbot_store.is_showing_chatbot = !chatbot_store.is_showing_chatbot"
style="--q-icon-size: 28px; min-width: auto;"
/>
</div>
</template>

View File

@ -0,0 +1,75 @@
<script
setup
lang="ts"
>
import DialogueContent from "./dialogue-content.vue";
import { onMounted, ref } from "vue";
import { useChatbotStore } from "src/stores/chatbot-store";
import { useChatbotApi } from "src/modules/chatbot/composables/chatbot-api";
const chatbot_api = useChatbotApi();
const chatbot_store = useChatbotStore();
const text = ref('');
const handleSend = async () => {
await chatbot_api.sendMessage(text.value.trim());
text.value = '';
};
onMounted(() => {
chatbot_store.showInstructionsOnce();
});
</script>
<template>
<q-drawer
v-model="chatbot_store.is_showing_chatbot"
overlay
side="right"
:width="500"
class="column justify-end no-wrap"
>
<div class="col q-px-md relative-position">
<DialogueContent class="absolute-full" />
</div>
<q-form
submit="handleSend"
class="col-auto row"
>
<q-input
v-model="text"
borderless
:label="$t('chatbot.chat_placeholder')"
:autogrow="false"
dark
label-color="white"
class="col q-px-md"
style="background: rgba(0, 0, 0, 0.3);"
@keyup.enter="handleSend"
/>
<!-- <q-input
v-model="custId"
:label="'Customer Id'"
:autogrow="false"
class="col"
style="margin-left: 8px;"
@keyup.enter="handleCustomerId"
/> -->
</q-form>
<!-- <div
class="drag-handle"
@mousedown.prevent="startDrag"
></div> -->
</q-drawer>
</template>
<style scoped>
:deep(.q-drawer) {
background: rgba(0, 0, 0, 0.3);
}
</style>

View File

@ -1,136 +0,0 @@
<script
setup
lang="ts"
>
import DialogueContent from "./dialogue-content.vue";
import { onMounted, ref } from "vue";
import { useChatbotStore } from "src/stores/chatbot-store";
import { useChatbotApi } from "src/modules/chatbot/composables/chatbot-api";
// Block to enable editing the width of the drawer
// const drawerWidth = ref(370);
// let dragging = false;
// const startDrag = (e: MouseEvent) => {
// dragging = true
// e.preventDefault()
// }
// const onDrag = (e: MouseEvent) => {
// if (!dragging) return
// // calculate new width
// const newWidth = window.innerWidth - e.clientX
// drawerWidth.value = Math.max(350, Math.min(1000, newWidth)) // min/max width
// }
// const stopDrag = () => {
// dragging = false
// }
// window.addEventListener('mousemove', onDrag)
// window.addEventListener('mouseup', stopDrag)
const chatbot_api = useChatbotApi();
const chatbot_store = useChatbotStore();
const text = ref('');
const handleSend = async () => {
await chatbot_api.sendMessage(text.value.trim());
text.value = '';
};
// Capture and send page context to n8n
// const currentContext = computed(() =>
// pageContexts.find(ctx => ctx.path === route.fullPath.replace('/', ''))
// )
// // Function that sends the page context to n8n
// watch([currentContext, userEmail, userRole], async ([ctx, email, role]) => {
// if (!ctx || !email || !role) return;
// const contextPayload: contextObject = {
// name: ctx.name,
// description: ctx.description,
// features: ctx.features,
// path: ctx.path
// }
// try {
// await Promise.all([chatbotService.sendUserCredentials(email, role),
// sendPageContext(contextPayload),
// ]);
// is_session_ready.value = true;
// } catch (err) {
// console.error("Error", err)
// }
// },
// { immediate: true }
// );
onMounted(() => {
chatbot_store.showInstructionsOnce();
})
// const custId = ref('')
// const handleCustomerId = async () => {
// const cId = custId.value;
// custId.value = '';
// await chatbotService.retrieveCustomerDiagnostics(cId);
// }
</script>
<template>
<q-drawer
side="right"
v-model="chatbot_store.is_showing_chatbot"
class="column justify-end"
>
<div class="bg-primary text-uppercase">{{ $t('chatbot.chat_header') }}</div>
<div class="line-separator"></div>
<div class="chat-body">
<DialogueContent />
</div>
<div class="line-separator"></div>
<q-form
submit="handleSend"
class="chat-footer row"
>
<q-input
v-model="text"
:label="$t('chatbot.chat_placeholder')"
:autogrow="false"
class="col"
style="margin-left: 8px;"
@keyup.enter="handleSend"
/>
<!-- <q-input
v-model="custId"
:label="'Customer Id'"
:autogrow="false"
class="col"
style="margin-left: 8px;"
@keyup.enter="handleCustomerId"
/> -->
</q-form>
<!-- <div
class="drag-handle"
@mousedown.prevent="startDrag"
></div> -->
</q-drawer>
</template>
<!-- <style scoped>
.drag-handle {
position: absolute;
top: 0;
left: 0;
width: 8px;
height: 100%;
cursor: ew-resize;
z-index: 1000;
background-color: rgba(0, 0, 0, 0);
}
</style> -->

View File

@ -3,18 +3,28 @@
lang="ts"
>
import DialoguePhrase from './dialogue-phrase.vue';
import { useChatbotStore } from 'src/stores/chatbot-store';
import { ref, watch } from 'vue';
import { useChatbotStore } from 'src/stores/chatbot-store';
import type { QScrollArea } from 'quasar';
const chatbot_store = useChatbotStore();
const scroll_area = ref<QScrollArea | null>(null);
watch(chatbot_store.messages, () => {
if (scroll_area.value) {
scroll_area.value.setScrollPercentage('vertical', 1.0, 500);
}
})
</script>
<template>
<q-infinite-scroll>
<template
<q-scroll-area ref="scroll_area">
<div
v-for="(msg, index) in chatbot_store.messages"
:key="index"
class="q-px-md"
>
<DialoguePhrase
v-if="!msg.isThinking"
@ -32,6 +42,6 @@
/>
<span class="q-ml-sm text-grey-7">{{ $t('chatbot.chat_thinking') }}</span>
</div>
</template>
</q-infinite-scroll>
</div>
</q-scroll-area>
</template>

View File

@ -7,6 +7,7 @@
import { useAuthStore } from 'src/stores/auth-store';
import type { Message } from 'src/modules/chatbot/models/dialogue-message.model';
const {t}
const auth_store = useAuthStore();
const { message } = defineProps<{
@ -33,15 +34,17 @@
// Compute parsed content
const parsedText = computed((): string => {
const cleaned = cleanMarkdown(message.text || '')
return md.render(cleaned)
return md.render(cleaned);
})
</script>
<template>
<q-chat-message
:sent="message.sent"
:text="[parsedText]"
:name="message.sent ? auth_store.user?.first_name : 'TargoBot'"
:bg-color="message.sent ? 'accent' : 'secondary'"
/>
:bg-color="message.sent ? 'accent' : 'white'"
:text-color="message.sent ? 'white' : ''"
>
<div v-html="parsedText"></div>
</q-chat-message>
</template>

View File

@ -1,8 +1,6 @@
// composables/chat-api.ts
import { useI18n } from "vue-i18n";
import { useChatbotStore } from "src/stores/chatbot-store";
import { RouteNames } from "src/router/router-constants";
import { PageContexts } from "src/modules/chatbot/models/page-context.model";
export const useChatbotApi = () => {
const chatbot_store = useChatbotStore();

View File

@ -5,20 +5,19 @@ import { api } from "src/boot/axios";
export const chatbotService = {
// Function to send the message to the backend
sendChatMessage: async (userInput: string): Promise<Message> => {
const response = await api.post("/chat", { userInput });
const response = await api.post("/chatbot", { userInput });
return response.data as Message;
},
// Function to send context to backend
sendPageContext: async (context: chatbotPageContext): Promise<string> => {
const response = await api.post("/chat/context", context);
const response = await api.post("/chatbot/context", context);
return response.data;
},
// Function to send user credentials to the backend to communicate with n8n.
// Will have to modify later on to accomodate newer versions of User Auth/User Structure
sendUserCredentials: async (email: string, role: string): Promise<boolean> => {
const response = await api.post("/chat/user", { email, role });
const response = await api.post("/chatbot/user", { email, role });
return response.data;
},

View File

@ -128,6 +128,7 @@ export const employee_access_options: QSelectOption<UserModuleAccess>[] = [
{ label: 'timesheets', value: 'timesheets' },
{ label: 'employee_management', value: 'employee_management' },
{ label: 'timesheets_approval', value: 'timesheets_approval' },
{ label: 'chatbot', value: 'chatbot' },
]
export const employee_access_presets: Record<ModuleAccessPreset, UserModuleAccess[]> = {
@ -145,5 +146,6 @@ export const getEmployeeAccessOptionIcon = (module: UserModuleAccess): string =>
case 'personal_profile': return 'las la-id-card';
case 'timesheets': return 'punch_clock';
case 'timesheets_approval': return 'event_available';
case 'chatbot': return 'las la-robot';
}
}

View File

@ -17,6 +17,7 @@
employee_list: default_employee_list,
employee_management: default_employee_management,
timesheets_approval: default_validation_page,
chatbot: '',
};
import type { HelpModuleOptions } from 'src/modules/help/models/help-module.model';

View File

@ -134,9 +134,6 @@ export const timesheets_approval_options: HelpModuleOptions[] = [
{ label: 'help.tutorial.shared.calendar', path: calendar, description: calendar_nav_desc, icon: 'calendar_month' },
];
export const help_module_details: Options = {
dashboard: [],
personal_profile: profile_options,
@ -144,6 +141,7 @@ export const help_module_details: Options = {
employee_list: employee_list_options,
employee_management: employee_management_options,
timesheets_approval: timesheets_approval_options,
chatbot: [],
};

View File

@ -34,11 +34,6 @@ export default defineRouter(function (/* { store, ssrContext } */) {
const auth_store = useAuthStore();
const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
if (auth_store.user?.user_module_access.includes('chatbot')) {
const chatbot_store = useChatbotStore();
chatbot_store.updatePageContext(destination_page.name as RouteNames);
}
if (destination_page.meta.requires_auth && !auth_store.user || (result.status >= 400 && destination_page.name !== RouteNames.LOGIN)) {
console.error('no user account found');
return { name: 'login' };
@ -50,5 +45,14 @@ export default defineRouter(function (/* { store, ssrContext } */) {
}
})
Router.afterEach( async (destination_page) => {
const auth_store = useAuthStore();
if (auth_store.user?.user_module_access.includes('chatbot')) {
const chatbot_store = useChatbotStore();
await chatbot_store.updatePageContext(destination_page.name as RouteNames);
}
})
return Router;
});

View File

@ -1,13 +1,11 @@
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { defineStore } from "pinia";
import { chatbotService } from "src/modules/chatbot/services/chatbot.service";
import type { Message } from "src/modules/chatbot/models/dialogue-message.model";
import { RouteNames } from "src/router/router-constants";
import type { RouteNames } from "src/router/router-constants";
import { PageContexts } from "src/modules/chatbot/models/page-context.model";
export const useChatbotStore = defineStore("chatbot", () => {
const { t } = useI18n();
const messages = ref<Message[]>([]);
const has_shown_instructions = ref(false);
const is_showing_chatbot = ref(false);
@ -21,14 +19,14 @@ export const useChatbotStore = defineStore("chatbot", () => {
last_chatbot_message.text = chatbot_response.text;
last_chatbot_message.isThinking = false;
} else {
last_chatbot_message.text = t('chatbot.error.NO_REPLY_RECEIVED');
last_chatbot_message.text = 'chatbot.error.NO_REPLY_RECEIVED';
last_chatbot_message.isThinking = false;
}
}
catch (error) {
last_chatbot_message.text = t('chatbot.error.SEND_MESSAGE_FAILED');
last_chatbot_message.isThinking = false
last_chatbot_message.text = 'chatbot.error.SEND_MESSAGE_FAILED';
last_chatbot_message.isThinking = false;
console.error('error sending message: ', error);
}
};
@ -42,7 +40,7 @@ export const useChatbotStore = defineStore("chatbot", () => {
const showInstructionsOnce = () => {
if (!has_shown_instructions.value) {
messages.value.push({
text: t("chatbot.chat_initial_message"),
text: "chatbot.chat_initial_message",
sent: false,
isThinking: false,
});
@ -58,4 +56,4 @@ export const useChatbotStore = defineStore("chatbot", () => {
updatePageContext,
showInstructionsOnce,
};
});
});