AIGC应用平台+知识库模块

This commit is contained in:
JEECG
2025-04-07 14:08:27 +08:00
parent 45e9b03e2d
commit 507289ff6c
52 changed files with 9147 additions and 161 deletions

View File

@ -0,0 +1,372 @@
<template>
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
<template v-if="dataSource">
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
<div class="content">
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
</div>
<div class="toggle-btn" @click="handleToggle">
<span class="icon">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="currentColor"
></path>
</svg>
</span>
</div>
</div>
<div class="rightArea" :class="[expand ? 'expand' : 'shrink']">
<chat
url="/airag/chat/send"
v-if="uuid && chatVisible"
:uuid="uuid"
:historyData="chatData"
type="view"
@save="handleSave"
:formState="appData"
:prologue="prologue"
:presetQuestion="presetQuestion"
@reload-message-title="reloadMessageTitle"
:chatTitle="chatTitle"
:quickCommandData="quickCommandData"
></chat>
</div>
</template>
<Spin v-else :spinning="true"></Spin>
</div>
</template>
<script setup lang="ts">
import slide from './slide.vue';
import chat from './chat.vue';
import { Spin } from 'ant-design-vue';
import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue';
import { useUserStore } from '/@/store/modules/user';
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
import { defHttp } from '/@/utils/http/axios';
import { useRouter } from 'vue-router';
const router = useRouter();
const userId = useUserStore().getUserInfo?.id;
const localKey = JEECG_CHAT_KEY + userId;
let timer: any = null;
let unwatch01: any = null;
const dataSource = ref<any>({});
const uuid = ref<string>('');
const chatData = ref<any>([]);
const expand = ref<any>(true);
const chatVisible = ref(true);
const chatContainerRef = ref<any>(null);
const chatContainerStyle = ref({});
//左侧聊天信息
const chatTitle = ref<string>('');
//左侧聊天点击的坐标
const chatActiveKey = ref<number>(0);
//预置开场白
const presetQuestion = ref<string>('');
const handleToggle = () => {
expand.value = !expand.value;
};
//应用id
const appId = ref<string>('');
//应用数据
const appData = ref<any>({});
//开场白
const prologue = ref<string>('');
//快捷指令
const quickCommandData = ref<any>([]);
const priming = () => {
dataSource.value = {
active: '1002',
usingContext: true,
history: [{ id: '1002', title: '新建聊天', isEdit: false, disabled: true }],
};
chatTitle.value = '新建聊天';
chatActiveKey.value = 0;
};
const handleSave = () => {
// 删除标签或清空内容之后的保存
//save(dataSource.value);
setTimeout(() => {
// 删除标签或清空内容也会触发watch保存此时不需watch保存需清除
//clearTimeout(timer);
}, 50);
};
// 监听dataSource变化执行操作
const execute = () => {
unwatch01 = watch(
() => dataSource.value.active,
(value) => {
if (value) {
if (value == '1002') {
uuid.value = '1002';
chatData.value = [];
chatTitle.value = "新建聊天";
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
return;
}
//update-begin---author:wangshuai---date:2025-03-14---for:【QQYUN-11421】聊天删除会话后聊天切换到新的会话但是聊天标题没有变---
let values = dataSource.value.history.filter((item) => item.id === value);
if(values && values.length>0){
chatTitle.value = values[0]?.title
}
//update-end---author:wangshuai---date:2025-03-14---for:【QQYUN-11421】聊天删除会话后聊天切换到新的会话但是聊天标题没有变---
//根据选中的id查询聊天内容
let params = { conversationId: value };
uuid.value = value;
defHttp.get({ url: '/airag/chat/messages', params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
chatData.value = res.result;
} else {
chatData.value = [];
}
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
});
}else{
chatData.value = [];
chatTitle.value = "";
}
},
{ immediate: true }
);
};
/**
* 初始化聊天信息
* @param appId
*/
function initChartData(appId = '') {
defHttp
.get(
{
url: '/airag/chat/conversations',
params: { appId: appId },
},
{ isTransformResponse: false }
)
.then((res) => {
if (res.success && res.result && res.result.length > 0) {
dataSource.value.history = res.result;
dataSource.value.active = res.result[0].id;
chatTitle.value = res.result[0].title;
chatActiveKey.value = 0;
} else {
priming();
}
!unwatch01 && execute();
})
.catch(() => {
priming();
});
}
onMounted(() => {
let params: any = router.currentRoute.value.params;
if (params.appId) {
appId.value = params.appId;
getApplicationData(params.appId);
initChartData(params.appId);
} else {
initChartData();
quickCommandData.value = [
{ name: '请介绍一下JeecgBoot', descr: "请介绍一下JeecgBoot" },
{ name: 'JEECG有哪些优势', descr: "JEECG有哪些优势" },
{ name: 'JEECG可以做哪些事情', descr: "JEECG可以做哪些事情" },];
}
});
onUnmounted(() => {
chatData.value = [];
chatTitle.value = "";
prologue.value = ""
presetQuestion.value = "";
quickCommandData.value = [];
})
/**
* 获取应用id
*
* @param appId
*/
async function getApplicationData(appId) {
await defHttp
.get(
{
url: '/airag/app/queryById',
params: { id: appId },
},
{ isTransformResponse: false }
)
.then((res) => {
if (res.success) {
appData.value = res.result;
if (res.result && res.result.prologue) {
prologue.value = res.result.prologue;
}
if (res.result && res.result.quickCommand) {
quickCommandData.value = JSON.parse(res.result.quickCommand);
}
if (res.result && res.result.presetQuestion) {
presetQuestion.value = res.result.presetQuestion;
}
} else {
appData.value = {};
}
});
}
/**
* 左侧消息列表点击事件
* @param title
* @param index
*/
function handleChatClick(title, index) {
chatTitle.value = title;
chatActiveKey.value = index;
}
/**
* 重新加载标题消息
* @param text
*/
function reloadMessageTitle(text) {
let title = dataSource.value.history[chatActiveKey.value].title;
if(title === '新建聊天'){
dataSource.value.history[chatActiveKey.value].title = text;
dataSource.value.history[chatActiveKey.value]['disabled'] = false;
}
}
/**
* 初始化聊天用于icon点击
*/
function initChat(value) {
appId.value = value;
getApplicationData(value);
initChartData(value);
}
defineExpose({
initChat
})
onUnmounted(() => {
unwatch01 && unwatch01();
});
watch(
() => chatContainerRef.value,
() => {
if(chatContainerRef.value.offsetHeight){
chatContainerStyle.value = { height: `${chatContainerRef.value.offsetHeight} px` };
}
}
);
</script>
<style scoped lang="less">
@width: 260px;
.chat-container {
height: 100%;
width: 100%;
position: relative;
background: white;
display: flex;
overflow: hidden;
z-index: 999;
border: 1px solid #eeeeee;
:deep(.ant-spin) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.leftArea {
width: @width;
transition: 0.3s left;
position: absolute;
left: 0;
height: 100%;
.content {
width: 100%;
height: 100%;
overflow: hidden;
}
&.shrink {
left: -@width;
.toggle-btn {
.icon {
transform: rotate(0deg);
}
}
}
.toggle-btn {
transition:
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
width: 24px;
height: 24px;
position: absolute;
top: 50%;
right: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
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);
transform: translateX(50%) translateY(-50%);
z-index: 1;
}
.icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: rotate(180deg);
font-size: 18px;
height: 18px;
svg {
height: 1em;
width: 1em;
vertical-align: top;
}
}
}
.rightArea {
margin-left: @width;
transition: 0.3s margin-left;
&.shrink {
margin-left: 0;
}
flex: 1;
min-width: 0;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="footer">
<div v-if="!showChat" class="footer-icon" @click="chatClick">
<Icon icon="ant-design:comment-outlined" size="22"></Icon>
</div>
<div v-if="showChat" class="footer-close-icon" @click="chatClick">
<Icon icon="ant-design:close-outlined" size="20"></Icon>
</div>
<div v-if="showChat" class="ai-chat">
<AiChat ref="aiChatRef"></AiChat>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import AiChat from './AiChat.vue';
import { useRouter } from 'vue-router';
//aiChat<61><74>ref
const aiChatRef = ref();
//Ӧ<><D3A6>id
const appId = ref<string>('');
//<2F>Ƿ<EFBFBD><C7B7><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD>
const showChat = ref<any>(false);
const router = useRouter();
//<2F>ж<EFBFBD><D0B6>Ƿ<EFBFBD>Ϊ<EFBFBD><CEAA>ʼ<EFBFBD><CABC>
const isInit = ref<boolean>(false);
/**
* chatͼ<74><CDBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>
*/
function chatClick() {
showChat.value = !showChat.value;
if(showChat.value && !isInit.value){
setTimeout(()=>{
isInit.value = true;
aiChatRef.value.initChat(appId.value);
},100)
}
}
onMounted(() => {
let params: any = router.currentRoute.value.params;
appId.value = params?.appId;
isInit.value = false;
});
</script>
<style scoped lang="less">
.footer {
position: fixed;
bottom: 16px;
right: 16px;
left: unset;
top: unset;
.footer-icon {
cursor: pointer;
background-color: #155eef;
color: white;
border-radius: 100%;
padding: 20px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
}
.footer-close-icon {
color: #0a3069;
height: 48px;
position: absolute;
right: 10px;
top: 20px;
cursor: pointer;
z-index: 9999;
}
.ai-chat {
border: 1px solid #eeeeee;
width: calc(100vh - 20px);
height: calc(100vh - 200px);
}
}
</style>

View File

@ -0,0 +1,919 @@
<template>
<div class="chatWrap">
<div class="content">
<div class="header-title" v-if="type === 'view' && headerTitle">
{{headerTitle}}
</div>
<div class="main">
<div id="scrollRef" ref="scrollRef" class="scrollArea">
<template v-if="chatData.length>0">
<div class="chatContentArea">
<chatMessage
v-for="(item, index) of chatData"
:key="index"
:date-time="item.dateTime || item.datetime"
:text="item.content"
:inversion="item.inversion || item.role"
:error="item.error"
:loading="item.loading"
:appData="appData"
:presetQuestion="item.presetQuestion"
:images = "item.images"
@send="handleOutQuestion"
></chatMessage>
</div>
</template>
</div>
</div>
<div class="footer">
<div class="topArea">
<presetQuestion @outQuestion="handleOutQuestion" :quickCommandData="quickCommandData"></presetQuestion>
</div>
<div class="bottomArea">
<a-button type="text" class="delBtn" @click="handleDelSession()">
<svg
t="1706504908534"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1584"
width="18"
height="18"
>
<path
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
fill="currentColor"
p-id="1585"
></path>
</svg>
</a-button>
<a-button v-if="type === 'view'" type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
></path>
</svg>
</a-button>
<div class="chat-textarea" :class="textareaActive?'textarea-active':''">
<div class="textarea-top" v-if="uploadUrlList && uploadUrlList.length>0">
<div v-for="(item,index) in uploadUrlList" class="top-image" :key="index">
<img :src="getImage(item)" @click="handlePreview(item)"/>
<div class="upload-icon" @click="deleteImage(index)">
<Icon icon="ant-design:close-outlined" size="12px"></Icon>
</div>
</div>
</div>
<div class="textarea-bottom">
<a-textarea
ref="inputRef"
v-model:value="prompt"
:autoSize="{ minRows: 1, maxRows: 6 }"
:placeholder="placeholder"
@pressEnter="handleEnter"
@focus="textareaActive = true"
@blur="textareaActive = false"
autofocus
:readonly="loading"
style="border-color: #ffffff !important;box-shadow:none"
>
</a-textarea>
<a-button v-if="loading" type="primary" danger @click="handleStopChat" class="stopBtn">
<svg
t="1706148514627"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5214"
width="18"
height="18"
>
<path
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
fill="currentColor"
p-id="5215"
></path>
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
</svg>
</a-button>
<a-upload
accept=".jpg,.jpeg,.png"
v-if="!loading"
name="file"
v-model:file-list="fileInfoList"
:showUploadList="false"
:headers="headers"
:beforeUpload="beforeUpload"
@change="handleChange"
:multiple="true"
:action="uploadUrl"
:max-count="3"
>
<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>
</a-button>
</a-tooltip>
</a-upload>
<a-divider v-if="!loading" type="vertical" style="border-color:#38374314"></a-divider>
<a-button
@click="
() => {
handleSubmit();
}
"
:disabled="!prompt"
class="sendBtn"
type="text"
v-if="!loading"
>
<svg
t="1706147858151"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4237"
width="1em"
height="1em"
>
<path
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
p-id="4238"
fill="currentColor"
></path>
</svg>
</a-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, watch } from 'vue';
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
import { useScroll } from './js/useScroll';
import chatMessage from './chatMessage.vue';
import presetQuestion from './presetQuestion.vue';
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { message, Modal, Tabs } from 'ant-design-vue';
import './style/github-markdown.less';
import './style/highlight.less';
import './style/github-markdown.less';
import dayjs from 'dayjs';
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";
message.config({
prefixCls: 'ai-chat-message',
});
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData']);
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>([]);
//应用数据
const appData = ref<any>({});
const usingContext = ref<any>(true);
const uuid = ref<string>(props.uuid);
const topicId = ref<string>('');
//请求id
const requestId = ref<string>('');
const conversationList = computed(() => chatData.value.filter((item) => item.inversion != 'user' && !!item.conversationOptions));
const placeholder = computed(() => {
return '来说点什么吧...Shift + Enter = 换行)';
});
//token
const headers = getHeaders();
//文本域点击事件
const textareaActive = ref<boolean>(false);
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
}
function handleSubmit() {
let message = prompt.value;
if (!message || message.trim() === '') return;
prompt.value = '';
onConversation(message);
}
const handleOutQuestion = (message) => {
onConversation(message);
};
async function onConversation(message) {
if(!props.type && props.type != 'view'){
if(appData.value.type && appData.value.type == 'chatSimple' && !appData.value.modelId) {
messageTip("请选择AI模型");
return;
}
if(appData.value.type && appData.value.type == 'chatFLow' && !appData.value.flowId) {
messageTip("请选择关联流程");
return;
}
if(!appData.value.name) {
messageTip("请填写应用名称");
return;
}
}
if (loading.value) return;
loading.value = true;
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
content: message,
images:uploadUrlList.value?uploadUrlList.value:[],
inversion: 'user',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
});
scrollToBottom();
let options: any = {};
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
if (lastContext && usingContext.value) {
options = { ...lastContext };
}
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
content: '思考中...',
loading: true,
inversion: 'ai',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
//发送消息
sendMessage(message,options);
}
onUnmounted(() => {
updateChatSome(uuid.value, chatData.value.length - 1, { loading: false });
});
const addChat = (uuid, data) => {
chatData.value.push({ ...data });
};
const updateChat = async (uuid, index, data) => {
chatData.value.splice(index, 1, data);
await scrollToBottom();
};
/**
* 顶置开场白
* @param txt
*/
const topChat = (txt) => {
let data = {
content: txt,
key: 'prologue',
loading: false,
dateTime: dayjs().format('YYYY/MM/DD HH:mm:ss'),
inversion: 'ai',
presetQuestion: props.presetQuestion ? JSON.parse(props.presetQuestion) : "",
};
if (chatData.value && chatData.value.length > 0) {
let key = chatData.value[0].key;
if (key === 'prologue') {
chatData.value[0] = { ...data };
return;
}
}
chatData.value.unshift({ ...data });
};
const updateChatSome = (uuid, index, data) => {
chatData.value[index] = { ...chatData.value[index], ...data };
};
const updateChatFail = (uuid, index, data) => {
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: data,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: null,
requestOptions: null,
});
scrollToBottom();
};
/**
* 清空会话
* @param id
*/
function handleDelSession (){
Modal.confirm({
title: '清空会话',
icon: createVNode(ExclamationCircleOutlined),
content: '是否清空会话?',
closable: true,
okText: '确定',
cancelText: '取消',
wrapClassName:'ai-chat-modal',
async onOk() {
try {
defHttp.get({
url: '/airag/chat/messages/clear/' + uuid.value,
},{ isTransformResponse: false }).then((res) => {
if(res.success){
chatData.value = [];
topicId.value = "";
if(props.prologue){
topChat(props.prologue);
}
}
})
} catch {
return console.log('Oops errors!');
}
},
});
};
// 停止响应
const handleStop = () => {
console.log('ai 聊天:::---停止响应');
if (loading.value) {
loading.value = false;
}
updateChatSome(uuid, chatData.value.length - 1, { loading: false });
};
handleStop();
/**
* 停止消息
*/
function handleStopChat() {
if(requestId.value){
//调用后端接口停止响应
defHttp.get({
url: '/airag/chat/stop/' + requestId.value,
},{ isTransformResponse: false });
}
handleStop();
}
/**
* 读取文本
* @param message
* @param options
*/
async function sendMessage(message, options) {
let param = {};
if (!props.type && props.type != 'view') {
param = {
content: message,
images: uploadUrlList.value?uploadUrlList.value:[],
topicId: topicId.value,
app: appData.value,
responseMode: 'streaming',
};
}else{
param = {
content: message,
topicId: usingContext.value?topicId.value:'',
images: uploadUrlList.value?uploadUrlList.value:[],
appId: appData.value.id,
responseMode: 'streaming',
conversationId: uuid.value === "1002"?'':uuid.value
};
if(headerTitle.value == '新建聊天'){
headerTitle.value = message.length>5?message.substring(0,5):message
}
emit("reload-message-title",message.length>5?message.substring(0,5):message)
}
uploadUrlList.value = [];
fileInfoList.value = [];
const readableStream = await defHttp.post(
{
url: props.url,
params: param,
adapter: 'fetch',
responseType: 'stream',
timeout: 5 * 60 * 1000,
},
{
isTransformResponse: false,
}
).catch((e)=>{
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
handleStop();
return;
});
const reader = readableStream.getReader();
const decoder = new TextDecoder('UTF-8');
let conversationId = '';
let buffer = '';
let text = ''; // 按 SSE 协议分割消息
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
let result = decoder.decode(value, { stream: true });
result = buffer + result;
const lines = result.split('\n\n');
for (const line of lines) {
if (line.startsWith('data:')) {
const content = line.replace('data:', '').trim();
if(!content){
continue;
}
if(!content.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
try {
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
let parse = JSON.parse(content);
await renderText(parse,conversationId,text,options).then((res)=>{
text = res.returnText;
conversationId = res.conversationId;
});
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
} catch (error) {
console.log('Error parsing update:', error);
}
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
}else{
if(!line){
continue;
}
if(!line.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
try {
let parse = JSON.parse(line);
await renderText(parse, conversationId, text, options).then((res) => {
text = res.returnText;
conversationId = res.conversationId;
});
}catch (error) {
console.log('Error parsing update:', error);
}
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
}
}
}
}
// 是否使用上下文
const handleUsingContext = () => {
usingContext.value = !usingContext.value;
if (usingContext.value) {
message.success("当前模式下, 发送消息会携带之前的聊天记录");
} else {
message.warning("当前模式下, 发送消息不会携带之前的聊天记录");
}
};
/**
* 提示
* @param value
*/
function messageTip(value) {
message.warning(value);
}
/**
* 渲染文本
* @param item
* @param conversationId
* @param text
* @param options
*/
async function renderText(item,conversationId,text,options) {
let returnText = "";
if (item.event == 'MESSAGE') {
text = text + item.data.message;
returnText = text;
//更新聊天信息
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: text,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
requestOptions: { prompt: message, options: { ...options } },
});
}
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') {
//update-begin---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程执行失败了但是没提示---
if(item.data && !item.data.success){
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
handleStop();
return "";
}
//update-end---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程执行失败了但是没提示---
topicId.value = item.topicId;
conversationId = item.conversationId;
uuid.value = item.conversationId;
requestId.value = item.requestId;
handleStop();
}
if (item.event == 'ERROR') {
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
handleStop();
return "";
}
//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.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】实时展示当前思考进度---
return { returnText, conversationId };
}
//上传文件列表集合
const uploadUrlList = ref<any>([]);
//文件集合
const fileInfoList = ref<any>([]);
/**
* 文件上传回调事件
* @param info
*/
function handleChange(info) {
let { fileList, file } = info;
fileInfoList.value = fileList;
if (file.status === 'error') {
message.error(file.response?.message || `${file.name} 上传失败,请查看服务端日志`);
}
if (file.status === 'done') {
uploadUrlList.value.push(file.response.message);
}
}
/**
* 获取图片地址
*
* @param url
*/
function getImage(url) {
return getFileAccessHttpUrl(url);
}
/**
* 上传前事件
*/
function beforeUpload(file) {
var fileType = file.type;
if (fileType === 'image') {
if (fileType.indexOf('image') < 0) {
message.warning('请上传图片');
return false;
}
}
return true;
}
/**
* 删除图片
*/
function deleteImage(index) {
uploadUrlList.value.splice(index,1);
fileInfoList.value.splice(index,1);
}
/**
* 图片预览
* @param url
*/
function handlePreview(url){
const onImgLoad = ({ index, url, dom }) => {
console.log(`${index + 1}张图片已加载URL为${url}`, dom);
};
let imageList = [getImage(url)];
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
}
//监听开场白
watch(
() => props.prologue,
(val) => {
try {
if (val) {
topChat(val);
}
} catch (e) {}
}
);
//监听开场白预制问题
watch(
() => props.presetQuestion,
(val) => {
topChat(props.prologue);
}
);
//监听应用信息
watch(
() => props.formState,
(val) => {
try {
if (val) {
appData.value = val;
}
} catch (e) {}
},
{ deep: true, immediate: true }
);
//监听历史信息
watch(
() => props.historyData,
(val) => {
try {
//update-begin---author:wangshuai---date:2025-03-06---for:【QQYUN-11384】浏览器打开应用开场白丢了---
if (val && val.length > 0) {
chatData.value = cloneDeep(val);
if(chatData.value[0]){
topicId.value = chatData.value[0].topicId
}
}else{
chatData.value = [];
headerTitle.value = props.chatTitle;
}
if(props.prologue && props.chatTitle){
topChat(props.prologue)
}
} catch (e) {
console.log(e)
}
//update-end---author:wangshuai---date:2025-03-06---for:【QQYUN-11384】浏览器打开应用开场白丢了---
},
{ deep: true, immediate: true }
);
onMounted(() => {
scrollToBottom();
uploadUrlList.value = [];
fileInfoList.value = [];
});
</script>
<style lang="less" scoped>
.chatWrap {
width: 100%;
height: 100%;
padding: 20px;
.content {
height: 100%;
width: 100%;
background: #fff;
display: flex;
flex-direction: column;
}
}
.main {
flex: 1;
min-height: 0;
.scrollArea {
overflow-y: auto;
height: 100%;
}
.chatContentArea {
padding: 10px;
}
}
.emptyArea {
display: flex;
justify-content: center;
align-items: center;
color: #d4d4d4;
}
.stopArea {
display: flex;
justify-content: center;
padding: 10px 0;
}
.footer {
display: flex;
flex-direction: column;
padding: 6px 16px;
.topArea {
padding-left: 6%;
margin-bottom: 6px;
}
.bottomArea {
display: flex;
align-items: center;
.ant-input {
margin: 0 8px;
}
.ant-input,
.ant-btn {
height: 36px;
}
textarea.ant-input {
padding-top: 6px;
padding-bottom: 6px;
}
.contextBtn,
.delBtn {
padding: 0;
width: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.delBtn {
margin-right: 8px;
}
.contextBtn {
color: #a8071a;
&.enabled {
color: @primary-color;
}
font-size: 18px;
}
.sendBtn {
font-size: 18px;
width: 36px;
display: flex;
padding: 8px;
align-items: center;
}
.stopBtn {
width: 32px;
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
}
}
}
:deep(.chatgpt .markdown-body) {
background-color: #f4f6f8;
}
:deep(.ant-message) {
top: 50% !important;
}
.header-title{
color: #101828;
font-size: 16px;
font-weight: 400;
padding-bottom: 8px;
margin-left: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-textarea{
display: flex;
align-items: center;
width: 100%;
border-radius: 15px;
border-style: solid;
border-width: 1px;
flex-direction: column;
transition: width 0.3s;
border-color: rgba(68,83,130,0.2);
.textarea-top{
border-bottom: 1px solid #f0f0f5;
padding: 12px 28px;
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 10px;
.top-image{
display: flex;
img{
border-radius: 8px;
cursor: pointer;
height: 60px;
position: relative;
width: 60px;
}
}
}
.textarea-bottom{
display: flex;
flex-direction: row;
align-items: center;
flex: 1 1;
min-height: 48px;
position: relative;
padding: 8px 8px 8px 10px;
width: 100%;
}
}
.chat-textarea:hover{
border-color: rgba(59,130,246,0.5)
}
.textarea-active{
border-color: rgba(59,130,246,0.5) !important;
}
:deep(.ant-divider-vertical){
margin: 0 2px;
}
.upload-icon{
cursor: pointer;
position: absolute;
background-color: #1D1C23;
color: white;
border-radius: 50%;
padding: 4px;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-left: 44px;
margin-top: -4px;
}
.top-image:hover{
.upload-icon{
display: flex;
}
}
</style>
<style lang="less">
.ai-chat-modal{
z-index: 9999 !important;
}
.ai-chat-message{
z-index: 9999 !important;
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']">
<div class="avatar">
<img v-if="inversion === 'user'" :src="avatar()" />
<img v-else :src="getAiImg()">
</div>
<div class="content">
<p class="date">
<span v-if="inversion === 'ai'" style="margin-right: 10px">{{appData.name || 'AI助手'}}</span>
<span>{{ dateTime }}</span>
</p>
<div v-if="inversion === 'user' && images && images.length>0" class="images">
<div v-for="(item,index) in images" :key="index" class="image" @click="handlePreview(item)">
<img :src="getImageUrl(item)"/>
</div>
</div>
<div class="msgArea">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
</div>
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
<span>{{item.descr}}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import chatText from './chatText.vue';
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']);
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { createImgPreview } from "@/components/Preview";
const { userInfo } = useUserStore();
const avatar = () => {
return getFileAccessHttpUrl(userInfo?.avatar) || defaultAvatar;
};
const emit = defineEmits(['send']);
const getAiImg = () => {
return getFileAccessHttpUrl(props.appData?.icon) || defaultImg;
};
/**
* 预设问题点击事件
*
*/
function presetQuestionClick(descr) {
emit("send",descr)
}
/**
* 获取图片
*
* @param item
*/
function getImageUrl(item) {
let url = item;
if(item.hasOwnProperty('url')){
url = item.url;
}
if(item.hasOwnProperty('base64Data') && item.base64Data){
return item.base64Data;
}
return getFileAccessHttpUrl(url);
}
/**
* 图片预览
* @param url
*/
function handlePreview(url){
const onImgLoad = ({ index, url, dom }) => {
console.log(`${index + 1}张图片已加载URL为${url}`, dom);
};
let imageList = [getImageUrl(url)];
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
}
</script>
<style lang="less" scoped>
.chat {
display: flex;
margin-bottom: 1.5rem;
&.self {
flex-direction: row-reverse;
.avatar {
margin-right: 0;
margin-left: 10px;
}
.msgArea {
flex-direction: row-reverse;
}
.date {
text-align: right;
}
}
}
.avatar {
flex: none;
margin-right: 10px;
img {
width: 34px;
height: 34px;
border-radius: 50%;
overflow: hidden;
}
svg {
font-size: 28px;
}
}
.content {
.date {
color: #b4bbc4;
font-size: 0.75rem;
margin-bottom: 10px;
}
.msgArea {
display: flex;
}
}
.question{
margin-top: 10px;
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
background-color: #ffffff;
font-size: 0.875rem;
line-height: 1.25rem;
cursor: pointer;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.images{
margin-bottom: 10px;
flex-wrap: wrap;
display: flex;
gap: 10px;
.image{
width: 120px;
height: 80px;
cursor: pointer;
img{
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
}
</style>

View File

@ -0,0 +1,128 @@
<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>
<div v-else class="msg" v-text="text" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue';
import MarkdownIt from 'markdown-it';
import mdKatex from '@traptitech/markdown-it-katex';
import mila from 'markdown-it-link-attributes';
import hljs from 'highlight.js';
import './style/github-markdown.less';
import './style/highlight.less';
import './style/style.less';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
const textRef = ref();
const mdi = new MarkdownIt({
html: false,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language));
if (validLang) {
const lang = language ?? '';
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
}
return highlightBlock(hljs.highlightAuto(code).value, '');
},
});
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
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;
});
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>`;
}
function addCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent;
if (code) {
copyToClip(code).then(() => {
btn.textContent = '复制成功';
setTimeout(() => {
btn.textContent = '复制代码';
}, 1e3);
});
}
});
});
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {});
});
}
}
onMounted(() => {
addCopyEvents();
});
onUpdated(() => {
addCopyEvents();
});
onUnmounted(() => {
removeCopyEvents();
});
function copyToClip(text: string) {
return new Promise((resolve, reject) => {
try {
const input: HTMLTextAreaElement = document.createElement('textarea');
input.setAttribute('readonly', 'readonly');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) document.execCommand('copy');
document.body.removeChild(input);
resolve(text);
} catch (error) {
reject(error);
}
});
}
</script>
<style lang="less" scoped>
.textWrap {
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;
color: #fff;
overflow-wrap: break-word;
line-height: 1.625;
min-width: 20px;
}
.chatgpt {
background-color: #f4f6f8;
font-size: 0.875rem;
line-height: 1.25rem;
}
</style>

View File

@ -0,0 +1,28 @@
import { useChatStore } from '@/store';
export function useChat() {
const chatStore = useChatStore();
const getChatByUuidAndIndex = (uuid: number, index: number) => {
return chatStore.getChatByUuidAndIndex(uuid, index);
};
const addChat = (uuid: number, chat: Chat.Chat) => {
chatStore.addChatByUuid(uuid, chat);
};
const updateChat = (uuid: number, index: number, chat: Chat.Chat) => {
chatStore.updateChatByUuid(uuid, index, chat);
};
const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {
chatStore.updateChatSomeByUuid(uuid, index, chat);
};
return {
addChat,
updateChat,
updateChatSome,
getChatByUuidAndIndex,
};
}

View File

@ -0,0 +1,41 @@
import type { Ref } from 'vue';
import { nextTick, ref } from 'vue';
type ScrollElement = HTMLDivElement | null;
interface ScrollReturn {
scrollRef: Ref<ScrollElement>;
scrollToBottom: () => Promise<void>;
scrollToTop: () => Promise<void>;
scrollToBottomIfAtBottom: () => Promise<void>;
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null);
const scrollToBottom = async () => {
await nextTick();
if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
};
const scrollToTop = async () => {
await nextTick();
if (scrollRef.value) scrollRef.value.scrollTop = 0;
};
const scrollToBottomIfAtBottom = async () => {
await nextTick();
if (scrollRef.value) {
const threshold = 100; // Threshold, indicating the distance threshold to the bottom of the scroll bar.
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight;
if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
}
};
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
};
}

View File

@ -0,0 +1,159 @@
// iframe-widget.js
(function () {
let widgetInstance = null;
const defaultConfig = {
// ֧<><D6A7>'top-left'<27><><EFBFBD><EFBFBD>, 'top-right'<27><><EFBFBD><EFBFBD>, 'bottom-left'<27><><EFBFBD><EFBFBD>, 'bottom-right'<27><><EFBFBD><EFBFBD>
iconPosition: 'bottom-right',
//ͼ<><CDBC><EFBFBD>Ĵ<EFBFBD>С
iconSize: '30px',
//ͼ<><CDBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
iconColor: '#155eef',
//<2F><><EFBFBD><EFBFBD><EEB2BB><EFBFBD><EFBFBD><EFBFBD>޸<EFBFBD>
appId: '',
//<2F><><EFBFBD><EFBFBD><ECB5AF><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD>
chatWidth: '800px',
//<2F><><EFBFBD><EFBFBD><ECB5AF><EFBFBD>ĸ߶<C4B8>
chatHeight: '700px',
};
/**
* <20><><EFBFBD><EFBFBD>aiͼ<69><CDBC>
* @param config
*/
function createAiChat(config) {
// <20><><EFBFBD><EFBFBD>ģʽ<C4A3><CABD>ȷ<EFBFBD><C8B7>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>ʵ<EFBFBD><CAB5>
if (widgetInstance) {
return;
}
// <20>ϲ<EFBFBD><CFB2><EFBFBD><EFBFBD><EFBFBD>
const finalConfig = { ...defaultConfig, ...config };
if (!finalConfig.appId) {
console.error('appIdΪ<EFBFBD>գ<EFBFBD>');
return;
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
z-index: 998;
${getPositionStyles(finalConfig.iconPosition)}
cursor: pointer;
`;
// <20><><EFBFBD><EFBFBD>ͼ<EFBFBD><CDBC>
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;
display: flex;
align-items: center;
justify-content: center;
color: white;
`;
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>';
// <20><><EFBFBD><EFBFBD>iframe<6D><65><EFBFBD><EFBFBD>
const iframeContainer = document.createElement('div');
iframeContainer.style.cssText = `
position: absolute;
right: 10px;
bottom: 10px;
width: ${finalConfig.chatWidth} !important;
height: ${finalConfig.chatHeight} !important;
background: white;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.2);
display: none;
z-index: 10000;
`;
// <20><><EFBFBD><EFBFBD>iframe
const iframe = document.createElement('iframe');
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
`;
iframe.id = 'ai-app-chat-document';
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId;
// <20><><EFBFBD><EFBFBD><EFBFBD>رհ<D8B1>ť
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;
cursor: pointer;
background: white;
width: 25px;
height: 25px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
// <20><>װԪ<D7B0><D4AA>
iframeContainer.appendChild(closeBtn);
iframeContainer.appendChild(iframe);
document.body.appendChild(iframeContainer);
container.appendChild(icon);
document.body.appendChild(container);
// <20>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD>
icon.addEventListener('click', () => {
iframeContainer.style.display = 'block';
});
closeBtn.addEventListener('click', () => {
iframeContainer.style.display = 'none';
});
// <20><><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
widgetInstance = {
remove: () => {
container.remove();
iframeContainer.remove();
},
};
}
/**
* <20><>ȡλ<C8A1><CEBB><EFBFBD><EFBFBD>Ϣ
*
* @param position
* @returns {*|string}
*/
function getPositionStyles(position) {
const positions = {
'top-left': 'top: 20px; left: 20px;',
'top-right': 'top: 20px; right: 20px;',
'bottom-left': 'bottom: 20px; left: 20px;',
'bottom-right': 'bottom: 20px; right: 20px;',
};
return positions[position] || positions['bottom-right'];
}
/**
* <20><>ȡsrc<72><63>ַ
*/
function getIframeSrc(finalConfig) {
const specificScript = document.getElementById("e7e007dd52f67fe36365eff636bbffbd");
if (specificScript) {
return specificScript.src.substring(0, specificScript.src.indexOf('/', specificScript.src.indexOf('://') + 3));
}
}
// <20><>¶ȫ<C2B6>ַ<EFBFBD><D6B7><EFBFBD>
window.createAiChat = createAiChat;
})();

