refactor(chatbot): change appearance and function of chatbot window, send context on chat message instead of with each page change

This commit is contained in:
Nic D 2026-01-15 12:19:45 -05:00
parent e44e2c1e20
commit 7cf23a6a55
10 changed files with 124 additions and 97 deletions

View File

@ -3,14 +3,8 @@
setup setup
> >
import { useUiStore } from 'src/stores/ui-store'; 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 uiStore = useUiStore();
const chatbot_store = useChatbotStore();
const auth_store = useAuthStore();
</script> </script>
<template> <template>
@ -37,19 +31,6 @@
/> />
</q-btn> </q-btn>
</q-toolbar-title> </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-toolbar>
</q-header> </q-header>
</template> </template>

View File

@ -12,10 +12,13 @@
const chatbot_store = useChatbotStore(); const chatbot_store = useChatbotStore();
const text = ref(''); const text = ref('');
const is_showing_right_drawer = ref(true);
const drawer_width = ref(85);
const handleSend = async () => { const handleSend = async () => {
const message = text.value.trim(); const message = text.value.trim();
text.value = ''; text.value = '';
console.log('message: ', message, ', length: ', message.length);
await chatbot_api.sendMessage(message); await chatbot_api.sendMessage(message);
}; };
@ -26,53 +29,79 @@
<template> <template>
<q-drawer <q-drawer
v-model="chatbot_store.is_showing_chatbot" v-model="is_showing_right_drawer"
overlay overlay
elevated persistent
:width="drawer_width"
side="right" side="right"
class="column justify-end no-wrap" class="column justify-end no-wrap relative-position no-scroll"
> >
<div class="col q-px-md relative-position"> <transition
<DialogueContent class="absolute-full" /> enter-active-class="animated fadeInRight"
</div> leave-active-class="animated fadeOutRight"
mode="out-in"
<q-form @after-leave="chatbot_store.is_showing_chatbot ? drawer_width = 500 : drawer_width = 85"
submit="handleSend"
class="col-auto row"
> >
<q-input <div
v-model="text" v-if="chatbot_store.is_showing_chatbot"
borderless class="col column"
:label="$t('chatbot.chat_placeholder')" style="background: rgba(0, 0, 0, 0.7); overflow: hidden;"
:autogrow="false" >
dark <q-btn
label-color="white" dense
class="col q-px-md" icon="las la-chevron-right"
style="background: rgba(0, 0, 0, 0.3);" text-color="white"
@keyup.enter="handleSend" color="accent"
/> align="left"
size="2em"
class="q-mb-sm"
@click="chatbot_store.is_showing_chatbot = false"
/>
<!-- <q-input <div class="col q-px-md relative-position">
v-model="custId" <DialogueContent class="absolute-full" />
:label="'Customer Id'" </div>
:autogrow="false"
class="col"
style="margin-left: 8px;"
@keyup.enter="handleCustomerId"
/> -->
</q-form>
<!-- <div <q-form
class="drag-handle" submit="handleSend"
@mousedown.prevent="startDrag" class="col-auto row"
></div> --> >
<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);"
@keydown.enter="handleSend"
/>
</q-form>
</div>
<div
v-else
class="row col-auto q-pa-sm self-end"
>
<q-btn
dense
round
icon="las la-robot"
color="accent"
size="2em"
class="shadow-5"
@click="chatbot_store.is_showing_chatbot = true"
/>
</div>
</transition>
</q-drawer> </q-drawer>
</template> </template>
<style scoped> <style scoped>
:deep(.q-drawer--right) { :deep(.q-drawer) {
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0);
width: 50vw !important; overflow: hidden;
} }
</style> </style>

View File

@ -20,11 +20,11 @@
</script> </script>
<template> <template>
<q-scroll-area ref="scroll_area"> <q-scroll-area ref="scroll_area" class="no-wrap">
<div <div
v-for="(msg, index) in chatbot_store.messages" v-for="(msg, index) in chatbot_store.messages"
:key="index" :key="index"
class="column q-px-md no-wrap" class="col-auto q-px-md no-wrap"
> >
<DialoguePhrase <DialoguePhrase
v-if="!msg.isThinking" v-if="!msg.isThinking"

View File

