@unif/react-native-chat-markdown 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/index.tsx +16 -0
- package/src/markdown-bubble/MarkdownBubble.tsx +90 -0
- package/src/markdown-bubble/index.md +61 -0
- package/src/streaming-bubble/StreamingBubble.tsx +74 -0
- package/src/streaming-bubble/index.md +47 -0
- package/src/think-block/ThinkBlock.tsx +111 -0
- package/src/think-block/index.md +57 -0
- package/src/utils/thinkTagParser.ts +41 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unif/react-native-chat-markdown",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UNIF React Native Chat Markdown — Markdown 渲染、ThinkBlock、流式气泡",
|
|
5
|
+
"main": "./src/index.tsx",
|
|
6
|
+
"types": "./src/index.tsx",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./src/index.tsx",
|
|
11
|
+
"default": "./src/index.tsx"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"!**/__tests__",
|
|
19
|
+
"!**/__mocks__",
|
|
20
|
+
"!**/.*"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "bob build",
|
|
24
|
+
"clean": "del-cli lib",
|
|
25
|
+
"typecheck": "tsc"
|
|
26
|
+
},
|
|
27
|
+
"keywords": ["react-native", "chat", "markdown", "streaming"],
|
|
28
|
+
"author": "qq382724935 <liulijun@pec.com.cn>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "*",
|
|
35
|
+
"react-native": "*",
|
|
36
|
+
"@ronradtke/react-native-markdown-display": ">=7.0.0"
|
|
37
|
+
},
|
|
38
|
+
"react-native-builder-bob": {
|
|
39
|
+
"source": "src",
|
|
40
|
+
"output": "lib",
|
|
41
|
+
"targets": [
|
|
42
|
+
["module", { "esm": true }],
|
|
43
|
+
["typescript", { "project": "tsconfig.build.json" }]
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unif/react-native-chat-markdown
|
|
3
|
+
* Markdown 渲染、ThinkBlock、流式气泡
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {default as ThinkBlock} from './think-block/ThinkBlock';
|
|
7
|
+
export type {ThinkBlockProps, ThinkBlockSemanticStyles} from './think-block/ThinkBlock';
|
|
8
|
+
|
|
9
|
+
export {default as StreamingBubble} from './streaming-bubble/StreamingBubble';
|
|
10
|
+
export type {StreamingBubbleProps} from './streaming-bubble/StreamingBubble';
|
|
11
|
+
|
|
12
|
+
export {default as MarkdownBubble} from './markdown-bubble/MarkdownBubble';
|
|
13
|
+
export type {MarkdownBubbleProps} from './markdown-bubble/MarkdownBubble';
|
|
14
|
+
|
|
15
|
+
export {parseThinkTags, hasUnclosedThink} from './utils/thinkTagParser';
|
|
16
|
+
export type {ParsedContent} from './utils/thinkTagParser';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownBubble — Markdown 渲染气泡
|
|
3
|
+
* 完成的消息用 Markdown 渲染 + think 折叠
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, {useMemo} from 'react';
|
|
7
|
+
import {View, StyleSheet, type ViewStyle} from 'react-native';
|
|
8
|
+
import Markdown from '@ronradtke/react-native-markdown-display';
|
|
9
|
+
import {parseThinkTags} from '../utils/thinkTagParser';
|
|
10
|
+
import ThinkBlock from '../think-block/ThinkBlock';
|
|
11
|
+
|
|
12
|
+
export interface MarkdownBubbleProps {
|
|
13
|
+
text: string;
|
|
14
|
+
markdownStyles?: Record<string, unknown>;
|
|
15
|
+
style?: ViewStyle;
|
|
16
|
+
testID?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultMarkdownStyles = {
|
|
20
|
+
body: {
|
|
21
|
+
fontSize: 15,
|
|
22
|
+
color: '#1F2937',
|
|
23
|
+
lineHeight: 22,
|
|
24
|
+
},
|
|
25
|
+
paragraph: {
|
|
26
|
+
marginTop: 0,
|
|
27
|
+
marginBottom: 6,
|
|
28
|
+
},
|
|
29
|
+
code_inline: {
|
|
30
|
+
backgroundColor: '#F5F5F5',
|
|
31
|
+
borderRadius: 3,
|
|
32
|
+
paddingHorizontal: 4,
|
|
33
|
+
fontSize: 13,
|
|
34
|
+
color: '#E8550A',
|
|
35
|
+
},
|
|
36
|
+
fence: {
|
|
37
|
+
backgroundColor: '#F5F5F5',
|
|
38
|
+
borderRadius: 6,
|
|
39
|
+
padding: 10,
|
|
40
|
+
fontSize: 13,
|
|
41
|
+
},
|
|
42
|
+
link: {
|
|
43
|
+
color: '#1677FF',
|
|
44
|
+
},
|
|
45
|
+
strong: {
|
|
46
|
+
fontWeight: '600' as const,
|
|
47
|
+
},
|
|
48
|
+
list_item: {
|
|
49
|
+
marginBottom: 4,
|
|
50
|
+
},
|
|
51
|
+
blockquote: {
|
|
52
|
+
borderLeftWidth: 3,
|
|
53
|
+
borderLeftColor: '#E5E7EB',
|
|
54
|
+
paddingLeft: 10,
|
|
55
|
+
marginLeft: 0,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const MarkdownBubble: React.FC<MarkdownBubbleProps> = ({
|
|
60
|
+
text,
|
|
61
|
+
markdownStyles,
|
|
62
|
+
style,
|
|
63
|
+
testID = 'markdown-bubble',
|
|
64
|
+
}) => {
|
|
65
|
+
const {body, thinks} = useMemo(() => parseThinkTags(text), [text]);
|
|
66
|
+
|
|
67
|
+
const mergedStyles = useMemo(
|
|
68
|
+
() => ({...defaultMarkdownStyles, ...markdownStyles}),
|
|
69
|
+
[markdownStyles]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<View style={[defaultContainerStyles.container, style]} testID={testID}>
|
|
74
|
+
{thinks.map((think, index) => (
|
|
75
|
+
<ThinkBlock key={index} content={think} />
|
|
76
|
+
))}
|
|
77
|
+
{body.length > 0 && (
|
|
78
|
+
<Markdown style={mergedStyles}>{body}</Markdown>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const defaultContainerStyles = StyleSheet.create({
|
|
85
|
+
container: {
|
|
86
|
+
paddingVertical: 4,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export default React.memo(MarkdownBubble);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: MarkdownBubble Markdown 渲染
|
|
3
|
+
nav:
|
|
4
|
+
title: 组件
|
|
5
|
+
path: /components
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MarkdownBubble Markdown 渲染
|
|
9
|
+
|
|
10
|
+
完整的 Markdown 渲染气泡,自动解析 `<think>` 标签并渲染为 ThinkBlock。
|
|
11
|
+
|
|
12
|
+
## 何时使用
|
|
13
|
+
|
|
14
|
+
- AI 回复完成后(status === 'success')渲染完整内容
|
|
15
|
+
- 内容包含 Markdown 格式(标题、代码、链接、列表等)
|
|
16
|
+
- 内容包含 `<think>` 标签需要折叠展示
|
|
17
|
+
|
|
18
|
+
## 代码示例
|
|
19
|
+
|
|
20
|
+
### 基本用法
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { MarkdownBubble } from '@unif/react-native-chat-markdown';
|
|
24
|
+
|
|
25
|
+
<MarkdownBubble text="**加粗** 和 `代码` 以及[链接](https://example.com)" />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 包含思考内容
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<MarkdownBubble
|
|
32
|
+
text="<think>分析用户意图...</think>根据您的需求,推荐以下方案..."
|
|
33
|
+
/>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 自定义 Markdown 样式
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<MarkdownBubble
|
|
40
|
+
text={messageText}
|
|
41
|
+
markdownStyles={{
|
|
42
|
+
body: { fontSize: 16, color: '#333' },
|
|
43
|
+
code_inline: { backgroundColor: '#F0F0F0' },
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### MarkdownBubbleProps
|
|
51
|
+
|
|
52
|
+
| 属性 | 说明 | 类型 | 默认值 |
|
|
53
|
+
|------|------|------|--------|
|
|
54
|
+
| text | Markdown 文本 | `string` | - |
|
|
55
|
+
| markdownStyles | 覆盖默认 Markdown 样式 | `Record<string, unknown>` | 内置样式 |
|
|
56
|
+
| style | 容器样式 | `ViewStyle` | - |
|
|
57
|
+
| testID | 测试标识 | `string` | `'markdown-bubble'` |
|
|
58
|
+
|
|
59
|
+
### 依赖
|
|
60
|
+
|
|
61
|
+
- `@ronradtke/react-native-markdown-display` — Markdown 渲染引擎
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamingBubble — 流式文本气泡
|
|
3
|
+
* 显示正在流入的文本 + 呼吸动画光标
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, {useEffect, useRef} from 'react';
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
Text,
|
|
10
|
+
Animated,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
|
|
15
|
+
export interface StreamingBubbleProps {
|
|
16
|
+
text: string;
|
|
17
|
+
cursorColor?: string;
|
|
18
|
+
cursorChar?: string;
|
|
19
|
+
style?: ViewStyle;
|
|
20
|
+
testID?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const StreamingBubble: React.FC<StreamingBubbleProps> = ({
|
|
24
|
+
text,
|
|
25
|
+
cursorColor = '#1677FF',
|
|
26
|
+
cursorChar = '▌',
|
|
27
|
+
style,
|
|
28
|
+
testID = 'streaming-bubble',
|
|
29
|
+
}) => {
|
|
30
|
+
const opacity = useRef(new Animated.Value(1)).current;
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const animation = Animated.loop(
|
|
34
|
+
Animated.sequence([
|
|
35
|
+
Animated.timing(opacity, {
|
|
36
|
+
toValue: 0,
|
|
37
|
+
duration: 400,
|
|
38
|
+
useNativeDriver: true,
|
|
39
|
+
}),
|
|
40
|
+
Animated.timing(opacity, {
|
|
41
|
+
toValue: 1,
|
|
42
|
+
duration: 400,
|
|
43
|
+
useNativeDriver: true,
|
|
44
|
+
}),
|
|
45
|
+
]),
|
|
46
|
+
);
|
|
47
|
+
animation.start();
|
|
48
|
+
return () => animation.stop();
|
|
49
|
+
}, [opacity]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<View style={[defaultStyles.container, style]} testID={testID}>
|
|
53
|
+
<Text style={defaultStyles.text}>
|
|
54
|
+
{text}
|
|
55
|
+
<Animated.Text style={[{color: cursorColor}, {opacity}]}>
|
|
56
|
+
{cursorChar}
|
|
57
|
+
</Animated.Text>
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const defaultStyles = StyleSheet.create({
|
|
64
|
+
container: {
|
|
65
|
+
paddingVertical: 4,
|
|
66
|
+
},
|
|
67
|
+
text: {
|
|
68
|
+
fontSize: 15,
|
|
69
|
+
color: '#1F2937',
|
|
70
|
+
lineHeight: 22,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export default React.memo(StreamingBubble);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: StreamingBubble 流式气泡
|
|
3
|
+
nav:
|
|
4
|
+
title: 组件
|
|
5
|
+
path: /components
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# StreamingBubble 流式气泡
|
|
9
|
+
|
|
10
|
+
展示正在流入的文本内容,附带闪烁光标动画。
|
|
11
|
+
|
|
12
|
+
## 何时使用
|
|
13
|
+
|
|
14
|
+
- AI 正在流式输出回复时(status === 'updating')
|
|
15
|
+
- 需要视觉上表示内容仍在生成中
|
|
16
|
+
|
|
17
|
+
## 代码示例
|
|
18
|
+
|
|
19
|
+
### 基本用法
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { StreamingBubble } from '@unif/react-native-chat-markdown';
|
|
23
|
+
|
|
24
|
+
<StreamingBubble text="正在生成的文本内容..." />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 自定义光标
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<StreamingBubble
|
|
31
|
+
text={streamingText}
|
|
32
|
+
cursorColor="#FF6B00"
|
|
33
|
+
cursorChar="●"
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### StreamingBubbleProps
|
|
40
|
+
|
|
41
|
+
| 属性 | 说明 | 类型 | 默认值 |
|
|
42
|
+
|------|------|------|--------|
|
|
43
|
+
| text | 当前流式文本 | `string` | - |
|
|
44
|
+
| cursorColor | 光标颜色 | `string` | `'#1677FF'` |
|
|
45
|
+
| cursorChar | 光标字符 | `string` | `'▌'` |
|
|
46
|
+
| style | 容器样式 | `ViewStyle` | - |
|
|
47
|
+
| testID | 测试标识 | `string` | `'streaming-bubble'` |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThinkBlock — 思考折叠块
|
|
3
|
+
* 卡片式,圆角 12px
|
|
4
|
+
* 标题:🧠 已深度思考(展开) ▾
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, {useState} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
type TextStyle,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
|
|
17
|
+
export interface ThinkBlockSemanticStyles {
|
|
18
|
+
root?: ViewStyle;
|
|
19
|
+
header?: ViewStyle;
|
|
20
|
+
label?: TextStyle;
|
|
21
|
+
content?: TextStyle;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ThinkBlockProps {
|
|
25
|
+
content: string;
|
|
26
|
+
defaultExpanded?: boolean;
|
|
27
|
+
label?: string;
|
|
28
|
+
style?: ViewStyle;
|
|
29
|
+
styles?: Partial<ThinkBlockSemanticStyles>;
|
|
30
|
+
testID?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ThinkBlock: React.FC<ThinkBlockProps> = ({
|
|
34
|
+
content,
|
|
35
|
+
defaultExpanded = false,
|
|
36
|
+
label = '深度思考',
|
|
37
|
+
style,
|
|
38
|
+
styles: semanticStyles,
|
|
39
|
+
testID = 'think-block',
|
|
40
|
+
}) => {
|
|
41
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={[defaultStyles.container, semanticStyles?.root, style]}>
|
|
45
|
+
<TouchableOpacity
|
|
46
|
+
style={[defaultStyles.header, semanticStyles?.header]}
|
|
47
|
+
onPress={() => setExpanded(!expanded)}
|
|
48
|
+
activeOpacity={0.7}
|
|
49
|
+
testID={`${testID}-toggle`}>
|
|
50
|
+
<View style={defaultStyles.titleRow}>
|
|
51
|
+
<Text style={defaultStyles.brainIcon}>🧠</Text>
|
|
52
|
+
<Text
|
|
53
|
+
style={[defaultStyles.label, semanticStyles?.label]}>
|
|
54
|
+
已{label}{expanded ? '(展开)' : '(收起)'}
|
|
55
|
+
</Text>
|
|
56
|
+
</View>
|
|
57
|
+
<Text style={defaultStyles.chevron}>
|
|
58
|
+
{expanded ? '▲' : '▼'}
|
|
59
|
+
</Text>
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
{expanded && (
|
|
62
|
+
<Text
|
|
63
|
+
style={[defaultStyles.content, semanticStyles?.content]}
|
|
64
|
+
testID={`${testID}-content`}>
|
|
65
|
+
{content}
|
|
66
|
+
</Text>
|
|
67
|
+
)}
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const defaultStyles = StyleSheet.create({
|
|
73
|
+
container: {
|
|
74
|
+
backgroundColor: '#F9FAFB',
|
|
75
|
+
borderRadius: 12,
|
|
76
|
+
marginBottom: 8,
|
|
77
|
+
overflow: 'hidden',
|
|
78
|
+
},
|
|
79
|
+
header: {
|
|
80
|
+
flexDirection: 'row',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
justifyContent: 'space-between',
|
|
83
|
+
paddingHorizontal: 14,
|
|
84
|
+
paddingVertical: 10,
|
|
85
|
+
},
|
|
86
|
+
titleRow: {
|
|
87
|
+
flexDirection: 'row',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
},
|
|
90
|
+
brainIcon: {
|
|
91
|
+
fontSize: 14,
|
|
92
|
+
},
|
|
93
|
+
label: {
|
|
94
|
+
marginLeft: 6,
|
|
95
|
+
fontSize: 12,
|
|
96
|
+
color: '#6B7280',
|
|
97
|
+
},
|
|
98
|
+
chevron: {
|
|
99
|
+
fontSize: 10,
|
|
100
|
+
color: '#6B7280',
|
|
101
|
+
},
|
|
102
|
+
content: {
|
|
103
|
+
fontSize: 12,
|
|
104
|
+
color: '#6B7280',
|
|
105
|
+
lineHeight: 20,
|
|
106
|
+
paddingHorizontal: 14,
|
|
107
|
+
paddingBottom: 12,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export default React.memo(ThinkBlock);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ThinkBlock 思考折叠块
|
|
3
|
+
nav:
|
|
4
|
+
title: 组件
|
|
5
|
+
path: /components
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ThinkBlock 思考折叠块
|
|
9
|
+
|
|
10
|
+
展示 AI 的思考过程,可折叠/展开。
|
|
11
|
+
|
|
12
|
+
## 何时使用
|
|
13
|
+
|
|
14
|
+
- AI 回复中包含 `<think>` 标签的思考内容
|
|
15
|
+
- 需要可折叠展示推理过程(如 DeepSeek 的 reasoning_content)
|
|
16
|
+
|
|
17
|
+
## 代码示例
|
|
18
|
+
|
|
19
|
+
### 基本用法
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { ThinkBlock } from '@unif/react-native-chat-markdown';
|
|
23
|
+
|
|
24
|
+
<ThinkBlock content="用户询问的是账户余额,需要调用查询接口..." />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 默认展开
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<ThinkBlock
|
|
31
|
+
content="分析用户意图..."
|
|
32
|
+
defaultExpanded={true}
|
|
33
|
+
label="思考过程"
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### ThinkBlockProps
|
|
40
|
+
|
|
41
|
+
| 属性 | 说明 | 类型 | 默认值 |
|
|
42
|
+
|------|------|------|--------|
|
|
43
|
+
| content | 思考内容 | `string` | - |
|
|
44
|
+
| defaultExpanded | 默认展开 | `boolean` | `false` |
|
|
45
|
+
| label | 标题文字 | `string` | `'深度思考'` |
|
|
46
|
+
| style | 容器样式 | `ViewStyle` | - |
|
|
47
|
+
| styles | 语义样式 | `Partial<ThinkBlockSemanticStyles>` | - |
|
|
48
|
+
| testID | 测试标识 | `string` | `'think-block'` |
|
|
49
|
+
|
|
50
|
+
### ThinkBlockSemanticStyles
|
|
51
|
+
|
|
52
|
+
| 属性 | 说明 | 类型 |
|
|
53
|
+
|------|------|------|
|
|
54
|
+
| root | 外层容器 | `ViewStyle` |
|
|
55
|
+
| header | 标题栏 | `ViewStyle` |
|
|
56
|
+
| label | 标题文字 | `TextStyle` |
|
|
57
|
+
| content | 内容文字 | `TextStyle` |
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <think> 标签解析器
|
|
3
|
+
* 将文本中的 <think>...</think> 提取出来,分离为普通内容和思考内容
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ParsedContent {
|
|
7
|
+
/** 去除 think 标签后的正文 */
|
|
8
|
+
body: string;
|
|
9
|
+
/** think 标签内的内容(可能多段) */
|
|
10
|
+
thinks: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const THINK_REGEX = /<think>([\s\S]*?)<\/think>/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 解析文本中的 <think> 标签
|
|
17
|
+
* 支持多个 <think> 块,返回分离后的正文和思考内容
|
|
18
|
+
*/
|
|
19
|
+
export function parseThinkTags(text: string): ParsedContent {
|
|
20
|
+
const thinks: string[] = [];
|
|
21
|
+
const body = text
|
|
22
|
+
.replace(THINK_REGEX, (_match, content: string) => {
|
|
23
|
+
const trimmed = content.trim();
|
|
24
|
+
if (trimmed) {
|
|
25
|
+
thinks.push(trimmed);
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
})
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
return {body, thinks};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 检查文本中是否包含未闭合的 <think> 标签(流式场景)
|
|
36
|
+
*/
|
|
37
|
+
export function hasUnclosedThink(text: string): boolean {
|
|
38
|
+
const opens = (text.match(/<think>/g) || []).length;
|
|
39
|
+
const closes = (text.match(/<\/think>/g) || []).length;
|
|
40
|
+
return opens > closes;
|
|
41
|
+
}
|