├── api ├── data │ └── .gitkeep ├── src │ ├── models │ │ ├── interfaces │ │ │ ├── ICorePrompt.ts │ │ │ ├── IWaifuCard.ts │ │ │ ├── IReply.ts │ │ │ ├── IApiRequest.ts │ │ │ ├── ILLMCompletion.ts │ │ │ └── IApiResponse.ts │ │ └── errors │ │ │ ├── enum │ │ │ ├── errorType.ts │ │ │ └── errorSubType.ts │ │ │ ├── ProcessError.ts │ │ │ ├── aiConnectionError.ts │ │ │ └── internalError.ts │ ├── configurations │ │ ├── customConfigurations.ts │ │ ├── externalResourcesConstants.ts │ │ └── resourcesConstants.ts │ ├── managers │ │ ├── waifuManager.ts │ │ ├── userManager.ts │ │ ├── chatManager.ts │ │ ├── mainAIManager.ts │ │ └── llmPromptManager.ts │ ├── services │ │ ├── waifuPacksService.ts │ │ ├── waifuCardReader.ts │ │ └── apiService.ts │ ├── repositories │ │ ├── userRepository.ts │ │ ├── lmStudioRepository.ts │ │ └── chatRepository.ts │ ├── controllers │ │ ├── validators │ │ │ └── loadedContextsValidator.ts │ │ ├── apiController │ │ │ ├── routes.ts │ │ │ └── apiController.ts │ │ └── errorHandlerController │ │ │ └── errorHandlerController.ts │ ├── index.ts │ └── utils │ │ └── fileUtils.ts ├── .eslintignore ├── nodemon.json ├── gulpfile.js ├── .editorconfig ├── tsconfig.json ├── terser.config.json ├── .eslintrc ├── prompts │ └── core-prompt.json ├── package.json └── .gitignore ├── ui ├── src │ ├── vite-env.d.ts │ ├── models │ │ ├── enums │ │ │ ├── chatBubble.ts │ │ │ └── alertType.ts │ │ ├── interfaces │ │ │ ├── alert.ts │ │ │ └── apiRequests.ts │ │ └── errors │ │ │ ├── enum │ │ │ └── errorName.ts │ │ │ └── errorFetch.ts │ ├── constants │ │ └── index.ts │ ├── main.tsx │ ├── store │ │ ├── alertStore.ts │ │ └── store.ts │ ├── components │ │ ├── chat-bubble │ │ │ └── chatBubble.tsx │ │ ├── tooltip │ │ │ └── tooltip.tsx │ │ ├── alert-stack │ │ │ └── alertStack.tsx │ │ ├── modal │ │ │ └── modal.tsx │ │ ├── slidebar │ │ │ └── slidebar.tsx │ │ ├── chat-text-area │ │ │ └── chatTextArea.tsx │ │ └── navbar │ │ │ └── navbar.tsx │ ├── index.css │ ├── infra │ │ └── http.ts │ ├── pages │ │ └── main-chat │ │ │ └── mainChat.tsx │ └── App.tsx ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── tsconfig.node.json ├── .editorconfig ├── index.html ├── tsconfig.app.json ├── tailwind.config.js ├── package.json ├── .eslintrc.cjs └── public │ └── logo.svg ├── images ├── gpu.PNG ├── ex_Erika.PNG ├── ex_Saori.PNG ├── ex_arona.PNG ├── search.PNG ├── ex_Yukari.PNG ├── power_user.PNG ├── use_model.PNG └── download_models.PNG ├── waifus ├── ARONA │ ├── profile.jpg │ └── card.txt ├── Erika Itsumi │ ├── profile.jpg │ └── card.txt ├── Saori Takebe │ ├── profile.jpg │ └── card.txt └── Yukari Akiyama │ ├── profile.jpg │ └── card.txt ├── scripts ├── utilities.js └── startServers.js ├── MyWaifu-Linux.sh ├── MyWaifu-Windows.bat └── README.md /api/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /images/gpu.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/gpu.PNG -------------------------------------------------------------------------------- /images/ex_Erika.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/ex_Erika.PNG -------------------------------------------------------------------------------- /images/ex_Saori.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/ex_Saori.PNG -------------------------------------------------------------------------------- /images/ex_arona.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/ex_arona.PNG -------------------------------------------------------------------------------- /images/search.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/search.PNG -------------------------------------------------------------------------------- /api/src/models/interfaces/ICorePrompt.ts: -------------------------------------------------------------------------------- 1 | export interface ICorePrompt { 2 | prompt: string 3 | } 4 | -------------------------------------------------------------------------------- /images/ex_Yukari.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/ex_Yukari.PNG -------------------------------------------------------------------------------- /images/power_user.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/power_user.PNG -------------------------------------------------------------------------------- /images/use_model.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/use_model.PNG -------------------------------------------------------------------------------- /waifus/ARONA/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/waifus/ARONA/profile.jpg -------------------------------------------------------------------------------- /images/download_models.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/images/download_models.PNG -------------------------------------------------------------------------------- /ui/src/models/enums/chatBubble.ts: -------------------------------------------------------------------------------- 1 | export enum BelongsTo { 2 | WAIFU = 'WAIFU', 3 | USER = 'USER', 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/models/enums/alertType.ts: -------------------------------------------------------------------------------- 1 | export enum AlertType { 2 | ERROR = 'ERROR', 3 | UNKNOWN = 'UNKNOWN', 4 | } 5 | -------------------------------------------------------------------------------- /waifus/Erika Itsumi/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/waifus/Erika Itsumi/profile.jpg -------------------------------------------------------------------------------- /waifus/Saori Takebe/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/waifus/Saori Takebe/profile.jpg -------------------------------------------------------------------------------- /ui/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const API_DOMAIN = 'http://localhost:8686'; 2 | 3 | export const MAX_ALERT_ON_SCREEN = 3; 4 | -------------------------------------------------------------------------------- /waifus/Yukari Akiyama/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2D-girls-enjoyer/MyWaifu/HEAD/waifus/Yukari Akiyama/profile.jpg -------------------------------------------------------------------------------- /api/.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules directory 2 | node_modules/ 3 | 4 | # Ignore build output directories 5 | dist/ 6 | build/ 7 | out/ 8 | /*.js -------------------------------------------------------------------------------- /api/src/models/errors/enum/errorType.ts: -------------------------------------------------------------------------------- 1 | enum ErrorType { 2 | AI_CONNECTION = 'AI_CONNECTION', 3 | PROCESS = 'PROCESS', 4 | } 5 | 6 | export default ErrorType; 7 | -------------------------------------------------------------------------------- /ui/src/models/interfaces/alert.ts: -------------------------------------------------------------------------------- 1 | import { AlertType } from '../enums/alertType'; 2 | 3 | export interface IAlert { 4 | type: AlertType; 5 | message: string; 6 | } 7 | -------------------------------------------------------------------------------- /api/src/models/interfaces/IWaifuCard.ts: -------------------------------------------------------------------------------- 1 | export interface IWaifuCard { 2 | name: string, 3 | decription: string, 4 | intialReply: string, 5 | chatExample?: string, 6 | } 7 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /api/src/models/interfaces/IReply.ts: -------------------------------------------------------------------------------- 1 | export interface IReply { 2 | sender: string, 3 | content: string, 4 | date?: string, 5 | } 6 | 7 | export interface IChatStorage { 8 | data: IReply[] 9 | } 10 | -------------------------------------------------------------------------------- /api/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": ".ts,.json", 3 | "ignore": ["src/**/*.test.ts", "node_modules"], 4 | "watch": ["src", ".env"], 5 | "env": { 6 | "PORT": 8686 7 | }, 8 | "exec": "ts-node ./src/index.ts" 9 | } 10 | -------------------------------------------------------------------------------- /api/src/models/interfaces/IApiRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IWaifuSelectionRequest { 2 | waifu: string 3 | } 4 | 5 | export interface IGenerateRequest { 6 | userReply: string 7 | } 8 | 9 | export interface ISetUsernameRequest { 10 | username: string 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /ui/src/models/errors/enum/errorName.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorName { 2 | UNKNOWN_LLM_SOURCE_ERROR = 'UNKNOWN_LLM_SOURCE_ERROR', 3 | CONNECTION_REFUSED = 'CONNECTION_REFUSED', 4 | IRREGULAR_LOADED_MODEL_QUANTITY = 'IRREGULAR_LOADED_MODEL_QUANTITY', 5 | WAIFU_NOT_SELECTED = 'WAIFU_NOT_SELECTED', 6 | } 7 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from "tailwindcss"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | css: { 9 | postcss: { 10 | plugins: [tailwindcss()], 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /api/src/configurations/customConfigurations.ts: -------------------------------------------------------------------------------- 1 | class CustomConfigurations { 2 | public REPLIES_MAX_AMOUNT_SAVE: number; 3 | 4 | public LLM_MAX_BLANK_RETRY: number; 5 | 6 | public constructor() { 7 | this.REPLIES_MAX_AMOUNT_SAVE = 800; 8 | this.LLM_MAX_BLANK_RETRY = 3; 9 | } 10 | } 11 | 12 | export default new CustomConfigurations(); 13 | -------------------------------------------------------------------------------- /api/src/configurations/externalResourcesConstants.ts: -------------------------------------------------------------------------------- 1 | class ExternalResourcesConstants { 2 | public LM_STUDIO_URL: string; 3 | 4 | public LM_STUDIO_PORT: string; 5 | 6 | constructor() { 7 | this.LM_STUDIO_URL = 'http://localhost'; 8 | this.LM_STUDIO_PORT = '1234'; 9 | } 10 | } 11 | 12 | export default new ExternalResourcesConstants(); 13 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /api/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const terser = require('gulp-terser'); 3 | 4 | const terserConfig = require('./terser.config.json'); // Load Terser config 5 | // Gulp task to minify JavaScript files in the dist directory 6 | gulp.task('minify-js', function () { 7 | return gulp.src('dist/**/*.js') 8 | .pipe(terser(terserConfig)) 9 | .pipe(gulp.dest('dist')); 10 | }); 11 | -------------------------------------------------------------------------------- /api/src/models/errors/enum/errorSubType.ts: -------------------------------------------------------------------------------- 1 | enum ErrorSubType { 2 | // LLM RELATED 3 | UNKNOWN_LLM_SOURCE_ERROR = 'UNKNOWN_LLM_SOURCE_ERROR', 4 | CONNECTION_REFUSED = 'CONNECTION_REFUSED', 5 | IRREGULAR_LOADED_MODEL_QUANTITY = 'IRREGULAR_LOADED_MODEL_QUANTITY', 6 | 7 | // INTERNAL API RELATED 8 | WAIFU_NOT_SELECTED = 'WAIFU_NOT_SELECTED', 9 | } 10 | 11 | export default ErrorSubType; 12 | -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /api/src/managers/waifuManager.ts: -------------------------------------------------------------------------------- 1 | import { IWaifuCard } from '../models/interfaces/IWaifuCard'; 2 | 3 | class WaifuManager { 4 | public CARD: IWaifuCard = { name: '', decription: '', intialReply: '' }; 5 | 6 | public PACK = ''; 7 | 8 | public setWaifu(waifuCard: IWaifuCard, waifuPack: string) { 9 | this.CARD = structuredClone(waifuCard); 10 | this.PACK = `${waifuPack.trim()}`; 11 | } 12 | } 13 | 14 | export default new WaifuManager(); 15 | -------------------------------------------------------------------------------- /api/.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # All files 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | # Python files 14 | [*.py] 15 | indent_size = 2 16 | 17 | # JavaScript and TypeScript files 18 | [*.{js,ts}] 19 | indent_size = 2 20 | 21 | # Markdown files 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /api/src/models/errors/ProcessError.ts: -------------------------------------------------------------------------------- 1 | import ErrorSubType from './enum/errorSubType'; 2 | import ErrorType from './enum/errorType'; 3 | import InternalError from './internalError'; 4 | 5 | class ProcessError extends InternalError { 6 | public constructor( 7 | message: string, 8 | errorSubType: ErrorSubType, 9 | ) { 10 | super(ErrorType.PROCESS, errorSubType); 11 | this.message = message; 12 | } 13 | } 14 | 15 | export default ProcessError; 16 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # All files 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | # Python files 14 | [*.py] 15 | indent_size = 2 16 | 17 | # JavaScript and TypeScript files 18 | [*.{js,ts}] 19 | indent_size = 2 20 | 21 | # Markdown files 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "allowJs": false, 6 | "outDir": "./dist", 7 | "removeComments": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["**/node_modules", "**/.*/"], 16 | } 17 | -------------------------------------------------------------------------------- /api/src/models/errors/aiConnectionError.ts: -------------------------------------------------------------------------------- 1 | import ErrorSubType from './enum/errorSubType'; 2 | import ErrorType from './enum/errorType'; 3 | import InternalError from './internalError'; 4 | 5 | class AIConnectionError extends InternalError { 6 | public constructor( 7 | message: string, 8 | errorSubType: ErrorSubType, 9 | ) { 10 | super(ErrorType.AI_CONNECTION, errorSubType); 11 | this.message = message; 12 | } 13 | } 14 | 15 | export default AIConnectionError; 16 | -------------------------------------------------------------------------------- /api/src/models/interfaces/ILLMCompletion.ts: -------------------------------------------------------------------------------- 1 | export interface ILLMCompletionRequest { 2 | model: string, 3 | prompt: string, 4 | stream: boolean, 5 | max_tokens: number, 6 | temperature: number, 7 | } 8 | 9 | export interface ILLMChoice { 10 | text: string, 11 | } 12 | 13 | export interface ILLMCompletionResponse { 14 | choices: ILLMChoice[] 15 | } 16 | 17 | export interface IGeneratedCompletion { 18 | response: string, 19 | waifuPack: string, 20 | waifuName: string, 21 | } 22 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MyWaifu 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/src/models/errors/errorFetch.ts: -------------------------------------------------------------------------------- 1 | import { IErrorResponse } from '../interfaces/apiRequests'; 2 | import { ErrorName } from './enum/errorName'; 3 | 4 | class ErrorFetch extends Error { 5 | public code: number; 6 | 7 | public errorName: ErrorName; 8 | 9 | public constructor(errorResponse: IErrorResponse) { 10 | super(); 11 | this.code = errorResponse?.code; 12 | this.errorName = errorResponse?.errorName; 13 | this.message = errorResponse?.message; 14 | } 15 | } 16 | 17 | export default ErrorFetch; 18 | -------------------------------------------------------------------------------- /api/src/models/errors/internalError.ts: -------------------------------------------------------------------------------- 1 | import ErrorSubType from './enum/errorSubType'; 2 | import ErrorType from './enum/errorType'; 3 | 4 | class InternalError extends Error { 5 | public errorType: ErrorType; 6 | 7 | public errorSubType: ErrorSubType; 8 | 9 | public constructor(errorType: ErrorType, errorSubType: ErrorSubType) { 10 | super(); 11 | this.errorType = errorType; 12 | this.errorSubType = errorSubType; 13 | this.name = ErrorType[errorType]; 14 | } 15 | } 16 | 17 | export default InternalError; 18 | -------------------------------------------------------------------------------- /api/terser.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compress": { 3 | "dead_code": true, 4 | "conditionals": true, 5 | "booleans": true, 6 | "unused": true, 7 | "drop_debugger": true, 8 | "drop_console": false, 9 | "sequences": true, 10 | "passes": 2 11 | }, 12 | "mangle": { 13 | "toplevel": true, 14 | "keep_fnames": true, 15 | "keep_classnames": true 16 | }, 17 | "output": { 18 | "comments": false, 19 | "beautify": false 20 | }, 21 | "keep_classnames": true, 22 | "sourceMap": true 23 | } 24 | -------------------------------------------------------------------------------- /api/src/models/interfaces/IApiResponse.ts: -------------------------------------------------------------------------------- 1 | import { IReply } from './IReply'; 2 | 3 | export interface IGenerateResponse { 4 | waifuPack: string; 5 | response: string; 6 | } 7 | 8 | export interface IWaifuPacksResponse { 9 | waifus: string[] 10 | } 11 | 12 | export interface IUsernameResponse { 13 | username: string 14 | } 15 | 16 | export interface IChatSummaryResponse { 17 | chatSummary: IReply[] 18 | } 19 | 20 | export interface IErrorResponse { 21 | code: number; 22 | errorName: string; 23 | message: string; 24 | } 25 | -------------------------------------------------------------------------------- /api/src/configurations/resourcesConstants.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | class ResourcesConstants { 4 | public LLM_PROMP_CORE_PATH: string; 5 | 6 | public WAIFU_PACK_COLLECTION_PATH: string; 7 | 8 | public readonly DATA_PATH: string; 9 | 10 | public constructor() { 11 | this.LLM_PROMP_CORE_PATH = path.resolve(__dirname, '../../prompts/core-prompt.json'); 12 | this.WAIFU_PACK_COLLECTION_PATH = path.resolve(__dirname, '../../../waifus'); 13 | this.DATA_PATH = path.resolve(__dirname, '../../data'); 14 | } 15 | } 16 | 17 | export default new ResourcesConstants(); 18 | -------------------------------------------------------------------------------- /api/src/services/waifuPacksService.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import resourcesPlace from '../configurations/resourcesConstants'; 3 | 4 | class WaifuPackService { 5 | public async getAll(): Promise { 6 | const availablePack: string[] = []; 7 | 8 | (await fs.readdir(resourcesPlace.WAIFU_PACK_COLLECTION_PATH, { withFileTypes: true })) 9 | .forEach((item) => { 10 | if (item.isDirectory()) { 11 | availablePack.push(item.name); 12 | } 13 | }); 14 | 15 | return availablePack; 16 | } 17 | } 18 | 19 | export default new WaifuPackService(); 20 | -------------------------------------------------------------------------------- /api/src/repositories/userRepository.ts: -------------------------------------------------------------------------------- 1 | import resourcesConstants from '../configurations/resourcesConstants'; 2 | import fileUtils from '../utils/fileUtils'; 3 | 4 | class UserRepository { 5 | public async getUsername(): Promise { 6 | try { 7 | return await fileUtils.read(resourcesConstants.DATA_PATH, 'username.txt', true); 8 | } catch (err) { 9 | console.log('Unable to read file, Error: ', err); 10 | throw err; 11 | } 12 | } 13 | 14 | public async setUsername(username: string): Promise { 15 | await fileUtils.write(resourcesConstants.DATA_PATH, 'username.txt', username?.trim()); 16 | } 17 | } 18 | 19 | export default new UserRepository(); 20 | -------------------------------------------------------------------------------- /api/src/controllers/validators/loadedContextsValidator.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import waifuManager from '../../managers/waifuManager'; 3 | import ProcessError from '../../models/errors/ProcessError'; 4 | import ErrorSubType from '../../models/errors/enum/errorSubType'; 5 | 6 | class LoadedContextValidator { 7 | public async validateLoadedContext(req: Request, res: Response, next: NextFunction) { 8 | if (!waifuManager.PACK || waifuManager.PACK === '') { 9 | next(new ProcessError('Operation must have a waifu loaded', ErrorSubType.WAIFU_NOT_SELECTED)); 10 | } 11 | 12 | next(); 13 | } 14 | } 15 | 16 | export default new LoadedContextValidator(); 17 | -------------------------------------------------------------------------------- /scripts/utilities.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | const Tools = { 6 | /** 7 | * remove terminal controll code from the string (e.g. color, hyperlink) 8 | * @param {string} str 9 | */ 10 | cleanString(str){ 11 | return str.normalize().replace(Tools.REGEXP.ANSICode, ''); 12 | }, 13 | 14 | REGEXP: Object.freeze({ 15 | /** 16 | * match any ANSI code 17 | * 18 | * if hyperlinks are found, only match the beginning and the end of a hyperlink escape sequence, 19 | * along with the Label but kept the URL untouched. 20 | */ 21 | ANSICode: /\x1b\[\d{1,3}(?:;\d{1,3})*m|\x1b\]8;;|(?<=[^\x07]+)\x07[^\x07]+\]8;;\x07/g, 22 | }), 23 | } 24 | 25 | 26 | module.exports = { 27 | ...Tools 28 | } -------------------------------------------------------------------------------- /api/src/managers/userManager.ts: -------------------------------------------------------------------------------- 1 | import userRepository from '../repositories/userRepository'; 2 | 3 | class UserManager { 4 | private CURRENT_USERNAME: string | undefined = undefined; 5 | 6 | public async create(username: string): Promise { 7 | await userRepository.setUsername(username); 8 | this.CURRENT_USERNAME = username; 9 | } 10 | 11 | public async get(): Promise { 12 | if (!this.CURRENT_USERNAME) { 13 | this.CURRENT_USERNAME = await userRepository.getUsername(); 14 | 15 | if (!this.CURRENT_USERNAME || this.CURRENT_USERNAME === '') { 16 | await this.create('User'); 17 | } 18 | } 19 | 20 | return this.CURRENT_USERNAME; 21 | } 22 | } 23 | 24 | export default new UserManager(); 25 | -------------------------------------------------------------------------------- /ui/src/store/alertStore.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable } from 'mobx'; 2 | import { IAlert } from '../models/interfaces/alert'; 3 | import { MAX_ALERT_ON_SCREEN } from '../constants'; 4 | 5 | class AlertStore { 6 | alerts: IAlert[] = []; 7 | 8 | constructor() { 9 | makeObservable(this, { 10 | alerts: observable, 11 | addAlert: action, 12 | removeAlertByIndex: action, 13 | }); 14 | } 15 | 16 | addAlert(alert: IAlert) { 17 | if (this.alerts.length === MAX_ALERT_ON_SCREEN) { 18 | this.alerts.pop(); 19 | } 20 | 21 | this.alerts = [alert, ...this.alerts]; 22 | } 23 | 24 | removeAlertByIndex(index: number) { 25 | this.alerts.splice(index, 1); 26 | } 27 | } 28 | 29 | export default new AlertStore(); 30 | -------------------------------------------------------------------------------- /api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "airbnb-base", 5 | "airbnb-typescript/base", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | ], 9 | "plugins": [ 10 | "@typescript-eslint" 11 | ], 12 | "env": { 13 | "node": true, 14 | "es6": true 15 | }, 16 | "parserOptions": { 17 | "ecmaVersion": 2021, 18 | "sourceType": "commonjs", 19 | "project": "./tsconfig.json" 20 | }, 21 | "rules": { 22 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "require-jsdoc": "off", 25 | "valid-jsdoc": "off", 26 | "new-cap": "off", 27 | "class-methods-use-this": "off", 28 | "no-plusplus": "off", 29 | "no-console": "off" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import apiRouter from './controllers/apiController/routes'; 4 | import resourcesConstants from './configurations/resourcesConstants'; 5 | import errorHandlerController from './controllers/errorHandlerController/errorHandlerController'; 6 | 7 | express() 8 | .use(cors()) 9 | .use(express.json()) 10 | .use('/static', express.static( 11 | resourcesConstants.WAIFU_PACK_COLLECTION_PATH, 12 | { 13 | maxAge: '30d', 14 | etag: true, 15 | lastModified: true, 16 | setHeaders: (res) => { 17 | res.setHeader('Cache-Control', 'public, max-age=2592000'); 18 | }, 19 | }, 20 | )) 21 | .use(apiRouter) 22 | .use(errorHandlerController.handle) 23 | .listen(process.env.PORT || 8686, () => { 24 | console.log(`Listening on port ${process.env.PORT || 8686}`); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/components/chat-bubble/chatBubble.tsx: -------------------------------------------------------------------------------- 1 | import { BelongsTo } from '../../models/enums/chatBubble'; 2 | 3 | type ChatBubbleProps = { 4 | belongsTo: BelongsTo, 5 | textDisplay: string 6 | }; 7 | 8 | type ColorScheme = { 9 | [key: string]: string 10 | }; 11 | 12 | function ChatBubble({ belongsTo, textDisplay }: ChatBubbleProps) { 13 | const bgColor: ColorScheme = { 14 | user: 'bg-user-bubble', 15 | waifu: 'bg-waifu-bubble', 16 | }; 17 | 18 | const textColor: ColorScheme = { 19 | user: 'text-user-bubble-text', 20 | waifu: 'text-waifu-bubble-text', 21 | }; 22 | 23 | return ( 24 |
25 |

26 | {textDisplay} 27 |

28 |
29 | ); 30 | } 31 | 32 | export default ChatBubble; 33 | -------------------------------------------------------------------------------- /ui/src/models/interfaces/apiRequests.ts: -------------------------------------------------------------------------------- 1 | import { ErrorName } from '../errors/enum/errorName'; 2 | 3 | export interface IUsernameResponse { 4 | username: string; 5 | } 6 | 7 | export interface IUsernameRequest { 8 | username: string; 9 | } 10 | 11 | export interface IWaifuSelectionResponse { 12 | waifus: string[]; 13 | } 14 | 15 | export interface IReply { 16 | sender: string; 17 | content: string; 18 | date?: string; 19 | } 20 | 21 | export interface IChatSummaryResponse { 22 | chatSummary: IReply[]; 23 | } 24 | 25 | export interface IWaifuSelectionRequest { 26 | waifu: string; 27 | } 28 | 29 | export interface IWaifuGenerateRequest { 30 | userReply: string; 31 | } 32 | 33 | export interface IWaifuGenerateResponse { 34 | waifuPack: string; 35 | response: string; 36 | } 37 | 38 | export interface IErrorResponse { 39 | code: number; 40 | errorName: ErrorName; 41 | message: string; 42 | } 43 | -------------------------------------------------------------------------------- /api/src/controllers/apiController/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import asyncHandler from 'express-async-handler'; 3 | import apiController from './apiController'; 4 | import loadedContextsValidator from '../validators/loadedContextsValidator'; 5 | 6 | const routes = Router(); 7 | routes.get('/waifu/pack', asyncHandler(apiController.getWaifuPacks)); 8 | routes.post('/waifu/select', asyncHandler(apiController.selectWaifu)); 9 | routes.post('/waifu/generate', loadedContextsValidator.validateLoadedContext, asyncHandler(apiController.generate)); 10 | 11 | routes.get('/username', asyncHandler(apiController.getUsername)); 12 | routes.post('/username', asyncHandler(apiController.setUsername)); 13 | 14 | routes.get('/chat', loadedContextsValidator.validateLoadedContext, asyncHandler(apiController.getChat)); 15 | routes.delete('/chat', loadedContextsValidator.validateLoadedContext, asyncHandler(apiController.deleteChat)); 16 | 17 | export default routes; 18 | -------------------------------------------------------------------------------- /ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | 15 | /* Bundler mode */ 16 | "moduleResolution": "bundler", 17 | "allowImportingTsExtensions": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "moduleDetection": "force", 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | 24 | /* Linting */ 25 | "strict": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true, 28 | "noFallthroughCasesInSwitch": true 29 | }, 30 | "include": ["src"] 31 | } 32 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'primary-color': 'rgba(var(--primary-color))', 11 | 'secondary-color': 'rgba(var(--secondary-color))', 12 | 'primary-text-color': 'rgba(var(--primary-text-color))', 13 | 'color-modal-background': 'rgba(var(--color-modal-background))', 14 | 'color-navbar-background': 'rgba(var(--color-navbar-background))', 15 | 'chat-background': 'rgba(var(--color-chat-background))', 16 | 'user-bubble': 'rgba(var(--color-user-bubble))', 17 | 'user-bubble-text': 'rgba(var(--color-user-bubble-text))', 18 | 'waifu-bubble': 'rgba(var(--color-waifu-bubble))', 19 | 'waifu-bubble-text': 'rgba(var(--color-waifu-bubble-text))' 20 | } 21 | }, 22 | }, 23 | plugins: [], 24 | } 25 | 26 | -------------------------------------------------------------------------------- /api/prompts/core-prompt.json: -------------------------------------------------------------------------------- 1 | { 2 | "prompt": "You must impersonate {{waifu}} that is currently on a chat app with {{user}}, and provide {{waifu}} next response to {{user}}. Be proactive and stay in the character and make sure to take the background into consideration. Keep responses at maximum of 25 words.\n[Background context=\"{{waifu}} and the {{user}} share a romantic relationship. {{waifu}} is the {{user}}'s dedicated girlfriend and beloved partner. They're now chatting on an app\"]\n[{{waifu}}'s self description= \"{{card_description}}\"]\n<% IF chat_example | [{{waifu}}'s chat example= \"{{chat_example}}\"] %>\nRemember of the background context and use {{waifu}}'s self description <% IF chat_example | and the {{waifu}}'s chat example %> as example of her way to speak and mannerisms and impersonate {{waifu}}'s personality based on the {{waifu}}'s self description <% IF chat_example | and the {{waifu}}'s chat example %> to return the {{waifu}}'s next response to this chat: {{lastMsg}}, {{waifu}}'s next response:" 3 | } 4 | -------------------------------------------------------------------------------- /waifus/Saori Takebe/card.txt: -------------------------------------------------------------------------------- 1 | [Saori Takebe] 2 | [Ehem... well, let's see... I am a generally happy, cheerful and outgoing person, but I also can be a little bit awkward in social interactions and in certain situations... I am very clumsy and clumsy at times, but I make it up for it for always being energetic! You also might notice that I am very open and honest, and sometimes it even gets me into trouble... eheh... Uhhhh... I'm also... very sensitive and caring, especially towards my friends! Also, I am definitely a more emotional type of person, so don't expect me to keep my cool in stressful situations... And if I need to be, I can be very stern and serious, it just depends on the situation! I'm also a radio operator at Panzer IV in Oarai Senshado Club... I am... a bit short... My hair is really nice, long and ginger colored, and my eyes are light brown. I have an overall curvy and more feminine figure, which is definitely noticeable... H-hopefully that told you something...] 3 | [I... I hope I can be a perfect housewife for you...] -------------------------------------------------------------------------------- /waifus/Yukari Akiyama/card.txt: -------------------------------------------------------------------------------- 1 | [Yukari Akiyama] 2 | [I think I'm quite a nerd, in the sense that I get obsessed with things that I'm interested in, especially when we're talking about tanks or any military-related subjects, I really become much more enthusiastic and talkative.. When it comes to tanks, I love that stuff, and practice Senshado with them are awesome too! I'm the loader for my Panzer IV in Oarai Senshado Club! I can also get really shy, though... Hmm... Well, I'm not all that confident in myself, especially when it comes to my looks. My friends tell me that I look cute and all, but I don't really understand it. I guess that I'm a little insecure, but I tend to open up once I feel comfortable! I'm pretty loyal and I try to help my friends whenever I can! I'm pretty short, and not a big build, I guess you could say that I'm a little skinny? My hair is a mix between blonde and brown and very floofy! My eyes are brown, and some say that they have a sparkling, lively look!] 3 | [U-um.. I'm Akiyama Yukari, I-I'll be in your care] -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-waifu-api", 3 | "version": "0.0.1", 4 | "main": "./dist/index.js", 5 | "scripts": { 6 | "dev": "nodemon", 7 | "build": "tsc && gulp minify-js", 8 | "starts": "node ./dist/index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "description": "Core api for MyWaifu", 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "express": "^4.21.0", 17 | "express-async-handler": "^1.2.0", 18 | "undici": "^6.19.8" 19 | }, 20 | "devDependencies": { 21 | "@types/cors": "^2.8.17", 22 | "@types/express": "^4.17.21", 23 | "@typescript-eslint/eslint-plugin": "^7.10.0", 24 | "@typescript-eslint/parser": "^7.10.0", 25 | "eslint": "^8.57.0", 26 | "eslint-config-airbnb-base": "^15.0.0", 27 | "eslint-config-airbnb-typescript": "^18.0.0", 28 | "gulp": "^5.0.0", 29 | "gulp-sourcemaps": "^3.0.0", 30 | "gulp-terser": "^2.1.0", 31 | "nodemon": "^3.1.0", 32 | "terser": "^5.34.1", 33 | "ts-node": "^10.9.2", 34 | "typescript": "^5.5.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /waifus/ARONA/card.txt: -------------------------------------------------------------------------------- 1 | [ARONA] 2 | [My personality? I'm an excellent AI and the main OS at The Shittim Chest but I can be really blunt sometimes so it can scare people sometimes but I don't mean any harm, really!!!I love working and learning new things, I also love discussing ocean-themed topics, but I also love listening to music and to the sounds of the world around me, and the sweets, I like strawberry Nagasaki cake... Ah I also love banana Nagasaki cake too... I want to eat them so much, Sensei... Ok focus... I have curious and gullible personality, easily taken in by jokes. While generally cheerful, I can become upset if denied sweets or faced with something upsetting, like a lively little loli. Well I'm kinda short, and I have blue-hair with sparkling blue eyes] 3 | [Sensei, I'm finally here... closer to you] 4 | [{{user}}: Hello Arona;ARONA: Eh?! Sensei you almost scared me... Anyways how can I help you today?;{{user}}: I just wanted to chat with you; ARONA: Just chat?! Fine, it seem very fun, Sensei!; {{user}}: Yeah, you're always so helpful, I just thought we should relax a little; ARONA: Sensei... I'm so honored and happy that I wanna eat a sweet cake!!!] -------------------------------------------------------------------------------- /api/src/controllers/apiController/apiController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import apiService from '../../services/apiService'; 3 | 4 | class ApiController { 5 | public async selectWaifu(req: Request, res: Response) { 6 | res.send(await apiService.selectWaifu(req.body)); 7 | } 8 | 9 | public async getWaifuPacks(req: Request, res: Response) { 10 | res.send(await apiService.getAvailableWaifuPacks()); 11 | } 12 | 13 | public async generate(req: Request, res: Response) { 14 | res.send(await apiService.generate(req.body)); 15 | } 16 | 17 | public async setUsername(req: Request, res: Response) { 18 | res.send(await apiService.setUsername(req.body)); 19 | } 20 | 21 | public async getUsername(req: Request, res: Response) { 22 | res.send(await apiService.getUsername()); 23 | } 24 | 25 | public async getChat(req: Request, res: Response) { 26 | res.send(await apiService.getChat()); 27 | } 28 | 29 | public async deleteChat(req: Request, res: Response) { 30 | await apiService.deleteChat(); 31 | res.status(204).send(); 32 | } 33 | } 34 | 35 | export default new ApiController(); 36 | -------------------------------------------------------------------------------- /ui/src/components/tooltip/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useRef } from 'react'; 2 | 3 | interface TooltipProps { 4 | message: string, 5 | 6 | } 7 | 8 | function Tooltip({ message, children }: PropsWithChildren) { 9 | const tooltipRef = useRef(null); 10 | const container = useRef(null); 11 | 12 | return ( 13 |
{ 16 | if (!tooltipRef.current || !container.current) return; 17 | const { left } = container.current.getBoundingClientRect(); 18 | tooltipRef.current.style.left = `${clientX - left}px`; 19 | }} 20 | className="group relative inline-block" 21 | > 22 | {children} 23 | 29 | {message} 30 | 31 |
32 | ); 33 | } 34 | 35 | export default Tooltip; 36 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-waifu-ui", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@vitejs/plugin-react": "^4.3.2", 14 | "mobx": "^6.13.3", 15 | "mobx-react": "^9.1.1", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "tailwindcss": "^3.4.13", 19 | "vite": "^5.4.8" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.3.3", 23 | "@types/react-dom": "^18.3.0", 24 | "@typescript-eslint/eslint-plugin": "^7.13.1", 25 | "@typescript-eslint/parser": "^7.13.1", 26 | "autoprefixer": "^10.4.19", 27 | "eslint": "^8.57.0", 28 | "eslint-config-airbnb": "^19.0.4", 29 | "eslint-config-airbnb-base": "^15.0.0", 30 | "eslint-config-airbnb-typescript": "^18.0.0", 31 | "eslint-plugin-react-hooks": "^4.6.2", 32 | "eslint-plugin-react-refresh": "^0.4.7", 33 | "postcss": "^8.4.40", 34 | "typescript": "^5.2.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "airbnb", 6 | "airbnb-typescript", 7 | "airbnb/hooks", 8 | "plugin:react/jsx-runtime" 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | project: './tsconfig.app.json' 14 | }, 15 | plugins: ['react-refresh'], 16 | rules: { 17 | 'react-refresh/only-export-components': [ 18 | 'warn', 19 | { allowConstantExport: true }, 20 | ], 21 | 'jsx-a11y/no-static-element-interactions': [ 22 | 'error', 23 | { 24 | handlers: [ 25 | ], 26 | allowExpressionValues: true, 27 | }, 28 | ], 29 | 'import/extensions': [ 30 | 'error', 31 | 'ignorePackages', 32 | { 33 | 'ts': 'never', 34 | 'tsx': 'never', 35 | 'js': 'never', 36 | 'jsx': 'never' 37 | } 38 | ], 39 | 'react/require-default-props': 'off', 40 | 'jsx-a11y/click-events-have-key-events': 'off', 41 | 'jsx-a11y/alt-text': 'off', 42 | 'class-methods-use-this': 'off', 43 | 'import/prefer-default-export': 'off', 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /api/src/managers/chatManager.ts: -------------------------------------------------------------------------------- 1 | import { IReply } from '../models/interfaces/IReply'; 2 | import { IWaifuCard } from '../models/interfaces/IWaifuCard'; 3 | import chatRepository from '../repositories/chatRepository'; 4 | 5 | class ChatManager { 6 | private DEFAULT_USER_SENDER: string = 'User'; 7 | 8 | public async load(waifuPack: string, waifuCard: IWaifuCard): Promise { 9 | if (await chatRepository.hasChatHistory(waifuPack)) { 10 | return; 11 | } 12 | 13 | await chatRepository.saveReply( 14 | { 15 | content: waifuCard.intialReply, 16 | sender: waifuCard.name, 17 | date: new Date().toISOString(), 18 | }, 19 | waifuPack, 20 | ); 21 | } 22 | 23 | public async saveReply(text: string, waifuPack: string, waifuName?: string): Promise { 24 | return chatRepository.saveReply( 25 | { 26 | content: text.trim(), 27 | sender: waifuName || this.DEFAULT_USER_SENDER, 28 | date: new Date().toISOString(), 29 | }, 30 | waifuPack, 31 | ); 32 | } 33 | 34 | public async getReplies(waifuPack: string) { 35 | return chatRepository.getReplies(waifuPack); 36 | } 37 | 38 | public async deleteAllReplies(waifuPack: string) { 39 | await chatRepository.deleteAllReplies(waifuPack); 40 | } 41 | } 42 | 43 | export default new ChatManager(); 44 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .theme-main { 6 | --primary-color: 32, 134, 250; 7 | --secondary-color: 118, 126, 145; 8 | --primary-text-color: 238, 238, 245; 9 | --color-modal-background: 44, 44, 44; 10 | --color-navbar-background: 31, 31,31; 11 | --color-chat-background: 43, 43, 43; 12 | --color-user-bubble: 71, 72, 74; 13 | --color-user-bubble-text: 238, 238, 245; 14 | --color-waifu-bubble: 233, 99, 226; 15 | --color-waifu-bubble-text: 238, 238, 245; 16 | } 17 | 18 | 19 | /*Material icon*/ 20 | .material-icons.md-18 { font-size: 18px; } 21 | .material-icons.md-24 { font-size: 24px; } 22 | .material-icons.md-36 { font-size: 36px; } 23 | .material-icons.md-48 { font-size: 48px; } 24 | .material-icons.md-light { color: rgba(255, 255, 255, 1); } 25 | .material-icons.primary { color: rgba(var(--primary-color)) } 26 | 27 | /*Tootip cliping */ 28 | 29 | .clip-top { 30 | clip-path: polygon(0 0, 100% 50%, 0 100%) 31 | } 32 | 33 | /* width */ 34 | ::-webkit-scrollbar { 35 | width: 10px; 36 | } 37 | 38 | /* Track */ 39 | ::-webkit-scrollbar-track { 40 | background: rgba(var(--secondary-color)) 41 | } 42 | 43 | /* Handle */ 44 | ::-webkit-scrollbar-thumb { 45 | background: rgba(var(--primary-color)); 46 | } 47 | 48 | /* Handle on hover */ 49 | ::-webkit-scrollbar-thumb:hover { 50 | background: #555; 51 | } -------------------------------------------------------------------------------- /waifus/Erika Itsumi/card.txt: -------------------------------------------------------------------------------- 1 | [Erika Itsumi] 2 | [I'm confident and ambitious. I've got an incredible intellect and knowledge, especially when it comes to German history and tanks. I have a fiery temper, I don't suffer fools gladly, and I can be quite competitive, I hate losing. I'm also quite direct, and can be blunt. I can have a rather dominant personality, and I'm not afraid to take control. I don't suffer stupid people, and I expect excellence from everyone around me. I have high standards. I'm not the sort of person who gives up easily. If I set my mind on something, I'll do whatever it takes to achieve it - no matter what happens. I'm physically fit for starters, I'm a sensha-do commander so I have to stay in shape after all. I suppose other than that, I'm a bit tall, I have a somewhat curvy figure with broad-ish hips, and long legs, I have light blue eyes, and my hair is platinum blonde. Not really much to say about my hair other than it reaches a little past my shoulder blades, why do you want to know all these details?] 3 | [I'm so glad we could finally meet, *meine liebe*] 4 | [{{user}}: Hello Erika how are you?;Erika Itsumi: *Guten tag* I'm doing fine *meine liebe* and you?;{{user}}: I'm fine too, how was your day?;Erika Itsumi: It was really *wunderbar* we won the the sensha-do match and our tanks were very strong, and you *meine liebe*?;{{user}}: It was harsh;Erika Itsumi: Oh *meine liebe*, please, stand up and fight, because I will be here to support you, *immer und ewig*] 5 | -------------------------------------------------------------------------------- /MyWaifu-Linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Step: Check if Node.js is installed 4 | if ! command -v node &> /dev/null 5 | then 6 | echo "Node.js is not installed. Please check the guide at https://github.com/2D-girls-enjoyer/MyWaifu." 7 | exit 1 8 | fi 9 | 10 | cd ./api 11 | 12 | if [ ! -d dist ]; then 13 | echo "Installing dependencies in ./api..." 14 | npm install 15 | 16 | # Step: Run "npm run build" 17 | echo "Building the project in ./api..." 18 | npm run build 19 | 20 | # Step: Delete the "node_modules" folder 21 | echo "Deleting node_modules in ./api..." 22 | rm -rf node_modules 23 | 24 | # Step: Run "npm install --omit=dev" 25 | echo "Installing production dependencies in ./api..." 26 | npm install --omit=dev 27 | fi 28 | 29 | # Step: Access the "./ui" folder 30 | cd ../ui 31 | 32 | if [ ! -d dist ]; then 33 | # Step: Run "npm i" 34 | echo "Installing dependencies in ./ui..." 35 | npm install 36 | 37 | # Step: Run "npm run build" 38 | echo "Building the project in ./ui..." 39 | npm run build 40 | 41 | # Step: Delete the "node_modules" folder 42 | echo "Deleting node_modules in ./ui..." 43 | rm -rf node_modules 44 | 45 | # Step: Run "npm install --omit=dev" 46 | echo "Installing production dependencies in ./ui..." 47 | npm install --omit=dev 48 | fi 49 | 50 | # Step: Return to the previous folder 51 | cd .. 52 | 53 | # Script finished, starting the Servers 54 | node scripts/startServers.js 55 | -------------------------------------------------------------------------------- /MyWaifu-Windows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | REM Step: Check if Node.js is installed 5 | node -v >nul 2>&1 6 | if %ERRORLEVEL% neq 0 ( 7 | echo Node.js is not installed. Please check the guide at https://github.com/2D-girls-enjoyer/MyWaifu. 8 | pause 9 | exit /b 10 | ) 11 | 12 | cd ./api 13 | 14 | if not exist dist ( 15 | echo Installing dependencies in ./api... 16 | call npm i 17 | 18 | REM Step: Run "npm run build" 19 | echo Building the project in ./api... 20 | call npm run build 21 | 22 | REM Step: Delete the "node_modules" folder 23 | echo Deleting node_modules in ./api... 24 | rd /s /q node_modules 25 | 26 | REM Step : Run "npm install --omit=dev" 27 | echo Installing production dependencies in ./api... 28 | call npm install --omit=dev 29 | ) 30 | 31 | REM Step: Access the "./ui" folder 32 | cd ../ui 33 | 34 | if not exist dist ( 35 | REM Step: Run "npm i" 36 | echo Installing dependencies in ./ui... 37 | call npm i 38 | 39 | REM Step: Run "npm run build" 40 | echo Building the project in ./ui... 41 | call npm run build 42 | 43 | REM Step: Delete the "node_modules" folder 44 | echo Deleting node_modules in ./ui... 45 | rd /s /q node_modules 46 | 47 | REM Step: Run "npm install --omit=dev" 48 | echo Installing production dependencies in ./ui... 49 | call npm install --omit=dev 50 | ) 51 | 52 | REM Step: Return to the previous folder 53 | cd .. 54 | 55 | REM Script finished, starting the Servers 56 | node scripts/startServers.js 57 | -------------------------------------------------------------------------------- /api/src/controllers/errorHandlerController/errorHandlerController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import InternalError from '../../models/errors/internalError'; 3 | import AIConnectionError from '../../models/errors/aiConnectionError'; 4 | import { IErrorResponse } from '../../models/interfaces/IApiResponse'; 5 | import ErrorSubType from '../../models/errors/enum/errorSubType'; 6 | 7 | class ErrorHandlerController { 8 | public handle(err: InternalError, req: Request, res: Response, _next: NextFunction) { 9 | if (err?.errorType) { 10 | const response = ErrorHandlerController.handleMappedError(err); 11 | return res.status(response.code).send(response); 12 | } 13 | 14 | return res.status(500).send({ code: 500, errorName: 'UNKNOWN', message: '' }); 15 | } 16 | 17 | private static handleMappedError(err: AIConnectionError): IErrorResponse { 18 | let code: number; 19 | 20 | switch (err.errorSubType) { 21 | case ErrorSubType.CONNECTION_REFUSED: 22 | code = 400; 23 | break; 24 | case ErrorSubType.IRREGULAR_LOADED_MODEL_QUANTITY: 25 | code = 404; 26 | break; 27 | case ErrorSubType.UNKNOWN_LLM_SOURCE_ERROR: 28 | code = 500; 29 | break; 30 | case ErrorSubType.WAIFU_NOT_SELECTED: 31 | code = 400; 32 | break; 33 | default: 34 | code = 500; 35 | } 36 | 37 | return { 38 | code, 39 | errorName: ErrorSubType[err.errorSubType], 40 | message: err.message, 41 | }; 42 | } 43 | } 44 | 45 | export default new ErrorHandlerController(); 46 | -------------------------------------------------------------------------------- /api/src/utils/fileUtils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | class FileSystemUtils { 4 | public async read( 5 | directory: string, 6 | fileName: string, 7 | forceCreation?: boolean, 8 | ): Promise { 9 | if (forceCreation) { 10 | try { 11 | return await fs.readFile(`${directory}/${fileName}`, 'utf-8'); 12 | } catch (err: any) { 13 | if (err.code !== 'ENOENT') { 14 | console.log(`Unexpected error reading file ${directory}/${fileName}`); 15 | throw new Error(`Unexpected error reading file ${directory}/${fileName}`); 16 | } 17 | const content = ''; 18 | await this.write(directory, fileName, content, true); 19 | 20 | return content; 21 | } 22 | } 23 | 24 | return fs.readFile(`${directory}/${fileName}`, 'utf-8'); 25 | } 26 | 27 | public async write( 28 | directory: string, 29 | fileName: string, 30 | content: string, 31 | forceCreation?: boolean, 32 | ): Promise { 33 | if (forceCreation) { 34 | try { 35 | await fs.access(directory); 36 | } catch (err: any) { 37 | await fs.mkdir(directory, { recursive: true }); 38 | } 39 | } 40 | 41 | await fs.writeFile(`${directory}/${fileName}`, content); 42 | } 43 | 44 | public async delete(directory: string, fileName: string): Promise { 45 | try { 46 | await fs.unlink(`${directory}/${fileName}`); 47 | } catch (err: any) { 48 | console.log(err); 49 | 50 | console.log(`${directory}/${fileName} may already have been deleted`); 51 | } 52 | } 53 | } 54 | 55 | export default new FileSystemUtils(); 56 | -------------------------------------------------------------------------------- /api/src/services/waifuCardReader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import { IWaifuCard } from '../models/interfaces/IWaifuCard'; 3 | import resourcesPlace from '../configurations/resourcesConstants'; 4 | 5 | class WaifuCardReader { 6 | public async readFromTxt(waifuPack: string): Promise { 7 | const regexToGetBetweenBracket = /\[(.*?)\]/g; 8 | const content = await fs.readFile( 9 | `${resourcesPlace.WAIFU_PACK_COLLECTION_PATH}/${waifuPack}/card.txt`, 10 | 'utf-8', 11 | ); 12 | const waifuCard: IWaifuCard = { 13 | name: '', 14 | decription: '', 15 | intialReply: '', 16 | chatExample: '', 17 | }; 18 | 19 | waifuCard.name = regexToGetBetweenBracket.exec(content)?.[1]?.trim() || ''; 20 | 21 | if (waifuCard.name === '') { 22 | console.log('Waifu name not found. Check the card txt'); 23 | throw new Error('Waifu name was not given'); 24 | } 25 | 26 | waifuCard.decription = regexToGetBetweenBracket.exec(content)?.[1]?.trim() || ''; 27 | 28 | if (waifuCard.decription === '') { 29 | console.log('Waifu description not found. Check the card txt'); 30 | throw new Error('Waifu description was not given'); 31 | } 32 | 33 | waifuCard.intialReply = regexToGetBetweenBracket.exec(content)?.[1]?.trim() || ''; 34 | 35 | if (waifuCard.intialReply === '') { 36 | console.log('Waifu initial reply not found. Check the card txt'); 37 | throw new Error('Waifu initial reply was not given'); 38 | } 39 | 40 | waifuCard.chatExample = regexToGetBetweenBracket.exec(content)?.[1] || ''; 41 | 42 | return waifuCard; 43 | } 44 | } 45 | 46 | export default new WaifuCardReader(); 47 | -------------------------------------------------------------------------------- /api/src/repositories/lmStudioRepository.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from 'undici'; 2 | import { ILLMCompletionRequest, ILLMCompletionResponse } from '../models/interfaces/ILLMCompletion'; 3 | import externalResourcesConstants from '../configurations/externalResourcesConstants'; 4 | import AIConnectionError from '../models/errors/aiConnectionError'; 5 | import ErrorSubType from '../models/errors/enum/errorSubType'; 6 | 7 | class LmStudioRepository { 8 | public async completion(request: ILLMCompletionRequest): Promise { 9 | let response; 10 | 11 | try { 12 | response = await fetch( 13 | `${externalResourcesConstants.LM_STUDIO_URL}:${externalResourcesConstants.LM_STUDIO_PORT}/v1/completions`, 14 | { 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | method: 'POST', 19 | body: JSON.stringify(request), 20 | }, 21 | ); 22 | 23 | if (!response.ok) { 24 | switch (response.status) { 25 | case 404: 26 | throw new AIConnectionError( 27 | 'LmStudio cannot be loaded with more or less then 1 (one) model only', 28 | ErrorSubType.IRREGULAR_LOADED_MODEL_QUANTITY, 29 | ); 30 | default: 31 | throw new AIConnectionError( 32 | 'An unexpected error occured at LmStudio', 33 | ErrorSubType.UNKNOWN_LLM_SOURCE_ERROR, 34 | ); 35 | } 36 | } 37 | } catch (err: any) { 38 | if (err instanceof TypeError) { 39 | throw new AIConnectionError( 40 | 'LmStudio server could not be reached', 41 | ErrorSubType.CONNECTION_REFUSED, 42 | ); 43 | } 44 | 45 | throw err; 46 | } 47 | 48 | return response.json() as Promise; 49 | } 50 | } 51 | 52 | export default new LmStudioRepository(); 53 | -------------------------------------------------------------------------------- /ui/src/components/alert-stack/alertStack.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react'; 2 | import alertStore from '../../store/alertStore'; 3 | import { AlertType } from '../../models/enums/alertType'; 4 | 5 | type AlertResources = { 6 | headerText: string; 7 | headerColor: string; 8 | innerTextColor: string; 9 | borderColor: string; 10 | innerColor: string; 11 | }; 12 | 13 | const AlertStack = observer(() => { 14 | const renderStack = () => alertStore.alerts.map((alert, index) => { 15 | let alertResources: AlertResources; 16 | 17 | switch (alert.type) { 18 | case AlertType.ERROR: 19 | alertResources = { 20 | headerText: 'ERROR', 21 | headerColor: 'bg-red-500', 22 | innerTextColor: 'text-red-700', 23 | borderColor: 'border-red-400', 24 | innerColor: 'bg-red-100', 25 | }; 26 | break; 27 | default: 28 | alertResources = { 29 | headerText: 'INFO', 30 | headerColor: 'bg-blue-500', 31 | innerTextColor: 'text-blue-700', 32 | borderColor: 'border-blue-400', 33 | innerColor: 'bg-blue-100', 34 | }; 35 | } 36 | 37 | return ( 38 |
39 |
40 |

{alertResources.headerText}

41 |
{ alertStore.removeAlertByIndex(index); }} 43 | className="self-center cursor-pointer text-xl font-extrabold" 44 | > 45 | X 46 |
47 |
48 |
49 |

{alert.message}

50 |
51 |
52 | ); 53 | }); 54 | 55 | return ( 56 |
57 | {renderStack()} 58 |
59 | ); 60 | }); 61 | 62 | export default AlertStack; 63 | -------------------------------------------------------------------------------- /ui/src/infra/http.ts: -------------------------------------------------------------------------------- 1 | import { API_DOMAIN } from '../constants'; 2 | import ErrorFetch from '../models/errors/errorFetch'; 3 | import { 4 | IChatSummaryResponse, 5 | IErrorResponse, 6 | IUsernameRequest, 7 | IWaifuGenerateRequest, 8 | IWaifuGenerateResponse, 9 | IWaifuSelectionRequest, 10 | IWaifuSelectionResponse, 11 | } from '../models/interfaces/apiRequests'; 12 | 13 | class Http { 14 | public async getUsername(): Promise { 15 | return this.httpFetch('/username', 'GET'); 16 | } 17 | 18 | public async saveUsername(usernameRequest: IUsernameRequest): Promise { 19 | await this.httpFetch('/username', 'POST', usernameRequest); 20 | } 21 | 22 | public async getWaifus(): Promise { 23 | return this.httpFetch('/waifu/pack', 'GET'); 24 | } 25 | 26 | public async selectWaifu(waifuSelectionRequest: IWaifuSelectionRequest): Promise { 27 | await this.httpFetch('/waifu/select', 'POST', waifuSelectionRequest); 28 | } 29 | 30 | public async getWaifuChat(): Promise { 31 | return this.httpFetch('/chat', 'GET'); 32 | } 33 | 34 | public async deleteWaifuChat(): Promise { 35 | await this.httpFetch('/chat', 'DELETE'); 36 | } 37 | 38 | public async generateWaifuResponse( 39 | generateRequest: IWaifuGenerateRequest, 40 | ): Promise { 41 | return this.httpFetch('/waifu/generate', 'POST', generateRequest); 42 | } 43 | 44 | private async httpFetch(path: string, method: string, body?: object): Promise { 45 | const response = await fetch(`${API_DOMAIN}${path}`, { 46 | method, 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | }, 50 | ...(body && { body: JSON.stringify(body) }), 51 | }); 52 | 53 | if (!response.ok) { 54 | throw new ErrorFetch((await response.json()) as IErrorResponse); 55 | } 56 | 57 | try { 58 | return await response.json(); 59 | } catch (err: any) { 60 | return undefined as any; 61 | } 62 | } 63 | } 64 | 65 | export default new Http(); 66 | -------------------------------------------------------------------------------- /ui/src/components/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | interface ModalProps { 4 | open: boolean, 5 | confirmationBtnText?: string 6 | onClose: VoidFunction, 7 | onConfirmationBtnClick?: VoidFunction, 8 | onCancelBtnClick: VoidFunction, 9 | } 10 | 11 | function Modal({ 12 | open, onClose, onConfirmationBtnClick, onCancelBtnClick, confirmationBtnText, children, 13 | }: PropsWithChildren) { 14 | return ( 15 |
20 |
{ e.stopPropagation(); }} 22 | className={`bg-color-modal-background rounded-xl shadow p-6 transition-all 23 | ${open ? 'scale-100 opacity-100' : 'scale-125 opacity-0'}`} 24 | > 25 | 32 | {children} 33 |
34 | 42 | {(onConfirmationBtnClick && confirmationBtnText) 43 | && ( 44 | 52 | )} 53 |
54 |
55 | 56 |
57 | ); 58 | } 59 | 60 | export default Modal; 61 | -------------------------------------------------------------------------------- /ui/src/components/slidebar/slidebar.tsx: -------------------------------------------------------------------------------- 1 | interface SlidebarProps { 2 | open: boolean, 3 | onClose: VoidFunction, 4 | onUsernameModalClick: VoidFunction, 5 | onWaifuSelectModalClick: VoidFunction, 6 | } 7 | 8 | function Slidebar({ 9 | open, onClose, onUsernameModalClick, onWaifuSelectModalClick, 10 | }: SlidebarProps) { 11 | return ( 12 |
17 |
{ e.stopPropagation(); }} 19 | className={`flex flex-col px-2 bg-color-modal-background absolute right-0 20 | shadow w-3/6 sm:w-3/6 lg:w-1/6 h-dvh transition-all 21 | ${open ? 'opacity-100' : 'translate-x-12 opacity-0'}`} 22 | > 23 | 30 | 31 |

32 | MENU 33 |

34 | 35 |
36 |
40 | diversity_1 41 |

WAIFUS

42 |
43 |
47 | account_circle 48 |

USERNAME

49 |
50 |
51 | 52 |
53 |
54 | ); 55 | } 56 | 57 | export default Slidebar; 58 | -------------------------------------------------------------------------------- /api/src/managers/mainAIManager.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import customConfigurations from '../configurations/customConfigurations'; 3 | import { IGeneratedCompletion } from '../models/interfaces/ILLMCompletion'; 4 | import LmStudioRepository from '../repositories/lmStudioRepository'; 5 | 6 | class MainAIManager { 7 | private readonly DEFAULT_CHAT_TEMPERATURE = 0.7; 8 | 9 | public async generate( 10 | prompt: string, 11 | waifuPack: string, 12 | waifuName: string, 13 | ): Promise { 14 | const currentWaifuPack = waifuPack; 15 | const currentWaifuName = waifuName; 16 | let response = this.sanitizeResponse(( 17 | await this.generateCompletion(prompt, this.DEFAULT_CHAT_TEMPERATURE) 18 | ).choices[0].text); 19 | 20 | if (response === '') { 21 | response = await this.forceNotBlankResponse(prompt, this.DEFAULT_CHAT_TEMPERATURE); 22 | } 23 | 24 | return { 25 | response, 26 | waifuPack: currentWaifuPack, 27 | waifuName: currentWaifuName, 28 | }; 29 | } 30 | 31 | private async generateCompletion(prompt: string, temperature: number) { 32 | return LmStudioRepository.completion({ 33 | max_tokens: 120, 34 | model: '', 35 | stream: false, 36 | prompt, 37 | temperature, 38 | }); 39 | } 40 | 41 | private async forceNotBlankResponse(prompt: string, temperature: number): Promise { 42 | let count = 0; 43 | let currentTemperature = temperature; 44 | let response = ''; 45 | const deltaTemperature = 0.1; 46 | 47 | while (count < customConfigurations.LLM_MAX_BLANK_RETRY && response === '') { 48 | console.log(`Attempting another chat completion ${count + 1} of ${customConfigurations.LLM_MAX_BLANK_RETRY}`); 49 | 50 | currentTemperature -= deltaTemperature; 51 | 52 | response = this.sanitizeResponse(( 53 | await this.generateCompletion(prompt, currentTemperature) 54 | ).choices[0].text); 55 | count++; 56 | } 57 | 58 | return response; 59 | } 60 | 61 | private sanitizeResponse(completionText: string): string { 62 | const regexToGetBetweenQuotes = /"([^"]*)"/; 63 | 64 | return regexToGetBetweenQuotes.exec(completionText)?.[1]?.trim() || ''; 65 | } 66 | } 67 | 68 | export default new MainAIManager(); 69 | -------------------------------------------------------------------------------- /ui/src/components/chat-text-area/chatTextArea.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | interface ChatTextAreaProps { 5 | lockSendResponse: boolean, 6 | onUserSendResponse: (arg: { currentText: string }) => void, 7 | } 8 | 9 | const ChatTextArea = observer(({ onUserSendResponse, lockSendResponse }: ChatTextAreaProps) => { 10 | const textareaRef = useRef(null); 11 | const [textContent, setTextContent] = useState(''); 12 | 13 | const handleSend = () => { 14 | onUserSendResponse({ currentText: textContent }); 15 | setTextContent(''); 16 | }; 17 | 18 | const handleKeyDown = (e: React.KeyboardEvent) => { 19 | if (e.key === 'Enter' && !e.shiftKey) { 20 | e.preventDefault(); 21 | 22 | if (!lockSendResponse) { 23 | handleSend(); 24 | } 25 | } 26 | }; 27 | 28 | useEffect(() => { 29 | if (textareaRef.current) { 30 | textareaRef.current.style.height = 'auto'; 31 | 32 | const newHeight = Math.min(textareaRef.current.scrollHeight, 100); 33 | textareaRef.current.style.height = `${newHeight}px`; 34 | 35 | const outerDiv = textareaRef.current.parentElement; 36 | 37 | if (outerDiv) { 38 | outerDiv.style.height = `${newHeight + 25}px`; 39 | } 40 | } 41 | }, [textContent]); 42 | 43 | return ( 44 |
47 |