fix(chatbot): continue integration and trim of chatbot-related code.

This commit is contained in:
Nicolas Drolet 2026-01-12 10:48:11 -05:00
parent ea5e2ef36e
commit 5ff20452a7
20 changed files with 352 additions and 418 deletions

View File

@ -4,6 +4,10 @@ export default {
chat_initial_message: "Welcome to your technical assistant.\nPlease provide the Customer ID to get a diagnostic report",
chat_placeholder: "Enter a Message",
chat_thinking: "Thinking...",
error: {
NO_REPLY_RECEIVED: "encountered an error while waiting for chatbot to reply",
SEND_MESSAGE_FAILED: "unable to send message to chatbot",
},
},
dashboard: {

View File

@ -4,6 +4,10 @@ export default {
chat_initial_message: "Bienvenue à votre assistant technique.\nVeuillez fournir le ID du Client pour avoir un diagnostique",
chat_placeholder: "Entré 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",
SEND_MESSAGE_FAILED: "Une erreur est survenu lors de l'envoi de votre message",
},
},
dashboard: {

View File

@ -4,131 +4,71 @@
>
import DialogueContent from "./dialogue-content.vue";
import { computed, onMounted, ref, watch } from "vue";
import { useChatbotApi } from "src/modules/chatbot/composables/chatbot-api";
import { chatbotService } from "src/modules/chatbot/services/messages.service";
import { pageContexts } from "src/page-contexts";
import type { contextObject } from "src/page-contexts/pages/types/context-object";
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 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)
// Block to handle the incomming and sending of the messages from the user and the ai
const text = ref('');
const chatbot_api = useChatbotApi();
const chatbot_store = useChatbotStore();
const chatbot_api = useChatApi();
const chatStore = useChatStore();
onMounted(() => {
chatStore.showInstructionsOnce();
})
const isSessionReady = ref(false);
const text = ref('');
const handleSend = async () => {
const userMessage = text.value.trim();
await chatbot_api.sendMessage(text.value.trim());
text.value = '';
messages.push({
text: userMessage,
sent: true,
isThinking: false,
})
const thinkingMessage = {
text: "Thinking...",
sent: false,
isThinking: true
}
messages.push(thinkingMessage)
try {
const aiResponse = await sendMessage(userMessage);
const index = messages.indexOf(thinkingMessage);
if (index !== -1) {
messages.splice(index, 1, {
text: aiResponse.text,
sent: false,
isThinking: false,
})
}
}
catch (error) {
const index = messages.indexOf(thinkingMessage);
if (index !== -1) {
messages.splice(index, 1, {
text: "Sorry, Message wasn't able to sent.",
sent: false,
isThinking: false,
});
}
throw error;
}
};
// Block to receive user from child
const userEmail = ref('')
const userRole = ref('')
const HandleEmail = (email: string) => {
userEmail.value = email;
}
const HandleRole = (role: string) => {
userRole.value = role;
}
// Block to handle sending the url to n8n
const route = useRoute()
const sendPageContext = chatbotService.sendPageContext;
// Capture and send page context to n8n
const currentContext = computed(() =>
pageContexts.find(ctx => ctx.path === route.fullPath.replace('/', ''))
)
// 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;
// // 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
}
// 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),
]);
isSessionReady.value = true;
} catch (err) {
console.error("Error", err)
}
},
{ immediate: true }
);
// 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 () => {
@ -136,31 +76,19 @@ import { useChatbotStore } from "src/stores/chatbot-store";
// custId.value = '';
// await chatbotService.retrieveCustomerDiagnostics(cId);
// }
</script>
<template>
<q-drawer
side="right"
v-model="isChatVisible"
class="chat-drawer"
style="box-shadow: -4px 0 12px rgba(0,0,0,0.15);"
:width="drawerWidth"
v-model="chatbot_store.is_showing_chatbot"
class="column justify-end"
>
<div
class="chat-header"
style="text-align:start; padding: 10px 10px 0px;"
>{{ $t('chatbot.chat_header') }}</div>
<div class="bg-primary text-uppercase">{{ $t('chatbot.chat_header') }}</div>
<div class="line-separator"></div>
<div class="chat-body">
<DialogueContent
@sendRole="HandleRole"
@sendEmail="HandleEmail"
:messages="messages"
/>
<DialogueContent />
</div>
<div class="line-separator"></div>
@ -176,6 +104,7 @@ import { useChatbotStore } from "src/stores/chatbot-store";
style="margin-left: 8px;"
@keyup.enter="handleSend"
/>
<!-- <q-input
v-model="custId"
:label="'Customer Id'"
@ -185,14 +114,15 @@ import { useChatbotStore } from "src/stores/chatbot-store";
@keyup.enter="handleCustomerId"
/> -->
</q-form>
<div
<!-- <div
class="drag-handle"
@mousedown.prevent="startDrag"
></div>
></div> -->
</q-drawer>
</template>
<style scoped>
<!-- <style scoped>
.drag-handle {
position: absolute;
top: 0;
@ -203,32 +133,4 @@ import { useChatbotStore } from "src/stores/chatbot-store";
z-index: 1000;
background-color: rgba(0, 0, 0, 0);
}
.line-separator {
height: 1px;
background-color: #ccc;
margin: 10px 0;
}
.chat-drawer {
display: flex;
flex-direction: column;
height: 100%;
}
/* Header at the top */
.chat-header {
color: #019547;
padding: 10px;
font-weight: bold;
flex-shrink: 0;
}
/* Scrollable middle */
.chat-body {
flex: 1;
padding: 10px;
overflow-y: auto;
height: 81%;
}
</style>
</style> -->

