added backend which maintains ai instances;
added frontend as control panel
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user