@ -17,7 +17,8 @@
const message_text = computed(() => message.text.includes('chatbot.') ? t(message.text) : message.text) const message_text = computed(() => message.text.includes('chatbot.') ? t(message.text) : message.text)
const is_error = computed(() => message.text.includes('NO_REPLY_RECEIVED') || message.text.includes('NO_REPLY_RECEIVED')); const is_error = computed(() => message.text.includes('NO_REPLY_RECEIVED') || message.text.includes('SEND_MESSAGE_FAILED'));
// Initialize Markdown parser // Initialize Markdown parser
// const md = new MarkdownIt({ // const md = new MarkdownIt({
// // breaks: false, // Support line breaks // // breaks: false, // Support line breaks
@ -48,6 +49,24 @@
<template> <template>
<q-chat-message <q-chat-message
v-if="is_error"
name="SystemBot"
bg-color="negative"
text-color="white"
>
<div class="row flex-center">
<q-icon
name="las la-exclamation-triangle"
class="col-auto q-pr-xs"
size="2em"
color="white"
/>
<span>{{ message_text }}</span>
</div>
</q-chat-message>
<q-chat-message
v-else
:sent="message.sent" :sent="message.sent"
:name="message.sent ? auth_store.user?.first_name : 'TargoBot'" :name="message.sent ? auth_store.user?.first_name : 'TargoBot'"
:bg-color="message.sent ? 'accent' : 'info'" :bg-color="message.sent ? 'accent' : 'info'"

View File

