mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2025-10-03 15:56:17 +00:00
Added Chatbot to the App!
This commit is contained in:
@@ -16,7 +16,7 @@ const deleteConfiguration = () => {
|
||||
clearInterval(store.Peers.RefreshInterval)
|
||||
deleting.value = true;
|
||||
fetchPost("/api/deleteWireguardConfiguration", {
|
||||
Name: configurationName
|
||||
ConfigurationName: configurationName
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
router.push('/')
|
||||
@@ -30,7 +30,6 @@ const deleteConfiguration = () => {
|
||||
|
||||
const loading = ref(true)
|
||||
const backups = ref([])
|
||||
let timeout = undefined;
|
||||
const getBackup = () => {
|
||||
loading.value = true;
|
||||
fetchGet("/api/getWireguardConfigurationBackup", {
|
||||
|
@@ -28,7 +28,7 @@ const rename = async () => {
|
||||
loading.value = true
|
||||
clearInterval(dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
await fetchPost("/api/renameWireguardConfiguration", {
|
||||
Name: props.configurationName,
|
||||
ConfigurationName: props.configurationName,
|
||||
NewConfigurationName: newConfigurationName.data
|
||||
}, async (res) => {
|
||||
if (res.status){
|
||||
|
@@ -5,10 +5,11 @@ import {fetchGet} from "@/utilities/fetch.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import HelpModal from "@/components/navbarComponents/helpModal.vue";
|
||||
import AgentModal from "@/components/navbarComponents/agentModal.vue";
|
||||
|
||||
export default {
|
||||
name: "navbar",
|
||||
components: {HelpModal, LocaleText},
|
||||
components: {HelpModal, LocaleText, AgentModal},
|
||||
setup(){
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
@@ -20,6 +21,7 @@ export default {
|
||||
updateMessage: "Checking for update...",
|
||||
updateUrl: "",
|
||||
openHelpModal: false,
|
||||
openAgentModal: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -78,7 +80,7 @@ export default {
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-3" role="button" @click="openHelpModal = true">
|
||||
<a class="nav-link rounded-3" role="button" @click="openAgentModal = true">
|
||||
<i class="bi bi-question-circle me-2"></i>
|
||||
<LocaleText t="Help"></LocaleText>
|
||||
</a>
|
||||
@@ -146,19 +148,23 @@ export default {
|
||||
<Transition name="zoom">
|
||||
<HelpModal v-if="this.openHelpModal" @close="openHelpModal = false;"></HelpModal>
|
||||
</Transition>
|
||||
<Transition name="slideIn">
|
||||
<AgentModal v-if="this.openAgentModal" @close="openAgentModal = false"></AgentModal>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 768px) {
|
||||
.navbar-container{
|
||||
position: absolute;
|
||||
position: absolute !important;
|
||||
z-index: 1000;
|
||||
animation-duration: 0.4s;
|
||||
animation-fill-mode: both;
|
||||
display: none;
|
||||
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.navbar-container.active{
|
||||
animation-direction: normal;
|
||||
display: block !important;
|
||||
@@ -168,14 +174,14 @@ export default {
|
||||
|
||||
.navbar-container{
|
||||
height: 100vh;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
@media screen and (max-width: 768px){
|
||||
.navbar-container{
|
||||
height: calc(100dvh - 50px);
|
||||
height: calc(100dvh - 58px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,4 +201,16 @@ export default {
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.slideIn-enter-active,
|
||||
.slideIn-leave-active{
|
||||
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 1);
|
||||
}
|
||||
|
||||
.slideIn-enter-from,
|
||||
.slideIn-leave-to {
|
||||
transform: translateX(30px);
|
||||
filter: blur(3px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import {computed, onMounted} from "vue";
|
||||
import {marked} from "marked";
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
content: String,
|
||||
id: String,
|
||||
role: String,
|
||||
time: String
|
||||
},
|
||||
ind: Number
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelector(".agentChatroomBody").scrollTop =
|
||||
document.querySelector(".agentChatroomBody").scrollHeight
|
||||
})
|
||||
|
||||
const convertMarkdown = computed(() => {
|
||||
return marked.parse(props.message.content)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{'d-flex flex-row align-items-end gap-2': message.role === 'assistant', 'mt-auto': ind === 0}">
|
||||
<div class="p-2 rounded-5 text-bg-secondary" style="line-height: 1" v-if="message.role === 'assistant'">
|
||||
<i class="bi bi-robot"></i>
|
||||
</div>
|
||||
<div class="d-flex text-body agentMessage" :class="{'ms-auto': message.role === 'user'}" >
|
||||
<div class="px-3 py-2 rounded-3 shadow-sm"
|
||||
:class="[ message.role === 'user' ?
|
||||
'text-bg-primary ms-auto align-items-end':'text-bg-secondary align-items-start']">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agentMessage{
|
||||
white-space: break-spaces;
|
||||
max-width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.text-bg-secondary{
|
||||
background-color: RGBA(var(--bs-secondary-rgb), 0.7) !important;
|
||||
}
|
||||
|
||||
.text-bg-primary{
|
||||
background-color: RGBA(var(--bs-primary-rgb), 0.7) !important;
|
||||
}
|
||||
</style>
|
291
src/static/app/src/components/navbarComponents/agentModal.vue
Normal file
291
src/static/app/src/components/navbarComponents/agentModal.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<script setup>
|
||||
import {onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from "vue";
|
||||
import { v4 } from "uuid"
|
||||
import dayjs from "dayjs";
|
||||
import AgentMessage from "@/components/navbarComponents/agentMessage.vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
|
||||
const userPrompt = ref("")
|
||||
const waitingMessage = ref(false)
|
||||
const messages = reactive({})
|
||||
const agentBaseUrl = "https://agent-aee6a811e474080613b1-ie5yg.ondigitalocean.app";
|
||||
const agentChatbotId = "JrYZtArj_C5FGRts06op58QUHPFCgUzo";
|
||||
const agentId = "1150ab95-025b-11f0-bf8f-4e013e2ddde4"
|
||||
let refreshTokenInterval = undefined;
|
||||
let agentToken = ref(undefined);
|
||||
const agentHealth = ref(false);
|
||||
const checkingAgentHealth = ref(false);
|
||||
const scrollMessageBody = () => {
|
||||
document.querySelector(".agentChatroomBody").scrollTop = document.querySelector(".agentChatroomBody").scrollHeight
|
||||
}
|
||||
const newMessage = (role, content) => {
|
||||
let sentMsgId = v4().toString();
|
||||
messages[sentMsgId] = {
|
||||
id: sentMsgId,
|
||||
role: role,
|
||||
content: content,
|
||||
time: dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||||
}
|
||||
return sentMsgId
|
||||
}
|
||||
|
||||
const pushPrompt = () => {
|
||||
if (userPrompt.value){
|
||||
newMessage('user', userPrompt.value)
|
||||
userPrompt.value = "";
|
||||
waitingMessage.value = true;
|
||||
fetch(`${agentBaseUrl}/api/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${agentToken.value.access_token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"include_functions_info": false,
|
||||
"include_retrieval_info": false,
|
||||
"include_guardrails_info": false,
|
||||
"stream": true,
|
||||
"messages": Object.values(messages)
|
||||
})
|
||||
}).then(response => {
|
||||
if (response.ok){
|
||||
const stream = response.body;
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let recvMsgId = newMessage('assistant', '')
|
||||
const readChunk = () => {
|
||||
reader.read()
|
||||
.then(({value, done}) => {
|
||||
if (done) {
|
||||
waitingMessage.value = false
|
||||
return
|
||||
}
|
||||
let chunkString = decoder.decode(value, {stream: true}).trim();
|
||||
chunkString = chunkString.split("\n")
|
||||
chunkString.forEach(x => {
|
||||
if (x){
|
||||
x = x.replace(/^data:\s*/, "")
|
||||
if (x !== "[DONE]"){
|
||||
let d = JSON.parse(x);
|
||||
d.choices.forEach(c => {
|
||||
if (c.delta.content){
|
||||
messages[recvMsgId].content += c.delta.content
|
||||
scrollMessageBody()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
readChunk();
|
||||
})
|
||||
.catch(error => {
|
||||
waitingMessage.value = false
|
||||
messages[recvMsgId].content = "Sorry, the bot is not responding."
|
||||
});
|
||||
};
|
||||
readChunk();
|
||||
}else{
|
||||
waitingMessage.value = false;
|
||||
throw new Error("Invalid response")
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const initAgent = async () => {
|
||||
checkingAgentHealth.value = true;
|
||||
await fetch(`${agentBaseUrl}/health`, {
|
||||
signal: AbortSignal.timeout(3000)
|
||||
}).then(res => res.json()).then(res => {
|
||||
agentHealth.value = res.status === 'ok'
|
||||
}).catch(() => {
|
||||
checkingAgentHealth.value = false;
|
||||
agentHealth.value = false;
|
||||
})
|
||||
if (agentHealth.value){
|
||||
await fetch(`https://cloud.digitalocean.com/gen-ai/auth/agents/${agentId}/token`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': agentChatbotId
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({})
|
||||
}).then(res => {
|
||||
if (!res.ok){
|
||||
throw new Error('Access token not available')
|
||||
}else{
|
||||
return res.json()
|
||||
}
|
||||
}).then(res => {
|
||||
agentToken.value = res;
|
||||
checkingAgentHealth.value = false;
|
||||
}).catch(() => {
|
||||
checkingAgentHealth.value = false;
|
||||
agentHealth.value = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const refreshAgentToken = async () => {
|
||||
if (agentToken.value){
|
||||
await fetch(
|
||||
`https://cloud.digitalocean.com/gen-ai/auth/agents/${agentId}/token?refresh_token=${agentToken.value.refresh_token}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': agentChatbotId
|
||||
},
|
||||
method: "PUT",
|
||||
body: JSON.stringify({})
|
||||
}).then(res => res.json()).then(res => {
|
||||
agentHealth.value = true;
|
||||
agentToken.value = res;
|
||||
}).catch(err => {
|
||||
agentHealth.value = false;
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
newMessage('assistant', GetLocale('Hi! How can I help you today?'))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await initAgent();
|
||||
refreshTokenInterval = setInterval(async () => {
|
||||
await refreshAgentToken()
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(refreshTokenInterval);
|
||||
})
|
||||
|
||||
const emits = defineEmits(['close'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agentContainer m-2 rounded-3 d-flex flex-column text-body" :class="{'connected': agentHealth && !checkingAgentHealth}">
|
||||
<TransitionGroup name="agent-message">
|
||||
<div key="header" class=" shadow ">
|
||||
<div class="p-3 d-flex gap-2 flex-column ">
|
||||
<div class="d-flex text-body" >
|
||||
<div class="d-flex flex-column align-items-start gap-1">
|
||||
<h5 class="mb-0">
|
||||
<LocaleText t="Help"></LocaleText>
|
||||
</h5>
|
||||
</div>
|
||||
<a role="button" class="ms-auto text-body" @click="emits('close')">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</h5>
|
||||
</a>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
<LocaleText t="You can visit our: "></LocaleText>
|
||||
</p>
|
||||
<div class="list-group">
|
||||
<a href="https://donaldzou.github.io/WGDashboard-Documentation/"
|
||||
target="_blank" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<i class="bi bi-book-fill"></i>
|
||||
<LocaleText class="ms-auto" t="Official Documentation"></LocaleText>
|
||||
</a>
|
||||
<a target="_blank" role="button" href="https://discord.gg/72TwzjeuWm"
|
||||
class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<i class="bi bi-discord"></i>
|
||||
<LocaleText class="ms-auto" t="Discord Server"></LocaleText>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center p-3">
|
||||
<h5 class="mb-0"><LocaleText t="WGDashboard Help Bot"></LocaleText></h5>
|
||||
<h6 class="mb-0 ms-auto">
|
||||
<span class="mb-0 text-muted d-flex gap-2 align-items-center" v-if="checkingAgentHealth">
|
||||
<span class="spinner-border spinner-border-sm"></span>
|
||||
<small>
|
||||
<LocaleText t="Connecting..."></LocaleText>
|
||||
</small>
|
||||
</span>
|
||||
<span class="mb-0 d-flex gap-2 align-items-center badge"
|
||||
:class="[agentHealth ? 'text-bg-success':'text-bg-danger']"
|
||||
v-else>
|
||||
{{ agentHealth ? 'Connected':'Not Connected'}}
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agentChatroomBody p-3 pb-5 d-flex flex-column gap-3 flex-grow-1"
|
||||
key="body"
|
||||
v-if="agentHealth && !checkingAgentHealth">
|
||||
<TransitionGroup name="agent-message">
|
||||
<AgentMessage :message="msg" v-for="(msg, index) in Object.values(messages)" :key="msg.id" :ind="index"></AgentMessage>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<div class="d-flex text-white align-items-center p-3 gap-3 rounded-bottom-3 mt-auto"
|
||||
key="input"
|
||||
v-if="agentHealth && !checkingAgentHealth"
|
||||
style="box-shadow: 1px -1rem 3rem 0 rgba(0, 0, 0, 0.175) !important">
|
||||
<input type="text" class="form-control rounded-3 bg-transparent border-0"
|
||||
:placeholder="GetLocale('What do you want to ask?')"
|
||||
@keyup.enter="pushPrompt"
|
||||
v-model="userPrompt" :disabled="waitingMessage">
|
||||
<a role="button" class="agentChatroomSendButton text-body" @click="pushPrompt">
|
||||
<i class="bi bi-send-fill" v-if="!waitingMessage"></i>
|
||||
<span class="spinner-border spinner-border-sm" v-else></span>
|
||||
</a>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agentContainer{
|
||||
--agentHeight: 100vh;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
width: 450px;
|
||||
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
background: linear-gradient(var(--degree), #009dff52 var(--distance2), #ff4a0052 100%);
|
||||
}
|
||||
|
||||
.agentContainer.connected{
|
||||
height: calc(var(--agentHeight) - 1rem);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.agentContainer{
|
||||
--agentHeight: 100vh !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
max-height: calc(var(--agentHeight) - 58px - 1rem);
|
||||
width: calc( 100% - 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
.agentChatroomBody{
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
max-height: calc(var(--agentHeight) - 70px - 244px);
|
||||
}
|
||||
|
||||
.agent-message-move, /* apply transition to moving elements */
|
||||
.agent-message-enter-active,
|
||||
.agent-message-leave-active {
|
||||
transition: all 0.5s cubic-bezier(0.82, 0.58, 0.17, 1);
|
||||
}
|
||||
.agent-message-enter-from,
|
||||
.agent-message-leave-to {
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transform: translateY(30px);
|
||||
}
|
||||
.agent-message-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
|
||||
const discordLoading = ref(true)
|
||||
const discord = ref(undefined)
|
||||
|
@@ -113,8 +113,8 @@
|
||||
}
|
||||
|
||||
.btn-brand:hover, .dashboardLogo:hover{
|
||||
--brandColor1: #009dff;
|
||||
--brandColor2: #ff875b;
|
||||
--brandColor1: rgba(0, 157, 255, 1);
|
||||
--brandColor2: rgba(255, 135, 91, 1);
|
||||
--distance2: 30%;
|
||||
}
|
||||
|
||||
|
@@ -49,7 +49,7 @@ export default {
|
||||
@supports (height: 100dvh) {
|
||||
@media screen and (max-width: 768px) {
|
||||
main{
|
||||
height: calc(100dvh - 50px);
|
||||
height: calc(100dvh - 58px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -308,5 +308,7 @@
|
||||
"Deleted ([0-9]{1,}) peer\\(s\\)": "",
|
||||
"Deleted ([0-9]{1,}) peer\\(s\\) successfully. Failed to delete ([0-9]{1,}) peer\\(s\\)": "",
|
||||
"Restricted ([0-9]{1,}) peer\\(s\\)": "",
|
||||
"Restricted ([0-9]{1,}) peer\\(s\\) successfully. Failed to restrict ([0-9]{1,}) peer\\(s\\)": ""
|
||||
"Restricted ([0-9]{1,}) peer\\(s\\) successfully. Failed to restrict ([0-9]{1,}) peer\\(s\\)": "",
|
||||
"Hi! How can I help you today\\?": "",
|
||||
"What do you want to ask\\?": ""
|
||||
}
|
@@ -308,5 +308,7 @@
|
||||
"Deleted ([0-9]{1,}) peer\\(s\\)": "删除了$1个端点",
|
||||
"Deleted ([0-9]{1,}) peer\\(s\\) successfully. Failed to delete ([0-9]{1,}) peer\\(s\\)": "成功删除了$1个端点,失败删除了$2个端点",
|
||||
"Restricted ([0-9]{1,}) peer\\(s\\)": "限制访问了$1个端点",
|
||||
"Restricted ([0-9]{1,}) peer\\(s\\) successfully. Failed to restrict ([0-9]{1,}) peer\\(s\\)": "成功限制访问了$1个端点,失败限制访问了$2个端点"
|
||||
"Restricted ([0-9]{1,}) peer\\(s\\) successfully. Failed to restrict ([0-9]{1,}) peer\\(s\\)": "成功限制访问了$1个端点,失败限制访问了$2个端点",
|
||||
"Hi! How can I help you today\\?": "您好!有什么可以帮到您的吗?",
|
||||
"What do you want to ask\\?": "您想问些什么?"
|
||||
}
|
Reference in New Issue
Block a user