当前位置: 首页 > news >正文

vue3项目中模拟AI的深度思考功能

需求:AI生成增加深度思考过程
1、html部分

// 上边是生成之后的展示内容
<div class="ai_generate_loading" v-else>
        <div class="ai_generate_waiting" v-if="isShowWaiting">
          <img src="@/assets/imgs/create.gif" alt="" class="think_logo" />
          <p class="think_tip">AI正在根据您的需求生成表单,请勿做编辑操作!</p>
          <p class="think_tip1">
            生成的内容需要系统处理,可以坐下喝杯茶,放松一下心情, 内容即将呈现!
          </p>
        </div>
        <div class="ai_generate_creating" v-else>
          <div class="creating_top">
            <div class="top_left">
              <h3 class="left_title">深度思考</h3>
              <p class="left_tips">正在为您生成表单,内容即将呈现!</p>
              <div class="left_progress">
                <ElProgress
                  :stroke-width="10"
                  :percentage="displayPercentage"
                  class="custom_progress"
                />
              </div>
            </div>
            <div class="top_right">
              <img src="@/assets/imgs/create.gif" alt="" />
            </div>
          </div>
          <div class="creating_bottom">
            <div class="dialog" ref="contentWrap">
              <div class="response_text" v-html="displayedHtml" ref="contentBox"></div>
            </div>
          </div>
        </div>
      </div>

2、js部分
我这边是有三个入口都会触发sse事件,所以需要监听sendStatus属性来去进行发送sse请求,大家可以根据需要进行处理。
我这边的sendStatus是在pinia中存储的哦,包括我的调用存储key的事件。
因为我这边需要本地存储对应的表单深度思考,当我发布表单之后就可以删除掉了。

watch(
  () => SSEStop,
  () => {
    if (SSEStop.value) {
      handleStreamEnd();
      clearTimeout(timer!);
      timer = null;
    }
  },
  { deep: true },
);

watch(
  () => sendStatus,
  () => {
    if (sendStatus.value) {
      connectSSE();
    }
  },
  { deep: true },
);

watch(
  () => getFormListFinish,
  () => {
    if (!isLoading.value && getFormListFinish.value) {
      percentage.value = 100;
      clearTimeout(timer!);
      timer = null;
      setDisplayedHtml();
    }
  },
  { deep: true },
);

const setDisplayedHtml = () => {
  const preGenerateInfo = getStorage('preGenerateInfo');
  if (preGenerateInfo?.formKey) {
    editorStore.setKeyValue(preGenerateInfo?.formKey, {
      displayedHtml: displayedHtml.value || '',
      aiDeepThink: true,
    });
    editorStore.setAiToolReserve(true);
  }
};

// 清理
onUnmounted(() => {
  clearTimeout(timer!);
  removeStorage('preGenerateInfo');
  disconnectSSE();
});

let eventSource: any = null;
let buffer = '';

const isConnected = ref(false);

const displayedHtml = ref('');

// SSE 配置
const SSE_CONFIG = {
  endpointExam: `${BASE_URL[import.meta.env.VITE_API_BASE_PATH]}web/chat/exam_cot`,
  endpoint: `${BASE_URL[import.meta.env.VITE_API_BASE_PATH]}web/chat/form_cot`,
  params: {
    token: unref(userInfo)?.token || '',
  },
  retryInterval: 3000,
  maxRetries: 2,
};

// 安全标签过滤
const sanitizeHTML = (html) => {
  const allowedTags = ['strong', 'em', 'span', 'br', 'p', 'ul', 'li', '\n'];
  return html.replace(/<\/?([a-z]+)( .*?)?>/gi, (match, tag) => {
    return allowedTags.includes(tag.toLowerCase()) ? match : '';
  });
};

// 连接SSE
const connectSSE = async () => {
  displayedHtml.value = '';
  const preGenerateInfo = getStorage('preGenerateInfo');
  if (preGenerateInfo?.formKey) {
    editorStore.setKeyValue(preGenerateInfo?.formKey, {
      displayedHtml: displayedHtml.value || '',
      aiDeepThink: false,
    });
  }
  try {
    resetState();
    isConnected.value = true;

    const url = new URL(preGenerateInfo?.type == 1 ? SSE_CONFIG.endpoint : SSE_CONFIG.endpointExam);
    SSE_CONFIG.params = { ...SSE_CONFIG.params, ...preGenerateInfo };
    Object.entries(SSE_CONFIG.params).forEach(([key, value]) => {
      url.searchParams.append(key, value);
    });

    eventSource = new EventSource(url);

    percentage.value = 0;
    eventSource.onopen = () => {
      console.log('SSE连接已建立');
      reconnectAttempts = 0;
    };

    eventSource.onmessage = (event) => {
      try {
        isShowWaiting.value = false;
        startProgress();
        const parsed = JSON.parse(event.data);
        handleDataChunk(parsed);
      } catch (err) {
        handleDataChunk(event.data);
      }
    };

    eventSource.onerror = (err) => {
      handleSSEError(err);
    };
  } catch (err) {
    handleSSEError(err);
  }
};

