mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
Add chatbot frontend UI
This commit is contained in:
parent
4214d2abcd
commit
2c3f3a60fa
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -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']
|
||||
|
||||
@ -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
110
public/js/inactivity.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
@ -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
BIN
src/assets/chatbot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
36
src/assets/inactivity-timer.js
Normal file
36
src/assets/inactivity-timer.js
Normal 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
|
||||
}
|
||||
256
src/components/ChatWidget.vue
Normal file
256
src/components/ChatWidget.vue
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user