View File

@ -0,0 +1,41 @@
import type { Ref } from 'vue';
import { nextTick, ref } from 'vue';
type ScrollElement = HTMLDivElement | null;
interface ScrollReturn {
scrollRef: Ref<ScrollElement>;
scrollToBottom: () => Promise<void>;
scrollToTop: () => Promise<void>;
scrollToBottomIfAtBottom: () => Promise<void>;
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null);
const scrollToBottom = async () => {
await nextTick();
if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
};
const scrollToTop = async () => {
await nextTick();
if (scrollRef.value) scrollRef.value.scrollTop = 0;
};
const scrollToBottomIfAtBottom = async () => {
await nextTick();
if (scrollRef.value) {
const threshold = 100; // Threshold, indicating the distance threshold to the bottom of the scroll bar.
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight;
if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
}
};
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
};
}

View File

@ -0,0 +1,172 @@
<template>
<div class="presetQuestion-wrap">
<svg
v-if="btnShow"
class="leftBtn"
:class="leftBtnStatus"
t="1710296339017"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5070"
@click="onScroll('prev')"
>
<path
d="M970.496 543.829333l30.165333-30.165333-415.829333-415.914667a42.837333 42.837333 0 0 0-60.288 0 42.538667 42.538667 0 0 0 0 60.330667l355.413333 355.498667-355.413333 355.285333a42.496 42.496 0 0 0 0 60.288c16.64 16.64 43.861333 16.469333 60.288 0.042667l383.914667-383.701334 1.749333-1.664z"
fill="currentColor"
p-id="5071"
></path>
</svg>
<div class="content">
<ul ref="ulElemRef">
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item.descr)">
<div class="question-descr">
<Icon v-if="item.icon" :icon="item.icon" size="20"></Icon>
<svg v-else width="14px" height="14px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
<span>{{ item.name }}</span>
</div>
</li>
</ul>
</div>
<svg
v-if="btnShow"
class="rightBtn"
:class="rightBtnStatus"
t="1710296339017"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5070"
@click="onScroll('next')"
>
<path
d="M970.496 543.829333l30.165333-30.165333-415.829333-415.914667a42.837333 42.837333 0 0 0-60.288 0 42.538667 42.538667 0 0 0 0 60.330667l355.413333 355.498667-355.413333 355.285333a42.496 42.496 0 0 0 0 60.288c16.64 16.64 43.861333 16.469333 60.288 0.042667l383.914667-383.701334 1.749333-1.664z"
fill="currentColor"
p-id="5071"
></path>
</svg>
</div>
</template>
<script name="presetQuestion" setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
const emit = defineEmits(['outQuestion']);
const props = defineProps({
quickCommandData:{ type: Object },
});
const data = ref(props.quickCommandData);
const leftBtnStatus = ref('');
const rightBtnStatus = ref('');
const rightBtn = ref('');
const ulElemRef = ref(null);
const btnShow = ref(false);
let timer = null;
const handleScroll = (e) => {
clearTimeout(timer);
timer = setTimeout(() => {
const scrollLeft = e.target.scrollLeft;
const offsetWidth = e.target.offsetWidth;
const scrollWidth = e.target.scrollWidth;
if (scrollWidth > offsetWidth) {
btnShow.value = true;
} else {
btnShow.value = false;
}
if (scrollLeft <= 0) {
leftBtnStatus.value = 'disabled';
} else if (scrollWidth - offsetWidth == scrollLeft) {
rightBtnStatus.value = 'disabled';
} else {
leftBtnStatus.value = '';
rightBtnStatus.value = '';
}
}, 100);
};
const onScroll = (flag) => {
const offsetWidth = ulElemRef.value.offsetWidth;
if (flag == 'prev') {
ulElemRef.value.scrollLeft = ulElemRef.value.scrollLeft - offsetWidth;
} else if (flag == 'next') {
ulElemRef.value.scrollLeft = ulElemRef.value.scrollLeft + offsetWidth;
}
};
const handleQuestion = (item) => {
emit('outQuestion', item);
};
watch(()=>props.quickCommandData, (val) => {
data.value = props.quickCommandData;
})
onMounted(() => {
ulElemRef.value.addEventListener('scroll', handleScroll, false);
handleScroll({ target: ulElemRef.value });
});
onBeforeUnmount(() => {
ulElemRef.value.removeEventListener('scroll', handleScroll);
});
</script>
<style lang="less" scoped>
.presetQuestion-wrap {
display: flex;
align-items: center;
svg {
width: 14px;
height: 14px;
flex: none;
cursor: pointer;
color: #c6c2c2;
&.leftBtn {
transform: rotate(180deg);
}
&.disabled {
opacity: 0.5;
pointer-events: none;
cursor: default;
}
}
.content {
flex: 1;
min-width: 0;
overflow: hidden;
}
ul {
display: flex;
margin-bottom: 0;
width: 100%;
overflow-y: auto;
/* 隐藏所有滚动条 */
&::-webkit-scrollbar {
display: none;
height: 0;
width: 0;
}
}
.item {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 16px;
cursor: pointer;
font-size: 14px;
padding: 2px 10px;
width: max-content;
margin-right: 6px;
white-space: nowrap;
transition: all 300ms ease;
&:last-child {
margin-right: 0;
}
&:hover {
color: @primary-color;
border-color: @primary-color;
}
}
.question-descr{
display: flex;
align-items: center;
span{
margin-left: 4px;
}
}
}
</style>

