wujingjing
2024-07-22 c123adf7cb8caa3a92f1143755092371ef4a0658
整理 chat.vue
已修改3个文件
已添加3个文件
427 ■■■■■ 文件已修改
src/components/chat/Chat.vue 209 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useAssistantContentOpt.ts 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useQueryProcess.ts 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useScrollToBottom.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/model/types.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/Chat.vue
@@ -27,21 +27,18 @@
                                            <p class="text-red-500">
                                                {{ item.content.msg }}
                                            </p>
                                            <div
                                                class="mt-5 flex items-center"
                                                v-if="showFixQuestion"
                                            >
                                            <div class="mt-5 flex items-center" v-if="showFixQuestion(item)">
                                                <div class="text-gray-600 flex-0">
                                                    {{ computedMessageList.at(-1).content.origin.err_json.fix_question.title + ':' }}
                                                    {{ item.content.origin.err_json.fix_question.title + ':' }}
                                                </div>
                                                <div class="ml-1 space-x-2 inline-flex flex-wrap">
                                                    <div
                                                        v-for="item in computedMessageList.at(-1).content.origin.err_json.fix_question?.values"
                                                        :key="item"
                                                        v-for="fixItem in item.content.origin.err_json.fix_question?.values"
                                                        :key="fixItem"
                                                        class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
                                                        @click="fixQuestionClick(item)"
                                                        @click="fixQuestionClick(fixItem)"
                                                    >
                                                        {{ item.title }}
                                                        {{ fixItem.title }}
                                                    </div>
                                                </div>
                                            </div>
