Merge pull request 'dev/lion/chatbot' (#48) from dev/lion/chatbot into main

Reviewed-on: Targo/targo_frontend#48
This commit is contained in:
Nicolas 2026-01-12 14:13:42 -05:00
commit ff25cf5082
23 changed files with 570 additions and 14 deletions

75
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@quasar/extras": "^1.17.0",
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"markdown-it": "^14.1.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"quasar": "^2.18.2",
@ -24,6 +25,7 @@
"@eslint/js": "^9.14.0",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@quasar/app-vite": "^2.1.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.5.9",
"@vue/eslint-config-typescript": "^14.4.0",
"@vue/test-utils": "^2.4.6",
@ -1900,6 +1902,31 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -2900,7 +2927,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-flatten": {
@ -6540,6 +6566,15 @@
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lint-staged": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz",
@ -7057,6 +7092,23 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -7066,6 +7118,12 @@
"node": ">= 0.4"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -7971,6 +8029,15 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -9634,6 +9701,12 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",

View File

@ -17,6 +17,7 @@
"@quasar/extras": "^1.17.0",
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"markdown-it": "^14.1.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"quasar": "^2.18.2",
@ -29,6 +30,7 @@
"@eslint/js": "^9.14.0",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@quasar/app-vite": "^2.1.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.5.9",
"@vue/eslint-config-typescript": "^14.4.0",
"@vue/test-utils": "^2.4.6",

View File

@ -1,4 +1,15 @@
export default {
chatbot: {
chat_header: "AI Assistant",
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: {
carousel: {
welcome_title: "Welcome to the new Targo Application!",
@ -8,6 +19,7 @@ export default {
},
useful_links: "useful links",
},
help: {
label: "Centre d'aide",
tutorial: {
@ -59,6 +71,7 @@ export default {
},
},
},
employee_list: {
page_header: "Employee Directory",
table: {
@ -110,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",
@ -390,6 +404,7 @@ export default {
unapprove: "remove approval",
},
},
descriptions: {
dashboard: {
menu: "To access the main menu, click the button located in the upper-left corner. This menu allows you to navigate through the entire application.",

View File

@ -1,4 +1,15 @@
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: "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",
SEND_MESSAGE_FAILED: "Une erreur est survenu lors de l'envoi de votre message",
},
},
dashboard: {
carousel: {
welcome_title: "Bienvenue dans la nouvelle application Targo!",
@ -8,6 +19,7 @@ export default {
},
useful_links: "liens utiles",
},
help: {
label: "Centre d'aide",
tutorial: {
@ -59,6 +71,7 @@ export default {
},
},
},
employee_list: {
page_header: "Répertoire du personnel",
table: {
@ -110,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",
@ -200,7 +214,6 @@ export default {
tab_title: "horaire",
selected_schedule: "Horaire Sélectionné",
new_preset: "Construire un nouvel horaire",
},
errors: {
must_enter_birthdate: "Vous devez entrer une date de naissance valide",
@ -391,6 +404,7 @@ export default {
unapprove: "enlever status approuvé",
},
},
descriptions: {
dashboard: {
menu: "Pour accéder au menu principal, cliquez sur le bouton situé dans le coin supérieur gauche. Ce menu vous permet de naviguer à travers l'ensemble de l'application.",
@ -462,5 +476,4 @@ export default {
calendar: "Le calendrier facilite la navigation. Il vous permet de sélectionner une date précise et d'afficher la période de paie qui inclut cette date.",
},
},
};

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

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

@ -0,0 +1,47 @@
<script
setup
lang="ts"
>
import DialoguePhrase from './dialogue-phrase.vue';
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-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"
:message="msg"
/>
<!-- thinking bubble while awaiting chatbot reply -->
<div
v-else
class="row flex-center q-px-md q-py-sm"
>
<q-spinner-dots
size="20px"
color="primary"
/>
<span class="q-ml-sm text-grey-7">{{ $t('chatbot.chat_thinking') }}</span>
</div>
</div>
</q-scroll-area>
</template>

View File

@ -0,0 +1,50 @@
<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 {t}
const auth_store = useAuthStore();
const { message } = defineProps<{
message: Message;
}>();
// Initialize Markdown parser
const md = new MarkdownIt({
breaks: false, // Support line breaks
linkify: true, // Make URLs clickable
html: false // Prevent raw HTML injection
})
// Removes all the h1,h2,h3 to make the Md more user friendly
const cleanMarkdown = (markdown: string): string => {
if (!markdown) return ''
return markdown
.replace(/\r\n/g, '\n') // normalize Windows line endings
.replace(/([.!?])(\s+)(?=[A-Z])/g, '$1\n') // insert line break after sentence-ending punctuation
.replace(/\n{3,}/g, '\n\n') // squash triple+ line breaks
.replace(/^#{1,3}\s(.*)$/gm, '#### $1') // downgrade headings
}
// Compute parsed content
const parsedText = computed((): string => {
const cleaned = cleanMarkdown(message.text || '')
return md.render(cleaned);
})
</script>
<template>
<q-chat-message
:sent="message.sent"
:name="message.sent ? auth_store.user?.first_name : 'TargoBot'"
:bg-color="message.sent ? 'accent' : 'white'"
:text-color="message.sent ? 'white' : ''"
>
<div v-html="parsedText"></div>
</q-chat-message>
</template>

View File

@ -0,0 +1,30 @@
// composables/chat-api.ts
import { useI18n } from "vue-i18n";
import { useChatbotStore } from "src/stores/chatbot-store";
export const useChatbotApi = () => {
const chatbot_store = useChatbotStore();
const { t } = useI18n();
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,
};
};

View File

@ -0,0 +1,5 @@
export interface Message {
text: string;
sent: boolean;
isThinking: boolean;
}

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,15 @@
<script setup lang="ts">
import ConversationBox from "../components/conversation-box.vue";
</script>
<template>
<div>
<ConversationBox></ConversationBox>
</div>
</template>
<style scoped>
.box-settings {
position: relative;
}
</style>

View File

@ -0,0 +1,28 @@
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("/chatbot", { userInput });
return response.data as Message;
},
// Function to send context to backend
sendPageContext: async (context: chatbotPageContext): Promise<string> => {
const response = await api.post("/chatbot/context", context);
return response.data;
},
// Function to send user credentials to the backend to communicate with n8n.
sendUserCredentials: async (email: string, role: string): Promise<boolean> => {
const response = await api.post("/chatbot/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

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

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

@ -10,7 +10,6 @@
const { user } = useAuthStore();
</script>
<template>
<q-page
class="column bg-secondary items-center"

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';
/*
@ -44,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

@ -0,0 +1,59 @@
import { ref } from "vue";
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 type { RouteNames } from "src/router/router-constants";
import { PageContexts } from "src/modules/chatbot/models/page-context.model";
export const useChatbotStore = defineStore("chatbot", () => {
const messages = ref<Message[]>([]);
const has_shown_instructions = ref(false);
const is_showing_chatbot = ref(false);
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 = 'chatbot.error.NO_REPLY_RECEIVED';
last_chatbot_message.isThinking = false;
}
}
catch (error) {
last_chatbot_message.text = 'chatbot.error.SEND_MESSAGE_FAILED';
last_chatbot_message.isThinking = false;
console.error('error sending message: ', error);
}
};
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 = () => {
if (!has_shown_instructions.value) {
messages.value.push({
text: "chatbot.chat_initial_message",
sent: false,
isThinking: false,
});
has_shown_instructions.value = true;
}
};
return {
messages,
has_shown_instructions,
is_showing_chatbot,
sendChatMessage,
updatePageContext,
showInstructionsOnce,
};
});