View File

@ -1,52 +1,30 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import DialoguePhrase from './dialogue-phrase.vue';
import type { Message } from '../types/dialogue-message';
import { useAuthStore } from 'src/stores/auth-store';
import { watch, nextTick, onMounted } from 'vue';
import { useChatbotStore } from 'src/stores/chatbot-store';
const authStore = useAuthStore();
const currentUser = authStore.user;
const emitUser = defineEmits(['sendRole', 'sendEmail']);
const sendUser = () => {
emitUser('sendEmail', currentUser.email)
emitUser('sendRole', currentUser.role)
}
const chatbot_store = useChatbotStore();
onMounted(() => {
sendUser();
})
const props = defineProps<{
messages: Message[];
}>();
watch(props.messages, async () => {
await nextTick(() => {
const chatBody = document.querySelector('.chat-body');
if (chatBody) chatBody.scrollTop = chatBody.scrollHeight;
});
});
</script>
<template>
<q-infinite-scroll>
<template
v-for="(msg, index) in messages"
v-for="(msg, index) in chatbot_store.messages"
:key="index"
>
<DialoguePhrase
v-if="!msg.isThinking"
:text="msg.text"
:sent="msg.sent"
:name="msg.sent ? currentUser.firstName :
$t('chatbot.chat_header')"
:class="['chat-message', msg.sent ? 'user' : 'ai']"
:message="msg"
/>
<!-- 💭 Thinking bubble -->
<!-- thinking bubble while awaiting chatbot reply -->
<div
v-else
class="chat-message ai flex items-center q-px-md q-py-sm"
class="row flex-center q-px-md q-py-sm"
>
<q-spinner-dots
size="20px"

View File

