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;
}
}
}
}
大家自行根据设计图进行修改哦~
以上就实现了深度思考的过程。
效果图如下: