├── 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 |
30 | X
31 |
32 | {children}
33 |
34 |
40 | Cancel
41 |
42 | {(onConfirmationBtnClick && confirmationBtnText)
43 | && (
44 |
50 | {confirmationBtnText}
51 |
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 |
28 | X
29 |
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 |
67 | );
68 | });
69 |
70 | export default ChatTextArea;
71 |
--------------------------------------------------------------------------------
/scripts/startServers.js:
--------------------------------------------------------------------------------
1 | const { spawn, exec } = require('node:child_process');
2 | const { resolve } = require('node:path');
3 |
4 | const { cleanString } = require('./utilities.js');
5 |
6 | const controller = new AbortController();
7 | const { signal } = controller;
8 | const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
9 | const viteSuccess_regex = /Local:\s*(http:\/\/localhost:[0-9]{1,5}\/)/;
10 | const EADDRINUSEErr_regex = /EADDRINUSE:\s+address already in use\s+.+:([0-9]{1,5})\n/;
11 | const currentDir = process.cwd();
12 |
13 | ['SIGINT', 'SIGTERM'].forEach((signal) => {
14 | process.on(signal, () => {
15 | console.log(`Received exit signal, exiting...`);
16 | controller.abort();
17 | process.exit(0);
18 | });
19 | });
20 |
21 | console.log(`Starting servers...\nCurrent working directory: ${currentDir}`);
22 |
23 | const apiServer = spawn(npm, ['run', 'starts'], {
24 | cwd: resolve(currentDir, './api'),
25 | signal,
26 | shell: process.platform === 'win32'
27 | });
28 |
29 | const uiServer = spawn(npm, ['run', 'preview'], {
30 | cwd: resolve(currentDir, './ui'),
31 | signal,
32 | shell: process.platform === 'win32'
33 | });
34 |
35 | apiServer.stderr.on('data', (data) => {
36 | data = data.toString();
37 |
38 | if (data.includes('listen EADDRINUSE: address already in use')) {
39 | const port = data.match(EADDRINUSEErr_regex)[1];
40 |
41 | console.error(`API Server Error: Port ${port} already in use. Exiting...`);
42 | controller.abort();
43 | return
44 | }
45 |
46 | console.error(`API Server Error: ${data}`);
47 | });
48 |
49 | uiServer.stderr.on('data', (data) => {
50 | console.error(`UI Server Error: ${data}`);
51 | });
52 |
53 | uiServer.stdout.on('data', (data) => {
54 | data = data.toString();
55 | process.stdout.write(data);
56 |
57 | // catch success message
58 | const successMsg = cleanString(data).match(viteSuccess_regex);
59 | if (successMsg) tellServerStarted(successMsg[1]);
60 | });
61 |
62 | function tellServerStarted(url) {
63 | setTimeout(() => { // vite takes some time to print all messages, we'll let it finish
64 | console.log(`\nServer started successfully.\nvisit: ${url} for User Interface\n[TIP] Press Ctrl+C to stop.`);
65 | }, 500);
66 |
67 | exec(process.platform === 'win32'
68 | ? `start "" ${url}`
69 | : `xdg-open ${url}`
70 | ); // open browser
71 | }
--------------------------------------------------------------------------------
/api/src/repositories/chatRepository.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import resourcesConstants from '../configurations/resourcesConstants';
3 | import { IChatStorage, IReply } from '../models/interfaces/IReply';
4 | import fileUtils from '../utils/fileUtils';
5 | import customConfigurations from '../configurations/customConfigurations';
6 | import ProcessError from '../models/errors/ProcessError';
7 | import ErrorSubType from '../models/errors/enum/errorSubType';
8 |
9 | class ChatRepository {
10 | // TEMPORARY!!! On prototype 3 (last) I'll test saving/control chat on SQLite
11 | public async saveReply(reply: IReply, waifuPack: string): Promise {
12 | if (!waifuPack || waifuPack === '') {
13 | throw new ProcessError('', ErrorSubType.WAIFU_NOT_SELECTED);
14 | }
15 |
16 | let chat: IReply[];
17 |
18 | try {
19 | chat = await this.getReplies(waifuPack);
20 | } catch (err: any) {
21 | if (err.code !== 'ENOENT') {
22 | console.log(`Unexpected error at access ${waifuPack} chat summary`);
23 | throw new Error(`Unexpected error at access ${waifuPack} chat summary`);
24 | }
25 | chat = [];
26 | }
27 |
28 | this.capIfNecessary(chat);
29 | chat.push(reply);
30 | fileUtils.write(
31 | `${resourcesConstants.DATA_PATH}/${waifuPack}`,
32 | 'recentChat.json',
33 | JSON.stringify({ data: chat } as IChatStorage),
34 | true,
35 | );
36 |
37 | return chat;
38 | }
39 |
40 | public async getReplies(waifuPack: string): Promise {
41 | return (JSON.parse(
42 | await fs.readFile(`${resourcesConstants.DATA_PATH}/${waifuPack}/recentChat.json`, 'utf-8'),
43 | ) as IChatStorage)?.data;
44 | }
45 |
46 | public async hasChatHistory(waifuPack: string): Promise {
47 | try {
48 | await fs.access(`${resourcesConstants.DATA_PATH}/${waifuPack}/recentChat.json`);
49 | return true;
50 | } catch (err: any) {
51 | return false;
52 | }
53 | }
54 |
55 | public async deleteAllReplies(waifuPack: string): Promise {
56 | await fileUtils.delete(
57 | `${resourcesConstants.DATA_PATH}/${waifuPack}`,
58 | 'recentChat.json',
59 | );
60 | }
61 |
62 | private capIfNecessary(replies: IReply[]): void {
63 | if (replies.length >= customConfigurations.REPLIES_MAX_AMOUNT_SAVE) {
64 | replies.shift();
65 | }
66 | }
67 | }
68 |
69 | export default new ChatRepository();
70 |
--------------------------------------------------------------------------------
/ui/src/components/navbar/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { API_DOMAIN } from '../../constants';
2 | import store from '../../store/store';
3 | import Tooltip from '../tooltip/tooltip';
4 |
5 | interface NavbarProps {
6 | onMenuClick: VoidFunction,
7 | onDeleteChatClick: VoidFunction,
8 | }
9 |
10 | function Navbar({ onMenuClick, onDeleteChatClick }: NavbarProps) {
11 | return (
12 |
13 |
14 |
15 | MyWaifu
16 |
17 |
18 |
19 |
20 | {store.waifuName !== '' && (
21 |
22 |
23 |
24 |
30 |
33 | delete_forever
34 |
35 |
36 |
37 |
38 |
39 |
44 |
{store.waifuName}
45 |
46 | )}
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default Navbar;
65 |
--------------------------------------------------------------------------------
/api/src/services/apiService.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IGenerateRequest,
3 | ISetUsernameRequest,
4 | IWaifuSelectionRequest,
5 | } from '../models/interfaces/IApiRequest';
6 | import chatManager from '../managers/chatManager';
7 | import LLMPromptManager from '../managers/llmPromptManager';
8 | import mainAIManager from '../managers/mainAIManager';
9 | import userManager from '../managers/userManager';
10 | import waifuCardReader from './waifuCardReader';
11 | import waifuPacksService from './waifuPacksService';
12 | import waifuManager from '../managers/waifuManager';
13 | import {
14 | IChatSummaryResponse, IGenerateResponse, IUsernameResponse, IWaifuPacksResponse,
15 | } from '../models/interfaces/IApiResponse';
16 |
17 | class ApiService {
18 | public async selectWaifu({ waifu }: IWaifuSelectionRequest): Promise {
19 | const waifuCard = await waifuCardReader.readFromTxt(waifu);
20 | waifuManager.setWaifu(waifuCard, waifu);
21 |
22 | await Promise.all([
23 | LLMPromptManager.load(waifuManager.CARD, await userManager.get()),
24 | chatManager.load(waifu, waifuCard)
25 | ]);
26 | }
27 |
28 | public async generate({ userReply }: IGenerateRequest): Promise {
29 | const { response, waifuPack, waifuName } = await mainAIManager.generate(
30 | LLMPromptManager.buildChatPrompt(
31 | await chatManager.saveReply(
32 | userReply,
33 | waifuManager.PACK,
34 | ),
35 | waifuManager.CARD.name,
36 | await userManager.get(),
37 | ),
38 | waifuManager.PACK,
39 | waifuManager.CARD.name,
40 | );
41 | await chatManager.saveReply(
42 | response,
43 | waifuPack,
44 | waifuName,
45 | );
46 |
47 | return { response, waifuPack };
48 | }
49 |
50 | public async getAvailableWaifuPacks(): Promise {
51 | return { waifus: await waifuPacksService.getAll() };
52 | }
53 |
54 | public async setUsername({ username }: ISetUsernameRequest): Promise {
55 | await userManager.create(username);
56 | }
57 |
58 | public async getUsername(): Promise {
59 | return { username: await userManager.get() };
60 | }
61 |
62 | public async getChat(): Promise {
63 | return { chatSummary: await chatManager.getReplies(waifuManager.PACK) };
64 | }
65 |
66 | public async deleteChat(): Promise {
67 | await chatManager.deleteAllReplies(waifuManager.PACK);
68 | await chatManager.load(waifuManager.PACK, waifuManager.CARD);
69 | }
70 | }
71 |
72 | export default new ApiService();
73 |
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | Node modules
2 | node_modules/
3 |
4 | # Logs
5 | logs/
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids/
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov/
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage/
22 | *.lcov
23 |
24 | # nyc test coverage
25 | .nyc_output/
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt/
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components/
32 |
33 | # Optional npm cache directory
34 | .npm/
35 |
36 | # TypeScript compiled output
37 | dist/
38 | dist-ssr/
39 | *.tsbuildinfo
40 |
41 | # TypeScript declaration files
42 | *.d.ts
43 |
44 | # Compiled JavaScript files
45 | *.js
46 | *.jsx
47 | *.js.map
48 |
49 | # Next.js build output
50 | .next/
51 | out/
52 |
53 | # Nuxt.js build output
54 | .nuxt/
55 | dist/
56 |
57 | # Gatsby files
58 | .cache/
59 | public/
60 |
61 | # Vuepress build output
62 | .vuepress/dist/
63 |
64 | # Serverless directories
65 | .serverless/
66 |
67 | # FuseBox cache
68 | .fusebox/
69 |
70 | # DynamoDB Local files
71 | .dynamodb/
72 |
73 | # TernJS port file
74 | .tern-port
75 |
76 | # VS Code directory and files
77 | .vscode/*
78 | !.vscode/settings.json
79 | !.vscode/tasks.json
80 | !.vscode/launch.json
81 | !.vscode/extensions.json
82 |
83 | # IntelliJ IDEA files
84 | .idea/
85 | *.iml
86 | *.iws
87 | *.ipr
88 |
89 | # SASS cache
90 | .sass-cache/
91 |
92 | # Favicon files
93 | favicon.ico
94 |
95 | # Docker files
96 | Dockerfile
97 | docker-compose.yml
98 | .dockerignore
99 |
100 | # dotenv environment variables file
101 | .env
102 | .env.test
103 | .env.production
104 |
105 | # Optional REPL history
106 | .node_repl_history
107 |
108 | # macOS specific files
109 | .DS_Store
110 | .AppleDouble
111 | .LSOverride
112 |
113 | # Windows specific files
114 | Thumbs.db
115 | ehthumbs.db
116 | Desktop.ini
117 | $RECYCLE.BIN/
118 |
119 | # Linux specific files
120 | *~
121 |
122 | # YARN integrity file
123 | .yarn-integrity
124 |
125 | # Lock files
126 | package-lock.json
127 | yarn.lock
128 | pnpm-lock.yaml
129 |
130 | # Prettier configuration
131 | .prettierignore
132 |
133 | # ESLint configuration
134 | .eslintcache
135 |
136 | # Parcel-bundler cache (https://parceljs.org/)
137 | .cache
138 |
139 | # JetBrains Rider
140 | .idea/
141 |
142 | # Sapper build / export directories
143 | __sapper__/
144 | export/
145 |
146 | data/
147 | !data/.gitkeep
--------------------------------------------------------------------------------
/ui/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Created with Fabric.js 5.2.4
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/api/src/managers/llmPromptManager.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import resourcesPlace from '../configurations/resourcesConstants';
3 | import { ICorePrompt } from '../models/interfaces/ICorePrompt';
4 | import { IWaifuCard } from '../models/interfaces/IWaifuCard';
5 | import { IReply } from '../models/interfaces/IReply';
6 |
7 | class LLMPromptManager {
8 | private CHAT_PROMPT: string = '';
9 |
10 | private readonly MAX_CHAT_REPLY = 8;
11 |
12 | /**
13 | * Pre load prompt with waifu's name and descriptions to build prompt faster
14 | * @param {string} waifuCard Waifu Card
15 | * @returns {void}
16 | */
17 | public async load(waifuCard: IWaifuCard, username: string): Promise {
18 | try {
19 | const corePrompt = await fs.readFile(resourcesPlace.LLM_PROMP_CORE_PATH, 'utf8');
20 | const { prompt } = JSON.parse(corePrompt) as ICorePrompt;
21 | this.CHAT_PROMPT = this.resolveConstantPlaceholders(prompt, waifuCard, username);
22 | } catch (err) {
23 | if (err) {
24 | console.log(
25 | 'Error while reading file for llm core prompt building configuration',
26 | err,
27 | );
28 | }
29 | }
30 | }
31 |
32 | /**
33 | * Build prompt for response on main chat
34 | * @param {IReply[]} chatHistory current chat
35 | * @returns {void}
36 | */
37 | public buildChatPrompt(chatHistory: IReply[], waifuName: string, username: string): string {
38 | const mostRecentReplies = chatHistory.length > this.MAX_CHAT_REPLY
39 | ? chatHistory.slice(-this.MAX_CHAT_REPLY)
40 | : chatHistory;
41 | let repliesPrompt = '';
42 |
43 | for (let i = 0; i < mostRecentReplies.length; i++) {
44 | repliesPrompt += `\n${mostRecentReplies[i].sender !== waifuName ? username : waifuName}: "${mostRecentReplies[i].content}"`;
45 | }
46 |
47 | return `${this.CHAT_PROMPT.replace('{{lastMsg}}', repliesPrompt)}`;
48 | }
49 |
50 | // private methods
51 |
52 | private resolveConstantPlaceholders(
53 | prompts: string,
54 | waifuCard: IWaifuCard,
55 | username: string,
56 | ): string {
57 | let chatExample = '';
58 |
59 | if (waifuCard.chatExample || waifuCard.chatExample !== '') {
60 | waifuCard.chatExample
61 | ?.split(';')
62 | ?.forEach((value) => {
63 | const [sender, message] = value.split(':');
64 | chatExample += `\n${sender.trim()}: "${message.trim()}"`;
65 | });
66 | }
67 |
68 | return this.resolveConditionals(prompts, waifuCard)
69 | .replaceAll('{{card_description}}', waifuCard.decription)
70 | .replaceAll('{{chat_example}}', chatExample || '')
71 | .replaceAll('{{waifu}}', waifuCard.name)
72 | .replaceAll('{{user}}', username);
73 | }
74 |
75 | private resolveConditionals(prompts: string, waifuCard: IWaifuCard): string {
76 | return prompts.replaceAll(/<%[\s\S]*?%>/g, (match) => {
77 | const [condition, value] = match.slice(2, -2).split('|');
78 | let content: string | undefined;
79 |
80 | switch (condition.trim().split(' ')?.[1]) {
81 | case 'chat_example':
82 | if (waifuCard.chatExample) {
83 | content = value.trim();
84 | }
85 | break;
86 | case 'waifu':
87 | if (waifuCard.name) {
88 | content = value.trim();
89 | }
90 | break;
91 | case 'card_description':
92 | if (waifuCard.decription) {
93 | content = value.trim();
94 | }
95 | break;
96 | default:
97 | console.log('Invalid IF condition');
98 | break;
99 | }
100 |
101 | return content || '';
102 | });
103 | }
104 | }
105 |
106 | export default new LLMPromptManager();
107 |
--------------------------------------------------------------------------------
/ui/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import {
2 | action, makeObservable, observable,
3 | } from 'mobx';
4 | import http from '../infra/http';
5 | import {
6 | IChatSummaryResponse, IReply, IUsernameResponse, IWaifuGenerateResponse,
7 | } from '../models/interfaces/apiRequests';
8 | import ErrorFetch from '../models/errors/errorFetch';
9 | import { ErrorName } from '../models/errors/enum/errorName';
10 |
11 | enum FetchOperation {
12 | GET_CHAT = 'GET_CHAT',
13 | GENERATE_RESPONSE = 'GENERATE_RESPONSE',
14 | DELETE_CHAT = 'DELETE_CHAT',
15 | SELECT_WAIFU = 'SELECT_WAIFU',
16 | DELETE_WAIFU = 'DELETE_WAIFU',
17 | GET_USERNAME = 'GET_USERNAME',
18 | SAVE_USERNAME = 'SAVE_USERNAME',
19 | }
20 |
21 | class Store {
22 | waifuName: string = '';
23 |
24 | username: string = '';
25 |
26 | chat: IReply[] = [];
27 |
28 | waifusTyping: string[] = [];
29 |
30 | constructor() {
31 | makeObservable(this, {
32 | waifuName: observable,
33 | username: observable,
34 | chat: observable,
35 | waifusTyping: observable,
36 | setWaifuName: action,
37 | setUsername: action,
38 | setChat: action,
39 | appendReply: action,
40 | appendWaifuTyping: action,
41 | isWaifuTyping: action,
42 | generateChat: action,
43 | selectWaifu: action,
44 | loadWaifuChat: action,
45 | deleteWaifuChat: action,
46 | loadUsername: action,
47 | saveUsername: action,
48 | });
49 | }
50 |
51 | setWaifuName(waifuName: string) {
52 | this.waifuName = waifuName;
53 | }
54 |
55 | setUsername(username: string) {
56 | this.username = username;
57 | }
58 |
59 | setChat(replies: IReply[]) {
60 | this.chat = replies;
61 | }
62 |
63 | appendReply(reply: IReply) {
64 | this.chat.push(reply);
65 | }
66 |
67 | appendWaifuTyping(waifu: string) {
68 | this.waifusTyping.push(waifu);
69 | }
70 |
71 | removeWaifuTyping(waifu: string) {
72 | const indexToRemove = this.waifusTyping.indexOf(waifu);
73 |
74 | if (indexToRemove > -1) {
75 | this.waifusTyping.splice(indexToRemove, 1);
76 | }
77 | }
78 |
79 | isWaifuTyping(waifu: string): boolean {
80 | return this.waifusTyping.indexOf(waifu) > -1;
81 | }
82 |
83 | async generateChat(userMessageToBeSent: string) {
84 | const waifuResponsePromise = this
85 | .fetch(FetchOperation.GENERATE_RESPONSE, {
86 | userReply: userMessageToBeSent,
87 | });
88 | this.appendWaifuTyping(this.waifuName);
89 | const waifuName = `${this.waifuName}`;
90 | let response: string;
91 | this.appendReply({
92 | content: userMessageToBeSent,
93 | sender: 'User',
94 | date: new Date().toString(),
95 | });
96 |
97 | try {
98 | response = (await waifuResponsePromise)?.response;
99 | } catch (err: any) {
100 | this.removeWaifuTyping(waifuName);
101 | throw err;
102 | }
103 |
104 | this.removeWaifuTyping(waifuName);
105 |
106 | if (this.waifuName === waifuName) {
107 | this.appendReply({
108 | content: response,
109 | sender: waifuName,
110 | date: new Date().toString(),
111 | });
112 | }
113 | }
114 |
115 | async selectWaifu(waifu: string) {
116 | if (waifu !== this.waifuName) {
117 | await this.fetch(FetchOperation.SELECT_WAIFU, { waifu });
118 | this.setWaifuName(waifu);
119 | }
120 | }
121 |
122 | async loadWaifuChat() {
123 | const { chatSummary } = await this.fetch(FetchOperation.GET_CHAT);
124 | this.setChat(chatSummary);
125 | }
126 |
127 | async deleteWaifuChat() {
128 | await this.fetch(FetchOperation.DELETE_WAIFU);
129 | const { chatSummary } = await this.fetch(FetchOperation.GET_CHAT);
130 | this.setChat(chatSummary);
131 | }
132 |
133 | async loadUsername() {
134 | const { username } = await this.fetch(FetchOperation.GET_USERNAME);
135 | this.setUsername(username);
136 | }
137 |
138 | async saveUsername() {
139 | http.saveUsername({ username: this.username.trim() });
140 | this.setUsername(this.username.trim());
141 | }
142 |
143 | private async fetch(operation: FetchOperation, args?: any): Promise {
144 | try {
145 | return await this.executeFetchOperation(operation, args) as T;
146 | } catch (err: any) {
147 | if (err instanceof ErrorFetch
148 | && err?.errorName === ErrorName.WAIFU_NOT_SELECTED
149 | && this.waifuName !== '') {
150 | await this.fetch(FetchOperation.SELECT_WAIFU, { waifu: this.waifuName });
151 | return this.executeFetchOperation(operation, args) as T;
152 | }
153 |
154 | throw err;
155 | }
156 | }
157 |
158 | private async executeFetchOperation(operation: FetchOperation, args?: any) {
159 | switch (operation) {
160 | case FetchOperation.GET_CHAT:
161 | return http.getWaifuChat();
162 | case FetchOperation.DELETE_CHAT:
163 | return http.deleteWaifuChat();
164 | case FetchOperation.GENERATE_RESPONSE:
165 | return http.generateWaifuResponse(args);
166 | case FetchOperation.SELECT_WAIFU:
167 | return http.selectWaifu(args);
168 | case FetchOperation.DELETE_WAIFU:
169 | return http.deleteWaifuChat();
170 | case FetchOperation.GET_USERNAME:
171 | return http.getUsername();
172 | case FetchOperation.SAVE_USERNAME:
173 | return http.saveUsername(args);
174 | default:
175 | return undefined;
176 | }
177 | }
178 | }
179 |
180 | export default new Store();
181 |
--------------------------------------------------------------------------------
/ui/src/pages/main-chat/mainChat.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | import { useEffect, useRef } from 'react';
3 | import { observer, useLocalObservable } from 'mobx-react';
4 | import { reaction } from 'mobx';
5 | import ChatBubble from '../../components/chat-bubble/chatBubble';
6 | import ChatTextArea from '../../components/chat-text-area/chatTextArea';
7 | import { BelongsTo } from '../../models/enums/chatBubble';
8 | import store from '../../store/store';
9 | import ErrorFetch from '../../models/errors/errorFetch';
10 | import { ErrorName } from '../../models/errors/enum/errorName';
11 | import alertStore from '../../store/alertStore';
12 | import { AlertType } from '../../models/enums/alertType';
13 |
14 | const MainChat = observer(() => {
15 | const chatRef = useRef(null);
16 |
17 | const localStore = useLocalObservable(() => ({
18 | lockSendResponse: true,
19 | isWaifuTyping: false,
20 |
21 | setLockSendResponse(lock: boolean) {
22 | this.lockSendResponse = lock;
23 | },
24 |
25 | setIsWaifuTyping(isTyping: boolean) {
26 | this.isWaifuTyping = isTyping;
27 | },
28 | }));
29 |
30 | const scrollToBottom = () => {
31 | if (chatRef.current) {
32 | chatRef.current?.scrollIntoView({ behavior: 'smooth' });
33 | }
34 | };
35 |
36 | const handleUserSentResponse = async (userMessageToBeSent: string) => {
37 | localStore.setLockSendResponse(true);
38 | localStore.setIsWaifuTyping(true);
39 | try {
40 | await store.generateChat(userMessageToBeSent);
41 | } catch (err: any) {
42 | if (err instanceof ErrorFetch) {
43 | switch (err.errorName) {
44 | case ErrorName.CONNECTION_REFUSED:
45 | alertStore.addAlert({
46 | type: AlertType.ERROR,
47 | message: 'Make sure the the LM Studio is running and with it\'s server on. Check the simplified "Usage guide" at: https://github.com/2D-girls-enjoyer/MyWaifu',
48 | });
49 | break;
50 | case ErrorName.IRREGULAR_LOADED_MODEL_QUANTITY:
51 | alertStore.addAlert({
52 | type: AlertType.ERROR,
53 | message: 'LmStudio should be loaded with one model only. Check the simplified "Usage guide" at: https://github.com/2D-girls-enjoyer/MyWaifu',
54 | });
55 | break;
56 | default:
57 | alertStore.addAlert({ type: AlertType.UNKNOWN, message: 'Unknown UI error occured' });
58 | }
59 | }
60 | } finally {
61 | localStore.setIsWaifuTyping(store.isWaifuTyping(store.waifuName));
62 | localStore.setLockSendResponse(false);
63 | }
64 | };
65 |
66 | const loadWaifuChatToLocalStore = async () => {
67 | await store.loadWaifuChat();
68 | localStore.setLockSendResponse(false);
69 | };
70 |
71 | const renderChat = () => (store.chat?.length > 0
72 | ? store.chat.map((reply) => (
73 |
79 |
87 |
88 | ))
89 | : (
90 |
91 |
92 | SELECT A WAIFU TO CHAT WITH
93 |
94 |
95 | )
96 | );
97 |
98 | useEffect(() => {
99 | const waifuNameDisposer = reaction(
100 | () => store.waifuName,
101 | () => {
102 | loadWaifuChatToLocalStore();
103 | },
104 | );
105 |
106 | return () => {
107 | waifuNameDisposer();
108 | };
109 | });
110 |
111 | useEffect(() => {
112 | scrollToBottom();
113 | }, [store.chat.length]);
114 |
115 | useEffect(() => {
116 | localStore.setIsWaifuTyping(store.isWaifuTyping(store.waifuName));
117 | }, [store.waifuName]);
118 |
119 | return (
120 |
121 |
124 | {renderChat()}
125 |
126 |
127 |
128 |
129 | { handleUserSentResponse(currentText); }}
132 | />
133 |
134 |
135 | {localStore.isWaifuTyping && (
136 |
137 |
142 |
143 | {store.waifuName}
144 | {' '}
145 | is typing
146 |
147 |
148 | )}
149 |
150 |
151 |
152 | );
153 | });
154 |
155 | export default MainChat;
156 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | MyWaifu
6 |
7 |
8 |
9 | []()
10 |
11 |
12 |
13 | ---
14 |
15 | The helpful waifu you want at your PC
16 |
17 |
18 |
19 |
20 | ## 💖 Help MyWaifu keep alive and improve
21 |
22 | **MyWaifu** is a free open source project and it'll always be.
23 |
24 | **You can help MyWaifu continuity with codes and/or donating at Ko-fi**
25 |
26 |
27 |
28 |
29 | * [Your talkative and helpful waifu](#problem_statement)
30 | * [🚨 Instalation guide](#instalation_guide)
31 | * [🧾Usage guide & recommendation](#usage_guide)
32 | * [✍️ Authors](#authors)
33 |
34 | ## Your talkative and helpful waifu
35 | Imagine having your favorite waifus as your assitant and spending time talking at your own PC, all for free and forever.
36 |
37 | Sugoi, right?
38 |
39 | For now only chatting is available but, in future releases, your waifu will also be able to assist you with general tasks on your computer and answer your questions, just like your personal assistant.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | ## 🚨 Instalation guide
53 | Here follows an intalation guide step by step, in future releases some steps may become automatic, making it even more easier for fresh install:
54 |
55 | ### 1 - Install Node.js
56 |
57 | Node.js will be the "fuel" of this project. No, you don't need to know what it is or what it does just have it installed.
58 |
59 | In case you already have it make sure it's above version 18
60 |
61 | Download it here: https://nodejs.org/en
62 |
63 | ### 2 - Install LM Studio
64 |
65 | LM Studio will contain the core of your waifu, it's imperative for it to be installed.
66 |
67 | Don't worry it has a very user friendly usage.
68 |
69 | Download it here: https://lmstudio.ai/
70 |
71 |
72 | ### 3 - Install MyWaifu
73 |
74 | Now install MyWaifu.
75 |
76 | You can download the zip or tar.gz right here on GitHub - https://github.com/2D-girls-enjoyer/MyWaifu/releases
77 |
78 |
79 | ### Done!!!
80 | Now that you have everything installed let's move to the usage guide, so you can begin using MyWaifu!
81 |
82 | ## 🧾Usage guide & recommendation
83 |
84 | ### 1 - Set up LM Studio
85 | First, you need to choose what AI it will be based on. This is a very important step since it will determine the quality and speed of the waifu responses.
86 |
87 |
88 | ⚠️Warning: Double check if you are at Power User mode before all⚠️
89 |
90 |
91 |
92 |
93 | (If you already have knoledge in LLM models feel free to choose any and try)
94 |
95 | #### 1.1 - Search and download the model (If your first time)
96 | Go to "Discover" to find new models and download it.
97 |
98 |
99 |
100 |
101 |
102 | Some models has multiple options to download.
103 |
104 | To simplify things, think that the less space (Gigabytes) it has, the more "dumb" the model will be, but it'll be faster.
105 |
106 | ⚠️Warning: Prefer models that can be fully or partially loaded on GPU⚠️
107 |
108 |
109 |
110 |
111 | ##### Recommended models
112 |
113 | 1. (Character fidelity | General activities) **Llama-3.2-3B-Instruct-GGUF** (Q4_K_M) by **lmstudio-community** - Note: In terms of character personification, it has a good fidelity to it's personality (so if your waifu is unresonable, so it'll be) and has decent grasp on general knowledge.
114 |
115 | 2. (Character "realism") **stablelm-zephyr-3b-GGUF** (Q4_K_M) by **TheBloke** - Note: It can assimilate the waifu personality but it'll ground it to reasonable responses. In other words, it'll be as if your waifu lived in the real world and had boundries.
116 |
117 | #### 1.2 - Use the model
118 | Go to "Developer" and click "Select a model to load" choose your model.
119 |
120 | Also make sure to click "Start Server", so MyWaifu can communicate with the AI.
121 |
122 |
123 |
124 |
125 |
126 |
127 | **TIP:** After loading the model you can balance the usage of GPU (in case your GPU is "weak"). Note this will be a trade off between your RAM and CPU, so "less GPU" more CPU and RAM it will consume.
128 |
129 | ### 2 - Start MyWaifu
130 | To start MyWaifu is very simple
131 |
132 | ⚠️Warning: The first time you start it might take some time, since it will build and run⚠️
133 |
134 | #### On Windows
135 | Double-click the **MyWaifu-Windows.bat** and it's up and running
136 |
137 | #### On Linux
138 | Double-click the **MyWaifu-Linux.sh** and it's up and running
139 |
140 |
141 | ## ✍️ Authors
142 |
143 | - [@weeb_head_yabai](https://twitter.com/weeb_head_yabai) - Idea & Initial work
144 |
--------------------------------------------------------------------------------
/ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { observer, useLocalObservable } from 'mobx-react';
3 | import Modal from './components/modal/modal';
4 | import Navbar from './components/navbar/navbar';
5 | import MainChat from './pages/main-chat/mainChat';
6 | import store from './store/store';
7 | import http from './infra/http';
8 | import { API_DOMAIN } from './constants';
9 | import Slidebar from './components/slidebar/slidebar';
10 | import AlertStack from './components/alert-stack/alertStack';
11 |
12 | const App = observer(() => {
13 | const localStore = useLocalObservable(() => ({
14 | waifuList: [] as string[],
15 | openSlidebar: false,
16 | openChatDeletionModal: false,
17 | openUsernameModal: false,
18 | openSelectWaifuModal: false,
19 |
20 | setWaifuList(waifuList: string[]) {
21 | this.waifuList = waifuList;
22 | },
23 | setOpenSlidebar(isOpen: boolean) {
24 | this.openSlidebar = isOpen;
25 | },
26 | setOpenChatDeletionModal(isOpen: boolean) {
27 | this.openChatDeletionModal = isOpen;
28 | },
29 | setOpenUsernameModal(isOpen: boolean) {
30 | this.openUsernameModal = isOpen;
31 | },
32 | setOpenSelectWaifuModal(isOpen: boolean) {
33 | this.openSelectWaifuModal = isOpen;
34 | },
35 | }));
36 |
37 | const deleteCurrentChat = async () => {
38 | await store.deleteWaifuChat();
39 | localStore.setOpenChatDeletionModal(false);
40 | };
41 |
42 | const saveUsername = () => {
43 | store.saveUsername();
44 | localStore.setOpenUsernameModal(false);
45 | };
46 |
47 | const onUsernameInputKeydown = (e: React.KeyboardEvent) => {
48 | if (e.key === 'Enter' && !e.shiftKey) {
49 | e.preventDefault();
50 | saveUsername();
51 | }
52 | };
53 |
54 | const openWaifuSelectionModel = async () => {
55 | const waifuListPromise = http.getWaifus();
56 | localStore.setOpenSelectWaifuModal(true);
57 | localStore.setWaifuList((await waifuListPromise).waifus);
58 | };
59 |
60 | const selectWaifu = async (waifu: string) => {
61 | await store.selectWaifu(waifu);
62 | localStore.setOpenSelectWaifuModal(false);
63 | localStore.setOpenSlidebar(false);
64 | };
65 |
66 | useEffect(() => {
67 | async function firstLoadUsername() {
68 | if (store.username === '') {
69 | await store.loadUsername();
70 | }
71 | }
72 |
73 | firstLoadUsername();
74 | }, []);
75 |
76 | return (
77 |
78 |
79 |
82 |
83 | localStore.setOpenSlidebar(true)}
85 | onDeleteChatClick={() => localStore.setOpenChatDeletionModal(true)}
86 | />
87 |
88 |
89 |
90 |
Waifu live reaction soon
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | {/* Modals and slidebars */}
99 |
100 | {/* Menu Slidebar */}
101 | { localStore.setOpenSlidebar(false); }}
103 | onWaifuSelectModalClick={openWaifuSelectionModel}
104 | onUsernameModalClick={() => localStore.setOpenUsernameModal(true)}
105 | open={localStore.openSlidebar}
106 | />
107 |
108 | {/* Chat deletion modal */}
109 | localStore.setOpenChatDeletionModal(false)}
111 | confirmationBtnText="Delete"
112 | onConfirmationBtnClick={deleteCurrentChat}
113 | onCancelBtnClick={() => localStore.setOpenChatDeletionModal(false)}
114 | open={localStore.openChatDeletionModal}
115 | >
116 |
117 |
118 | DELETE CURRENT CHAT
119 |
120 |
121 | You will delete current chat forever
122 |
123 |
124 |
125 |
126 | {/* Waifu selection modal */}
127 | localStore.setOpenSelectWaifuModal(false)}
129 | onConfirmationBtnClick={() => {}}
130 | onCancelBtnClick={() => localStore.setOpenSelectWaifuModal(false)}
131 | open={localStore.openSelectWaifuModal}
132 | >
133 |
134 |
135 | CHOOSE YOUR WAIFU
136 |
137 |
138 | {localStore.waifuList.map((waifu) => (
139 |
selectWaifu(waifu)}
143 | className="flex flex-row p-2 w-full mb-3 cursor-pointer outline
144 | outline-offset-0 outline-2 outline-primary-color rounded-sm shadow-md"
145 | >
146 |
147 |
152 |
155 | {waifu}
156 |
157 |
158 | ))}
159 |
160 |
161 |
162 |
163 | {/* Username modal */}
164 | localStore.setOpenUsernameModal(false)}
166 | confirmationBtnText="Save"
167 | onConfirmationBtnClick={saveUsername}
168 | onCancelBtnClick={() => localStore.setOpenUsernameModal(false)}
169 | open={localStore.openUsernameModal}
170 | >
171 |
172 |
173 | SET USERNAME
174 |
175 |
176 | How do you want your waifu to call you?
177 |
178 |
188 |
189 |
190 | );
191 | });
192 |
193 | export default App;
194 |
--------------------------------------------------------------------------------