View File

@ -0,0 +1,65 @@
import type { App } from 'vue';
import { router } from "/@/router";
import type { RouteRecordRaw } from "vue-router";
import { LAYOUT } from "@/router/constant";
const ChatRoutes: RouteRecordRaw[] = [
{
path: "/ai/app/chat/:appId",
name: "ai-chat-@appId-@modeType",
component: () => import("/@/views/super/airag/aiapp/chat/AiChat.vue"),
meta: {
title: 'AI聊天',
ignoreAuth: true,
},
},
{
path: "/ai/app/chatIcon/:appId",
name: "ai-chatIcon-@appId",
component: () => import("/@/views/super/airag/aiapp/chat/AiChatIcon.vue"),
meta: {
title: 'AI聊天',
ignoreAuth: true,
},
},
{
path: '/ai/chat',
name: 'aiChat',
component: LAYOUT,
meta: {
title: 'ai聊天',
},
children: [
{
path: "/ai/chat/:appId",
name: "ai-chat-@appId",
component: () => import("/@/views/super/airag/aiapp/chat/AiChat.vue"),
meta: {
title:'AI助手',
ignoreAuth: false,
},
},
{
path: "/ai/chat",
name: "ai-chat",
component: () => import("/@/views/super/airag/aiapp/chat/AiChat.vue"),
meta: {
title:'AI助手',
ignoreAuth: false,
},
}
],
},
]
/** 注册路由 */
export async function register(app: App) {
await registerMyAppRouter(app);
console.log('[聊天路由] 注册完成!');
}
async function registerMyAppRouter(_: App) {
for(let appRoute of ChatRoutes){
await router.addRoute(appRoute);
}
}

