UI重构以及历史问答bug修复
This commit is contained in:
parent
9cec978383
commit
d947d82602
23
run.sh
Normal file
23
run.sh
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Function to stop the services
|
||||||
|
stop_services() {
|
||||||
|
echo "Stopping services..."
|
||||||
|
pkill -f "npm run server"
|
||||||
|
pkill -f "flask --app=api run"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trap signals to stop services
|
||||||
|
trap stop_services SIGINT SIGTERM
|
||||||
|
|
||||||
|
# Start the frontend service
|
||||||
|
cd web
|
||||||
|
npm run server &
|
||||||
|
|
||||||
|
# Start the backend service
|
||||||
|
cd ../src
|
||||||
|
flask --app=api run &
|
||||||
|
|
||||||
|
# Wait for all background jobs to finish
|
||||||
|
wait
|
||||||
37
src/api.py
37
src/api.py
@ -4,7 +4,7 @@ from flask import Flask, jsonify, Response, request
|
|||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from core import HistoryManager
|
from core import HistoryManager
|
||||||
from core import PreRetrieval
|
from core import Retriever
|
||||||
from config import Config
|
from config import Config
|
||||||
from models import select_model
|
from models import select_model
|
||||||
from utils.logging_config import setup_logger
|
from utils.logging_config import setup_logger
|
||||||
@ -16,7 +16,7 @@ logger = setup_logger("server")
|
|||||||
|
|
||||||
config = Config("config/base.yaml")
|
config = Config("config/base.yaml")
|
||||||
model = select_model(config)
|
model = select_model(config)
|
||||||
pre_retrieval = PreRetrieval(config)
|
retriever = Retriever(config)
|
||||||
|
|
||||||
|
|
||||||
apps = Flask(__name__)# 这段代码是为了解决跨域问题,Flask默认不支持跨域
|
apps = Flask(__name__)# 这段代码是为了解决跨域问题,Flask默认不支持跨域
|
||||||
@ -39,27 +39,17 @@ def page_not_found(e):
|
|||||||
def chat_get():
|
def chat_get():
|
||||||
return "Chat Get!"
|
return "Chat Get!"
|
||||||
|
|
||||||
|
|
||||||
@apps.route('/chat', methods=['POST'])
|
@apps.route('/chat', methods=['POST'])
|
||||||
def chat():
|
def chat():
|
||||||
request_data = json.loads(request.data)
|
request_data = json.loads(request.data)
|
||||||
query = request_data['query']
|
query = request_data['query']
|
||||||
logger.debug(f"Web query: {query}")
|
logger.debug(f"Web query: {query}")
|
||||||
|
|
||||||
external = ""
|
new_query, refs = retriever(query)
|
||||||
if config.enable_knowledge_base:
|
|
||||||
kb_res = pre_retrieval.search(query)
|
|
||||||
if kb_res:
|
|
||||||
kb_res = "\n".join([f"{r['id']}: {r['entity']['text']}" for r in kb_res])
|
|
||||||
kb_res = f"知识库信息: {kb_res}"
|
|
||||||
external += kb_res
|
|
||||||
|
|
||||||
if len(external) > 0:
|
|
||||||
query = f"以下是参考资料:\n\n\n {external} 请根据前面的知识回答:{query}"
|
|
||||||
|
|
||||||
|
|
||||||
history_manager = HistoryManager(request_data['history'])
|
history_manager = HistoryManager(request_data['history'])
|
||||||
messages = history_manager.add_user(query)
|
messages = history_manager.get_history_with_msg(new_query)
|
||||||
|
history_manager.add_user(query)
|
||||||
logger.debug(f"Web history: {history_manager}")
|
logger.debug(f"Web history: {history_manager}")
|
||||||
|
|
||||||
def generate_response():
|
def generate_response():
|
||||||
@ -68,14 +58,27 @@ def chat():
|
|||||||
content += delta.content
|
content += delta.content
|
||||||
response_chunk = json.dumps({
|
response_chunk = json.dumps({
|
||||||
"history": history_manager.update_ai(content),
|
"history": history_manager.update_ai(content),
|
||||||
"response": content
|
"response": content,
|
||||||
|
"refs": refs # TODO: 优化 refs,不需要每次都返回
|
||||||
}, ensure_ascii=False).encode('utf8') + b'\n'
|
}, ensure_ascii=False).encode('utf8') + b'\n'
|
||||||
yield response_chunk
|
yield response_chunk
|
||||||
|
|
||||||
return Response(generate_response(), content_type='application/json', status=200)
|
return Response(generate_response(), content_type='application/json', status=200)
|
||||||
|
|
||||||
|
@apps.route('/call', methods=['POST'])
|
||||||
|
def call():
|
||||||
|
request_data = json.loads(request.data)
|
||||||
|
query = request_data['query']
|
||||||
|
logger.debug(f"Web query: {query}")
|
||||||
|
response = model.predict(query, stream=False)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"response": response.content,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("Starting model...")
|
print("Starting model...")
|
||||||
apps.secret_key = os.urandom(24)
|
apps.secret_key = os.urandom(24)
|
||||||
apps.run(host='0.0.0.0', port=8000, debug=False, threaded=True)
|
apps.run(host='0.0.0.0', port=8000, debug=True, threaded=True)
|
||||||
@ -23,9 +23,7 @@ if __name__ == "__main__":
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 检索结果
|
# 检索结果
|
||||||
refs = retriever(query)
|
query, refs = retriever(query)
|
||||||
# 重新构建用户的输入
|
|
||||||
query = retriever.construct_query(query, refs)
|
|
||||||
|
|
||||||
messages = history_manager.add_user(query)
|
messages = history_manager.add_user(query)
|
||||||
response = model.predict(messages, stream=config.stream)
|
response = model.predict(messages, stream=config.stream)
|
||||||
|
|||||||
@ -25,6 +25,12 @@ class HistoryManager():
|
|||||||
self.add_ai(content)
|
self.add_ai(content)
|
||||||
return self.messages
|
return self.messages
|
||||||
|
|
||||||
|
def get_history_with_msg(self, msg, role="user"):
|
||||||
|
"""Get history with new message, but not append it to history."""
|
||||||
|
history = self.messages[:]
|
||||||
|
history.append({"role": role, "content": msg})
|
||||||
|
return history
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
history_str = ""
|
history_str = ""
|
||||||
for message in self.messages:
|
for message in self.messages:
|
||||||
|
|||||||
@ -47,4 +47,6 @@ class Retriever:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __call__(self, query):
|
def __call__(self, query):
|
||||||
return self.retrieval(query)
|
refs = self.retrieval(query)
|
||||||
|
query = self.construct_query(query, refs)
|
||||||
|
return query, refs
|
||||||
12
web/package-lock.json
generated
12
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"echarts": "^5.4.2",
|
"echarts": "^5.4.2",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
|
"marked": "^13.0.2",
|
||||||
"pinia": "^2.0.32",
|
"pinia": "^2.0.32",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
@ -2153,6 +2154,17 @@
|
|||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "13.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-13.0.2.tgz",
|
||||||
|
"integrity": "sha512-J6CPjP8pS5sgrRqxVRvkCIkZ6MFdRIjDkwUwgJ9nL2fbmM6qGQeB2C16hi8Cc9BOzj6xXzy0jyi0iPIfnMHYzA==",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
"echarts": "^5.4.2",
|
"echarts": "^5.4.2",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
|
"marked": "^13.0.2",
|
||||||
"pinia": "^2.0.32",
|
"pinia": "^2.0.32",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
|
|||||||
BIN
web/public/jnu.png
Normal file
BIN
web/public/jnu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@ -1,79 +1,3 @@
|
|||||||
<script setup>
|
|
||||||
import { KeepAlive } from 'vue'
|
|
||||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
// 打印当前页面的路由信息,使用 vue3 的 setup composition API
|
|
||||||
const route = useRoute()
|
|
||||||
console.log(route)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="header">
|
<router-view />
|
||||||
<nav class="nav">
|
|
||||||
<RouterLink to="/" class="nav-item" active-class="active">主页</RouterLink>
|
|
||||||
<RouterLink to="/chat" class="nav-item" active-class="active">问答</RouterLink>
|
|
||||||
<!-- <RouterLink to="/kg" class="nav-item" active-class="active">图谱</RouterLink> -->
|
|
||||||
<!-- <RouterLink to="/knowledge" class="nav-item" active-class="active">知识</RouterLink> -->
|
|
||||||
<!-- <RouterLink to="/about" class="nav-item" active-class="active">关于</RouterLink> -->
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<router-view v-slot="{ Component }">
|
|
||||||
<keep-alive>
|
|
||||||
<component :is="Component" />
|
|
||||||
</keep-alive>
|
|
||||||
</router-view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
height: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: background-color 0.2s ease-in-out;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:hover {
|
|
||||||
background-color: #e2eef3;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #005f77;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
width: 2rem;
|
|
||||||
// left: 0;
|
|
||||||
height: 2px;
|
|
||||||
background-color: #005f77;
|
|
||||||
margin-top: 4px;
|
|
||||||
// z-index: 10;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -34,6 +34,13 @@
|
|||||||
--color-text: var(--vt-c-text-light-1);
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
--section-gap: 160px;
|
--section-gap: 160px;
|
||||||
|
|
||||||
|
--main-color: #005f77;
|
||||||
|
--main-color-light: #007f96;
|
||||||
|
--main-color-dark: #004d5c;
|
||||||
|
--min-width: 400px;
|
||||||
|
--min-header-width: 80px;
|
||||||
|
--min-sider-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @media (prefers-color-scheme: dark) {
|
/* @media (prefers-color-scheme: dark) {
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
@import './base.css';
|
@import './base.css';
|
||||||
|
|
||||||
#app {
|
:root {
|
||||||
max-width: 1280px;
|
--header-height: 60px;
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
|
|
||||||
font-weight: normal;
|
|
||||||
/* height: 100vh; */
|
|
||||||
}
|
}
|
||||||
470
web/src/components/ChatComponent.vue
Normal file
470
web/src/components/ChatComponent.vue
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
<!-- ChatComponent.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="chat">
|
||||||
|
<div class="header">
|
||||||
|
<div class="header__left">
|
||||||
|
<div
|
||||||
|
v-if="!state.isSidebarOpen"
|
||||||
|
class="close nav-btn"
|
||||||
|
@click="state.isSidebarOpen = true"
|
||||||
|
>
|
||||||
|
<MenuOutlined />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!state.isSidebarOpen"
|
||||||
|
class="newchat nav-btn"
|
||||||
|
@click="$emit('newconv')"
|
||||||
|
>
|
||||||
|
<FormOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header__right">
|
||||||
|
<div class="nav-btn text" @click="myAlert('未开发')">张文杰</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="conv.messages.length == 0" class="chat-examples">
|
||||||
|
<h1>你好,我是 Athena 😊</h1>
|
||||||
|
<div class="opt">
|
||||||
|
<div
|
||||||
|
class="opt__button"
|
||||||
|
v-for="(exp, key) in examples"
|
||||||
|
:key="key"
|
||||||
|
@click="autoSend(exp)"
|
||||||
|
>
|
||||||
|
{{ exp }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="chatBox" class="chat-box">
|
||||||
|
<div
|
||||||
|
v-for="message in conv.messages"
|
||||||
|
:key="message.id"
|
||||||
|
class="message-box"
|
||||||
|
:class="message.role"
|
||||||
|
>
|
||||||
|
<p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
|
||||||
|
<p v-else v-html="renderMarkdown(message.text)" class="message-md" ></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-box">
|
||||||
|
<input
|
||||||
|
class="user-input"
|
||||||
|
v-model="conv.inputText"
|
||||||
|
@keydown.enter="sendMessage"
|
||||||
|
placeholder="输入问题……"
|
||||||
|
/>
|
||||||
|
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)">
|
||||||
|
<template #icon> <SendOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<p class="note">即便强如雅典娜也可能会出错,请注意辨别内容的可靠性</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onMounted, toRefs } from 'vue'
|
||||||
|
import { SendOutlined, MenuOutlined, FormOutlined, LoadingOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conv: Object,
|
||||||
|
state: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['renameTitle'])
|
||||||
|
|
||||||
|
const { conv, state } = toRefs(props)
|
||||||
|
const chatBox = ref(null)
|
||||||
|
const isStreaming = ref(false)
|
||||||
|
const examples = ref([
|
||||||
|
'写一个冒泡排序',
|
||||||
|
'介绍一下 MECT',
|
||||||
|
'介绍一下江南大学',
|
||||||
|
'A大于B,B小于C,A和C哪个大?',
|
||||||
|
'今天天气怎么样?'
|
||||||
|
])
|
||||||
|
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
tables: true,
|
||||||
|
// 更多选项可以在 marked 文档中找到:https://marked.js.org/
|
||||||
|
});
|
||||||
|
|
||||||
|
const renameTitle = () => {
|
||||||
|
const prompt = '请用一个很短的句子关于下面的对话内容的主题起一个名字,不要带标点符号:'
|
||||||
|
const firstUserMessage = conv.value.messages[0].text
|
||||||
|
const firstAiMessage = conv.value.messages[1].text
|
||||||
|
const context = `${prompt}\n\n问题: ${firstUserMessage}\n\n回复: ${firstAiMessage},主题是(一句话):`
|
||||||
|
simpleCall(context).then((data) => {
|
||||||
|
emit('renameTitle', data.response.split(":")[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const myAlert = (message) => {
|
||||||
|
alert(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderMarkdown = (text) => {
|
||||||
|
return marked(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
chatBox.value.scrollTop = chatBox.value.scrollHeight - chatBox.value.clientHeight
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateRandomHash = (length) => {
|
||||||
|
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let hash = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
hash += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendMessage = (message, role) => {
|
||||||
|
conv.value.messages.push({
|
||||||
|
id: generateRandomHash(16),
|
||||||
|
role,
|
||||||
|
text: message
|
||||||
|
})
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendUserMessage = (message) => {
|
||||||
|
conv.value.messages.push({
|
||||||
|
id: generateRandomHash(16),
|
||||||
|
role: 'sent',
|
||||||
|
text: message
|
||||||
|
})
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendAiMessage = (message, refs=null) => {
|
||||||
|
conv.value.messages.push({
|
||||||
|
id: generateRandomHash(16),
|
||||||
|
role: 'received',
|
||||||
|
text: message,
|
||||||
|
refs
|
||||||
|
})
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMessage = (text, id) => {
|
||||||
|
const message = conv.value.messages.find((message) => message.id === id)
|
||||||
|
if (message) {
|
||||||
|
message.text = text
|
||||||
|
} else {
|
||||||
|
console.error('Message not found')
|
||||||
|
}
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const simpleCall = (message) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch('/api/call', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: message,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => resolve(data))
|
||||||
|
.catch((error) => reject(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (conv.value.inputText.trim()) {
|
||||||
|
isStreaming.value = true
|
||||||
|
appendUserMessage(conv.value.inputText)
|
||||||
|
const user_input = conv.value.inputText
|
||||||
|
var cur_res_id = null
|
||||||
|
conv.value.inputText = ''
|
||||||
|
fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: user_input,
|
||||||
|
history: conv.value.history,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then((response) => {const reader = response.body.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ''
|
||||||
|
// 逐步读取响应文本
|
||||||
|
const readChunk = () => {
|
||||||
|
return reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
console.log(conv.value)
|
||||||
|
console.log('Finished')
|
||||||
|
isStreaming.value = false
|
||||||
|
if (conv.value.messages.length === 2) {
|
||||||
|
renameTitle()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
const message = buffer.trim().split('\n').pop()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message)
|
||||||
|
if (cur_res_id === null) {
|
||||||
|
appendAiMessage(data.response, data.refs)
|
||||||
|
cur_res_id = conv.value.messages[conv.value.messages.length - 1].id
|
||||||
|
} else {
|
||||||
|
updateMessage(data.response, cur_res_id)
|
||||||
|
}
|
||||||
|
conv.value.history = data.history
|
||||||
|
buffer = ''
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
return readChunk()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return readChunk()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('Please enter a message')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoSend = (message) => {
|
||||||
|
conv.value.inputText = message
|
||||||
|
sendMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearChat = () => {
|
||||||
|
conv.value.messages = []
|
||||||
|
conv.value.history = []
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.chat {
|
||||||
|
width: 200px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: white;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 5 5 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat div.header {
|
||||||
|
height: var(--header-height);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat div.header .header__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #6D6D6D;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.text {
|
||||||
|
font-size: 1rem;
|
||||||
|
width: auto;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ECECEC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div.chat-examples {
|
||||||
|
padding: 0 50px;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-examples h1 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt__button {
|
||||||
|
background-color: white;
|
||||||
|
color: #222;
|
||||||
|
padding: 4px 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid white;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fcfcfc;
|
||||||
|
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-box {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
|
max-width: 90%;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
padding: 0.8rem;
|
||||||
|
user-select: text;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 16px;
|
||||||
|
font-variation-settings: 'wght' 400, 'opsz' 10.5;
|
||||||
|
font-weight: 400;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16); */
|
||||||
|
/* animation: slideInUp 0.1s ease-in; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.sent {
|
||||||
|
color: white;
|
||||||
|
background-color: #efefef;
|
||||||
|
line-height: 24px;
|
||||||
|
background: linear-gradient(45deg, var(--main-color-light) 10.79%, var(--main-color) 87.08%);
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.received {
|
||||||
|
color: initial;
|
||||||
|
padding-top: 16px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
text-align: left;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.message-text {
|
||||||
|
max-width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.message-md {
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.message-image {
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 50vh;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
max-width: calc(1100px - 2rem);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #F4F4F4;
|
||||||
|
border-radius: 2rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.user-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0 0.6rem;
|
||||||
|
color: #111111;
|
||||||
|
font-size: 16px;
|
||||||
|
font-variation-settings: 'wght' 400, 'opsz' 10.5;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-icon-only {
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 2.5rem;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 3rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #D7D7D7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.note {
|
||||||
|
width: 100%;
|
||||||
|
font-size: small;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0rem;
|
||||||
|
color: #ccc;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
</style>
|
||||||
169
web/src/layouts/AppLayout.vue
Normal file
169
web/src/layouts/AppLayout.vue
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<script setup>
|
||||||
|
import { KeepAlive } from 'vue'
|
||||||
|
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||||
|
import {
|
||||||
|
MessageOutlined,
|
||||||
|
MessageFilled,
|
||||||
|
SettingOutlined,
|
||||||
|
SettingFilled,
|
||||||
|
BookOutlined,
|
||||||
|
BookFilled
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
// 打印当前页面的路由信息,使用 vue3 的 setup composition API
|
||||||
|
const route = useRoute()
|
||||||
|
console.log(route)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<router-link to="/"><img src="/jnu.png"> </router-link>
|
||||||
|
</div>
|
||||||
|
<div class="nav">
|
||||||
|
<RouterLink to="/chat" class="nav-item" active-class="active">
|
||||||
|
<component :is="route.path === '/chat' ? MessageFilled : MessageOutlined" />
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink to="/knowledge" class="nav-item" active-class="active">
|
||||||
|
<component :is="route.path === '/knowledge' ? BookFilled : BookOutlined" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div class="fill" style="flex-grow: 1;"></div>
|
||||||
|
<RouterLink to="/setting" class="setting" active-class="active">
|
||||||
|
<component :is="route.path === '/setting' ? SettingFilled : SettingOutlined" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<router-view v-slot="{ Component }" id="app-router-view">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import '@/assets/main.css';
|
||||||
|
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
min-width: var(--min-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header, #app-router-view {
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-router-view {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 80px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #F9F9F9;
|
||||||
|
height: 100%;
|
||||||
|
width: 80px;
|
||||||
|
|
||||||
|
& .logo {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
height: 45px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav .nav-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #222;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
margin: 0 10px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--main-color);
|
||||||
|
background-color: #e2eef3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e2eef3;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .setting {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-layout div.header {
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-layout .nav {
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
web/src/layouts/BlankLayout.vue
Normal file
9
web/src/layouts/BlankLayout.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomeView from '../views/HomeView.vue'
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
|
import BlankLayout from '@/layouts/BlankLayout.vue';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@ -7,21 +8,59 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView,
|
component: BlankLayout,
|
||||||
meta: { keepAlive: true }
|
children: [ {
|
||||||
|
path: '',
|
||||||
|
name: 'home',
|
||||||
|
component: import('../views/HomeView.vue'),
|
||||||
|
meta: { keepAlive: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
component: () => import('../views/ChatView.vue'),
|
component: AppLayout,
|
||||||
meta: { keepAlive: true }
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'Chat',
|
||||||
|
component: import('../views/ChatView.vue'),
|
||||||
|
meta: { keepAlive: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: '/knowledge',
|
path: '/knowledge',
|
||||||
// name: 'knowledge',
|
name: 'knowledge',
|
||||||
// component: () => import('../views/KnowledgeView.vue'),
|
component: AppLayout,
|
||||||
// meta: { keepAlive: true }
|
children: [
|
||||||
// },
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'Knowledge',
|
||||||
|
component: import('../views/EmptyView.vue'),
|
||||||
|
meta: { keepAlive: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting',
|
||||||
|
name: 'setting',
|
||||||
|
component: AppLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'setting',
|
||||||
|
component: import('../views/EmptyView.vue'),
|
||||||
|
meta: { keepAlive: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('../views/NotFoundView.vue')
|
||||||
|
}
|
||||||
// {
|
// {
|
||||||
// path: '/kg',
|
// path: '/kg',
|
||||||
// name: 'knowledge-graph',
|
// name: 'knowledge-graph',
|
||||||
|
|||||||
@ -1,411 +1,200 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat">
|
<div v-if="state.isSidebarOpen" class="conversations">
|
||||||
<div ref="chatBox" class="chat-box">
|
<div class="actions">
|
||||||
<div
|
<div class="action new" @click="addNewConv"><FormOutlined /></div>
|
||||||
v-for="message in state.messages"
|
<div class="action close" @click="state.isSidebarOpen = false"><MenuOutlined /></div>
|
||||||
:key="message.id"
|
|
||||||
class="message-box"
|
|
||||||
:class="message.role"
|
|
||||||
>
|
|
||||||
<img v-if="message.filetype === 'image'" :src="message.url" class="message-image" alt="">
|
|
||||||
<p v-else style="white-space: pre-line" class="message-text">{{ message.text }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="input-box">
|
<div class="conversation"
|
||||||
<a-button size="large" @click="clearChat">
|
v-for="(state, index) in convs"
|
||||||
<template #icon> <ClearOutlined /> </template>
|
:key="index"
|
||||||
</a-button>
|
:class="{ active: curConvId === index }"
|
||||||
<a-input
|
@click="goToConversation(index)">
|
||||||
type="text"
|
<div class="conversation__title">{{ state.title }}</div>
|
||||||
class="user-input"
|
<div class="conversation__delete" @click.prevent="delConv(index)"><DeleteOutlined /></div>
|
||||||
v-model:value="state.inputText"
|
|
||||||
@keydown.enter="sendMessage"
|
|
||||||
placeholder="输入问题……"
|
|
||||||
/>
|
|
||||||
<a-button size="large" @click="sendMessage" :disabled="!state.inputText">
|
|
||||||
<template #icon> <SendOutlined /> </template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="info">
|
<ChatComponent
|
||||||
<h1>{{ info.title }}</h1>
|
:conv="convs[curConvId]"
|
||||||
|
:state="state"
|
||||||
<p v-if="info.docs.length > 0" class="docs">{{ info.docs }}</p>
|
@rename-title="renameTitle"
|
||||||
|
@newconv="addNewConv"/>
|
||||||
<img v-if="info.image && typeof info.image === 'string'" :src="info.image" class="info-image" alt="">
|
|
||||||
<div v-else-if="info.image && Array.isArray(info.image)">
|
|
||||||
<img v-for="(img, index) in info.image" :key="index" :src="img" class="info-image" alt="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-show="info.graph?.nodes?.length > 0"><b>关联图谱</b></p>
|
|
||||||
<div id="lite_graph" v-show="info.graph?.nodes?.length > 0"></div>
|
|
||||||
<a-collapse v-model:activeKey="state.activeKey" v-if="info.docs.length > 0" accordion>
|
|
||||||
<a-collapse-panel
|
|
||||||
v-for="(doc, index) in info.docs"
|
|
||||||
:key="index"
|
|
||||||
:header="'来源' + (index + 1) + ':' + doc.fileName"
|
|
||||||
>
|
|
||||||
<p>{{ doc.text }}</p>
|
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
<p class="description" v-else-if="info.description && typeof info.description === 'string'">{{ info.description }}</p>
|
|
||||||
<div v-else-if="info.description && Array.isArray(info.description)">
|
|
||||||
<p class="description" v-for="(desc, index) in info.description" :key="index">{{ desc }}</p>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="note">即便强如雅典娜也可能会出错,请注意辨别内容的可靠性</p>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// import * as echarts from 'echarts';
|
import { reactive, ref, watch, onMounted } from 'vue'
|
||||||
import { reactive, ref, onMounted } from 'vue'
|
import { FormOutlined, MenuOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
import { SendOutlined, ClearOutlined } from '@ant-design/icons-vue'
|
import ChatComponent from '@/components/ChatComponent.vue'
|
||||||
|
|
||||||
|
const convs = reactive(JSON.parse(localStorage.getItem('chat-convs')) || [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: '新对话',
|
||||||
|
history: [],
|
||||||
|
messages: [],
|
||||||
|
inputText: ''
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
// let myChart = null;
|
|
||||||
const chatBox = ref(null)
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
history: [],
|
isSidebarOpen: true,
|
||||||
messages: [],
|
|
||||||
activeKey: [],
|
|
||||||
inputText: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const default_info = {
|
const curConvId = ref(0)
|
||||||
title: '你好,我是 Project: Athena',
|
|
||||||
description: [
|
|
||||||
'基于特定领域知识图谱的问答系统,支持多轮对话,支持外部信息检索,你可以:',
|
|
||||||
'1. 图谱问答:输入问题,获取相关的答案',
|
|
||||||
'2. 多轮筛选:在对话页面,可以通过多轮对话筛选来缩小搜索范围。例如,可以根据实体、具体类别、类型等进行筛选,以快速找到所需的专业知识。',
|
|
||||||
'3. 知识图谱可视化:在知识图谱页面,用户可以通过可视化界面直观地了解实体之间的关系。可以缩放、平移和旋转图谱以查看不同层次的关系,还可以点击实体节点查看更多详细信息。',
|
|
||||||
'4. 实体相关信息查看:可以通过右侧知识图谱下方的相关信息查看实体所有出现的地方,帮助全面查询理解,更清晰全面。',
|
|
||||||
],
|
|
||||||
image: [],
|
|
||||||
graph: null,
|
|
||||||
docs: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = reactive({
|
const generateRandomHash = (length) => {
|
||||||
...default_info
|
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
})
|
let hash = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
const scrollToBottom = () => {
|
hash += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
setTimeout(() => {
|
|
||||||
chatBox.value.scrollTop = chatBox.value.scrollHeight - chatBox.value.clientHeight
|
|
||||||
}, 10) // 10ms 后滚动到底部
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} message 消息内容
|
|
||||||
* @param {*} role 消息
|
|
||||||
*/
|
|
||||||
const appendMessage = (message, role) => {
|
|
||||||
state.messages.push({
|
|
||||||
id: state.messages.length + 1,
|
|
||||||
role,
|
|
||||||
text: message
|
|
||||||
})
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// const appendPicMessage = (pic, type) => {
|
|
||||||
// state.messages.push({
|
|
||||||
// id: state.messages.length + 1,
|
|
||||||
// type,
|
|
||||||
// filetype: "image",
|
|
||||||
// url: pic
|
|
||||||
// })
|
|
||||||
// scrollToBottom()
|
|
||||||
// }
|
|
||||||
|
|
||||||
const updateLastReceivedMessage = (message, id) => {
|
|
||||||
const lastReceivedMessage = state.messages.find((message) => message.id === id)
|
|
||||||
if (lastReceivedMessage) {
|
|
||||||
lastReceivedMessage.text = message
|
|
||||||
} else {
|
|
||||||
state.messages.push({
|
|
||||||
id,
|
|
||||||
type: 'received',
|
|
||||||
text: message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendMessage = () => {
|
|
||||||
if (state.inputText.trim()) {
|
|
||||||
appendMessage(state.inputText, 'sent')
|
|
||||||
appendMessage('检索中……', 'received')
|
|
||||||
const user_input = state.inputText
|
|
||||||
const cur_res_id = state.messages[state.messages.length - 1].id
|
|
||||||
state.inputText = ''
|
|
||||||
fetch('/api/chat', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
query: user_input,
|
|
||||||
history: state.history,
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then((response) => {const reader = response.body.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
let buffer = ''
|
|
||||||
// 逐步读取响应文本
|
|
||||||
const readChunk = () => {
|
|
||||||
return reader.read().then(({ done, value }) => {
|
|
||||||
if (done) {
|
|
||||||
console.log('Finished')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
|
||||||
console.log(buffer)
|
|
||||||
const message = buffer.trim().split('\n').pop()
|
|
||||||
// 尝试解析 message
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(message)
|
|
||||||
updateLastReceivedMessage(data.response, cur_res_id)
|
|
||||||
state.history = data.history
|
|
||||||
buffer = ''
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return readChunk()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return readChunk()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('Please enter a message')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFormattedDocs = (docs) => {
|
|
||||||
const formattedDocs = docs.map((doc) => {
|
|
||||||
var match = /出处 \[(\d+)\] \[([^\]]*)\]\(([^\)]*)\) \n\n(.*)/g.exec(doc);
|
|
||||||
return {
|
|
||||||
index: match[1],
|
|
||||||
fileName: match[2],
|
|
||||||
link: match[3],
|
|
||||||
text: match[4].trim()
|
|
||||||
}
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameTitle = (newTitle) => {
|
||||||
|
convs[curConvId.value].title = newTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToConversation = (index) => {
|
||||||
|
curConvId.value = index
|
||||||
|
console.log(convs[curConvId.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewConv = () => {
|
||||||
|
curConvId.value = 0
|
||||||
|
if (convs.length > 0 && convs[0].messages.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
convs.unshift({
|
||||||
|
id: generateRandomHash(8),
|
||||||
|
title: `新对话`,
|
||||||
|
history: [],
|
||||||
|
messages: [],
|
||||||
|
inputText: ''
|
||||||
})
|
})
|
||||||
console.log(formattedDocs)
|
|
||||||
return formattedDocs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendDeafultMessage = () => {
|
const delConv = (index) => {
|
||||||
appendMessage('你好?我是 Project: Athena,有什么可以帮你?😊', 'received')
|
convs.splice(index, 1)
|
||||||
|
if (index === curConvId.value) {
|
||||||
|
curConvId.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convs.length === 0) {
|
||||||
|
addNewConv()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearChat = () => {
|
// Watch convs and save to localStorage
|
||||||
state.messages = []
|
watch(
|
||||||
state.history = []
|
() => convs,
|
||||||
info.title = default_info.title
|
(newStates) => {
|
||||||
info.description = default_info.description
|
localStorage.setItem('chat-convs', JSON.stringify(newStates))
|
||||||
info.image = default_info.image
|
},
|
||||||
info.graph = default_info.graph
|
{ deep: true }
|
||||||
info.sents = default_info.sents
|
)
|
||||||
info.docs = default_info.docs
|
|
||||||
sendDeafultMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Load convs from localStorage on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
sendDeafultMessage()
|
const savedSonvs = JSON.parse(localStorage.getItem('chat-convs'))
|
||||||
|
if (savedSonvs) {
|
||||||
|
for (let i = 0; i < savedSonvs.length; i++) {
|
||||||
|
convs[i] = savedSonvs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@import '@/assets/main.css';
|
||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.chat, div.info {
|
|
||||||
height: calc(100vh - 180px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1100px;
|
height: 100%;
|
||||||
flex-grow: 1;
|
position: relative;
|
||||||
margin: 0 auto;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 0.6px 2.3px rgba(0, 0, 0, 0.1), 0px 1px 5px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-box {
|
.chat-container .conversations {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
overflow-y: auto;
|
}
|
||||||
padding: 1rem;
|
|
||||||
|
.conversations {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: 100px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
min-width: var(--min-sider-width);
|
||||||
|
max-width: 200px;
|
||||||
|
|
||||||
// 平滑滚动
|
& .actions {
|
||||||
scroll-behavior: smooth;
|
height: var(--header-height);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 9;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
.action {
|
||||||
width: 0rem;
|
font-size: 1.2rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #6D6D6D;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ECECEC;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-box {
|
.conversation {
|
||||||
width: fit-content;
|
|
||||||
max-width: 80%;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
padding: 10px 16px;
|
|
||||||
user-select: text;
|
|
||||||
word-break: break-word;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
font-variation-settings: 'wght' 400, 'opsz' 10.5;
|
|
||||||
font-weight: 400;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16);
|
|
||||||
animation: slideInUp 0.1s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-box.sent {
|
|
||||||
color: white;
|
|
||||||
background-color: #efefef;
|
|
||||||
// background: linear-gradient(90deg, #006880 10.79%, #005366 87.08%);
|
|
||||||
background: linear-gradient(90deg, #40788c 10.79%, #005f77 87.08%);
|
|
||||||
// background-color: #333;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-box.received {
|
|
||||||
color: #111111;
|
|
||||||
background-color: #ffffff;
|
|
||||||
text-align: left;
|
|
||||||
// animation-delay: 0.2s; /* 延迟 100ms 开始动画 */
|
|
||||||
}
|
|
||||||
|
|
||||||
p.message-text {
|
|
||||||
word-wrap: break-word;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.message-image {
|
|
||||||
max-width: 300px;
|
|
||||||
max-height: 50vh;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-box {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 16px;
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.user-input {
|
|
||||||
flex: 1;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin: 0 0.6rem;
|
|
||||||
color: #111111;
|
|
||||||
font-size: 16px;
|
|
||||||
// line-height: 22px;
|
|
||||||
font-variation-settings: 'wght' 400, 'opsz' 10.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-btn-icon-only {
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
width: 100%;
|
||||||
|
|
||||||
// button:disabled {
|
&__title {
|
||||||
// // background: #ccc;
|
white-space: nowrap; /* 禁止换行 */
|
||||||
// cursor: not-allowed;
|
overflow: hidden; /* 超出部分隐藏 */
|
||||||
// }
|
text-overflow: ellipsis; /* 显示省略号 */
|
||||||
|
|
||||||
div.info {
|
|
||||||
width: 400px;
|
|
||||||
min-width: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
// 平滑滚动
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0rem;
|
|
||||||
}
|
|
||||||
// background-color: #ccc;
|
|
||||||
// margin: 0 1rem;
|
|
||||||
|
|
||||||
& > h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
// padding: 0.5rem;
|
|
||||||
// text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.description {
|
&__delete {
|
||||||
font-size: 1rem;
|
display: none;
|
||||||
margin: 0;
|
color: #7D7D7D;
|
||||||
// padding: 0.5rem;
|
|
||||||
// max-height: 10rem;
|
&:hover {
|
||||||
margin-bottom: 20px;
|
color: #F93A37;
|
||||||
// text-align: center;
|
background-color: #EEE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
&.active {
|
||||||
width: 100%;
|
background-color: #f0f0f0;
|
||||||
height: fit-content;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#lite_graph {
|
&:hover {
|
||||||
width: 400px;
|
background-color: #f0f0f0;
|
||||||
height: 300px;
|
|
||||||
background: #f5f5f5;
|
& .conversation__delete {
|
||||||
// border: 4px solid #ccc;
|
display: block;
|
||||||
border-radius: 8px;
|
}
|
||||||
margin-bottom: 1rem;
|
|
||||||
box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 0.6px 2.3px rgba(0, 0, 0, 0.1),
|
|
||||||
0px 1px 5px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.note {
|
|
||||||
font-size: small;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
31
web/src/views/EmptyView.vue
Normal file
31
web/src/views/EmptyView.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-found">
|
||||||
|
<h1>404 - 页面还没做</h1>
|
||||||
|
<p>Sorry, Yemian has not been zuoed.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 80vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -1,17 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
|
<header>江南大学人工智能与计算机学院</header>
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
<p>{{ description }}</p>
|
<button class="home-btn" @click="goToChat">开始对话</button>
|
||||||
<img src="/home.png" alt="Placeholder Image" />
|
<img src="/home.png" alt="Placeholder Image" />
|
||||||
|
<footer>© 江南语析 2024</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const title = ref('Athena ✨')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goToChat = () => {
|
||||||
|
router.push("/chat")
|
||||||
|
}
|
||||||
|
|
||||||
const title = ref('Project: Athena ✨')
|
|
||||||
const description = ref('代号:雅典娜!')
|
|
||||||
const image = ref('/home.png')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -24,17 +31,24 @@ const image = ref('/home.png')
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-color: var(--main-color);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: aliceblue;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0;
|
margin-top: calc(20vh - 80px);
|
||||||
margin-top: calc(10vh - 80px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: calc(15vh - 80px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@ -43,5 +57,25 @@ img {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.05);
|
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.home-btn {
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
background-color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-bottom: calc(15vh - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
43
web/src/views/NotFoundView.vue
Normal file
43
web/src/views/NotFoundView.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-found">
|
||||||
|
<h1>404 - Page Not Found</h1>
|
||||||
|
<p>Sorry, the page you are looking for does not exist.</p>
|
||||||
|
<router-link to="/chat">Go back</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NotFoundView'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found a {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -15,7 +15,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'^/api': {
|
'^/api': {
|
||||||
target: 'http://127.0.0.1:8000',
|
target: 'http://127.0.0.1:5000', // 5000端口是flask的Debug模式默认端口, 8000是非Debug模式默认端口
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user