@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.
Files changed (28) hide show
  1. package/components/ai-assistant/package.json +10 -12
  2. package/components/ai-assistant/template/ai-assistant.tsx +48 -7
  3. package/components/ai-assistant/template/components/chat-assistant-message.tsx +78 -7
  4. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +35 -11
  5. package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -1
  6. package/components/ai-assistant/template/components/chat-floating-card.tsx +49 -3
  7. package/components/ai-assistant/template/components/chat-header.tsx +1 -1
  8. package/components/ai-assistant/template/components/chat-input.tsx +57 -22
  9. package/components/ai-assistant/template/components/chat-messages.tsx +118 -25
  10. package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
  11. package/components/ai-assistant/template/components/voice-input.tsx +11 -2
  12. package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
  13. package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
  14. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +52 -5
  15. package/components/ai-assistant/template/hooks/useDraggable.ts +11 -3
  16. package/components/ai-assistant/template/hooks/usePosition.ts +19 -31
  17. package/components/ai-assistant/template/i18n.ts +8 -0
  18. package/components/ai-assistant/template/types.ts +2 -0
  19. package/components/ai-assistant-taro/package.json +16 -8
  20. package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +24 -2
  21. package/components/ai-assistant-taro/template/components/ChatInput.tsx +50 -28
  22. package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
  23. package/components/ai-assistant-taro/template/components/markdown.tsx +343 -137
  24. package/components/ai-assistant-taro/template/hooks/useConversation.ts +542 -424
  25. package/components/ai-assistant-taro/template/index.tsx +2 -2
  26. package/components/ai-assistant-taro/template/types.ts +16 -0
  27. package/package.json +1 -1
  28. package/packages/cli/package.json +1 -1
@@ -1,156 +1,362 @@
1
1
  // components/MarkdownLite.tsx
2
2
 
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'
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
- // 如果需要代码高亮,可以在这里加 highlight 函数
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
- case 'paragraph_open':
43
- return (
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
- case 'image': {
98
- const src = token.attrs?.find((a: string[]) => a[0] === 'src')?.[1] || ''
99
- return (
100
- <Image
101
- key={key}
102
- src={src}
103
- mode="widthFix"
104
- lazyLoad
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
- case 'link_open': {
113
- const href = token.attrs?.find((a: string[]) => a[0] === 'href')?.[1] || ''
114
- return (
115
- <Text
116
- key={key}
117
- className="text-blue-600 dark:text-blue-400 no-underline hover:underline cursor-pointer"
118
- onClick={() => {
119
- if (href) {
120
- Taro.setClipboardData({data: href}).then(() => Taro.showToast({title: '链接已复制', icon: 'none'}))
121
- }
122
- }}>
123
- {children}
124
- </Text>
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
- // 关闭标签(我们通常不渲染 closing token 的内容,因为 children 已处理)
129
- case 'heading_close':
130
- case 'paragraph_close':
131
- case 'strong_close':
132
- case 'em_close':
133
- case 'link_close':
134
- return null
135
-
136
- // 其他未处理的 token,直接渲染 children(兜底)
137
- default:
138
- return children ? <View key={key}>{children}</View> : null
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 tokens = useMemo(() => {
149
- if (!content) return []
150
- return md.parse(content, {})
151
- }, [content])
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}`}>{tokens.map((token, i) => renderToken(token, `root-${i}`))}</View>
154
- })
359
+ return <View className={`text-lg ${className || ""}`}>{elements}</View>;
360
+ });
155
361
 
156
- export default MarkdownLite
362
+ export default MarkdownLite;