const isLoading = ref(false);
let timer: ReturnType<typeof setInterval> | null = null;

// 显示格式化后的百分比(自动处理小数)
const displayPercentage = computed(() => {
  return Math.floor(percentage.value);
});

// 进度条配置,这里我有两种情况,一种很快一种会很慢,所以写了两个初始速度
const config = {
  initialSpeed: 1, // 初始速度
  initialSpeedExam: 0.4, // 初始速度
  deceleration: 1, // 减速因子
  maxSimulate: 99, // 模拟最大值
  interval: 200, // 刷新间隔(毫秒)
};

// 启动进度条
const startProgress = () => {
  if (timer) return;

  const preGenerateInfo = getStorage('preGenerateInfo');

  isLoading.value = true;
  let currentSpeed = preGenerateInfo?.type == 1 ? config.initialSpeed : config.initialSpeedExam;

  timer = setInterval(() => {
    if (percentage.value >= config.maxSimulate) {
      clearInterval(timer!);
      timer = null;
      return;
    }

    // 动态速度计算(指数衰减)
    percentage.value = Math.min(percentage.value + currentSpeed, config.maxSimulate);

    // 应用减速曲线(可自定义更复杂的缓动函数)
    currentSpeed *= config.deceleration;

    // 保证最小步进(避免停滞)
    currentSpeed = Math.max(currentSpeed, 0.03);
  }, config.interval);
};

// 处理数据块
const handleDataChunk = (data) => {
  // 文本数据处理
  const safeData = sanitizeHTML(data);
  if (safeData === '[done]') {
    handleStreamEnd();
  } else {
    appendContent(safeData);
  }
};

// 追加内容
const appendContent = async (content) => {
  buffer += content;
  displayedHtml.value = buffer;

  await nextTick();
  scrollToBottom();
};

// 处理流结束
const handleStreamEnd = () => {
  if (getFormListFinish.value) {
    percentage.value = 100;
    clearTimeout(timer!);
    timer = null;
    setDisplayedHtml();
  }
  isLoading.value = false;
  disconnectSSE();
};

// 处理SSE错误
const handleSSEError = (err) => {
  console.error('SSE错误:', err);
  reconnectSSE();
};

// 重连机制
let reconnectAttempts = 0;
const reconnectSSE = () => {
  if (reconnectAttempts++ < SSE_CONFIG.maxRetries) {
    setTimeout(() => {
      console.log(`尝试重新连接 (${reconnectAttempts}/${SSE_CONFIG.maxRetries})`);
      disconnectSSE();
      connectSSE();
    }, SSE_CONFIG.retryInterval);
  } else {
    disconnectSSE();
  }
};

// 断开连接
const disconnectSSE = () => {
  if (eventSource) {
    eventSource.close();
    eventSource = null;
  }
  isConnected.value = false;
  setTimeout(() => {
    editorStore.setSendStatus(false);
  }, 100);
};

// 滚动到底部
const scrollToBottom = () => {
  if (contentWrap.value) {
    contentWrap.value.scrollTop = contentWrap.value.scrollHeight;
  }
};

// 重置状态
const resetState = () => {
  displayedHtml.value = '';
  buffer = '';
};

// pinia部分,以下是我的action方法,属性自行在state里边定义哦!
setSendStatus(val: boolean) {
    this.sendStatus = val;
  },
  setFormListFinish(val: boolean) {
    this.getFormListFinish = val;
  },
  setAiStop(val: any) {
    this.SSEStop = val;
  },
 setKeyValue(key, value) {
    this.deepThinkDataList[key] = value;
  },
  deleteKeyValue(key) {
    if (this.deepThinkDataList.hasOwnProperty(key)) {
      delete this.deepThinkDataList[key];
    }
  },

