yangyin
2024-11-04 4eb38b646ea064450208d28f94ee1e348a7d10d9
提交代码
已修改7个文件
296 ■■■■■ 文件已修改
customer_list/common/static/fonts/ywiconfont/iconfont.css 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ai/chat.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/Chat.vue 236 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useScrollLoad.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/model/types.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/common/static/fonts/ywiconfont/iconfont.css
@@ -1,8 +1,14 @@
@font-face {
  font-family: "ywifont"; /* Project id 4655417 */
<<<<<<< HEAD
  src: url('iconfont.woff2?t=1730700980632') format('woff2'),
       url('iconfont.woff?t=1730700980632') format('woff'),
       url('iconfont.ttf?t=1730700980632') format('truetype');
=======
  src: url('iconfont.woff2?t=1730692341015') format('woff2'),
       url('iconfont.woff?t=1730692341015') format('woff'),
       url('iconfont.ttf?t=1730692341015') format('truetype');
>>>>>>> 3e52ec41322cd3bbb96f8d1492dc9b69cebd6661
}
.ywifont {
@@ -13,10 +19,13 @@
  -moz-osx-font-smoothing: grayscale;
}
<<<<<<< HEAD
.ywicon-cubelifangti:before {
  content: "\e6fc";
}
=======
>>>>>>> 3e52ec41322cd3bbb96f8d1492dc9b69cebd6661
.ywicon-loading1:before {
  content: "\e617";
}
src/api/ai/chat.ts
@@ -266,6 +266,9 @@
            method: 'post',
            params: {},
            data: params,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
        },
        callback
    );