@ -1,10 +1,16 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import { computed } from 'vue';
import MarkdownIt from 'markdown-it'
import { useAuthStore } from 'src/stores/auth-store';
import type { Message } from 'src/modules/chatbot/models/dialogue-message.model';
const props = defineProps<{
text: string;
sent: boolean;
const auth_store = useAuthStore();
const { message } = defineProps<{
message: Message;
}>();
// Initialize Markdown parser
@ -26,38 +32,16 @@ const cleanMarkdown = (markdown: string): string => {
// Compute parsed content
const parsedText = computed((): string => {
const cleaned = cleanMarkdown(props.text || '')
const cleaned = cleanMarkdown(message.text || '')
return md.render(cleaned)
})
</script>
<template>
<q-chat-message
:sent="props.sent"
:text="[text]"
:bg-color="!sent ? 'secondary' : 'accent'"
class="bubble"
:class="sent ? 'user' : 'ai'"
>
<div v-html="parsedText"></div>
</q-chat-message>
:sent="message.sent"
:text="[parsedText]"
:name="message.sent ? auth_store.user?.first_name : 'TargoBot'"
:bg-color="message.sent ? 'accent' : 'secondary'"
/>
</template>
<style scoped>
.bubble {
margin: 8px;
border-radius: 12px;
line-height: 1.5;
word-break: break-word;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.user {
align-self: flex-end;
}
.ai {
align-self: flex-start;
}
</style>

View File

@ -1,13 +1,32 @@
// composables/chat-api.ts
import { useI18n } from "vue-i18n";
import { useChatbotStore } from "src/stores/chatbot-store";
import type { Message } from "src/modules/chatbot/models/dialogue-message.model";
import { RouteNames } from "src/router/router-constants";
import { PageContexts } from "src/modules/chatbot/models/page-context.model";
export const useChatbotApi = () => {
const chatStore = useChatbotStore();
const chatbot_store = useChatbotStore();
const { t } = useI18n();
const sendMessage = async (text: string): Promise<Message> => {
return await chatStore.sendMessage(text);
const sendMessage = async (user_message: string) => {
// push user input
chatbot_store.messages.push({
text: user_message,
sent: true,
isThinking: false,
})
// automatically push chatbot pending reply
chatbot_store.messages.push({
text: t('chatbot.chat_thinking'),
sent: false,
isThinking: true
})
await chatbot_store.sendChatMessage(user_message);
};
return { sendMessage };
return {
sendMessage,
};
};

View File

@ -0,0 +1,111 @@
import { RouteNames } from "src/router/router-constants";
export interface chatbotPageContext {
name: string;
description: string;
features: string[];
path: RouteNames | null;
}
export const dashboardContext: chatbotPageContext = {
name: "Dashboard",
description: "Landing page containing useful links and a carousel showcasing recent news, as well as a local weather widget in the top right corner",
features: [
"Used as a landing platform and navigate the left drawer",
"Access the AI chatbot from the header",
"Access common external tools using the links below the news Carousel",
],
path: RouteNames.DASHBOARD,
};
export const leftDrawerContext: chatbotPageContext = {
name: "Left Drawer",
description: "A drawer that acts as a navigation bar, routes to different parts of the website. Some icons will be hidden according to user access",
features: [
"The user can navigate to the home page.",
"Can review and approve timesheets. requires access to timesheet_approval module.",
"By default, the user can see the full list of employees. Requires user_management module access to edit, add, or view detailed profiles.",
"Can access the timesheet interface to input employee hours.",
"Can access your user profile to view information or change your UI preferences (light/dark mode and display language)",
"Can access the Help page to view common Q&A regarding different modules",
"Can logout",
],
path: null,
};
export const profileContext: chatbotPageContext = {
name: "Profile",
description: "Display and edit user information",
features: [
"View personal information such as first and last name, phone, birthdate and address.",
"View Career information such as job title, company, supervisor, email and hiring date.",
"Edit available preferences such as Display options of light and dark mode, as well as display language",
],
path: RouteNames.PROFILE,
};
export const employeeListContext: chatbotPageContext = {
name: "Employee List",
description: "View all the hired and currently active Staff",
features: [
"View the list of hired and active employees",
"Access an individual employee's detailed profile if user has user_management module access",
"Add, edit, or remove an employee if user has user_management module access",
"Add, edit, or remove module access for any given employee if user has user_managmenet module access",
"Add, edit, or remove schedule presets and assign those presets to employees. These presets can then be applied to the employee's timesheet with a single click. Requires user_management module access."
],
path: RouteNames.EMPLOYEE_LIST,
};
export const timesheetApprovalContext: chatbotPageContext = {
name: "Timesheet Approval",
description: "Page where employee hours and shifts are approved for accounting purposes. Requires timesheet_approval module access.",
features: [
"See a grid or list view of total hours for all employees in the given 2-week pay period.",
"Access different periods thanks to the pay period navigator buttons (previous, next, date picker)",
"Approve the hours for the cards displayed.",
"Open a detailed dialog window for any employee",
"The detailed dialog window includes bar and circle charts to visually see employee hours worked daily, the type of shifts worked, and expenses accrued",
"The detailed dialog window allows the user to edit the any employee's timesheets or expenses",
],
path: RouteNames.TIMESHEET_APPROVALS,
};
export const timesheetContext: chatbotPageContext = {
name: "Timesheet",
description:
"Page where an employee can enter their hours for their day and week. Display in 2-week pay periods.",
features: [
"Enter your in and out times per day",
"Add and edit what kind of hours your shift was example regular, overtime, vacation etc.",
"Edit your own shift hours",
"Delete your shift hours",
"List your expenses for the week",
"Add expenses for the week, along with attached files for said expenses",
],
path: RouteNames.TIMESHEET,
};
export const helpContext: chatbotPageContext = {
name: "Help",
description: "page containing common Q&A segments regarding website functionalities",
features: [
"Browse by module for common questions and answers",
"The modules displayed will only be those the user has access to.",
"Each module consists of a series of Expanding Items. Clicking on a question item reveals the answer beneath.",
"Each Expanding Item also has its own support image that appears next to the items.",
],
path: RouteNames.HELP,
}
export const PageContexts: Record<RouteNames, chatbotPageContext | null> = {
"login": null,
"login-success": null,
"error": null,
"/": dashboardContext,
"employees": employeeListContext,
"profile": profileContext,
"timesheet": timesheetContext,
"timesheet-approvals": timesheetApprovalContext,
"help": helpContext,
}

View File

@ -0,0 +1,29 @@
import type { chatbotPageContext } from "src/modules/chatbot/models/page-context.model";
import type { Message } from "src/modules/chatbot/models/dialogue-message.model";
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 });
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);
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 });
return response.data;
},
retrieveCustomerDiagnostics: async (id: string): Promise<string> => {
const response = await api.get(`/chat/customer/${id}`);
return response.data;
},
};

