|
@@ -1,107 +1,166 @@
|
|
<script setup>
|
|
<script setup>
|
|
import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
|
|
import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
|
|
-import { useRoute } from 'vue-router';
|
|
|
|
import { useMessage } from 'naive-ui';
|
|
import { useMessage } from 'naive-ui';
|
|
-import { useChatStore } from '@/stores/modules/chatStore';
|
|
|
|
-import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome } from '@/components';
|
|
|
|
|
|
+import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
|
|
import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
|
|
import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
|
|
|
|
+import { chatApi } from '@/api/chat';
|
|
import { helperApi } from '@/api/helper';
|
|
import { helperApi } from '@/api/helper';
|
|
|
|
|
|
|
|
+import { useInfinite, useScroll, useChat } from '@/composables';
|
|
|
|
|
|
-import { useInfinite } from '@/composables/useInfinite';
|
|
|
|
-import { useScroll } from '@/composables/useScroll';
|
|
|
|
-import { useChat } from '@/composables/useChat';
|
|
|
|
-import { useRecommend } from '@/composables/useRecommend';
|
|
|
|
|
|
+const ANSWER_ID_KEY = '@@id@@';
|
|
|
|
|
|
|
|
+let controller = new AbortController();
|
|
|
|
+
|
|
|
|
+// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
|
|
const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 2 });
|
|
const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 2 });
|
|
|
|
+const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
|
|
|
|
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
|
|
|
|
|
|
-const dataList = ref(new Array(100).fill({ a:"hello" }));
|
|
|
|
-const isLoading = ref(true);
|
|
|
|
-
|
|
|
|
-const lists = [
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告开场白',
|
|
|
|
- content:'我现在正在写一篇大学生研究报告,报告的开场白需要让普通人能快速理解。我的报告主题是 #关于AIGC快速发展的调查和研究#,请提供三种开头方式,要简单到普通人都能听懂,同时要足够能吸引人,让他们愿意专心听下去',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'邮件回复',
|
|
|
|
- content:'你扮演一个 #调度工程师#,帮我写一封邮件,邮件的内容是 #向公司的领导申请更多的开发资源支持#,邮件内容需要简单明了,语气大方得体,使用中文表达'
|
|
|
|
- },{
|
|
|
|
- title:'周报撰写',
|
|
|
|
- content:'你扮演一个资深员工,我把这一周的工作内容发给你,你帮我撰写为一篇完整的周报。周报要分成本周工作总结、下周工作计划、遇到的困难和解决方案、个人感悟四个段落分别撰写。每一段落都要以分条列表形式写出具体事项,你要一步一步的引导我,让我写出一篇完整的周报。'
|
|
|
|
- },{
|
|
|
|
- title:'PPT大纲',
|
|
|
|
- content:'你扮演一个PPT专家,我把PPT主题#论智慧水务新时代#发给你,请自动生成主题、内容、讲稿'
|
|
|
|
- },{
|
|
|
|
- title:'报告开场白',
|
|
|
|
- content:'我现在正在写一篇大学生研究报告,报告的开场白需要让普通人能快速理解。我的报告主题是 #关于AIGC快速发展的调查和研究#,请提供三种开头方式,要简单到普通人都能听懂,同时要足够能吸引人,让他们愿意专心听下去',
|
|
|
|
- },{
|
|
|
|
- title:'SWTO分析',
|
|
|
|
- content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
|
|
|
|
- },{
|
|
|
|
- title:'周报撰写',
|
|
|
|
- content:'你扮演一个资深员工,我把这一周的工作内容发给你,你帮我撰写为一篇完整的周报。周报要分成本周工作总结、下周工作计划、遇到的困难和解决方案、个人感悟四个段落分别撰写。每一段落都要以分条列表形式写出具体事项,你要一步一步的引导我,让我写出一篇完整的周报。'
|
|
|
|
- },{
|
|
|
|
- title:'PPT大纲',
|
|
|
|
- content:'你扮演一个PPT专家,我把PPT主题#论智慧水务新时代#发给你,请自动生成主题、内容、讲稿'
|
|
|
|
- },{
|
|
|
|
- title:'SWTO分析',
|
|
|
|
- content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
|
|
|
|
- },{
|
|
|
|
- title:'SWTO分析',
|
|
|
|
- content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
|
|
|
|
- },{
|
|
|
|
- title:'SWTO分析',
|
|
|
|
- content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
- title:'报告研究',
|
|
|
|
- content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
|
|
|
|
- },
|
|
|
|
-]
|
|
|
|
-
|
|
|
|
-const getMoreData = () => {
|
|
|
|
-
|
|
|
|
|
|
+const helperList = ref([]);
|
|
|
|
+const message = useMessage();
|
|
|
|
+
|
|
|
|
+const switchActive = ref(false);
|
|
|
|
+
|
|
|
|
+const isLoading = ref(false);
|
|
|
|
+const inputRef = ref(null);
|
|
|
|
+
|
|
|
|
+const currenSessionId = ref(null);
|
|
|
|
+
|
|
|
|
+const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
|
|
|
|
+
|
|
|
|
+// 新建对话
|
|
|
|
+const handleCreateDialog = async () => {
|
|
|
|
+ message.destroyAll();
|
|
|
|
+
|
|
|
|
+ if (unref(isLoading)) {
|
|
|
|
+ return message.warning('当前对话生成中');
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!unref(chatDataSource).length) {
|
|
|
|
+ return message.info('已切换最新会话');
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ inputRef.value.clearInpVal();
|
|
|
|
+
|
|
|
|
+ currenSessionId.value = null;
|
|
|
|
+
|
|
|
|
+ clearChat();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+// 查询对话详情
|
|
|
|
+const handleChatDetail = async ({ sessionId }) => {
|
|
|
|
+ isLoading.value = false;
|
|
|
|
+
|
|
|
|
+ controller.abort();
|
|
|
|
|
|
-const fetchHelperList = () => {
|
|
|
|
|
|
+ const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
|
|
|
|
|
|
|
|
+ chatDataSource.value = data.map(item => ({ ...item, loading: false, }));
|
|
|
|
+ currenSessionId.value = sessionId;
|
|
|
|
+
|
|
|
|
+ scrollToBottom();
|
|
}
|
|
}
|
|
|
|
|
|
-const handleCreateDialog = () => {
|
|
|
|
|
|
+const onRegenerate = async ({ question, realQuestion }) => {
|
|
|
|
+ controller = new AbortController();
|
|
|
|
+
|
|
|
|
+ const sessionId = unref(currenSessionId);
|
|
|
|
+ const params = {
|
|
|
|
+ data: {
|
|
|
|
+ sessionId,
|
|
|
|
+ showVal: question,
|
|
|
|
+ question: realQuestion || question,
|
|
|
|
+ module: 2,
|
|
|
|
+ isStrong: Number(unref(switchActive))
|
|
|
|
+ },
|
|
|
|
+ signal: controller.signal,
|
|
|
|
+ onDownloadProgress: ({ event }) => {
|
|
|
|
+ const xhr = event.target;
|
|
|
|
+ const { responseText } = xhr;
|
|
|
|
+ const [ answer ] = responseText.split(ANSWER_ID_KEY);
|
|
|
|
+
|
|
|
|
+ updateChat({
|
|
|
|
+ sessionId,
|
|
|
|
+ question,
|
|
|
|
+ answer,
|
|
|
|
+ loading: true,
|
|
|
|
+ delayLoading: false
|
|
|
|
+ })
|
|
|
|
|
|
|
|
+ scrollToBottomIfAtBottom();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ const { data } = await chatApi.getChatStream(params);
|
|
|
|
+
|
|
|
|
+ const [ answer, id ] = data.split(ANSWER_ID_KEY);
|
|
|
|
+
|
|
|
|
+ updateChat({
|
|
|
|
+ id,
|
|
|
|
+ sessionId,
|
|
|
|
+ question,
|
|
|
|
+ answer,
|
|
|
|
+ loading: false,
|
|
|
|
+ delayLoading: false
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ scrollToBottomIfAtBottom();
|
|
|
|
+ }
|
|
|
|
+ catch (error){
|
|
|
|
+ console.log("取消了请求 - catch", error);
|
|
|
|
+ }
|
|
|
|
+ finally {
|
|
|
|
+ isLoading.value = false;
|
|
|
|
+ onReset();
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
+// 提交问题
|
|
|
|
+const handleSubmit = async (question, realQuestion = '') => {
|
|
|
|
+ // 用于模拟 - 内容生成前置等待状态
|
|
|
|
+
|
|
|
|
+ if (unref(isExistInHistory)) {
|
|
|
|
+ const { data: sessionId } = await chatApi.getChatSessionTag();
|
|
|
|
+ currenSessionId.value = sessionId;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ isLoading.value = true;
|
|
|
|
+
|
|
|
|
+ addChat({
|
|
|
|
+ sessionId: unref(currenSessionId),
|
|
|
|
+ question,
|
|
|
|
+ realQuestion,
|
|
|
|
+ answer: '',
|
|
|
|
+ loading: true,
|
|
|
|
+ delayLoading: true
|
|
|
|
+ })
|
|
|
|
|
|
|
|
+ scrollToBottom();
|
|
|
|
|
|
-onMounted(() => {
|
|
|
|
|
|
+ setTimeout(() => onRegenerate({ question, realQuestion }), 2 * 1000);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 处理推荐问题
|
|
|
|
+const handleWelcomeRecommend = ({ content }) => {
|
|
|
|
+ inputRef.value.inpVal = content;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 删除历史对话
|
|
|
|
+const handeChatDelete = async (id) => {
|
|
|
|
+ await chatApi.deleteHistory(id);
|
|
|
|
+ onReset();
|
|
|
|
+ clearChat();
|
|
|
|
+ message.success('删除成功');
|
|
|
|
+}
|
|
|
|
|
|
- helperApi.getHelperList();
|
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
+ const { data } = await helperApi.getHelperList();
|
|
|
|
+ helperList.value = data;
|
|
|
|
+})
|
|
|
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
+ controller.abort();
|
|
})
|
|
})
|
|
</script>
|
|
</script>
|
|
<template>
|
|
<template>
|
|
@@ -109,7 +168,7 @@ onMounted(() => {
|
|
<TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
|
|
<TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
|
|
<template #top>
|
|
<template #top>
|
|
<div class="create-btn px-[11px] pb-[22px]">
|
|
<div class="create-btn px-[11px] pb-[22px]">
|
|
- <BaseButton @click="handleCreateDialog">新建对话</BaseButton>
|
|
|
|
|
|
+ <BaseButton @click="handleCreateDialog">新建指令</BaseButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
|
|
@@ -126,29 +185,31 @@ onMounted(() => {
|
|
</div>
|
|
</div>
|
|
</TheSubMenu>
|
|
</TheSubMenu>
|
|
|
|
|
|
-
|
|
|
|
-
|
|
|
|
<TheChatView ref="scrollRef">
|
|
<TheChatView ref="scrollRef">
|
|
- <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:"
|
|
|
|
|
|
+ <div v-if="!chatDataSource.length">
|
|
|
|
+ <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:"
|
|
:sub-title="[
|
|
:sub-title="[
|
|
'LibarAI智能助手模块,具有强大的文本理解和生成能力,可快速响应用户需求',
|
|
'LibarAI智能助手模块,具有强大的文本理解和生成能力,可快速响应用户需求',
|
|
'提供撰写文章、生成报告等服务。'
|
|
'提供撰写文章、生成报告等服务。'
|
|
- ]"
|
|
|
|
- :card-content="recommendList"
|
|
|
|
- @on-click="handleWelcomeRecommend"
|
|
|
|
- />
|
|
|
|
-
|
|
|
|
|
|
+ ]"
|
|
|
|
+ />
|
|
<div class="grid-container">
|
|
<div class="grid-container">
|
|
<div class="grid-content">
|
|
<div class="grid-content">
|
|
- <div class="grid-item" v-for="item,index in lists">
|
|
|
|
- <div class="grid-item-icon">
|
|
|
|
- <SvgIcon name="report"></SvgIcon>
|
|
|
|
- <h3 class="grid-item-title">{{index}}{{item.title}}</h3>
|
|
|
|
|
|
+ <div
|
|
|
|
+ class="grid-item"
|
|
|
|
+ v-for="item in helperList"
|
|
|
|
+ :key="item.id"
|
|
|
|
+ @click="handleWelcomeRecommend(item)"
|
|
|
|
+ >
|
|
|
|
+ <div class="grid-item-icon space-x-[8px]">
|
|
|
|
+ <SvgIcon name="tool-report" size="24"></SvgIcon>
|
|
|
|
+ <h3 class="grid-item-title">{{item.title}}</h3>
|
|
</div>
|
|
</div>
|
|
- <div class="text-[#555555] mt-2">
|
|
|
|
- <!--如果item.content中包含#号,把#号用span标签包裹并设置为蓝色--->
|
|
|
|
|
|
+ <div class="text-[#5E5E5E] mt-[8px] text-justify">
|
|
<template v-if="item.content.indexOf('#') !== -1">
|
|
<template v-if="item.content.indexOf('#') !== -1">
|
|
- <span v-for="word in item.content.split('#')">{{word}}<span style="color: #2454FF;"> # </span></span>
|
|
|
|
|
|
+ <span v-for="word, i in item.content.split('#')" :key="word">
|
|
|
|
+ {{ word }}<i v-if="i !== item.content.split('#').length - 1" class="text-[#2454FF]">#</i>
|
|
|
|
+ </span>
|
|
</template>
|
|
</template>
|
|
<template v-else>
|
|
<template v-else>
|
|
<p>{{item.content}}</p>
|
|
<p>{{item.content}}</p>
|
|
@@ -157,6 +218,21 @@ onMounted(() => {
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div class="conversation-item" v-if="chatDataSource.length">
|
|
|
|
+ <template v-for="item in chatDataSource" :key="item.id">
|
|
|
|
+ <ChatAsk :content="item.question" :sessionId="item.sessionId"></ChatAsk>
|
|
|
|
+ <ChatAnswer
|
|
|
|
+ :id="item.id"
|
|
|
|
+ :content="item.answer"
|
|
|
|
+ :loading="item.loading"
|
|
|
|
+ :delay-loading="item.delayLoading"
|
|
|
|
+ :isSatisfied="item.isSatisfied"
|
|
|
|
+ @on-click-icon=" params => updateById(params)"
|
|
|
|
+ ></ChatAnswer>
|
|
|
|
+ </template>
|
|
|
|
+ </div>
|
|
|
|
|
|
<template #footer>
|
|
<template #footer>
|
|
<ChatInput
|
|
<ChatInput
|
|
@@ -174,7 +250,7 @@ onMounted(() => {
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
.grid-container{
|
|
.grid-container{
|
|
position: relative;
|
|
position: relative;
|
|
- height: calc(100% - 184px);
|
|
|
|
|
|
+ height: calc(100vh - 460px);
|
|
padding-bottom: 20px;
|
|
padding-bottom: 20px;
|
|
margin-top: 36px;
|
|
margin-top: 36px;
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
@@ -185,23 +261,28 @@ onMounted(() => {
|
|
}
|
|
}
|
|
|
|
|
|
.grid-content{
|
|
.grid-content{
|
|
- column-count:3;
|
|
|
|
- column-gap:20px;
|
|
|
|
- -moz-column-count:3;
|
|
|
|
- -webkit-column-count:3;
|
|
|
|
- -moz-column-gap:20px;
|
|
|
|
- -webkit-column-gap:20px;
|
|
|
|
|
|
+ column-count: 3;
|
|
|
|
+ column-gap: 16px;
|
|
|
|
+ -moz-column-count: 3;
|
|
|
|
+ -webkit-column-count: 3;
|
|
|
|
+ -moz-column-gap: 16px;
|
|
|
|
+ -webkit-column-gap: 16px;
|
|
|
|
|
|
.grid-item{
|
|
.grid-item{
|
|
height: auto;
|
|
height: auto;
|
|
- padding: 20px 16px 16px 16px;
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
|
+ padding: 16px;
|
|
|
|
+ margin-bottom: 16px;
|
|
border-radius: 10px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
- -moz-page-break-inside: avoid;
|
|
|
|
-webkit-column-break-inside: avoid;
|
|
-webkit-column-break-inside: avoid;
|
|
break-inside: avoid;
|
|
break-inside: avoid;
|
|
|
|
+ border:1px solid #fff;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ border:1px solid #2454FF;
|
|
|
|
+ }
|
|
|
|
|
|
.grid-item-icon{
|
|
.grid-item-icon{
|
|
display: flex;
|
|
display: flex;
|
|
@@ -209,17 +290,13 @@ onMounted(() => {
|
|
justify-content: start;
|
|
justify-content: start;
|
|
|
|
|
|
.grid-item-title{
|
|
.grid-item-title{
|
|
- margin-left: 20px;
|
|
|
|
- font-size: 16px;
|
|
|
|
|
|
+ font-size: 14px;
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
color: #1A2029;
|
|
color: #1A2029;
|
|
}
|
|
}
|
|
-
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
-
|
|
|
|
}
|
|
}
|
|
|
|
|
|
</style>
|
|
</style>
|