mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
WIP log in frontend
This commit is contained in:
parent
8c646e0053
commit
c6b0dd5a42
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -38,6 +38,7 @@ declare module 'vue' {
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
GlossarySearchNav: typeof import('./src/components/GlossarySearchNav.vue')['default']
|
||||
HeaderNav: typeof import('./src/components/HeaderNav.vue')['default']
|
||||
|
||||
@ -3,13 +3,11 @@ import { OpeyController } from "../controllers/OpeyIIController";
|
||||
import OpeyClientService from '../services/OpeyClientService';
|
||||
import OBPClientService from '../services/OBPClientService';
|
||||
import OBPConsentsService from '../services/OBPConsentsService';
|
||||
import { OpeyConfig } from '../schema/OpeySchema';
|
||||
import Stream, { Readable } from 'stream';
|
||||
import { Request, Response } from 'express';
|
||||
import httpMocks from 'node-mocks-http'
|
||||
import { EventEmitter } from 'events';
|
||||
import { InlineResponse2017 } from 'obp-api-typescript';
|
||||
import { c } from 'vitest/dist/reporters-5f784f42.js';
|
||||
|
||||
vi.mock("../../server/services/OpeyClientService", () => {
|
||||
return {
|
||||
@ -43,7 +41,9 @@ describe('OpeyController', () => {
|
||||
let opeyController: OpeyController
|
||||
// Mock the OpeyClientService class
|
||||
|
||||
const { mockClear } = getMockRes()
|
||||
beforeEach(() => {
|
||||
mockClear()
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
@ -171,16 +171,7 @@ describe('OpeyController consents', () => {
|
||||
|
||||
const MockOpeyClientService = {
|
||||
authConfig: {},
|
||||
opeyConfig: {
|
||||
baseUri: 'http://localhost:8080',
|
||||
paths: {
|
||||
invoke: '/invoke',
|
||||
status: '/status',
|
||||
stream: '/stream',
|
||||
approve_tool: '/approve_tool/{thread_id}',
|
||||
feedback: '/feedback',
|
||||
}
|
||||
},
|
||||
opeyConfig: {},
|
||||
getOpeyStatus: vi.fn(async () => {
|
||||
return {status: 'running'}
|
||||
}),
|
||||
@ -200,36 +191,17 @@ describe('OpeyController consents', () => {
|
||||
return {
|
||||
content: 'Hi this is Opey',
|
||||
}
|
||||
}),
|
||||
getOpeyConfig: vi.fn(async (partialConfig?) => {
|
||||
return {
|
||||
baseUri: 'http://localhost:8080',
|
||||
paths: {
|
||||
invoke: '/invoke',
|
||||
status: '/status',
|
||||
stream: '/stream',
|
||||
approve_tool: '/approve_tool/{thread_id}',
|
||||
feedback: '/feedback',
|
||||
}
|
||||
}
|
||||
})
|
||||
} as unknown as OpeyClientService
|
||||
|
||||
const MockOBPConsentsService = {
|
||||
createConsent: vi.fn(async (session) => {
|
||||
|
||||
const mockConsentResponse = {
|
||||
createConsent: vi.fn(async () => {
|
||||
return {
|
||||
"consent_id": "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0",
|
||||
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9CUCBDb25zZW50IFRva2VuIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
||||
"status": "INITIATED",
|
||||
} as InlineResponse2017
|
||||
|
||||
session['opeyConfig'] = {
|
||||
authConfig: { obpConsent: mockConsentResponse }
|
||||
}
|
||||
|
||||
return mockConsentResponse
|
||||
}),
|
||||
})
|
||||
} as unknown as OBPConsentsService
|
||||
|
||||
// Instantiate OpeyController with the mocked OpeyClientService
|
||||
@ -242,21 +214,10 @@ describe('OpeyController consents', () => {
|
||||
it('should return 200 and consent ID when consent is created at OBP', async () => {
|
||||
|
||||
|
||||
// Mock the request and response objects
|
||||
const req = {}
|
||||
const req = getMockReq()
|
||||
const session = {}
|
||||
|
||||
const res: Partial<Response> = {
|
||||
status: vi.fn().mockImplementation((status) => {
|
||||
return res
|
||||
}),
|
||||
json: vi.fn().mockImplementation((data) => {
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
const { res } = getMockRes()
|
||||
await opeyController.getConsent(session, req, res)
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200)
|
||||
|
||||
// Obviously if you change the MockOBPConsentsService.createConsent mock implementation, you will need to change this test
|
||||
@ -265,14 +226,9 @@ describe('OpeyController consents', () => {
|
||||
})
|
||||
|
||||
// Expect that the consent object was saved in the session
|
||||
expect(session).toHaveProperty('opeyConfig')
|
||||
const opeyConfig = session['opeyConfig']
|
||||
console.log(opeyConfig)
|
||||
expect(opeyConfig).toHaveProperty('authConfig')
|
||||
expect(session['opeyConfig']).toHaveProperty('authConfig')
|
||||
expect(session['opeyConfig']['authConfig']).toHaveProperty('obpConsent')
|
||||
expect(session['opeyConfig']['authConfig']['obpConsent']).toHaveProperty('consent_id', "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0")
|
||||
expect(session['opeyConfig']['authConfig']['obpConsent']).toHaveProperty('jwt', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9CUCBDb25zZW50IFRva2VuIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
|
||||
expect(session['opeyConfig']['authConfig']['obpConsent']).toHaveProperty('status', "INITIATED")
|
||||
expect(session).toHaveProperty('obpConsent')
|
||||
expect(session['obpConsent']).toHaveProperty('consent_id', "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0")
|
||||
expect(session['obpConsent']).toHaveProperty('jwt', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9CUCBDb25zZW50IFRva2VuIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
|
||||
expect(session['obpConsent']).toHaveProperty('status', "INITIATED")
|
||||
})
|
||||
})
|
||||
@ -69,26 +69,11 @@ export default {
|
||||
content: this.input,
|
||||
isToolCallApproval: false,
|
||||
};
|
||||
this.chat.addMessage(userMessage);
|
||||
|
||||
// Create a placeholder for the assistant's response
|
||||
this.chat.currentAssistantMessage = {
|
||||
id: uuidv4(),
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
};
|
||||
this.chat.addMessage(this.chat.currentAssistantMessage);
|
||||
|
||||
// Set status to loading
|
||||
// Set status to loading // Clear input field after sending
|
||||
this.chat.status = 'loading';
|
||||
|
||||
// Clear input field after sending
|
||||
this.input = '';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try {
|
||||
await this.chat.stream({
|
||||
message: userMessage,
|
||||
@ -129,16 +114,20 @@ export default {
|
||||
<el-button type="danger" :icon="Close" @click="toggleChat" size="small" circle></el-button>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<div class="messages-container">
|
||||
<div v-if="!chat.userIsAuthenticated" class="login-container">
|
||||
<p class="login-message" size="large">Opey is only available once logged on.</p>
|
||||
<a href="/api/connect" class="login-button router-link">Log on</a>
|
||||
</div>
|
||||
<div v-else class="messages-container" v-bind:class="{ disabled: !chat.userIsAuthenticated }">
|
||||
<el-scrollbar>
|
||||
<ChatMessage v-for="message in chat.messages" :key="message.id" :message="message" />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<el-footer v-bind:class="{ disabled: !chat.userIsAuthenticated }">
|
||||
<div class="user-input-container">
|
||||
<div class="user-input">
|
||||
<textarea v-model="input" type="textarea" placeholder="Type your message..." :disabled="chat.status !== 'ready'" @keypress.enter="onSubmit" />
|
||||
<textarea v-model="input" type="textarea" placeholder="Type your message..." :disabled="(chat.status !== 'ready') || (!chat.userIsAuthenticated)" @keypress.enter="onSubmit" />
|
||||
</div>
|
||||
<el-button type="primary" @click="onSubmit" color="#253047" :icon="ElTop" circle></el-button>
|
||||
</div>
|
||||
@ -179,7 +168,7 @@ export default {
|
||||
min-height: 470px;
|
||||
max-height: 90vh;
|
||||
max-width: 90vw;
|
||||
background-color: tomato;
|
||||
background-color: #151d30;
|
||||
resize: both;
|
||||
overflow: auto;
|
||||
transform: rotate(180deg);
|
||||
@ -187,6 +176,25 @@ export default {
|
||||
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin:auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-message {
|
||||
color: #fff;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
margin-left: 100px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
font-family: Roboto-Light, sans-serif;
|
||||
}
|
||||
@ -197,6 +205,12 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.chat-container .el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -84,6 +84,16 @@ export const useChat = defineStore('chat', {
|
||||
return store.threadId
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getLastAssistantMessage(store): OpeyMessage | undefined {
|
||||
return this.getMessageById(store.currentAssistantMessage.id)
|
||||
},
|
||||
|
||||
getMessageById: (store) => {
|
||||
return (id: string): OpeyMessage | undefined => {
|
||||
return store.messages.find(m => m.id === id)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -112,6 +122,13 @@ export const useChat = defineStore('chat', {
|
||||
this.messages = this.messages.filter(m => m.id !== messageId);
|
||||
},
|
||||
|
||||
async applyErrorToMessage(messageId: string, errorMessageString: string): Promise<void> {
|
||||
const message = this.getMessageById(messageId);
|
||||
if (message) {
|
||||
message.error = errorMessageString;
|
||||
}
|
||||
},
|
||||
|
||||
async handleAuthentication(): Promise<void> {
|
||||
// Handle authentication
|
||||
// get consent for Opey from user
|
||||
@ -130,6 +147,17 @@ export const useChat = defineStore('chat', {
|
||||
},
|
||||
|
||||
async stream(input: ChatStreamInput): Promise<void> {
|
||||
// Add user message to chat
|
||||
this.addMessage(input.message)
|
||||
|
||||
// Create a placecholder for the assistant message
|
||||
this.currentAssistantMessage = {
|
||||
content: '',
|
||||
role: 'assistant',
|
||||
id: uuidv4(),
|
||||
}
|
||||
this.addMessage(this.currentAssistantMessage)
|
||||
|
||||
// Handle stream
|
||||
try {
|
||||
const response = await fetch('/api/opey/stream', {
|
||||
@ -163,8 +191,13 @@ export const useChat = defineStore('chat', {
|
||||
await processOpeyStream(stream, context);
|
||||
} catch (error) {
|
||||
console.error('Error sending Opey message:', error);
|
||||
|
||||
const errorMessage = "Hmmm, Looks like smething went wrong. Please try again later.";
|
||||
// Apply error state to the assistant message
|
||||
await this.applyErrorToMessage(this.currentAssistantMessage.id, errorMessage);
|
||||
|
||||
this.status = 'ready';
|
||||
throw new Error(`Error sending Opey message: ${error}`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,25 +41,13 @@ describe('ChatWidget', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
it('should call fetch when sending a user message', async () => {
|
||||
it('should call the stream function when sending a user message', async () => {
|
||||
const wrapper = mount(ChatWidget, {})
|
||||
|
||||
wrapper.vm.chat.stream = vi.fn(async () => {})
|
||||
await wrapper.vm.onSubmit()
|
||||
expect(global.fetch).toHaveBeenCalled()
|
||||
})
|
||||
it('should clear the assistant message placeholder from messages list on error', async () => {
|
||||
// mock the fetch function with a rejected promise
|
||||
const wrapper = mount(ChatWidget, {})
|
||||
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.reject(new Error('Test error'))
|
||||
);
|
||||
|
||||
await wrapper.vm.onSubmit()
|
||||
expect(wrapper.vm.opeyContext.messages.find(m => m.id === wrapper.vm.opeyContext.currentAssistantMessage.id)).toBeUndefined()
|
||||
|
||||
|
||||
expect(wrapper.vm.chat.stream).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should trigger onSubmit when enter key is pressed in the input', async () => {
|
||||
const wrapper = mount(ChatWidget, {})
|
||||
|
||||
@ -71,12 +59,22 @@ describe('ChatWidget', () => {
|
||||
// This will probably fail if the class name of the parent div is changed, or if the input type is moved i.e. from textarea to input or el-input
|
||||
const input = wrapper.get('.user-input-container textarea')
|
||||
input.trigger('keypress.enter')
|
||||
expect(global.fetch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('displays chat when chatOpen is set to true', async () => {
|
||||
const wrapper = mount(ChatWidget, {})
|
||||
wrapper.vm.chatOpen = true
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('.chat-container').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should show a log in screen if user is not authenticated', async () => {
|
||||
const wrapper = mount(ChatWidget, {})
|
||||
wrapper.vm.chat.userIsAuthenticated = false
|
||||
|
||||
wrapper.vm.chatOpen = true
|
||||
await wrapper.vm.$nextTick()
|
||||
console.log(wrapper.html())
|
||||
expect(wrapper.find('.login-container').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -1,7 +1,7 @@
|
||||
// Tesing the Pinia chat store in src/stores/chat.ts
|
||||
|
||||
import { useChat } from '@/stores/chat'
|
||||
import { beforeEach, describe, it, expect } from 'vitest'
|
||||
import { beforeEach, describe, it, expect, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
describe('Chat Store', () => {
|
||||
@ -33,4 +33,62 @@ describe('Chat Store', () => {
|
||||
const newThreadId = chatStore.getThreadId('5678')
|
||||
expect(newThreadId).toBe('1234')
|
||||
})
|
||||
|
||||
it('should apply an error state to the assistant message on error', async () => {
|
||||
// mock the fetch function with a rejected promise
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.reject(new Error('Test error'))
|
||||
);
|
||||
const chatStore = useChat()
|
||||
|
||||
await chatStore.stream({message: {
|
||||
content: 'Hello Opey',
|
||||
role: 'user',
|
||||
id: '123',
|
||||
isToolCallApproval: false
|
||||
}})
|
||||
console.log("Messages: ", chatStore.messages)
|
||||
const assistantMessage = chatStore.getLastAssistantMessage
|
||||
expect(assistantMessage).toBeDefined()
|
||||
expect(assistantMessage?.error).toBeDefined()
|
||||
})
|
||||
|
||||
it('should stream messages correctly', async () => {
|
||||
// create a mock stream
|
||||
const mockStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(`data: {"type":"token","content":"test"}\n`));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
// mock the fetch function
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve(new Response(mockStream, {
|
||||
headers: { 'content-type': 'text/event-stream' },
|
||||
status: 200,
|
||||
}))
|
||||
);
|
||||
|
||||
const chatStore = useChat()
|
||||
|
||||
await chatStore.stream({message: {
|
||||
content: 'Hello Opey',
|
||||
role: 'user',
|
||||
id: '123',
|
||||
isToolCallApproval: false
|
||||
}})
|
||||
|
||||
const assistantMessage = chatStore.getLastAssistantMessage
|
||||
expect(assistantMessage).toBeDefined()
|
||||
expect(assistantMessage?.content).toBe('test')
|
||||
})
|
||||
|
||||
it('should be able to handle tool messages', async () => {
|
||||
const mockStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(`data: {"type":"tool","content":"test"}\n`));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
})
|
||||
})
|
||||
@ -65,6 +65,10 @@ describe('processOpeyStream', async () => {
|
||||
.toThrow('Stream closed by server')
|
||||
})
|
||||
|
||||
it('should be able to handle empty content', async () => {
|
||||
|
||||
})
|
||||
|
||||
it('should throw an error when the chunk is not valid json', async () => {
|
||||
const invalidJsonStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user