diff --git a/backend/package-lock.json b/backend/package-lock.json index c6e4b91..feb83f0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,6 +8,7 @@ "name": "ai-backend", "version": "1.0.0", "dependencies": { + "@types/express-ws": "^3.0.4", "ajv-formats": "^3.0.1", "bcrypt": "^5.1.1", "compression": "^1.7.4", @@ -18,9 +19,9 @@ "filewatcher": "^3.0.1", "glob": "^10.3.16", "knex": "^3.1.0", + "mitt": "^3.0.1", "mysql2": "^3.9.7", "objection": "^3.1.4", - "semaphore": "^1.1.0", "sqlite3": "^5.1.7", "typescript-ioc": "^3.2.2" }, @@ -2163,7 +2164,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -2182,7 +2182,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -2209,7 +2208,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2221,7 +2219,6 @@ "version": "4.19.1", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.1.tgz", "integrity": "sha512-ej0phymbFLoCB26dbbq5PGScsf2JAJ4IJHjG10LalgUV36XKTmA4GdA+PVllKvRk0sEKt64X8975qFnkSi0hqA==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2238,6 +2235,17 @@ "@types/express": "*" } }, + "node_modules/@types/express-ws": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.4.tgz", + "integrity": "sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2256,8 +2264,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -2328,8 +2335,7 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/multer": { "version": "1.4.11", @@ -2351,14 +2357,12 @@ "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", - "dev": true + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/semaphore": { "version": "1.1.4", @@ -2371,7 +2375,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -2381,7 +2384,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -6629,6 +6631,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7894,14 +7902,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/semaphore": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", - "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", diff --git a/backend/package.json b/backend/package.json index 5e9bbec..1ba465d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "tsoa:routes": "npm exec tsoa routes" }, "dependencies": { + "@types/express-ws": "^3.0.4", "ajv-formats": "^3.0.1", "bcrypt": "^5.1.1", "compression": "^1.7.4", @@ -28,6 +29,7 @@ "filewatcher": "^3.0.1", "glob": "^10.3.16", "knex": "^3.1.0", + "mitt": "^3.0.1", "mysql2": "^3.9.7", "objection": "^3.1.4", "sqlite3": "^5.1.7", diff --git a/backend/src/event/emitter.ts b/backend/src/event/emitter.ts new file mode 100644 index 0000000..0bb78b2 --- /dev/null +++ b/backend/src/event/emitter.ts @@ -0,0 +1,7 @@ +import mitt from 'mitt'; +import {Events} from './events'; + +const emitter = mitt(); +export default function getEmitter() { + return emitter; +} diff --git a/backend/src/event/events.ts b/backend/src/event/events.ts new file mode 100644 index 0000000..6a2d719 --- /dev/null +++ b/backend/src/event/events.ts @@ -0,0 +1,25 @@ +import AiInstance from '../models/business/AiInstance'; + +export type AiEvent = { + aiInstance: AiInstance, +} + +export type ChatEvent = AiEvent & { + role: string, + name: string, + content: string, +}; + +export type DiscordOnlineEvent = AiEvent & { + online: boolean, +}; + +export type DiscordReactToChatEvent = AiEvent & { + reactToChat: boolean, +}; + +export type Events = { + chatText: ChatEvent, + discordOnline: DiscordOnlineEvent, + discordReactToChat: DiscordReactToChatEvent, +} diff --git a/backend/src/event/listener.ts b/backend/src/event/listener.ts new file mode 100644 index 0000000..a1c5056 --- /dev/null +++ b/backend/src/event/listener.ts @@ -0,0 +1,35 @@ +import getEmitter from './emitter'; +import {Container} from 'typescript-ioc'; +import {ExpressWsProvider} from '../ioc'; +import {ChatEvent, DiscordOnlineEvent, DiscordReactToChatEvent} from './events'; + +export function registerEvents(): void { + const emitter = getEmitter(); + + emitter.on('chatText', (event: ChatEvent) => { + Container.get(ExpressWsProvider).get().getWss().clients + .forEach(c => c.send(JSON.stringify({ + type: 'chatText', + aiInstance: event.aiInstance.configuration.id, + role: event.role, + name: event.name, + content: event.content, + }))); + }); + emitter.on('discordOnline', (event: DiscordOnlineEvent) => { + Container.get(ExpressWsProvider).get().getWss().clients + .forEach(c => c.send(JSON.stringify({ + type: 'discordOnline', + aiInstance: event.aiInstance.configuration.id, + online: event.online, + }))); + }); + emitter.on('discordReactToChat', (event: DiscordReactToChatEvent) => { + Container.get(ExpressWsProvider).get().getWss().clients + .forEach(c => c.send(JSON.stringify({ + type: 'discordReactToChat', + aiInstance: event.aiInstance.configuration.id, + reactToChat: event.reactToChat, + }))); + }); +} \ No newline at end of file diff --git a/backend/src/ioc.ts b/backend/src/ioc.ts index 15b4abd..e8419d3 100644 --- a/backend/src/ioc.ts +++ b/backend/src/ioc.ts @@ -1,12 +1,25 @@ import {Container, Scope} from 'typescript-ioc'; import {IocContainer} from '@tsoa/runtime'; import {ConfigProvider} from './config/confighelper'; -import AiService from "./services/AiService"; +import AiService from './services/AiService'; import AiPythonConnector from './services/AiPythonConnector'; +import expressWs from 'express-ws'; const iocContainer: () => IocContainer = () => Container; Container.bind(ConfigProvider).to(ConfigProvider).scope(Scope.Singleton); Container.bind(AiPythonConnector).to(AiPythonConnector).scope(Scope.Singleton); Container.bind(AiService).to(AiService).scope(Scope.Singleton); -export {iocContainer}; \ No newline at end of file +export {iocContainer}; + +export class ExpressWsProvider { + private readonly instance: expressWs.Instance; + + constructor(instance: expressWs.Instance) { + this.instance = instance; + } + + get(): expressWs.Instance { + return this.instance; + } +} \ No newline at end of file diff --git a/backend/src/models/business/AiInstance.ts b/backend/src/models/business/AiInstance.ts index c2a5646..742d897 100644 --- a/backend/src/models/business/AiInstance.ts +++ b/backend/src/models/business/AiInstance.ts @@ -2,6 +2,7 @@ import AiConfiguration from '../db/AiConfiguration'; import DiscordStatus from './DiscordStatus'; import AiPythonConnector from '../../services/AiPythonConnector'; import Semaphore from '../../util/Semaphore'; +import getEmitter from '../../event/emitter'; export type ChatMessage = { role: string, @@ -27,9 +28,9 @@ export default class AiInstance { try { await this._messagesSemaphore.acquire(); this.messages.push({'role': 'user', 'name': user, 'content': text}); - // chat_text_signal.send(sender = this.__class__, ai_name = this.configuration.name, message = this.messages[-1]); + getEmitter().emit('chatText', {aiInstance: this, ...this.messages[this.messages.length - 1]!}); this.messages = await this.aiPythonConnector.chat(this.configuration.modelIdOrPath, this.messages); - // chat_text_signal.send(sender = this.__class__, ai_name = this.configuration.name, message = this.messages[-1]); + getEmitter().emit('chatText', {aiInstance: this, ...this.messages[this.messages.length - 1]!}); return this.messages[this.messages.length - 1]!; } finally { this._messagesSemaphore.release(); diff --git a/backend/src/models/business/DiscordStatus.ts b/backend/src/models/business/DiscordStatus.ts index cb8c5cc..50f24fd 100644 --- a/backend/src/models/business/DiscordStatus.ts +++ b/backend/src/models/business/DiscordStatus.ts @@ -1,14 +1,15 @@ import type AiInstance from './AiInstance'; import DcClient from './DcClient'; +import getEmitter from '../../event/emitter'; export default class DiscordStatus { - // private _instance: AiInstance; + private readonly _instance: AiInstance; private _client: DcClient; private _online = false; private _reactToChat = false; constructor(instance: AiInstance) { - // this._instance = instance; + this._instance = instance; this._client = new DcClient(instance); } @@ -25,7 +26,7 @@ export default class DiscordStatus { return; } this._online = value; - // discord_online_signal.send(sender = this.__class__, ai_name = this._instance.configuration.name, status = this.online); + getEmitter().emit('discordOnline', {aiInstance: this._instance, online: value}); } isReactToChat(): boolean { @@ -34,8 +35,7 @@ export default class DiscordStatus { setReactToChat(value: boolean) { this._reactToChat = value; - // discord_react_to_chat_signal.send(sender = this.__class__, ai_name = this._instance.configuration.name, - // status = this._reactToChat); + getEmitter().emit('discordReactToChat', {aiInstance: this._instance, reactToChat: value}); } async dispose(): Promise { diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 81820dd..f5c6386 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -2,9 +2,19 @@ import {Express, static as _static} from 'express'; import type {ConfigType} from './config/confighelper'; import catchAllRedirect from './middleware/CatchAllRedirect'; import {RegisterRoutes} from './tsoa.gen/routes'; +import expressWs from 'express-ws'; -export function registerRoutes(express: Express, config: ConfigType) { +export function registerRoutes(express: Express, config: ConfigType, ws: expressWs.Instance): void { RegisterRoutes(express); + // (express as any as expressWs.Application).ws('ws/dashboard', (ws, req, next) => isAuthenticatedMiddleware(req, req.res!, next)); + (express as any as expressWs.Application).ws('ws/dashboard', (ws, req) => { + // ws.on('open', function () { + // console.log(this); + // }); + // ws.on('message', function (msg) { + // ws.send(msg); + // }); + }); express.use(_static('_client')); //for production express.use(catchAllRedirect(express, '/')); } diff --git a/backend/src/server.ts b/backend/src/server.ts index 0f7b135..3c59388 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -16,7 +16,10 @@ import {initGlobals} from './util/GlobalInit'; import {seedDb} from './DbSeed'; import {uncaughtErrorHandler} from './middleware/UncaughtErrorHandler'; import compression from 'compression'; -import {iocContainer} from './ioc'; +import {ExpressWsProvider, iocContainer} from './ioc'; +import expressWs from 'express-ws'; +import {Container, Scope} from 'typescript-ioc'; +import {registerEvents} from './event/listener'; export default class Server { // @ts-ignore TS6133 @@ -24,6 +27,7 @@ export default class Server { private config!: ConfigType; private express: Express | undefined; + private expressWsInstance: expressWs.Instance | undefined; private server: http.Server | undefined; private expressHost: string | undefined; private expressPort: number | undefined; @@ -38,6 +42,7 @@ export default class Server { initGlobals(); this.initConfigChangeWatcher(); this.initShutdownHooks(); + registerEvents(); } private initConfigChangeWatcher(): void { @@ -75,6 +80,7 @@ export default class Server { this.expressPort = this.config.server.port ?? 3000; this.express = express(); + this.expressWsInstance = expressWs(this.express); const sessionStore = new DbStore(); @@ -84,12 +90,14 @@ export default class Server { this.express.use(session(getSessionSettings(this.config, sessionStore))); this.express.use(sessionUserdataMiddleware()); this.express.use(express.json()); - registerRoutes(this.express, this.config); - // RegisterRoutes(app); + registerRoutes(this.express, this.config, this.expressWsInstance); this.express.use(errorLogHandler()); this.express.use(apiErrorHandler()); // registerOpenApiLast(this.express); this.express.use(uncaughtErrorHandler()); + + const expressWsProviderFactory = () => new ExpressWsProvider(this.expressWsInstance!); + Container.bind(ExpressWsProvider).factory(expressWsProviderFactory).scope(Scope.Singleton); } private async initDatabase(): Promise { diff --git a/frontend/src/components/dashboard/DashBoard.vue b/frontend/src/components/dashboard/DashBoard.vue index f204164..748aba2 100644 --- a/frontend/src/components/dashboard/DashBoard.vue +++ b/frontend/src/components/dashboard/DashBoard.vue @@ -1,15 +1,19 @@ diff --git a/frontend/src/components/dashboard/WsUpdater.vue b/frontend/src/components/dashboard/WsUpdater.vue new file mode 100644 index 0000000..d175b11 --- /dev/null +++ b/frontend/src/components/dashboard/WsUpdater.vue @@ -0,0 +1,78 @@ + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 877ff9d..ae591ea 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -29,11 +29,15 @@ export default defineConfig({ }, }, clearScreen: false, - server: { + server: { proxy: {//for dev '^/api/.*$': { target: 'http://localhost:5172', changeOrigin: true, + }, '^/ws/.*$': { + target: 'http://localhost:5172', + changeOrigin: true, + ws: true, }, }, },