Added Chatbot to the App!

This commit is contained in:
Donald Zou
2025-03-28 00:13:38 +08:00
parent 5067485e94
commit f0c3ef0aa1
14 changed files with 854 additions and 25 deletions

View File

@@ -2218,27 +2218,25 @@ def API_UpdateWireguardConfigurationRawFile():
@app.post(f'{APP_PREFIX}/api/deleteWireguardConfiguration')
def API_deleteWireguardConfiguration():
data = request.get_json()
if "Name" not in data.keys() or data.get("Name") is None or data.get("Name") not in WireguardConfigurations.keys():
if "ConfigurationName" not in data.keys() or data.get("ConfigurationName") is None or data.get("ConfigurationName") not in WireguardConfigurations.keys():
return ResponseObject(False, "Please provide the configuration name you want to delete")
status = WireguardConfigurations[data.get("Name")].deleteConfiguration()
status = WireguardConfigurations[data.get("ConfigurationName")].deleteConfiguration()
if status:
WireguardConfigurations.pop(data.get("Name"))
WireguardConfigurations.pop(data.get("ConfigurationName"))
return ResponseObject(status)
@app.post(f'{APP_PREFIX}/api/renameWireguardConfiguration')
def API_renameWireguardConfiguration():
data = request.get_json()
keys = ["Name", "NewConfigurationName"]
keys = ["ConfigurationName", "NewConfigurationName"]
for k in keys:
if (k not in data.keys() or data.get(k) is None or len(data.get(k)) == 0 or
(k == "Name" and data.get(k) not in WireguardConfigurations.keys())):
(k == "ConfigurationName" and data.get(k) not in WireguardConfigurations.keys())):
return ResponseObject(False, "Please provide the configuration name you want to rename")
status, message = WireguardConfigurations[data.get("Name")].renameConfiguration(data.get("NewConfigurationName"))
status, message = WireguardConfigurations[data.get("ConfigurationName")].renameConfiguration(data.get("NewConfigurationName"))
if status:
WireguardConfigurations.pop(data.get("Name"))
WireguardConfigurations.pop(data.get("ConfigurationName"))
WireguardConfigurations[data.get("NewConfigurationName")] = WireguardConfiguration(data.get("NewConfigurationName"))
return ResponseObject(status, message)
@@ -3156,4 +3154,4 @@ def startThreads():
if __name__ == "__main__":
startThreads()
app.run(host=app_ip, debug=False, port=app_port)
app.run(host=app_ip, debug=False, port=app_port)

View File

@@ -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", {

View File

@@ -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){

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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)

View File

@@ -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%;
}

View File

@@ -49,7 +49,7 @@ export default {
@supports (height: 100dvh) {
@media screen and (max-width: 768px) {
main{
height: calc(100dvh - 50px);
height: calc(100dvh - 58px);
}
}
}

View File

@@ -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\\?": ""
}

View File

@@ -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\\?": "您想问些什么?"
}