@ -1,13 +1,13 @@
import { RouteNames } from "src/router/router-constants"; import { RouteNames } from "src/router/router-constants";
export interface chatbotPageContext { export interface ChatbotPageContext {
name: string; name: string;
description: string; description: string;
features: string[]; features: string[];
path: RouteNames | null; path: RouteNames | null;
} }
export const dashboardContext: chatbotPageContext = { export const dashboardContext: ChatbotPageContext = {
name: "Dashboard", 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", 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: [ features: [
@ -18,7 +18,7 @@ export const dashboardContext: chatbotPageContext = {
path: RouteNames.DASHBOARD, path: RouteNames.DASHBOARD,
}; };
export const leftDrawerContext: chatbotPageContext = { export const leftDrawerContext: ChatbotPageContext = {
name: "Left Drawer", 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", 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: [ features: [
@ -33,7 +33,7 @@ export const leftDrawerContext: chatbotPageContext = {
path: null, path: null,
}; };
export const profileContext: chatbotPageContext = { export const profileContext: ChatbotPageContext = {
name: "Profile", name: "Profile",
description: "Display and edit user information", description: "Display and edit user information",
features: [ features: [
@ -44,7 +44,7 @@ export const profileContext: chatbotPageContext = {
path: RouteNames.PROFILE, path: RouteNames.PROFILE,
}; };
export const employeeListContext: chatbotPageContext = { export const employeeListContext: ChatbotPageContext = {
name: "Employee List", name: "Employee List",
description: "View all the hired and currently active Staff", description: "View all the hired and currently active Staff",
features: [ features: [
@ -57,7 +57,7 @@ export const employeeListContext: chatbotPageContext = {
path: RouteNames.EMPLOYEE_LIST, path: RouteNames.EMPLOYEE_LIST,
}; };
export const timesheetApprovalContext: chatbotPageContext = { export const timesheetApprovalContext: ChatbotPageContext = {
name: "Timesheet Approval", name: "Timesheet Approval",
description: "Page where employee hours and shifts are approved for accounting purposes. Requires timesheet_approval module access.", description: "Page where employee hours and shifts are approved for accounting purposes. Requires timesheet_approval module access.",
features: [ features: [
@ -71,7 +71,7 @@ export const timesheetApprovalContext: chatbotPageContext = {
path: RouteNames.TIMESHEET_APPROVALS, path: RouteNames.TIMESHEET_APPROVALS,
}; };
export const timesheetContext: chatbotPageContext = { export const timesheetContext: ChatbotPageContext = {
name: "Timesheet", name: "Timesheet",
description: description:
"Page where an employee can enter their hours for their day and week. Display in 2-week pay periods.", "Page where an employee can enter their hours for their day and week. Display in 2-week pay periods.",
@ -86,7 +86,7 @@ export const timesheetContext: chatbotPageContext = {
path: RouteNames.TIMESHEET, path: RouteNames.TIMESHEET,
}; };
export const helpContext: chatbotPageContext = { export const helpContext: ChatbotPageContext = {
name: "Help", name: "Help",
description: "page containing common Q&A segments regarding website functionalities", description: "page containing common Q&A segments regarding website functionalities",
features: [ features: [
@ -98,7 +98,7 @@ export const helpContext: chatbotPageContext = {
path: RouteNames.HELP, path: RouteNames.HELP,
} }
export const PageContexts: Record<RouteNames, chatbotPageContext | null> = { export const PageContexts: Record<RouteNames, ChatbotPageContext | null> = {
"login": null, "login": null,
"login-success": null, "login-success": null,
"error": null, "error": null,

View File

@ -1,28 +1,28 @@
import type { chatbotPageContext } from "src/modules/chatbot/models/page-context.model"; import type { ChatbotPageContext } from "src/modules/chatbot/models/page-context.model";
import type { Message } from "src/modules/chatbot/models/dialogue-message.model"; import type { Message } from "src/modules/chatbot/models/dialogue-message.model";
import { api } from "src/boot/axios"; import { api } from "src/boot/axios";
export const chatbotService = { export const chatbotService = {
// Function to send the message to the backend // Function to send the message to the backend
sendChatMessage: async (userInput: string): Promise<Message> => { sendChatMessage: async (userInput: string, pageContext: ChatbotPageContext | undefined): Promise<Message> => {
const response = await api.post("/chatbot", { userInput }); const response = await api.post("/chatbot", { userInput, pageContext });
return response.data as Message; return response.data as Message;
}, },
// Function to send context to backend // // Function to send context to backend
sendPageContext: async (context: chatbotPageContext): Promise<string> => { // sendPageContext: async (context: chatbotPageContext): Promise<string> => {
const response = await api.post("/chatbot/context", context); // const response = await api.post("/chatbot/context", context);
return response.data; // return response.data;
}, // },
// Function to send user credentials to the backend to communicate with n8n. // // Function to send user credentials to the backend to communicate with n8n.
sendUserCredentials: async (email: string, role: string): Promise<boolean> => { // sendUserCredentials: async (email: string, role: string): Promise<boolean> => {
const response = await api.post("/chatbot/user", { email, role }); // const response = await api.post("/chatbot/user", { email, role });
return response.data; // return response.data;
}, // },
retrieveCustomerDiagnostics: async (id: string): Promise<string> => { // retrieveCustomerDiagnostics: async (id: string): Promise<string> => {
const response = await api.get(`/chat/customer/${id}`); // const response = await api.get(`/chat/customer/${id}`);
return response.data; // return response.data;
}, // },
}; };

View File

@ -14,7 +14,6 @@
onMounted(() => { onMounted(() => {
Object.values(overview_column_names).map(column => column_options.value.push({ label: `timesheet_approvals.table.${column}`, value: column as OverviewColumns })) Object.values(overview_column_names).map(column => column_options.value.push({ label: `timesheet_approvals.table.${column}`, value: column as OverviewColumns }))
column_options.value = column_options.value.filter(column => !EXCLUDED_COLUMNS.includes(column.value)); column_options.value = column_options.value.filter(column => !EXCLUDED_COLUMNS.includes(column.value));
console.log('filter column values: ', column_options.value )
}) })
</script> </script>

View File

@ -30,7 +30,7 @@
const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved)) const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved))
const total_hours = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) => const total_hours = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
sum + timesheet.weekly_hours.regular sum += timesheet.weekly_hours.regular
+ timesheet.weekly_hours.evening + timesheet.weekly_hours.evening
+ timesheet.weekly_hours.emergency + timesheet.weekly_hours.emergency
+ timesheet.weekly_hours.overtime, + timesheet.weekly_hours.overtime,

View File

@ -2,19 +2,20 @@ import { ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { chatbotService } from "src/modules/chatbot/services/chatbot.service"; import { chatbotService } from "src/modules/chatbot/services/chatbot.service";
import type { Message } from "src/modules/chatbot/models/dialogue-message.model"; import type { Message } from "src/modules/chatbot/models/dialogue-message.model";
import { type ChatbotPageContext, PageContexts } from "src/modules/chatbot/models/page-context.model";
import type { 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", () => { export const useChatbotStore = defineStore("chatbot", () => {
const messages = ref<Message[]>([]); const messages = ref<Message[]>([]);
const has_shown_instructions = ref(false); const has_shown_instructions = ref(false);
const is_showing_chatbot = ref(false); const is_showing_chatbot = ref(false);
const current_page_context = ref<ChatbotPageContext | undefined>(undefined);
const sendChatMessage = async (user_message: string) => { const sendChatMessage = async (user_message: string) => {
const last_chatbot_message = messages.value.at(messages.value.length - 1)!; const last_chatbot_message = messages.value.at(messages.value.length - 1)!;
try { try {
const chatbot_response = await chatbotService.sendChatMessage(user_message); const chatbot_response = await chatbotService.sendChatMessage(user_message, current_page_context.value);
if (chatbot_response) { if (chatbot_response) {
last_chatbot_message.text = chatbot_response.text; last_chatbot_message.text = chatbot_response.text;
last_chatbot_message.isThinking = false; last_chatbot_message.isThinking = false;
@ -30,11 +31,9 @@ export const useChatbotStore = defineStore("chatbot", () => {
} }
}; };
const updatePageContext = async (page_name: RouteNames) => { const updatePageContext = (page_name: RouteNames) => {
const chatbot_page_context = PageContexts[page_name]; current_page_context.value = PageContexts[page_name] ?? undefined;
if (chatbot_page_context)
await chatbotService.sendPageContext(chatbot_page_context);
}; };
const showInstructionsOnce = () => { const showInstructionsOnce = () => {