| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- <template>
- <div class="meeting-summary">
- <div class="meeting-summary-header">
- <img class="icon" src="../../assets/image/home/meet-summary.png" />
- <div class="title">会议纪要</div>
- </div>
- <div class="meeting-summary-body" ref="meeting_summary_body">
- <SummaryItem v-for="(item, index) in summary_data" :key="index" :summary_item="item">
- <template #tools>
- <a-link v-if="item.type != 'loading'" type="text" class="tools tools-item" @click="toRegenerate(item)">
- <template #icon>
- <icon-refresh class="tools-icon" />
- </template>
- 重新生成
- </a-link>
- <a-link v-if="item.type != 'loading'" type="text" class="tools-item" @click="toCopy(item)">
- <template #icon>
- <icon-copy class="tools-icon" />
- </template>
- 复制
- </a-link>
- </template>
- </SummaryItem>
- <div class="loading" v-if="is_loading_summary">
- <a-spin :loading="is_loading_summary" tip="会议纪要生成中..."></a-spin>
- </div>
- </div>
- <div class="meeting-summary-footer">
- <MessageInput @submit="handleSubmit"></MessageInput>
- <div class="tips">内容由 AI 大模型生成,仅供参考</div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, nextTick } from 'vue';
- import MessageInput from './meeting-summary/MessageInput.vue';
- import SummaryItem from './meeting-summary/SummaryItem.vue';
- import { formatMarkdownString } from './meeting-summary/format-anwser-string';
- import { getMeetingSummaryList, conversation, regenerateSummary } from '../../apis/meeting';
- import { useMeetingStore } from '../../base/store/use-meeting';
- import copyText from '../../3rd-libs/clipboard';
- import { Message } from '@arco-design/web-vue';
- const useMeeting = useMeetingStore();
- /**
- * 存放生成会议纪要的数据
- * */
- const summary_data = ref([]);
- const meeting_summary_body = ref(null);
- /**
- * 加载会议纪要数据
- * */
- const is_loading_summary = ref(false);
- /**
- * 获取会议纪要列表
- * 注意:该方法是在结束录音或者点击历史会议时会被调用,
- * 当结束录音是调用时,会出现会议纪要还在生成中的情况,返回的res.data.rows为空,
- * 所以需要等待一段时间,再重新获取会议纪要列表,直到返回的res.data.rows不为空,才继续执行。
- * */
- const getMeetingSummaryListFun = async () => {
- is_loading_summary.value = true;
- try {
- const res = await getMeetingSummaryList(useMeeting.current_meeting_data.id);
- if (res.code == 200) {
- if (res.data.rows.length) {
- const format_data = res.data.rows.map((item) => {
- return {
- ...item,
- format_content: item.content.length ? formatMarkdownString(item.content) : ''
- };
- });
- summary_data.value = format_data;
- // 解除输入框和按钮禁用
- useMeeting.changeDisabledInputOrBtn(false);
- scrollToBottom();
- } else {
- // 等待3s后重新获取会议纪要列表
- await new Promise((resolve) => setTimeout(resolve, 3000));
- await getMeetingSummaryListFun();
- }
- }
- } catch (e) {
- console.log(e);
- } finally {
- is_loading_summary.value = false;
- }
- };
- /**
- * 问答
- */
- const conversationToAi = async (data) => {
- addLoadingToList();
- const res = await conversation(data);
- if (res.code == 200) {
- summary_data.value.pop();
- summary_data.value.push({
- ...res.data,
- format_content: formatMarkdownString(res.data.content)
- });
- // 解除输入框和按钮禁用
- useMeeting.changeDisabledInputOrBtn(false);
- scrollToBottom();
- }
- };
- /**
- * 发送消息
- * */
- const handleSubmit = (input_text_value) => {
- if (!input_text_value.trim()) return;
- // 发送消息添加到列表中
- const questionItem = {
- content: input_text_value,
- speecher: 1 // 1: 用户,2:AI
- };
- addMessageToList(questionItem);
- // 禁用输入框和按钮禁用
- useMeeting.changeDisabledInputOrBtn(true);
- // 滚动到底部
- scrollToBottom();
- // 获取数组中最后一条Ai返回数据的content
- const lastItem = summary_data.value.findLast((item) => item.speecher == 2);
- // 接口所需数据
- const data = {
- content: lastItem ? lastItem.content : '',
- message: input_text_value,
- meeting_id: useMeeting.current_meeting_data.id
- };
- // 发送消息问答
- conversationToAi(data);
- };
- /**
- * 添加消息到列表
- * */
- const addMessageToList = (data) => {
- summary_data.value.push(data);
- };
- /**
- * 添加AI加载中
- * */
- const addLoadingToList = () => {
- const loadingItem = {
- // 是否需要替换
- type: 'loading',
- content: '会议纪要生成中...',
- format_content: '<p>会议纪要生成中...</p>',
- speecher: 2 // 1: 用户,2:AI
- };
- addMessageToList(loadingItem);
- // 滚动到底部
- scrollToBottom();
- };
- /**
- * 滚动到底部
- * */
- const scrollToBottom = () => {
- nextTick(() => {
- if (meeting_summary_body.value) {
- meeting_summary_body.value.scrollTop = meeting_summary_body.value.scrollHeight;
- }
- });
- };
- /**
- * 重新生成
- * */
- const regenerateSummaryFun = async (id, content) => {
- const data = {
- message: '重新生成',
- id,
- content
- };
- const res = await regenerateSummary(data);
- return res;
- };
- /**
- * 重新生成
- * */
- const toRegenerate = (data) => {
- summary_data.value.forEach(async (item) => {
- if (item.id == data.id) {
- item.type = 'loading';
- item.format_content = '<p>重新生成中...</p>';
- useMeeting.changeDisabledInputOrBtn(true);
- const res = await regenerateSummaryFun(item.id, item.content);
- if (res.code == 200) {
- item.type = 'stream'; //重新生成后,给AI返回的数据加一个type标识为stream,暂时没有用到该属性值做判断
- item.content = res.data.content;
- item.updated_at = res.data.updated_at;
- item.format_content = formatMarkdownString(res.data.content);
- // 解除输入框和按钮禁用
- useMeeting.changeDisabledInputOrBtn(false);
- }
- }
- });
- };
- /**
- * 复制
- * */
- const toCopy = (item) => {
- copyText(item.format_content);
- Message.success('复制成功');
- };
- onMounted(() => {
- if (useMeeting.meet_type == 'editMeet') {
- getMeetingSummaryListFun();
- }
- });
- </script>
- <style scoped lang="css">
- .meeting-summary {
- /* position: relative; */
- width: 851px;
- height: 100%;
- border-radius: 20px;
- background-color: var(--bg-body-color);
- /* background-color: #dfdfdf; */
- box-shadow: -10px 4px 10px 0px rgba(0, 0, 0, 0.08);
- }
- .meeting-summary-header {
- display: flex;
- gap: 10px;
- align-items: center;
- justify-content: center;
- padding: 20px 0;
- box-sizing: border-box;
- }
- .icon {
- width: 21px;
- }
- .title {
- font-size: 16px;
- font-weight: bold;
- color: var(--text-meet-summary-title-color);
- }
- .meeting-summary-body {
- position: relative;
- height: calc(100% - 60px - 175px);
- padding: 10px 30px;
- box-sizing: border-box;
- overflow-y: auto;
- }
- .loading {
- position: absolute;
- top: 0px;
- left: 0px;
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .meeting-summary-footer {
- /* position: absolute; */
- /* bottom: 30px; */
- width: 100%;
- height: 175px;
- padding: 0px 30px;
- box-sizing: border-box;
- }
- .tips {
- margin-top: 10px;
- text-align: center;
- color: var(--text-qa-tip-color);
- }
- .tools {
- margin-right: 10px;
- }
- .tools-item {
- color: var(--text-download-color);
- }
- .tools-icon {
- font-size: 16px;
- }
- </style>
|