@amaster.ai/components-templates 1.5.0 → 1.8.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 (40) hide show
  1. package/README.md +12 -8
  2. package/components/ai-assistant/amaster.config.json +3 -0
  3. package/components/ai-assistant/package.json +3 -3
  4. package/components/ai-assistant/template/components/chat-assistant-message.tsx +1 -1
  5. package/components/ai-assistant/template/components/chat-banner.tsx +1 -1
  6. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +6 -6
  7. package/components/ai-assistant/template/components/chat-floating-button.tsx +4 -5
  8. package/components/ai-assistant/template/components/chat-floating-card.tsx +3 -3
  9. package/components/ai-assistant/template/components/chat-header.tsx +5 -5
  10. package/components/ai-assistant/template/components/chat-input.tsx +36 -24
  11. package/components/ai-assistant/template/components/chat-messages.tsx +11 -3
  12. package/components/ai-assistant/template/components/chat-recommends.tsx +12 -14
  13. package/components/ai-assistant/template/components/chat-user-message.tsx +1 -1
  14. package/components/ai-assistant/template/components/ui-renderer.tsx +1 -1
  15. package/components/ai-assistant/template/components/voice-input.tsx +10 -2
  16. package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
  17. package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
  18. package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
  19. package/components/ai-assistant-taro/amaster.config.json +3 -0
  20. package/components/ai-assistant-taro/package.json +94 -0
  21. package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +176 -0
  22. package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
  23. package/components/ai-assistant-taro/template/components/ChatInput.tsx +233 -0
  24. package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
  25. package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
  26. package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
  27. package/components/ai-assistant-taro/template/components/markdown.tsx +362 -0
  28. package/components/ai-assistant-taro/template/hooks/useConversation.ts +905 -0
  29. package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
  30. package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
  31. package/components/ai-assistant-taro/template/i18n.ts +157 -0
  32. package/components/ai-assistant-taro/template/index.config.ts +10 -0
  33. package/components/ai-assistant-taro/template/index.tsx +83 -0
  34. package/components/ai-assistant-taro/template/types.ts +74 -0
  35. package/package.json +5 -2
  36. package/packages/cli/dist/index.js +14 -3
  37. package/packages/cli/dist/index.js.map +1 -1
  38. package/packages/cli/package.json +1 -1
  39. package/components/ai-assistant/example.md +0 -34
  40. package/components/ai-assistant/others.md +0 -16
