Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</block>
<block wx:else>
<view class="{{classPrefix}}__assistant">
<t-chat-markdown content="{{textInfo}}" options="{{markdownProps && markdownProps.options}}"></t-chat-markdown>
<t-chat-markdown content="{{textInfo}}" options="{{markdownProps && markdownProps.options}}" streaming="{{markdownProps && markdownProps.streaming}}"></t-chat-markdown>
</view>
</block>
</view>
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Component({
},
],
},
markdownProps: { streaming: { hasNextChunk: true, tail: true } },
chatId: getUniqueKey(),
};

Expand All @@ -208,6 +209,7 @@ Component({
complete() {
that.setData({
'chatList[0].message.status': 'complete',
'chatList[0].markdownProps': {},
loading: false,
});
},
Expand Down
17 changes: 15 additions & 2 deletions packages/pro-components/chat/chat-list/_example/docs/index.wxml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,25 @@
avatar="{{item.avatar || ''}}"
name="{{item.name || ''}}"
datetime="{{item.datetime || ''}}"
content="{{item.message.content}}"
role="{{item.message.role}}"
chatContentProps="{{chatContentProps}}"
placement="{{item.message.role === 'user' ? 'right' : 'left'}}"
bind:message-longpress="showPopover"
>
<view slot="content">
<block
wx:for="{{item.message.content}}"
wx:for-item="contentItem"
wx:for-index="contentIndex"
wx:key="contentIndex"
>
<t-chat-content
wx:if="{{contentItem.type === 'text' || contentItem.type === 'markdown'}}"
content="{{contentItem}}"
role="{{item.message.role}}"
markdownProps="{{item.markdownProps}}"
/>
</block>
</view>
<t-chat-actionbar
wx:if="{{chatIndex !== chatList.length - 1 && item.message.status === 'complete' && item.message.role === 'assistant'}}"
slot="actionbar"
Expand Down
5 changes: 5 additions & 0 deletions packages/pro-components/chat/chat-markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ isComponent: true

{{ refer }}

### 05 流式输出光标

{{ tail }}

## API

### ChatMarkdown Props
Expand All @@ -63,6 +67,7 @@ isComponent: true
style | Object | - | 样式 | N
custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
content | String | - | 必需。markdown 内容文本 | Y
streaming | Object | - | 流式输出配置,控制光标显示。TS 类型:`TdChatStreamingConfig` `interface TdChatStreamingConfig { hasNextChunk?: boolean; tail?: boolean \| { content?: string } }`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/pro-components/chat/chat-markdown/type.ts) | N
options | Object | { gfm: true, pedantic: false, breaks: true } | Markdown 解析器基础配置。TS 类型:`TdChatContentMDOptions ` `interface TdChatContentMDOptions {gfm?: boolean; pedantic?: boolean; smartLists?: boolean; breaks?: boolean}`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/pro-components/chat/chat-markdown/type.ts) | N

### ChatMarkdown Events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"code": "./code",
"sheet": "./sheet",
"url": "./url",
"refer": "./refer"
"refer": "./refer",
"tail": "./tail"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@
<t-demo title="04 引用">
<refer />
</t-demo>
<t-demo title="05 流式输出 Tail 光标">
<tail />
</t-demo>
</view>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import markdownData from '../base/mock2.js';

const CHUNK_SIZE = 5;
const INTERVAL_MS = 80;

