added backend which maintains ai instances;
added frontend as control panel
This commit is contained in:
68
frontend/README.md
Normal file
68
frontend/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# MangaStatus
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
|
||||
|
||||
```sh
|
||||
npm run test:e2e:dev
|
||||
```
|
||||
|
||||
This runs the end-to-end tests against the Vite development server.
|
||||
It is much faster than the production build.
|
||||
|
||||
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
8
frontend/cypress.config.ts
Normal file
8
frontend/cypress.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://localhost:4173'
|
||||
}
|
||||
})
|
||||
8
frontend/cypress/e2e/example.cy.ts
Normal file
8
frontend/cypress/e2e/example.cy.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// https://on.cypress.io/api
|
||||
|
||||
describe('My First Test', () => {
|
||||
it('visits the app root url', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('h1', 'You did it!')
|
||||
})
|
||||
})
|
||||
10
frontend/cypress/e2e/tsconfig.json
Normal file
10
frontend/cypress/e2e/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["./**/*", "../support/**/*"],
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
||||
5
frontend/cypress/fixtures/example.json
Normal file
5
frontend/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
39
frontend/cypress/support/commands.ts
Normal file
39
frontend/cypress/support/commands.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export {}
|
||||
20
frontend/cypress/support/e2e.ts
Normal file
20
frontend/cypress/support/e2e.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
1
frontend/env.d.ts
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Laura</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
6983
frontend/package-lock.json
generated
Normal file
6983
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
frontend/package.json
Normal file
64
frontend/package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"author": "wea_ondara",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
|
||||
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/",
|
||||
"clean:src": "find src | grep -E '((\\.d\\.ts)|(\\.js(\\.map)?))$' | xargs rm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.11.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-vue-next": "^0.16.6",
|
||||
"html-entities": "^2.5.2",
|
||||
"idb": "^7.1.1",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-class-component": "^0.9.4",
|
||||
"string-similarity-js": "^2.1.4",
|
||||
"vue": "^3.3.4",
|
||||
"vue-class-component": "^8.0.0-rc.1",
|
||||
"vue-good-table-next": "^0.2.1",
|
||||
"vue-i18n": "^9.10.2",
|
||||
"vue-property-decorator": "^10.0.0-rc.3",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue3-toastify": "^0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^18.19.26",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^13.7.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all2": "^6.1.2",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.72.0",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"typescript": "~5.2.0",
|
||||
"vite": "^4.5.2",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
36
frontend/src/App.vue
Normal file
36
frontend/src/App.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import {RouterView} from 'vue-router';
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import DocumentLocaleSetter from '@/components/locale/DocumentLocaleSetter.vue';
|
||||
import NavBar from '@/components/navbar/NavBar.vue';
|
||||
import LocaleSaver from '@/components/locale/LocaleSaver.vue';
|
||||
import SideBar from '@/components/sidebar/SideBar.vue';
|
||||
|
||||
@Options({
|
||||
name: 'App',
|
||||
components: {
|
||||
DocumentLocaleSetter,
|
||||
LocaleSaver,
|
||||
NavBar,
|
||||
RouterView,
|
||||
SideBar,
|
||||
},
|
||||
})
|
||||
export default class App extends Vue {
|
||||
sidebarToggled = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-100 w-100 overflow-hidden">
|
||||
<LocaleSaver/>
|
||||
<DocumentLocaleSetter/>
|
||||
<SideBar ref="sidebar" class="h-100 w-100 overflow-hidden" :toggled="sidebarToggled"
|
||||
@close="sidebarToggled=false"/>
|
||||
<div class="d-flex flex-column h-100 w-100 overflow-hidden">
|
||||
<NavBar @toggleSidebar="sidebarToggled = !sidebarToggled"/>
|
||||
<RouterView class="flex-grow-1"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/src/assets/bootstrap.extensions.scss
vendored
Normal file
8
frontend/src/assets/bootstrap.extensions.scss
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
//@use 'sass:map';
|
||||
//@import '~bootstrap/scss/bootstrap';
|
||||
//@import '~bootstrap/scss/variables';
|
||||
//@import '~bootstrap/scss/mixins';
|
||||
|
||||
.c-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
11
frontend/src/assets/main.css
Normal file
11
frontend/src/assets/main.css
Normal file
@@ -0,0 +1,11 @@
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
22
frontend/src/components/Logo.vue
Normal file
22
frontend/src/components/Logo.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<img src="/favicon.ico" alt="logo" height="24" width="24" class="me-1">
|
||||
<b>Laura</b>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Options({name: 'Logo'})
|
||||
export default class Logo extends Vue {
|
||||
}
|
||||
</script>
|
||||
11
frontend/src/components/__tests__/HelloWorld.spec.ts
Normal file
11
frontend/src/components/__tests__/HelloWorld.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// import {describe, expect, it} from 'vitest';
|
||||
//
|
||||
// import {mount} from '@vue/test-utils';
|
||||
// import HelloWorld from '../HelloWorld.vue';
|
||||
//
|
||||
// describe('HelloWorld', () => {
|
||||
// it('renders properly', () => {
|
||||
// const wrapper = mount(HelloWorld, {props: {msg: 'Hello Vitest'}});
|
||||
// expect(wrapper.text()).toContain('Hello Vitest');
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {reactive} from 'vue';
|
||||
import {Watch} from 'vue-property-decorator';
|
||||
|
||||
const sharedDarkMode = reactive({
|
||||
internalDarkMode: false,
|
||||
internalLoaded: false,
|
||||
|
||||
get darkMode() {
|
||||
if (!this.internalLoaded) {
|
||||
this.internalDarkMode = localStorage.getItem('darkmode') === ('' + true);
|
||||
this.internalLoaded = true;
|
||||
}
|
||||
return this.internalDarkMode;
|
||||
},
|
||||
|
||||
set darkMode(value: boolean) {
|
||||
this.internalDarkMode = value;
|
||||
localStorage.setItem('darkmode', '' + this.internalDarkMode);
|
||||
},
|
||||
});
|
||||
|
||||
@Options({
|
||||
name: 'BootstrapThemeSwitch',
|
||||
})
|
||||
export default class BootstrapThemeSwitch extends Vue {
|
||||
get sharedDarkMode() {
|
||||
return sharedDarkMode;
|
||||
}
|
||||
|
||||
mounted(): void {
|
||||
this.onThemeChanged(this.dark);
|
||||
}
|
||||
|
||||
get dark(): boolean {
|
||||
return this.sharedDarkMode.darkMode;
|
||||
}
|
||||
|
||||
set dark(value: boolean) {
|
||||
this.sharedDarkMode.darkMode = value;
|
||||
}
|
||||
|
||||
@Watch('dark', {immediate: true})
|
||||
private onThemeChanged(isDarkMode: boolean): void {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-bs-theme');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-check form-switch light-dark-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" v-model="dark">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.light-dark-switch .form-check-input {
|
||||
--bs-form-switch-bg: url(assets/sun.svg) !important;
|
||||
}
|
||||
|
||||
.light-dark-switch .form-check-input:focus {
|
||||
--bs-form-switch-bg: url(assets/sun_focus.svg) !important;
|
||||
}
|
||||
|
||||
.light-dark-switch .form-check-input:checked,
|
||||
.light-dark-switch .form-check-input:checked:focus {
|
||||
--bs-form-switch-bg: url(assets/moon.svg) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g transform="scale(0.75 0.75) translate(92 92)" fill="#fff">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M144.7 98.7c-21 34.1-33.1 74.3-33.1 117.3c0 98 62.8 181.4 150.4 211.7c-12.4 2.8-25.3 4.3-38.6 4.3C126.6 432 48 353.3 48 256c0-68.9 39.4-128.4 96.8-157.3zm62.1-66C91.1 41.2 0 137.9 0 256C0 379.7 100 480 223.5 480c47.8 0 92-15 128.4-40.6c1.9-1.3 3.7-2.7 5.5-4c4.8-3.6 9.4-7.4 13.9-11.4c2.7-2.4 5.3-4.8 7.9-7.3c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-3.7 .6-7.4 1.2-11.1 1.6c-5 .5-10.1 .9-15.3 1c-1.2 0-2.5 0-3.7 0c-.1 0-.2 0-.3 0c-96.8-.2-175.2-78.9-175.2-176c0-54.8 24.9-103.7 64.1-136c1-.9 2.1-1.7 3.2-2.6c4-3.2 8.2-6.2 12.5-9c3.1-2 6.3-4 9.6-5.8c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-3.6-.3-7.1-.5-10.7-.6c-2.7-.1-5.5-.1-8.2-.1c-3.3 0-6.5 .1-9.8 .2c-2.3 .1-4.6 .2-6.9 .4z"/>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g transform="scale(0.75 0.75) translate(92 92)" fill="rgba(0,0,0,0.25)">
|
||||
<svg viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M375.7 19.7c-1.5-8-6.9-14.7-14.4-17.8s-16.1-2.2-22.8 2.4L256 61.1 173.5 4.2c-6.7-4.6-15.3-5.5-22.8-2.4s-12.9 9.8-14.4 17.8l-18.1 98.5L19.7 136.3c-8 1.5-14.7 6.9-17.8 14.4s-2.2 16.1 2.4 22.8L61.1 256 4.2 338.5c-4.6 6.7-5.5 15.3-2.4 22.8s9.8 13 17.8 14.4l98.5 18.1 18.1 98.5c1.5 8 6.9 14.7 14.4 17.8s16.1 2.2 22.8-2.4L256 450.9l82.5 56.9c6.7 4.6 15.3 5.5 22.8 2.4s12.9-9.8 14.4-17.8l18.1-98.5 98.5-18.1c8-1.5 14.7-6.9 17.8-14.4s2.2-16.1-2.4-22.8L450.9 256l56.9-82.5c4.6-6.7 5.5-15.3 2.4-22.8s-9.8-12.9-17.8-14.4l-98.5-18.1L375.7 19.7zM269.6 110l65.6-45.2 14.4 78.3c1.8 9.8 9.5 17.5 19.3 19.3l78.3 14.4L402 242.4c-5.7 8.2-5.7 19 0 27.2l45.2 65.6-78.3 14.4c-9.8 1.8-17.5 9.5-19.3 19.3l-14.4 78.3L269.6 402c-8.2-5.7-19-5.7-27.2 0l-65.6 45.2-14.4-78.3c-1.8-9.8-9.5-17.5-19.3-19.3L64.8 335.2 110 269.6c5.7-8.2 5.7-19 0-27.2L64.8 176.8l78.3-14.4c9.8-1.8 17.5-9.5 19.3-19.3l14.4-78.3L242.4 110c8.2 5.7 19 5.7 27.2 0zM256 368a112 112 0 1 0 0-224 112 112 0 1 0 0 224zM192 256a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z"/>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g transform="scale(0.75 0.75) translate(92 92)" fill="#86b7fe">
|
||||
<svg viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M375.7 19.7c-1.5-8-6.9-14.7-14.4-17.8s-16.1-2.2-22.8 2.4L256 61.1 173.5 4.2c-6.7-4.6-15.3-5.5-22.8-2.4s-12.9 9.8-14.4 17.8l-18.1 98.5L19.7 136.3c-8 1.5-14.7 6.9-17.8 14.4s-2.2 16.1 2.4 22.8L61.1 256 4.2 338.5c-4.6 6.7-5.5 15.3-2.4 22.8s9.8 13 17.8 14.4l98.5 18.1 18.1 98.5c1.5 8 6.9 14.7 14.4 17.8s16.1 2.2 22.8-2.4L256 450.9l82.5 56.9c6.7 4.6 15.3 5.5 22.8 2.4s12.9-9.8 14.4-17.8l18.1-98.5 98.5-18.1c8-1.5 14.7-6.9 17.8-14.4s2.2-16.1-2.4-22.8L450.9 256l56.9-82.5c4.6-6.7 5.5-15.3 2.4-22.8s-9.8-12.9-17.8-14.4l-98.5-18.1L375.7 19.7zM269.6 110l65.6-45.2 14.4 78.3c1.8 9.8 9.5 17.5 19.3 19.3l78.3 14.4L402 242.4c-5.7 8.2-5.7 19 0 27.2l45.2 65.6-78.3 14.4c-9.8 1.8-17.5 9.5-19.3 19.3l-14.4 78.3L269.6 402c-8.2-5.7-19-5.7-27.2 0l-65.6 45.2-14.4-78.3c-1.8-9.8-9.5-17.5-19.3-19.3L64.8 335.2 110 269.6c5.7-8.2 5.7-19 0-27.2L64.8 176.8l78.3-14.4c9.8-1.8 17.5-9.5 19.3-19.3l14.4-78.3L242.4 110c8.2 5.7 19 5.7 27.2 0zM256 368a112 112 0 1 0 0-224 112 112 0 1 0 0 224zM192 256a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z"/>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
26
frontend/src/components/dashboard/AiInstanceComponent.vue
Normal file
26
frontend/src/components/dashboard/AiInstanceComponent.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from "vue-property-decorator";
|
||||
import type {AiInstance} from "@/data/models/AiInstance";
|
||||
import Discord from "@/components/dashboard/Discord.vue";
|
||||
import Chat from "@/components/dashboard/Chat.vue";
|
||||
|
||||
@Options({
|
||||
name: 'AiInstanceComponent',
|
||||
components: {Chat, Discord},
|
||||
})
|
||||
export default class AiInstanceComponent extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly aiInstance!: AiInstance;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3>Name: {{ aiInstance.configuration.name }}</h3>
|
||||
<div class="d-flex flex-row">
|
||||
<Chat class="flex-grow-1 me-2" style="width: 66.66%" :ai-instance="aiInstance"/>
|
||||
<Discord class="flex-grow-1" style="width: 33.33%" :ai-instance="aiInstance"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
66
frontend/src/components/dashboard/AiInstanceTabs.vue
Normal file
66
frontend/src/components/dashboard/AiInstanceTabs.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import type {AiInstance} from "@/data/models/AiInstance";
|
||||
import AiInstanceApi from "@/data/api/AiInstanceApi";
|
||||
import AiInstanceComponent from "@/components/dashboard/AiInstanceComponent.vue";
|
||||
|
||||
@Options({
|
||||
name: 'AiInstanceTabs',
|
||||
components: {AiInstanceComponent},
|
||||
})
|
||||
export default class AiInstanceTabs extends Vue {
|
||||
readonly aiInstances: AiInstance[] = [];
|
||||
readonly aiInstanceApi = new AiInstanceApi();
|
||||
selectedAiInstance: AiInstance | null = null;
|
||||
|
||||
mounted(): void {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
const list = await this.aiInstanceApi.fetchAiInstances();
|
||||
this.aiInstances.splice(0);
|
||||
this.aiInstances.push(...list);
|
||||
if (list.length > 0) {
|
||||
this.selectedAiInstance = list[0];
|
||||
} else {
|
||||
this.selectedAiInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column">
|
||||
<!-- tabs -->
|
||||
<div class="d-flex flex-row">
|
||||
<div class="flex-grow-1">
|
||||
<!-- no ais warning -->
|
||||
<div v-if="aiInstances.length == 0" class="mt-2 alert alert-warning" role="alert">
|
||||
No AIs defined!
|
||||
</div>
|
||||
|
||||
<!-- actual tabs -->
|
||||
<div class="btn-group">
|
||||
<button v-for="aiInstance in aiInstances"
|
||||
:key="aiInstance.configuration.name"
|
||||
class="btn" :class="{
|
||||
'btn-secondary': aiInstance === selectedAiInstance,
|
||||
'btn-secondary-outline': aiInstance !== selectedAiInstance
|
||||
}"
|
||||
@click="selectedAiInstance = aiInstance">
|
||||
{{ aiInstance.configuration.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reload -->
|
||||
<button class="btn btn-secondary" @click="reload()">
|
||||
<i class="fa fa-refresh"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- panels -->
|
||||
<AiInstanceComponent v-if="selectedAiInstance" class="flex-grow-1 mt-2" :ai-instance="selectedAiInstance"/>
|
||||
</div>
|
||||
</template>
|
||||
78
frontend/src/components/dashboard/Chat.vue
Normal file
78
frontend/src/components/dashboard/Chat.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from "vue-property-decorator";
|
||||
import AiInstanceApi from "@/data/api/AiInstanceApi";
|
||||
import type {AiInstance} from "@/data/models/AiInstance";
|
||||
import {toast} from "vue3-toastify";
|
||||
import type {ChatMessage} from "@/data/models/chat/ChatMessage";
|
||||
import {BSpinner} from "bootstrap-vue-next";
|
||||
|
||||
@Options({
|
||||
name: 'Chat',
|
||||
components: {BSpinner},
|
||||
})
|
||||
export default class Chat extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly aiInstance!: AiInstance;
|
||||
readonly aiInstanceApi = new AiInstanceApi();
|
||||
user: string = 'alice';
|
||||
text: string = '';
|
||||
waiting: boolean = false;
|
||||
message: ChatMessage | null = null;
|
||||
|
||||
async send(): Promise<void> {
|
||||
this.waiting = true;
|
||||
try {
|
||||
this.message = {'role': 'user', 'name': this.user, 'content': this.text};
|
||||
this.text = '';
|
||||
const response = await this.aiInstanceApi.chatText(this.aiInstance.configuration.name, this.message);
|
||||
debugger;
|
||||
this.aiInstance.messages.push(this.message);
|
||||
this.aiInstance.messages.push(response);
|
||||
} catch (e) {
|
||||
toast.error('Error while chatting: ' + JSON.stringify(e));
|
||||
} finally {
|
||||
this.waiting = false;
|
||||
this.message = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.input-group-vertical :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group-vertical :not(:last-child) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Chat</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group-vertical">
|
||||
<div class="form-control overflow-y-auto">
|
||||
<div v-if="aiInstance.messages.length === 0">
|
||||
<i>No conversation history</i>
|
||||
</div>
|
||||
<div v-for="message in aiInstance.messages">
|
||||
<b>{{ message.name }}</b>{{ ': ' + message.content }}
|
||||
</div>
|
||||
<div v-if="waiting">
|
||||
<b>{{ message!.name }}</b>{{ ': ' + message!.content }}
|
||||
<Spinner/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" v-model="user" placeholder="Username" style="max-width: 10em"/>
|
||||
<input type="text" class="form-control" v-model="text" placeholder="Type message here" @keydown.enter="send"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
15
frontend/src/components/dashboard/DashBoard.vue
Normal file
15
frontend/src/components/dashboard/DashBoard.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import AiInstanceTabs from "@/components/dashboard/AiInstanceTabs.vue";
|
||||
|
||||
@Options({
|
||||
name: 'DashBoard',
|
||||
components: {AiInstanceTabs},
|
||||
})
|
||||
export default class DashBoard extends Vue {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AiInstanceTabs class="m-3"/>
|
||||
</template>
|
||||
67
frontend/src/components/dashboard/Discord.vue
Normal file
67
frontend/src/components/dashboard/Discord.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from "vue-property-decorator";
|
||||
import type {AiInstance} from "@/data/models/AiInstance";
|
||||
import {getCurrentInstance} from "vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
import AiInstanceApi from "@/data/api/AiInstanceApi";
|
||||
|
||||
@Options({
|
||||
name: 'Discord',
|
||||
methods: {getCurrentInstance},
|
||||
components: {},
|
||||
})
|
||||
export default class Discord extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly aiInstance!: AiInstance;
|
||||
readonly aiInstanceApi = new AiInstanceApi();
|
||||
|
||||
get online(): boolean {
|
||||
return this.aiInstance.discord.online;
|
||||
}
|
||||
|
||||
set online(val: boolean) {
|
||||
const oldVal = this.online;
|
||||
this.aiInstance.discord.online = val;
|
||||
this.aiInstanceApi.discordOnline(this.aiInstance.configuration.name, val).catch(e => {
|
||||
toast.error('Error while set online status: ' + JSON.stringify(e));
|
||||
this.aiInstance.discord.online = oldVal;
|
||||
});
|
||||
}
|
||||
|
||||
get react_to_chat(): boolean {
|
||||
return this.aiInstance.discord.react_to_chat;
|
||||
}
|
||||
|
||||
set react_to_chat(val: boolean) {
|
||||
const oldVal = this.online;
|
||||
this.aiInstance.discord.react_to_chat = val;
|
||||
this.aiInstanceApi.discordReactToChat(this.aiInstance.configuration.name, val).catch(e => {
|
||||
toast.error('Error while setting react_to_chat status: ' + JSON.stringify(e));
|
||||
this.aiInstance.discord.react_to_chat = oldVal;
|
||||
});
|
||||
}
|
||||
|
||||
get uid(): string {
|
||||
return "" + getCurrentInstance()?.uid!;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card" style="min-width: 15em">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Discord</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" :id="uid + '_online'" v-model="online">
|
||||
<label class="form-check-label" :for="uid + '_online'">Online</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" :id="uid + '_reactToChat'" v-model="react_to_chat">
|
||||
<label class="form-check-label" :for="uid + '_reactToChat'">React to chat</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
23
frontend/src/components/locale/DocumentLocaleSetter.vue
Normal file
23
frontend/src/components/locale/DocumentLocaleSetter.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Watch} from 'vue-property-decorator';
|
||||
|
||||
@Options({name: 'DocumentLocaleSetter'})
|
||||
export default class DocumentLocaleSetter extends Vue {
|
||||
mounted(): void {
|
||||
this.onLocaleChanged(this.$i18n.locale);
|
||||
}
|
||||
|
||||
@Watch('$i18n.locale')
|
||||
onLocaleChanged(value: string): void {
|
||||
this.setDocumentLocale(value);
|
||||
}
|
||||
|
||||
setDocumentLocale(locale: string): void {
|
||||
document.documentElement.setAttribute('lang', locale);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
</template>
|
||||
21
frontend/src/components/locale/LocaleSaver.vue
Normal file
21
frontend/src/components/locale/LocaleSaver.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Watch} from 'vue-property-decorator';
|
||||
|
||||
@Options({name: 'LocaleSaver'})
|
||||
export default class LocaleSaver extends Vue {
|
||||
mounted(): void {
|
||||
const locale = window.localStorage.getItem('locale');
|
||||
if (locale)
|
||||
this.$i18n.locale = locale;
|
||||
}
|
||||
|
||||
@Watch('$i18n.locale')
|
||||
onLocaleChanged(value: string): void {
|
||||
window.localStorage.setItem('locale', value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
</template>
|
||||
37
frontend/src/components/locale/LocaleSelector.vue
Normal file
37
frontend/src/components/locale/LocaleSelector.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
|
||||
@Options({name: 'LocaleSelector'})
|
||||
export default class LocaleSelector extends Vue {
|
||||
get locale(): string {
|
||||
return this.$i18n.locale;
|
||||
}
|
||||
|
||||
set locale(value: string) {
|
||||
this.$i18n.locale = value;
|
||||
}
|
||||
|
||||
get supportedLocales(): { value: string, text: string } [] {
|
||||
return [
|
||||
{value: 'en', text: this.$t('locales.en')},
|
||||
{value: 'de', text: this.$t('locales.de')},
|
||||
];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown">
|
||||
<button aria-expanded="false" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" type="button">
|
||||
<i class="fa fa-language"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start">
|
||||
<li v-for="l in supportedLocales" :key="l.value">
|
||||
<a :class="{active: locale === l.value}" class="dropdown-item" href="#"
|
||||
@click.prevent="locale = l.value">
|
||||
{{ l.text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
85
frontend/src/components/navbar/NavBar.vue
Normal file
85
frontend/src/components/navbar/NavBar.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import BootstrapThemeSwitch from '@/components/bootstrapThemeSwitch/BootstrapThemeSwitch.vue';
|
||||
import LocaleSelector from '@/components/locale/LocaleSelector.vue';
|
||||
import Logo from '@/components/Logo.vue';
|
||||
|
||||
@Options({
|
||||
name: 'NavBar',
|
||||
components: {
|
||||
BootstrapThemeSwitch,
|
||||
LocaleSelector,
|
||||
Logo,
|
||||
},
|
||||
emits: {
|
||||
'toggleSidebar': undefined,
|
||||
},
|
||||
})
|
||||
export default class NavBar extends Vue {
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- z-index needed, otherwise shadow not showing -->
|
||||
<nav class="navbar border-bottom shadow flex-nowrap" style="z-index: 1">
|
||||
<div>
|
||||
<div class="navbar-sidebar-toggler mx-1 c-pointer">
|
||||
<i class="fa fa-bars me-2" @click="$emit('toggleSidebar')"/>
|
||||
</div>
|
||||
<div class="navbar-nav flex-row">
|
||||
<Logo class="navbar-logo"/>
|
||||
<div class="nav-item">
|
||||
<RouterLink class="nav-link" aria-current="page" to="/" active-class="active">Laura</RouterLink>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<RouterLink class="nav-link" aria-current="page" to="/about" active-class="active">About</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex flex-row">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<LocaleSelector class="navbar-locale-select"/>
|
||||
<BootstrapThemeSwitch class="navbar-theme-switch ms-2"/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'bootstrap/scss/functions';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'bootstrap/scss/mixins/breakpoints';
|
||||
|
||||
nav.navbar .navbar-logo,
|
||||
nav.navbar .nav-item {
|
||||
margin-inline-end: 1rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
nav.navbar {
|
||||
--bs-navbar-padding-x: 0.5rem;
|
||||
}
|
||||
|
||||
nav.navbar .navbar-nav,
|
||||
nav.navbar .navbar-locale-select,
|
||||
nav.navbar .navbar-theme-switch {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
nav.navbar {
|
||||
--bs-navbar-padding-x: 1rem;
|
||||
}
|
||||
|
||||
nav.navbar .navbar-sidebar-toggler {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
96
frontend/src/components/sidebar/SideBar.vue
Normal file
96
frontend/src/components/sidebar/SideBar.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import SideBarNavItem from '@/components/sidebar/SideBarNavItem.vue';
|
||||
import SideBarNavLink from '@/components/sidebar/SideBarNavLink.vue';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import LocaleSelector from '@/components/locale/LocaleSelector.vue';
|
||||
import BootstrapThemeSwitch from '@/components/bootstrapThemeSwitch/BootstrapThemeSwitch.vue';
|
||||
import SideBarHead from '@/components/sidebar/SideBarHead.vue';
|
||||
|
||||
@Options({
|
||||
name: 'SideBar',
|
||||
components: {
|
||||
BootstrapThemeSwitch,
|
||||
LocaleSelector,
|
||||
SideBarHead,
|
||||
SideBarNavItem,
|
||||
SideBarNavLink,
|
||||
},
|
||||
emits: {
|
||||
'close': undefined,
|
||||
},
|
||||
})
|
||||
export default class SideBar extends Vue {
|
||||
@Prop({default: false})
|
||||
toggled!: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ toggled: toggled }" class="sidebar border-right shadow bg-body">
|
||||
<div class="d-flex flex-column">
|
||||
<ul class="nav flex-column mb-auto">
|
||||
<li>
|
||||
<SideBarHead @close="$emit('close')"/>
|
||||
</li>
|
||||
<SideBarNavLink :text="$t('menu.list')" to="/" faIcon="fa-list"
|
||||
@click="$emit('close')"></SideBarNavLink>
|
||||
<SideBarNavLink :text="$t('menu.about')" to="/about" faIcon="fa-question"
|
||||
@click="$emit('close')"></SideBarNavLink>
|
||||
<li>
|
||||
<hr class="m-0"/>
|
||||
</li>
|
||||
<SideBarNavItem :text="$t('locale')" faIcon="fa-language">
|
||||
<template #end>
|
||||
<LocaleSelector class="navbar-locale-select me-1"/>
|
||||
</template>
|
||||
</SideBarNavItem>
|
||||
<SideBarNavItem :text="$t('design')" faIcon="fa-moon">
|
||||
<template #end>
|
||||
<BootstrapThemeSwitch class="navbar-theme-switch"/>
|
||||
</template>
|
||||
</SideBarNavItem>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'bootstrap/scss/functions';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'bootstrap/scss/mixins/breakpoints';
|
||||
|
||||
$width: 15em;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: $width;
|
||||
max-width: #{'clamp(0em, '}$width#{', 100%)'};
|
||||
transition: width ease-in-out 0.25s;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
height: 100%;
|
||||
|
||||
&:not(.toggled) {
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
.sidebar-nav-item {
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
26
frontend/src/components/sidebar/SideBarHead.vue
Normal file
26
frontend/src/components/sidebar/SideBarHead.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import Logo from '@/components/Logo.vue';
|
||||
|
||||
@Options({
|
||||
name: 'SideBar',
|
||||
components: {Logo},
|
||||
emits: {
|
||||
'close': undefined,
|
||||
},
|
||||
})
|
||||
export default class SideBar extends Vue {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="navbar border-bottom shadow flex-nowrap">
|
||||
<div class="sidebar-closer mx-1 c-pointer d-flex flex-row align-items-center">
|
||||
<i class="fa fa-arrow-left me-2" @click="$emit('close')"/>
|
||||
|
||||
<!-- 'hack' to ensure this nav is the same height as the navbar -->
|
||||
<input class="form-control d-inline mx-0 px-0" style="visibility: hidden; width: 0;"/>
|
||||
<Logo/>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
45
frontend/src/components/sidebar/SideBarNavItem.vue
Normal file
45
frontend/src/components/sidebar/SideBarNavItem.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
|
||||
@Options({
|
||||
name: 'SideBarNavItem',
|
||||
components: {},
|
||||
})
|
||||
export default class SideBarNavItem extends Vue {
|
||||
@Prop({required: false})
|
||||
faIcon!: string;
|
||||
@Prop({required: false})
|
||||
text!: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="nav-item sidebar-nav-item">
|
||||
<slot>
|
||||
<div class="d-flex flex-row p-2 align-items-center">
|
||||
<i :class="[faIcon]" class="sidebar-nav-item-icon text-center fa"></i>
|
||||
<span class="sidebar-nav-item-text me-auto">
|
||||
<slot name="text" :text="text">
|
||||
{{ text }}
|
||||
</slot>
|
||||
</span>
|
||||
<slot name="end"/>
|
||||
</div>
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-nav-item-text {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
max-width: 10em;
|
||||
transition: display ease-in-out 0.25s, opacity ease-in-out 0.25s, max-width ease-in-out 0.25s;
|
||||
}
|
||||
|
||||
.sidebar-nav-item-icon {
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
35
frontend/src/components/sidebar/SideBarNavLink.vue
Normal file
35
frontend/src/components/sidebar/SideBarNavLink.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import SideBarNavItem from '@/components/sidebar/SideBarNavItem.vue';
|
||||
|
||||
@Options({
|
||||
name: 'SideBarNavLink',
|
||||
components: {SideBarNavItem},
|
||||
})
|
||||
export default class SideBarNavLink extends Vue {
|
||||
@Prop({required: false})
|
||||
faIcon!: string;
|
||||
@Prop({required: false})
|
||||
text!: string;
|
||||
@Prop({required: true})
|
||||
to!: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.no-link, .no-link:link, .no-link:hover {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<SideBarNavItem :fa-icon="faIcon" :text="text">
|
||||
<template #text="props">
|
||||
<RouterLink :to="to" class="no-link">
|
||||
{{ props.text }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
</SideBarNavItem>
|
||||
</template>
|
||||
45
frontend/src/data/api/AiInstanceApi.ts
Normal file
45
frontend/src/data/api/AiInstanceApi.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {handleJsonResponse} from '@/data/api/ApiUtils';
|
||||
import type {AiInstance} from "@/data/models/AiInstance";
|
||||
import type {ChatMessage} from "@/data/models/chat/ChatMessage";
|
||||
|
||||
export default class AiInstanceApi {
|
||||
readonly baseurl = 'http://localhost:8877/'
|
||||
|
||||
async fetchAiInstances(): Promise<AiInstance[]> {
|
||||
const res = fetch(this.baseurl + 'api/v1/ai',
|
||||
{
|
||||
headers: {'Accept': 'application/json'}
|
||||
});
|
||||
return await handleJsonResponse(res);
|
||||
}
|
||||
|
||||
async chatText(aiName: string, message: ChatMessage): Promise<ChatMessage> {
|
||||
const res = fetch(this.baseurl + 'api/v1/ai/' + aiName + '/chat/text',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
return await handleJsonResponse(res);
|
||||
}
|
||||
|
||||
async discordOnline(aiName: string, online: boolean): Promise<void> {
|
||||
const res = fetch(this.baseurl + 'api/v1/ai/' + aiName + '/discord',
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({online: online}),
|
||||
});
|
||||
await handleJsonResponse(res);
|
||||
}
|
||||
|
||||
async discordReactToChat(aiName: string, react_to_chat: boolean): Promise<void> {
|
||||
const res = fetch(this.baseurl + 'api/v1/ai/' + aiName + '/discord',
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({react_to_chat: react_to_chat}),
|
||||
});
|
||||
await handleJsonResponse(res);
|
||||
}
|
||||
}
|
||||
22
frontend/src/data/api/ApiUtils.ts
Normal file
22
frontend/src/data/api/ApiUtils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export class ApiError extends Error {
|
||||
readonly statusCode: number;
|
||||
|
||||
constructor(statusCode: number, message?: string) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleJsonResponse<T>(res: Promise<Response>): Promise<T> {
|
||||
return res.then(async e => {
|
||||
if (e.status === 200) {
|
||||
const text = await e.text();
|
||||
if (!text.length) {
|
||||
return undefined;
|
||||
} else {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
}
|
||||
throw new ApiError(e.status, 'Api error ' + e.status + ': ' + await e.text());
|
||||
});
|
||||
}
|
||||
3
frontend/src/data/models/AiConfiguration.ts
Normal file
3
frontend/src/data/models/AiConfiguration.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type AiConfiguration = {
|
||||
name: string;
|
||||
}
|
||||
11
frontend/src/data/models/AiInstance.ts
Normal file
11
frontend/src/data/models/AiInstance.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type {AiConfiguration} from "./AiConfiguration";
|
||||
import type {ChatMessage} from "@/data/models/chat/ChatMessage";
|
||||
|
||||
export type AiInstance = {
|
||||
configuration: AiConfiguration;
|
||||
discord: {
|
||||
online: boolean;
|
||||
react_to_chat: boolean;
|
||||
}
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
1
frontend/src/data/models/chat/ChatMessage.ts
Normal file
1
frontend/src/data/models/chat/ChatMessage.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type ChatMessage = { 'role': string, 'name': string, 'content': string }
|
||||
35
frontend/src/locale/de.ts
Normal file
35
frontend/src/locale/de.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {messagesEn} from '@/locale/en';
|
||||
|
||||
export const messagesDe = {
|
||||
design: 'Design',
|
||||
locale: 'Sprache',
|
||||
locales: messagesEn.locales,
|
||||
menu: {
|
||||
about: 'Über',
|
||||
list: 'Liste',
|
||||
},
|
||||
search: 'Suche',
|
||||
};
|
||||
|
||||
export const datetimeFormatsDe = {
|
||||
date: {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
},
|
||||
datetime: {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
datetimeSeconds: {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
seconds: 'numeric',
|
||||
},
|
||||
};
|
||||
36
frontend/src/locale/en.ts
Normal file
36
frontend/src/locale/en.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export const messagesEn = {
|
||||
design: 'Design',
|
||||
locale: 'Language',
|
||||
locales: {
|
||||
'de': 'Deutsch',
|
||||
'en': 'English',
|
||||
},
|
||||
menu: {
|
||||
about: 'About',
|
||||
list: 'List',
|
||||
},
|
||||
search: 'Search',
|
||||
};
|
||||
|
||||
export const datetimeFormatsEn = {
|
||||
date: {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
},
|
||||
datetime: {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
datetimeSeconds: {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
seconds: 'numeric',
|
||||
},
|
||||
};
|
||||
32
frontend/src/locale/locale.ts
Normal file
32
frontend/src/locale/locale.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type {I18n} from 'vue-i18n';
|
||||
import {createI18n as vueCreateI18n} from 'vue-i18n';
|
||||
import {datetimeFormatsEn, messagesEn} from '@/locale/en';
|
||||
import {datetimeFormatsDe, messagesDe} from '@/locale/de';
|
||||
|
||||
const messages = {
|
||||
en: messagesEn,
|
||||
de: messagesDe,
|
||||
};
|
||||
|
||||
const datetimeFormats = {
|
||||
en: datetimeFormatsEn,
|
||||
de: datetimeFormatsDe,
|
||||
};
|
||||
|
||||
export function createI18n(): I18n {
|
||||
let browserLocale = (navigator.language ?? '').toLowerCase();
|
||||
const match = browserLocale.match(/^([a-z][a-z])/);
|
||||
if (match) {
|
||||
browserLocale = match[1];
|
||||
} else {
|
||||
browserLocale = 'en';
|
||||
}
|
||||
return vueCreateI18n({
|
||||
locale: browserLocale,
|
||||
fallbackLocale: 'en',
|
||||
messages: messages,
|
||||
//@ts-ignore TS2769 TODO weird typing issue
|
||||
datetimeFormats: datetimeFormats,
|
||||
legacy: true,
|
||||
});
|
||||
}
|
||||
24
frontend/src/main.ts
Normal file
24
frontend/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {createApp} from 'vue';
|
||||
import {createPinia} from 'pinia';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import './assets/main.css';
|
||||
import './assets/bootstrap.extensions.scss';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
// noinspection ES6UnusedImports
|
||||
import * as bootstrap from 'bootstrap';
|
||||
// noinspection ES6UnusedImports
|
||||
import * as popper from '@popperjs/core';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
import {createI18n} from '@/locale/locale';
|
||||
import 'vue3-toastify/dist/index.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(createI18n());
|
||||
|
||||
app.mount('#app');
|
||||
23
frontend/src/router/index.ts
Normal file
23
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {createRouter, createWebHistory} from 'vue-router';
|
||||
import HomeView from '../views/HomeView.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
10
frontend/src/views/AboutView.vue
Normal file
10
frontend/src/views/AboutView.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="p-3 overflow-y-auto">
|
||||
<div class="mb-3">
|
||||
<h1>About</h1>
|
||||
Control panel for Ais
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
15
frontend/src/views/HomeView.vue
Normal file
15
frontend/src/views/HomeView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import DashBoard from '../components/dashboard/DashBoard.vue'
|
||||
|
||||
@Options({
|
||||
name: 'HomeView',
|
||||
components: {DashBoard},
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DashBoard />
|
||||
</template>
|
||||
18
frontend/tsconfig.app.json
Normal file
18
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": [
|
||||
"ES2021",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
]
|
||||
}
|
||||
}
|
||||
14
frontend/tsconfig.json
Normal file
14
frontend/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
frontend/tsconfig.node.json
Normal file
17
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
7
frontend/tsconfig.vitest.json
Normal file
7
frontend/tsconfig.vitest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
25
frontend/vite.config.ts
Normal file
25
frontend/vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {fileURLToPath, URL} from 'node:url';
|
||||
|
||||
import {defineConfig} from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(), VueI18nPlugin({compositionOnly: false}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
input: ['./src/main.ts', './index.html'],
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
});
|
||||
14
frontend/vitest.config.ts
Normal file
14
frontend/vitest.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {configDefaults, defineConfig, mergeConfig} from 'vitest/config';
|
||||
import viteConfig from './vite.config';
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/*'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
},
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user