View File

@ -0,0 +1,319 @@
<template>
<div class="slide-wrap">
<div class="header">
<img class="header-image" :src="getImage()" />
<div class="header-name">{{ appData.name || 'AI助手' }}</div>
</div>
<div class="createArea">
<a-button type="dashed" @click="handleCreate">新建聊天</a-button>
</div>
<div class="historyArea">
<ul>
<li
v-for="(item, index) in dataSource.history"
:key="item.id"
class="list"
:class="[item.id == dataSource.active ? 'active' : 'normal', dataSource.history.length == 1 ? 'last' : '']"
@click="handleToggleChat(item, index)"
>
<i class="icon message">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"
></path>
</svg>
</i>
<a-input
class="title"
ref="inputRef"
v-if="item.isEdit"
:defaultValue="item.title"
placeholder="请输入标题"
@change="handleInputChange"
@keyup.enter="inputBlur(item)"
/>
<span class="title" v-else>{{ item.title }}</span>
<span class="icon edit" @click.stop="handleEdit(item)" v-if="!item.isEdit && !item.disabled">
<svg xmlns="http://www.w3.org/2000/svg" role="img" class="iconify iconify--ri" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6.414 15.89L16.556 5.748l-1.414-1.414L5 14.476v1.414zm.829 2H3v-4.243L14.435 2.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414zM3 19.89h18v2H3z"
></path>
</svg>
</span>
<span class="icon del">
<a-popconfirm
:overlayStyle="{ 'z-index': 9999 }"
title="确定删除此记录?"
placement="bottom"
ok-text="确定"
cancel-text="取消"
@confirm.stop="handleDel(item)"
>
<svg xmlns="http://www.w3.org/2000/svg" role="img" class="iconify iconify--ri" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"
></path>
</svg>
</a-popconfirm>
</span>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { defHttp } from '@/utils/http/axios';
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
import defaultImg from '../img/ailogo.png';
const props = defineProps(['dataSource', 'appData']);
const emit = defineEmits(['save', 'click', 'reloadRight', 'prologue']);
const inputRef = ref(null);
const router = useRouter();
let inputValue = '';
//新建聊天
const handleCreate = () => {
const uuid = getUuid();
props.dataSource.history.unshift({ title: '新建聊天', id: uuid, isEdit: false, disabled: true });
// 新建第一个(需要高亮选中)
props.dataSource.active = uuid;
emit('click', "新建聊天", 0);
};
// 切换聊天
const handleToggleChat = (item, index) => {
if (item.id != props.dataSource.active) {
props.dataSource.active = item.id;
emit('click', item.title, index);
}
};
const handleInputChange = (e) => {
inputValue = e.target.value.trim();
};
// 失去焦点
const inputBlur = (item) => {
item.isEdit = false;
item.title = inputValue;
defHttp
.put(
{
url: '/airag/chat/conversation/update/title',
params: { id: item.id, title: inputValue },
},
{ joinParamsToUrl: true }
)
.then((res) => {});
};
// 编辑
const handleEdit = (item) => {
console.log(item);
item.isEdit = true;
inputValue = item.title;
};
// 保存
const handleSave = (item) => {
item.isEdit = false;
item.title = inputValue;
};
/**
* 删除
* @param data
*/
function handleDel(data) {
const findIndex = props.dataSource.history.findIndex((item) => item.id == data.id);
if (findIndex != -1) {
props.dataSource.history.splice(findIndex, 1);
// 删除的是当前active的active往前移前面没了往后移。
if (props.dataSource.history.length) {
if (props.dataSource.active == data.id) {
if (findIndex > 0) {
props.dataSource.active = props.dataSource.history[findIndex - 1].id;
} else {
props.dataSource.active = props.dataSource.history[0].id;
}
}
emit('click', props.dataSource.history[0].title, findIndex);
} else {
// 删没了(删除了最后一个)
props.dataSource.active = null;
emit('click', "", -1);
}
}
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11560】新建聊天内容为空无法删除---
if(data.disabled){
return;
}
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11560】新建聊天内容为空无法删除---
defHttp.delete({
url: '/airag/chat/conversation/' + data.id,
},{ isTransformResponse: false });
}
/**
* 获取图片
*/
function getImage() {
return props.appData.icon ? getFileAccessHttpUrl(props.appData.icon) : defaultImg;
}
watch(
() => inputRef.value,
(newVal: any) => {
if (newVal?.length) {
newVal[0].focus();
}
},
{ deep: true }
);
// 指定长度和基数
const getUuid = (len = 10, radix = 16) => {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid: any = [],
i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
};
</script>
<style scoped lang="less">
.slide-wrap {
border-right: 1px solid #e5e7eb;
height: 100%;
display: flex;
flex-direction: column;
.historyArea {
padding: 20px;
padding-top: 0;
flex: 1;
min-height: 0;
overflow: auto;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
}
.historyArea ul li:hover {
.del {
display: block;
}
}
.createArea {
padding: 20px;
padding-bottom: 0;
}
.ant-btn {
width: 100%;
margin-bottom: 10px;
}
}
ul {
margin-bottom: 0;
}
.list {
width: 100%;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-radius: 0.375rem;
border-width: 1px;
cursor: pointer;
margin-bottom: 10px;
color: #333;
display: flex;
justify-content: flex-start;
align-items: center;
&:hover,
&.active {
border-color: @primary-color;
color: @primary-color;
}
.edit,
.save,
.del {
display: none;
}
&.active {
.edit,
.save,
.del {
display: block;
}
&.last {
.del {
display: none;
}
}
}
.message {
margin-right: 8px;
}
.edit {
margin-right: 8px;
}
.title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.ant-input {
margin-right: 20px;
}
}
svg {
vertical-align: middle;
}
}
:deep(.ant-popover) {
z-index: 9999 !important;
}
:deep(.ant-popconfirm) {
z-index: 9999 !important;
}
.header {
display: flex;
padding: 20px 4px 0 4px;
margin-left: 16px;
.header-image {
height: 35px;
width: 35px;
border-radius: 4px;
margin-right: 10px;
}
.header-name {
align-self: center;
color: #1d2939;
font-weight: 600;
font-size: 16px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,206 @@
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.hljs {
color: #abb2bf;
background: #282c34;
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672;
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #a6e22e;
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9ccc65;
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag,
.hljs-formula {
color: #c678dd;
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379;
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66;
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4;
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801;
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2;
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}

View File

@ -0,0 +1,132 @@
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #fff;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
&.markdown-body-generate > dd:last-child:after,
&.markdown-body-generate > dl:last-child:after,
&.markdown-body-generate > dt:last-child:after,
&.markdown-body-generate > h1:last-child:after,
&.markdown-body-generate > h2:last-child:after,
&.markdown-body-generate > h3:last-child:after,
&.markdown-body-generate > h4:last-child:after,
&.markdown-body-generate > h5:last-child:after,
&.markdown-body-generate > h6:last-child:after,
&.markdown-body-generate > li:last-child:after,
&.markdown-body-generate > ol:last-child li:last-child:after,
&.markdown-body-generate > p:last-child:after,
&.markdown-body-generate > pre:last-child code:after,
&.markdown-body-generate > td:last-child:after,
&.markdown-body-generate > ul:last-child li:last-child:after {
animation: blink 1s steps(5, start) infinite;
color: #000;
content: '_';
font-weight: 700;
margin-left: 3px;
vertical-align: baseline;
}
@keyframes blink {
to {
visibility: hidden;
}
}
}
html.dark {
.markdown-body {
&.markdown-body-generate > dd:last-child:after,
&.markdown-body-generate > dl:last-child:after,
&.markdown-body-generate > dt:last-child:after,
&.markdown-body-generate > h1:last-child:after,
&.markdown-body-generate > h2:last-child:after,
&.markdown-body-generate > h3:last-child:after,
&.markdown-body-generate > h4:last-child:after,
&.markdown-body-generate > h5:last-child:after,
&.markdown-body-generate > h6:last-child:after,
&.markdown-body-generate > li:last-child:after,
&.markdown-body-generate > ol:last-child li:last-child:after,
&.markdown-body-generate > p:last-child:after,
&.markdown-body-generate > pre:last-child code:after,
&.markdown-body-generate > td:last-child:after,
&.markdown-body-generate > ul:last-child li:last-child:after {
color: #65a665;
}
}
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
@media screen and (max-width: 533px) {
.markdown-body .code-block-wrapper {
padding: unset;
code {
padding: 24px 16px 16px 16px;
}
}
}