mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
refactor to use pinia store for conversation state
The chat widget endpoint is now using stores/chat.ts to manage the conversation state. The connection state is managed by stores/connection.ts. OpeyController /chat proxy has been removed and the socket connection is managed by the store instead. OpeyController /token proxy is used to get a jwt token from the server when the chat widget is loaded. The token is used to authenticate the socket connection.
This commit is contained in:
parent
c58ed1fe14
commit
baa978e44f
@ -39,80 +39,11 @@ import superagent from 'superagent'
|
||||
@Service()
|
||||
@Controller('/opey')
|
||||
export class OpeyController {
|
||||
private obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST
|
||||
private chatBotUrl = process.env.VITE_CHATBOT_URL
|
||||
private opeySecret = process.env.VITE_OPEY_SECRET
|
||||
constructor(
|
||||
private obpClientService: OBPClientService,
|
||||
private oauthInjectedService: OauthInjectedService
|
||||
) {}
|
||||
|
||||
@Post('/chat')
|
||||
async chat(
|
||||
@Session() session: any,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response
|
||||
): Response {
|
||||
try {
|
||||
const oauthService = this.oauthInjectedService
|
||||
const consumer = oauthService.getConsumer()
|
||||
// Get current user
|
||||
const oauthConfig = session['clientConfig']
|
||||
const version = this.obpClientService.getOBPVersion()
|
||||
const currentUser = await this.obpClientService.get(`/obp/${version}/users/current`, oauthConfig)
|
||||
const currentResponseKeys = Object.keys(currentUser)
|
||||
// If current user is logged in, issue JWT signed with private key
|
||||
if (currentResponseKeys.includes('user_id')) {
|
||||
// sign
|
||||
const jwtToken = this.generateJWT(currentUser.user_id, currentUser.username, session)
|
||||
|
||||
// Establish websocket connection
|
||||
const ws = new WebSocket(`${this.chatBotUrl.replace('http', 'ws')}/chat`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${jwtToken}`
|
||||
}
|
||||
});
|
||||
|
||||
// Send request with jwt token
|
||||
ws.on('open', () => {
|
||||
// Send request data
|
||||
ws.send(JSON.stringify({
|
||||
session_id: request.body.session_id,
|
||||
message: request.body.message,
|
||||
obp_api_host: request.body.obp_api_host,
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data.toString());
|
||||
// Handle incoming message
|
||||
console.log("Message delta: ", message);
|
||||
response.write(JSON.stringify(message));
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
response.end();
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error("WebSocket error: ", error);
|
||||
response.status(500).json({ error: 'WebSocket Error' });
|
||||
});
|
||||
|
||||
request.on('close', () => {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
return response;
|
||||
} else {
|
||||
return response.status(400).json({ message: 'User not logged in, Authentication required' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in chat endpoint: ", error);
|
||||
return response.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/token')
|
||||
async getToken(
|
||||
@Session() session: any,
|
||||
@ -120,8 +51,6 @@ export class OpeyController {
|
||||
@Res() response: Response
|
||||
): Response {
|
||||
try {
|
||||
const oauthService = this.oauthInjectedService
|
||||
const consumer = oauthService.getConsumer()
|
||||
// Get current user
|
||||
const oauthConfig = session['clientConfig']
|
||||
const version = this.obpClientService.getOBPVersion()
|
||||
|
||||
@ -30,12 +30,14 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import 'prismjs/themes/prism.css'; // Choose a theme you like
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import axios from 'axios';
|
||||
import { inject, onMounted, ref } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import { obpApiHostKey } from '@/obp/keys';
|
||||
import { getCurrentUser } from '../obp';
|
||||
import { getOpeyJWT } from '@/obp/common-functions'
|
||||
import { storeToRefs } from "pinia";
|
||||
import { socket } from '@/socket';
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { useConnectionStore } from '@/stores/connection';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
|
||||
import 'prismjs/components/prism-markup';
|
||||
import 'prismjs/components/prism-javascript';
|
||||
@ -47,11 +49,23 @@
|
||||
import 'prismjs/themes/prism-okaidia.css';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const chatStore = useChatStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
socket.off()
|
||||
|
||||
chatStore.bindEvents();
|
||||
connectionStore.bindEvents();
|
||||
|
||||
const { isStreaming, chatMessages, currentMessageSnapshot } = storeToRefs(chatStore);
|
||||
|
||||
return {isStreaming, chatMessages, currentMessageSnapshot, chatStore, connectionStore}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
userInput: '',
|
||||
messages: [],
|
||||
sessionId: uuidv4(),
|
||||
isLoading: false,
|
||||
obpApiHost: null,
|
||||
@ -83,20 +97,52 @@
|
||||
}
|
||||
},
|
||||
async establishWebSocketConnection() {
|
||||
// Get the Opey JWT token
|
||||
let token = ''
|
||||
try {
|
||||
token = await getOpeyJWT()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
token = ''
|
||||
}
|
||||
|
||||
socket.auth = { token };
|
||||
|
||||
// Establish the WebSocket connection
|
||||
console.log('Establishing WebSocket connection');
|
||||
socket.connect();
|
||||
this.connectionStore.connect(token)
|
||||
|
||||
},
|
||||
async sendMessage() {
|
||||
if (this.userInput.trim()) {
|
||||
const newMessage = { role: 'user', content: this.userInput };
|
||||
this.messages.push(newMessage);
|
||||
this.chatMessages.push(newMessage);
|
||||
this.userInput = '';
|
||||
this.isLoading = true;
|
||||
this.currentMessage = "",
|
||||
|
||||
// Send the user message to the backend and get the response
|
||||
console.log('Sending message:', newMessage.content);
|
||||
socket.emit('chat', {
|
||||
session_id: this.sessionId,
|
||||
message: newMessage.content,
|
||||
obp_api_host: this.obpApiHost
|
||||
});
|
||||
|
||||
socket.on('response stream start', (response) => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
/*
|
||||
|
||||
|
||||
socket.on('response stream delta', (response) => {
|
||||
this.isLoading = false;
|
||||
console.log('Response:', response);
|
||||
this.currentMessage += response.assistant;
|
||||
});
|
||||
|
||||
|
||||
*/
|
||||
/*
|
||||
try {
|
||||
const response = await axios.post('/api/opey/chat', {
|
||||
@ -112,10 +158,10 @@
|
||||
console.log(`Response: ${response.status}`);
|
||||
throw new Error("We're having trouble connecting you to Opey right now...");
|
||||
}
|
||||
this.messages.push({ role: 'assistant', content: response.data.reply });
|
||||
this.chatMessages.push({ role: 'assistant', content: response.data.reply });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
this.messages.push({ role: 'error', content: "We're having trouble connecting you to Opey right now..."})
|
||||
this.chatMessages.push({ role: 'error', content: "We're having trouble connecting you to Opey right now..."})
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@ -214,10 +260,13 @@
|
||||
<img alt="Powered by OpenAI" src="@/assets/powered-by-openai-badge-outlined-on-dark.svg" height="32">
|
||||
</div>
|
||||
<div v-if="this.isLoggedIn" class="chat-messages" ref="messages">
|
||||
<div v-for="(message, index) in messages" :key="index" :class="['chat-message', message.role]">
|
||||
<div v-for="(message, index) in chatMessages" :key="index" :class="['chat-message', message.role]">
|
||||
<div v-if="message.role=='error'">
|
||||
<el-icon><Warning /></el-icon> <div v-html="renderMarkdown(message.content)"></div>
|
||||
</div>
|
||||
<div v-else-if="(this.isStreaming)&&(index === this.chatMessages.length -1)">
|
||||
<div v-html="renderMarkdown(this.currentMessageSnapshot)"></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-html="renderMarkdown(message.content)"></div>
|
||||
</div>
|
||||
|
||||
@ -1,39 +1,39 @@
|
||||
import { reactive } from "vue";
|
||||
/*
|
||||
* 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/)
|
||||
*
|
||||
*/
|
||||
|
||||
import { io } from "socket.io-client";
|
||||
import axios from 'axios';
|
||||
|
||||
import { getOpeyJWT } from './obp/common-functions'
|
||||
|
||||
export const state = reactive({
|
||||
connected: false,
|
||||
});
|
||||
|
||||
// "undefined" means the URL will be computed from the `window.location` object
|
||||
const URL = import.meta.env.VITE_CHATBOT_URL
|
||||
const token = await getOpeyJWT()
|
||||
|
||||
export const socket = io(
|
||||
URL,
|
||||
{
|
||||
extraHeaders: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
autoConnect: false,
|
||||
}
|
||||
);
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Websocket connection established");
|
||||
state.connected = true;
|
||||
});
|
||||
|
||||
socket.on('open', () => {
|
||||
console.log('WebSocket connection established, authenticating...');
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
state.connected = false;
|
||||
});
|
||||
|
||||
socket.on("message", (message) => {
|
||||
console.log(message);
|
||||
});
|
||||
63
src/stores/chat.ts
Normal file
63
src/stores/chat.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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/)
|
||||
*
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { socket } from '@/socket'
|
||||
|
||||
export const useChatStore = defineStore('chat', {
|
||||
state: () => ({
|
||||
chatMessages: [] as {role: string; content: string}[],
|
||||
isStreaming: false,
|
||||
currentMessageSnapshot: "" as string,
|
||||
waitingForResponse: false,
|
||||
}),
|
||||
actions: {
|
||||
bindEvents() {
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected to chatbot");
|
||||
})
|
||||
|
||||
socket.on('response stream start', (response) => {
|
||||
this.isStreaming = true;
|
||||
this.waitingForResponse = true;
|
||||
this.chatMessages.push({ role: 'assistant', content: " "})
|
||||
});
|
||||
|
||||
socket.on('response stream delta', (response) => {
|
||||
this.currentMessageSnapshot += response.assistant;
|
||||
});
|
||||
|
||||
socket.on('response stream end', (response) => {
|
||||
this.isStreaming = false;
|
||||
console.log(this.chatMessages[this.chatMessages.length - 1].content)
|
||||
this.chatMessages[this.chatMessages.length - 1].content = this.currentMessageSnapshot
|
||||
this.currentMessageSnapshot = ""
|
||||
console.log(this.chatMessages)
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
52
src/stores/connection.ts
Normal file
52
src/stores/connection.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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/)
|
||||
*
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { socket } from "@/socket";
|
||||
|
||||
export const useConnectionStore = defineStore("connection", {
|
||||
state: () => ({
|
||||
isConnected: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
bindEvents() {
|
||||
socket.on("connect", () => {
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
this.isConnected = false;
|
||||
});
|
||||
},
|
||||
|
||||
connect(token: string) {
|
||||
socket.auth = { token };
|
||||
socket.connect();
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user