You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
908 lines
26 KiB
908 lines
26 KiB
<template>
|
|
<div style="display: flex;
|
|
height: 100vh;">
|
|
<Menu
|
|
:initial-active="1"
|
|
/>
|
|
<div class="qwen-chat">
|
|
|
|
<!-- 聊天消息区域 -->
|
|
<div ref="messagesContainer" class="chat-messages">
|
|
<div
|
|
v-for="(msg, index) in messages"
|
|
:key="index"
|
|
:class="['message', msg.role]"
|
|
>
|
|
<div class="time">{{ msg.time }}</div>
|
|
<div class="word" v-if="msg.role === 'user'&& msg.hasFile" style="margin-bottom: 6px">
|
|
<img src="../assets/word.svg" style="width: 28px;margin-right: 5px; flex-shrink: 0; flex-grow: 0;">
|
|
<div style="flex: 1; overflow: auto;">
|
|
<div class="wordName" style="text-align: left;margin-bottom: 1px;font-size: 11px;line-height: 14px;">{{msg.file.name}}</div>
|
|
<div style=" align-items: center;height: 18px;font-size: 10px; line-height: 12px;display: flex;">{{msg.file.size}}</div>
|
|
</div>
|
|
<!-- <img-->
|
|
<!-- src="../assets/close.svg"-->
|
|
<!-- alt="close"-->
|
|
<!-- class="close-btn"-->
|
|
<!-- @click="removeFile"-->
|
|
<!-- />-->
|
|
|
|
</div>
|
|
<div v-if="msg.role === 'user'" style="display: flex;align-items: flex-start; justify-content: flex-end;">
|
|
<div class="bubble" v-if="msg.content">
|
|
{{ msg.content }}
|
|
</div>
|
|
|
|
</div>
|
|
<div v-else-if="msg.role === 'assistant'" style=" display: flex;align-items: flex-start; justify-content: flex-start;">
|
|
<img src="../assets/gpt.png" style="width: 45px;height: 45px;margin-right: 15px">
|
|
<div v-if="msg.isKG" class="kg-card">
|
|
<div v-if="msg.entities?.length" class="kg-section">
|
|
<h5 style="text-align: left">识别出的实体</h5>
|
|
<div class="entity-list">
|
|
<div
|
|
v-for="(ent, i) in msg.entities"
|
|
:key="i"
|
|
class="entity-item"
|
|
style="display: flex;margin-bottom: 10px"
|
|
>
|
|
<!-- 复选框放在外面 -->
|
|
<input
|
|
type="checkbox"
|
|
:id="'entity-' + i"
|
|
:value="ent"
|
|
v-model="ent.selected"
|
|
@change="handleEntitySelectionChange(msg,ent)"
|
|
/>
|
|
<!-- 使用 for 关联到 input 的 id -->
|
|
<label
|
|
:for="'entity-' + i"
|
|
class="entity-tag"
|
|
:class="'tag-' + ent.t"
|
|
>
|
|
{{ ent.n }}<small>({{ ent.t }})</small>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="msg.relations?.length" class="kg-section">
|
|
<h5 style="text-align: left">识别出的关系</h5>
|
|
<ul class="relation-list">
|
|
<li v-for="(rel, i) in msg.relations" :key="i" class="relation-item">
|
|
<!-- 复选框 -->
|
|
<input
|
|
type="checkbox"
|
|
:id="'rel-' + i"
|
|
:value="rel"
|
|
v-model="rel.selected"
|
|
:disabled="!isRelationEnabled(msg,rel)"
|
|
/>
|
|
<!-- 标签区域(可点击) -->
|
|
<label :for="'rel-' + i" class="relation-label">
|
|
<span class="rel-subject">{{ rel.e1 }}</span>
|
|
<span class="rel-predicate">— {{ rel.r }} →</span>
|
|
<span class="rel-object">{{ rel.e2 }}</span>
|
|
</label>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div v-if="msg.entities?.length || msg.relations?.length" style="text-align: right; display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 12px;">
|
|
<button class="send-btn" @click="build(msg)" style="border-radius: 10px">
|
|
构建
|
|
</button>
|
|
</div>
|
|
<div v-if="!msg.entities?.length && !msg.relations?.length" style="font-size: 12px;
|
|
text-align: left;" >
|
|
未提取到有效医学实体或关系。</div>
|
|
</div>
|
|
<div v-else class="bubble assistant-text">
|
|
{{ msg.content }}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div v-if="isSending" style="display: flex;margin-left: 5px;margin-bottom: 10px">
|
|
<div class="loading-indicator">
|
|
<div class="dot dot-1"></div>
|
|
<div class="dot dot-2"></div>
|
|
<div class="dot dot-3"></div>
|
|
<div class="dot dot-4"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 输入区域(Qwen 风格) -->
|
|
<div class="input-area">
|
|
<!-- 下拉图标:绝对定位在 input-area 上方居中 -->
|
|
<div class="dropdown-icon" v-if="showScrollToBottom" @click="scrollToLatest">
|
|
<img src="../assets/下拉.png" style="width: 45px" />
|
|
</div>
|
|
<div class="word" v-if="showFile">
|
|
<img src="../assets/word.png" style="width: 28px;margin-right: 5px; flex-shrink: 0; flex-grow: 0;">
|
|
<div style="flex: 1; overflow: auto;">
|
|
<div class="wordName" style="text-align: left;margin-bottom: 1px;font-size: 11px;line-height: 14px;">{{file.fileName}}</div>
|
|
<div style=" color:#a2a5a7; align-items: center;height: 18px;font-size: 10px; line-height: 12px;display: flex;">{{file.size}}</div>
|
|
</div>
|
|
<img
|
|
src="../assets/close.svg"
|
|
alt="close"
|
|
class="close-btn"
|
|
@click="removeFile"
|
|
/>
|
|
|
|
</div>
|
|
|
|
<textarea
|
|
v-model="inputText"
|
|
placeholder="有什么可以帮助您?"
|
|
class="input-box"
|
|
@keyup.enter.exact.prevent="sendMessage"
|
|
:rows="rows"
|
|
></textarea>
|
|
|
|
|
|
|
|
<div class="icon-group">
|
|
<img
|
|
|
|
src="../assets/upload.png"
|
|
alt="用户头像"
|
|
class="avatar"
|
|
@click="upload"
|
|
style="margin-left: 10px;cursor:pointer;border-radius: 50%;width: 30px;box-shadow: rgb(0 0 0 / 18%) 0px 2px 8px; "
|
|
>
|
|
<div style="display: flex;align-items: center;">
|
|
<img v-if="inputStatus"
|
|
|
|
src="../assets/放大.png"
|
|
alt="用户头像"
|
|
class="avatar"
|
|
style="margin-left: 10px;cursor:pointer;width: 15px;margin-right: 26px "
|
|
@click="adjustHeight(false)"
|
|
>
|
|
<img v-if="!inputStatus"
|
|
|
|
src="../assets/缩小.png"
|
|
alt="用户头像"
|
|
class="avatar"
|
|
style="margin-left: 10px;cursor:pointer;width: 15px;margin-right: 26px "
|
|
@click="adjustHeight(true)"
|
|
>
|
|
<button class="send-btn" @click="sendMessage" title="发送" :disabled="isSending">
|
|
发送
|
|
</button>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
style="display: none;"
|
|
accept=".docx"
|
|
@change="handleFileChange"
|
|
/>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<script>
|
|
import Menu from "@/components/Menu.vue";
|
|
import axios from "axios";
|
|
import {analyze, build, getAIStatus} from "@/api/builder";
|
|
import {qaAnalyze} from "@/api/qa";
|
|
|
|
export default {
|
|
name: 'QwenChat',
|
|
components: {Menu},
|
|
data() {
|
|
return {
|
|
inputText: '',
|
|
messages: [
|
|
{ role: 'assistant', content: '你好!我是图谱构建助手,有什么可以帮你的吗?',isKG:false,hasFile:false }
|
|
],
|
|
status:"wait",
|
|
rows:3,
|
|
inputStatus:true,
|
|
fileName:"",
|
|
file:{},
|
|
showFile:false,
|
|
selectedEntities:[],
|
|
selectedRelations:[],
|
|
showScrollToBottom: false,
|
|
isSending: false,
|
|
|
|
};
|
|
},
|
|
methods: {
|
|
handleEntitySelectionChange(msg, ent) {
|
|
// 如果该实体被取消选中
|
|
if (!ent.selected) {
|
|
// 遍历所有的关系,找到与当前实体相关的关系,将它们的 selected 设置为 false
|
|
msg.relations.forEach(rel => {
|
|
if (rel.e1 === ent.n || rel.e2 === ent.n) {
|
|
rel.selected = false;
|
|
}
|
|
});
|
|
}
|
|
console.log(`实体 "${ent.n}" 的选中状态:`, ent.selected);
|
|
|
|
// 如果需要,你可以在这里做其他处理,比如更新状态等
|
|
},
|
|
// 为 entities 或 relations 添加 selected 标志
|
|
addSelectedFlag(items, isRelation = false) {
|
|
if (!items) return [];
|
|
return items.map(item => {
|
|
if (isRelation) {
|
|
return {
|
|
...item,
|
|
selected: true // 对于关系类型,添加 selected 属性,默认值为 false
|
|
};
|
|
}
|
|
return {
|
|
...item,
|
|
selected: true // 对于实体类型,添加 selected 属性,默认值为 false
|
|
};
|
|
});
|
|
},
|
|
handleScroll() {
|
|
const container = this.$refs.messagesContainer;
|
|
const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 20;
|
|
this.showScrollToBottom = !isAtBottom;
|
|
},
|
|
scrollToBottom(behavior = 'smooth') {
|
|
this.$nextTick(() => {
|
|
const container = this.$refs.messagesContainer;
|
|
container.scrollTo({
|
|
top: container.scrollHeight,
|
|
behavior
|
|
});
|
|
this.showScrollToBottom = false; // 滚到底后隐藏按钮
|
|
});
|
|
},
|
|
// 当用户点击“下拉”图标时
|
|
scrollToLatest() {
|
|
this.scrollToBottom('smooth');
|
|
},
|
|
build(msg) {
|
|
// 获取选中的实体和关系
|
|
let selectedEntities = msg.entities.filter(ent => ent.selected); // 从 msg 中获取被选中的 entities
|
|
let selectedRelations = msg.relations.filter(rel => rel.selected); // 从 msg 中获取被选中的 relations
|
|
|
|
// 构建数据对象
|
|
let data = {
|
|
entities: selectedEntities, // 选中的实体
|
|
relations: selectedRelations // 选中的关系
|
|
};
|
|
// 调用后端的 build 方法
|
|
build(data);
|
|
},
|
|
handleRelationSelectionChange(msg, rel) {
|
|
// 处理关系的选中状态
|
|
console.log(`关系 "${rel.e1} — ${rel.r} → ${rel.e2}" 被选择了: `, rel.selected);
|
|
},
|
|
|
|
// 更新实体和关系的选中状态
|
|
isRelationEnabled(msg, rel) {
|
|
const selectedEntities = msg.entities.filter(ent => ent.selected);
|
|
|
|
// 确保 e1 和 e2 都在选中的实体中
|
|
const hasE1 = selectedEntities.some(ent => ent.n === rel.e1);
|
|
const hasE2 = selectedEntities.some(ent => ent.n === rel.e2);
|
|
|
|
return hasE1 && hasE2;
|
|
},
|
|
removeFile(){
|
|
this.file={}
|
|
this.showFile=false
|
|
},
|
|
upload(){
|
|
this.$refs.fileInput.value = '';
|
|
this.$refs.fileInput.click(); // 点击按钮时打开文件选择框
|
|
},
|
|
handleFileChange(event) {
|
|
console.log(event)
|
|
const file = event.target.files[0]; // 获取第一个选择的文件
|
|
console.log(file)
|
|
if (file) {
|
|
this.fileName = file.name; // 存储文件名
|
|
|
|
const sizeInBytes = file.size; // 获取文件的字节数
|
|
let fileSize;
|
|
|
|
if (sizeInBytes < 1024 * 1024) {
|
|
// 文件小于 1MB,显示为 KB
|
|
const sizeInKB = sizeInBytes / 1024; // 将字节转换为 KB
|
|
fileSize = `${sizeInKB.toFixed(2)} KB`;
|
|
} else {
|
|
// 文件大于等于 1MB,显示为 MB
|
|
const sizeInMB = sizeInBytes / (1024 * 1024); // 将字节转换为 MB
|
|
fileSize = `${sizeInMB.toFixed(2)} MB`;
|
|
}
|
|
|
|
this.file = {
|
|
fileName: file.name,
|
|
size: fileSize, // 自动加上单位 KB 或 MB
|
|
file:file
|
|
};
|
|
this.showFile = true;
|
|
console.log("文件信息:", this.file); // 打印文件信息
|
|
}
|
|
},
|
|
allSelect(msg){
|
|
if (msg.entities && msg.entities.length > 0) {
|
|
msg.entities.forEach(ent => {
|
|
ent.selected=true
|
|
this.selectedEntities.push(ent);
|
|
});
|
|
}
|
|
if (msg.relations && msg.relations.length > 0) {
|
|
msg.relations.forEach(rel => {
|
|
rel.selected=true
|
|
this.selectedRelations.push(rel);
|
|
});
|
|
}
|
|
},
|
|
adjustHeight(status){
|
|
if(status){
|
|
this.rows=3
|
|
this.inputStatus=true
|
|
}else{
|
|
this.rows=10
|
|
this.inputStatus=false
|
|
}
|
|
},
|
|
pollAIStatus() {
|
|
if (this.status !== "wait") return; // 如果已结束,不再继续
|
|
try {
|
|
const data = getAIStatus();
|
|
if (data.status === "ok") {
|
|
this.status = "ok";
|
|
} else if (data.status === "error") {
|
|
this.status = "error";
|
|
} else {
|
|
setTimeout(() => this.pollAIStatus(), 3000);
|
|
}
|
|
} catch (error) {
|
|
console.error("轮询出错:", error);
|
|
this.status = "error";
|
|
alert("查询状态失败,请重试");
|
|
}
|
|
},
|
|
getNowDate(){
|
|
const currentDate = new Date();
|
|
|
|
// 获取年份、月份、日期、小时、分钟和秒
|
|
const year = currentDate.getFullYear();
|
|
const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以加1
|
|
const day = String(currentDate.getDate()).padStart(2, '0');
|
|
const hours = String(currentDate.getHours()).padStart(2, '0');
|
|
const minutes = String(currentDate.getMinutes()).padStart(2, '0');
|
|
const seconds = String(currentDate.getSeconds()).padStart(2, '0');
|
|
|
|
// 格式化时间为 YYYY-MM-DD HH:mm:ss
|
|
const formattedTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
return formattedTime;
|
|
},
|
|
fileToBase64(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
// 移除 "data:...;base64," 前缀,只保留纯 base64 字符串
|
|
const base64 = reader.result.split(',')[1];
|
|
resolve(base64);
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
},
|
|
async sendMessage() {
|
|
this.isSending=true
|
|
const text = this.inputText.trim();
|
|
const hasFile = this.showFile && this.file.file;
|
|
|
|
// 如果既没文本也没文件,不发送
|
|
if (!text && !hasFile) {
|
|
this.isSending = false;
|
|
return;
|
|
}
|
|
let data={
|
|
"text":text
|
|
}
|
|
// this.pollAIStatus()
|
|
// 添加用户消息
|
|
|
|
const userMessage = {
|
|
role: 'user',
|
|
content: text,
|
|
time: this.getNowDate(),
|
|
file: hasFile ? { name: this.file.fileName, size: this.file.size,file:this.file.file } : null,
|
|
hasFile:hasFile
|
|
};
|
|
this.messages.push(userMessage);
|
|
this.inputText = '';
|
|
this.showFile=false
|
|
|
|
if (hasFile) {
|
|
// 📎 有文件:使用 FormData 上传
|
|
// 将 File 转为 Base64
|
|
const fileBase64 = await this.fileToBase64(this.file.file);
|
|
|
|
// 构造 JSON payload(不再是 FormData!)
|
|
const payload = {
|
|
file_base64: fileBase64,
|
|
filename: this.file.file.name,
|
|
text: text || ''
|
|
};
|
|
|
|
// 调用新的 analyzeJson 接口(注意:不是传 formData)
|
|
analyze(payload).then(res => {
|
|
let message = {
|
|
role: 'assistant',
|
|
content: res,
|
|
entities: this.addSelectedFlag(res.entities),
|
|
relations: this.addSelectedFlag(res.relations, true),
|
|
isKG: true,
|
|
time: this.getNowDate()
|
|
};
|
|
this.messages.push(message);
|
|
this.scrollToBottom('smooth');
|
|
this.isSending = false;
|
|
this.file={}
|
|
this.allSelect(message)
|
|
})
|
|
} else {
|
|
const payload = {
|
|
text: text || ''
|
|
};
|
|
analyze(payload).then(res => {
|
|
let message = {
|
|
role: 'assistant',
|
|
content: res,
|
|
entities: res.entities,
|
|
relations: res.relations,
|
|
isKG: true,
|
|
time: this.getNowDate()
|
|
};
|
|
this.messages.push(message);
|
|
this.scrollToBottom('smooth');
|
|
this.isSending = false;
|
|
this.file={}
|
|
this.allSelect(message)
|
|
})
|
|
}
|
|
this.scrollToBottom('smooth');
|
|
},
|
|
insertPrefix(prefix) {
|
|
this.inputText += prefix;
|
|
this.$nextTick(() => {
|
|
// 自动聚焦到 textarea 末尾(简单处理)
|
|
const el = this.$el.querySelector('.input-box');
|
|
el.focus();
|
|
});
|
|
},
|
|
// scrollToBottom() {
|
|
// this.$nextTick(() => {
|
|
// const container = this.$refs.messagesContainer;
|
|
// console.log(container)
|
|
// if (container) {
|
|
// // 使用 setTimeout 确保所有 DOM 更新完成后再执行滚动
|
|
// setTimeout(() => {
|
|
// container.scrollTop = container.scrollHeight;
|
|
// console.log(container.scrollTop)
|
|
// console.log(container.scrollHeight)
|
|
// }, 100); // 延迟 100ms
|
|
// }
|
|
// });
|
|
// },
|
|
|
|
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')) || [];
|
|
console.log(savedMessages)
|
|
this.messages = savedMessages; // 加载之前存储的消息
|
|
},
|
|
handleBeforeUnload() {
|
|
localStorage.removeItem('messages');
|
|
// 页面刷新或关闭前,保存所有消息
|
|
console.log("页面刷新或关闭前,保存消息...");
|
|
this.messages.forEach((message) => {
|
|
this.saveMessage(message);
|
|
});
|
|
},
|
|
|
|
},
|
|
watch: {
|
|
messages() {
|
|
this.scrollToBottom('smooth');
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
const container = this.$refs.messagesContainer;
|
|
container.removeEventListener('scroll', this.handleScroll);
|
|
},
|
|
beforeRouteLeave(to, from, next) {
|
|
console.log('【守卫触发】离开 /graph 路由');
|
|
// alert('即将离开,保存数据!'); // 临时测试
|
|
this.handleBeforeUnload();
|
|
next();
|
|
},
|
|
mounted() {
|
|
const container = this.$refs.messagesContainer;
|
|
container.addEventListener('scroll', this.handleScroll);
|
|
this.loadMessages();
|
|
|
|
this.scrollToBottom('smooth');
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
|
},
|
|
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.qwen-chat {
|
|
flex: 1; overflow: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
margin: 0 auto;
|
|
border: 1px solid #e0e0e0;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background-color: #F3F3F3;
|
|
}
|
|
|
|
/* === 消息区域 === */
|
|
.chat-messages {
|
|
flex: 1;
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.message {
|
|
display: flex;
|
|
margin-bottom: 12px;
|
|
align-items: flex-start;
|
|
flex-direction: column; /* 垂直排列时间和内容 */
|
|
}
|
|
|
|
.message.user {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.message.assistant {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.bubble {
|
|
padding: 16px;
|
|
border-radius: 13px;
|
|
max-width: 55%;
|
|
word-break: break-word;
|
|
font-size: 12px;
|
|
text-align: left;
|
|
}
|
|
|
|
.message.user .bubble {
|
|
background-color: #e9e9e9;
|
|
color: #333;
|
|
}
|
|
|
|
.message.assistant .bubble {
|
|
background-color: #fff;
|
|
color: #333;
|
|
}
|
|
|
|
/* === 输入区域 === */
|
|
.input-area {
|
|
position: relative;
|
|
padding: 16px;
|
|
background: white;
|
|
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 {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
resize: none;
|
|
outline: none;
|
|
transition: border-color 0.2s ease;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.input-box:focus {
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin: 12px 0;
|
|
white-space: nowrap;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.btn {
|
|
display: flex; /* 使用flex布局 */
|
|
justify-content: center; /* 水平居中 */
|
|
align-items: center; /* 垂直居中 */
|
|
width: 20px; /* 设置按钮的宽度为20px */
|
|
height: 20px; /* 设置按钮的高度为20px */
|
|
border-radius: 50%; /* 圆形按钮 */
|
|
background-color: white; /* 背景色为白色 */
|
|
font-size: 18px; /* 设置加号的字体大小 */
|
|
line-height: 20px; /* 设置行高,确保加号垂直居中 */
|
|
color: #333; /* 加号的颜色 */
|
|
font-weight: bold; /* 加号字体加粗 */
|
|
cursor: pointer; /* 鼠标悬停时显示为点击效果 */
|
|
}
|
|
|
|
.btn:hover {
|
|
background: #e9e9e9;
|
|
}
|
|
|
|
.btn span {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.more-btn {
|
|
background: transparent;
|
|
border: 1px dashed #ccc;
|
|
color: #666;
|
|
}
|
|
|
|
.icon-group {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.icon,
|
|
.send-btn span {
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
color: #999;
|
|
}
|
|
|
|
.icon:hover,
|
|
.send-btn:hover span {
|
|
color: #333;
|
|
}
|
|
|
|
.send-btn {
|
|
background: #B9CDFF;
|
|
border: none;
|
|
width: 60px;
|
|
height: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.send-btn:hover {
|
|
background: #155DFF;
|
|
}
|
|
/* === 自定义助手 KG 卡片 === */
|
|
.kg-card {
|
|
background: white;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
width: 100%;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
.kg-card h4 {
|
|
margin: 0 0 16px 0;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
color: #1a73e8;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.kg-section {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.kg-section h5 {
|
|
margin: 0 0 8px 0;
|
|
color: #555;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* 实体标签 */
|
|
.entity-list {
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.entity-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 4px 10px;
|
|
border-radius: 20px;
|
|
font-size: 13px;
|
|
background: #f0f8ff;
|
|
color: #1e88e5;
|
|
border: 1px solid #bbdefb;
|
|
}
|
|
|
|
.entity-tag small {
|
|
margin-left: 6px;
|
|
opacity: 0.8;
|
|
font-weight: normal;
|
|
}
|
|
|
|
/* 按类型着色(可扩展) */
|
|
/*
|
|
.tag-疾病 { background: #ffebee; color: #c62828; border-color: #ffcdd2; }
|
|
.tag-症状 { background: #e8f5e9; color: #2e7d32; border-color: #c8e6c9; }
|
|
.tag-检查 { background: #fff8e1; color: #ff8f00; border-color: #ffecb3; }
|
|
.tag-药物 { background: #f3e5f5; color: #7b1fa2; border-color: #ce93d8; }
|
|
*/
|
|
|
|
/* 关系列表 */
|
|
.relation-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.relation-list li {
|
|
padding: 6px 0;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.relation-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
|
|
|
|
/* 空状态 */
|
|
.empty-state {
|
|
color: #999;
|
|
font-style: italic;
|
|
padding: 12px 0;
|
|
}
|
|
|
|
/* 普通助手文本(非 KG) */
|
|
.assistant-text {
|
|
background-color: #e9ecef;
|
|
color: #333;
|
|
}
|
|
.message.assistant .time{
|
|
text-align: left;
|
|
color: #A5A5A5;
|
|
margin-bottom: 5px;
|
|
font-size: 12px;
|
|
margin-left: 60px;
|
|
}
|
|
.message.user .time{
|
|
text-align: right;
|
|
color: #A5A5A5;
|
|
margin-bottom: 5px;
|
|
font-size: 12px;
|
|
|
|
}
|
|
.word{
|
|
/* border: 1px solid rgba(17,17,51,.1);*/
|
|
border-radius: 8px;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
width: 16%;
|
|
min-width: 149px;
|
|
max-width: 206px;
|
|
padding: 9px 6px;
|
|
display: flex;
|
|
margin-left: 10px;
|
|
position: relative;
|
|
background-color: #e9e9e9;
|
|
}
|
|
.wordName{
|
|
overflow: hidden; /* 隐藏超出部分 */
|
|
display: -webkit-box; /* 使用 webkit 的盒子模型 */
|
|
-webkit-box-orient: vertical; /* 设置为垂直方向 */
|
|
-webkit-line-clamp: 1; /* 限制为两行 */
|
|
text-overflow: ellipsis; /* 显示省略号 */
|
|
}
|
|
.word:hover .close-btn {
|
|
display: block; /* 悬浮时显示X按钮 */
|
|
}
|
|
.close-btn{
|
|
position: absolute;
|
|
|
|
width: 20px; /* 设置适合的图片大小 */
|
|
height: 20px;
|
|
cursor: pointer;
|
|
padding: 5px;
|
|
box-sizing: border-box;
|
|
top: -8px;
|
|
right: -7px;
|
|
border: 1px solid rgba(17,17,51,.1);
|
|
border-radius: 50%;
|
|
background-color: #fff;
|
|
display: none;
|
|
}
|
|
.dropdown-icon {
|
|
position: absolute;
|
|
top: -45px; /* 图标高度约45px,一半是22.5,所以 -22~ -25 可居中于顶部线上 */
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 10;
|
|
cursor: pointer;
|
|
}
|
|
.send-btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
background-color: #ccc;
|
|
}
|
|
/* 加载指示器的整体样式 */
|
|
.loading-indicator {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px; /* 圆点之间的间距 */
|
|
}
|
|
|
|
/* 单个圆点的基本样式 */
|
|
.dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
animation: bounce 1.4s infinite ease-in-out both;
|
|
}
|
|
|
|
/* 给每个圆点设定特定的灰色 */
|
|
.dot-1 { background-color: #e0e0e0; } /* 浅灰色 */
|
|
.dot-2 { background-color: #b0b0b0; } /* 中浅灰色 */
|
|
.dot-3 { background-color: #808080; } /* 中深灰色 */
|
|
.dot-4 { background-color: #505050; } /* 深灰色 */
|
|
|
|
/* 给每个圆点设置不同的动画延迟以创造连续的效果 */
|
|
.dot:nth-child(1) { animation-delay: 0.1s; }
|
|
.dot:nth-child(2) { animation-delay: 0.2s; }
|
|
.dot:nth-child(3) { animation-delay: 0.3s; }
|
|
.dot:nth-child(4) { animation-delay: 0.4s; }
|
|
|
|
.dot-1 { animation-delay: -0.4s; }
|
|
.dot-2 { animation-delay: -0.2s; }
|
|
.dot-3 { animation-delay: 0s; }
|
|
.dot-4 { animation-delay: 0.2s; }
|
|
/* 定义动画效果 */
|
|
@keyframes bounce {
|
|
0%, 80%, 100% {
|
|
transform: scale(0);
|
|
} 40% {
|
|
transform: scale(1.0);
|
|
}
|
|
}
|
|
</style>
|