Add chatbot frontend UI

This commit is contained in:
nemo 2024-05-26 17:39:30 +01:00
parent 4214d2abcd
commit 2c3f3a60fa
8 changed files with 1504 additions and 970 deletions

1
components.d.ts vendored
View File

@ -9,6 +9,7 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ChatWidget: typeof import('./src/components/ChatWidget.vue')['default']
Collections: typeof import('./src/components/Collections.vue')['default']
Content: typeof import('./src/components/Content.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']

View File

@ -25,20 +25,24 @@
"express": "^4.18.2",
"express-session": "^1.17.3",
"highlight.js": "^11.7.0",
"markdown-it": "^14.1.0",
"oauth": "^0.10.0",
"obp-typescript": "^1.0.36",
"pinia": "^2.0.32",
"prismjs": "^1.29.0",
"redis": "^4.6.13",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.10.3",
"typedi": "^0.10.0",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-markdown-renderer": "^0.2.7",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/jsdom": "^21.1.0",
"@types/markdown-it": "^14.1.1",
"@types/node": "^18.14.2",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",

110
public/js/inactivity.js Normal file
View File

@ -0,0 +1,110 @@
import * as countdownTimer from '../../src/assets/inactivity-timer.js'
// holds the idle duration in ms (current value = 301 seconds)
var timeoutIntervalInMillis = 5 * 60 * 1000 + 1000;
// holds the timeout variables for easy destruction and reconstruction of the setTimeout hooks
var timeHook = null;
function initializeTimeHook() {
// this method has the purpose of creating our timehooks and scheduling the call to our logout function when the idle time has been reached
if (timeHook == null) {
timeHook = setTimeout( function () { destroyTimeHook(); logout(); }.bind(this), timeoutIntervalInMillis);
}
}
function destroyTimeHook() {
// this method has the sole purpose of destroying any time hooks we might have created
clearTimeout(timeHook);
timeHook = null;
}
function resetTimeHook(event) {
// this method replaces the current time hook with a new time hook
destroyTimeHook();
initializeTimeHook();
countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000);
// show event type, element and coordinates of the click
// console.log(event.type + " at " + event.currentTarget);
// console.log("Coordinates: " + event.clientX + ":" + event.clientY);
console.log("Reset inactivity of a user");
}
function setupListeners() {
// here we setup the event listener for the mouse click operation
document.addEventListener("click", resetTimeHook);
document.addEventListener("mousemove", resetTimeHook);
document.addEventListener("mousedown", resetTimeHook);
document.addEventListener("keypress", resetTimeHook);
document.addEventListener("touchmove", resetTimeHook);
console.log("Listeners for user inactivity activated");
}
function destroyListeners() {
// here we destroy event listeners for the mouse click operation
document.removeEventListener("click", resetTimeHook);
document.removeEventListener("mousemove", resetTimeHook);
document.removeEventListener("mousedown", resetTimeHook);
document.removeEventListener("keypress", resetTimeHook);
document.removeEventListener("touchmove", resetTimeHook);
console.log("Listeners for user inactivity deactivated");
}
function logout() {
destroyListeners();
countdownTimer.destroyCountdownTimer();
console.log("Logging you out due to inactivity..");
const logoffButton = document.getElementById("logout");
logoffButton.click();
}
async function makeObpApiCall() {
//debug
console.log("calling API");
let timeoutInSeconds;
try {
let obpApiHost = document.getElementById("nav");
console.log(obpApiHost);
if(obpApiHost) {
obpApiHost = obpApiHost.href.split("?")[0];
}
const response = await fetch(`${obpApiHost}/obp/v5.1.0/ui/suggested-session-timeout`);
const json = await response.json();
if(json.timeout_in_seconds) {
timeoutInSeconds = json.timeout_in_seconds;
console.log(`Suggested value ${timeoutInSeconds} is used`);
} else {
timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds
console.log(`Default value ${timeoutInSeconds} is used`);
}
} catch (e) {
console.error(e);
timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds, even if the session timeout endpoint is not reachable for whatever reason
console.log(`Default value ${timeoutInSeconds} is used`);
}
return timeoutInSeconds;
}
async function getSuggestedSessionTimeout() {
if(!sessionStorage.getItem("suggested-session-timeout-in-seconds")) {
let timeoutInSeconds = await makeObpApiCall();
sessionStorage.setItem("suggested-session-timeout-in-seconds", timeoutInSeconds);
}
return sessionStorage.getItem("suggested-session-timeout-in-seconds") * 1000 + 1000; // We need timeout in millis
}
// self executing function to trigger the operation on page load
(async function () {
timeoutIntervalInMillis = await getSuggestedSessionTimeout(); // Try to get suggested value
const logoffButton = document.getElementById("countdown-timer-span");
if(logoffButton) {
// to prevent any lingering timeout handlers preventing memory leaks
destroyTimeHook();
// setup a fresh time hook
initializeTimeHook();
// setup initial event listeners
setupListeners();
// Reset countdown timer
countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000);
}
})();

View File

@ -27,6 +27,7 @@
<script setup lang="ts">
import HeaderNav from './components/HeaderNav.vue'
import ChatWidget from './components/ChatWidget.vue'
</script>
<template>
@ -37,6 +38,7 @@ import HeaderNav from './components/HeaderNav.vue'
<HeaderNav />
</el-header>
<RouterView />
<ChatWidget />
</el-container>
</div>
</template>