@@ -0,0 +1,169 @@
1
+ import { Text, View } from "@tarojs/components";
2
+ import Taro from "@tarojs/taro";
3
+ import type React from "react";
4
+ import { useCallback, useRef, useState } from "react";
5
+ import { useVoiceInput, type VoiceInputStatus } from "../hooks/useVoiceInput";
6
+ import { useAiAssistantI18n } from "../i18n";
7
+
8
+ interface VoiceInputProps {
9
+ onResult: (text: string) => void;
10
+ onError?: (error: string) => void;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ const getStatusIcon = (status: VoiceInputStatus): string => {
15
+ switch (status) {
16
+ case "recording":
17
+ return "i-lucide-mic";
18
+ case "recognizing":
19
+ return "i-lucide-brain";
20
+ default:
21
+ return "i-lucide-mic";
22
+ }
23
+ };
24
+
25
+ const getStatusColor = (status: VoiceInputStatus): string => {
26
+ switch (status) {
27
+ case "recording":
28
+ return "bg-red-500";
29
+ case "recognizing":
30
+ return "bg-blue-500";
31
+ default:
32
+ return "bg-gray-500";
33
+ }
34
+ };
35
+
36
+ export const VoiceInput: React.FC<VoiceInputProps> = ({
37
+ onResult,
38
+ disabled = false,
39
+ }) => {
40
+ const [showOverlay, setShowOverlay] = useState(false);
41
+ const shouldStopRef = useRef(false);
42
+ const { t } = useAiAssistantI18n();
43
+
44
+ const getStatusText = (status: VoiceInputStatus): string => {
45
+ switch (status) {
46
+ case "recording":
47
+ return t.VoiceInputStatus.recording;
48
+ case "recognizing":
49
+ return t.VoiceInputStatus.recognizing;
50
+ default:
51
+ return "";
52
+ }
53
+ };
54
+
55
+ const handleStatusChange = useCallback((newStatus: VoiceInputStatus) => {
56
+ if (newStatus === "idle") {
57
+ setShowOverlay(false);
58
+ // setTimeout(() => {
59
+ // }, 500);
60
+ }
61
+ }, []);
62
+
63
+ const handleResult = useCallback(
64
+ (text: string) => {
65
+ if (text) {
66
+ onResult(text);
67
+ }
68
+ },
69
+ [onResult],
70
+ );
71
+
72
+ const { status, startRecording, stopRecording } = useVoiceInput({
73
+ onResult: handleResult,
74
+ onError: (error) => {
75
+ let errorType = error;
76
+ if (error.includes("deny")) {
77
+ errorType = "MIC_PERMISSION_DENIED";
78
+ }
79
+ Taro.showToast({
80
+ title: t.voiceInputError[errorType] || error,
81
+ icon: "none",
82
+ });
83
+ },
84
+ onStatusChange: handleStatusChange,
85
+ });
86
+
87
+ const handleTouchStart = () => {
88
+ if (disabled) return;
89
+ shouldStopRef.current = false;
90
+ setShowOverlay(true);
91
+ startRecording();
92
+ };
93
+
94
+ const handleTouchEnd = () => {
95
+ shouldStopRef.current = true;
96
+ stopRecording();
97
+ };
98
+
99
+ return (
100
+ <>
101
+ {showOverlay && (
102
+ <View
103
+ className="fixed top-0 left-0 right-0 bottom-0 w-screen h-screen z-[9999] flex items-center justify-center"
104
+ style={{ backgroundColor: "rgba(0, 0, 0, 0.6)" }}
105
+ catchMove
106
+ >
107
+ <View className="flex flex-col items-center gap-6 px-20 py-10 bg-black/80 rounded-3xl shadow-2xl relative">
108
+ <View
109
+ className={`size-20 rounded-full ${getStatusColor(status)} flex items-center justify-center shadow-lg ${status === "recording" ? "animate-pulse" : ""}`}
110
+ >
111
+ <View
112
+ className={`text-white text-5xl ${getStatusIcon(status)}`}
113
+ />
114
+ </View>
115
+
116
+ <Text className="text-white text-center text-xl font-medium whitespace-pre-line leading-relaxed">
117
+ {getStatusText(status)}
118
+ </Text>
119
+
120
+ {(status === "recording" || status === "idle") && (
121
+ <View className="flex items-center gap-2 mt-2">
122
+ <View className="w-2 h-2 bg-red-400 rounded-full animate-pulse" />
123
+ <View
124
+ className="w-2 h-2 bg-red-400 rounded-full animate-pulse"
125
+ style={{ animationDelay: "150ms" }}
126
+ />
127
+ <View
128
+ className="w-2 h-2 bg-red-400 rounded-full animate-pulse"
129
+ style={{ animationDelay: "300ms" }}
130
+ />
131
+ </View>
132
+ )}
133
+
134
+ {/* 点击结束 */}
135
+ <View
136
+ className="w-6 h-6 i-lucide-x text-white rounded-full bg-gray-500 absolute top-2 right-2"
137
+ onClick={() => {
138
+ stopRecording();
139
+ }}
140
+ />
141
+ </View>
142
+ </View>
143
+ )}
144
+
145
+ <View
146
+ onTouchStart={handleTouchStart}
147
+ onTouchEnd={handleTouchEnd}
148
+ onTouchCancel={handleTouchEnd}
149
+ onLongPress={handleTouchStart}
150
+ className={`flex items-center justify-center gap-1.5 px-4 py-2 bg-danger rounded-xl active:opacity-80 transition-all duration-200 shadow-sm ${
151
+ disabled
152
+ ? "bg-gray-200 opacity-50"
153
+ : status !== "idle"
154
+ ? "bg-red-500 shadow-md"
155
+ : "bg-gradient-to-r from-primary to-primary/80 active:opacity-80 shadow-md"
156
+ }`}
157
+ hoverClass={disabled ? "" : "opacity-90"}
158
+ >
159
+ <View
160
+ className={`i-lucide-mic text-lg ${
161
+ disabled ? "text-gray-400" : "text-white"
162
+ }`}
163
+ />
164
+ </View>
165
+ </>
166
+ );
167
+ };
168
+
169
+ export default VoiceInput;
@@ -0,0 +1,362 @@
1
+ // components/MarkdownLite.tsx
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";
7
+
8
+ const md = new MarkdownIt({
9
+ html: true,
10
+ breaks: true,
11
+ linkify: true,
12
+ });
13
+
14
+ function buildTokenPairs(tokens: any[]): Map<number, number> {
15
+ const pairs = new Map<number, number>();
16
+ const stack: number[] = [];
17
+
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
+ }
27
+ }
28
+ }
29
+
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
+ }
140
+
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
+ }
334
+ }
335
+
336
+ return { elements, nextIndex: i };
337
+ }
338
+
339
+ interface MarkdownLiteProps {
340
+ content: string;
341
+ className?: string;
342
+ }
343
+
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]);
358
+
359
+ return <View className={`text-lg ${className || ""}`}>{elements}</View>;
360
+ });
361
+
362
+ export default MarkdownLite;