@amaster.ai/components-templates 1.6.0 → 1.10.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/components/ai-assistant/package.json +10 -12
- package/components/ai-assistant/template/ai-assistant.tsx +48 -7
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +78 -7
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +35 -11
- package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -1
- package/components/ai-assistant/template/components/chat-floating-card.tsx +49 -3
- package/components/ai-assistant/template/components/chat-header.tsx +1 -1
- package/components/ai-assistant/template/components/chat-input.tsx +57 -22
- package/components/ai-assistant/template/components/chat-messages.tsx +118 -25
- package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
- package/components/ai-assistant/template/components/voice-input.tsx +11 -2
- package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
- package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
- package/components/ai-assistant/template/hooks/useDisplayMode.tsx +52 -5
- package/components/ai-assistant/template/hooks/useDraggable.ts +11 -3
- package/components/ai-assistant/template/hooks/usePosition.ts +19 -31
- package/components/ai-assistant/template/i18n.ts +8 -0
- package/components/ai-assistant/template/types.ts +2 -0
- package/components/ai-assistant-taro/package.json +16 -8
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +24 -2
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +50 -28
- package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
- package/components/ai-assistant-taro/template/components/markdown.tsx +343 -137
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +542 -424
- package/components/ai-assistant-taro/template/index.tsx +2 -2
- package/components/ai-assistant-taro/template/types.ts +16 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
|
@@ -1,156 +1,362 @@
|
|
|
1
1
|
// components/MarkdownLite.tsx
|
|
2
2
|
|
|
3
|
-
import {Image, Text, View} from
|
|
4
|
-
import Taro from
|
|
5
|
-
import MarkdownIt from
|
|
6
|
-
import {memo, useMemo} from
|
|
3
|
+
import { Image, Text, View } from "@tarojs/components";
|
|
4
|
+
import Taro from "@tarojs/taro";
|
|
5
|
+
import MarkdownIt from "markdown-it";
|
|
6
|
+
import { memo, useMemo } from "react";
|
|
7
7
|
|
|
8
8
|
const md = new MarkdownIt({
|
|
9
9
|
html: true,
|
|
10
10
|
breaks: true,
|
|
11
|
-
linkify: true
|
|
12
|
-
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
function renderToken(token: any, key: string): JSX.Element | null {
|
|
16
|
-
const children = token.children
|
|
17
|
-
? token.children.map((t: any, i: number) => renderToken(t, `${key}-child-${i}`))
|
|
18
|
-
: null
|
|
19
|
-
|
|
20
|
-
switch (token.type) {
|
|
21
|
-
// 标题
|
|
22
|
-
case 'heading_open': {
|
|
23
|
-
const level = token.markup.length
|
|
24
|
-
const hClasses =
|
|
25
|
-
{
|
|
26
|
-
1: 'text-4xl font-bold mt-4 mb-4 border-b border-gray-200 dark:border-gray-700',
|
|
27
|
-
2: 'text-3xl font-bold mt-3 mb-3 border-l-4 border-blue-500',
|
|
28
|
-
3: 'text-2xl font-bold mt-2 mb-2',
|
|
29
|
-
4: 'text-xl font-bold mt-1 mb-1',
|
|
30
|
-
5: 'text-lg font-bold',
|
|
31
|
-
6: 'text-base font-bold'
|
|
32
|
-
}[level] || 'text-lg font-bold mt-2 mb-2'
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<View key={key} className={hClasses}>
|
|
36
|
-
{children}
|
|
37
|
-
</View>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
11
|
+
linkify: true,
|
|
12
|
+
});
|
|
40
13
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<View key={key} className="mb-1 leading-relaxed text-base">
|
|
45
|
-
{children}
|
|
46
|
-
</View>
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
// 文本节点
|
|
50
|
-
case 'text':
|
|
51
|
-
case 'softbreak':
|
|
52
|
-
case 'hardbreak':
|
|
53
|
-
return <Text key={key}>{token.content}</Text>
|
|
54
|
-
|
|
55
|
-
// 粗体
|
|
56
|
-
case 'strong_open':
|
|
57
|
-
return (
|
|
58
|
-
<Text key={key} className="font-bold text-gray-900 dark:text-gray-100">
|
|
59
|
-
{children}
|
|
60
|
-
</Text>
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
// 斜体
|
|
64
|
-
case 'em_open':
|
|
65
|
-
return (
|
|
66
|
-
<Text key={key} className="italic text-gray-700 dark:text-gray-300">
|
|
67
|
-
{children}
|
|
68
|
-
</Text>
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
// 行内代码
|
|
72
|
-
case 'code_inline':
|
|
73
|
-
return (
|
|
74
|
-
<Text
|
|
75
|
-
key={key}
|
|
76
|
-
className="font-mono text-red-600 dark:text-red-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
|
|
77
|
-
{token.content}
|
|
78
|
-
</Text>
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
// 代码块
|
|
82
|
-
case 'fence': {
|
|
83
|
-
const lang = token.info ? token.info.trim() : ''
|
|
84
|
-
return (
|
|
85
|
-
<View
|
|
86
|
-
key={key}
|
|
87
|
-
className="my-2 bg-gray-900 text-gray-200 p-6 rounded-xl overflow-x-auto shadow-lg font-mono text-sm leading-6">
|
|
88
|
-
<Text className="whitespace-pre">{token.content.trim()}</Text>
|
|
89
|
-
{lang && (
|
|
90
|
-
<View className="absolute top-2 right-3 text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded">{lang}</View>
|
|
91
|
-
)}
|
|
92
|
-
</View>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
14
|
+
function buildTokenPairs(tokens: any[]): Map<number, number> {
|
|
15
|
+
const pairs = new Map<number, number>();
|
|
16
|
+
const stack: number[] = [];
|
|
95
17
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
className="rounded-xl shadow-md my-2 max-w-full inline-block"
|
|
106
|
-
onClick={() => src && Taro.previewImage({urls: [src]})}
|
|
107
|
-
/>
|
|
108
|
-
)
|
|
18
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
19
|
+
const token = tokens[i];
|
|
20
|
+
if (token.nesting === 1) {
|
|
21
|
+
stack.push(i);
|
|
22
|
+
} else if (token.nesting === -1) {
|
|
23
|
+
const openIndex = stack.pop();
|
|
24
|
+
if (openIndex !== undefined) {
|
|
25
|
+
pairs.set(openIndex, i);
|
|
26
|
+
}
|
|
109
27
|
}
|
|
28
|
+
}
|
|
110
29
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
30
|
+
return pairs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RenderResult {
|
|
34
|
+
elements: JSX.Element[];
|
|
35
|
+
nextIndex: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderTokens(
|
|
39
|
+
tokens: any[],
|
|
40
|
+
pairs: Map<number, number>,
|
|
41
|
+
startIndex: number,
|
|
42
|
+
endIndex: number,
|
|
43
|
+
keyPrefix: string,
|
|
44
|
+
listContext?: { isOrdered: boolean; start: number },
|
|
45
|
+
): RenderResult {
|
|
46
|
+
const elements: JSX.Element[] = [];
|
|
47
|
+
let i = startIndex;
|
|
48
|
+
|
|
49
|
+
while (i < endIndex) {
|
|
50
|
+
const token = tokens[i];
|
|
51
|
+
const key = `${keyPrefix}-${i}`;
|
|
52
|
+
|
|
53
|
+
switch (token.type) {
|
|
54
|
+
case "heading_open": {
|
|
55
|
+
const level = token.markup.length;
|
|
56
|
+
const hClasses =
|
|
57
|
+
{
|
|
58
|
+
1: "text-4xl font-bold mt-4 mb-4 border-b border-gray-200 dark:border-gray-700",
|
|
59
|
+
2: "text-3xl font-bold mt-3 mb-3 border-l-4 border-blue-500",
|
|
60
|
+
3: "text-2xl font-bold mt-2 mb-2",
|
|
61
|
+
4: "text-xl font-bold mt-1 mb-1",
|
|
62
|
+
5: "text-lg font-bold",
|
|
63
|
+
6: "text-base font-bold",
|
|
64
|
+
}[level] || "text-lg font-bold mt-2 mb-2";
|
|
65
|
+
|
|
66
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
67
|
+
const { elements: children } = renderTokens(
|
|
68
|
+
tokens,
|
|
69
|
+
pairs,
|
|
70
|
+
i + 1,
|
|
71
|
+
closeIndex,
|
|
72
|
+
key,
|
|
73
|
+
);
|
|
74
|
+
elements.push(
|
|
75
|
+
<View key={key} className={hClasses}>
|
|
76
|
+
{children}
|
|
77
|
+
</View>,
|
|
78
|
+
);
|
|
79
|
+
i = closeIndex + 1;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "paragraph_open": {
|
|
84
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
85
|
+
const { elements: children } = renderTokens(
|
|
86
|
+
tokens,
|
|
87
|
+
pairs,
|
|
88
|
+
i + 1,
|
|
89
|
+
closeIndex,
|
|
90
|
+
key,
|
|
91
|
+
listContext,
|
|
92
|
+
);
|
|
93
|
+
elements.push(
|
|
94
|
+
<View key={key} className="mb-1 leading-relaxed text-base">
|
|
95
|
+
{children}
|
|
96
|
+
</View>,
|
|
97
|
+
);
|
|
98
|
+
i = closeIndex + 1;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "bullet_list_open": {
|
|
103
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
104
|
+
const { elements: children } = renderTokens(
|
|
105
|
+
tokens,
|
|
106
|
+
pairs,
|
|
107
|
+
i + 1,
|
|
108
|
+
closeIndex,
|
|
109
|
+
key,
|
|
110
|
+
{ isOrdered: false, start: 1 },
|
|
111
|
+
);
|
|
112
|
+
elements.push(
|
|
113
|
+
<View key={key} className="my-2 pl-2">
|
|
114
|
+
{children}
|
|
115
|
+
</View>,
|
|
116
|
+
);
|
|
117
|
+
i = closeIndex + 1;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "ordered_list_open": {
|
|
122
|
+
const start = token.attrGet?.("start") || 1;
|
|
123
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
124
|
+
const { elements: children } = renderTokens(
|
|
125
|
+
tokens,
|
|
126
|
+
pairs,
|
|
127
|
+
i + 1,
|
|
128
|
+
closeIndex,
|
|
129
|
+
key,
|
|
130
|
+
{ isOrdered: true, start },
|
|
131
|
+
);
|
|
132
|
+
elements.push(
|
|
133
|
+
<View key={key} className="my-2 pl-2">
|
|
134
|
+
{children}
|
|
135
|
+
</View>,
|
|
136
|
+
);
|
|
137
|
+
i = closeIndex + 1;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
case "list_item_open": {
|
|
142
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
143
|
+
let itemIndex = 0;
|
|
144
|
+
for (let j = startIndex; j < i; j++) {
|
|
145
|
+
if (
|
|
146
|
+
tokens[j].type === "list_item_open" &&
|
|
147
|
+
tokens[j].level === token.level
|
|
148
|
+
) {
|
|
149
|
+
itemIndex++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const itemNumber = (listContext?.start || 1) + itemIndex;
|
|
153
|
+
|
|
154
|
+
const { elements: children } = renderTokens(
|
|
155
|
+
tokens,
|
|
156
|
+
pairs,
|
|
157
|
+
i + 1,
|
|
158
|
+
closeIndex,
|
|
159
|
+
key,
|
|
160
|
+
listContext,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
elements.push(
|
|
164
|
+
<View key={key} className="flex flex-row items-start mb-1">
|
|
165
|
+
<Text className="mr-2 text-gray-700 dark:text-gray-300 w-5 text-right shrink-0">
|
|
166
|
+
{listContext?.isOrdered ? `${itemNumber}.` : "•"}
|
|
167
|
+
</Text>
|
|
168
|
+
<View className="flex-1 overflow-hidden">{children}</View>
|
|
169
|
+
</View>,
|
|
170
|
+
);
|
|
171
|
+
i = closeIndex + 1;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "strong_open": {
|
|
176
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
177
|
+
const { elements: children } = renderTokens(
|
|
178
|
+
tokens,
|
|
179
|
+
pairs,
|
|
180
|
+
i + 1,
|
|
181
|
+
closeIndex,
|
|
182
|
+
key,
|
|
183
|
+
listContext,
|
|
184
|
+
);
|
|
185
|
+
elements.push(
|
|
186
|
+
<Text
|
|
187
|
+
key={key}
|
|
188
|
+
className="font-bold text-gray-900 dark:text-gray-100"
|
|
189
|
+
>
|
|
190
|
+
{children}
|
|
191
|
+
</Text>,
|
|
192
|
+
);
|
|
193
|
+
i = closeIndex + 1;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case "em_open": {
|
|
198
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
199
|
+
const { elements: children } = renderTokens(
|
|
200
|
+
tokens,
|
|
201
|
+
pairs,
|
|
202
|
+
i + 1,
|
|
203
|
+
closeIndex,
|
|
204
|
+
key,
|
|
205
|
+
listContext,
|
|
206
|
+
);
|
|
207
|
+
elements.push(
|
|
208
|
+
<Text key={key} className="italic text-gray-700 dark:text-gray-300">
|
|
209
|
+
{children}
|
|
210
|
+
</Text>,
|
|
211
|
+
);
|
|
212
|
+
i = closeIndex + 1;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case "link_open": {
|
|
217
|
+
const href =
|
|
218
|
+
token.attrs?.find((a: string[]) => a[0] === "href")?.[1] || "";
|
|
219
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
220
|
+
const { elements: children } = renderTokens(
|
|
221
|
+
tokens,
|
|
222
|
+
pairs,
|
|
223
|
+
i + 1,
|
|
224
|
+
closeIndex,
|
|
225
|
+
key,
|
|
226
|
+
listContext,
|
|
227
|
+
);
|
|
228
|
+
elements.push(
|
|
229
|
+
<Text
|
|
230
|
+
key={key}
|
|
231
|
+
className="text-blue-600 dark:text-blue-400 no-underline hover:underline cursor-pointer"
|
|
232
|
+
onClick={() => {
|
|
233
|
+
if (href) {
|
|
234
|
+
Taro.setClipboardData({ data: href }).then(() =>
|
|
235
|
+
Taro.showToast({ title: "链接已复制", icon: "none" }),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
{children}
|
|
241
|
+
</Text>,
|
|
242
|
+
);
|
|
243
|
+
i = closeIndex + 1;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "code_inline":
|
|
248
|
+
elements.push(
|
|
249
|
+
<Text
|
|
250
|
+
key={key}
|
|
251
|
+
className="font-mono text-red-600 dark:text-red-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded"
|
|
252
|
+
>
|
|
253
|
+
{token.content}
|
|
254
|
+
</Text>,
|
|
255
|
+
);
|
|
256
|
+
i++;
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case "fence": {
|
|
260
|
+
const lang = token.info ? token.info.trim() : "";
|
|
261
|
+
elements.push(
|
|
262
|
+
<View
|
|
263
|
+
key={key}
|
|
264
|
+
className="my-2 bg-gray-900 text-gray-200 p-4 rounded-xl overflow-x-auto shadow-lg font-mono text-sm leading-6 relative"
|
|
265
|
+
>
|
|
266
|
+
<Text className="whitespace-pre">{token.content.trim()}</Text>
|
|
267
|
+
{lang && (
|
|
268
|
+
<View className="absolute top-2 right-3 text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded">
|
|
269
|
+
{lang}
|
|
270
|
+
</View>
|
|
271
|
+
)}
|
|
272
|
+
</View>,
|
|
273
|
+
);
|
|
274
|
+
i++;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case "image": {
|
|
279
|
+
const src =
|
|
280
|
+
token.attrs?.find((a: string[]) => a[0] === "src")?.[1] || "";
|
|
281
|
+
elements.push(
|
|
282
|
+
<Image
|
|
283
|
+
key={key}
|
|
284
|
+
src={src}
|
|
285
|
+
mode="widthFix"
|
|
286
|
+
lazyLoad
|
|
287
|
+
className="rounded-xl shadow-md my-2 max-w-full inline-block"
|
|
288
|
+
onClick={() => src && Taro.previewImage({ urls: [src] })}
|
|
289
|
+
/>,
|
|
290
|
+
);
|
|
291
|
+
i++;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case "text":
|
|
296
|
+
elements.push(<Text key={key}>{token.content}</Text>);
|
|
297
|
+
i++;
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
case "softbreak":
|
|
301
|
+
case "hardbreak":
|
|
302
|
+
elements.push(<Text key={key}> </Text>);
|
|
303
|
+
i++;
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case "heading_close":
|
|
307
|
+
case "paragraph_close":
|
|
308
|
+
case "strong_close":
|
|
309
|
+
case "em_close":
|
|
310
|
+
case "link_close":
|
|
311
|
+
case "bullet_list_close":
|
|
312
|
+
case "ordered_list_close":
|
|
313
|
+
case "list_item_close":
|
|
314
|
+
// 这些由对应的 open 处理,直接跳过
|
|
315
|
+
i++;
|
|
316
|
+
break;
|
|
317
|
+
|
|
318
|
+
default:
|
|
319
|
+
if (token.children && token.children.length > 0) {
|
|
320
|
+
const childPairs = buildTokenPairs(token.children);
|
|
321
|
+
const { elements: childElements } = renderTokens(
|
|
322
|
+
token.children,
|
|
323
|
+
childPairs,
|
|
324
|
+
0,
|
|
325
|
+
token.children.length,
|
|
326
|
+
key,
|
|
327
|
+
listContext,
|
|
328
|
+
);
|
|
329
|
+
elements.push(<Text key={key}>{childElements}</Text>);
|
|
330
|
+
}
|
|
331
|
+
i++;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
139
334
|
}
|
|
335
|
+
|
|
336
|
+
return { elements, nextIndex: i };
|
|
140
337
|
}
|
|
141
338
|
|
|
142
339
|
interface MarkdownLiteProps {
|
|
143
|
-
content: string
|
|
144
|
-
className?: string
|
|
340
|
+
content: string;
|
|
341
|
+
className?: string;
|
|
145
342
|
}
|
|
146
343
|
|
|
147
|
-
const MarkdownLite = memo(({content, className}: MarkdownLiteProps) => {
|
|
148
|
-
const
|
|
149
|
-
if (!content) return []
|
|
150
|
-
|
|
151
|
-
|
|
344
|
+
const MarkdownLite = memo(({ content, className }: MarkdownLiteProps) => {
|
|
345
|
+
const elements = useMemo(() => {
|
|
346
|
+
if (!content) return [];
|
|
347
|
+
const tokens = md.parse(content, {});
|
|
348
|
+
const pairs = buildTokenPairs(tokens);
|
|
349
|
+
const { elements: rendered } = renderTokens(
|
|
350
|
+
tokens,
|
|
351
|
+
pairs,
|
|
352
|
+
0,
|
|
353
|
+
tokens.length,
|
|
354
|
+
"root",
|
|
355
|
+
);
|
|
356
|
+
return rendered;
|
|
357
|
+
}, [content]);
|
|
152
358
|
|
|
153
|
-
return <View className={`text-lg ${className}`}>{
|
|
154
|
-
})
|
|
359
|
+
return <View className={`text-lg ${className || ""}`}>{elements}</View>;
|
|
360
|
+
});
|
|
155
361
|
|
|
156
|
-
export default MarkdownLite
|
|
362
|
+
export default MarkdownLite;
|