View File

@ -1,33 +0,0 @@
import type { contextObject } from "src/page-contexts/pages/types/context-object";
import type { Message } from "../types/dialogue-message";
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/respond", { userInput });
return response.data as Message;
},
// Function to send context to backend
sendPageContext: async (context: contextObject): Promise<string> => {
console.log(context.path, " ", context.features);
const response = await api.post("/chat/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 });
return response.data;
},
retrieveCustomerDiagnostics: async (id: string): Promise<string> => {
const response = await api.get(`/chat/customer/${id}`);
return response.data;
},
};

View File

@ -15,6 +15,7 @@ export const ModuleNames = {
PERSONAL_PROFILE: 'personal_profile',
TIMESHEETS: 'timesheets',
TIMESHEETS_APPROVAL: 'timesheets_approval',
CHATBOT: 'chatbot',
} as const;
export type UserModuleAccess = typeof ModuleNames[keyof typeof ModuleNames];

View File

@ -1,16 +0,0 @@
import { dashboardContext } from "./pages/dashboard-context";
import { leftDrawerContext } from "./pages/left-drawer-context";
import { profileContext } from "./pages/profile-context";
import { supervisorContext } from "./pages/supervisor-crew-context";
import { timesheetApprovalContext } from "./pages/timesheet-approval-context";
import { timesheetContext } from "./pages/timesheet-context";
import type { contextObject } from "./pages/types/context-object";
export const pageContexts: contextObject[] = [
dashboardContext,
leftDrawerContext,
profileContext,
supervisorContext,
timesheetApprovalContext,
timesheetContext,
];

View File

@ -1,11 +0,0 @@
export const dashboardContext = {
name: "Test-Page",
description:
"Temporary static landing page while the application is still in development",
features: [
"Used as a landing platform and navigate the left drawer",
"Access the ai chatbot from the header",
"See your user icon with a notification icon",
],
path: "",
};

View File

