Browse Source

all

yangrongze
hanyuqing 3 months ago
parent
commit
20de262a2c
  1. 10
      controller/BuilderController.py
  2. 2
      controller/LoginController.py
  3. 16
      controller/QAController.py
  4. 2
      controller/__init__.py
  5. 3
      vue/src/App.vue
  6. BIN
      vue/src/assets/upload.png
  7. BIN
      vue/src/assets/放大.png
  8. 57
      vue/src/components/Menu.vue
  9. 133
      vue/src/system/GraphBuilder.vue
  10. 4
      vue/vue.config.js

10
controller/BuilderController.py

@ -1,9 +1,11 @@
# 全局 client(可复用) # 全局 client(可复用)
import traceback
import httpx import httpx
from robyn import jsonify, Response from robyn import jsonify, Response
from app import app from app import app
from controller import client from controller.client import client
@app.post("/api/analyze") @app.post("/api/analyze")
@ -33,4 +35,8 @@ async def analyze(request):
"detail": resp.text "detail": resp.text
}), resp.status_code }), resp.status_code
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 error_trace = traceback.format_exc()
print("❌ 发生异常:")
print(error_trace)
return jsonify({"error": str(e), "traceback": error_trace}), 500

2
controller/LoginController.py

@ -1,3 +1,5 @@
import os
from robyn import jsonify, Response, Request from robyn import jsonify, Response, Request
from app import app from app import app
from datetime import datetime, timedelta from datetime import datetime, timedelta

16
controller/QAController.py

@ -28,16 +28,26 @@ async def analyze(request):
resp_json_data = json.loads(resp_json_data) resp_json_data = json.loads(resp_json_data)
entities = resp_json_data.get("entities", []) entities = resp_json_data.get("entities", [])
print(entities) print(entities)
data = []
for name in entities: for name in entities:
Neo4jUtil.find_neighbors_with_relationships( neighbors =neo4j_client.find_neighbors_with_relationshipsAI(
node_label=None, node_label=None,
direction="both", direction="both",
node_properties={"name": name}, node_properties={"name": name},
rel_type=None
) )
data.append({
name:neighbors
})
resp = await client.post(
"/question_agent",
json={"neo4j_data": [],
"text": input_text},
timeout=1800.0 # 30分钟
)
return Response( return Response(
status_code=200, status_code=200,
description=jsonify(entities), description=jsonify(resp.json()),
headers={"Content-Type": "text/plain; charset=utf-8"} headers={"Content-Type": "text/plain; charset=utf-8"}
) )
else: else:

2
controller/__init__.py

@ -5,7 +5,7 @@ from .BuilderController import *
from .GraphController import * from .GraphController import *
from .LoginController import * from .LoginController import *
from .QAController import * from .QAController import *
from .RegisterController import *
# 可选:如果控制器里定义了 blueprint,也可以在这里统一导出 # 可选:如果控制器里定义了 blueprint,也可以在这里统一导出
# from .BuilderController import builder_bp # from .BuilderController import builder_bp
# from .GraphController import graph_bp # from .GraphController import graph_bp

3
vue/src/App.vue

@ -14,6 +14,7 @@ export default {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif; //font-family: 'Noto Serif SC', "SimSun", "", serif;
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
} }
</style> </style>

BIN
vue/src/assets/upload.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
vue/src/assets/放大.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

57
vue/src/components/Menu.vue