Page({
data: {
content: '',
streaming: { hasNextChunk: false, tail: true },
},

onLoad() {
this.startStreaming();
},

startStreaming() {
let index = 0;

this.setData({
content: '',
streaming: { hasNextChunk: true, tail: true },
});

const timer = setInterval(() => {
index += CHUNK_SIZE;
const isDone = index >= markdownData.length;
this.setData({
content: markdownData.slice(0, index),
streaming: { hasNextChunk: !isDone, tail: true },
});
if (isDone) clearInterval(timer);
}, INTERVAL_MS);
},

handleReplay() {
this.startStreaming();
},

handleNodeTap(e) {
const { node } = e.detail;
if (node && node.type === 'image') {
wx.previewImage({ urls: [node.href], current: node.href });
}
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"usingComponents": {
"t-chat-markdown": "tdesign-miniprogram/chat-markdown/chat-markdown"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<view class="chat-example-block">
<t-chat-markdown content="{{content}}" streaming="{{streaming}}" bind:click="handleNodeTap" />
<button bindtap="handleReplay" style="margin-top: 16px">重新播放</button>
</view>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.chat-example-block {
background-color: var(--td-bg-color-container);
padding: 32rpx;
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@
<block wx:if="{{item.tokens && item.tokens.length}}">
<chat-markdown-node nodes="{{item.tokens}}" />
</block>
<block wx:else>{{''+item.raw+''}}</block>
<block wx:else>
{{''+item.raw+''}}
<text wx:if="{{item.isTail}}" class="{{classPrefix}}-tail">{{item.tailContent}}</text>
</block>
</view>
</block>
<block wx:elif="{{item.type==='strong'}}">
Expand Down
16 changes: 16 additions & 0 deletions packages/pro-components/chat/chat-markdown/chat-markdown.less
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,20 @@
border: 1rpx solid @component-border;
}
}

// 流式输出尾部光标
&-tail {
display: inline-block;
animation: chat-markdown-tail-blink 1s step-start infinite;
}
}

@keyframes chat-markdown-tail-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
60 changes: 60 additions & 0 deletions packages/pro-components/chat/chat-markdown/chat-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,55 @@ import { TdChatMarkdownProps } from './type';
const { prefix } = config;
const name = `${prefix}-chat-markdown`;

const DEFAULT_TAIL_CONTENT = '▋';

/** 解析 tail 参数,返回光标字符;不需要显示时返回 null */
function resolveTailContent(tail?: boolean | { content?: string }): string | null {
if (!tail) return null;
if (typeof tail === 'boolean') return DEFAULT_TAIL_CONTENT;
return tail.content || DEFAULT_TAIL_CONTENT;
}

/**
* 将列表项的子 tokens 展平,供 injectTailToTokens 递归使用。
* marked 的 list token 结构:list.items[].tokens(而非 list.tokens)
*/
function flatListItems(items: any[]): any[] {
return items.reduce((result: any[], item: any) => {
if (item.tokens?.length) result.push(...item.tokens);
return result;
}, []);
}

/**
* 从后往前遍历 token 树,找到最后一个非空 text 叶子节点,打上 isTail 标记。
* - 有子节点(tokens / items)时优先递归
* - 末尾是 code / table / image 等非 text 节点时静默跳过,不注入
* @returns 是否成功注入
*/
function injectTailToTokens(tokens: any[], tailChar: string): boolean {
for (let i = tokens.length - 1; i >= 0; i -= 1) {
const token = tokens[i];
// 优先递归子节点
let children: any[] | null = null;
if (token.tokens?.length) {
children = token.tokens;
} else if (token.items?.length) {
children = flatListItems(token.items);
}
if (children?.length) {
if (injectTailToTokens(children, tailChar)) return true;
}
// 叶子文本节点且内容非空
if (token.type === 'text' && (token.text || token.raw)?.trim()) {
token.isTail = true;
token.tailContent = tailChar;
return true;
}
}
return false;
}

export interface ChatMarkdownProps extends TdChatMarkdownProps {}

@wxComponent()
Expand All @@ -28,6 +77,10 @@ export default class ChatMarkdown extends SuperComponent {
content: function (markdown: string) {
this.parseMarkdown(markdown);
},
// streaming 变化时重新解析(如 hasNextChunk 从 true 变 false,光标消失)
streaming: function () {
this.parseMarkdown(this.data.content);
},
};

methods = {
Expand All @@ -37,6 +90,13 @@ export default class ChatMarkdown extends SuperComponent {
const lexer = new Lexer(this.data.options);
const tokens = lexer.lex(markdown);

// 尾部光标注入
const { streaming } = this.data;
const tailChar = resolveTailContent(streaming?.tail);
if (streaming?.hasNextChunk && tailChar) {
injectTailToTokens(tokens, tailChar);
}

this.setData({ nodes: tokens });
} catch (error) {
console.error('Markdown parsing error:', error);
Expand Down
5 changes: 5 additions & 0 deletions packages/pro-components/chat/chat-markdown/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const props: TdChatMarkdownProps = {
type: Object,
value: { gfm: true, pedantic: false, breaks: true },
},
/** 流式输出配置 */
streaming: {
type: Object,
value: null,
},
};

export default props;
19 changes: 19 additions & 0 deletions packages/pro-components/chat/chat-markdown/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export interface TdChatMarkdownProps {
type: ObjectConstructor;
value?: TdChatContentMDOptions;
};
/**
* 流式输出配置,控制尾部光标的显示与隐藏
*/
streaming?: {
type: ObjectConstructor;
value?: TdChatMarkdownStreamingOption;
};
}

export interface TdChatContentMDOptions {
Expand All @@ -30,3 +37,15 @@ export interface TdChatContentMDOptions {
smartLists?: boolean;
breaks?: boolean;
}

export interface TdChatMarkdownTailOption {
/** 自定义光标字符,默认 '▋' */
content?: string;
}

export interface TdChatMarkdownStreamingOption {
/** 是否还有后续内容块,false 时光标消失 */
hasNextChunk: boolean;
/** 尾部光标配置,true 使用默认光标 ▋,false/不传则不显示 */
tail?: boolean | TdChatMarkdownTailOption;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<t-chat-markdown
:content="textInfo"
:options="markdownProps && markdownProps.options"
:streaming="markdownProps && markdownProps.streaming"
@click="onClick"
/>
</view>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,28 @@
:avatar="item.avatar || ''"
:name="item.name || ''"
:datetime="item.datetime || ''"
:content="item.message.content"
:role="item.message.role"
:chat-content-props="chatContentProps"
:placement="item.message.role === 'user' ? 'right' : 'left'"
@message-longpress="showPopover"
>
<template #content>
<block
v-for="(contentItem, contentIndex) in item.message.content"
:key="contentIndex"
>
<t-chat-content
v-if="contentItem.type === 'text' || contentItem.type === 'markdown'"
:content="contentItem"
:role="item.message.role"
:markdown-props="{
...chatContentProps,
streaming: loading && chatIndex === 0 && item.message.role === 'assistant'
? { hasNextChunk: true, tail: true }
: null,
}"
/>
</block>
</template>
<template #actionbar>
<t-chat-actionbar
v-if="
Expand Down Expand Up @@ -69,6 +85,7 @@

<script>
import TChatMessage from '@tdesign/uniapp-chat/chat-message/chat-message.vue';
import TChatContent from '@tdesign/uniapp-chat/chat-content/chat-content.vue';
import TChatList from '@tdesign/uniapp-chat/chat-list/chat-list.vue';
import TChatSender from '@tdesign/uniapp-chat/chat-sender/chat-sender.vue';
import TChatActionbar from '@tdesign/uniapp-chat/chat-actionbar/chat-actionbar.vue';
Expand Down Expand Up @@ -97,6 +114,7 @@ const fetchStream = async (str, options) => {
export default {
components: {
TChatMessage,
TChatContent,
TChatList,
TChatSender,
TChatActionbar,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@
</block>
<block v-else>
{{ '' + item.raw + '' }}
<text
v-if="item.isTail"
:class="classPrefix + '-tail'"
>
{{ item.tailContent }}
</text>
</block>
</view>
</block>
Expand Down
5 changes: 5 additions & 0 deletions packages/uniapp-pro-components/chat/chat-markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ import TChatMarkdown from '@tdesign/uniapp-chat/chat-markdown/chat-markdown.vue'

{{ refer }}

### 05 流式输出光标

{{ tail }}

## API

### ChatMarkdown Props
Expand All @@ -51,6 +55,7 @@ import TChatMarkdown from '@tdesign/uniapp-chat/chat-markdown/chat-markdown.vue'
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
content | String | - | 必需。markdown 内容文本 | Y
streaming | Object | - | 流式输出配置,控制光标显示。TS 类型:`TdChatStreamingConfig` `interface TdChatStreamingConfig { hasNextChunk?: boolean; tail?: boolean \| { content?: string } }`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-pro-components/chat/chat-markdown/type.ts) | N
options | Object | { gfm: true, pedantic: false, breaks: true } | Markdown 解析器基础配置。TS 类型:`TdChatContentMDOptions ` `interface TdChatContentMDOptions {gfm?: boolean; pedantic?: boolean; smartLists?: boolean; breaks?: boolean}`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-pro-components/chat/chat-markdown/type.ts) | N

### ChatMarkdown Events
Expand Down
Loading
Loading