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

@ -1,125 +0,0 @@
<template>
<div class="textWrap" :class="[inversion ? 'self' : 'chatgpt']" ref="textRef">
<div v-if="!inversion">
<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';
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) 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

@ -1,151 +0,0 @@
<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)">{{ item }}</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 } from 'vue';
const emit = defineEmits(['outQuestion']);
const data = ref(['请介绍一下JeecgBoot', 'JEECG有哪些优势', 'JEECG可以做哪些事情']);
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);
};
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: #333;
&.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;
}
}
}
</style>

View File

@ -1,290 +0,0 @@
<template>
<div class="slide-wrap">
<div class="createArea">
<a-button type="dashed" @click="handleCreate">新建聊天</a-button>
</div>
<div class="historyArea">
<ul>
<li
v-for="item in dataSource.history"
:key="item.uuid"
class="list"
:class="[item.uuid == dataSource.active ? 'active' : 'normal', dataSource.history.length == 1 ? 'last' : '']"
@click="handleToggleChat(item)"
>
<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"
@blur="inputBlur(item)"
/>
<span class="title" v-else>{{ item.title }}</span>
<span class="icon edit" @click="handleEdit(item)" v-if="!item.isEdit">
<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="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" v-if="!item.isEdit">
<a-popconfirm title="确定删除此记录?" placement="bottom" ok-text="确定" cancel-text="取消" @confirm="handleDel(item)">
<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"
@click.stop=""
>
<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>
<span class="icon save" v-if="item.isEdit" @click="handleSave(item)">
<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="M7 19v-6h10v6h2V7.828L16.172 5H5v14zM4 3h13l4 4v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5 12v4h6v-4z"
></path>
</svg>
</span>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps(['dataSource']);
const emit = defineEmits(['save']);
const inputRef = ref(null);
let inputValue = '';
//新建聊天
const handleCreate = () => {
const uuid = getUuid();
props.dataSource.history.unshift({ title: '新建聊天', uuid, isEdit: false });
props.dataSource.chat.unshift({ uuid, data: [] });
// 新建第一个(需要高亮选中)
if (props.dataSource.history.length == 1) {
props.dataSource.active = uuid;
}
};
// 切换聊天
const handleToggleChat = (item) => {
if (item.uuid != props.dataSource.active) {
props.dataSource.active = item.uuid;
const findItem = props.dataSource.history.find((item) => item.isEdit);
if (findItem) {
handleSave(findItem);
}
}
};
const handleInputChange = (e) => {
inputValue = e.target.value.trim();
};
// 失去焦点
const inputBlur = (item) => {
item.isEdit = false;
item.title = inputValue;
};
// 编辑
const handleEdit = (item) => {
item.isEdit = true;
inputValue = item.title;
};
// 保存
const handleSave = (item) => {
item.isEdit = false;
item.title = inputValue;
};
// 删除
const handleDel = (data) => {
const findIndex = props.dataSource.history.findIndex((item) => item.uuid == data.uuid);
if (findIndex != -1) {
props.dataSource.history.splice(findIndex, 1);
props.dataSource.chat.splice(findIndex, 1);
// 删除的是当前active的active往前移前面没了往后移。
if (props.dataSource.history.length) {
if (props.dataSource.active == data.uuid) {
if (findIndex > 0) {
props.dataSource.active = props.dataSource.history[findIndex - 1].uuid;
} else {
props.dataSource.active = props.dataSource.history[0].uuid;
}
}
} else {
// 删没了(删除了最后一个)
props.dataSource.active = null;
}
emit('save');
}
};
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;
}
}
</style>

View File

@ -1,28 +0,0 @@
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,
}
}