src/components/chat/Chat.vue
@@ -23,98 +23,88 @@
                        />
                        <div class="flex-auto flex" :class="{ 'justify-end': item.role === RoleEnum.user }">
                            <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }">
                                <div class="w-full" v-if="isTalking && index === computedMessageList.length - 1">
                                    <div class="text-sm rounded-[6px] p-4 leading-relaxed bg-white" v-if="item.role === RoleEnum.assistant">
                                        <!-- 过程输出 -->
                                        <el-steps direction="vertical" :active="activeStep">
                                            <!-- <el-step title="process" status="process" />
                                                    <el-step title="success" status="success" />
                                                    <el-step title="wait" status="wait" />
                                                    <el-step title="finish" status="finish" />
                                                    <el-steps title="error" status="error" /> -->
                                            <el-step v-for="item in stepList" :title="item.title" :status="stepEnumMap[item.status]" />
                                        </el-steps>
                                    </div>
                                </div>
                                <div class="w-full" v-if="item.content?.values">
                                <div class="w-full">
                                    <div
                                        class="text-sm rounded-[6px] p-4 leading-relaxed"
                                        :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }"
                                    >
                                        <div v-if="item.content.errCode === ErrorCode.Message" class="flex-column w-full">
                                            <p class="text-red-500">
                                                {{ item.content.errMsg }}
                                            </p>
                                            <div class="mt-5 flex items-center" v-if="showFixQuestion(item)">
                                                <div class="text-gray-600 flex-0">
                                                    {{ item.content.origin.err_json.fix_question.title + ':' }}
                                                </div>
                                                <div class="ml-1 space-x-2 inline-flex flex-wrap">
                                                    <div
                                                        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(fixItem, item.content.origin)"
                                                    >
                                                        {{ fixItem.title }}
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                        <template v-else>
                                            <el-popover placement="bottom-start" trigger="hover" :popper-style="{ minWidth: '70px' }" :width="70">
                                                <template #default>
                                                    <div class="action" v-if="item.role === RoleEnum.user">
                                                        <div class="flex items-center justify-center size-[20px]">
                                                            <i
                                                                class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] font-medium !text-[15px]"
                                                                @click="copyUserClick(item)"
                                                            />
                                                        </div>
                                                        <div class="flex items-center justify-center size-[20px]">
                                                            <i
                                                                class="p-2 ywifont ywicon-cubelifangti cursor-pointer hover:text-[#0284ff] text-[#000] font-[590] !text-[15px]"
                                                                @click="setCommonQuestionClick(item)"
                                                            />
                                                        </div>
                                                    </div>
                                                </template>
                                                <template #reference>
                                                    <component
                                                        :is="answerTypeMapCom[item.content.type]"
                                                        v-if="item.role === RoleEnum.user"
                                                        :data="item.content.values"
                                                        :originData="item"
                                                    />
                                                </template>
                                            </el-popover>
                                            <component
                                                :is="answerTypeMapCom[item.content.type]"
                                                v-if="item.role !== RoleEnum.user"
                                                :data="item.content.values"
                                                :originData="item"
                                            />
                                        <div class="flex flex-col" v-if="item?.stepList?.length > 0">
                                            <div
                                                v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list"
                                                class="flex font-bold items-center mt-6"
                                                @click="toggleStepList(item)"
                                                class="cursor-pointer border border-gray-300 border-solid w-fit px-2 flex items-center space-x-2 rounded-lg mb-3 hover:bg-gray-100 active:bg-gray-200"
                                            >
                                                <div class="flex-0 mb-auto -mr-4">关联功能:</div>
                                                <div class="space-x-5 flex flex-wrap">
                                                    <div
                                                        v-for="callItem in item.content.origin?.ext_call_list"
                                                        :key="callItem.call_ext_id"
                                                        @click="relativeQueryClick(callItem)"
                                                        class="cursor-pointer hover:underline first-of-type:ml-5"
                                                    >
                                                        {{ callItem.question }}
                                                <span >
                                                    {{ toggleStepLabel(item) }}
                                                </span>
                                                <span class="ywifont" :class="{ 'ywicon-unfold': !item.stepIsShow, 'ywicon-fold': item.stepIsShow }"></span>
                                            </div>
                                            <!-- 过程输出 -->
                                            <el-steps v-show="item.stepIsShow" direction="vertical" :active="activeStep">
                                                <el-step
                                                    v-for="(subItem, index) in item.stepList"
                                                    :title="subItem.title"
                                                    :status="stepEnumMap[subItem.status]"
                                                >
                                                    <!-- <template #icon>
                                                    <span v-if="subItem.status === StepEnum.Loading" class="ywifont ywicon-loading animate-spin"></span>
                                                    <span v-else class="ywifont ywicon-loading1 animate-spin"></span>
                                                </template> -->
                                                    <template #title>
                                                        <span class="text-sm">{{ subItem.title }}</span>
                                                    </template>
                                                </el-step>
                                            </el-steps>
                                        </div>
                                        <template v-if="item.content?.values">
                                            <div v-if="item.content.errCode === ErrorCode.Message" class="flex-column w-full">
                                                <p class="text-red-500">
                                                    {{ item.content.errMsg }}
                                                </p>
                                                <div class="mt-5 flex items-center" v-if="showFixQuestion(item)">
                                                    <div class="text-gray-600 flex-0">
                                                        {{ item.content.origin.err_json.fix_question.title + ':' }}
                                                    </div>
                                                    <div class="ml-1 space-x-2 inline-flex flex-wrap">
                                                        <div
                                                            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(fixItem, item.content.origin)"
                                                        >
                                                            {{ fixItem.title }}
                                                        </div>
                                                    </div>
                                                </div>
                                            </div>
                                            <template v-else>
                                                <component :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :originData="item" />
                                                <div
                                                    v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list"
                                                    class="flex font-bold items-center mt-6"
                                                >
                                                    <div class="flex-0 mb-auto -mr-4">关联功能:</div>
                                                    <div class="space-x-5 flex flex-wrap">
                                                        <div
                                                            v-for="callItem in item.content.origin?.ext_call_list"
                                                            :key="callItem.call_ext_id"
                                                            @click="relativeQueryClick(callItem)"
                                                            class="cursor-pointer hover:underline first-of-type:ml-5"
                                                        >
                                                            {{ callItem.question }}
                                                        </div>
                                                    </div>
                                                </div>
                                            </template>
                                        </template>
                                    </div>
                                    <!-- 操作 -->
                                    <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 mr-4 mt-2 space-x-2">
                                    <div
                                        v-if="item.role === RoleEnum.assistant && item.content?.values"
                                        class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"
                                    >
                                        <div
                                            class="flex items-center justify-center size-[15px]"
                                            v-if="item.content?.type === AnswerType.Text || item.content?.type === AnswerType.Knowledge"
