Browse Source

add: 添加参考资料

master
tiantian 5 months ago
parent
commit
4b8cb1b188
  1. 16
      frontend/src/components/localchat/AiChatMain.vue
  2. 161
      frontend/src/components/localchat/AiChatMessage.vue

16
frontend/src/components/localchat/AiChatMain.vue

@ -6,7 +6,7 @@ import { notifyError } from "@/util/msg.ts";
import { ElScrollbar } from "element-plus"; import { ElScrollbar } from "element-plus";
import { getSystemConfig } from "@/system/config"; import { getSystemConfig } from "@/system/config";
import { Vue3Lottie } from "vue3-lottie"; import { Vue3Lottie } from "vue3-lottie";
import { BrowserWindow } from "@/system"; import { BrowserWindow, useSystem } from "@/system";
import { isMobileDevice } from "@/util/device"; import { isMobileDevice } from "@/util/device";
const chatStore = useAiChatStore(); const chatStore = useAiChatStore();
const modelStore = useModelStore(); const modelStore = useModelStore();
@ -22,6 +22,7 @@ const messageInnerRef = ref<HTMLDivElement>();
// User Input Message // User Input Message
const userMessage = ref(""); const userMessage = ref("");
const knowledgeId = ref(0) const knowledgeId = ref(0)
const system = useSystem()
const promptMessage = computed(() => { const promptMessage = computed(() => {
return [ return [
{ {
@ -132,7 +133,7 @@ const createCompletion = async () => {
}; };
const chatConfig = modelStore.chatConfig.chat; const chatConfig = modelStore.chatConfig.chat;
const knowledgeId = chatStore.chatInfo.knowledgeId*1; const knowledgeId = chatStore.chatInfo.knowledgeId * 1;
let postMsg: any = { let postMsg: any = {
messages: requestMessages.value, messages: requestMessages.value,
model: chatStore.chatInfo.model, model: chatStore.chatInfo.model,
@ -182,11 +183,17 @@ const createCompletion = async () => {
return; return;
} }
const res = await completion.json(); const res = await completion.json();
//console.log(res) console.log(res)
if (res && res.choices && res.choices.length > 0) { if (res && res.choices && res.choices.length > 0) {
if (res.choices[0].message.content) { if (res.choices[0].message.content) {
const msg = res.choices[0].message.content; const msg = res.choices[0].message.content;
saveMessage.content = msg; saveMessage.content = msg;
if (res.documents && res.documents.length > 0) {
saveMessage.doc = res.documents;
}
if (res.web_search && res.web_search.length > 0) {
saveMessage.web_search = res.web_search;
}
chatStore.messageList.push(saveMessage); chatStore.messageList.push(saveMessage);
await chatStore.addMessages(chatStore.activeId, saveMessage); await chatStore.addMessages(chatStore.activeId, saveMessage);
} }
@ -275,7 +282,8 @@ const uploadImage = async (event: any) => {
<el-scrollbar v-if="chatStore.messageList.length > 0" class="message-container" ref="messageContainerRef"> <el-scrollbar v-if="chatStore.messageList.length > 0" class="message-container" ref="messageContainerRef">
<div ref="messageInnerRef"> <div ref="messageInnerRef">
<ai-chat-message v-for="message in chatStore.messageList" :key="message.messageId" :content="message.content" <ai-chat-message v-for="message in chatStore.messageList" :key="message.messageId" :content="message.content"
:link="message.link" :role="message.role" :createdAt="message.createdAt" /> :link="message.link" :role="message.role" :createdAt="message.createdAt" :doc="message.doc || []"
:web_search="message.web_search || []" :system="system" />
</div> </div>
</el-scrollbar> </el-scrollbar>
<div class="no-message-container" v-else> <div class="no-message-container" v-else>

161
frontend/src/components/localchat/AiChatMessage.vue

@ -3,16 +3,33 @@ import { renderMarkdown } from "@/util/markdown.ts";
import moment from "moment"; import moment from "moment";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { getSystemKey } from "@/system/config"; import { getSystemKey } from "@/system/config";
import { System } from "@/system";
import { ElMessageBox } from "element-plus";
interface Props { interface Props {
content: string; content: string;
role?: string; role?: string;
link?: any; link?: any;
createdAt: number | string; createdAt: number | string;
doc: any[];
web_search: any[];
system: System;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const sourceExpand = ref(false);
const getDateTime = (t:any) => { const openFile = (filePath: string) => {
props.system.openFile(filePath);
};
const openlink = (item: any) => {
if (!item.link) return ElMessageBox.alert(
item.content,
{
confirmButtonText: '确定',
}
)
window.open(item.link)
}
const getDateTime = (t: any) => {
return moment(t).format("MM-DD HH:mm"); return moment(t).format("MM-DD HH:mm");
}; };
const showLink = ref(false); const showLink = ref(false);
@ -36,7 +53,7 @@ const imgList = computed<any[]>(() => {
if (props.link && props.link.length > 0) { if (props.link && props.link.length > 0) {
const list: any = []; const list: any = [];
const url = getSystemKey("apiUrl") + "/showimage?path="; const url = getSystemKey("apiUrl") + "/showimage?path=";
props.link.forEach((item:any) => { props.link.forEach((item: any) => {
if (item.metadata.type === "image") { if (item.metadata.type === "image") {
list.push(url + item.metadata.file); list.push(url + item.metadata.file);
} }
@ -47,7 +64,7 @@ const imgList = computed<any[]>(() => {
}); });
async function showDetail(file: string, type: string) { async function showDetail(file: string, type: string) {
const filePath = getSystemKey("apiUrl") + "/filedetail?path=" + file const filePath = getSystemKey("apiUrl") + "/filedetail?path=" + file
if(type != "image") { if (type != "image") {
const reponse = await fetch(filePath); const reponse = await fetch(filePath);
if (reponse.ok) { if (reponse.ok) {
const data = await reponse.text(); const data = await reponse.text();
@ -55,12 +72,12 @@ async function showDetail(file: string, type: string) {
detailTxt.value = data; detailTxt.value = data;
detailImg.value = ""; detailImg.value = "";
} }
}else{ } else {
refbox.value = true; refbox.value = true;
detailTxt.value = ''; detailTxt.value = '';
detailImg.value = filePath; detailImg.value = filePath;
} }
} }
</script> </script>
@ -76,7 +93,9 @@ async function showDetail(file: string, type: string) {
<div v-if="props.role === 'assistant'" class="assistant align-start spacing"> <div v-if="props.role === 'assistant'" class="assistant align-start spacing">
<div class="assistant-avatar"> <div class="assistant-avatar">
<div class="icon-container"> <div class="icon-container">
<el-icon><GoldMedal /></el-icon> <el-icon>
<GoldMedal />
</el-icon>
</div> </div>
</div> </div>
<div class="message assistant-message rounded-al"> <div class="message assistant-message rounded-al">
@ -86,31 +105,13 @@ async function showDetail(file: string, type: string) {
{{ getDateTime(props.createdAt) }} {{ getDateTime(props.createdAt) }}
</div> </div>
<div> <div>
<el-button <el-button icon="InfoFilled" circle size="small" v-if="props.link" @click="handleFlag" />
icon="InfoFilled" <el-button icon="PictureFilled" circle size="small" v-if="imgList.length > 0" @click="handleLink" />
circle
size="small"
v-if="props.link"
@click="handleFlag"
/>
<el-button
icon="PictureFilled"
circle
size="small"
v-if="imgList.length > 0"
@click="handleLink"
/>
</div> </div>
</el-row> </el-row>
<el-card v-if="showLink" style="color: black"> <el-card v-if="showLink" style="color: black">
<el-row <el-row type="flex" justify="space-between" align="middle" :gutter="24" style="margin-top: 10px"
type="flex" v-for="(item, key) in props.link">
justify="space-between"
align="middle"
:gutter="24"
style="margin-top: 10px"
v-for="(item, key) in props.link"
>
<el-col :span="2"> <el-col :span="2">
<el-avatar size="small"> <el-avatar size="small">
{{ key + 1 }} {{ key + 1 }}
@ -124,35 +125,52 @@ async function showDetail(file: string, type: string) {
<el-card v-if="showImg" style="color: black"> <el-card v-if="showImg" style="color: black">
<div class="image-grid"> <div class="image-grid">
<div v-for="(item, index) in imgList" :key="index" class="image-item"> <div v-for="(item, index) in imgList" :key="index" class="image-item">
<el-image <el-image :src="item" alt="Image" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2"
:src="item" :preview-src-list="imgList" :initial-index="index" fit="cover" class="image" />
alt="Image"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="imgList"
:initial-index="index"
fit="cover"
class="image"
/>
</div> </div>
</div> </div>
</el-card> </el-card>
<!-- 参考源 -->
<div class="reference-source" v-if="props.doc.length || props.web_search.length">
<div class="source-title" @click="sourceExpand = !sourceExpand" style="cursor: pointer;">
<el-button type="default" style="margin-right: 5px; width: 20px;height: 20px;" icon="Document" circle />
<span style="margin-right: 5px">{{ props.doc.length || props.web_search.length }}篇资料作为参考</span>
<ArrowDown v-if="sourceExpand" class="icon"></ArrowDown>
<ArrowRight v-else class="icon"></ArrowRight>
</div>
<!-- doucument -->
<div v-if="props.doc.length">
<ul class="source-list" v-if="sourceExpand" v-for="(item, i) in props.doc" :key="i">
<li class="source-item" @click="openFile(item.file_path)">
{{ item.file_name }}</li>
</ul>
</div>
<!-- web_search -->
<div v-else>
<ul class="source-list" v-if="sourceExpand" v-for="item in props.web_search" :key="item.title">
<li class="source-item" @click="openlink(item)">
<img :src="item.icon" v-if="item.icon" style="width: 15px;height: 15px; margin-right: 5px;">{{
item.title
}}
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
<!-- 用户信息 --> <!-- 用户信息 -->
<div v-else-if="props.role === 'user'" class="user align-center spacing"> <div v-else-if="props.role === 'user'" class="user align-center spacing">
<div class="message-container"> <div class="message-container">
<div <div class="message user-message rounded-xl" style="max-height: 80px; overflow-y: auto">
class="message user-message rounded-xl"
style="max-height: 80px; overflow-y: auto"
>
<div>{{ props.content }}</div> <div>{{ props.content }}</div>
</div> </div>
</div> </div>
<div class="avatar"> <div class="avatar">
<div class="icon-container"> <div class="icon-container">
<el-icon><UserFilled /></el-icon> <el-icon>
<UserFilled />
</el-icon>
</div> </div>
</div> </div>
</div> </div>
@ -165,12 +183,14 @@ $win10-blue: #0078d7;
$win10-light-blue: #c7e8ff; $win10-light-blue: #c7e8ff;
$win10-grey: #afafaf; $win10-grey: #afafaf;
$win10-light-grey: #f2f2f2; $win10-light-grey: #f2f2f2;
.message-container { .message-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
width: 99%; width: 99%;
} }
.message { .message {
margin-top: 10px; margin-top: 10px;
padding: 8px; padding: 8px;
@ -181,9 +201,11 @@ $win10-light-grey: #f2f2f2;
text-align: left; text-align: left;
margin: 8px; margin: 8px;
} }
.text-grey { .text-grey {
color: $win10-grey; color: $win10-grey;
} }
.user-message { .user-message {
background-color: $win10-blue; background-color: $win10-blue;
border-radius: 12px 0 0 12px; border-radius: 12px 0 0 12px;
@ -191,19 +213,25 @@ $win10-light-grey: #f2f2f2;
font-size: 14px; font-size: 14px;
color: $win10-light-blue; color: $win10-light-blue;
} }
.assistant-message { .assistant-message {
background-color: $win10-blue; flex: 1;
background-color: #fff;
border-radius: 0 12px 12px 0; border-radius: 0 12px 12px 0;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
color: $win10-light-blue; color: #333;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
padding: 10px 20px;
} }
.avatar { .avatar {
display: flex; display: flex;
align-items: center; align-items: center;
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
.assistant-avatar { .assistant-avatar {
display: flex; display: flex;
align-items: center; align-items: center;
@ -211,6 +239,7 @@ $win10-light-grey: #f2f2f2;
height: 35px; height: 35px;
padding-left: 10px; padding-left: 10px;
} }
.icon-container { .icon-container {
display: flex; display: flex;
align-items: center; align-items: center;
@ -225,9 +254,11 @@ $win10-light-grey: #f2f2f2;
.rounded-xl { .rounded-xl {
border-radius: 12px 0px 12px 12px; border-radius: 12px 0px 12px 12px;
} }
.rounded-al { .rounded-al {
border-radius: 0px 12px 12px 12px; border-radius: 0px 12px 12px 12px;
} }
.align-start, .align-start,
.align-center { .align-center {
display: flex; display: flex;
@ -237,6 +268,7 @@ $win10-light-grey: #f2f2f2;
.align-center { .align-center {
justify-content: flex-end; justify-content: flex-end;
} }
.image-grid { .image-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@ -245,7 +277,8 @@ $win10-light-grey: #f2f2f2;
.image-item { .image-item {
position: relative; position: relative;
border: 1px solid white; /* 白边效果 */ border: 1px solid white;
/* 白边效果 */
overflow: hidden; overflow: hidden;
} }
@ -253,4 +286,30 @@ $win10-light-grey: #f2f2f2;
width: 100%; width: 100%;
height: auto; height: auto;
} }
.reference-source {
font-size: 13px;
color: #9b9b9b;
margin-top: 10px;
.source-title {
display: flex;
align-items: center;
}
.source-list {
list-style: disc inside none;
margin-top: 5px;
.source-item {
background-color: #ecebeb;
height: 30px;
padding: 0 10px;
line-height: 30px;
border-radius: 15px;
cursor: pointer;
overflow: hidden;
}
}
}
</style> </style>

Loading…
Cancel
Save