diff --git a/package-lock.json b/package-lock.json index a9575ea..3aefaad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3d535b7..6ffffb1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/i18n/en-ca/index.ts b/src/i18n/en-ca/index.ts index f344a5b..df05cdc 100644 --- a/src/i18n/en-ca/index.ts +++ b/src/i18n/en-ca/index.ts @@ -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", @@ -127,7 +141,7 @@ export default { }, }, - error :{ + error: { not_found_header: "page not found", not_found_description: "You may have entered the wrong URL, or you may not have access to this page", go_back: "go back", @@ -369,7 +383,7 @@ export default { verified: "approved", unverified: "pending", inactive: "inactive", - regular: "regular", + regular: "regular", evening: "evening", emergency: "emergency", overtime: "overtime", @@ -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.", diff --git a/src/i18n/fr-ca/index.ts b/src/i18n/fr-ca/index.ts index e197001..1672ca9 100644 --- a/src/i18n/fr-ca/index.ts +++ b/src/i18n/fr-ca/index.ts @@ -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.", }, }, - }; \ No newline at end of file diff --git a/src/layouts/components/main-layout-footer-bar.vue b/src/layouts/components/main-layout-footer-bar.vue index 80cc340..722af4b 100644 --- a/src/layouts/components/main-layout-footer-bar.vue +++ b/src/layouts/components/main-layout-footer-bar.vue @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/layouts/components/main-layout-header-bar.vue b/src/layouts/components/main-layout-header-bar.vue index 31d67da..01af21c 100644 --- a/src/layouts/components/main-layout-header-bar.vue +++ b/src/layouts/components/main-layout-header-bar.vue @@ -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(); \ No newline at end of file + diff --git a/src/modules/chatbot/components/chatbot-drawer.vue b/src/modules/chatbot/components/chatbot-drawer.vue new file mode 100644 index 0000000..c1e307b --- /dev/null +++ b/src/modules/chatbot/components/chatbot-drawer.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/modules/chatbot/components/dialogue-content.vue b/src/modules/chatbot/components/dialogue-content.vue new file mode 100644 index 0000000..e3a3685 --- /dev/null +++ b/src/modules/chatbot/components/dialogue-content.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/src/modules/chatbot/components/dialogue-phrase.vue b/src/modules/chatbot/components/dialogue-phrase.vue new file mode 100644 index 0000000..5d89874 --- /dev/null +++ b/src/modules/chatbot/components/dialogue-phrase.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/modules/chatbot/composables/chatbot-api.ts b/src/modules/chatbot/composables/chatbot-api.ts new file mode 100644 index 0000000..69d50dc --- /dev/null +++ b/src/modules/chatbot/composables/chatbot-api.ts @@ -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, + }; +}; diff --git a/src/modules/chatbot/models/dialogue-message.model.ts b/src/modules/chatbot/models/dialogue-message.model.ts new file mode 100644 index 0000000..2e26586 --- /dev/null +++ b/src/modules/chatbot/models/dialogue-message.model.ts @@ -0,0 +1,5 @@ +export interface Message { + text: string; + sent: boolean; + isThinking: boolean; +} diff --git a/src/modules/chatbot/models/page-context.model.ts b/src/modules/chatbot/models/page-context.model.ts new file mode 100644 index 0000000..ea6e5d7 --- /dev/null +++ b/src/modules/chatbot/models/page-context.model.ts @@ -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 = { + "login": null, + "login-success": null, + "error": null, + "/": dashboardContext, + "employees": employeeListContext, + "profile": profileContext, + "timesheet": timesheetContext, + "timesheet-approvals": timesheetApprovalContext, + "help": helpContext, +} diff --git a/src/modules/chatbot/pages/chatbot-page.vue b/src/modules/chatbot/pages/chatbot-page.vue new file mode 100644 index 0000000..9d3a2b4 --- /dev/null +++ b/src/modules/chatbot/pages/chatbot-page.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/modules/chatbot/services/chatbot.service.ts b/src/modules/chatbot/services/chatbot.service.ts new file mode 100644 index 0000000..b68f6b4 --- /dev/null +++ b/src/modules/chatbot/services/chatbot.service.ts @@ -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 => { + const response = await api.post("/chatbot", { userInput }); + return response.data as Message; + }, + + // Function to send context to backend + sendPageContext: async (context: chatbotPageContext): Promise => { + 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 => { + const response = await api.post("/chatbot/user", { email, role }); + return response.data; + }, + + retrieveCustomerDiagnostics: async (id: string): Promise => { + const response = await api.get(`/chat/customer/${id}`); + return response.data; + }, +}; diff --git a/src/modules/employee-list/models/employee-profile.models.ts b/src/modules/employee-list/models/employee-profile.models.ts index 72660fc..c6e3172 100644 --- a/src/modules/employee-list/models/employee-profile.models.ts +++ b/src/modules/employee-list/models/employee-profile.models.ts @@ -128,6 +128,7 @@ export const employee_access_options: QSelectOption[] = [ { 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 = { @@ -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'; } } \ No newline at end of file diff --git a/src/modules/help/components/help-module.vue b/src/modules/help/components/help-module.vue index f947c35..efcab3f 100644 --- a/src/modules/help/components/help-module.vue +++ b/src/modules/help/components/help-module.vue @@ -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'; diff --git a/src/modules/help/models/help-module.model.ts b/src/modules/help/models/help-module.model.ts index 261fc5e..ccfb55c 100644 --- a/src/modules/help/models/help-module.model.ts +++ b/src/modules/help/models/help-module.model.ts @@ -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: [], }; diff --git a/src/modules/shared/models/user.models.ts b/src/modules/shared/models/user.models.ts index a4c46c8..cb16991 100644 --- a/src/modules/shared/models/user.models.ts +++ b/src/modules/shared/models/user.models.ts @@ -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]; \ No newline at end of file diff --git a/src/pages/profile-page.vue b/src/pages/profile-page.vue index cfb2309..0598b8a 100644 --- a/src/pages/profile-page.vue +++ b/src/pages/profile-page.vue @@ -4,8 +4,8 @@ > import MenuEmployee from 'src/modules/profile/components/employee/menu-employee.vue'; import { useAuthStore } from 'src/stores/auth-store'; -import { useEmployeeStore } from 'src/stores/employee-store'; -import { onMounted } from 'vue'; + import { useEmployeeStore } from 'src/stores/employee-store'; + import { onMounted } from 'vue'; const auth_store = useAuthStore(); const employee_store = useEmployeeStore(); diff --git a/src/pages/timesheet-page.vue b/src/pages/timesheet-page.vue index 067341b..38b8180 100644 --- a/src/pages/timesheet-page.vue +++ b/src/pages/timesheet-page.vue @@ -10,7 +10,6 @@ const { user } = useAuthStore(); -