Bubble 打字机效果实践:从项目配置到源码实现
Bubble æåæºææå®è·µï¼ä»é¡¹ç®é ç½®å°æºç å®ç°
one_last_FE 2026-08-20 0 é 读12åéBubble æåæºææå®è·µï¼ä»é¡¹ç®é ç½®å°æºç å®ç°
åè¨
å¨ AI 对è¯äº§åéï¼æåæºææå 乿¯ä¸ä¸ªé»è®¤ä½éªã
ç¨æ·åéä¸å¥è¯åï¼AI 䏿¯çå¾ä¹ åçªç¶ä¸¢åºä¸å¤§æ®µå 容ï¼èæ¯åæ£å¨æèåè¡¨è¾¾ä¸æ ·ï¼ä¸ç¹ç¹æåå¤å±ç¤ºåºæ¥ã
è¿ä¸ªææçèµ·æ¥åªæ¯âæåæ ¢æ ¢åºç°âï¼ä½å¨çå®é¡¹ç®éï¼å®è§£å³çæ¯ä¸ä¸ªå¾å®é çé®é¢ï¼
è®©ç¨æ·æç¥ç³»ç»æ£å¨å·¥ä½ï¼è䏿¯å¡ä½äºã
æå¨å Zero Code Agent å·¥ä½å°æ¶ï¼ä¹éå°äºè¿ä¸ªé®é¢ã
å端已ç»éè¿ SSE 䏿®µæ®µè¿åå 容äºï¼ä½å¦æåç«¯åªæ¯æå®æ´å å®¹ç´æ¥æ¸²æåºæ¥ï¼ç¨æ·çå°çè¿æ¯ï¼
çå¾
å ç§
çªç¶åºç°ä¸å¤§æ®µåå¤
è¿ä¸ªä½éªå¹¶ä¸å¥½ã
æä»¥æ¶æ¯å表ééè¦å¼å ¥æåæºææã
å½å项ç®ä½¿ç¨çæ¯ Ant Design X Vue ç BubbleList å Bubbleãè¿ç¯æç« 就记å½ä¸ä¸ï¼
- 为ä»ä¹è¦åæåæºææï¼
- æå¨é¡¹ç®éæ¯æä¹é ç½®çï¼
- æåæºå SSE æ¯ä»ä¹å ³ç³»ï¼
- æåç»åæºç çä¸ä¸
Bubbleæ¯å¦ä½å®ç°è¿ä¸ªææçã
è¿ç¯ä¼æ¯å纯çç»ä»¶ä½¿ç¨æ´æ·±å ¥ä¸ç¹ï¼ä½ä¸ä¼ä¸ºäºè®²æºç è讲æºç ï¼è¿æ¯ä»çå®ä¸å¡è½å°åºåã
为ä»ä¹ AI 对è¯éè¦æåæºææ
æ®éæ¥å£è¯·æ±éï¼ç¨æ·ç¹å»æé®åï¼é¡µé¢é常åªéè¦ä¸ä¸ª loadingã
æ¯å¦ï¼
æäº¤è¡¨å
æ¾ç¤º loading
请æ±å®æ
å±ç¤ºç»æ
ä½ AI 对è¯ä¸å¤ªä¸æ ·ã
AI åå¤å¾å¾æ¯è¾é¿ï¼å¯è½æ¯ä¸æ®µè§£éãä¸ä»½æ¹æ¡ï¼çè³æ¯ä¸æ´æ®µä»£ç ã
å¦æçæ¨¡å宿´çæååå±ç¤ºï¼ç¨æ·ä½éªä¼åæï¼
ç¨æ·åéæ¶æ¯
页é¢ç©ºç
çå¾
å ç§æåå ç§
çªç¶åºç°ä¸å¤§æ®µå
容
ç¨æ·å¾é¾å¤æï¼
æ¯ç½ç»æ
¢ï¼
æ¯æ¨¡åå¡ä½ï¼
è¿æ¯ç³»ç»æ²¡æååºï¼
èæåæºææå¯ä»¥æä½éªåæï¼
ç¨æ·åéæ¶æ¯
AI æ°æ³¡åºç°
å
容å¼å§éæ¥å±ç¤º
ç¨æ·æç»æç¥ç³»ç»æ£å¨è¾åº
è¿ç§åé¦å¯¹ AI 产åé常éè¦ã
å®ä¸åªæ¯ä¸ä¸ªå¨ç»ï¼èæ¯ä¸ä¸ªç¶æè¡¨è¾¾ã
æåæºåæµå¼è¾åºä¸æ¯ä¸åäº
è¿éæä¸ä¸ªå¾å®¹ææ··æ·çç¹ï¼
æåæºææä¸çäºæµå¼æ¥å£ã
å®ä»¬å±äºä¸¤å±è½åã
SSE / Stream è´è´£æ°æ®æä¹ä»å端å°å端
Typing è´è´£å端æä¹æææ¬å±ç¤ºç»ç¨æ·
å¦æåªæ SSEï¼æ²¡æ typingï¼å端å¯è½ä¼éå¸¸å¿«å°æ chunk æ¼æ¥åºæ¥ï¼è§è§ä¸è¿æ¯åæ´æ®µåºç°ã
å¦æåªæ typingï¼æ²¡æ SSEï¼ä¹è½æå®æ´ææ¬æ ¢æ ¢æ¾ç¤ºåºæ¥ï¼ä½ç¨æ·è¦çåç«¯å®æ´è¿ååæè½çå°ç¬¬ä¸ä¸ªåã
æ¯è¾çæ³çæ¹å¼æ¯ï¼
å端éè¿ SSE æç»è¿å chunk
å端æç»æ´æ° message.content
Bubble æ ¹æ® typing é
ç½®åæåæºå±ç¤º
ä¹å°±æ¯ï¼
æ°æ®å±ï¼ç宿µå¼
å±ç¤ºå±ï¼æåæºå¨ç»
è¿ä¹æ¯æå½å项ç®éçå®ç°æ¹å¼ã
项ç®éçæ¶æ¯å±ç¤ºç»æ
å½åæ¶æ¯å±ç¤ºç»ä»¶æ¯ï¼
src/features/workbench/components/chat/ChatMessageList.vue
å®ä¸»è¦ä½¿ç¨äº BubbleListï¼
<BubbleList
:items="conversation.messages"
:roles="roles"
:auto-scroll="true"
>
</BubbleList>
å ¶ä¸ï¼
| åæ° | ä½ç¨ |
|---|---|
items | å½åä¼è¯çæ¶æ¯å表 |
roles | ç¨æ·å AI çæ°æ³¡æ ·å¼é ç½® |
auto-scroll | å 容ååæ¶èªå¨æ»å¨ |
æåæºæææ¬èº«ä¸å¨ ChatMessageList éå宿¶å¨ï¼ä¹æ²¡æèªå·±æä½ DOMã
å®ä¾èµæ¶æ¯å¯¹è±¡éç typing åæ®µã
ææ¯å¦ä½é ç½® typing ç
å½ç¨æ·åéæ¶æ¯åï¼å端ä¼å å¾å½åä¼è¯éæå ¥ä¸¤æ¡æ¶æ¯ã
ç¬¬ä¸æ¡æ¯ç¨æ·æ¶æ¯ï¼
{
key: `${now}-user`,
role: 'user',
content: payload.message,
skillKeys: payload.skillKeys,
}
ç¬¬äºæ¡æ¯ AI æ¶æ¯ï¼
{
key: assistantKey,
role: 'assistant',
content: '',
loading: false,
typing: { step: 2, interval: 24 },
skillKeys: payload.skillKeys,
}
å ³é®å°±æ¯è¿éï¼
typing: { step: 2, interval: 24 }
å®çå«ä¹æ¯ï¼
| åæ®µ | ä½ç¨ |
|---|---|
step | æ¯æ¬¡å¢å å 个å符 |
interval | æ¯æ¬¡å¢å ä¹é´çé´éæ¶é´ |
å½åé ç½®æ¯ï¼
æ¯ 24ms å±ç¤º 2 个å符
è¿ä¸ªå¼æ²¡æç»å¯¹æ åã
妿 interval 太大ï¼AI çèµ·æ¥ä¼é常æ
¢ï¼å¦æå¤ªå°ï¼å伿¥è¿âæ´æ®µéªåºâã
æè¿ééæ© step: 2 å interval: 24ï¼æ¯ä¸ä¸ªæ¯è¾æä¸çä½éªï¼
ææåæºæ
ä¸ä¼ææ
¢å¤ªå¤
é¿ææ¬ä¹ä¸ä¼è®©ç¨æ·ç太ä¹
为ä»ä¹åªç»æ°å»ºä¼è¯ç AI åå¤é ç½® typing
è¿éè¿æä¸ä¸ªå¾éè¦ç项ç®ç»èï¼
æåæºææåªåºè¯¥åºç°å¨âå½åæ£å¨çæçè¿æ¡ AI åå¤âä¸ï¼ä¸åºè¯¥åºç°å¨ææ AI æ¶æ¯ä¸ã
å¨å½åå·¥ä½å°éï¼æ¶æ¯å¤§æ¦æä¸¤ç±»æ¥æºã
第ä¸ç±»æ¯ç¨æ·åååéæ¶æ¯åï¼å端临æ¶å建ç宿¶åå¤ï¼
{
key: assistantKey,
role: 'assistant',
content: '',
loading: false,
typing: { step: 2, interval: 24 },
}
è¿ç±»æ¶æ¯æ£å¨æ¥æ¶ SSE chunkï¼å®åºè¯¥ææåæºææã
第äºç±»æ¯åå²å¯¹è¯æ¥å£å è½½åºæ¥çæ¶æ¯ã
æ¯å¦ç¨æ·ç¹å»å·¦ä¾§æä¸ªåå²ä¼è¯æ¶ï¼å端ä¼è¯·æ±å端å岿¶æ¯ï¼ç¶åæ chat_history æ å°ææ¶æ¯å表ï¼
{
key: history.id,
role: history.messageType === 'ai' ? 'assistant' : 'user',
content: history.message,
}
è¿éä¸ä¼é ç½®ï¼
typing: { step: 2, interval: 24 }
åå å¾ç®åï¼
å岿¶æ¯å·²ç»çæå®æäº
ç¨æ·æå¼åå²ä¼è¯æ¶åºè¯¥ç´æ¥çå°å®æ´å
容
ä¸åºè¯¥æè¿å»çæ¯æ¡ AI åå¤éæ°æä¸é
妿æ typing æ¾å° roles.assistant éï¼ææ AI æ¶æ¯é½ä¼é»è®¤å¯ç¨æåæºã
è¿ä¼å¯¼è´ï¼
欢è¿è¯ä¼æå
å岿¶æ¯ä¼æå
éè¯¯æ¶æ¯å¯è½ä¹ä¼æå
å·²ç»å®æçåå¤éæ°è¿å
¥é¡µé¢æ¶è¿ä¼æå
è¿äºé½ä¸æ¯ä¸ä¸ªçå®äº§åéæ³è¦çææã
æä»¥ææ´å¾åäºæèè´£åå¼ï¼
roles è´è´£è§è²çç¨³å®æ ·å¼
message item è´è´£åæ¡æ¶æ¯ç宿¶ç¶æ
éåæ¾å¨ roles.assistant çæ¯ï¼
{
placement: 'start',
variant: 'borderless',
avatar: {...},
classNames: { content: 'agent-message-ai' },
}
è¿äºå±äºâAI æ¶æ¯é¿ä»ä¹æ ·âã
éåæ¾å¨åæ¡æ¶æ¯éçææ¯ï¼
{
loading: false,
typing: { step: 2, interval: 24 },
}
è¿äºå±äºâè¿æ¡æ¶æ¯å½åæ¯ä»ä¹ç¶æâã
è¿ä¹æ¯æå¨å®é 项ç®éæ´æ¨èçåæ³ï¼
æ°åå»ºçæµå¼ assistant æ¶æ¯é ç½® typingï¼å岿¶æ¯ä¸é ç½® typingã
SSE chunk å¦ä½è¿å ¥æ¶æ¯
AI æ¶æ¯ä¸å¼å§æ¯ç©ºçï¼
content: ''
å½å端éè¿ SSE è¿å chunk æ¶ï¼å端ä¼ä¸æè¿½å å°è¿æ¡æ¶æ¯ç contentã
æ ¸å¿é»è¾æ¯ï¼
onChunk: (chunk) => {
replaceMessage(target, assistantKey, item => ({
...item,
loading: false,
content: `${item.content}${chunk}`,
}));
}
å设åç«¯ä¾æ¬¡è¿åï¼
ä½
好
ï¼
æ
å¯
以
帮
ä½
é£ä¹æ¶æ¯ç¶æä¼ä¸æååï¼
content = 'ä½ '
content = 'ä½ å¥½'
content = 'ä½ å¥½ï¼'
content = 'ä½ å¥½ï¼æ'
content = 'ä½ å¥½ï¼æå¯ä»¥'
使¯ç¨æ·çå°çå¹¶ä¸ä¸å®æ¯å®æ´ç contentã
å 为 Bubble å
é¨ä¼æ ¹æ® typing åä¸å±å±ç¤ºå¤çã
ä¹å°±æ¯è¯´ï¼
message.content æ¯å®æ´æ°æ®
Bubble å®é
æ¾ç¤ºçæ¯æåæºå¤çåçå
容
è¿ä¸ªåå±å¾éè¦ã
ä¸å¡å±åªéè¦ç»´æ¤ç宿æ¬ï¼å±ç¤ºèå¥äº¤ç»ç»ä»¶ã
为ä»ä¹ done æ¶ä¸è½ç«å»å ³é typing
è¿ä¸ªå°æ¹æè¸©è¿ä¸ä¸ªåã
ä¸å¼å§æå¨æ¶å° SSE ç done äºä»¶åï¼ç´æ¥æ typing 设置æäº falseã
çèµ·æ¥å¾åçï¼
åç«¯ç»æäº
é£å端ä¹ç»æ typing
ä½å®é ææä¸å¯¹ã
å 为å端 chunk å¯è½è¿åå¾é常快ï¼done å¾å¿«å°±å°äºã
å¦æè¿æ¶ç«å»å
³é typingï¼ç»ä»¶è¿æ²¡æ¥å¾åææåä¸ä¸ªä¸ªæåºæ¥ï¼æç»ç¨æ·çå°çå°±æ¯ï¼
çªç¶åºç°ä¸æ´æ®µ
æä»¥ç°å¨çå¤çæ¹å¼æ¯ï¼
onDone: () => {
finishRunning(target, 'åå宿åå¤');
}
注æè¿é没æå
³é typingã
done åªè¡¨ç¤ºï¼
å端æµå¼æ°æ®ä¼ è¾ç»æ
ä½è§è§ä¸çæåæºå±ç¤ºï¼åºè¯¥ç»§ç»äº¤ç» Bubble èªå·±å®æã
åªæå¨æ¥éæç¨æ·åæ¶æ¶ï¼æéè¦ä¸»å¨å ³éï¼
typing: false
è¿ä¸ªç»èå¾å°ï¼ä½å¯¹ä½éªå½±å徿æ¾ã
ä¸è¦ç¨ loading æ¢å å 容åº
å¦ä¸ä¸ªå®¹æè¸©çç¹æ¯ loadingã
Bubble æ¯æ loading ç¶æï¼å¦æè®¾ç½®ï¼
loading: true
å®ä¼ä¼å å±ç¤º loading å 容ã
卿®é请æ±éè¿å¾åçã
ä½å¨ SSE + typing çåºæ¯éï¼å¦æä¸å¼å§å°±ç» AI æ¶æ¯è®¾ç½® loading: trueï¼ç¨æ·çå°çå¯è½æ¯ï¼
æ£å¨çæä¸...
ç loading ç»æåï¼åçªç¶æ¾ç¤ºå®æ´å 容ã
è¿å°±ææåæºä½éªç ´åäºã
æä»¥å½å项ç®éï¼AI æ¶æ¯åå»ºæ¶æ¯ï¼
loading: false
è¿æ · Bubble å¯ä»¥ç´æ¥æ¥ç®¡ content å typingã
æ¢å¥è¯è¯´ï¼
æµå¼è¾åºåºæ¯éï¼loading ä¸åºè¯¥æ¢å æ¶æ¯å
容åº
仿ºç ç typing æ¯æä¹çæç
ä¸é¢ç»å Ant Design X Vue çæºç çä¸ä¸å®æ¯æä¹å®ç°çã
è¿éä¸»è¦æ¶åä¸ä¸ªé¨åï¼
Bubble.vue
useTypingConfig
useTypedEffect
æºç ä½ç½®å¤§è´å¨ï¼
node_modules/ant-design-x-vue/lib/bubble/
useTypingConfigï¼è§£æ typing é ç½®
typing å¯ä»¥ä¼ å¸å°å¼ï¼ä¹å¯ä»¥ä¼ 对象ã
æ¯å¦ï¼
typing: true
æè ï¼
typing: { step: 2, interval: 24 }
æºç éä¼å æå®è§£ææç»ä¸é ç½®ã
æ ¸å¿é»è¾å¯ä»¥ç®åæï¼
const typingEnabled = computed(() => !!typing);
const defaultConfig = {
step: 1,
interval: 50,
suffix: null,
};
const mergedConfig = computed(() => {
return {
...defaultConfig,
...(typeof typing === 'object' ? typing : {}),
};
});
ä¹å°±æ¯è¯´ï¼å¦æåªåï¼
typing: true
ç»ä»¶ä¼ä½¿ç¨é»è®¤é ç½®ï¼
step = 1
interval = 50
妿åï¼
typing: { step: 2, interval: 24 }
å°±ä¼è¦çé»è®¤å¼ã
æä»¥é¡¹ç®éè¿æ®µé ç½®ï¼
typing: { step: 2, interval: 24 }
æç»ä¼è¢«è§£ææï¼
å¯ç¨ typing
æ¯æ¬¡å¢å 2 个å符
æ¯ 24ms å¢å 䏿¬¡
useTypedEffectï¼çæ£å®ç°æåæº
çæ£åæåæºææçæ¯ useTypedEffectã
å®å é¨ç»´æ¤äºä¸ä¸ªå½åå±ç¤ºé¿åº¦ã
å¯ä»¥ç®åçè§£æï¼
const currentLength = ref(1);
ç¶åæ ¹æ® currentLength ä»å®æ´å
å®¹éæªåä¸é¨åï¼
displayContent = content.slice(0, currentLength);
å½ typing å¼å¯ï¼å¹¶ä¸å½åå±ç¤ºé¿åº¦è¿æ²¡è¾¾å°å®æ´å
容é¿åº¦æ¶ï¼å®ä¼å¯å¨ä¸ä¸ªå®æ¶å¨ï¼
setTimeout(() => {
currentLength += step;
}, interval);
äºæ¯å®æ´å 容æ¯ï¼
ä½ å¥½ï¼æå¯ä»¥å¸®ä½ åæè¿ä¸ªéæ±
å®é å±ç¤ºä¼åæï¼
ä½
ä½ å¥½
ä½ å¥½ï¼
ä½ å¥½ï¼æ
ä½ å¥½ï¼æå¯
...
è¿å°±æ¯æåæºææçæ¬è´¨ï¼
宿´ææ¬ä¸å
å±ç¤ºé¿åº¦ä¸ç¹ç¹å¢å
ç»ä»¶ä¸æ¯ä¸ææ¼å符串ï¼èæ¯ä¸ææªåå·²æå符串ã
å 容ååæ¶å¦ä½ç»§ç»æå
AI åå¤ä¸æ¯ä¸æ¬¡æ§ç»åºå®æ´ææ¬ï¼èæ¯ SSE æç»è¿½å ã
æä»¥ content ä¼ä¸æååï¼
ä½ å¥½
ä½ å¥½ï¼æ
ä½ å¥½ï¼æå¯ä»¥
ä½ å¥½ï¼æå¯ä»¥å¸®ä½
useTypedEffect éä¼çå¬ content çååã
彿°å å®¹æ¯æ§å 容çå»¶ç»æ¶ï¼å®ä¸ä¼ææåæºéç½®å°å¼å¤´ï¼èæ¯ç»§ç»å¾åå±ç¤ºã
ä¹å°±æ¯è¯´ï¼
æ§å
容ï¼ä½ å¥½ï¼æ
æ°å
容ï¼ä½ å¥½ï¼æå¯ä»¥å¸®ä½
ç»ä»¶ç¥éè¿æ¯å䏿¡æ¶æ¯å¨ç»§ç»å¢é¿ï¼äºæ¯ä»å½åå±ç¤ºä½ç½®ç»§ç»æã
è¿ç¹å¯¹ SSE åºæ¯å¾å ³é®ã
å¦åæ¯æ¥ä¸ä¸ª chunkï¼æåæºé½ä»ç¬¬ä¸ä¸ªåéæ°å¼å§ï¼é£ä½éªå°±å´©äºã
è¿èåæä¸ä¸ªå ³é®é»è¾ï¼å¤ææ°æ§å 容ä¹é´çå ³ç³»ã
æºç éæä¸ä¸ªç±»ä¼¼è¿æ ·ç彿°ï¼ç¨æ¥æ¾ä¸¤ä¸ªå符串çæé¿å ¬å ±åç¼ï¼
function getCommonPrefixLength(prev: string, next: string) {
let index = 0;
const maxLength = Math.min(prev.length, next.length);
while (index < maxLength && prev[index] === next[index]) {
index += 1;
}
return index;
}
å®è¦è§£å³çé®é¢æ¯ï¼
åé¢å·²ç»å±ç¤ºè¿çé¨åï¼ä¸è¦éæ°æ
å颿°å¢è¿æ¥çé¨åï¼ç»§ç»æ typing å¾åæ
æ¯å¦ç¬¬ä¸æ¬¡å 容æ¯ï¼
ä½ å¥½ï¼æ
è¿æ¶é¡µé¢å¯è½å·²ç»å±ç¤ºå°ï¼
ä½ å¥½
ä¸ä¸æ¬¡ SSE chunk å°è¾¾åï¼å®æ´å 容åæï¼
ä½ å¥½ï¼æå¯ä»¥å¸®ä½
æ°å 容并䏿¯ä¸æ¡å ¨æ°çæ¶æ¯ï¼èæ¯å¨æ§å 容åé¢ç»§ç»è¿½å ã
æä»¥åççè¡ä¸ºæ¯ï¼
å·²ç»å±ç¤ºè¿çâä½ å¥½âä¸è¦æ¶å¤±
ä¹ä¸è¦ä»âä½ âéæ°å¼å§æ
ç»§ç»æåé¢çâï¼æå¯ä»¥å¸®ä½ âæåºæ¥
useTypedEffect éä¼ä¿åä¸ä¸æ¬¡ç宿´å
容ã
å½ content ååæ¶ï¼å®ä¼å¤æï¼
æ°å
容æ¯ä¸æ¯ä»¥æ§å
容å¼å¤´
妿æ¯ï¼è¯´æè¿æ¯æ£å¸¸è¿½å ï¼
æ§å
容ï¼ä½ å¥½ï¼æ
æ°å
容ï¼ä½ å¥½ï¼æå¯ä»¥å¸®ä½
è¿ç§æ åµä¸ä¸éè¦éç½®æåæºï¼åªè¦ç»§ç»å¢å å±ç¤ºé¿åº¦ã
妿䏿¯æ£å¸¸è¿½å ï¼æ¯å¦ï¼
æ§å
容ï¼ä½ å¥½ï¼æå¯ä»¥å¸®ä½
æ°å
å®¹ï¼æ±æï¼åæçæå¤±è´¥
说æå 容åçäºæ¿æ¢ã
è¿æ¶å°±ä¸è½ç®å仿§å±ç¤ºé¿åº¦ç»§ç»å¾åæï¼å 为åé¢å 容已ç»ä¸ä¸æ ·äºã
æä»¥æºç 伿¾æé¿å ¬å ±åç¼ï¼
妿æ°å
容䏿¯æ§å
容çåç¼
说æå
容åçäºæ¿æ¢
éè¦æ¾å°å
Œ
±åç¼æéç½®å±ç¤ºä½ç½®
å¯ä»¥çè§£æï¼
æ§å
容ï¼ä½ å¥½ï¼æå¯ä»¥å¸®ä½
æ°å
容ï¼ä½ 好ï¼è¿ééè¦éæ°çæ
å
Œ
±åç¼ï¼ä½ 好ï¼
è¿æ¶åæåæºå¯ä»¥ä»å ¬å ±åç¼åé¢ç»§ç»ï¼è䏿¯å®å ¨ä»ç¬¬ä¸ä¸ªåå¼å§ã
å¦æå ¬å ±åç¼é¿åº¦ä¸º 0ï¼å°±ä»å¼å¤´éæ°æã
è¿æ®µé»è¾çèµ·æ¥å¾ç»ï¼ä½å®é é常éè¦ã
å 为å¨çå®é¡¹ç®éï¼æ¶æ¯å 容ä¸ä¸å®æ°¸è¿æ¯ç®å追å ã
å®å¯è½éå°ï¼
SSE chunk åå¹¶
å
容éè¯
éè¯¯æ¶æ¯è¦ç
模åè¾åºä¿®æ£
åç«¯éæ°è®¾ç½® content
æé¿å ¬å ±åç¼çæä¹å°±å¨äºï¼
å°½éä¿çç¨æ·å·²ç»çå°ç稳å®é¨åï¼åªå¯¹æ°ååçé¨ååæåæºå±ç¤ºã
è¿ä¸ªå¤çè®©å®æ¢è½æ¯ææ£å¸¸è¿½å ï¼ä¹è½æ¯æå å®¹è¢«æ¿æ¢æ¶çå±ç¤ºä¿®æ£ã
Bubble å¦ä½ä½¿ç¨ typedContent
å¨ Bubble ç»ä»¶éï¼å®ä¸ä¼ç´æ¥æåå§ content 渲æåºæ¥ã
å®ä¼å
ç»è¿ useTypedEffectï¼
const [typedContent, isTyping] = useTypedEffect(...);
ç¶åçæ£æ¸²æçæ¯ï¼
typedContent
å¯ä»¥çè§£æï¼
props.content 宿´å
容
typedContent å½ååºè¯¥å±ç¤ºç»ç¨æ·ççå
容
å½ typing å¼å¯æ¶ï¼
typedContent = content.slice(0, currentLength)
å½ typing å
³éæ¶ï¼
typedContent = content
æä»¥ä¸å¡ä¾§åªéè¦æç»æ´æ° contentï¼ç»ä»¶å
é¨ä¼è´è´£æå®åæéåå±ç¤ºã
å æ æææ¯æä¹æ¥ç
æåæºææé¤äºæå鿥åºç°ï¼è¿é叏伿ä¸ä¸ªéªçå æ ã
Bubble éä¹åäºè¿ä¸ªææã
å½ç»ä»¶å¤äº typing ç¶ææ¶ï¼ä¼ç»æ°æ³¡å ä¸ç±»ä¼¼è¿æ ·ç classï¼
ant-bubble-typing
ç¶å CSS ééè¿ä¼ªå ç´ å ä¸ä¸ªå æ ï¼
.ant-bubble-typing .ant-bubble-content:last-child::after {
content: "|";
animation: cursorBlink 0.8s infinite;
}
æä»¥ç¨æ·çå°çæææ¯ï¼
ä½ å¥½ï¼ææ£å¨åæ |
è¿ä¸ªå
æ 䏿¯æä»¬ä¸å¡ä»£ç éåçï¼èæ¯ Bubble èªå·±æ ¹æ® typing ç¶æå ä¸çã
为ä»ä¹ä¸è¦èªå®ä¹ message ææ§½
æä¸å¼å§è¿è¸©è¿å¦ä¸ä¸ªåï¼ç¨äº BubbleList ç message ææ§½æå¨æ¸²æå
容ã
ç±»ä¼¼è¿æ ·ï¼
<template #message="{ item }">
<span>{{ item.content }}</span>
</template>
è¿ä¼å¯¼è´ä¸ä¸ªé®é¢ï¼
ä½ èªå·±ç´æ¥æ¸²æ item.content
Bubble å
é¨ç typedContent 就被ç»å¼äº
ä¹å°±æ¯è¯´ï¼å³ä½¿æ¶æ¯ä¸é ç½®äºï¼
typing: { step: 2, interval: 24 }
æå页é¢è¿æ¯ç´æ¥æ¾ç¤ºå®æ´ item.contentã
æä»¥å½åé¡¹ç®æ²¡æä½¿ç¨ message ææ§½ã
妿è¦ä½¿ç¨æåæºææï¼åºè¯¥è®© Bubble èªå·±æ¸²æå
容ï¼
<BubbleList :items="conversation.messages" />
å¯ä»¥èªå®ä¹ï¼
header
footer
avatar
roles
ä½ä¸è¦è½»ææ¥ç®¡ message 渲æã
é¤éä½ é常æ¸
æ¥å¦ä½æ content åæ°æ¥è¿æ¥ï¼è䏿¯ç´æ¥è¯»åå§ item.contentã
å½åé¡¹ç®æç»é¾è·¯
ç°å¨é¡¹ç®éç宿´é¾è·¯æ¯ï¼
ç¨æ·è¾å
¥æ¶æ¯
â
å端æå
¥ user æ¶æ¯
â
å端æå
¥ assistant ç©ºæ¶æ¯ï¼å¹¶é
ç½® typing
â
å»ºç« SSE è¿æ¥
â
å端æç»è¿å chunk
â
å端æç»è¿½å assistant.content
â
Bubble çå¬ content åå
â
useTypedEffect æ§å¶ typedContent å±ç¤ºé¿åº¦
â
页é¢å½¢ææåæºææ
è¿æ¡é¾è·¯éï¼æ¯ä¸å±çèè´£æ¯æ¸ æ¥çï¼
| å±çº§ | 责任 |
|---|---|
| SSE | ä¼ è¾æµå¼æ°æ® |
| useWorkbenchChat | ç»´æ¤æ¶æ¯ç¶æ |
| BubbleList | æ¸²ææ¶æ¯å表 |
| Bubble | å¤çåæ¡æ¶æ¯å±ç¤º |
| useTypedEffect | çææåæºå±ç¤ºå 容 |
è¿ç§å屿¯å¨ä¸å¡ä»£ç éèªå·±å宿¶å¨æ´ç¨³å®ã
å 为ä¸å¡å±ä¸åºè¯¥å ³å¿ï¼
å½åå±ç¤ºå°ç¬¬å 个å
宿¶å¨ä»ä¹æ¶åæ¸
ç
å
æ ä»ä¹æ¶åæ¾ç¤º
å
å®¹æ¿æ¢æ¶æä¹å¤ç
è¿äºé½åºè¯¥äº¤ç» UI ç»ä»¶å é¨è§£å³ã
ä¸å¡å±åªè´è´£ï¼
æçå® message.content æ´æ°å¥½
æ typing é
ç½®ä¼ è¿å»
å°ç»
è¿æ¬¡å®ç°æåæºææï¼æç»å ¶å®åªéè¦ä¸ä¸ªå¾å°çé ç½®ï¼
typing: { step: 2, interval: 24 }
ä½çæ£è¦æå®ç¨å¥½ï¼éè¦çè§£å®å SSEãloadingãmessage ææ§½ä¹é´çå ³ç³»ã
æçå®é ç»éªæ¯ï¼
1. SSE è´è´£æµå¼æ°æ®ï¼ä¸è´è´£è§è§èå¥
2. typing è´è´£è§è§å±ç¤ºï¼ä¸æ¿ä»£ SSE
3. ä¸è¦å¨ done æ¶ç«å»å
³é typing
4. ä¸è¦ç¨ loading æ¢å æµå¼æ¶æ¯å
容åº
5. ä¸è¦éä¾¿ç¨ message ææ§½ç»å¼ Bubble å
鍿¸²æ
ä»é¡¹ç®è§åº¦çï¼æçæ³çç¶ææ¯ï¼
æ¥å£å±åªå¤çæ°æ®æµ
ç¶æå±åªç»´æ¤å®æ´æ¶æ¯
ç»ä»¶å±è´è´£å±ç¤ºä½éª
è¿æ ·æåæºæææ¢è½è·èµ·æ¥ï¼ä¹ä¸ä¼æä¸å¡ä»£ç åå¾å¾éã
åé¢å¦æç»§ç»æ·±å
¥ï¼å¯ä»¥ååç¬æä¸ç¯æç« ï¼ä¸é¨åæ BubbleList ç autoScroll æ¯æä¹é
å BubbleContext åèªå¨æ»å¨çã
Aitishiku.com