3、css部分

 .ai_generate_loading {
      // flex: 1;
      margin: 12px;
      width: calc(100% - 24px);
      // min-height: 260px;
      height: 448px;
      background: url('@/assets/imgs/thinkBg.png') no-repeat;
      background-size: cover;
      border-radius: 10px;
      // display: flex;
      // flex-direction: column;
      // align-items: center;
      // justify-content: center;
      // padding-top: 42px;
      box-sizing: border-box;
      .ai_generate_waiting {
        // flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        // justify-content: center;
        padding-top: 93px;
        padding-bottom: 124px;
        box-sizing: border-box;
        .think_logo {
          width: 214px;
          height: 125px;
        }
        .think_tip {
          width: 341px;
          height: 22px;
          font-weight: 500;
          font-size: 16px;
          color: #333333;
          line-height: 19px;
          text-align: left;
          font-style: normal;
          text-transform: none;
          margin-top: 26px;
          margin-bottom: 0;
        }
        .think_tip1 {
          width: 378px;
          height: 40px;
          font-weight: 400;
          font-size: 14px;
          color: #797b7d;
          line-height: 20px;
          text-align: center;
          font-style: normal;
          text-transform: none;
          margin-top: 18px;
          margin-bottom: 0;
        }
      }
      .ai_generate_creating {
        width: 100%;
        flex: 1;
        padding: 40px 40px 46px 46px;
        box-sizing: border-box;
        .creating_top {
          display: flex;
          justify-content: start;
          align-items: flex-end;
          .top_left {
            flex: 1;
            // margin-top: 32px;
            .left_title {
              width: 120px;
              height: 42px;
              font-weight: 600;
              font-size: 30px;
              color: #333333;
              line-height: 42px;
              text-align: left;
              font-style: normal;
              text-transform: none;
              margin: 0;
            }
            .left_tips {
              width: 288px;
              height: 25px;
              font-weight: 400;
              font-size: 18px;
              color: #333333;
              line-height: 25px;
              text-align: left;
              font-style: normal;
              text-transform: none;
              margin-top: 10px;
              margin-bottom: 0;
            }
            .left_progress {
              margin-top: 24px;
              position: relative;
              :deep(.custom_progress) {
                .el-progress-bar__inner {
                  background: linear-gradient(117deg, #409fff 1%, #1a77ff 100%);
                  border-radius: 5px 5px 5px 5px;
                }
                .el-progress__text {
                  position: absolute;
                  bottom: 150%;
                  right: 0px;
                  min-width: 68px;
                  height: 56px;
                  font-weight: 600;
                  font-size: 40px !important;
                  line-height: 47px;
                  text-align: left;
                  font-style: normal;
                  text-transform: none;
                  background: linear-gradient(27.478432868269714deg, #409fff 1%, #1a77ff 100%);
                  -webkit-background-clip: text;
                  background-clip: text;
                  -webkit-text-fill-color: transparent;
                  font-weight: bold;
                }
              }
            }
          }
          .top_right {
            margin-left: 34px;
            display: flex;
            justify-content: start;
            align-items: flex-end;
            img {
              width: 198px;
              height: 116px;
            }
          }
        }
        .creating_bottom {
          margin-top: 34px;
          .dialog {
            // width: 100%;
            max-height: 211px;
            border-radius: 0px 0px 0px 0px;
            color: #666666;
            overflow-y: auto;
            overflow-x: hidden;
            scrollbar-width: thin;
            border-left: 1px solid #cbd5e1;
            position: relative;
            scrollbar-color: #787878 transparent;
            padding: 0 15px 0 10px;
            .response_text {
              white-space: pre-line;
              word-break: break-word;
              line-height: 25px;
              box-sizing: border-box;
              color: #666666;
              font-size: 14px;
              :deep(p) {
                margin: 0;
                padding: 0;
              }

              :deep(*) {
                margin: 0;
              }
            }

            /* 优化光标定位 */
            .cursor {
              position: absolute;
              left: 10px;
              width: 14px;
              height: 14px;
              background: url('@/assets/imgs/ai_analyze_icon.png') center/contain no-repeat;
              animation: blink 1.5s infinite;
              pointer-events: none;
              transition:
                left 0.1s,
                top 0.1s;
            }
          }
          .controls {
            margin-top: 20px;
            display: flex;
            gap: 10px;
          }

          @keyframes blink {
            50% {
              opacity: 0;
            }
          }

          .end-mark {
            color: #666;
            font-size: 0.9em;
            padding-left: 8px;
          }
        }
      }
    }

大家自行根据设计图进行修改哦~

以上就实现了深度思考的过程。
效果图如下:
在这里插入图片描述

相关文章:

  • svelte+vite+ts+melt-ui从0到1完整框架搭建
  • 我的第一个开源小项目:内网文件传输工具技术解析
  • Android设置adjustResize时无法生效 解决办法
  • Go 错误处理
  • 单轨小车悬挂输送机安全规程
  • 通过学习opencv图像库编程借助第三方库函数完成一个综合程序设计
  • moviepy学习使用笔记
  • 浅层神经网络:全面解析(扩展)
  • 【OSG学习笔记】Day 1: OSG初探——环境搭建与第一个3D窗口
  • 【SpringCloud】构建分布式系统的利器
  • 超详解glusterfs部署
  • 【Java设计模式】第8章 单列模式讲解
  • 初学STM32之编码器测速以及测频法的实现
  • 【Java设计模式】第9章 原型模式讲解
  • linux下的进程线程管理
  • JavaWeb 课堂笔记 —— 03 Vue
  • 2023年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析
  • 解决Spring Boot启动时YAML配置占位符导致的ScannerException(yml占位符动态替换)
  • android 14.0 工厂模式 测试音频的一些问题(高通)
  • 【AGI-Eval行业动态】OpenAI 语音模型三连发,AI 语音进入“声优”时代
  • 北京网站建设费用/怎么做网站赚钱
  • 网站建设售后培训/软文代写新闻稿
  • 网站可以做多少个关键词/镇江网站建设
  • 襄阳做网站排行榜/游戏广告推广平台
  • 网站上的百度地图标注咋样做/网站发稿平台
  • 做网站需要注意的/广告关键词排名