@ -45,9 +45,14 @@
'border-top': isCollapsed ? 'none' : '2px solid rgba(255, 255, 255, 0.1)' 'border-top': isCollapsed ? 'none' : '2px solid rgba(255, 255, 255, 0.1)'
}"> }">
<div v-if="!isCollapsed" class="user-block"> <div v-if="!isCollapsed" class="user-block">
<div class="avatar" @click="handleProfile">用户</div> <img
:src="userProfile.avatar"
alt="用户头像"
class="avatar"
@click="handleProfile"
>
<div class="info"> <div class="info">
<div class="name" @click="handleProfile">用户名字</div> <div class="name" @click="handleProfile">{{ userProfile.username }}</div>
<div class="id">8866990099</div> <div class="id">8866990099</div>
</div> </div>
<div class="exit-wrap"> <div class="exit-wrap">
@ -65,12 +70,13 @@
</template> </template>
<script setup> <script setup>
import {ref} from 'vue'; import {onMounted, ref} from 'vue';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import i1 from '@/assets/图标1.png'; import i1 from '@/assets/图标1.png';
import i2 from '@/assets/图标2.png'; import i2 from '@/assets/图标2.png';
import i3 from '@/assets/图标3.png'; import i3 from '@/assets/图标3.png';
import i4 from '@/assets/图标4.png'; import i4 from '@/assets/图标4.png';
import {getUserProfile} from "@/api/profile";
const router = useRouter(); const router = useRouter();
// const activeIndex = ref(0); // const activeIndex = ref(0);
@ -86,7 +92,10 @@ const props = defineProps({
// //
const emit = defineEmits(['menu-click']); const emit = defineEmits(['menu-click']);
const userProfile = ref({
username: '用户',
avatar: '/resource/avatar/4.png'
});
// //
const activeIndex = ref(props.initialActive); const activeIndex = ref(props.initialActive);
@ -110,9 +119,46 @@ const handleProfile = () => {
router.push('/profile'); router.push('/profile');
}; };
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('messages');
// 使Vue Router // 使Vue Router
router.push('/login'); router.push('/login');
}; };
onMounted(async () => {
try {
// localStoragetoken
const token = localStorage.getItem('token');
console.log('Profile组件挂载,获取到的token:', token);
if (token) {
// API
const response = await getUserProfile(token);
if (response.success) {
//
//
let avatarUrl = response.user.avatar || '/resource/avatar/4.png';
if (avatarUrl.startsWith('/resource/')) {
avatarUrl = avatarUrl; // 使
}
console.log('设置头像URL:', avatarUrl);
userProfile.value = {
username: response.user.username,
avatar: avatarUrl
};
} else {
// tokentoken
if (response.message && response.message.includes('登录')) {
localStorage.removeItem('token');
}
}
} else {
console.log('用户未登录');
errorMessage.value = '用户未登录,请先登录';
}
} catch (error) {
console.error('获取用户信息失败:', error);
errorMessage.value = '获取用户信息时发生错误';
}
});
</script> </script>
<style scoped> <style scoped>
@ -191,7 +237,6 @@ const handleLogout = () => {
line-height: 1.25; line-height: 1.25;
white-space: nowrap; white-space: nowrap;
font-weight: 600; font-weight: 600;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
} }
/* --- 菜单导航 --- */ /* --- 菜单导航 --- */
@ -268,7 +313,6 @@ const handleLogout = () => {
margin-left: 10px; margin-left: 10px;
font-weight: 500; font-weight: 500;
font-size: 12px; font-size: 12px;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
} }
.active-tag { .active-tag {
@ -327,7 +371,6 @@ const handleLogout = () => {
font-size: 11px; font-size: 11px;
color: #fff; color: #fff;
line-height: 1; line-height: 1;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
cursor: pointer; cursor: pointer;
} }

133
vue/src/system/GraphBuilder.vue

@ -17,10 +17,8 @@
</div> </div>
<div v-else-if="msg.role === 'assistant'"> <div v-else-if="msg.role === 'assistant'">
<div v-if="msg.isKG" class="kg-card"> <div v-if="msg.isKG" class="kg-card">
<h4>🔬 病历结构化分析结果</h4>
<div v-if="msg.content.entities?.length" class="kg-section"> <div v-if="msg.content.entities?.length" class="kg-section">
<h5>识别出的实体</h5> <h5 style="text-align: left">识别出的实体</h5>
<div class="entity-list"> <div class="entity-list">
<span <span
v-for="(ent, i) in msg.content.entities" v-for="(ent, i) in msg.content.entities"
@ -34,7 +32,7 @@
</div> </div>
<div v-if="msg.content.relations?.length" class="kg-section"> <div v-if="msg.content.relations?.length" class="kg-section">
<h5>识别出的关系</h5> <h5 style="text-align: left">识别出的关系</h5>
<ul class="relation-list"> <ul class="relation-list">
<li v-for="(rel, i) in msg.content.relations" :key="i"> <li v-for="(rel, i) in msg.content.relations" :key="i">
<span class="rel-subject">{{ rel.e1 }}</span> <span class="rel-subject">{{ rel.e1 }}</span>
@ -58,36 +56,23 @@
<div class="input-area"> <div class="input-area">
<textarea <textarea
v-model="inputText" v-model="inputText"
placeholder="向千问提问" placeholder="有什么可以帮助您?"
class="input-box" class="input-box"
@keyup.enter.exact.prevent="sendMessage" @keyup.enter.exact.prevent="sendMessage"
rows="1" rows="1"
></textarea> ></textarea>
<div class="action-buttons">
<button class="btn" @click="insertPrefix('深度思考:')">
<span></span> 深度思考
</button>
<button class="btn" @click="insertPrefix('深度研究:')">
<span>🔍</span> 深度研究
</button>
<button class="btn" @click="insertPrefix('```代码\n')">
<span><></span> 代码
</button>
<button class="btn" @click="insertPrefix('生成一张图片:')">
<span>🖼</span> 图像
</button>
<button class="btn" @click="insertPrefix('翻译成英文:')">
<span>🌐</span> 翻译
</button>
<button class="btn more-btn">...</button>
</div>
<img
src="../assets/upload.png"
alt="用户头像"
class="avatar"
style="border-radius: 50%;width: 30px;box-shadow: 0 2px 8px rgba(0,0,0,0.05); "
>
<div class="icon-group"> <div class="icon-group">
<span class="icon" title="切换语言">🌍</span>
<span class="icon" title="编辑模式"></span>
<button class="send-btn" @click="sendMessage" title="发送"> <button class="send-btn" @click="sendMessage" title="发送">
<span>📤</span> 发送
</button> </button>
</div> </div>
</div> </div>
@ -140,16 +125,18 @@ export default {
} }
// this.pollAIStatus() // this.pollAIStatus()
// //
this.messages.push({ role: 'user', content: text }); let message={ role: 'user', content: text }
this.messages.push(message);
this.inputText = ''; this.inputText = '';
// analyze(data).then(res=>{ analyze(data).then(res=>{
// console.log(res) console.log(res)
// this.messages.push({ role: 'assistant', let message={ role: 'assistant',
// content: res,entities:res.entities,relations:res.relations,isKG:true }); content: res,entities:res.entities,relations:res.relations,isKG:true }
// }) this.messages.push(message);
qaAnalyze(data).then(res=>{
}) })
// qaAnalyze(data).then(res=>{
//
// })
}, },
insertPrefix(prefix) { insertPrefix(prefix) {
this.inputText += prefix; this.inputText += prefix;
@ -166,6 +153,23 @@ export default {
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
}); });
},
saveMessage(message) {
const messages = JSON.parse(localStorage.getItem('messages')) || [];
messages.push(message); //
localStorage.setItem('messages', JSON.stringify(messages));
},
loadMessages() {
const savedMessages = JSON.parse(localStorage.getItem('messages')) || [];
this.messages = savedMessages; //
},
handleBeforeUnload() {
localStorage.removeItem('messages');
//
console.log("页面刷新或关闭前,保存消息...");
this.messages.forEach((message) => {
this.saveMessage(message);
});
} }
}, },
watch: { watch: {
@ -173,9 +177,13 @@ export default {
this.scrollToBottom(); this.scrollToBottom();
} }
}, },
mounted() { mounted() {
this.loadMessages();
this.scrollToBottom(); this.scrollToBottom();
} window.addEventListener('beforeunload', this.handleBeforeUnload);
},
}; };
</script> </script>
@ -189,6 +197,7 @@ export default {
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #F3F3F3;
} }
/* === 消息区域 === */ /* === 消息区域 === */
@ -196,7 +205,6 @@ export default {
flex: 1; flex: 1;
padding: 16px; padding: 16px;
overflow-y: auto; overflow-y: auto;
background-color: #f9f9f9;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -220,33 +228,36 @@ export default {
max-width: 75%; max-width: 75%;
word-break: break-word; word-break: break-word;
line-height: 1.5; line-height: 1.5;
font-size: 14px; font-size: 12px;
text-align: left; text-align: left;
} }
.message.user .bubble { .message.user .bubble {
background-color: #007bff; background-color: #155DFF;
color: white; color: white;
border-bottom-right-radius: 4px;
} }
.message.assistant .bubble { .message.assistant .bubble {
background-color: #e9ecef; background-color: #fff;
color: #333; color: #333;
border-bottom-left-radius: 4px;
} }
/* === 输入区域 === */ /* === 输入区域 === */
.input-area { .input-area {
padding: 16px; padding: 16px;
background: white; background: white;
border-top: 1px solid #eee; border-top-left-radius: 24px;
border-top-right-radius: 24px;
box-shadow: 2px -1px 14px 0px rgb(159 160 161 / 22%);
}
.input-box::placeholder {
color: #AFAFAF; /* 设置字体颜色为红色 */
font-weight: 500;
} }
.input-box { .input-box {
width: 100%; width: 100%;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid #d9d9d9; border: none;
border-radius: 12px; border-radius: 12px;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
@ -264,22 +275,23 @@ export default {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin: 12px 0; margin: 12px 0;
overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding: 0 4px; padding: 0 4px;
} }
.btn { .btn {
padding: 6px 12px; display: flex; /* 使用flex布局 */
background: #f5f5f5; justify-content: center; /* 水平居中 */
border: 1px solid #ddd; align-items: center; /* 垂直居中 */
border-radius: 12px; width: 20px; /* 设置按钮的宽度为20px */
font-size: 12px; height: 20px; /* 设置按钮的高度为20px */
color: #333; border-radius: 50%; /* 圆形按钮 */
cursor: pointer; background-color: white; /* 背景色为白色 */
display: flex; font-size: 18px; /* 设置加号的字体大小 */
align-items: center; line-height: 20px; /* 设置行高,确保加号垂直居中 */
gap: 4px; color: #333; /* 加号的颜色 */
font-weight: bold; /* 加号字体加粗 */
cursor: pointer; /* 鼠标悬停时显示为点击效果 */
} }
.btn:hover { .btn:hover {
@ -317,21 +329,21 @@ export default {
} }
.send-btn { .send-btn {
background: #9b68ff; background: #B9CDFF;
border: none; border: none;
width: 32px; width: 50px;
height: 32px; height: 20px;
border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: white; color: white;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
font-size: 11px;
} }
.send-btn:hover { .send-btn:hover {
background: #8a53e0; background: #155DFF;
} }
/* === 自定义助手 KG 卡片 === */ /* === 自定义助手 KG 卡片 === */
.kg-card { .kg-card {
@ -428,6 +440,5 @@ export default {
.assistant-text { .assistant-text {
background-color: #e9ecef; background-color: #e9ecef;
color: #333; color: #333;
border-bottom-left-radius: 4px;
} }
</style> </style>

4
vue/vue.config.js

@ -15,6 +15,10 @@ module.exports = defineConfig({
target: 'http://localhost:8088', target: 'http://localhost:8088',
changeOrigin: true, changeOrigin: true,
// 不需要 pathRewrite,因为前后都是 /api/xxx // 不需要 pathRewrite,因为前后都是 /api/xxx
},
'/resource': {
target: 'http://localhost:8088', // 更新为8088端口
changeOrigin: true
} }
}, },
historyApiFallback: { historyApiFallback: {

Loading…
Cancel
Save