@ -1,15 +0,0 @@
export const leftDrawerContext = {
name: "Left Drawer",
description:
"A drawer that acts as a navigation bar, able to surf to differnent parts of the website.",
features: [
"The user can navigate to the home page.",
"Depending on their role and clearence can approve timesheets. ",
"Depending on their role, can see the full list of employees.",
"Can access the timesheet interface to input employee hours.",
"Can acess your user profile to add or change information",
"Can access the Help page to ask for assistance.",
"Can logout",
],
path: "none",
};

View File

@ -1,10 +0,0 @@
export const profileContext = {
name: "Profile",
description: "Display and edit user information",
features: [
"Add and edit Personal information such as first and last name, phone, birthdate and address.",
"Add and edit Career information such job title, company, supervisor, email and hiring date.",
"Edit available preferences such as Display options of light and dark mode, and language options",
],
path: "user/profile",
};

View File

@ -1,9 +0,0 @@
export const supervisorContext = {
name: "Supervisor Crew Page",
description: "View all the hired Staff",
features: [
"View the list of hired employees",
"Access an individual employee",
],
path: "employees",
};

View File

@ -1,15 +0,0 @@
export const timesheetApprovalContext = {
name: "Timesheet-Approval",
description:
"Page where employee hours and shifts are approved by the HR deparment",
features: [
"See a list of cards with the total hours for the week for each employee.",
"Access different weeks thanks to the calender button",
"Approve the hours for the cards displayed.",
"Open a detailed modal for each card",
"Display a bar chart within the modal to display the type of hours, expenses, and mileage.",
"Edit the hours, and their type such as regular, holiday, vacation etc.",
"Add and edit expenses for the week, along with attached files for said expenses",
],
path: "timesheet-approvals",
};

View File

@ -1,14 +0,0 @@
export const timesheetContext = {
name: "Timesheet-page",
description:
"Page where an employee can enter their hours for their day and week",
features: [
"Enter your in and out times per day",
"Add and edit what kind of hours your shift was example regular, overtime, vacation etc.",
"Edit your own shift hours",
"Delete your shift hours",
"List your expenses for the week",
"Add expenses for the week, along with attached files for said expenses",
],
path: "timesheet-temp",
};

View File

@ -1,6 +0,0 @@
export interface contextObject {
name: string;
description: string;
features: string[];
path: string;
}

View File

@ -3,6 +3,7 @@ import { createMemoryHistory, createRouter, createWebHashHistory, createWebHisto
import routes from './routes';
import { useAuthStore } from 'src/stores/auth-store';
import { RouteNames } from 'src/router/router-constants';
import { useChatbotStore } from 'src/stores/chatbot-store';
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
/*
@ -33,6 +34,11 @@ 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' };

View File

@ -1,18 +1,42 @@
import type { Message } from "src/modules/chatbot/models/dialogue-message.model";
import { defineStore } from "pinia";
import { chatbotService } from "src/modules/chatbot/services/messages.service";
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 { 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);
const { t } = useI18n();
const sendMessage = async (userInput: string) => {
const reply = await chatbotService.sendChatMessage(userInput);
return reply;
const sendChatMessage = async (user_message: string) => {
const last_chatbot_message = messages.value.at(messages.value.length - 1)!;
try {
const chatbot_response = await chatbotService.sendChatMessage(user_message);
if (chatbot_response) {
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.isThinking = false;
}
}
catch (error) {
last_chatbot_message.text = t('chatbot.error.SEND_MESSAGE_FAILED');
last_chatbot_message.isThinking = false
}
};
const updatePageContext = async (page_name: RouteNames) => {
const chatbot_page_context = PageContexts[page_name];
if (chatbot_page_context)
await chatbotService.sendPageContext(chatbot_page_context);
};
const showInstructionsOnce = () => {
@ -30,7 +54,8 @@ export const useChatbotStore = defineStore("chatbot", () => {
messages,
has_shown_instructions,
is_showing_chatbot,
sendMessage,
sendChatMessage,
updatePageContext,
showInstructionsOnce,
};
});