mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-05 10:05:33 +08:00
v3.8.1发布,上传前端代码
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
|
||||
<template v-if="dataSource">
|
||||
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<div v-if="isMultiSession" class="leftArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<div class="content">
|
||||
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
|
||||
<slide :source="source" v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
|
||||
</div>
|
||||
<div class="toggle-btn" @click="handleToggle">
|
||||
<span class="icon">
|
||||
@ -30,6 +30,7 @@
|
||||
@reload-message-title="reloadMessageTitle"
|
||||
:chatTitle="chatTitle"
|
||||
:quickCommandData="quickCommandData"
|
||||
:showAdvertising = "showAdvertising"
|
||||
></chat>
|
||||
</div>
|
||||
</template>
|
||||
@ -46,6 +47,7 @@
|
||||
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAppInject } from "@/hooks/web/useAppInject";
|
||||
|
||||
const router = useRouter();
|
||||
const userId = useUserStore().getUserInfo?.id;
|
||||
@ -77,6 +79,8 @@
|
||||
const prologue = ref<string>('');
|
||||
//快捷指令
|
||||
const quickCommandData = ref<any>([]);
|
||||
//是否显示广告位
|
||||
const showAdvertising = ref<boolean>(false);
|
||||
|
||||
const priming = () => {
|
||||
dataSource.value = {
|
||||
@ -142,6 +146,13 @@
|
||||
);
|
||||
};
|
||||
|
||||
//是否为多会话模式
|
||||
const isMultiSession = ref<boolean>(true);
|
||||
//是否为手机
|
||||
const { getIsMobile } = useAppInject();
|
||||
//来源
|
||||
const source = ref<string>('');
|
||||
|
||||
/**
|
||||
* 初始化聊天信息
|
||||
* @param appId
|
||||
@ -184,6 +195,13 @@
|
||||
{ name: 'JEECG有哪些优势?', descr: "JEECG有哪些优势?" },
|
||||
{ name: 'JEECG可以做哪些事情?', descr: "JEECG可以做哪些事情?" },];
|
||||
}
|
||||
let query: any = router.currentRoute.value.query;
|
||||
source.value = query.source;
|
||||
if(query.source){
|
||||
showAdvertising.value = query.source === 'chatJs';
|
||||
}else{
|
||||
showAdvertising.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -203,7 +221,7 @@
|
||||
await defHttp
|
||||
.get(
|
||||
{
|
||||
url: '/airag/app/queryById',
|
||||
url: '/airag/chat/init',
|
||||
params: { id: appId },
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
@ -220,6 +238,23 @@
|
||||
if (res.result && res.result.presetQuestion) {
|
||||
presetQuestion.value = res.result.presetQuestion;
|
||||
}
|
||||
if (res.result && res.result.metadata) {
|
||||
let metadata = JSON.parse(res.result.metadata);
|
||||
//判斷是否为手机模式
|
||||
if(!getIsMobile.value){
|
||||
//是否为多会话模式
|
||||
if((metadata.multiSession && metadata.multiSession === '1') || !metadata.multiSession) {
|
||||
isMultiSession.value = true;
|
||||
} else {
|
||||
isMultiSession.value = false;
|
||||
expand.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(getIsMobile.value){
|
||||
isMultiSession.value = false;
|
||||
expand.value = false;
|
||||
}
|
||||
} else {
|
||||
appData.value = {};
|
||||
}
|
||||
@ -281,7 +316,7 @@
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
background: white;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
@ -339,7 +374,7 @@
|
||||
color: rgb(51, 54, 57);
|
||||
border: 1px solid rgb(239, 239, 245);
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 2px 4px 0px #e7e9ef;
|
||||
transform: translateX(50%) translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@ -17,19 +17,19 @@
|
||||
import AiChat from './AiChat.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
//aiChat<EFBFBD><EFBFBD>ref
|
||||
//aiChat的ref
|
||||
const aiChatRef = ref();
|
||||
//Ӧ<EFBFBD><EFBFBD>id
|
||||
//应用id
|
||||
const appId = ref<string>('');
|
||||
|
||||
//<EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
//是否显示聊天
|
||||
const showChat = ref<any>(false);
|
||||
const router = useRouter();
|
||||
//<EFBFBD>ж<EFBFBD><EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>
|
||||
//判断是否为初始化
|
||||
const isInit = ref<boolean>(false);
|
||||
|
||||
/**
|
||||
* chatͼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>
|
||||
* chat图标点击事件
|
||||
*/
|
||||
function chatClick() {
|
||||
showChat.value = !showChat.value;
|
||||
@ -67,7 +67,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
|
||||
box-shadow: #cccccc 0 4px 8px 0;
|
||||
}
|
||||
.footer-close-icon {
|
||||
color: #0a3069;
|
||||
|
||||
@ -3,6 +3,13 @@
|
||||
<div class="content">
|
||||
<div class="header-title" v-if="type === 'view' && headerTitle">
|
||||
{{headerTitle}}
|
||||
<div v-if="showAdvertising" class="header-advertisint">
|
||||
AI客服由
|
||||
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
|
||||
敲敲云
|
||||
</a>
|
||||
提供
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div id="scrollRef" ref="scrollRef" class="scrollArea">
|
||||
@ -19,6 +26,8 @@
|
||||
:appData="appData"
|
||||
:presetQuestion="item.presetQuestion"
|
||||
:images = "item.images"
|
||||
:retrievalText="item.retrievalText"
|
||||
:referenceKnowledge="item.referenceKnowledge"
|
||||
@send="handleOutQuestion"
|
||||
></chatMessage>
|
||||
</div>
|
||||
@ -86,6 +95,7 @@
|
||||
autofocus
|
||||
:readonly="loading"
|
||||
style="border-color: #ffffff !important;box-shadow:none"
|
||||
@paste="paste"
|
||||
>
|
||||
</a-textarea>
|
||||
<a-button v-if="loading" type="primary" danger @click="handleStopChat" class="stopBtn">
|
||||
@ -122,7 +132,7 @@
|
||||
>
|
||||
<a-tooltip title="图片上传,支持jpg/jpeg/png">
|
||||
<a-button class="sendBtn" type="text">
|
||||
<Icon icon="ant-design:picture-outlined" style="color: rgba(15,21,40,0.8)"></Icon>
|
||||
<Icon icon="ant-design:picture-outlined" style="color: #3d4353"></Icon>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-upload>
|
||||
@ -178,21 +188,22 @@
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import {getFileAccessHttpUrl, getHeaders} from "@/utils/common/compUtils";
|
||||
import { uploadUrl } from '/@/api/common/api';
|
||||
import { createImgPreview } from "@/components/Preview";
|
||||
|
||||
import { useAppInject } from "@/hooks/web/useAppInject";
|
||||
import { useGlobSetting } from "@/hooks/setting";
|
||||
|
||||
message.config({
|
||||
prefixCls: 'ai-chat-message',
|
||||
});
|
||||
|
||||
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData']);
|
||||
|
||||
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData','showAdvertising']);
|
||||
const emit = defineEmits(['save','reload-message-title']);
|
||||
const { scrollRef, scrollToBottom } = useScroll();
|
||||
const prompt = ref<string>('');
|
||||
const loading = ref<boolean>(false);
|
||||
const inputRef = ref<Ref | null>(null);
|
||||
const headerTitle = ref<string>(props.chatTitle);
|
||||
|
||||
|
||||
//聊天数据
|
||||
const chatData = ref<any>([]);
|
||||
//应用数据
|
||||
@ -202,15 +213,24 @@
|
||||
const topicId = ref<string>('');
|
||||
//请求id
|
||||
const requestId = ref<string>('');
|
||||
const { getIsMobile } = useAppInject();
|
||||
const conversationList = computed(() => chatData.value.filter((item) => item.inversion != 'user' && !!item.conversationOptions));
|
||||
const placeholder = computed(() => {
|
||||
return '来说点什么吧...(Shift + Enter = 换行)';
|
||||
if(getIsMobile.value){
|
||||
return '来说点什么吧...'
|
||||
} else {
|
||||
return '来说点什么吧...(Shift + Enter = 换行)';
|
||||
}
|
||||
});
|
||||
//token
|
||||
const headers = getHeaders();
|
||||
//文本域点击事件
|
||||
const textareaActive = ref<boolean>(false);
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const baseUploadUrl = globSetting.uploadUrl;
|
||||
const uploadUrl = ref<string>(`${baseUploadUrl}/airag/chat/upload`);
|
||||
|
||||
function handleEnter(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
@ -269,6 +289,7 @@
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
referenceKnowledge: [],
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
@ -318,7 +339,7 @@
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: data,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
error: true,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: null,
|
||||
@ -370,19 +391,27 @@
|
||||
|
||||
handleStop();
|
||||
|
||||
const knowList = ref<Recordable[]>([])
|
||||
|
||||
/**
|
||||
* 停止消息
|
||||
*/
|
||||
function handleStopChat() {
|
||||
if(requestId.value){
|
||||
//调用后端接口停止响应
|
||||
defHttp.get({
|
||||
url: '/airag/chat/stop/' + requestId.value,
|
||||
},{ isTransformResponse: false });
|
||||
//update-begin---author:wangshuai---date:2025-06-03---for:【issues/8338】AI应用聊天回复stop无效,仍会继续输出回复---
|
||||
const currentRequestId = requestId.value
|
||||
if(currentRequestId){
|
||||
try{
|
||||
//调用后端接口停止响应
|
||||
defHttp.get({
|
||||
url: '/airag/chat/stop/' + currentRequestId,
|
||||
},{ isTransformResponse: false });
|
||||
} finally {
|
||||
handleStop();
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-06-03---for:【issues/8338】AI应用聊天回复stop无效,仍会继续输出回复---
|
||||
}
|
||||
handleStop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 读取文本
|
||||
* @param message
|
||||
@ -409,15 +438,16 @@
|
||||
};
|
||||
|
||||
if(headerTitle.value == '新建聊天'){
|
||||
headerTitle.value = message.length>5?message.substring(0,5):message
|
||||
headerTitle.value = message.length>10?truncateString(message,10):message
|
||||
}
|
||||
|
||||
emit("reload-message-title",message.length>5?message.substring(0,5):message)
|
||||
emit("reload-message-title",message.length>10?truncateString(message,10):message)
|
||||
}
|
||||
|
||||
uploadUrlList.value = [];
|
||||
fileInfoList.value = [];
|
||||
|
||||
knowList.value = [];
|
||||
|
||||
const readableStream = await defHttp.post(
|
||||
{
|
||||
url: props.url,
|
||||
@ -430,9 +460,18 @@
|
||||
isTransformResponse: false,
|
||||
}
|
||||
).catch((e)=>{
|
||||
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
|
||||
handleStop();
|
||||
return;
|
||||
//update-begin---author:wangshuai---date:2025-04-28---for:【QQYUN-12297】【AI】聊天,超时以后提示---
|
||||
if(e.code === 'ETIMEDOUT'){
|
||||
updateChatFail(uuid, chatData.value.length - 1, "当前用户较多,排队中,请稍候再次重试!");
|
||||
handleStop();
|
||||
return;
|
||||
}else{
|
||||
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
|
||||
handleStop();
|
||||
return;
|
||||
}
|
||||
console.error(e)
|
||||
//update-end---author:wangshuai---date:2025-04-28---for:【QQYUN-12297】【AI】聊天,超时以后提示---
|
||||
});
|
||||
const reader = readableStream.getReader();
|
||||
const decoder = new TextDecoder('UTF-8');
|
||||
@ -448,9 +487,9 @@
|
||||
let result = decoder.decode(value, { stream: true });
|
||||
result = buffer + result;
|
||||
const lines = result.split('\n\n');
|
||||
for (const line of lines) {
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const content = line.replace('data:', '').trim();
|
||||
let content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
@ -461,6 +500,9 @@
|
||||
buffer = "";
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
if(content.indexOf(":::card:::") !== -1){
|
||||
content = content.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse,conversationId,text,options).then((res)=>{
|
||||
text = res.returnText;
|
||||
@ -482,6 +524,9 @@
|
||||
buffer = "";
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
if(line.indexOf(":::card:::") !== -1){
|
||||
line = line.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(line);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
@ -523,24 +568,42 @@
|
||||
async function renderText(item,conversationId,text,options) {
|
||||
let returnText = "";
|
||||
if (item.event == 'MESSAGE') {
|
||||
text = text + item.data.message;
|
||||
returnText = text;
|
||||
let message = item.data.message;
|
||||
let messageText = "";
|
||||
//update-begin---author:wangshuai---date:2025-04-24---for:应该先判断是否包含card---
|
||||
if(message && message.indexOf("::card::") !== -1){
|
||||
messageText = message;
|
||||
} else {
|
||||
text = text + item.data.message;
|
||||
messageText = text;
|
||||
returnText = text;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-04-24---for:应该先判断是否包含card---
|
||||
// 从消息中获取 requestId
|
||||
if (item.requestId) {
|
||||
requestId.value = item.requestId;
|
||||
}
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: text,
|
||||
content: messageText,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
referenceKnowledge: knowList.value,
|
||||
});
|
||||
}
|
||||
if(item.event == 'INIT_REQUEST_ID'){
|
||||
if (item.requestId) {
|
||||
requestId.value = item.requestId;
|
||||
}
|
||||
}
|
||||
if (item.event == 'MESSAGE_END') {
|
||||
topicId.value = item.topicId;
|
||||
conversationId = item.conversationId;
|
||||
uuid.value = item.conversationId;
|
||||
requestId.value = item.requestId;
|
||||
handleStop();
|
||||
}
|
||||
if (item.event == 'FLOW_FINISHED') {
|
||||
@ -565,41 +628,60 @@
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
|
||||
if(item.event === "NODE_STARTED"){
|
||||
let aiText = "";
|
||||
if(item.data.type === 'llm'){
|
||||
aiText = "正在构建响应内容";
|
||||
if(!item.data || item.data.type !== 'end'){
|
||||
let aiText = "";
|
||||
if(item.data.type === 'llm'){
|
||||
aiText = "正在构建响应内容";
|
||||
}
|
||||
if(item.data.type === 'knowledge'){
|
||||
aiText = "正在对知识库进行深度检索";
|
||||
}
|
||||
if(item.data.type === 'classifier'){
|
||||
aiText = "正在分类";
|
||||
}
|
||||
if(item.data.type === 'code'){
|
||||
aiText = "正在实施代码运行操作";
|
||||
}
|
||||
if(item.data.type === 'subflow'){
|
||||
aiText = "正在运行子流程";
|
||||
}
|
||||
if(item.data.type === 'enhanceJava'){
|
||||
aiText = "正在执行java增强";
|
||||
}
|
||||
if(item.data.type === 'http'){
|
||||
aiText = "正在发送http请求";
|
||||
}
|
||||
if(!text){
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
retrievalText: aiText,
|
||||
text:"",
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
referenceKnowledge: knowList.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
if(item.data.type === 'knowledge'){
|
||||
aiText = "正在对知识库进行深度检索";
|
||||
}
|
||||
if(item.data.type === 'classifier'){
|
||||
aiText = "正在分类";
|
||||
}
|
||||
if(item.data.type === 'code'){
|
||||
aiText = "正在实施代码运行操作";
|
||||
}
|
||||
if(item.data.type === 'subflow'){
|
||||
aiText = "正在运行子流程";
|
||||
}
|
||||
if(item.data.type === 'enhanceJava'){
|
||||
aiText = "正在执行java增强";
|
||||
}
|
||||
if(item.data.type === 'http'){
|
||||
aiText = "正在发送http请求";
|
||||
}
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: aiText,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
|
||||
|
||||
else if (item.event === 'NODE_FINISHED') {
|
||||
if(!item.data || item.data.type !== 'end'){
|
||||
if(item.data.type === 'knowledge'){
|
||||
const id = item.data.id;
|
||||
const data = item.data.outputs[id + ".data"]
|
||||
knowList.value.push(data)
|
||||
//更新聊天信息
|
||||
updateChatSome(uuid.value, chatData.value.length - 1, {referenceKnowledge: knowList.value})
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!returnText){
|
||||
returnText = text;
|
||||
}
|
||||
return { returnText, conversationId };
|
||||
}
|
||||
|
||||
@ -607,7 +689,7 @@
|
||||
const uploadUrlList = ref<any>([]);
|
||||
//文件集合
|
||||
const fileInfoList = ref<any>([]);
|
||||
|
||||
|
||||
/**
|
||||
* 文件上传回调事件
|
||||
* @param info
|
||||
@ -615,8 +697,9 @@
|
||||
function handleChange(info) {
|
||||
let { fileList, file } = info;
|
||||
fileInfoList.value = fileList;
|
||||
if (file.status === 'error') {
|
||||
if (file.status === 'error' || (file.response && file.response.code == 500)) {
|
||||
message.error(file.response?.message || `${file.name} 上传失败,请查看服务端日志`);
|
||||
return;
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
uploadUrlList.value.push(file.response.message);
|
||||
@ -625,7 +708,7 @@
|
||||
|
||||
/**
|
||||
* 获取图片地址
|
||||
*
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
function getImage(url) {
|
||||
@ -643,6 +726,10 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(uploadUrlList.value && uploadUrlList.value.length > 2){
|
||||
message.warning("最多只能上传三张!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -665,7 +752,85 @@
|
||||
let imageList = [getImage(url)];
|
||||
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
* @param str
|
||||
* @param maxLength
|
||||
*/
|
||||
function truncateString(str, maxLength) {
|
||||
if (str.length <= maxLength){
|
||||
return str;
|
||||
}
|
||||
let chineseCount = 0;
|
||||
let englishCount = 0;
|
||||
let digitCount = 0;
|
||||
let result = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i];
|
||||
if (/[\u4e00-\u9fa5]/.test(char)) { // 判断是否为汉字
|
||||
chineseCount++;
|
||||
} else if (/[a-zA-Z]/.test(char)) { // 判断是否为英文字母
|
||||
englishCount++;
|
||||
} else if (/\d/.test(char)) { // 判断是否为数字
|
||||
digitCount++;
|
||||
}
|
||||
if (chineseCount + englishCount / 2 + digitCount / 2 > maxLength) {
|
||||
break;
|
||||
}
|
||||
result += char;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 粘贴事件
|
||||
* @param event
|
||||
*/
|
||||
function paste(event) {
|
||||
if(uploadUrlList.value && uploadUrlList.value.length > 2){
|
||||
message.warning("最多只能上传三张!");
|
||||
return;
|
||||
}
|
||||
const items = (event.clipboardData || window.clipboardData).items;
|
||||
if (!items || items.length === 0){
|
||||
//说明浏览器不支持复制图片
|
||||
message.error('当前浏览器不支持本地打开图片!');
|
||||
return;
|
||||
}
|
||||
let image = null;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
image = items[i].getAsFile();
|
||||
handleUploadImage(image);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 粘贴图片
|
||||
* @param image
|
||||
*/
|
||||
async function handleUploadImage(image) {
|
||||
const isReturn = (fileInfo) => {
|
||||
try {
|
||||
if (fileInfo.code === 0) {
|
||||
let { message } = fileInfo;
|
||||
uploadUrlList.value.push(message);
|
||||
fileInfoList.value.push(image);
|
||||
} else if (fileInfo.code === 500 || fileInfo.code === 510) {
|
||||
message.error(fileInfo.message || `${image.name} 导入失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('导入的数据异常', error);
|
||||
message.error(`${image.name} 导入失败`);
|
||||
}
|
||||
};
|
||||
await defHttp.uploadFile({ url: "/airag/chat/upload" }, { file: image }, { success: isReturn });
|
||||
}
|
||||
|
||||
//监听开场白
|
||||
watch(
|
||||
() => props.prologue,
|
||||
@ -676,8 +841,8 @@
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
);
|
||||
|
||||
);
|
||||
|
||||
//监听开场白预制问题
|
||||
watch(
|
||||
() => props.presetQuestion,
|
||||
@ -698,7 +863,7 @@
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
|
||||
//监听历史信息
|
||||
watch(
|
||||
() => props.historyData,
|
||||
@ -840,6 +1005,14 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
.header-advertisint{
|
||||
display:flex;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.chat-textarea{
|
||||
display: flex;
|
||||
@ -850,7 +1023,7 @@
|
||||
border-width: 1px;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
border-color: rgba(68,83,130,0.2);
|
||||
border-color: #d2d7e5;
|
||||
.textarea-top{
|
||||
border-bottom: 1px solid #f0f0f5;
|
||||
padding: 12px 28px;
|
||||
@ -881,10 +1054,10 @@
|
||||
}
|
||||
}
|
||||
.chat-textarea:hover{
|
||||
border-color: rgba(59,130,246,0.5)
|
||||
border-color: #9dc1fb;
|
||||
}
|
||||
.textarea-active{
|
||||
border-color: rgba(59,130,246,0.5) !important;
|
||||
border-color: #98bdfa !important;
|
||||
}
|
||||
:deep(.ant-divider-vertical){
|
||||
margin: 0 2px;
|
||||
@ -899,7 +1072,7 @@
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
margin-left: 44px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
@ -908,6 +1081,24 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
//手机下的样式 平板不需要调整
|
||||
.footer{
|
||||
padding: 0;
|
||||
.bottomArea{
|
||||
.delBtn{
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chatWrap{
|
||||
padding: 10px 10px 10px 0;
|
||||
}
|
||||
.main .chatContentArea{
|
||||
padding: 10px 0 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.ai-chat-modal{
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']">
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
|
||||
<div class="avatar">
|
||||
<img v-if="inversion === 'user'" :src="avatar()" />
|
||||
<img v-else :src="getAiImg()">
|
||||
<img v-else :src="getAiImg()" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="date">
|
||||
@ -14,8 +14,24 @@
|
||||
<img :src="getImageUrl(item)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msgArea">
|
||||
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
|
||||
<div v-if="inversion === 'ai' && retrievalText && loading" class="retrieval">
|
||||
{{retrievalText}}
|
||||
</div>
|
||||
<div v-if="inversion === 'ai' && isCard" class="card">
|
||||
<a-row>
|
||||
<a-col :xl="6" :lg="8" :md="10" :sm="24" style="flex:1" v-for="item in getCardList()">
|
||||
<a-card class="ai-card" @click="aiCardHandleClick(item.linkUrl)">
|
||||
<div class="ai-card-title">{{item.productName}}</div>
|
||||
<div class="ai-card-img">
|
||||
<img :src="item.productImage">
|
||||
</div>
|
||||
<span class="ai-card-desc">{{item.descr}}</span>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div class="msgArea" v-if="!isCard">
|
||||
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading" :referenceKnowledge="referenceKnowledge"></chatText>
|
||||
</div>
|
||||
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
|
||||
<span>{{item.descr}}</span>
|
||||
@ -26,13 +42,31 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import chatText from './chatText.vue';
|
||||
import defaultAvatar from '/@/assets/images/ai/avatar.jpg';
|
||||
import defaultAvatar from "@/assets/images/ai/avatar.jpg";
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import defaultImg from '../img/ailogo.png';
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images']);
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images','retrievalText', 'referenceKnowledge']);
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { createImgPreview } from "@/components/Preview";
|
||||
import { computed } from "vue";
|
||||
|
||||
const getText = computed(()=>{
|
||||
let text = props.text || props.retrievalText;
|
||||
if(text){
|
||||
text = text.trim();
|
||||
}
|
||||
return text;
|
||||
})
|
||||
|
||||
const isCard = computed(() => {
|
||||
let text = props.text;
|
||||
if (text && text.indexOf('::card::') != -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
const avatar = () => {
|
||||
return getFileAccessHttpUrl(userInfo?.avatar) || defaultAvatar;
|
||||
@ -44,7 +78,7 @@
|
||||
|
||||
/**
|
||||
* 预设问题点击事件
|
||||
*
|
||||
*
|
||||
*/
|
||||
function presetQuestionClick(descr) {
|
||||
emit("send",descr)
|
||||
@ -52,7 +86,7 @@
|
||||
|
||||
/**
|
||||
* 获取图片
|
||||
*
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function getImageUrl(item) {
|
||||
@ -77,7 +111,28 @@
|
||||
let imageList = [getImageUrl(url)];
|
||||
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取卡片列表
|
||||
*/
|
||||
function getCardList() {
|
||||
let text = props.text;
|
||||
let card = text.replace('::card::', '').replace(/\s+/g, '');
|
||||
try {
|
||||
return JSON.parse(card);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ai卡片点击事件
|
||||
* @param url
|
||||
*/
|
||||
function aiCardHandleClick(url){
|
||||
window.open(url,'_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@ -112,6 +167,7 @@
|
||||
}
|
||||
}
|
||||
.content {
|
||||
width: 90%;
|
||||
.date {
|
||||
color: #b4bbc4;
|
||||
font-size: 0.75rem;
|
||||
@ -134,14 +190,15 @@
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
}
|
||||
|
||||
|
||||
.images{
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: end;
|
||||
.image{
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
@ -154,4 +211,76 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.retrieval,
|
||||
.card {
|
||||
background-color: #f4f6f8;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.retrieval:after{
|
||||
animation: blink 1s steps(5, start) infinite;
|
||||
color: #000;
|
||||
content: '_';
|
||||
font-weight: 700;
|
||||
margin-left: 3px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.card{
|
||||
width: 100%;
|
||||
background-color: unset;
|
||||
}
|
||||
.ai-card{
|
||||
width: 98%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
.ai-card-title{
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: #191919;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
.ai-card-img{
|
||||
margin-top: 10px;
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
.ai-card-desc{
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0;
|
||||
white-space: pre-line;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
color: #666f;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.content{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
<template>
|
||||
<div v-if="text != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : 'chatgpt']" ref="textRef">
|
||||
<div v-if="inversion != 'user'">
|
||||
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
|
||||
<div v-if="inversion != 'user'" :style="{ width: getIsMobile? screenWidth : 'auto' }">
|
||||
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" :style="{color:error?'#FF4444 !important':''}" v-html="text" />
|
||||
<template v-if="showRefKnow">
|
||||
<a-divider orientation="left">引用</a-divider>
|
||||
<template v-for="(item, idx) of referenceKnowledge" :key="idx">
|
||||
<a-tooltip :title="item.substring(0, 800)">
|
||||
<a-tag >
|
||||
<a-space>
|
||||
<img :src="knowledgePng" width="16" height="16"/>
|
||||
<div style="max-width: 240px; overflow: hidden;white-space: nowrap;text-overflow: ellipsis;">
|
||||
{{ item }}
|
||||
</div>
|
||||
</a-space>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="msg" v-text="text" />
|
||||
<div v-else class="msg" v-html="text" />
|
||||
</div>
|
||||
<ImageViewer v-if="amplifyImage" :imageUrl="imageUrl" @hide="pictureHide"></ImageViewer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -16,11 +32,21 @@
|
||||
import './style/github-markdown.less';
|
||||
import './style/highlight.less';
|
||||
import './style/style.less';
|
||||
import ImageViewer from '@/views/super/airag/aiapp/chat/components/ImageViewer.vue';
|
||||
import { useAppInject } from "@/hooks/web/useAppInject";
|
||||
import { useGlobSetting } from "@/hooks/setting";
|
||||
import knowledgePng from '../../aiknowledge/icon/knowledge.png'
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
|
||||
/**
|
||||
* 屏幕宽度
|
||||
*/
|
||||
const screenWidth = ref<string>();
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading', 'referenceKnowledge']);
|
||||
const textRef = ref();
|
||||
const mdi = new MarkdownIt({
|
||||
html: false,
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language));
|
||||
@ -36,11 +62,49 @@
|
||||
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
|
||||
|
||||
const text = computed(() => {
|
||||
const value = props.text ?? '';
|
||||
if (props.inversion != 'user') return mdi.render(value);
|
||||
return value;
|
||||
let value = props.text ?? '';
|
||||
if (props.inversion != 'user'){
|
||||
value = replaceImageWith(value);
|
||||
value = replaceDomainUrl(value);
|
||||
return mdi.render(value);
|
||||
}
|
||||
return value.replace("\n","<br>");
|
||||
});
|
||||
|
||||
// 是否显示引用知识库
|
||||
const showRefKnow = computed(() => {
|
||||
const {loading, referenceKnowledge} = props
|
||||
if (loading) {
|
||||
return false;
|
||||
}
|
||||
return Array.isArray(referenceKnowledge) && referenceKnowledge.length > 0;
|
||||
})
|
||||
|
||||
//替换图片宽度
|
||||
const replaceImageWith = markdownContent => {
|
||||
// 支持图片设置width的写法 
|
||||
const regex = /!\[([^\]]*)\]\(([^)]+)=([0-9]+)\)/g;
|
||||
return markdownContent.replace(regex, (match, alt, src, width) => {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
src = src.replace(reg,domainUrl);
|
||||
return `<div><img src='${src}' alt='${alt}' width='${width}' /></div>`;
|
||||
});
|
||||
};
|
||||
const { domainUrl } = useGlobSetting();
|
||||
//替换domainURL
|
||||
const replaceDomainUrl = markdownContent => {
|
||||
const regex = /!\[([^\]]*)\]\(.*?#\s*{\s*domainURL\s*}.*?\)/g;
|
||||
return markdownContent.replace(regex, (match) => {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
return match.replace(reg,domainUrl);
|
||||
})
|
||||
}
|
||||
|
||||
//是否放大图片
|
||||
const amplifyImage = ref<boolean>(false);
|
||||
//图片地址
|
||||
const imageUrl = ref<string>('');
|
||||
|
||||
function highlightBlock(str: string, lang?: string) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
|
||||
}
|
||||
@ -72,16 +136,74 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加图片点击事件
|
||||
*/
|
||||
function addImageClickEvent() {
|
||||
if (textRef.value) {
|
||||
const image = textRef.value.querySelectorAll('img');
|
||||
image.forEach((img) => {
|
||||
img.addEventListener('click', () => {
|
||||
imageUrl.value = img.src;
|
||||
amplifyImage.value = true;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移出图片点击事件
|
||||
*/
|
||||
function removeImageClickEvent(){
|
||||
if (textRef.value) {
|
||||
const image = textRef.value.querySelectorAll('img');
|
||||
image.forEach((img) => {
|
||||
img.removeEventListener('click', () => { })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片隐藏
|
||||
*/
|
||||
function pictureHide(){
|
||||
amplifyImage.value = false;
|
||||
imageUrl.value = ""
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置markdown body整体宽度
|
||||
*/
|
||||
function setMarkdownBodyWidth() {
|
||||
//平板
|
||||
console.log("window.innerWidth::",window.innerWidth)
|
||||
if(window.innerWidth>600 && window.innerWidth<1024){
|
||||
screenWidth.value = window.innerWidth - 120 + 'px';
|
||||
}else if(window.innerWidth < 600){
|
||||
//手机
|
||||
screenWidth.value = window.innerWidth - 60 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addCopyEvents();
|
||||
addImageClickEvent();
|
||||
setMarkdownBodyWidth();
|
||||
window.addEventListener('resize', setMarkdownBodyWidth);
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
addCopyEvents();
|
||||
addImageClickEvent();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeCopyEvents();
|
||||
removeImageClickEvent();
|
||||
window.removeEventListener('resize', setMarkdownBodyWidth);
|
||||
});
|
||||
|
||||
function copyToClip(text: string) {
|
||||
@ -111,6 +233,18 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: linear-gradient(135deg, #FF4444, #FF914D) !important;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.self {
|
||||
// background-color: #d2f9d1;
|
||||
background-color: @primary-color;
|
||||
@ -125,4 +259,11 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
//手机和平板下的样式
|
||||
.textWrap{
|
||||
margin-left: -40px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
<!--image放大封装-->
|
||||
<template>
|
||||
<div class="amplify-image">
|
||||
<div class="img-preview-content" @click="hideImageClick" @mousewheel="handlePicMousewheel">
|
||||
<img :src="imageUrl" ref="imageRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//图片地址
|
||||
import {onMounted, ref, unref} from 'vue';
|
||||
const props = defineProps(['imageUrl']);
|
||||
const emit = defineEmits(['register', 'hide']);
|
||||
//图片的ref
|
||||
const imageRef = ref();
|
||||
//缩放级别
|
||||
const scale = ref<number>(1);
|
||||
|
||||
/**
|
||||
* 隐藏图片
|
||||
*/
|
||||
function hideImageClick() {
|
||||
scale.value = 1;
|
||||
emit('hide')
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标滑轮滚动
|
||||
* @param event
|
||||
*/
|
||||
function handlePicMousewheel(event) {
|
||||
event.preventDefault();
|
||||
// 判断是放大还是缩小
|
||||
const delta = event.deltaY > 0 ? -1 : 1;
|
||||
const scaleStep = 0.1;
|
||||
// 更新缩放级别
|
||||
scale.value = scale.value + delta * scaleStep
|
||||
imageRef.value.style.transform = `scale(${unref(scale)})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.amplify-image{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
.img-preview-content{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
touch-action: none;
|
||||
-webkit-user-drag: none;
|
||||
img{
|
||||
transition: transform 0.3s;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,38 +2,40 @@
|
||||
(function () {
|
||||
let widgetInstance = null;
|
||||
const defaultConfig = {
|
||||
// ֧<EFBFBD><EFBFBD>'top-left'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 'top-right'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 'bottom-left'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 'bottom-right'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 支持'top-left'左上, 'top-right'右上, 'bottom-left'左下, 'bottom-right'右下
|
||||
iconPosition: 'bottom-right',
|
||||
//ͼ<EFBFBD><EFBFBD><EFBFBD>Ĵ<EFBFBD>С
|
||||
iconSize: '30px',
|
||||
//ͼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
|
||||
//图标的大小
|
||||
iconSize: '45px',
|
||||
//图标的颜色
|
||||
iconColor: '#155eef',
|
||||
//<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
//必填不允许修改
|
||||
appId: '',
|
||||
//<EFBFBD><EFBFBD><EFBFBD>쵯<EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><EFBFBD><EFBFBD>
|
||||
//聊天弹窗的宽度
|
||||
chatWidth: '800px',
|
||||
//<EFBFBD><EFBFBD><EFBFBD>쵯<EFBFBD><EFBFBD><EFBFBD>ĸ߶<EFBFBD>
|
||||
//聊天弹窗的高度
|
||||
chatHeight: '700px',
|
||||
};
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD><EFBFBD><EFBFBD>aiͼ<EFBFBD><EFBFBD>
|
||||
* 创建ai图标
|
||||
* @param config
|
||||
*/
|
||||
function createAiChat(config) {
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD>ȷ<EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>
|
||||
// 单例模式,确保只存在一个实例
|
||||
if (widgetInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// <EFBFBD>ϲ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 合并配置
|
||||
const finalConfig = { ...defaultConfig, ...config };
|
||||
|
||||
if (!finalConfig.appId) {
|
||||
console.error('appIdΪ<EFBFBD>գ<EFBFBD>');
|
||||
console.error('appId为空!');
|
||||
return;
|
||||
}
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
let body = document.body;
|
||||
body.style.margin = "0";
|
||||
// 创建容器
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
@ -41,39 +43,50 @@
|
||||
${getPositionStyles(finalConfig.iconPosition)}
|
||||
cursor: pointer;
|
||||
`;
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ<EFBFBD><EFBFBD>
|
||||
// 创建图标
|
||||
const icon = document.createElement('div');
|
||||
icon.style.cssText = `
|
||||
width: ${finalConfig.iconSize};
|
||||
height: ${finalConfig.iconSize};
|
||||
background-color: ${finalConfig.iconColor};
|
||||
border-radius: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
|
||||
padding: 12px;
|
||||
box-shadow: #cccccc 0 4px 8px 0;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
icon.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" d="M573 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40m-280 0c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path><path fill="currentColor" d="M894 345c-48.1-66-115.3-110.1-189-130v.1c-17.1-19-36.4-36.5-58-52.1c-163.7-119-393.5-82.7-513 81c-96.3 133-92.2 311.9 6 439l.8 132.6c0 3.2.5 6.4 1.5 9.4c5.3 16.9 23.3 26.2 40.1 20.9L309 806c33.5 11.9 68.1 18.7 102.5 20.6l-.5.4c89.1 64.9 205.9 84.4 313 49l127.1 41.4c3.2 1 6.5 1.6 9.9 1.6c17.7 0 32-14.3 32-32V753c88.1-119.6 90.4-284.9 1-408M323 735l-12-5l-99 31l-1-104l-8-9c-84.6-103.2-90.2-251.9-11-361c96.4-132.2 281.2-161.4 413-66c132.2 96.1 161.5 280.6 66 412c-80.1 109.9-223.5 150.5-348 102m505-17l-8 10l1 104l-98-33l-12 5c-56 20.8-115.7 22.5-171 7l-.2-.1C613.7 788.2 680.7 742.2 729 676c76.4-105.3 88.8-237.6 44.4-350.4l.6.4c23 16.5 44.1 37.1 62 62c72.6 99.6 68.5 235.2-8 330"></path><path fill="currentColor" d="M433 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path></svg>';
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>iframe<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 创建iframe容器
|
||||
const iframeContainer = document.createElement('div');
|
||||
let right = finalConfig.chatWidth === '100%' ? '0' : '10px';
|
||||
let bottom = finalConfig.chatHeight === '100%' ? '0' : '10px';
|
||||
let chatWidth = finalConfig.chatWidth;
|
||||
let chatHeight = finalConfig.chatHeight;
|
||||
if(isMobileDevice()){
|
||||
chatWidth = "100%";
|
||||
chatHeight = "100%";
|
||||
right = '0';
|
||||
bottom = '0';
|
||||
}
|
||||
iframeContainer.style.cssText = `
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: ${finalConfig.chatWidth} !important;
|
||||
height: ${finalConfig.chatHeight} !important;
|
||||
position: fixed;
|
||||
right: ${right};
|
||||
bottom: ${bottom};
|
||||
width: ${chatWidth} !important;
|
||||
height: ${chatHeight} !important;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0 20px #cccccc;
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
`;
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>iframe
|
||||
// 创建iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
@ -83,15 +96,23 @@
|
||||
`;
|
||||
|
||||
iframe.id = 'ai-app-chat-document';
|
||||
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId;
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD>رհ<D8B1>ť
|
||||
//update-begin---author:wangshuai---date:2025-04-25---for:【QQYUN-12159】【AI 广告位】让需要自建AI知识库的用户知道如何通过敲敲云搭建自己的AI知识库---
|
||||
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId + "?source=chatJs";
|
||||
//update-end---author:wangshuai---date:2025-04-25---for:【QQYUN-12159】【AI 广告位】让需要自建AI知识库的用户知道如何通过敲敲云搭建自己的AI知识库---
|
||||
let iconRight = finalConfig.chatWidth === '100%'?'0':'-6px';
|
||||
let iconTop = finalConfig.chatWidth === '100%'?'0':'-9px';
|
||||
if(isMobileDevice()){
|
||||
iconRight = '2px';
|
||||
iconTop = '2px';
|
||||
}
|
||||
// 创建关闭按钮
|
||||
const closeBtn = document.createElement('div');
|
||||
closeBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" fill-rule="evenodd" d="M799.855 166.312c.023.007.043.018.084.059l57.69 57.69c.041.041.052.06.059.084a.1.1 0 0 1 0 .069c-.007.023-.018.042-.059.083L569.926 512l287.703 287.703c.041.04.052.06.059.083a.12.12 0 0 1 0 .07c-.007.022-.018.042-.059.083l-57.69 57.69c-.041.041-.06.052-.084.059a.1.1 0 0 1-.069 0c-.023-.007-.042-.018-.083-.059L512 569.926L224.297 857.629c-.04.041-.06.052-.083.059a.12.12 0 0 1-.07 0c-.022-.007-.042-.018-.083-.059l-57.69-57.69c-.041-.041-.052-.06-.059-.084a.1.1 0 0 1 0-.069c.007-.023.018-.042.059-.083L454.073 512L166.371 224.297c-.041-.04-.052-.06-.059-.083a.12.12 0 0 1 0-.07c.007-.022.018-.042.059-.083l57.69-57.69c.041-.041.06-.052.084-.059a.1.1 0 0 1 .069 0c.023.007.042.018.083.059L512 454.073l287.703-287.702c.04-.041.06-.052.083-.059a.12.12 0 0 1 .07 0Z"></path></svg>';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -10px;
|
||||
margin-top: ${iconTop};
|
||||
right: ${iconRight};
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
width: 25px;
|
||||
@ -100,17 +121,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 2px 5px #cccccc;
|
||||
`;
|
||||
|
||||
// <EFBFBD><EFBFBD>װԪ<EFBFBD><EFBFBD>
|
||||
// 组装元素
|
||||
iframeContainer.appendChild(closeBtn);
|
||||
iframeContainer.appendChild(iframe);
|
||||
document.body.appendChild(iframeContainer);
|
||||
container.appendChild(icon);
|
||||
document.body.appendChild(container);
|
||||
|
||||
// <EFBFBD>¼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 事件监听
|
||||
icon.addEventListener('click', () => {
|
||||
iframeContainer.style.display = 'block';
|
||||
});
|
||||
@ -119,7 +140,7 @@
|
||||
iframeContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 保存实例引用
|
||||
widgetInstance = {
|
||||
remove: () => {
|
||||
container.remove();
|
||||
@ -129,7 +150,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD>ȡλ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
* 获取位置信息
|
||||
*
|
||||
* @param position
|
||||
* @returns {*|string}
|
||||
@ -145,7 +166,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD>ȡsrc<EFBFBD><EFBFBD>ַ
|
||||
* 获取src地址
|
||||
*/
|
||||
function getIframeSrc(finalConfig) {
|
||||
const specificScript = document.getElementById("e7e007dd52f67fe36365eff636bbffbd");
|
||||
@ -154,6 +175,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// <20><>¶ȫ<C2B6>ַ<EFBFBD><D6B7><EFBFBD>
|
||||
/**
|
||||
* 判断是否为手机
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isMobileDevice() {
|
||||
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// 暴露全局方法
|
||||
window.createAiChat = createAiChat;
|
||||
})();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="presetQuestion-wrap">
|
||||
<svg
|
||||
<!-- <svg
|
||||
v-if="btnShow"
|
||||
class="leftBtn"
|
||||
:class="leftBtnStatus"
|
||||
@ -16,7 +16,7 @@
|
||||
fill="currentColor"
|
||||
p-id="5071"
|
||||
></path>
|
||||
</svg>
|
||||
</svg>-->
|
||||
<div class="content">
|
||||
<ul ref="ulElemRef">
|
||||
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item.descr)">
|
||||
@ -28,7 +28,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<svg
|
||||
<!-- <svg
|
||||
v-if="btnShow"
|
||||
class="rightBtn"
|
||||
:class="rightBtnStatus"
|
||||
@ -44,7 +44,7 @@
|
||||
fill="currentColor"
|
||||
p-id="5071"
|
||||
></path>
|
||||
</svg>
|
||||
</svg>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -135,7 +135,8 @@ import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
/* 隐藏所有滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@ -143,8 +144,15 @@ import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
ul:hover {
|
||||
&::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
|
||||
@ -71,6 +71,13 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="left-footer" v-if="source!='chatJs'">
|
||||
AI客服由
|
||||
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
|
||||
敲敲云
|
||||
</a>
|
||||
提供
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -80,7 +87,7 @@
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
|
||||
import defaultImg from '../img/ailogo.png';
|
||||
const props = defineProps(['dataSource', 'appData']);
|
||||
const props = defineProps(['dataSource', 'appData','source']);
|
||||
const emit = defineEmits(['save', 'click', 'reloadRight', 'prologue']);
|
||||
const inputRef = ref(null);
|
||||
const router = useRouter();
|
||||
@ -149,8 +156,7 @@
|
||||
emit('click', props.dataSource.history[0].title, findIndex);
|
||||
} else {
|
||||
// 删没了(删除了最后一个)
|
||||
props.dataSource.active = null;
|
||||
emit('click', "", -1);
|
||||
handleCreate();
|
||||
}
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11560】新建聊天内容为空,无法删除---
|
||||
@ -216,6 +222,7 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-bottom: 20px;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@ -285,6 +292,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
&.ant-input {
|
||||
margin-right: 20px;
|
||||
}
|
||||
@ -316,4 +326,13 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.left-footer{
|
||||
display:flex;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -38,10 +38,10 @@ html.dark {
|
||||
--color-canvas-subtle: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||
--color-neutral-muted: #bfc4cc;
|
||||
--color-accent-fg: #58a6ff;
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||
--color-attention-subtle: #ece6d9;
|
||||
--color-danger-fg: #f85149;
|
||||
}
|
||||
}
|
||||
@ -86,7 +86,7 @@ html {
|
||||
--color-canvas-subtle: #f6f8fa;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-border-muted: hsla(210, 18%, 87%, 1);
|
||||
--color-neutral-muted: rgba(175, 184, 193, 0.2);
|
||||
--color-neutral-muted: #e7ebf2;
|
||||
--color-accent-fg: #0969da;
|
||||
--color-accent-emphasis: #0969da;
|
||||
--color-attention-subtle: #fff8c5;
|
||||
|
||||
Reference in New Issue
Block a user