BIN
src/assets/chatbot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,36 @@
function addSeconds(date, seconds) {
date.setSeconds(date.getSeconds() + seconds);
return date;
}
export function showCountdownTimer() {
// Get current date and time
var now = new Date().getTime();
let distance = countDownDate - now;
// Output the result in an element with id="countdown-timer-span"
let elementId = ("countdown-timer-span");
document.getElementById(elementId).innerHTML = "in " + Math.floor(distance / 1000) + "s";
// If the count down is over release resources
if (distance < 0) {
destroyCountdownTimer();
}
}
// Set the date we're counting down to
let countDownDate = addSeconds(new Date(), 5);
let showTimerInterval = null;
export function destroyCountdownTimer() {
clearInterval(showTimerInterval);
}
export function resetCountdownTimer(seconds) {
destroyCountdownTimer(); // Destroy previous timer if any
countDownDate = addSeconds(new Date(), seconds); // Set the date we're counting down to
showTimerInterval = setInterval(showCountdownTimer, 1000); // Update the count down every 1 second
}

View File

@ -0,0 +1,256 @@
<!--
- Open Bank Project - API Explorer II
- Copyright (C) 2023-2024, TESOBE GmbH
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- Email: contact@tesobe.com
- TESOBE GmbH
- Osloerstrasse 16/17
- Berlin 13359, Germany
-
- This product includes software developed at
- TESOBE (http://www.tesobe.com/)
-
-->
<script>
import Prism from 'prismjs';
import MarkdownIt from "markdown-it";
import 'prismjs/themes/prism.css'; // Choose a theme you like
export default {
data() {
return {
isOpen: false,
userInput: '',
messages: []
};
},
methods: {
toggleChat() {
this.isOpen = !this.isOpen;
this.$nextTick(() => {
if (this.isOpen) {
this.scrollToBottom();
}
});
},
async sendMessage() {
if (this.userInput.trim()) {
const newMessage = { role: 'user', content: this.userInput };
this.messages.push(newMessage);
this.userInput = '';
// Send the user message to the backend and get the response
const response = await fetch('http://localhost:5000/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: newMessage.content })
});
console.log('Response status:', response.status);
console.log('Response headers:', response.headers);
const data = await response.json();
this.messages.push({ role: 'assistant', content: data.reply });
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
highlightCode(content) {
return Prism.highlight(content, Prism.languages.markup, 'markup');
},
renderMarkdown(content) {
const markdown = new MarkdownIt();
return markdown.render(content);
},
scrollToBottom() {
const messages = this.$refs.messages;
messages.scrollTop = messages.scrollHeight;
},
initResize(event) {
console.log("resizing")
this.isResizing = true;
this.startX = event.clientX;
this.startY = event.clientY;
this.startWidth = parseInt(document.defaultView.getComputedStyle(this.$refs.chatContainer).width, 10);
this.startHeight = parseInt(document.defaultView.getComputedStyle(this.$refs.chatContainer).height, 10);
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
},
resize(event) {
if (this.isResizing) {
const chatContainer = this.$refs.chatContainer;
const newWidth = this.startWidth - (event.clientX - this.startX);
const newHeight = this.startHeight - (event.clientY - this.startY);
if (newWidth > 100) {
chatContainer.style.width = `${newWidth}px`;
}
if (newHeight > 100) {
chatContainer.style.height = `${newHeight}px`;
}
}
},
stopResize() {
this.isResizing = false;
window.removeEventListener('mousemove', this.resize);
window.removeEventListener('mouseup', this.stopResize);
}
}
};
</script>
<template>
<div>
<div class="chat-button" @click="toggleChat">
<img alt="AI Help" v-show="!logo" src="@/assets/chatbot.png" />
</div>
<div v-if="isOpen" class="chat-container" ref="chatContainer">
<div class="resizer" @mousedown="initResize"></div>
<div class="chat-header">
<span>Chat with us</span>
<button @click="toggleChat">X</button>
</div>
<div class="chat-messages" ref="messages">
<div v-for="(message, index) in messages" :key="index" :class="['chat-message', message.role]">
<div v-html="renderMarkdown(message.content)"></div>
</div>
</div>
<div class="chat-input">
<textarea v-model="userInput" placeholder="Type your message..."></textarea>
<button @click="sendMessage">Send</button>
</div>
</div>
</div>
</template>
<style>
.chat-button {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
background-color: white;
color: #fff;
border-radius: 50%;
cursor: pointer;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 0 20px rgba(0, 123, 255, 0.6);
transition: box-shadow 0.3s;
}
.chat-button:hover {
box-shadow: 0 0 30px rgba(0, 123, 255, 0.8);
}
.chat-button img {
width: 30px;
}
.chat-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
height: 400px;
border: 1px solid #ccc;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: #fff;
display: flex;
flex-direction: column;
z-index: 1000;
overflow: hidden;
}
.chat-header {
padding: 10px;
background-color: #007bff;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
padding: 10px;
overflow-y: auto;
background-color: #f9f9f9;
}
.chat-message {
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.chat-message.user {
background-color: #e1ffc7;
align-self: flex-end;
}
.chat-message.assistant {
background-color: #fff;
}
.chat-input {
display: flex;
padding: 10px;
border-top: 1px solid #ccc;
background-color: #fff;
}
.chat-input textarea {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
resize: none;
font-size: 14px;
}
.chat-input button {
margin-left: 10px;
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
.chat-input button:hover {
background-color: #0056b3;
}
.resizer {
width: 15px;
height: 15px;
background: #ccc;
position: absolute;
left: 0;
top: 0;
cursor: nwse-resize;
z-index: 1001;
}
</style>

2065
yarn.lock

File diff suppressed because it is too large Load Diff