@@ -205,14 +195,23 @@
<script setup lang="ts">
import _ from 'lodash';
import moment from 'moment';
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted, ref, triggerRef } from 'vue';
import FeedbackPanel from './components/FeedbackPanel.vue';
import { useAssistantContentOpt } from './hooks/useAssistantContentOpt';
import { useQueryProcess } from './hooks/useQueryProcess';
import { useScrollLoad } from './hooks/useScrollLoad';
import { convertProcessItem, useScrollLoad } from './hooks/useScrollLoad';
import { useScrollToBottom } from './hooks/useScrollToBottom';
import type { ChatContent } from './model/types';
import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types';
import type { ChatContent, StepItem } from './model/types';
import {
    AnswerState,
    AnswerType,
    RoleEnum,
    answerTypeMapCom,
    roleImageMap,
    type ChatMessage,
    StepEnum,
    stepEnumMap,
} from './model/types';
import { extCallQuery, questionStreamByPost } from '/@/api/ai/chat';
import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
import CustomDrawer from '/@/components/drawer/CustomDrawer.vue';
@@ -227,6 +226,7 @@
    getRoomConfig,
    roomConfig,
} from '/@/stores/chatRoom';
import { deepClone } from '/@/utils/other';
import { ErrorCode } from '/@/utils/request';
const chatWidth = '75%';
@@ -306,35 +306,43 @@
const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
//#region ====================== 步骤 step ======================
const enum StepEnum {
    Loading,
    Success,
    Error,
}
const stepEnumMap = {
    [StepEnum.Loading]: 'process',
    [StepEnum.Success]: 'success',
    [StepEnum.Error]: 'error',
};
type StepItem = {
    title: string;
    status: StepEnum;
};
const activeStep = ref(-1);
const stepList = ref<StepItem[]>([]);
const stepList = ref<StepItem[]>([
    {
        title: '意图分析中...',
        status: 0,
    },
    {
        title: '意图分析完成',
        status: 1,
    },
    {
        title: '思考如何执行:指标明细查询',
        status: 1,
    },
    {
        title: '指标明细查询完成',
        status: 1,
    },
]);
const resetStep = () => {
    activeStep.value = -1;
    stepList.value = [];
};
const toggleStepLabel = (item: ChatMessage) => (item.stepIsShow ? '收起' : '展开');
const toggleStepList = (item: ChatMessage) => {
    item.stepIsShow = !item.stepIsShow;
};
//#endregion
const DEFAULT_SECTION_A_ID = 'knowledge_base';
let questionRes = null;
let finalCalcSectionAId = null;
const questionAi = async (text) => {
const questionAi = async (text, assistantMsg: ChatMessage) => {
    // processId.value = uuidv4();
    let judgeParams = null;
    if (!preQuestion.value) {
@@ -395,22 +403,12 @@
        if (chunkRes.mode === 'result') {
            res = chunkRes.value;
        } else {
            switch (chunkRes.mode) {
                case 'begin':
                    break;
                case 'end':
                    break;
            }
            stepList.value.push({
                title: chunkRes.value,
                status: StepEnum.Success,
            });
            const stepItem = convertProcessItem(chunkRes);
            computedMessageList.value.at(-1).stepList.push(stepItem);
            scrollToBottom();
            // process.value = chunkRes.value;
        }
    }).finally(() => {
        computedMessageList.value.at(-1).stepIsShow = false;
        resetStep();
    });
    questionRes = res;
@@ -445,7 +443,13 @@
    try {
        isTalking.value = true;
        const userItem: ChatMessage = { role: RoleEnum.user, content } as any;
        const assistantItem: ChatMessage = { role: RoleEnum.assistant, content: null, state: AnswerState.Null } as any;
        const assistantItem: ChatMessage = {
            role: RoleEnum.assistant,
            content: null,
            state: AnswerState.Null,
            stepList: [],
            stepIsShow: true,
        } as any;
        // 发送当前
        messageList.value.push(userItem);
        // 清空输入框
@@ -461,7 +465,7 @@
            questionRes = extRes;
            resMsgContent = parseContent(extRes);
        } else {
            resMsgContent = await questionAi(content.values);
            resMsgContent = await questionAi(content.values, assistantItem);
        }
        nextUserMsgEndIndex.value++;
        if (isNewChat) {
src/components/chat/hooks/useScrollLoad.ts
@@ -1,7 +1,7 @@
import moment from 'moment';
import { Ref, ShallowRef, computed, nextTick, onBeforeUnmount, ref, unref } from 'vue';
import { LOAD_CHAT_LIMIT } from '../constants';
import { AnswerType, ChatContent, ChatMessage, RoleEnum } from '../model/types';
import { AnswerType, ChatContent, ChatMessage, RoleEnum, StepEnum, StepItem } from '../model/types';
import { GetHistoryAnswer, QueryHistoryDetail } from '/@/api/ai/chat';
type UseScrollLoadOption = {
    container: ShallowRef<HTMLDivElement>;
@@ -10,6 +10,24 @@
    parseAnswerContent: (res: any) => ChatContent;
};
export const convertProcessItem = (processItem: any) => {
    switch (processItem.mode) {
        case 'begin':
            break;
        case 'end':
            break;
    }
    return {
        status: StepEnum.Success,
        title: processItem.value,
    };
};
export const convertProcessToStep = (process: any[]) => {
    const stepList = (process??[]).map<StepItem>((item) => {
        return convertProcessItem(item);
    });
    return stepList;
};
/**
 * 滚动加载数据
 * @returns
@@ -75,6 +93,8 @@
                            state: item.answer_state,
                            sectionAId: mapUser?.section_a_id,
                            createTime: answerTime,
                            stepList: convertProcessToStep(item?.answer?.exec_process),
                            stepIsShow:false
                      }
            );
            i++;
src/components/chat/model/types.ts
@@ -55,10 +55,30 @@
    content?: ChatContent;
    state?: null | '1' | '0';
    sectionAId?:string,
    createTime?:string
    createTime?:string,
    stepList:StepItem[]
    stepIsShow:boolean
}
export const roleImageMap = {
    [RoleEnum.user]: userPic,
    [RoleEnum.assistant]: assistantPic,
};
export const enum StepEnum {
    Loading,
    Success,
    Error,
}
export const stepEnumMap = {
    [StepEnum.Loading]: 'process',
    [StepEnum.Success]: 'process',
    [StepEnum.Error]: 'process',
};
export type StepItem = {
    title: string;
    status: StepEnum;
};
src/utils/request.ts
@@ -131,7 +131,7 @@
const createAxiosInstance = (option: Partial<CreateAxiosDefaults<any>> = {}) => {
    return axios.create({
        baseURL: MAIN_URL,
        timeout: 50000,
        timeout: 120000,
        headers: { 'Content-Type': 'application/json;charset=utf-8 ' },
        ...option,
    });
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: false,
            hmr: true,
        },
        build: {
            // outDir: 'dist/' + mode.mode,