@@ -142,16 +139,17 @@
import { ElMessage } from 'element-plus';
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { computed, nextTick, onActivated, onMounted, ref, watch } from 'vue';
import useClipboard from 'vue-clipboard3';
import { computed, onMounted, ref } from 'vue';
import FeedbackPanel from './components/FeedbackPanel.vue';
import Loding from './components/Loding.vue';
import { useAssistantContentOpt } from './hooks/useAssistantContentOpt';
import { useQueryProcess } from './hooks/useQueryProcess';
import { useScrollToBottom } from './hooks/useScrollToBottom';
import type { ChatContent } from './model/types';
import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types';
import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi, SetHistoryAnswerState, getQuestionProcess } from '/@/api/ai/chat';
import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi } from '/@/api/ai/chat';
import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
import CustomDrawer from '/@/components/drawer/CustomDrawer.vue';
import { useClickOther } from '/@/hooks/useClickOther';
import router from '/@/router';
import { activeChatRoom, activeLLMId, activeSampleId, activeSectionAId, getRoomConfig, roomConfig } from '/@/stores/chatRoom';
import { ErrorCode } from '/@/utils/request';
@@ -168,22 +166,9 @@
const chatListDom = ref<HTMLDivElement>();
const messageList = ref<ChatMessage[]>([]);
const computedMessageList = computed(() => {
    return messageList.value.filter((v) => v && v.role !== RoleEnum.system);
    return messageList.value.filter((v) => !!v);
});
const showAskMore = computed(() => {
    if (!computedMessageList.value || computedMessageList.value.length === 0) return false;
    const last = computedMessageList.value.at(-1);
    const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
    return isShow;
});
const showFixQuestion = computed(() => {
    if (!computedMessageList.value || computedMessageList.value.length === 0) return false;
    const last = computedMessageList.value.at(-1);
    const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.origin?.err_json?.fix_question;
    return isShow;
});
const parseContent = (res) => {
    if (!res) return null;
    let content: ChatContent = {
@@ -243,54 +228,23 @@
    content.origin = res;
    return content;
};
//#region ====================== æŸ¥è¯¢è¿›åº¦ ======================
let processId = '';
const QUERY_PROCESS_INTERVAL = 1000;
const process = ref('');
let processTimer = null;
let finishProcess = true;
const queryProcessApi = async () => {
    const res = await getQuestionProcess({
        process_id: processId,
    }).catch((err) => {
        process.value = err;
    });
const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
    process.value = res.process;
    finishProcess = true;
};
const queryProcess = () => {
    processTimer = setInterval(() => {
        if (!finishProcess) return;
        finishProcess = false;
        queryProcessApi();
    }, QUERY_PROCESS_INTERVAL);
};
const clearQueryProcess = () => {
    process.value = '';
    clearInterval(processTimer);
};
//#endregion
let isNextChat = false;
let questionRes = null;
const questionAi = async (text) => {
    if (!currentSectionId) {
        ElMessage.warning('发送失败,未确定应用场景!');
    }
    processId = uuidv4();
    processId.value = uuidv4();
    const params = {
        process_id: processId,
        process_id: processId.value,
        question: text,
        // FIXME: æš‚时这样
        section_a_id: currentSectionId,
        history_group_id: currentRouteId,
        raw_mode: roomConfig.value?.[currentRouteId]?.isAnswerByLLM ?? false,
        next_chat: isNextChat,
        next_chat: isNextChat.value,
    } as any;
    if (currentSampleId) {
@@ -316,10 +270,6 @@
        values: '',
    });
const scrollToBottom = () => {
    if (!chatListDom.value) return;
    chatListDom.value.lastElementChild?.scrollIntoView();
};
let currentSectionId = null;
let currentSampleId = null;
@@ -390,24 +340,6 @@
    }
};
const askMoreClick = (item) => {
    if (!item.question) return;
    sendChatMessage({ type: AnswerType.Text, values: item.question });
};
const fixQuestionClick = (item) => {
    if (!item.question) return;
    isNextChat = true;
    try {
        sendChatMessage({
            type: AnswerType.Text,
            values: item.question,
        });
    } finally {
        isNextChat = false;
    }
};
onMounted(async () => {
    const res = await QueryHistoryDetail({
        history_group_id: currentRouteId,
@@ -454,97 +386,32 @@
        sendChatMessage();
    }
});
let forbidScroll = false;
watch(
    messageList,
    () => {
        if (forbidScroll) return;
        nextTick(() => scrollToBottom());
    },
    {
        deep: true,
    }
);
onActivated(() => {
    if (forbidScroll) return;
    nextTick(() => scrollToBottom());
const { forbidScroll } = useScrollToBottom({
    chatListDom: chatListDom,
    displayMessageList: computedMessageList,
});
//#region ====================== èŠå¤©å†…容操作 ======================
const { toClipboard } = useClipboard();
const copyClick = (item) => {
    const type = item.content.type;
    let text = '';
    if (type === AnswerType.Knowledge) {
        text = item.content.values?.map((item) => item.answer).join('\n\n') ?? '';
    } else {
        text = item.content.values;
    }
    ElMessage.success('复制成功');
    toClipboard(text);
};
const likeClick = async (item) => {
    const toSetState = item.state === AnswerState.Like ? AnswerState.Null : AnswerState.Like;
    const res = await SetHistoryAnswerState({
        history_id: item.historyId,
        answer_state: toSetState,
    });
    item.state = toSetState;
    forbidScroll = true;
    nextTick(() => {
        forbidScroll = false;
    });
};
const unLikeClick = async (item) => {
    const toSetState = item.state === AnswerState.Unlike ? AnswerState.Null : AnswerState.Unlike;
    const res = await SetHistoryAnswerState({
        history_id: item.historyId,
        answer_state: toSetState,
    });
    item.state = toSetState;
    forbidScroll = true;
    nextTick(() => {
        forbidScroll = false;
    });
};
const feedbackPosition = ref({
    x: 0,
    y: 0,
});
const feedbackIsShow = ref(false);
const feedbackContent = ref('');
const feedbackPanelRef = ref<HTMLDivElement>(null);
const currentFeedbackMapItem = ref(null);
const curFeedbackIndex = ref(0);
const feedbackClick = async (e, item, index) => {
    currentFeedbackMapItem.value = item;
    curFeedbackIndex.value = index;
    const offsetX = -4;
    const offsetY = -8;
    feedbackIsShow.value = true;
    nextTick(() => {
        feedbackPosition.value = {
            x: -feedbackPanelRef.value[index].$el.clientWidth + offsetX,
            y: -feedbackPanelRef.value[index].$el.clientHeight + offsetY,
        };
    });
};
useClickOther(
    computed(() => feedbackPanelRef.value[curFeedbackIndex.value]),
const {
    copyClick,
    likeClick,
    unLikeClick,
    feedbackPosition,
    feedbackIsShow,
    () => {
        feedbackIsShow.value = false;
        feedbackContent.value = '';
    }
);
//#endregion
    feedbackContent,
    feedbackPanelRef,
    currentFeedbackMapItem,
    feedbackClick,
    askMoreClick,
    fixQuestionClick,
    isNextChat,
    showFixQuestion,
    showAskMore,
} = useAssistantContentOpt({
    forbidScroll,
    sendChatMessage,
    displayMessageList: computedMessageList,
});
//#region ====================== ä¾§è¾¹æ drawer ======================
const drawerIsShow = ref(false);
src/components/chat/hooks/useAssistantContentOpt.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
import { ElMessage } from 'element-plus';
import type { ComputedRef, Ref } from 'vue';
import { computed, nextTick, ref } from 'vue';
import useClipboard from 'vue-clipboard3';
import type { ChatMessage } from '../model/types';
import { AnswerState, AnswerType, RoleEnum } from '../model/types';
import { SetHistoryAnswerState } from '/@/api/ai/chat';
import { useClickOther } from '/@/hooks/useClickOther';
export type AssistantContentOptOption = {
    forbidScroll: Ref<boolean>;
    sendChatMessage: any;
    displayMessageList: ComputedRef<ChatMessage[]>;
};
export const useAssistantContentOpt = (option: AssistantContentOptOption) => {
    const { forbidScroll, sendChatMessage, displayMessageList } = option;
    const { toClipboard } = useClipboard();
    const isNextChat = ref(false);
    const copyClick = (item) => {
        const type = item.content.type;
        let text = '';
        if (type === AnswerType.Knowledge) {
            text = item.content.values?.map((item) => item.answer).join('\n\n') ?? '';
        } else {
            text = item.content.values;
        }
        ElMessage.success('复制成功');
        toClipboard(text);
    };
    const likeClick = async (item) => {
        const toSetState = item.state === AnswerState.Like ? AnswerState.Null : AnswerState.Like;
        const res = await SetHistoryAnswerState({
            history_id: item.historyId,
            answer_state: toSetState,
        });
        item.state = toSetState;
        forbidScroll.value = true;
        nextTick(() => {
            forbidScroll.value = false;
        });
    };
    const unLikeClick = async (item) => {
        const toSetState = item.state === AnswerState.Unlike ? AnswerState.Null : AnswerState.Unlike;
        const res = await SetHistoryAnswerState({
            history_id: item.historyId,
            answer_state: toSetState,
        });
        item.state = toSetState;
        forbidScroll.value = true;
        nextTick(() => {
            forbidScroll.value = false;
        });
    };
    const feedbackPosition = ref({
        x: 0,
        y: 0,
    });
    const feedbackIsShow = ref(false);
    const feedbackContent = ref('');
    const feedbackPanelRef = ref<HTMLDivElement>(null);
    const currentFeedbackMapItem = ref(null);
    const curFeedbackIndex = ref(0);
    const feedbackClick = async (e, item, index) => {
        currentFeedbackMapItem.value = item;
        curFeedbackIndex.value = index;
        const offsetX = -4;
        const offsetY = -8;
        feedbackIsShow.value = true;
        nextTick(() => {
            feedbackPosition.value = {
                x: -feedbackPanelRef.value[index].$el.clientWidth + offsetX,
                y: -feedbackPanelRef.value[index].$el.clientHeight + offsetY,
            };
        });
    };
    useClickOther(
        computed(() => feedbackPanelRef.value[curFeedbackIndex.value]),
        feedbackIsShow,
        () => {
            feedbackIsShow.value = false;
            feedbackContent.value = '';
        }
    );
    const showAskMore = computed(() => {
        if (!displayMessageList.value || displayMessageList.value.length === 0) return false;
        const last = displayMessageList.value.at(-1);
        const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
        return isShow;
    });
    const showFixQuestion = (item) => {
        const isShow = item?.role === RoleEnum.assistant && item?.content?.values && item.content?.origin?.err_json?.fix_question;
        return isShow;
    };
    const askMoreClick = (item) => {
        if (!item.question) return;
        sendChatMessage({ type: AnswerType.Text, values: item.question });
    };
    const fixQuestionClick = (item) => {
        if (!item.question) return;
        isNextChat.value = true;
        try {
            sendChatMessage({
                type: AnswerType.Text,
                values: item.question,
            });
        } finally {
            isNextChat.value = false;
        }
    };
    return {
        copyClick,
        likeClick,
        unLikeClick,
        feedbackPosition,
        feedbackIsShow,
        feedbackContent,
        feedbackPanelRef,
        currentFeedbackMapItem,
        curFeedbackIndex,
        feedbackClick,
        askMoreClick,
        fixQuestionClick,
        isNextChat,
        showAskMore,
        showFixQuestion,
    };
};
src/components/chat/hooks/useQueryProcess.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
import { ref } from 'vue';
import { getQuestionProcess } from '/@/api/ai/chat';
export const useQueryProcess = () => {
    const processId = ref('');
    const QUERY_PROCESS_INTERVAL = 1000;
    const process = ref('');
    let processTimer = null;
    let finishProcess = true;
    const queryProcessApi = async () => {
        const res = await getQuestionProcess({
            process_id: processId.value,
        }).catch((err) => {
            process.value = err;
        });
        process.value = res.process;
        finishProcess = true;
    };
    const queryProcess = () => {
        processTimer = setInterval(() => {
            if (!finishProcess) return;
            finishProcess = false;
            queryProcessApi();
        }, QUERY_PROCESS_INTERVAL);
    };
    const clearQueryProcess = () => {
        process.value = '';
        clearInterval(processTimer);
    };
    return {
        processId,
        process,
        queryProcess,
        clearQueryProcess
    };
};
src/components/chat/hooks/useScrollToBottom.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
import type { ComputedRef, Ref } from 'vue';
import { nextTick, onActivated, ref, watch } from 'vue';
import type { ChatMessage } from '../model/types';
export type UseScrollToBottomOption = {
    chatListDom: Ref<HTMLDivElement>;
    displayMessageList: ComputedRef<ChatMessage[]>;
};
export const useScrollToBottom = (option:UseScrollToBottomOption) => {
    const {chatListDom,displayMessageList} = option;
    const scrollToBottom = () => {
        if (!chatListDom.value) return;
        chatListDom.value.lastElementChild?.scrollIntoView();
    };
    const forbidScroll = ref(false);
    watch(
        displayMessageList,
        () => {
            if (forbidScroll.value) return;
            nextTick(() => scrollToBottom());
        },
        {
            deep: true,
        }
    );
    onActivated(() => {
        if (forbidScroll.value) return;
        nextTick(() => scrollToBottom());
    });
    return {
        forbidScroll
    };
};
src/components/chat/model/types.ts
@@ -24,7 +24,6 @@
export const enum RoleEnum {
    user = 'user',
    assistant = 'assistant',
    system = 'system',
}
export const AnswerState = {
    Null: null,
@@ -58,5 +57,4 @@
export const roleImageMap = {
    [RoleEnum.user]: userPic,
    [RoleEnum.assistant]: assistantPic,
    [RoleEnum.system]: userPic,
};
vite.config.ts
@@ -35,7 +35,7 @@
            host: '0.0.0.0',
            port: env.VITE_PORT as unknown as number,
            open: JSON.parse(env.VITE_OPEN),
            hmr: true,
            hmr: false,
        },
        build: {
            // outDir: 'dist/' + mode.mode,